Skip to content

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. maxFireCount y 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 dorado

Hay una decisión de diseño importante aquí: ¿por qué usar variable-crossed en lugar de state-change?

  • state-change significa "comprobar cuando cualquier variable cambia" — muy amplio. Si usas state-change + condición gold gt 100, entonces cada vez que el oro vaya de 101 a 102, 102 a 103... la condición se re-evalúa. Aunque maxFireCount: 1 evita el re-disparo, el motor sigue haciendo una evaluación inútil cada vez.
  • variable-crossed significa "dispararse solo en el instante en que el oro va de <= 100 a > 100" — preciso y eficiente. Combinado con maxFireCount: 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

CampoValorPor qué
Nombre visibleOroPara tu propia referencia en la lista de variables
IDgoldLos comportamientos y el Root Component usan este ID para leer/escribir
TipoNumberEl oro es numérico y necesita aritmética
Valor por defecto0Las nuevas sesiones empiezan con 0 de oro
CategoríaStatsLo agrupa con los atributos del personaje
Reglas de comportamientoCantidad 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

CampoValorPor qué
Nombre visibleVictorias en combateFácil de identificar
IDcombat_winsReferenciado por los comportamientos
TipoNumberEs un contador
Valor por defecto0Empezar desde 0
CategoríaStatsAtributo del personaje
Reglas de comportamientoNú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

CampoValorPor qué
Nombre visibleLogro: Big SpenderFácil de identificar
IDachievement_richTodas las variables de logro usan el prefijo achievement_
TipoBooleanSolo dos estados: desbloqueado o bloqueado
Valor por defectofalseBloqueado al inicio
CategoríaAchievementsAgrupa todas las variables de logro bajo una categoría para una gestión sencilla
Reglas de comportamientoNo 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

CampoValorPor qué
Nombre visibleLogro: First BloodFácil de identificar
IDachievement_warriorMisma convención de prefijo
TipoBooleanIgual que antes
Valor por defectofalseBloqueado al inicio
CategoríaAchievementsIgual que antes
Reglas de comportamientoNo 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

CampoValorPor qué
Nombre visibleLogro: TrailblazerFácil de identificar
IDachievement_explorerMisma convención de prefijo
TipoBooleanIgual que antes
Valor por defectofalseBloqueado al inicio
CategoríaAchievementsIgual que antes
Reglas de comportamientoNo 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:

CampoValorPor qué
NombreLogro: Big SpenderPara tu propia referencia
Max Fire Count1Los logros se desbloquean una vez — tras dispararse, este comportamiento nunca vuelve a ejecutarse

Disparador (WHEN):

CampoValorPor qué
Trigger TypeVariable Crossed Threshold (variable-crossed)Queremos detectar el instante en que el oro cruza 100
Variable IDgoldMonitorear la variable de oro
DirectionRises Above (rises-above)Dispararse cuando el oro va de <= 100 a > 100
Threshold100El valor del hito

Acciones (DO):

Tipo de acciónConfiguraciónPropósito
Establecer variableachievement_rich set a trueMarca el logro como desbloqueado, para que el Root Component lo lea
Mostrar notificaciónMensaje Logro desbloqueado: Manirroto, estilo achievementAbre 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:

CampoValorPor qué
NombreLogro: First BloodPara tu propia referencia
Max Fire Count1Igual que antes

Disparador (WHEN):

CampoValorPor qué
Trigger TypeVariable Crossed Threshold (variable-crossed)Detectar el instante en que combat_wins cruza 5
Variable IDcombat_winsMonitorear el conteo de victorias en combate
DirectionRises Above (rises-above)Dispararse cuando combat_wins va de <= 5 a > 5
Threshold5El valor del hito

Acciones (DO):

Tipo de acciónConfiguraciónPropósito
Establecer variableachievement_warrior set a trueMarca el logro como desbloqueado
Mostrar notificaciónMensaje Logro desbloqueado: Primera sangre, estilo achievementAbre 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:

CampoValorPor qué
NombreLogro: TrailblazerPara tu propia referencia
Max Fire Count1Igual 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)

CampoValorPor qué
Trigger TypePlayer Said Keyword (keyword)Monitorear mensajes del jugador
KeywordexploreCoincide cuando el jugador dice "I want to explore"
Max Fire Count1Disparar solo una vez

Condición (ONLY IF):

Variable IDOperadorValorPor qué
achievement_explorerIgual (eq)falseSolo disparar si el logro aún no se ha desbloqueado

Acciones (DO):

Tipo de acciónConfiguraciónPropósito
Establecer variableachievement_explorer set a trueMarca el logro como desbloqueado
Mostrar notificaciónMensaje Logro desbloqueado: Explorador, estilo achievementAbre el toast dorado de logro

Comportamiento 3b: Trailblazer (palabra clave de la IA)

CampoValorPor qué
Trigger TypeAI Said Keyword (ai-keyword)Monitorear respuestas de la IA
KeyworddiscoverCoincide cuando la IA menciona "discover"
Max Fire Count1Disparar 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 — establece achievement_explorer a true y agota su propio maxFireCount. ¡Pero el maxFireCount del 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 que achievement_explorer ya sea true, 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 />):

tsx
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 burbuja
  • const api = useYumina() — obtiene la API de Yumina para leer el estado de las variables
  • msg.messageIndex === msgs.length - 1 — solo muestra el panel en el último mensaje, así no se repite en cada mensaje
  • msg.contentHtml — la plataforma ya renderizó el Markdown a HTML; pásalo directamente a dangerouslySetInnerHTML
  • 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á desbloqueado
  • unlockedCount — 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

  1. Haz clic en Save en la parte superior del editor
  2. Haz clic en Start Game o vuelve a la página de inicio e inicia una nueva sesión
  3. Debajo del último mensaje deberías ver el panel de logros — los 3 logros en gris con iconos de candado
  4. Prueba el logro de oro: chatea con la IA y haz que tu personaje gane más de 100 de oro. Cuando gold vaya de <= 100 a > 100, aparece una notificación dorada: "Logro desbloqueado: Manirroto", y el primer logro del panel se vuelve dorado
  5. Prueba el logro de combate: haz que tu personaje gane 6 batallas. Cuando combat_wins vaya de 5 a 6, aparece la notificación: "Logro desbloqueado: Primera sangre"
  6. 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íntomaCausa probableSolución
No puedo ver el panel de logrosEl código del Root Component no se guardó o tiene un error de sintaxisComprueba 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ónLa variable no "cruzó" de <= 100 a > 100 — se estableció directamente a 200Asegú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 vecesEl maxFireCount del comportamiento no está establecido en 1Vuelve al editor y comprueba la configuración del comportamiento
El logro de exploración apareció dos vecesAmbos comportamientos 3a y 3b se dispararon, y falta la comprobación de condiciónConfirma 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 ComponentConfirma 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 maxFireCount lo 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:

  1. gold va de 95 a 101 → se dispara → condición cumplida → se ejecuta (correcto)
  2. gold va de 101 a 102 → se dispara → condición cumplida → intenta ejecutarse de nuevo (¡incorrecto! maxFireCount lo bloquea, pero el motor aún hizo una evaluación inútil)
  3. gold va de 102 a 103 → se dispara de nuevo → comprueba la condición de nuevo...

Con variable-crossed:

  1. gold va de 95 a 101 → cruce detectado por encima de 100 → se dispara → se ejecuta (correcto)
  2. 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 logroID de variableMétodo de disparoCondición
Chatterboxachievement_talkativeCrea una variable message_count, +1 cada turno, dispara cuando cruza 50variable-crossed, message_count sube por encima de 50
Hoarderachievement_hoarderDispara cuando el oro cruza 500variable-crossed, gold sube por encima de 500
Socialiteachievement_socialLa IA dice la palabra clave "become friends" o "trusts you"ai-keyword, condición achievement_social eq false
Back from the Deadachievement_survivorHP 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:

  1. Añadir una variable booleana (achievement_xxx, predeterminado false)
  2. Añadir un comportamiento (disparador + acciones + maxFireCount: 1)
  3. Añadir una entrada al array achievements en el Root Component

Referencia rápida

Qué quieres hacerCómo hacerlo
Desbloquear logro cuando un número alcance un objetivoDisparador del comportamiento: "Variable Crossed Threshold" (variable-crossed), dirección: rises above, establecer umbral
Disparar logro con una palabra claveDisparador del comportamiento: "Player Said Keyword" (keyword) o "AI Said Keyword" (ai-keyword)
Asegurar que el logro se dispare solo una vezEstablecer 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 logroAcción del comportamiento: Show Notification, estilo achievement
Mostrar un panel de logros en el chatEl Root Component lee variables booleanas y renderiza estados desbloqueado/bloqueado
Añadir un nuevo logroAñ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:

recipe-13-demo.json

Cómo importar:

  1. Ve a Yumina → My WorldsCreate New World
  2. En el editor, haz clic en More ActionsImport Package
  3. Selecciona el archivo .json descargado
  4. Se crea un nuevo mundo con todas las variables, comportamientos y el Root Component preconfigurados
  5. 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í.