Skip to content

Tienda y comercio

Construye una UI de tienda — los jugadores navegan por los artículos, hacen clic para comprar, el oro se descuenta automáticamente y los artículos van directamente a su inventario. Esta receta te muestra cómo combinar variables, comportamientos y un Root Component en un sistema de comercio completo.


Lo que vas a construir

Un panel de tienda integrado en la interfaz del chat. El jugador puede ver cuánto oro tiene, qué hay a la venta y el precio de cada artículo. Cuando hace clic en un botón "Comprar":

  • El oro se reduce automáticamente por el precio del artículo
  • El artículo se añade al inventario (un array JSON)
  • Aparece una notificación "¡Compra exitosa!"
  • Si no hay suficiente oro, aparece una advertencia "¡No hay suficiente oro!" — no se descuenta oro y no se añade artículo

También hay una cuadrícula de inventario en la parte inferior que muestra todos los artículos en la bolsa del jugador en tiempo real.

El jugador hace clic en "Comprar Poción (20 oro)"
  → El comportamiento comprueba: ¿oro >= 20?
    → Sí: oro menos 20, inventory push "Potion", mostrar notificación de éxito
    → No: mostrar advertencia "¡No hay suficiente oro!"

Cómo funciona

Este sistema de tienda combina tres mecanismos centrales:

  1. Variable numérica + comprobación de condición — El oro es una variable numérica. El comportamiento comprueba si es suficiente antes de ejecutarse.
  2. Variable JSON + operación push — El inventario es un array JSON. Cada compra usa push para añadir un artículo.
  3. Disparador de acción — Cada botón de compra corresponde a un action ID. Los botones en el Root Component llaman a executeAction() para disparar comportamientos.

El flujo completo:

UI de botones del Root Component
  → El jugador hace clic en "Comprar Poción"
  → Llama a api.executeAction("buy-potion")
  → El motor encuentra el comportamiento con action ID "buy-potion"
  → Comprueba la condición: ¿oro >= 20?
    → Pasa → Ejecuta acciones: modificar variable (oro -20), modificar variable (inventory push "Potion"), mostrar notificación
    → Falla → No hace nada (el mensaje "no hay suficiente oro" lo gestiona un comportamiento separado)

Paso a paso

Paso 1: Crea variables

Necesitamos dos variables — una para seguir el oro, otra para seguir lo que hay en el inventario.

Editor → barra lateral → pestaña Variables → haz clic en Add Variable

Variable 1: Oro

CampoValorPor qué
NombreOroPara tu propia referencia en el editor
IDgoldUsado en código y comportamientos para leer/escribir esta variable
TipoNumberEl oro es numérico — necesitamos operaciones aritméticas
Valor por defecto100El jugador empieza con 100 de oro en una sesión nueva
Valor mínimo0Evita que el oro sea negativo — el motor lo limitará
CategoríaResourcesEl oro es una variable de tipo recurso
Reglas de comportamientoEl oro se descuenta automáticamente cuando el jugador compra artículos en la tienda. También puedes aumentar o disminuir el oro en la historia — por ejemplo, recompensas de misiones, ser robado por ladrones o encontrar un cofre del tesoro.Le dice a la IA que el oro puede cambiar durante la historia, no solo en la tienda

¿Por qué fijar un valor mínimo de 0? Ya comprobamos "¿puede el jugador permitirse esto?" en la condición del comportamiento, pero añadir protección a nivel del motor es más seguro. Si algo se cuela, el oro tampoco será negativo.

Variable 2: Inventario

CampoValorPor qué
NombreInventarioPara tu propia referencia
IDinventoryUsado en código y comportamientos
TipoJSONEl inventario es un array — necesita el tipo JSON para almacenarlo
Valor por defecto[]Array vacío — el inventario empieza vacío en una sesión nueva
CategoríaInventoryEsta es una variable de tipo inventario
Reglas de comportamientoLos artículos se añaden automáticamente al comprarlos en la tienda. También puedes añadir o quitar artículos en la historia — por ejemplo, el jugador recoge algo, un artículo se rompe, se lo roban o lo recibe como recompensa de misión.Le dice a la IA que el inventario puede cambiar durante la historia, no solo en la tienda

Las variables JSON pueden almacenar cualquier estructura de datos JSON. Aquí usamos un array ([]) para contener una lista de nombres de artículos. Cada compra usa push para añadir una cadena al final del array. Por ejemplo, tras comprar una poción el valor pasa de [] a ["Potion"], y al comprar una espada de hierro después se convierte en ["Potion", "Iron Sword"].


Paso 2: Crea comportamientos de tienda

Necesitamos varios comportamientos — un "compra exitosa" y un "no hay suficiente oro" para cada artículo. Aquí usaremos Poción y Espada de Hierro como ejemplos.

Editor → pestaña Behaviors → haz clic en Add Behavior

Comportamiento 1: Comprar Poción (éxito)

WHEN (disparador):

CampoValorPor qué
Trigger TypeAction button pressedSe dispara cuando el Root Component llama a executeAction("buy-potion")
Action IDbuy-potionDebe coincidir con la llamada executeAction("buy-potion") en el código del Root Component

ONLY IF (condiciones):

VariableOperadorValorPor qué
goldMayor o igual (gte)20La poción cuesta 20 de oro — solo se puede comprar si tienes suficiente

DO (acciones):

Añade las siguientes acciones en orden:

Tipo de acciónConfiguraciónEfecto
Modificar variableVariable gold, operación subtract, valor 20Descuenta 20 de oro
Modificar variableVariable inventory, operación push, valor "Potion"Añade "Potion" al array de inventario
Mostrar notificaciónMensaje Purchase successful! You got a Potion., estilo achievementMuestra una notificación de éxito dorada

La operación push es específica para arrays JSON. Añade un elemento al final del array sin sobrescribir el contenido existente. Así que cada vez que compres una poción, otra cadena "Potion" se añade al inventario.

Comportamiento 2: Comprar Poción (sin suficiente oro)

Este comportamiento escucha el mismo action ID, pero la condición es "el oro no es suficiente".

WHEN:

CampoValor
Trigger TypeAction button pressed
Action IDbuy-potion

ONLY IF:

VariableOperadorValorPor qué
goldMenor que (lt)20El oro es menor que 20 — no puedes permitírtelo

DO:

Tipo de acciónConfiguraciónEfecto
Mostrar notificaciónMensaje Not enough gold! The potion costs 20 gold., estilo warningMuestra una notificación de advertencia amarilla

¿Por qué dos comportamientos separados? Porque un único comportamiento solo puede tener un conjunto de condiciones. Si la condición pasa, las acciones se ejecutan; si falla, no pasa nada. Así que usamos dos comportamientos para cubrir ambos casos: oro suficiente → compra exitosa; oro insuficiente → mostrar advertencia. Escuchan el mismo action ID pero tienen condiciones mutuamente exclusivas, por lo que solo uno se dispara.

Comportamiento 3: Comprar Espada de Hierro (éxito)

WHEN:

CampoValor
Trigger TypeAction button pressed
Action IDbuy-sword

ONLY IF:

VariableOperadorValor
goldMayor o igual (gte)50

DO:

Tipo de acciónConfiguraciónEfecto
Modificar variableVariable gold, operación subtract, valor 50Descuenta 50 de oro
Modificar variableVariable inventory, operación push, valor "Iron Sword"Añade "Iron Sword" al array de inventario
Mostrar notificaciónMensaje Purchase successful! You got an Iron Sword., estilo achievementMuestra una notificación de éxito dorada

Comportamiento 4: Comprar Espada de Hierro (sin suficiente oro)

WHEN:

CampoValor
Trigger TypeAction button pressed
Action IDbuy-sword

ONLY IF:

VariableOperadorValor
goldMenor que (lt)50

DO:

Tipo de acciónConfiguraciónEfecto
Mostrar notificaciónMensaje Not enough gold! The iron sword costs 50 gold., estilo warningMuestra una notificación de advertencia amarilla

¿Quieres añadir más artículos?

Solo repite el patrón — dos comportamientos por artículo (éxito + insuficiente), cambiando el action ID, el precio y el nombre del artículo. Por ejemplo, para añadir un "Escudo" de 30 de oro: action ID buy-shield, condición gold gte 30, acciones subtract 30 + push "Shield".


Paso 3: Añade el panel de tienda en el Root Component

Este es el paso clave que hace que la UI de la tienda aparezca en el chat. Mostraremos tres áreas debajo de cada mensaje: saldo de oro, lista de artículos (con botones de compra) y una cuadrícula de inventario.

Editor → sección Custom UI → abre index.tsx → pega el siguiente código (reemplazando el predeterminado return <Chat />):

tsx
export default function MyWorld() {
  const api = useYumina();
  const msgs = api.messages || [];

  // Lee las variables
  const gold = Number(api.variables.gold ?? 100);
  const inventory = Array.isArray(api.variables.inventory)
    ? api.variables.inventory
    : [];

  // Definiciones de artículos de la tienda
  const shopItems = [
    { name: "Potion",     price: 20, actionId: "buy-potion", icon: "\u{1F9EA}", desc: "Restores a small amount of health" },
    { name: "Iron Sword", price: 50, actionId: "buy-sword",  icon: "⚔️", desc: "A plain iron sword" },
  ];

  return (
    <Chat renderBubble={(msg) => {
      const isLastMsg = msg.messageIndex === msgs.length - 1;
      return (
    <div>
      {/* Renderiza el texto del mensaje normalmente (la plataforma ya renderizó el HTML, usa contentHtml directamente) */}
      <div
        style={{ color: "#e2e8f0", lineHeight: 1.7 }}
        dangerouslySetInnerHTML={{ __html: msg.contentHtml }}
      />

      {/* Solo muestra la tienda debajo del último mensaje */}
      {isLastMsg && (
        <div style={{
          marginTop: "16px",
          padding: "16px",
          background: "rgba(15, 23, 42, 0.6)",
          borderRadius: "12px",
          border: "1px solid #334155",
        }}>

          {/* ====== Visualización de oro ====== */}
          <div style={{
            display: "flex",
            alignItems: "center",
            gap: "8px",
            marginBottom: "16px",
            padding: "10px 14px",
            background: "linear-gradient(135deg, #78350f, #92400e)",
            borderRadius: "8px",
            border: "1px solid #b45309",
          }}>
            <span style={{ fontSize: "20px" }}>{"💰"}</span>
            <span style={{ color: "#fde68a", fontSize: "16px", fontWeight: "bold" }}>
              {gold} Gold
            </span>
          </div>

          {/* ====== Encabezado de tienda ====== */}
          <div style={{
            fontSize: "14px",
            fontWeight: "bold",
            color: "#94a3b8",
            marginBottom: "10px",
            textTransform: "uppercase",
            letterSpacing: "1px",
          }}>
            Shop
          </div>

          {/* ====== Lista de artículos ====== */}
          <div style={{ display: "flex", flexDirection: "column", gap: "8px", marginBottom: "16px" }}>
            {shopItems.map((item) => (
              <div
                key={item.actionId}
                style={{
                  display: "flex",
                  alignItems: "center",
                  justifyContent: "space-between",
                  padding: "10px 14px",
                  background: "rgba(30, 41, 59, 0.8)",
                  borderRadius: "8px",
                  border: "1px solid #475569",
                }}
              >
                <div style={{ display: "flex", alignItems: "center", gap: "10px" }}>
                  <span style={{ fontSize: "22px" }}>{item.icon}</span>
                  <div>
                    <div style={{ color: "#e2e8f0", fontSize: "14px", fontWeight: "600" }}>
                      {item.name}
                    </div>
                    <div style={{ color: "#64748b", fontSize: "12px" }}>
                      {item.desc}
                    </div>
                  </div>
                </div>
                <button
                  onClick={() => api.executeAction(item.actionId)}
                  style={{
                    padding: "6px 16px",
                    background: gold >= item.price
                      ? "linear-gradient(135deg, #065f46, #047857)"
                      : "linear-gradient(135deg, #374151, #4b5563)",
                    border: gold >= item.price
                      ? "1px solid #10b981"
                      : "1px solid #6b7280",
                    borderRadius: "6px",
                    color: gold >= item.price ? "#a7f3d0" : "#9ca3af",
                    fontSize: "13px",
                    fontWeight: "600",
                    cursor: gold >= item.price ? "pointer" : "not-allowed",
                    opacity: gold >= item.price ? 1 : 0.6,
                    whiteSpace: "nowrap",
                  }}
                >
                  {item.price} Gold
                </button>
              </div>
            ))}
          </div>

          {/* ====== Encabezado del inventario ====== */}
          <div style={{
            fontSize: "14px",
            fontWeight: "bold",
            color: "#94a3b8",
            marginBottom: "10px",
            textTransform: "uppercase",
            letterSpacing: "1px",
          }}>
            Inventory
          </div>

          {/* ====== Cuadrícula de inventario ====== */}
          {inventory.length === 0 ? (
            <div style={{
              padding: "20px",
              textAlign: "center",
              color: "#475569",
              fontSize: "13px",
              background: "rgba(30, 41, 59, 0.4)",
              borderRadius: "8px",
              border: "1px dashed #334155",
            }}>
              Inventory is empty
            </div>
          ) : (
            <div style={{
              display: "grid",
              gridTemplateColumns: "repeat(auto-fill, minmax(80px, 1fr))",
              gap: "8px",
            }}>
              {inventory.map((item, idx) => (
                <div
                  key={idx}
                  style={{
                    display: "flex",
                    flexDirection: "column",
                    alignItems: "center",
                    justifyContent: "center",
                    padding: "10px 6px",
                    background: "rgba(30, 41, 59, 0.8)",
                    borderRadius: "8px",
                    border: "1px solid #475569",
                    gap: "4px",
                  }}
                >
                  <span style={{ fontSize: "24px" }}>
                    {item === "Potion" ? "\u{1F9EA}" : item === "Iron Sword" ? "⚔️" : "📦"}
                  </span>
                  <span style={{ color: "#cbd5e1", fontSize: "11px", textAlign: "center" }}>
                    {String(item)}
                  </span>
                </div>
              ))}
            </div>
          )}
        </div>
      )}
    </div>
      );
    }} />
  );
}

Recorrido por el código

No dejes que la longitud del código te intimide — lo que hace es muy directo. Vamos sección por sección:

Configuración básica

tsx
const api = useYumina();
const msgs = api.messages || [];
// ...
<Chat renderBubble={(msg) => {
  const isLastMsg = msg.messageIndex === msgs.length - 1;
  // ...
}} />
  • El Root Component MyWorld() es la entrada para la UI del mundo. <Chat renderBubble={...} /> deja que la plataforma gestione la lista de mensajes, el cuadro de entrada y el desplazamiento — solo tomas el control de cómo se ve una burbuja individual
  • useYumina() — Obtiene la API de Yumina para que puedas leer variables y disparar acciones
  • msg.messageIndex — El índice de la burbuja actual en la lista de mensajes, usado para comprobar si es el último. El panel de tienda solo se muestra debajo del último mensaje para que no se repita en cada mensaje del chat
  • msg.contentHtml — El HTML que la plataforma ya renderizó desde Markdown, se puede usar directamente en dangerouslySetInnerHTML

Leyendo variables

tsx
const gold = Number(api.variables.gold ?? 100);
const inventory = Array.isArray(api.variables.inventory)
  ? api.variables.inventory
  : [];
  • api.variables.gold — Lee la variable de oro. ?? 100 es un fallback en caso de que la variable no se haya cargado aún
  • api.variables.inventory — Lee la variable de inventario. Usamos Array.isArray() para confirmar que de hecho es un array, protegiéndonos contra datos inesperados

Definiciones de artículos de la tienda

tsx
const shopItems = [
  { name: "Potion",     price: 20, actionId: "buy-potion", icon: "\u{1F9EA}", desc: "Restores a small amount of health" },
  { name: "Iron Sword", price: 50, actionId: "buy-sword",  icon: "⚔️", desc: "A plain iron sword" },
];

Toda la información del artículo se define en un único array, luego se renderiza con .map(). ¿Quieres añadir un nuevo artículo? Solo añade una línea al array — y por supuesto, crea los comportamientos correspondientes también en el editor.

El botón de compra

tsx
<button onClick={() => api.executeAction(item.actionId)}>
  {item.price} Gold
</button>

Esta es la línea más importante. Hacer clic en el botón llama a api.executeAction("buy-potion"), y el motor encuentra el comportamiento con action ID "buy-potion", comprueba las condiciones y ejecuta las acciones. Toda la lógica (comprobar oro, descontarlo, añadir el artículo, mostrar la notificación) se define en los comportamientos — el botón solo los dispara.

Retroalimentación visual del botón

tsx
background: gold >= item.price
  ? "linear-gradient(135deg, #065f46, #047857)"   // asequible → verde
  : "linear-gradient(135deg, #374151, #4b5563)",   // no asequible → gris
cursor: gold >= item.price ? "pointer" : "not-allowed",
opacity: gold >= item.price ? 1 : 0.6,

El color del botón, el estilo del cursor y la opacidad cambian dinámicamente según si el jugador puede permitirse el artículo. Los artículos asequibles obtienen botones verdes; los inasequibles aparecen en gris. Esto es retroalimentación puramente visual — la lógica real de la compra vive en las condiciones del comportamiento.

Cuadrícula de inventario

tsx
<div style={{
  display: "grid",
  gridTemplateColumns: "repeat(auto-fill, minmax(80px, 1fr))",
  gap: "8px",
}}>
  {inventory.map((item, idx) => (
    <div key={idx} style={{ /* estilos de celda */ }}>
      <span>{item === "Potion" ? "\u{1F9EA}" : item === "Iron Sword" ? "⚔️" : "📦"}</span>
      <span>{String(item)}</span>
    </div>
  ))}
</div>

Usa CSS Grid para distribuir los artículos del inventario. auto-fill + minmax(80px, 1fr) hace que las celdas se adapten al ancho disponible — las ventanas más anchas muestran más artículos por fila, las más estrechas muestran menos. Cada celda muestra el icono y el nombre del artículo.

¿No quieres escribir código? Usa Studio AI

En la parte superior del editor, haz clic en Enter Studio → panel AI Assistant → describe lo que quieres, por ejemplo, "Construye una UI de tienda con visualización de oro, lista de artículos y cuadrícula de inventario" — 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 y abre una nueva sesión
  3. Verás un panel de tienda debajo de la respuesta de la IA: 100 de oro, dos artículos, inventario vacío
  4. Haz clic en 20 Gold para comprar una poción — el oro baja a 80, aparece un icono de poción en el inventario y una notificación dorada dice "Purchase successful! You got a Potion."
  5. Haz clic de nuevo — el oro baja a 60, ahora hay dos pociones en el inventario
  6. Haz clic en 50 Gold para comprar una espada de hierro — el oro baja a 10, el inventario gana una espada
  7. Ahora intenta comprar cualquier cosa — aparece una advertencia amarilla que dice "Not enough gold!", y el oro y el inventario se mantienen sin cambios
  8. Continúa chateando con la IA — el panel de tienda permanece en la parte inferior del último mensaje, actualizándose en tiempo real

Si algo sale mal:

SíntomaCausa probableSolución
El panel de tienda no apareceEl código del Root Component no se guardó o tiene un error de sintaxisComprueba el estado de compilación en la parte inferior de la sección Custom UI — debería mostrar un "OK" verde
Los botones no responden a los clicsLos action IDs en los comportamientos no coinciden con el códigoConfirma que los action IDs del comportamiento son buy-potion / buy-sword, coincidiendo exactamente con los argumentos executeAction() en el código
Se descuenta oro pero el inventario no cambiaLa acción push en el comportamiento no está bien configuradaComprueba la acción de modificar variable: la variable debe ser inventory, la operación debe ser push, el valor debe ser "Potion" (con comillas)
No hay suficiente oro pero no aparece advertenciaLa condición del comportamiento "sin suficiente oro" está invertidaConfirma que la condición es gold lt 20 (menor que), no gold gte 20
Los artículos del inventario no muestran iconosLos nombres de los artículos no coinciden con el mapeo de iconos en el códigoConfirma que el valor push del comportamiento coincide con el mapeo de iconos del código ("Potion" mapea al emoji de tubo de ensayo, etc.)
La visualización del oro no se actualiza tras la compraNormal — se refresca con el siguiente mensajeEnvía un mensaje y vuelve a comprobar, o comprueba si apareció la notificación (si lo hizo, la compra fue exitosa)

Yendo más allá: ampliando el sistema de tienda

Una vez que tengas los conceptos básicos, puedes usar los mismos patrones para construir sistemas más complejos.

Añadir más artículos

Añade una línea al array shopItems en el Root Component:

tsx
const shopItems = [
  { name: "Potion",       price: 20, actionId: "buy-potion", icon: "\u{1F9EA}", desc: "Restores a small amount of health" },
  { name: "Iron Sword",   price: 50, actionId: "buy-sword",  icon: "⚔️", desc: "A plain iron sword" },
  { name: "Shield",       price: 30, actionId: "buy-shield",  icon: "🛡️", desc: "Provides basic protection" },
  { name: "Magic Scroll", price: 80, actionId: "buy-scroll", icon: "📜", desc: "Unleashes a fireball spell" },
];

Luego en la pestaña Behaviors del editor, crea dos comportamientos para cada nuevo artículo (éxito + insuficiente), siguiendo el mismo patrón exacto que Poción y Espada de Hierro.

Hacer saber a la IA lo que el jugador compró

Si quieres que la historia de la IA reaccione a las compras (por ejemplo, tras comprar una espada de hierro la IA sabe que el jugador está armado), añade una acción "Tell AI" al comportamiento de éxito de compra:

Tipo de acciónConfiguración
Tell AIContenido: El jugador acaba de comprar una Espada de Hierro en la tienda. Por favor haz referencia a esta arma en las respuestas siguientes cuando sea apropiado.

Esto inyecta una instrucción temporal en el contexto de la IA, dejándole saber lo que pasó.

Ganar oro

Ahora mismo el jugador solo puede gastar oro, no ganarlo. Puedes usar comportamientos para dar oro al jugador:

  • Recompensa por turno: Crea un comportamiento con el disparador "Every N turns" (por ejemplo, cada 3 turnos), con la acción Modify Variable gold add 10. El jugador gana automáticamente 10 de oro cada 3 rondas de conversación.
  • Recompensa por palabra clave: Usa el disparador "AI said keyword" con una palabra clave como "battle won" o "quest complete". Cuando la IA mencione estas palabras en una respuesta, el oro se añade automáticamente.
  • Botón manual de ganancia: Añade un botón "Trabajar por oro" en el Root Component usando executeAction("earn-gold") para disparar un comportamiento con la acción gold add 15.

Referencia rápida

Qué quieresCómo hacerlo
Seguir el oroCrea una variable numérica, categoría: Resources
Seguir el inventarioCrea una variable JSON, predeterminado [], categoría: Inventory
Descontar oro en la compraAcción del comportamiento: Modify Variable, operación subtract
Añadir artículo en la compraAcción del comportamiento: Modify Variable, operación push
Comprobar si el jugador puede permitírseloCondición del comportamiento: gold gte price
Mostrar advertencia "no hay suficiente oro"Comportamiento separado, condición gold lt price, acción: Show Notification (warning)
Mostrar alerta "compra exitosa"Acción del comportamiento: Show Notification (estilo achievement)
Botón dispara compraEn el Root Component, llama a api.executeAction("actionId")
Mostrar cuadrícula de inventarioEn el Root Component, usa CSS Grid + inventory.map() para renderizar
Añadir más artículosAñade una línea al array shopItems + crea dos comportamientos en el editor

Pruébalo tú mismo — mundo demo importable

Descarga este archivo JSON e impórtalo para experimentar el sistema de tienda completo:

recipe-3-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 Root Component preconfigurados
  5. Inicia una nueva sesión y pruébalo

Qué incluye:

  • 2 variables (gold + inventory)
  • 4 comportamientos (éxito/insuficiente de compra de poción + éxito/insuficiente de compra de espada de hierro)
  • Un Root Component (visualización de oro + lista de artículos + cuadrícula de inventario)

Esta es la Receta #3

Las recetas anteriores cubrieron el salto de escenas y la modificación de entradas. Esta receta te muestra cómo combinar comprobaciones de condición de variables + arrays JSON + acciones de comportamiento en un sistema interactivo. El mismo patrón funciona para sistemas de misiones, sistemas de combate, sistemas de crafteo — cualquier cosa que necesite "comprobar condición → descontar recurso → añadir artículo → dar retroalimentación".