Sistema de logros
Construye un sistema de logros completo — cuando los jugadores alcancen hitos específicos (oro por encima de 100, 5+ victorias en combate, descubrir un área oculta...), aparecerá en pantalla una notificación dorada de logro. Usa variables booleanas para seguir qué logros están desbloqueados, y el Root Component para mostrar un panel de logros.
Lo que vas a construir
Un sistema de logros integrado directamente en el chat:
- Notificaciones emergentes doradas — en el instante en que un jugador alcance un hito, aparece un toast dorado de logro en pantalla (usando el estilo
achievement), por ejemplo "Achievement Unlocked: Big Spender" - Detección automática — el motor monitorea los cambios de variables en segundo plano y se dispara automáticamente cuando se cumplen las condiciones, no se requiere acción del jugador
- Garantía de un solo disparo — cada logro se desbloquea exactamente una vez y nunca vuelve a aparecer.
maxFireCounty las variables booleanas proporcionan una doble red de seguridad - Panel de logros — un mini panel debajo del último mensaje lista todos los logros y su estado de desbloqueo (desbloqueado = icono dorado, bloqueado = candado gris)
Cómo funciona
El bucle central es: la variable cambia → el motor detecta que la variable cruza un umbral → el comportamiento se dispara → aparece la notificación + la variable booleana se establece en true.
El jugador acumula 101 de oro durante la aventura
→ El motor detecta que gold cruzó por encima de 100
→ El comportamiento "Big Spender" se dispara
→ Se ejecutan acciones: achievement_rich set a true, notificación dorada "Achievement Unlocked: Big Spender"
→ maxFireCount: 1 asegura que este comportamiento nunca se vuelva a disparar
→ El Root Component lee achievement_rich = true, el panel muestra el icono de trofeo doradoHay una decisión de diseño importante aquí: ¿por qué usar variable-crossed en lugar de state-change?
state-changesignifica "comprobar cuando cualquier variable cambia" — muy amplio. Si usasstate-change+ condicióngold gt 100, entonces cada vez que el oro vaya de 101 a 102, 102 a 103... la condición se re-evalúa. AunquemaxFireCount: 1evita el re-disparo, el motor sigue haciendo una evaluación inútil cada vez.variable-crossedsignifica "dispararse solo en el instante en que el oro va de <= 100 a > 100" — preciso y eficiente. Combinado conmaxFireCount: 1, obtienes una doble red de seguridad.
Paso a paso
Paso 1: Crea variables
Necesitas 5 variables — 2 variables numéricas para seguir el progreso, y 3 variables booleanas para seguir si cada logro está desbloqueado.
Editor → barra lateral izquierda → pestaña Variables → haz clic en "Add Variable" para cada una
Variable 1: Oro
| Campo | Valor | Por qué |
|---|---|---|
| Nombre visible | Oro | Para tu propia referencia en la lista de variables |
| ID | gold | Los comportamientos y el Root Component usan este ID para leer/escribir |
| Tipo | Number | El oro es numérico y necesita aritmética |
| Valor por defecto | 0 | Las nuevas sesiones empiezan con 0 de oro |
| Categoría | Stats | Lo agrupa con los atributos del personaje |
| Reglas de comportamiento | Cantidad de oro actual. La IA puede modificarla mediante directivas cuando la narrativa lo requiera. | Le dice a la IA qué es esto y cómo usarlo |
Variable 2: Victorias en combate
| Campo | Valor | Por qué |
|---|---|---|
| Nombre visible | Victorias en combate | Fácil de identificar |
| ID | combat_wins | Referenciado por los comportamientos |
| Tipo | Number | Es un contador |
| Valor por defecto | 0 | Empezar desde 0 |
| Categoría | Stats | Atributo del personaje |
| Reglas de comportamiento | Número acumulado de batallas que el jugador ha ganado. La IA puede sumarle +1 mediante una directiva cuando el jugador gane un combate. | Le dice a la IA cuándo incrementar |
Variable 3: Logro — Big Spender
| Campo | Valor | Por qué |
|---|---|---|
| Nombre visible | Logro: Big Spender | Fácil de identificar |
| ID | achievement_rich | Todas las variables de logro usan el prefijo achievement_ |
| Tipo | Boolean | Solo dos estados: desbloqueado o bloqueado |
| Valor por defecto | false | Bloqueado al inicio |
| Categoría | Achievements | Agrupa todas las variables de logro bajo una categoría para una gestión sencilla |
| Reglas de comportamiento | No modifiques esta variable directamente — los logros se desbloquean automáticamente mediante reglas de comportamiento cuando se cumplen las condiciones, lo que también dispara una notificación. Modificarla manualmente omite el sistema de notificaciones. | Los logros deben dispararse a través de comportamientos para mostrar la notificación correctamente |
Variable 4: Logro — First Blood
| Campo | Valor | Por qué |
|---|---|---|
| Nombre visible | Logro: First Blood | Fácil de identificar |
| ID | achievement_warrior | Misma convención de prefijo |
| Tipo | Boolean | Igual que antes |
| Valor por defecto | false | Bloqueado al inicio |
| Categoría | Achievements | Igual que antes |
| Reglas de comportamiento | No modifiques esta variable directamente — los logros se desbloquean automáticamente mediante reglas de comportamiento cuando se cumplen las condiciones, lo que también dispara una notificación. Modificarla manualmente omite el sistema de notificaciones. | Misma razón |
Variable 5: Logro — Trailblazer
| Campo | Valor | Por qué |
|---|---|---|
| Nombre visible | Logro: Trailblazer | Fácil de identificar |
| ID | achievement_explorer | Misma convención de prefijo |
| Tipo | Boolean | Igual que antes |
| Valor por defecto | false | Bloqueado al inicio |
| Categoría | Achievements | Igual que antes |
| Reglas de comportamiento | No modifiques esta variable directamente — los logros se desbloquean automáticamente mediante reglas de comportamiento cuando se cumplen las condiciones, lo que también dispara una notificación. Modificarla manualmente omite el sistema de notificaciones. | Misma razón |
¿Por qué usar variables booleanas separadas para los logros?
Porque el Root Component necesita leer el estado de cada logro para mostrar el panel. Si solo confiaras en maxFireCount para evitar el re-disparo, el componente no tendría forma de saber "¿este logro está desbloqueado o no?" — no puede ver el conteo de disparos de un comportamiento. Las variables booleanas son el estado público que el Root Component y otros comportamientos pueden leer.
Paso 2: Crea comportamientos
Necesitas 3 comportamientos — uno por cada logro.
Editor → barra lateral izquierda → pestaña Behaviors → haz clic en "Add Behavior" para cada uno
Comportamiento 1: Big Spender (oro > 100)
Información básica:
| Campo | Valor | Por qué |
|---|---|---|
| Nombre | Logro: Big Spender | Para tu propia referencia |
| Max Fire Count | 1 | Los logros se desbloquean una vez — tras dispararse, este comportamiento nunca vuelve a ejecutarse |
Disparador (WHEN):
| Campo | Valor | Por qué |
|---|---|---|
| Trigger Type | Variable Crossed Threshold (variable-crossed) | Queremos detectar el instante en que el oro cruza 100 |
| Variable ID | gold | Monitorear la variable de oro |
| Direction | Rises Above (rises-above) | Dispararse cuando el oro va de <= 100 a > 100 |
| Threshold | 100 | El valor del hito |
Acciones (DO):
| Tipo de acción | Configuración | Propósito |
|---|---|---|
| Establecer variable | achievement_rich set a true | Marca el logro como desbloqueado, para que el Root Component lo lea |
| Mostrar notificación | Mensaje Logro desbloqueado: Manirroto, estilo achievement | Abre el toast dorado de logro |
Sobre
maxFireCount: 1. Este campo se establece en el comportamiento mismo (no en el disparador). Significa "este comportamiento puede ejecutarse como máximo 1 vez en total". Una vez que se ha disparado, sin importar cómo cambie el oro después, este comportamiento nunca volverá a ejecutarse. Esta es la salvaguarda central del sistema de logros — nadie quiere ver el mismo logro aparecer dos veces.
Comportamiento 2: First Blood (victorias en combate > 5)
Información básica:
| Campo | Valor | Por qué |
|---|---|---|
| Nombre | Logro: First Blood | Para tu propia referencia |
| Max Fire Count | 1 | Igual que antes |
Disparador (WHEN):
| Campo | Valor | Por qué |
|---|---|---|
| Trigger Type | Variable Crossed Threshold (variable-crossed) | Detectar el instante en que combat_wins cruza 5 |
| Variable ID | combat_wins | Monitorear el conteo de victorias en combate |
| Direction | Rises Above (rises-above) | Dispararse cuando combat_wins va de <= 5 a > 5 |
| Threshold | 5 | El valor del hito |
Acciones (DO):
| Tipo de acción | Configuración | Propósito |
|---|---|---|
| Establecer variable | achievement_warrior set a true | Marca el logro como desbloqueado |
| Mostrar notificación | Mensaje Logro desbloqueado: Primera sangre, estilo achievement | Abre el toast dorado de logro |
Comportamiento 3: Trailblazer (disparador de palabra clave)
Este logro es diferente de los dos primeros — en lugar de monitorear un umbral numérico, monitorea el contenido de los mensajes. Cuando el jugador dice "explore" o la IA dice "discover", y el logro aún no está desbloqueado, se dispara.
Información básica:
| Campo | Valor | Por qué |
|---|---|---|
| Nombre | Logro: Trailblazer | Para tu propia referencia |
| Max Fire Count | 1 | Igual que antes |
Disparador (WHEN):
Este logro necesita monitorear dos fuentes — mensajes del jugador y mensajes de la IA. En Yumina, un comportamiento solo puede tener un disparador, así que necesitas crear dos comportamientos para cubrir ambas fuentes.
El enfoque más simple es crear dos comportamientos:
Comportamiento 3a: Trailblazer (palabra clave del jugador)
| Campo | Valor | Por qué |
|---|---|---|
| Trigger Type | Player Said Keyword (keyword) | Monitorear mensajes del jugador |
| Keyword | explore | Coincide cuando el jugador dice "I want to explore" |
| Max Fire Count | 1 | Disparar solo una vez |
Condición (ONLY IF):
| Variable ID | Operador | Valor | Por qué |
|---|---|---|---|
achievement_explorer | Igual (eq) | false | Solo disparar si el logro aún no se ha desbloqueado |
Acciones (DO):
| Tipo de acción | Configuración | Propósito |
|---|---|---|
| Establecer variable | achievement_explorer set a true | Marca el logro como desbloqueado |
| Mostrar notificación | Mensaje Logro desbloqueado: Explorador, estilo achievement | Abre el toast dorado de logro |
Comportamiento 3b: Trailblazer (palabra clave de la IA)
| Campo | Valor | Por qué |
|---|---|---|
| Trigger Type | AI Said Keyword (ai-keyword) | Monitorear respuestas de la IA |
| Keyword | discover | Coincide cuando la IA menciona "discover" |
| Max Fire Count | 1 | Disparar solo una vez |
Las condiciones y acciones son idénticas al Comportamiento 3a.
¿Por qué necesitamos la condición
achievement_explorer eq false? Porque dos comportamientos (3a y 3b) pueden ambos desbloquear el mismo logro. Supón que el Comportamiento 3a se dispara primero — estableceachievement_exploreratruey agota su propiomaxFireCount. ¡Pero elmaxFireCountdel Comportamiento 3b aún no se ha usado! Sin la condición, el Comportamiento 3b aún se dispararía la próxima vez que coincidiera con una palabra clave, y el jugador vería dos notificaciones. Con la condición en su lugar, el Comportamiento 3b comprueba queachievement_explorerya seatrue, la condición falla y no se dispara.
Paso 3: Añade el panel de logros al Root Component
Este es el paso clave para que el panel de logros aparezca en el chat. El panel solo aparece debajo del último mensaje.
Editor → sección Custom UI → abre index.tsx → pega lo siguiente (reemplaza el predeterminado return <Chat />):
export default function MyWorld() {
const api = useYumina();
const msgs = api.messages || [];
// Definición de la lista de logros
const achievements = [
{
id: "achievement_rich",
name: "Big Spender",
desc: "Accumulate over 100 gold",
icon: "💰",
},
{
id: "achievement_warrior",
name: "First Blood",
desc: "Win more than 5 battles",
icon: "⚔️",
},
{
id: "achievement_explorer",
name: "Trailblazer",
desc: "Discover a hidden area or secret",
icon: "🗺️",
},
];
// Cuenta los logros desbloqueados
const unlockedCount = achievements.filter(
(a) => api.variables[a.id] === true
).length;
return (
<Chat renderBubble={(msg) => {
const isLastMsg = msg.messageIndex === msgs.length - 1;
return (
<div>
{/* Renderiza el texto del mensaje normalmente (la plataforma ya produjo HTML — solo usa contentHtml) */}
<div
style={{ color: "#e2e8f0", lineHeight: 1.7 }}
dangerouslySetInnerHTML={{ __html: msg.contentHtml }}
/>
{/* Panel de logros — solo debajo del último mensaje */}
{isLastMsg && (
<div
style={{
marginTop: "16px",
padding: "12px 16px",
background: "linear-gradient(135deg, #1c1917, #292524)",
border: "1px solid #44403c",
borderRadius: "10px",
}}
>
{/* Encabezado del panel */}
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: "10px",
}}
>
<span
style={{
fontSize: "13px",
fontWeight: "bold",
color: "#fbbf24",
letterSpacing: "0.05em",
}}
>
🏆 Achievements
</span>
<span style={{ fontSize: "12px", color: "#a8a29e" }}>
{unlockedCount} / {achievements.length}
</span>
</div>
{/* Lista de logros */}
<div style={{ display: "flex", flexDirection: "column", gap: "6px" }}>
{achievements.map((a) => {
const unlocked = api.variables[a.id] === true;
return (
<div
key={a.id}
style={{
display: "flex",
alignItems: "center",
gap: "10px",
padding: "6px 8px",
borderRadius: "6px",
background: unlocked
? "rgba(251, 191, 36, 0.08)"
: "rgba(120, 113, 108, 0.08)",
}}
>
{/* Icono */}
<span style={{ fontSize: "18px", opacity: unlocked ? 1 : 0.3 }}>
{unlocked ? a.icon : "🔒"}
</span>
{/* Texto */}
<div style={{ flex: 1, minWidth: 0 }}>
<div
style={{
fontSize: "13px",
fontWeight: "600",
color: unlocked ? "#fbbf24" : "#78716c",
}}
>
{a.name}
</div>
<div
style={{
fontSize: "11px",
color: unlocked ? "#a8a29e" : "#57534e",
marginTop: "1px",
}}
>
{a.desc}
</div>
</div>
{/* Distintivo de estado */}
{unlocked && (
<span style={{ fontSize: "11px", color: "#fbbf24" }}>
✓ Unlocked
</span>
)}
</div>
);
})}
</div>
</div>
)}
</div>
);
}} />
);
}Desglose línea por línea:
MyWorld()es el Root Component — el punto de entrada de la UI del mundo.<Chat renderBubble={...} />mantiene a la plataforma a cargo de la lista de mensajes, el cuadro de entrada y el desplazamiento; solo personalizamos el layout por burbujaconst api = useYumina()— obtiene la API de Yumina para leer el estado de las variablesmsg.messageIndex === msgs.length - 1— solo muestra el panel en el último mensaje, así no se repite en cada mensajemsg.contentHtml— la plataforma ya renderizó el Markdown a HTML; pásalo directamente adangerouslySetInnerHTML- Array
achievements— define todos los metadatos de los logros (ID, nombre, descripción, icono) directamente en el Root Component. ¿Quieres añadir un nuevo logro? Solo añade otra entrada a este array api.variables[a.id] === true— lee el valor de la variable booleana para comprobar si el logro está desbloqueadounlockedCount— cuenta cuántos están desbloqueados, mostrado en el encabezado (por ejemplo, "2 / 3")- Los logros bloqueados muestran un icono de candado gris, los desbloqueados muestran su icono dorado más un distintivo "Unlocked"
¿No quieres escribir código tú mismo? Usa Studio AI
Barra superior del editor → haz clic en "Enter Studio" → panel AI Assistant → describe en lenguaje natural lo que quieres, y la IA generará el código por ti.
Paso 4: Guarda y prueba
- Haz clic en Save en la parte superior del editor
- Haz clic en Start Game o vuelve a la página de inicio e inicia una nueva sesión
- Debajo del último mensaje deberías ver el panel de logros — los 3 logros en gris con iconos de candado
- Prueba el logro de oro: chatea con la IA y haz que tu personaje gane más de 100 de oro. Cuando
goldvaya de <= 100 a > 100, aparece una notificación dorada: "Logro desbloqueado: Manirroto", y el primer logro del panel se vuelve dorado - Prueba el logro de combate: haz que tu personaje gane 6 batallas. Cuando
combat_winsvaya de 5 a 6, aparece la notificación: "Logro desbloqueado: Primera sangre" - Prueba el logro de exploración: envía un mensaje que contenga "explore" (por ejemplo, "I want to explore this cave"). Si la palabra clave coincide, aparece la notificación: "Logro desbloqueado: Explorador"
Si algo sale mal:
| Síntoma | Causa probable | Solución |
|---|---|---|
| No puedo ver el panel de logros | El código del Root Component no se guardó o tiene un error de sintaxis | Comprueba el estado de compilación en la parte inferior del panel Custom UI — debería mostrar un "OK" verde |
| El oro pasó de 100 pero no hay notificación | La variable no "cruzó" de <= 100 a > 100 — se estableció directamente a 200 | Asegúrate de que el oro cambie incrementalmente (la IA añade/resta mediante directivas), no en un único salto a un número grande |
| El logro apareció dos veces | El maxFireCount del comportamiento no está establecido en 1 | Vuelve al editor y comprueba la configuración del comportamiento |
| El logro de exploración apareció dos veces | Ambos comportamientos 3a y 3b se dispararon, y falta la comprobación de condición | Confirma que ambos comportamientos tengan la condición achievement_explorer eq false |
| El estado del panel no se actualizó | El ID de la variable está mal escrito en el código del Root Component | Confirma que el a.id de api.variables[a.id] coincida exactamente con el ID de la variable |
Análisis profundo: variable-crossed vs state-change
Esta es la distinción conceptual más importante en el sistema de logros — vale la pena ampliarla.
variable-crossed (Variable cruzó umbral)
Detecta un evento instantáneo: "la variable cruzó de un lado del umbral al otro".
gold: 80 → 95 → 101 ← se dispara en el paso 95→101 (cruzó por encima de 100)
gold: 101 → 150 → 200 ← NO se dispara (ya por encima del umbral)
gold: 200 → 50 → 120 ← se dispara en el paso 50→120 (cruzó por encima de 100 de nuevo)Características clave:
- Solo se dispara en el instante del cruce, no "se dispara continuamente mientras esté por encima del umbral"
- Si el valor cae por debajo del umbral y vuelve a subir, se dispara de nuevo (a menos que
maxFireCountlo impida) - Bueno para: desbloqueo de logros, notificaciones de hitos, comprobaciones de muerte cuando HP llega a cero
state-change (Variable cambió)
Detecta un evento en curso: "cualquier variable cambió en absoluto".
gold: 80 → 95 ← se dispara (gold cambió)
gold: 95 → 101 ← se dispara (gold cambió de nuevo)
gold: 101 → 150 ← se dispara (gold sigue cambiando)
hp: 100 → 90 ← también se dispara (hp cambió)Características clave:
- Cualquier cambio en cualquier variable lo dispara
- Necesita condiciones (ONLY IF) para filtrar
- Bueno para: monitoreo general de estado, cambiar el contexto del mundo según el estado actual
Por qué variable-crossed es lo correcto para los logros
Porque los logros son hitos — solo te importa el instante en que se cruza la línea. Si usaras state-change + condición gold gt 100:
- gold va de 95 a 101 → se dispara → condición cumplida → se ejecuta (correcto)
- gold va de 101 a 102 → se dispara → condición cumplida → intenta ejecutarse de nuevo (¡incorrecto!
maxFireCountlo bloquea, pero el motor aún hizo una evaluación inútil) - gold va de 102 a 103 → se dispara de nuevo → comprueba la condición de nuevo...
Con variable-crossed:
- gold va de 95 a 101 → cruce detectado por encima de 100 → se dispara → se ejecuta (correcto)
- gold va de 101 a 102 → sin evento de cruce → no se dispara en absoluto (eficiente)
Conclusión: disparadores precisos = menos evaluaciones desperdiciadas = mejor rendimiento y lógica más limpia.
Ideas de extensión
Una vez que hayas construido los 3 logros básicos, puedes extender con más usando el mismo patrón:
| Nombre del logro | ID de variable | Método de disparo | Condición |
|---|---|---|---|
| Chatterbox | achievement_talkative | Crea una variable message_count, +1 cada turno, dispara cuando cruza 50 | variable-crossed, message_count sube por encima de 50 |
| Hoarder | achievement_hoarder | Dispara cuando el oro cruza 500 | variable-crossed, gold sube por encima de 500 |
| Socialite | achievement_social | La IA dice la palabra clave "become friends" o "trusts you" | ai-keyword, condición achievement_social eq false |
| Back from the Dead | achievement_survivor | HP cruza por debajo de 10 (casi muerte), luego más tarde cruza por encima de 50 (recuperación) | Dos comportamientos vinculados |
Para cada nuevo logro, solo necesitas:
- Añadir una variable booleana (
achievement_xxx, predeterminadofalse) - Añadir un comportamiento (disparador + acciones +
maxFireCount: 1) - Añadir una entrada al array
achievementsen el Root Component
Referencia rápida
| Qué quieres hacer | Cómo hacerlo |
|---|---|
| Desbloquear logro cuando un número alcance un objetivo | Disparador del comportamiento: "Variable Crossed Threshold" (variable-crossed), dirección: rises above, establecer umbral |
| Disparar logro con una palabra clave | Disparador del comportamiento: "Player Said Keyword" (keyword) o "AI Said Keyword" (ai-keyword) |
| Asegurar que el logro se dispare solo una vez | Establecer maxFireCount: 1 en el comportamiento; para disparadores de palabra clave, también añadir condición achievement_xxx eq false |
| Mostrar una notificación dorada de logro | Acción del comportamiento: Show Notification, estilo achievement |
| Mostrar un panel de logros en el chat | El Root Component lee variables booleanas y renderiza estados desbloqueado/bloqueado |
| Añadir un nuevo logro | Añadir variable booleana + añadir comportamiento + añadir entrada al array achievements del Root Component |
Pruébalo tú mismo — mundo demo importable
Descarga este JSON e impórtalo para ver todo en acción:
Cómo importar:
- Ve a Yumina → My Worlds → Create New World
- En el editor, haz clic en More Actions → Import Package
- Selecciona el archivo
.jsondescargado - Se crea un nuevo mundo con todas las variables, comportamientos y el Root Component preconfigurados
- Inicia una nueva sesión y pruébalo
Qué incluye:
- 5 variables (
gold,combat_wins,achievement_rich,achievement_warrior,achievement_explorer) - 4 comportamientos (Big Spender, First Blood, Trailblazer x2)
- Un Root Component con el panel de logros
Esta es la Receta #13
El sistema de logros se combina libremente con otras recetas — emparéjalo con el sistema de combate para seguir victorias en batalla, el sistema de tienda para seguir la acumulación de oro, o el rastreador de misiones para seguir misiones completadas. Las variables son universales, y los comportamientos no interfieren entre sí.
