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:
- Variable numérica + comprobación de condición — El oro es una variable numérica. El comportamiento comprueba si es suficiente antes de ejecutarse.
- Variable JSON + operación push — El inventario es un array JSON. Cada compra usa
pushpara añadir un artículo. - 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
| Campo | Valor | Por qué |
|---|---|---|
| Nombre | Oro | Para tu propia referencia en el editor |
| ID | gold | Usado en código y comportamientos para leer/escribir esta variable |
| Tipo | Number | El oro es numérico — necesitamos operaciones aritméticas |
| Valor por defecto | 100 | El jugador empieza con 100 de oro en una sesión nueva |
| Valor mínimo | 0 | Evita que el oro sea negativo — el motor lo limitará |
| Categoría | Resources | El oro es una variable de tipo recurso |
| Reglas de comportamiento | El 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
| Campo | Valor | Por qué |
|---|---|---|
| Nombre | Inventario | Para tu propia referencia |
| ID | inventory | Usado en código y comportamientos |
| Tipo | JSON | El 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ía | Inventory | Esta es una variable de tipo inventario |
| Reglas de comportamiento | Los 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 usapushpara 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):
| Campo | Valor | Por qué |
|---|---|---|
| Trigger Type | Action button pressed | Se dispara cuando el Root Component llama a executeAction("buy-potion") |
| Action ID | buy-potion | Debe coincidir con la llamada executeAction("buy-potion") en el código del Root Component |
ONLY IF (condiciones):
| Variable | Operador | Valor | Por qué |
|---|---|---|---|
gold | Mayor o igual (gte) | 20 | La 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ón | Configuración | Efecto |
|---|---|---|
| Modificar variable | Variable gold, operación subtract, valor 20 | Descuenta 20 de oro |
| Modificar variable | Variable inventory, operación push, valor "Potion" | Añade "Potion" al array de inventario |
| Mostrar notificación | Mensaje Purchase successful! You got a Potion., estilo achievement | Muestra 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:
| Campo | Valor |
|---|---|
| Trigger Type | Action button pressed |
| Action ID | buy-potion |
ONLY IF:
| Variable | Operador | Valor | Por qué |
|---|---|---|---|
gold | Menor que (lt) | 20 | El oro es menor que 20 — no puedes permitírtelo |
DO:
| Tipo de acción | Configuración | Efecto |
|---|---|---|
| Mostrar notificación | Mensaje Not enough gold! The potion costs 20 gold., estilo warning | Muestra 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:
| Campo | Valor |
|---|---|
| Trigger Type | Action button pressed |
| Action ID | buy-sword |
ONLY IF:
| Variable | Operador | Valor |
|---|---|---|
gold | Mayor o igual (gte) | 50 |
DO:
| Tipo de acción | Configuración | Efecto |
|---|---|---|
| Modificar variable | Variable gold, operación subtract, valor 50 | Descuenta 50 de oro |
| Modificar variable | Variable inventory, operación push, valor "Iron Sword" | Añade "Iron Sword" al array de inventario |
| Mostrar notificación | Mensaje Purchase successful! You got an Iron Sword., estilo achievement | Muestra una notificación de éxito dorada |
Comportamiento 4: Comprar Espada de Hierro (sin suficiente oro)
WHEN:
| Campo | Valor |
|---|---|
| Trigger Type | Action button pressed |
| Action ID | buy-sword |
ONLY IF:
| Variable | Operador | Valor |
|---|---|---|
gold | Menor que (lt) | 50 |
DO:
| Tipo de acción | Configuración | Efecto |
|---|---|---|
| Mostrar notificación | Mensaje Not enough gold! The iron sword costs 50 gold., estilo warning | Muestra 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 />):
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
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 accionesmsg.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 chatmsg.contentHtml— El HTML que la plataforma ya renderizó desde Markdown, se puede usar directamente endangerouslySetInnerHTML
Leyendo variables
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.?? 100es un fallback en caso de que la variable no se haya cargado aúnapi.variables.inventory— Lee la variable de inventario. UsamosArray.isArray()para confirmar que de hecho es un array, protegiéndonos contra datos inesperados
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" },
];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
<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
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
<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
- Haz clic en Save en la parte superior del editor
- Haz clic en Start Game o vuelve a la página de inicio y abre una nueva sesión
- Verás un panel de tienda debajo de la respuesta de la IA: 100 de oro, dos artículos, inventario vacío
- 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."
- Haz clic de nuevo — el oro baja a 60, ahora hay dos pociones en el inventario
- Haz clic en 50 Gold para comprar una espada de hierro — el oro baja a 10, el inventario gana una espada
- Ahora intenta comprar cualquier cosa — aparece una advertencia amarilla que dice "Not enough gold!", y el oro y el inventario se mantienen sin cambios
- 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íntoma | Causa probable | Solución |
|---|---|---|
| El panel de tienda no aparece | 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 de la sección Custom UI — debería mostrar un "OK" verde |
| Los botones no responden a los clics | Los action IDs en los comportamientos no coinciden con el código | Confirma 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 cambia | La acción push en el comportamiento no está bien configurada | Comprueba 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 advertencia | La condición del comportamiento "sin suficiente oro" está invertida | Confirma que la condición es gold lt 20 (menor que), no gold gte 20 |
| Los artículos del inventario no muestran iconos | Los nombres de los artículos no coinciden con el mapeo de iconos en el código | Confirma 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 compra | Normal — se refresca con el siguiente mensaje | Enví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:
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ón | Configuración |
|---|---|
| Tell AI | Contenido: 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óngold add 15.
Referencia rápida
| Qué quieres | Cómo hacerlo |
|---|---|
| Seguir el oro | Crea una variable numérica, categoría: Resources |
| Seguir el inventario | Crea una variable JSON, predeterminado [], categoría: Inventory |
| Descontar oro en la compra | Acción del comportamiento: Modify Variable, operación subtract |
| Añadir artículo en la compra | Acción del comportamiento: Modify Variable, operación push |
| Comprobar si el jugador puede permitírselo | Condició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 compra | En el Root Component, llama a api.executeAction("actionId") |
| Mostrar cuadrícula de inventario | En el Root Component, usa CSS Grid + inventory.map() para renderizar |
| Añadir más artículos | Añ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:
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 Root Component preconfigurados
- 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".
