v
Skip to content

Modo Novela Visual ​

Una novela visual a pantalla completa: fondos de escena, sprites de personajes, un cuadro de diálogo y botones de elección, movidos por variables que la IA cambia según avanza la historia. El código de la interfaz (index.tsx) dibuja la pantalla entera sin <Chat /> y pone <MessageInput /> donde escribe el jugador. Tailwind y estilos en línea, sin biblioteca de componentes.


Sin código ​

Varias partes de una novela visual ya no necesitan código de interfaz:

  • Retratos de personaje: en el lienzo, haz clic derecho en la entrada de lore de un personaje y elige Retrato…. En el chat, esa imagen y el nombre del personaje aparecen encima de cada frase que dice. Mira Imagen y sonido
  • Un estilo de texto como de libro: en la Interfaz del jugador, selecciona el bloque Conversación y pon Aspecto de los mensajes en Novela
  • Un fondo o un sprite que sigue a una variable: en la interfaz del jugador, añade una parte Imagen y pon su Imagen en Sigue a «<variable>». Muestra la referencia @asset: que tenga la variable, así que cuando la IA cambia la variable, cambia la imagen
  • Imágenes en momentos de la historia: las imágenes de escena aparecen solas cuando se cumple su frase de Cuándo mostrarla
  • Botones de elección: botones de la interfaz del jugador con un paso Decir una frase por el jugador

El resto de la página monta toda la pantalla con código, para cuando quieras un cuadro de diálogo que lea la respuesta y separe la narración del diálogo, botones de elección sacados del texto de la respuesta y control total sobre la disposición.


Lo que vas a construir ​

Una interfaz de novela visual a pantalla completa:

  • Fondos de escena: la IA cambia la imagen de fondo cambiando una variable (aula, calle, cielo nocturno...), y el código de la interfaz la muestra como un background-image a pantalla completa
  • Sprites de personajes: la IA pone quién habla y con qué emoción, y el código de la interfaz muestra el sprite que corresponde en el centro de la pantalla
  • Cuadro de diálogo: un cuadro semitransparente en la parte de abajo muestra el nombre del personaje y lo que dice. El texto en cursiva se trata como narración o monólogo interior; el texto normal, como diálogo del personaje
  • Botones de elección: cuando la IA ofrece opciones, el código de la interfaz pone encima botones que se pueden pulsar
  • Modo pantalla completa: el código de la interfaz devuelve su propia disposición a pantalla completa sin meter <Chat />, así que no hay burbujas de chat normales

Cómo funciona ​

La IA controla la pantalla con directivas en cada respuesta:

Respuesta de la IA:
[current_bg: set "@asset:your-classroom-reference"]
[current_speaker: set "Yuki"]
[speaker_emotion: set "happy"]

*La luz de la mañana baña el aula. De vez en cuando entra algún pétalo de cerezo por la ventana.*

Yuki se gira hacia ti con una sonrisa:

¡Buenos días! Hoy has llegado temprano.

Cuando el motor ha procesado estas directivas:

  1. current_bg pasa a ser la imagen del aula → el código de la interfaz cambia el background-image al aula
  2. current_speaker pasa a ser "Yuki" → el cuadro de diálogo muestra el nombre «Yuki»
  3. speaker_emotion pasa a ser "happy" → el código de la interfaz busca el sprite contento de Yuki y lo muestra
  4. El código de la interfaz analiza el texto del último mensaje: lo que va en cursiva sale como narración y el texto normal, como diálogo del personaje
Flujo de procesamiento del motor:
  Respuesta de la IA → el motor extrae las directivas → actualiza las variables → el código de la interfaz lee las variables y redibuja
    → capa de fondo: <div style={{ backgroundImage: ... }} />
    → capa de sprites: <img src={SPRITES[speaker + "_" + emotion]} />
    → capa del cuadro de diálogo: separa la narración (cursiva gris) del diálogo (blanco, letra normal)
    → capa de elecciones: aparecen botones cuando show_choices = true

Paso a paso ​

Paso 1: Crea las variables ​

Necesitas 4 variables para controlar lo que muestra la novela visual.

En el lienzo, pulsa + Variable en la fila Puedes añadir, abajo en la carta, una vez por cada una. Ponle a cada una el nombre de abajo; tanto el código de la interfaz como la IA encuentran las variables por su nombre.

Variable 1: Fondo actual ​

CampoValorPor qué
Nombre de la variablecurrent_bgLa IA escribe [current_bg: set "..."] para cambiar el fondo; el código lee api.variables.current_bg
TipoTextoEl valor es una referencia @asset: o la URL de una imagen
Valor inicialLa referencia @asset: del fondo de tu primera escenaLo que se ve en una sesión nueva antes de que la IA cambie nada. Rellénalo después del Paso 3
Reglas de comportamientoLa imagen de fondo de la escena. Cámbiala cada vez que la escena pase a otro lugar, usando la referencia que corresponda de la tabla de recursos.Le dice a la IA cuándo cambiarla y qué valores usar

Variable 2: Quién habla ​

CampoValorPor qué
Nombre de la variablecurrent_speakerLa IA escribe [current_speaker: set "nombre"] para cambiar de personaje
TipoTextoEl valor es el nombre de un personaje
Valor inicialNarratorEmpieza en modo narración, sin ningún personaje hablando
Reglas de comportamientoQuién habla ahora mismo. Pon el nombre del personaje cuando hable un personaje, y "Narrator" para la narración o el monólogo interior.Le explica a la IA cómo usarla

Variable 3: Emoción del personaje ​

CampoValorPor qué
Nombre de la variablespeaker_emotionLa IA escribe [speaker_emotion: set "happy"] para cambiar de expresión
TipoTextoEl valor es una palabra clave de emoción
Valor inicialneutralEmpieza con una expresión neutra
Reglas de comportamientoLa expresión del personaje que habla. Solo una de estas: neutral, happy, sad, angry, surprised, shy. Actualízala cada vez que cambie la expresión.Al enumerar las emociones, la IA no se inventa expresiones para las que no tienes sprite

Variable 4: Mostrar elecciones ​

CampoValorPor qué
Nombre de la variableshow_choicesLa IA escribe [show_choices: set true] para mostrar los botones de elección
TipoInterruptorSolo dos estados: mostrar u ocultar
Valor inicialDesactivadoLos botones de elección están ocultos por defecto
Reglas de comportamientoActívalo cuando le ofrezcas una elección al jugador. Si no, déjalo desactivado.Le dice a la IA que solo lo active cuando haga falta que el jugador elija

No tienes que enseñarle a la IA el formato [nombre: set valor]; eso lo hace el motor. Las Reglas de comportamiento solo dicen cuándo cambia cada valor.

La IA nunca ejecuta código

La IA escribe directivas, el motor las aplica a las variables y el código de la interfaz lee las variables y redibuja: la IA escribe directivas → el motor las analiza → las variables se actualizan → el código de la interfaz vuelve a pintar.


Paso 2: Añade una entrada de lore con las instrucciones de la novela visual ​

La IA tiene que saber que está escribiendo para una pantalla de novela visual y cómo dar formato a su texto.

En el bloque Personaje y mundo del lienzo (se envía en cada turno), pulsa el + de arriba a la derecha del bloque y añade una entrada:

CampoValorPor qué
NombreInstrucciones de la novela visualPara que tú la reconozcas
Cuándo lo ve la IAPersonaje y mundo (en cada turno)Las reglas de formato valen para todas las respuestas

Contenido:

[Modo novela visual]
Estás generando contenido para un motor de novela visual. Cada respuesta tiene que incluir directivas para controlar la pantalla.

Reglas de formato:
1. Prepara la escena con directivas al principio de tu respuesta:
   [current_bg: set "referenciaDelFondo"]
   [current_speaker: set "nombreDelPersonaje"]
   [speaker_emotion: set "emoción"]

2. Formato del texto:
   - *Texto en cursiva* = narración o monólogo interior. Úsalo para describir el entorno, las acciones de los personajes y sus pensamientos.
   - Texto normal (sin formato) = diálogo, lo que dice el personaje.
   - No pongas el diálogo entre comillas; escribe el texto tal cual.

3. Cuando quieras darle una elección al jugador:
   - Usa [show_choices: set true]
   - Pon las opciones al final del texto con este formato:
     A) Texto de la opción
     B) Texto de la opción
     C) Texto de la opción

4. Cada respuesta debe tener un solo fragmento de escena (de 3 a 5 frases). Mantén un ritmo ágil, como en una novela visual de verdad.

5. Emociones disponibles: neutral, happy, sad, angry, surprised, shy

6. Actualiza siempre current_bg al cambiar de escena. Actualiza siempre current_speaker y speaker_emotion cuando hable un personaje.

La IA no sabe cómo lee tu código de interfaz su texto, así que explícale bien el formato: cursiva para la narración, texto normal para el diálogo. Si no, puede que dé formato a su aire y el código no sepa distinguir la narración del diálogo.


Paso 3: Prepara y sube los recursos ​

Una novela visual necesita imágenes de fondo y sprites de personajes. Hay dos formas de darlos:

  • Opción A (recomendada): súbelos a la biblioteca de recursos de Yumina y usa referencias @asset:. Son estables y no caducan
  • Opción B: usa URLs de imágenes externas con https:// (tu propio servidor, un alojamiento de imágenes). Es más sencillo, pero el enlace se puede romper

Subir recursos a Yumina ​

  1. Abre Paneles → Recursos
  2. Arrastra y suelta ahí tus archivos de imagen, o pulsa Subir
  3. Cada archivo subido recibe una referencia @asset: (como @asset:a1b2c3d4-e5f6-7890)
  4. Usa Copiar Ref en un recurso para copiar su referencia

¿Qué es una referencia @asset:? Es el identificador interno de los recursos de Yumina. En el código de interfaz, <img src="@asset:xxx" /> y las @asset: dentro de estilos en línea se convierten en URLs reales de la CDN al dibujarse, y api.resolveAssetUrl("@asset:xxx") te da la URL como texto. Las variables también pueden guardar valores @asset:.

Recursos que conviene preparar ​

Fondos (se recomienda proporción 16:9, de 1920×1080 o más):

EscenaNombre de archivo sugeridoPara qué
Aula (de día)classroom_morning.jpgClase, escenas de conversación
Pasillo del institutohallway.jpgEscenas de transición
Calle (al atardecer)street_evening.jpgEscenas después de clase
Dormitorio (de noche)room_night.jpgEscenas nocturnas

Después de subirlos, apunta la referencia @asset: de cada fondo. Las pondrás en la entrada de lore para que la IA sepa qué referencia va con cada escena.

Sprites de personajes (se recomienda PNG transparente, de 1000 px de alto o más):

Prepara un sprite por cada personaje y expresión. Ponles nombres coherentes, nombrePersonaje_emoción.png, para que sea fácil emparejarlos en el Paso 5.

PersonajeNombre de archivo de ejemploReferencia de ejemplo
Yuki (contenta)yuki_happy.png@asset:abc123...
Yuki (triste)yuki_sad.png@asset:def456...
Profesor (neutral)teacher_neutral.png@asset:ghi789...

Dile a la IA qué fondos usar ​

Añade una tabla de referencias a la entrada de instrucciones del Paso 2, para que la IA sepa qué referencia @asset: va con cada escena. Los sprites no van aquí: la IA solo pone quién habla y la emoción, y es tu código el que elige el sprite.

[Tabla de referencias de fondos]
- Aula de día: @asset:your-classroom-reference
- Pasillo del instituto: @asset:your-hallway-reference
- Calle al atardecer: @asset:your-street-reference
- Dormitorio de noche: @asset:your-bedroom-reference

Al cambiar de fondo, usa las referencias de arriba como valores. Por ejemplo:
[current_bg: set "@asset:your-classroom-reference"]

Ahora vuelve a current_bg y pon en su Valor inicial la referencia del aula.

¿Aún no tienes recursos? Puedes probarlo igual

El código de la interfaz muestra un fondo de color liso cuando no hay imagen. Haz que funcione la lógica primero y añade los recursos después. Para prototipos rápidos sirven URLs https:// de bancos de imágenes.


Paso 4: Escribe la apertura ​

La apertura es la primera escena de la novela visual. En el lienzo, haz clic en la fila de la apertura y escríbela:

*El primer día de abril. Los últimos coletazos de la temporada de los cerezos.*

*Abres la puerta del aula. Te llega el olor de siempre a tiza y a madera. Casi todos los pupitres siguen vacíos: faltan diez minutos para que empiece la clase.*

*En el sitio de la ventana, una chica a la que no habías visto nunca mira hacia fuera en silencio.*

*¿Una alumna nueva? No recuerdas a nadie como ella en tu clase.*

La apertura no pone variables con directivas. Una sesión nueva empieza desde el Valor inicial de cada variable, y por eso current_bg ya apunta al aula. Si añades más aperturas que empiezan en otro sitio, arrastra una apertura sobre una variable en el lienzo para darle su propio valor inicial con esa apertura (mira Apertura y lore).


Paso 5: Escribe el código de interfaz de la novela visual ​

El código de la interfaz se queda con la pantalla entera. En lugar de meter <Chat />, pinta él mismo el fondo, los sprites, el cuadro de diálogo y los botones de elección. Lo que escribe el jugador se recoge con <MessageInput />, colocado en la parte de abajo de la pantalla.

Abre Paneles → Código de frontend → index.tsx y sustituye su contenido por lo siguiente. Rellena SPRITES con tus propias referencias del Paso 3:

tsx
// Sprite for each "name_emotion" (lowercase name). Copy the refs from Panels → Assets.
const SPRITES = {
  yuki_neutral: "@asset:your-yuki-neutral-reference",
  yuki_happy: "@asset:your-yuki-happy-reference",
  yuki_sad: "@asset:your-yuki-sad-reference",
  teacher_neutral: "@asset:your-teacher-reference",
};

export default function MyWorld() {
  const api = useYumina();
  const renderMarkdown = api.renderMarkdown;
  const msgs = api.messages || [];
  const lastMsg = msgs[msgs.length - 1];
  const content = lastMsg ? String(lastMsg.content || "") : "";

  // ---- Read variables ----
  const bgRef = String(api.variables.current_bg || "");
  const bgUrl = bgRef ? api.resolveAssetUrl(bgRef) : "";
  const speaker = String(api.variables.current_speaker || "Narrator");
  const emotion = String(api.variables.speaker_emotion || "neutral");
  const showChoices = Boolean(api.variables.show_choices);

  // ---- Clean content: strip directive lines, keep only narrative text ----
  const cleanContent = content
    .split("\n")
    .filter((line) => !line.trim().match(/^\[.+:\s*(set|add|subtract|multiply|toggle|append|merge|push|delete)\s+.+\]$/) && !line.trim().match(/^\[.+:\s*[+-]?\d+\]$/))
    .join("\n")
    .trim();

  // ---- Parse text: distinguish narration (italic) from dialogue (plain text) ----
  // Split text into paragraphs and classify each one
  const paragraphs = cleanContent
    .split("\n\n")
    .map((p) => p.trim())
    .filter((p) => p.length > 0);

  const parsed = paragraphs.map((p) => {
    // If the entire paragraph is wrapped in *, or every line starts with *, it's narration
    const isNarration = /^\*[^*].*[^*]\*$/.test(p.trim())
      || p.trim().startsWith("*");
    // Check if it's a choice line (A) B) C) format)
    const isChoice = /^[A-Z]\)\s/.test(p.trim());
    return { text: p, isNarration, isChoice };
  });

  // ---- Sprite (looked up from the character name and emotion) ----
  const spriteRef = speaker !== "Narrator"
    ? SPRITES[`${speaker.toLowerCase()}_${emotion}`]
    : null;
  const spriteUrl = spriteRef ? api.resolveAssetUrl(spriteRef) : null;

  // ---- Extract choices ----
  const choices = parsed
    .filter((p) => p.isChoice)
    .map((p) => p.text.replace(/^[A-Z]\)\s*/, ""));

  // ---- Render ----
  return (
    <div style={{
      position: "relative",
      width: "100%",
      minHeight: "500px",
      borderRadius: "12px",
      overflow: "hidden",
      background: "#000",
    }}>
      {/* ===== Background layer ===== */}
      <div style={{
        position: "absolute",
        inset: 0,
        backgroundImage: bgUrl ? `url(${bgUrl})` : "linear-gradient(135deg, #1e293b, #0f172a)",
        backgroundSize: "cover",
        backgroundPosition: "center",
        filter: "brightness(0.7)",
        transition: "background-image 0.8s ease",
      }} />

      {/* ===== Character sprite layer ===== */}
      {spriteUrl && (
        <div style={{
          position: "absolute",
          bottom: "120px",
          left: "50%",
          transform: "translateX(-50%)",
          zIndex: 2,
          transition: "opacity 0.5s ease",
        }}>
          <img
            src={spriteUrl}
            alt={`${speaker} - ${emotion}`}
            style={{
              maxHeight: "350px",
              objectFit: "contain",
              filter: "drop-shadow(0 4px 12px rgba(0,0,0,0.5))",
            }}
            onError={(e) => { e.target.style.display = "none"; }}
          />
        </div>
      )}

      {/* ===== Dialogue box layer ===== */}
      <div style={{
        position: "absolute",
        bottom: 0,
        left: 0,
        right: 0,
        zIndex: 3,
        background: "linear-gradient(transparent, rgba(0,0,0,0.85) 30%)",
        padding: "60px 24px 24px",
      }}>
        {/* Character name label */}
        {speaker !== "Narrator" && (
          <div style={{
            display: "inline-block",
            padding: "4px 16px",
            marginBottom: "8px",
            background: "rgba(99,102,241,0.8)",
            borderRadius: "6px 6px 0 0",
            color: "#e0e7ff",
            fontSize: "14px",
            fontWeight: "bold",
            letterSpacing: "0.05em",
          }}>
            {speaker}
          </div>
        )}

        {/* Text content */}
        <div style={{
          background: "rgba(15,23,42,0.9)",
          borderRadius: speaker !== "Narrator" ? "0 12px 12px 12px" : "12px",
          padding: "16px 20px",
          border: "1px solid rgba(148,163,184,0.2)",
          minHeight: "80px",
        }}>
          {parsed
            .filter((p) => !p.isChoice)
            .map((p, i) => (
              <p key={i} style={{
                margin: i > 0 ? "10px 0 0" : "0",
                color: p.isNarration ? "#94a3b8" : "#e2e8f0",
                fontStyle: p.isNarration ? "italic" : "normal",
                fontSize: "15px",
                lineHeight: 1.8,
              }}
              dangerouslySetInnerHTML={{
                __html: renderMarkdown(
                  p.isNarration
                    ? p.text.replace(/^\*|\*$/g, "")
                    : p.text
                ),
              }}
              />
            ))
          }
        </div>
      </div>

      {/* ===== Choice button layer ===== */}
      {showChoices && choices.length > 0 && (
        <div style={{
          position: "absolute",
          top: "50%",
          left: "50%",
          transform: "translate(-50%, -50%)",
          zIndex: 4,
          display: "flex",
          flexDirection: "column",
          gap: "10px",
          width: "80%",
          maxWidth: "400px",
        }}>
          {choices.map((choice, i) => (
            <button
              key={i}
              onClick={() => {
                api.setVariable("show_choices", false);
                api.sendMessage(choice);
              }}
              style={{
                padding: "14px 20px",
                background: "rgba(30,27,75,0.9)",
                border: "1px solid rgba(99,102,241,0.6)",
                borderRadius: "10px",
                color: "#c7d2fe",
                fontSize: "15px",
                fontWeight: "600",
                cursor: "pointer",
                textAlign: "left",
                transition: "all 0.2s ease",
                backdropFilter: "blur(8px)",
              }}
              onMouseEnter={(e) => {
                e.target.style.background = "rgba(67,56,202,0.8)";
                e.target.style.borderColor = "#818cf8";
              }}
              onMouseLeave={(e) => {
                e.target.style.background = "rgba(30,27,75,0.9)";
                e.target.style.borderColor = "rgba(99,102,241,0.6)";
              }}
            >
              {choice}
            </button>
          ))}
        </div>
      )}

      {/* ===== Player input layer ===== */}
      {/* No <Chat /> wrapper — drop <MessageInput /> in directly so the player can still type */}
      <div style={{
        position: "absolute",
        bottom: 0,
        left: 0,
        right: 0,
        zIndex: 5,
      }}>
        <MessageInput />
      </div>
    </div>
  );
}

Explicación bloque a bloque:

  • Limpiar el contenido: cleanContent quita las líneas de directivas como [current_bg: set "xxx"] (reconoce todas las operaciones: set/add/subtract/multiply/toggle/append/merge/push/delete, además de las directivas abreviadas como [hp: -10]). El motor ya ha procesado las directivas, así que el componente no tiene por qué mostrarlas
  • Analizar los párrafos: divide el texto en párrafos por las líneas en blanco y clasifica cada uno como narración (empieza por *), diálogo (texto normal) o elección (empieza con el formato A))
  • Capa de fondo: usa backgroundImage para mostrar el fondo de la escena actual. filter: brightness(0.7) lo oscurece un poco para que el texto de delante se lea bien. transition añade un fundido al cambiar de fondo
  • Capa de sprites: busca el sprite en SPRITES según speaker y emotion. onError oculta las imágenes que no cargan. No se muestra ningún sprite en modo Narrator ni cuando no hay entrada para ese nombre y esa emoción
  • Capa del cuadro de diálogo: un cuadro semitransparente en la parte de abajo. Cuando speaker no es «Narrator», aparece una etiqueta con el nombre del personaje encima del cuadro. La narración sale en gris y en cursiva; el diálogo, en blanco y con letra normal
  • Capa de botones de elección: cuando show_choices es true y el texto tiene opciones con el formato A) B) C), aparecen botones en el centro de la pantalla. Al pulsar uno se ocultan las opciones (show_choices pasa a false) y se envía lo que eligió el jugador

Cómo se busca el sprite

Las claves de SPRITES son el nombre del personaje en minúsculas, un guion bajo y la emoción: yuki_happy. Añade una fila por cada sprite que hayas subido. Si el nombre de un personaje no es ASCII simple, usa la clave que quieras y dile a la IA que escriba exactamente ese nombre en current_speaker. Un valor también puede ser una URL https://.


Paso 6: Pantalla completa o chat con burbujas propias ​

El código de arriba devuelve su propia disposición, así que ocupa toda la pantalla. El elemento raíz usa width: "100%" y minHeight: "500px"; usa 100vh si quieres que llene siempre la ventana. No hay un interruptor aparte de pantalla completa.

Los dos patrones, uno al lado del otro:

  • Chat normal + burbuja propia: return <Chat renderBubble={...} />. Conservas el chat de la plataforma y solo cambias el estilo de las burbujas
  • Novela visual pura (esta receta): return <div>...todos los elementos de la novela visual...<MessageInput /></div>. Nada de <Chat />; tu código lo dibuja todo

Cambiar entre chat y novela visual: si quieres «chat normal casi siempre y novela visual en momentos especiales», ramifica dentro del código de la interfaz según una variable (por ejemplo vn_mode). Devuelve <Chat /> cuando esté desactivada y la disposición de novela visual cuando esté activada. La IA o un comportamiento cambian la variable, y la pantalla cambia en mitad de la sesión.


Paso 7: Cómo mueve la IA la pantalla, con ejemplos de directivas ​

Así cambian la pantalla las respuestas de la IA durante la partida.

Escena 1: Apertura (modo Narrator)

Respuesta de la IA:

[current_bg: set "@asset:your-classroom-reference"]
[current_speaker: set "Narrator"]
[speaker_emotion: set "neutral"]

*Una mañana de abril. El aire trae el olor dulce de los cerezos en flor.*

*Entras en el aula y ves junto a la ventana a una chica que no conoces. Tiene la barbilla apoyada en la mano y mira hacia fuera, absorta.*

Lo que se ve: fondo del aula + ningún sprite + narración en cursiva gris.

Escena 2: Diálogo de un personaje

Respuesta de la IA:

[current_speaker: set "Yuki"]
[speaker_emotion: set "surprised"]

*Parece darse cuenta de que la miras y gira la cabeza.*

Ah, hola. ¿Tú también eres de esta clase?

[speaker_emotion: set "shy"]

Perdona, es que me han trasladado hoy... Todavía no conozco a nadie.

Lo que se ve: el fondo no cambia (si no hay directiva current_bg, se queda el valor anterior) + el sprite de Yuki con la última expresión de la respuesta (shy, tímida) + el cuadro de diálogo con el nombre «Yuki» + narración en cursiva y diálogo en letra normal, alternándose.

Escena 3: Darle una elección al jugador

Respuesta de la IA:

[current_speaker: set "Narrator"]
[show_choices: set true]

*Yuki te mira con un punto de expectación en los ojos.*

*¿Qué haces?*

A) Te presentas y empiezas a hablar con ella
B) Le haces un gesto con la cabeza y vuelves a tu sitio
C) Te ofreces a enseñarle el aula y el instituto

Lo que se ve: el texto de narración + tres botones en el centro de la pantalla. Cuando el jugador pulsa uno, los botones desaparecen y el texto elegido se envía a la IA como respuesta del jugador.

Escena 4: Cambio de escena

Respuesta de la IA:

[current_bg: set "@asset:your-hallway-reference"]
[current_speaker: set "Narrator"]

*Suena el timbre. El pasillo se llena de vida al instante, con alumnos saliendo en parejas y en grupitos.*

[current_speaker: set "Yuki"]
[speaker_emotion: set "happy"]

¿Comemos juntas en la azotea? He encontrado un sitio muy bonito.

Lo que se ve: el fondo pasa al pasillo (con un fundido) + narración + el sprite contento de Yuki + diálogo.


Paso 8: Narración en cursiva o diálogo en texto normal: reglas de análisis ​

El código de la interfaz distingue dos tipos de texto con una regla sencilla:

FormatoSe reconoce comoCómo se muestraPara qué
*Esto es texto en cursiva*NarraciónGris (#94a3b8), en cursivaDescripciones del entorno, acciones de los personajes, monólogo interior
Esto es texto normalDiálogoBlanco (#e2e8f0), letra normalLo que dice el personaje
A) Texto de la opciónElecciónBotónUna opción que el jugador puede pulsar

A la IA ya se le han dado estas reglas en la entrada de lore. Si alguna vez se equivoca de formato (por ejemplo, pone diálogo en cursiva), todo lo que no empieza por * se trata como diálogo, así que no se rompe nada.

¿Por qué cursiva y no citas con > o **negrita**? La mayoría de los modelos ya usan la cursiva para la narración y las acciones en el rol, así que es el formato que siguen con más fiabilidad.


Paso 9: Guarda y prueba ​

  1. Pulsa Guardar en la barra superior
  2. Pulsa Jugar para empezar una prueba (las pruebas gastan saldo)
  3. Deberías ver la novela visual a pantalla completa: fondo + cuadro de diálogo + la narración de la apertura
  4. Escribe un mensaje en el cuadro de texto (por ejemplo, «Saludarla»)
  5. La respuesta de la IA debería traer directivas: puede cambiar el fondo, aparecer un personaje y salir diálogo en el cuadro
  6. Si la IA ofrece opciones, aparecen botones en el centro de la pantalla. Pulsa uno para probarlo
  7. Sigue la conversación y fíjate en si la IA actualiza current_bg al cambiar de escena, y current_speaker y speaker_emotion cuando hablan los personajes. Este turno, en la prueba, te dice por qué un valor cambió o no

Si algo falla:

SíntomaCausa probableSolución
El fondo sale negro o con un degradado lisocurrent_bg está vacía o no es una referencia de imagen válidaMira el valor de la variable en la prueba. Tiene que ser una referencia @asset: de Paneles → Recursos o una URL https://
No se ve ningún spriteNo hay entrada en SPRITES para ese nombre y esa emociónComprueba que la clave sea el nombre en minúsculas + _ + la emoción (yuki_happy) y que la referencia sea correcta
Las líneas de directivas salen en pantallaEl formato de la directiva no es el estándar y la expresión regular no la reconoceComprueba que el formato sea [nombreVariable: set "valor"], con un espacio después de los dos puntos
Todo sale como narración o todo como diálogoLa IA no sigue las reglas de formatoComprueba que las instrucciones de formato de la entrada de lore estén claras. Puedes reforzarlas en las Reglas de comportamiento
No aparecen los botones de elecciónshow_choices no se puso en true, o no hay opciones con el formato A)Comprueba que la respuesta de la IA incluya [show_choices: set true] y opciones con el formato A)
La pantalla no está a pantalla completaEl elemento raíz no llena la zona visibleAñade minHeight: "100vh" o height: "100%" al div más exterior
No se dibuja nadaUn error de compilaciónPaneles → Código de frontend muestra Error con el mensaje; arregla la línea que indica

Consejos avanzados ​

Diálogo con varios personajes ​

Puedes cambiar entre varios personajes en la misma respuesta:

[current_speaker: set "Yuki"]
[speaker_emotion: set "happy"]
¡Qué buen día hace hoy!

[current_speaker: set "Teacher"]
[speaker_emotion: set "neutral"]
Bueno, todo el mundo, empieza la clase. Sentaos, por favor.

[current_speaker: set "Narrator"]
*El aula se queda en silencio al instante.*

El motor las aplica en orden, así que al final la pantalla muestra el sprite del último current_speaker. Para mostrar cada fragmento con el sprite de su personaje, cambia el código para que, en cada párrafo, lea la directiva [current_speaker: set ...] más cercana que tenga delante (sacándola del texto original del mensaje).

Efectos de transición ​

El CSS de la capa de fondo incluye transition: background-image 0.8s ease, que le da un fundido a los cambios de fondo. También puedes usar transiciones distintas según el tipo de escena:

  • Cambio normal: fundido (ya está hecho)
  • Flashback o recuerdo: añade un destello blanco por encima
  • Escena tensa: añade una animación de temblor de pantalla

Con sonido y BGM ​

Dale a cada escena su propia música. Puedes usar BGM condicional en las pistas de audio (que suene el tema del pasillo mientras current_bg sea la referencia del pasillo; mira Diseño de audio), o añadir un comportamiento: Cuándo se dispara → Una variable cambia sobre current_bg, SOLO SI current_bg es igual a la referencia de la escena, efecto Reproducir música. La receta del ciclo día-noche usa la misma idea para la hora del día.


Referencia rápida ​

Lo que quieresCómo hacerlo
Cambiar el fondoLa IA envía [current_bg: set "@asset:..."]
Cambiar quién hablaLa IA envía [current_speaker: set "nombreDelPersonaje"]
Cambiar la expresiónLa IA envía [speaker_emotion: set "emoción"]
Mostrar botones de elecciónLa IA envía [show_choices: set true] + opciones con el formato A) B) C)
Distinguir la narración del diálogo*cursiva* = narración, texto normal = diálogo
Novela visual a pantalla completaindex.tsx devuelve directamente la disposición de la novela visual (sin <Chat />), con <MessageInput /> abajo para que escriba el jugador
Sprites de personajesSúbelos a Paneles → Recursos y pon sus referencias en SPRITES como nombre_emoción
Enviar un mensaje cuando el jugador pulsa una opciónEl onClick del botón llama a api.sendMessage(textoDeLaOpción)
Retratos, un fondo que sigue a una variable y botones de elección, sin códigoEntrada de lore → Retrato…; parte Imagen de la interfaz del jugador puesta en Sigue a una variable; botones de la interfaz del jugador