pixi-vn-narration

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Pixi'VN Narration module

Pixi'VN Narration 模块

When to use this skill

何时使用此 skill

Use this skill whenever the task is about the story itself: creating a new scene/label, writing a line of dialogue, adding a menu of player choices, branching a label's content based on game state, or driving story progression (starting the game, moving to the next step, calling/jumping between labels). This is the core engine of Pixi'VN — almost every "write a scene" or "add a choice" request touches this module.
Do not use this skill for: character definitions/appearance (see
pixi-vn-characters
), undo/go-back mechanics (see
pixi-vn-history
), save files (see
pixi-vn-saves
), or persistent game variables (see
pixi-vn-storage
).
当任务涉及故事本身时,请使用此 skill:创建新的场景/label、 编写一行对话、添加玩家选项菜单、根据游戏状态分支处理某个 label 的内容, 或驱动故事推进(启动游戏、前进到下一步、在 label 之间调用/跳转)。这是 Pixi'VN 的核心引擎——几乎每个“编写场景”或“添加选项”的请求 都会涉及此模块。
请勿将此 skill 用于:角色定义/外观(参见
pixi-vn-characters
)、撤销/返回 机制(参见
pixi-vn-history
)、存档(参见
pixi-vn-saves
),或持久化游戏变量 (参见
pixi-vn-storage
)。

Optional runtime AI:
@drincs/pixi-vn-ai

可选运行时 AI:
@drincs/pixi-vn-ai

For dialogue or images generated during play, install the separate
@drincs/pixi-vn-ai
package and its required peers; import its
ai
singleton from that package. This API is experimental: check the installed types before implementing provider-specific options.
Await
ai.init(...)
once during startup. Without options it downloads a local WebLLM text model in the browser; image generation still needs an external provider. Configure
textProvider
and/or
imageProvider
for AI SDK models, or use
ComfyUIImageModel
from
@drincs/pixi-vn-ai/comfyui
with a ComfyUI API-format workflow for images.
In an async label step, generate the text and assign it to the usual dialogue state:
ts
import { narration } from "@drincs/pixi-vn";
import { ai } from "@drincs/pixi-vn-ai";

// Inside an async step, after startup has awaited ai.init(...):
narration.dialogue = await ai.text.generateDialog(
  "Describe the empty station as the last train leaves.",
  { scene: "A rural station at dusk", language: "English", history: true },
);
ai.image.generateBackground
and
ai.image.generateElement
generate scene imagery; the library builds prompts from the request and structured context. Consult the AI guide and plugin README for dependencies, provider configuration, image results, and display helpers before wiring images into canvas.
对于游戏过程中生成的对话或图像,请安装独立的
@drincs/pixi-vn-ai
package 及其必需的 peer dependencies,并从该 package 导入
ai
singleton。 此 API 属于实验性功能:实现特定 provider 的选项前,请检查已安装的 types。
在启动期间,只需执行一次
await ai.init(...)
。不传入 options 时,它会在浏览器中下载本地 WebLLM 文本模型;图像生成仍需要外部 provider。为 AI SDK 模型配置
textProvider
和/或
imageProvider
,或使用
@drincs/pixi-vn-ai/comfyui
中的
ComfyUIImageModel
,并为图像提供采用 ComfyUI API 格式的工作流。
在异步 label step 中,生成文本并将其赋给常规对话状态:
ts
import { narration } from "@drincs/pixi-vn";
import { ai } from "@drincs/pixi-vn-ai";

// Inside an async step, after startup has awaited ai.init(...):
narration.dialogue = await ai.text.generateDialog(
  "Describe the empty station as the last train leaves.",
  { scene: "A rural station at dusk", language: "English", history: true },
);
ai.image.generateBackground
和
ai.image.generateElement
可生成场景图像; 该 library 会根据 request 和 structured context 构建 prompts。在将图像接入 canvas 前, 请查阅 AI guide 和 plugin README,了解 dependencies、 provider 配置、图像结果和 display helpers。

1. Defining a label

1. 定义 label

A label is the Pixi'VN equivalent of a Ren'Py "bookmark"/landmark in the story (the same concept is called a
knot
in the ink language): a container for an ordered list of steps (plain functions), each one run when the player advances the story. Labels are created with
newLabel
and are automatically registered so they can be referenced by id anywhere (choices,
narration.call
,
narration.jump
,
Game.start
).
ts
import { narration, newLabel } from "@drincs/pixi-vn";

const startLabel = newLabel("start", [
  () => {
    narration.dialogue = "Welcome to the game!";
  },
  () => {
    narration.dialogue = { character: "liam", text: "Hi, I'm Liam." };
  },
]);
Key points:
  • newLabel(id, steps, props?)
    —
    id
    must be unique across the whole game;
    steps
    is an array of
    StepLabelType
    functions, executed in order as the player continues;
    props
    is optional (
    onStepStart
    ,
    onStepEnd
    ,
    onLoadingLabel
    hooks — rarely needed for basic authoring).
  • Each step is
    (props, { labelId }) => StepLabelResultType | Promise<StepLabelResultType>
    . Steps can be
    async
    when they need to
    await
    something (e.g. calling another label, loading assets). Return value is optional — most steps return nothing (
    void
    ); returning a string/object is only needed by code that consumes
    StepLabelResultType
    explicitly.
  • Every label file should call
    newLabel(...)
    at module scope so the label registers itself simply by being imported. Make sure the module gets imported somewhere reachable at startup.
  • steps
    can also be a function that returns the array, e.g.
    () => [...]
    , which lets step content change dynamically based on game state — see "Conditional branching" below.
一个 label 是 Pixi'VN 中与 Ren'Py 的“书签”/故事中的“地标”等价的概念(同一概念 在 ink language 中称为
knot
):它是按顺序排列的 steps(普通函数)的容器, 玩家推进故事时,其中每个 step 都会运行。Labels 使用
newLabel
创建, 并会自动注册,因此可以在任何位置通过 id 引用(choices、
narration.call
、
narration.jump
、
Game.start
)。
ts
import { narration, newLabel } from "@drincs/pixi-vn";

const startLabel = newLabel("start", [
  () => {
    narration.dialogue = "Welcome to the game!";
  },
  () => {
    narration.dialogue = { character: "liam", text: "Hi, I'm Liam." };
  },
]);
要点:
  • newLabel(id, steps, props?)
    ——
    id
    必须在整个 game 中保持唯一;
    steps
    是一个
    StepLabelType
    functions 数组,会在玩家继续游戏时按顺序执行;
    props
    为可选参数 (包含
    onStepStart
    、
    onStepEnd
    、
    onLoadingLabel
    hooks——基础创作很少需要使用)。
  • 每个 step 的签名是
    (props, { labelId }) => StepLabelResultType | Promise<StepLabelResultType>
    。 当 step 需要
    await
    某些内容(例如调用另一个 label、加载 assets)时,可以使用
    async
    。 返回值是可选的——大多数 step 不返回任何内容(
    void
    );只有当代码显式消费
    StepLabelResultType
    时,才需要返回 string/object。
  • 每个 label file 都应在 module scope 调用
    newLabel(...)
    ,这样只需 import 该 module 即可完成 label 注册。请确保该 module 在启动时能从某处被 import。
  • steps
    也可以是一个返回数组的 function,例如
    () => [...]
    ,这样 step 内容便可根据 game state 动态变化——参见下文的“条件分支”。

2. Dialogue

2. 对话

Set
narration.dialogue
inside a step to display a line. It accepts either a plain string/array of strings, or a
DialogueInterface
-shaped object with
text
and an optional
character
:
ts
// Narrator line (no character)
narration.dialogue = "He thrusts out his hand.";

// A line with a speaker — character can be a registered character's string id...
narration.dialogue = {
  character: "james",
  text: `You're my roommate's replacement, huh?`,
};

// ...or the character object itself, e.g. imported from your characters module
narration.dialogue = { character: liam, text: "Hi, I'm Liam." };
Which form to prefer depends on the project's setup — see
pixi-vn-characters
for the full rationale, but in short: prefer the instance unless the
@drincs/pixi-vn/vite
plugin's
typeFilePath
generation (narrowing
CharacterIdType
to known ids, see
pixi-vn-getting-started
) is set up, in which case prefer the string-id form — it's just as typo-safe then, without needing to import the character module in every label file.
narration.dialogue
is a getter too: reading it returns
{ text, character }
(with
character
resolved to the full
CharacterInterface
when a registered id was used, or
undefined
/the raw string otherwise).
Gluing dialogue across steps: set
narration.dialogGlue = true
before assigning
narration.dialogue
in a step to have that line's text appended to the current dialogue text instead of replacing it. This lets a paragraph be split across several steps (each free to change a character's pose/sprite in between) while it reads to the player as one continuous block of text, with no waiting/empty-box flash in between. From the official React template (
src/content/labels/second.label.ts
):
ts
async () => {
    await showImageContainer("steph", ["fm02-body", "fm02-eyes-joy", "fm02-mouth-smile00"]);
    narration.dialogue = `She enters my room before I'VE even had a chance to.`;
},
async () => {
    await showImageContainer("steph", ["fm02-body", "fm02-eyes-joy", "fm02-mouth-smile00"]);
    narration.dialogGlue = true;
    narration.dialogue = `\n\n...I could've just come back and gotten the platter later...`;
},
The player still advances one step at a time as usual (
dialogGlue
only affects how the text accumulates, not whether
continue()
/player input is needed); it resets automatically once a non-glued
narration.dialogue
assignment is made.
Character objects (
CharacterBaseModel
and registration via
RegisteredCharacters
) are covered by the
pixi-vn-characters
skill — this module only needs a character's
id
(or the object) to attach it to a line.
在 step 中设置
narration.dialogue
以显示一行内容。它既可以接受普通 string/string 数组, 也可以接受具有
DialogueInterface
结构的 object,其中包含
text
和可选的
character
:
ts
// Narrator line (no character)
narration.dialogue = "He thrusts out his hand.";

// A line with a speaker — character can be a registered character's string id...
narration.dialogue = {
  character: "james",
  text: `You're my roommate's replacement, huh?`,
};

// ...or the character object itself, e.g. imported from your characters module
narration.dialogue = { character: liam, text: "Hi, I'm Liam." };
采用哪种形式取决于项目的配置——完整的取舍理由参见
pixi-vn-characters
, 但简而言之:如果尚未配置
@drincs/pixi-vn/vite
plugin 的
typeFilePath
generation (将
CharacterIdType
收窄到已知 ids,参见
pixi-vn-getting-started
), 请优先使用 instance;如果已经配置,则优先使用 string-id form——后者同样不易拼写错误, 而且无需在每个 label file 中 import character module。
narration.dialogue
同时也是 getter:读取它会返回
{ text, character }
(使用已注册的 id 时,
character
会解析为完整的
CharacterInterface
;否则为
undefined
/原始 string)。
跨 step 拼接对话:在 step 中赋值
narration.dialogue
之前,将
narration.dialogueGlue
设为
true
,即可让该行的文本追加到当前对话文本,而不是替换它。这样可以将一个段落拆分到多个 step 中(每个 step 之间都可以更改角色的 pose/sprite),同时让玩家将其读作一个连续文本块, 中间不会出现等待/空白对话框闪烁。内容来自官方 React template (
src/content/labels/second.label.ts
):
ts
async () => {
    await showImageContainer("steph", ["fm02-body", "fm02-eyes-joy", "fm02-mouth-smile00"]);
    narration.dialogue = `She enters my room before I'VE even had a chance to.`;
},
async () => {
    await showImageContainer("steph", ["fm02-body", "fm02-eyes-joy", "fm02-mouth-smile00"]);
    narration.dialogGlue = true;
    narration.dialogue = `\n\n...I could've just come back and gotten the platter later...`;
},
玩家仍会像平常一样一次推进一个 step(
dialogGlue
只影响文本如何累积, 不影响是否需要
continue()
/玩家输入);一旦进行了非 glued 的
narration.dialogue
赋值, 它就会自动重置。
Character objects(
CharacterBaseModel
以及通过
RegisteredCharacters
进行注册) 由
pixi-vn-characters
skill 覆盖——本模块只需使用 character 的
id
(或对象本身), 即可将其附加到一行对话中。

3. Rich text: Markdown and HTML in dialogue

3. 富文本:对话中的 Markdown 和 HTML

Pixi'VN isn't tied to any markup language for
narration.dialogue
text, but Markdown is the recommended one, and Markdown can be freely mixed with raw HTML for anything Markdown itself can't express. Every official template already renders dialogue this way — in the React template specifically, via
react-markdown
with the
remark-gfm
(GFM tables/strikethrough/etc.) and
rehype-raw
(allows raw HTML inside the Markdown source) plugins:
tsx
import Markdown from "react-markdown";
import rehypeRaw from "rehype-raw";
import remarkGfm from "remark-gfm";

<Markdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]}>
  {narration.dialogue?.text}
</Markdown>;
When writing dialogue text, default to plain Markdown's classic inline styles —
*italic*
,
**bold**
, and similar simple emphasis — rather than reaching for raw HTML/CSS. Only use HTML (or anything fancier) when the user actually asks for it (e.g. a specific color, an animation, an underline a specific character always has):
ts
narration.dialogue = "*She* was **not** amused.";
When HTML is asked for, the recommended CSS approach (per the docs) is Tailwind CSS — already installed and configured in every official template, optionally with plugins like
@tailwindcss/typography
,
tw-animate-css
, or
tailwind-animations
for richer text effects. A Tailwind-styled inline element inside dialogue text must be
inline-block
— a plain
inline
element won't apply
transform
/animation utilities correctly mid-paragraph:
ts
narration.dialogue = `The night was <span class="inline-block animate-pulse text-violet-400">still</span>.`;
Pixi'VN 的
narration.dialogue
文本不限定于某种 markup language,但推荐使用 Markdown, 并且可以自由地将 Markdown 与 raw HTML 混用,以表达 Markdown 本身无法表示的内容。 每个官方 template 都以这种方式渲染对话——具体来说,在 React template 中, 通过带有
remark-gfm
(GFM 表格/删除线等)和
rehype-raw
(允许在 Markdown source 中使用 raw HTML) 插件的
react-markdown
实现:
tsx
import Markdown from "react-markdown";
import rehypeRaw from "rehype-raw";
import remarkGfm from "remark-gfm";

<Markdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]}>
  {narration.dialogue?.text}
</Markdown>;
编写对话文本时,默认采用 plain Markdown 的经典行内样式——
*italic*
、
**bold**
等简单强调——而不是直接使用 raw HTML/CSS。只有当用户明确要求时,才使用 HTML (或更复杂的形式),例如指定颜色、动画,或为某个始终出现的字符添加下划线:
ts
narration.dialogue = "*She* was **not** amused.";
当用户确实要求使用 HTML 时,推荐的 CSS 方案(根据文档)是 Tailwind CSS——每个官方 template 中都已安装并配置, 还可选用
@tailwindcss/typography
、
tw-animate-css
或
tailwind-animations
等 plugins 获得更丰富的文本效果。 对话文本中带 Tailwind 样式的 inline element 必须使用
inline-block
——普通的
inline
element 在段落中间无法正确应用
transform
/animation utilities:
ts
narration.dialogue = `The night was <span class="inline-block animate-pulse text-violet-400">still</span>.`;

4. Choices

4. 选项

A choice menu is created by assigning an array to
narration.choices
, typically built with
newChoiceOption
(opens another label) and/or
newCloseChoiceOption
(just closes the menu and lets the current label's next step run).
ts
import {
  narration,
  newChoiceOption,
  newCloseChoiceOption,
  newLabel,
} from "@drincs/pixi-vn";

const choiceLabel = newLabel("choice", [
  () => {
    narration.dialogue = "What do you choose?";
    narration.choices = [
      newChoiceOption("A", ALabel, {}), // default type is "call"
      newChoiceOption("B", BLabel, {}, { type: "jump" }),
      newCloseChoiceOption("Close"),
    ];
  },
  () => {
    narration.dialogue = "end";
  },
]);
  • newChoiceOption(text, label, props, options?)
    —
    text
    is the option's label text;
    label
    is the target
    Label
    object or its string id;
    props
    are passed to the target label's steps (pass
    {}
    if none are needed);
    options.type
    is
    "call"
    (default — the target runs and can return to this label afterwards) or
    "jump"
    (the current label is closed first, no return).
  • newCloseChoiceOption(text, options?)
    — closes the choice menu without opening any label; narration simply continues into the current label's next step.
    options.closeCurrentLabel: true
    additionally closes the current label when chosen.
  • Other
    ChoiceInterface
    options available in both:
    oneTime
    (option disappears once chosen),
    onlyHaveNoChoice
    (shown only as a fallback if every other option has been removed),
    autoSelect
    (if it ends up being the only available option, it's chosen automatically without player input).
  • Read
    narration.choices.list
    (a
    StoredChoiceInterface[]
    , an indexed array — each option carries a
    choiceIndex
    ), and call
    narration.choices.select(choice, props)
    when the player picks one. Pass through any
    StepLabelProps
    the target label needs plus the option's own stored
    props
    :
ts
const item = narration.choices.list![0];
await narration.choices.select(item, { ...item.props });
choices.select
dispatches to
narration.call
/
narration.jump
/closing the menu based on the chosen option's
type
, and records the pick so
narration.queries.alreadyCurrentStepMadeChoices
/
queries.timesChoiceMade
can reflect past choices (useful for
oneTime
options across replays).
通过给
narration.choices
赋一个数组来创建 choice menu,通常使用
newChoiceOption
(打开另一个 label) 和/或
newCloseChoiceOption
(仅关闭菜单并让当前 label 的下一个 step 继续运行)来构建。
ts
import {
  narration,
  newChoiceOption,
  newCloseChoiceOption,
  newLabel,
} from "@drincs/pixi-vn";

const choiceLabel = newLabel("choice", [
  () => {
    narration.dialogue = "What do you choose?";
    narration.choices = [
      newChoiceOption("A", ALabel, {}), // default type is "call"
      newChoiceOption("B", BLabel, {}, { type: "jump" }),
      newCloseChoiceOption("Close"),
    ];
  },
  () => {
    narration.dialogue = "end";
  },
]);
  • newChoiceOption(text, label, props, options?)
    ——
    text
    是 option 的 label text;
    label
    是目标
    Label
    object 或其 string id;
    props
    会传给目标 label 的 steps (不需要时传
    {}
    );
    options.type
    为
    "call"
    (默认——目标运行完毕后可以返回当前 label) 或
    "jump"
    (先关闭当前 label,不返回)。
  • newCloseChoiceOption(text, options?)
    ——关闭 choice menu,不打开任何 label;narration 随后继续进入当前 label 的下一个 step。
    options.closeCurrentLabel: true
    还会关闭当前 label。
  • 两者都可使用其他
    ChoiceInterface
    options:
    oneTime
    (选项被选择后消失)、
    onlyHaveNoChoice
    (仅当其他所有选项均被移除时作为 fallback 显示)、
    autoSelect
    (如果最终只剩该选项可用,则无需玩家输入即可自动选择)。
  • 读取
    narration.choices.list
    (一个
    StoredChoiceInterface[]
    ,即带索引的 array——每个 option 都带有
    choiceIndex
    ),并在玩家选择时调用
    narration.choices.select(choice, props)
    。 传入目标 label 所需的任何
    StepLabelProps
    ,以及 option 自身存储的
    props
    :
ts
const item = narration.choices.list![0];
await narration.choices.select(item, { ...item.props });
choices.select
会根据所选 option 的
type
,将操作分派到
narration.call
/
narration.jump
/关闭菜单, 并记录选择,使
narration.queries.alreadyCurrentStepMadeChoices
/
queries.timesChoiceMade
能够反映过去的 choices(这有助于在多次重玩中处理
oneTime
options)。

5. Progressing and closing labels

5. 推进和关闭 labels

  • Start the game:
    Game.start(label, props)
    clears all game data and calls the given label — this is the standard entry point, e.g.
    Game.start(startLabel, {})
    .
    label
    can be the
    Label
    object itself (what the docs show) or its string id (
    Game.start("start", {})
    ) — both work,
    Game.start
    is a thin wrapper around
    narration.call
    plus
    Game.clear()
    .
  • Advance one step:
    await narration.continue(props)
    runs the current label's next step. Guard advancing with
    narration.canContinue
    (false while a step is running, or a choice menu / required input is pending) — this is the shape a "next" button/handler should use:
ts
while (narration.canContinue) {
  await narration.continue({});
}
  • Call a sub-label (like a function call — returns to the caller when it runs out of steps):
    await narration.call(otherLabel, props)
    . Can be used both to start the very first label and, from inside a step, to invoke a nested label. If you call it from inside a step, return the result and
    await
    it, so the history bookkeeping stays correct:
ts
async (props) => {
  return await narration.call(otherLabel, props);
};
  • Jump to another label (closes the current label first, no return):
    await narration.jump(otherLabel, props)
    . Same rule applies —
    return await narration.jump(...)
    when called from within a step.
  • A label closes automatically once its steps are exhausted; the previously calling label (if any) resumes automatically. To close explicitly from within a step (e.g. after setting state), just
    return
    — Pixi'VN advances past the label's end on the next
    continue()
    . Use
    narration.labels.closeCurrent()
    /
    narration.labels.closeAll()
    only for lower-level manual stack control (
    closeAll()
    ends the game if nothing is called afterwards).
  • narration.labels.opened
    and
    narration.labels.current
    reflect the current call stack of nested labels, useful for debugging/branching logic.
  • Going back a step is handled by the history module, not
    narration
    — see
    stepHistory.back()
    /
    stepHistory.canGoBack
    in the
    pixi-vn-history
    skill.
  • 启动游戏:
    Game.start(label, props)
    会清除所有 game data 并调用给定 label—— 这是标准 entry point,例如
    Game.start(startLabel, {})
    。
    label
    既可以是
    Label
    object 本身(文档所示),也可以是其 string id(
    Game.start("start", {})
    )——两者都可用;
    Game.start
    本质上是
    narration.call
    加
    Game.clear()
    的轻量包装。
  • 推进一步:
    await narration.continue(props)
    会运行当前 label 的下一个 step。 推进前应使用
    narration.canContinue
    进行防护(step 正在运行,或 choice menu/required input 处于等待状态时为 false)——“下一步”按钮/handler 应采用以下结构:
ts
while (narration.canContinue) {
  await narration.continue({});
}
  • 调用 sub-label(类似 function call——其 steps 耗尽后返回调用方):
    await narration.call(otherLabel, props)
    。它既可用于启动第一个 label,也可从某个 step 内部调用嵌套 label。 如果在 step 内部调用,必须返回结果并
    await
    它
    ,以确保 history bookkeeping 保持正确:
ts
async (props) => {
  return await narration.call(otherLabel, props);
};
  • 跳转到另一个 label(先关闭当前 label,不返回):
    await narration.jump(otherLabel, props)
    。同样,如果从 step 内调用,应使用
    return await narration.jump(...)
    。
  • 当 label 的 steps 耗尽后,它会自动关闭;之前调用它的 label(如有)会自动恢复。 如需从 step 内部显式关闭 label(例如设置完 state 后),只需
    return
    ——Pixi'VN 会在下一次
    continue()
    时推进到 label 末尾之后。只有在更低层的手动栈控制中,才应使用
    narration.labels.closeCurrent()
    /
    narration.labels.closeAll()
    (如果
    closeAll()
    后没有其他调用, 游戏将结束)。
  • narration.labels.opened
    和
    narration.labels.current
    会反映嵌套 labels 当前的调用栈, 便于调试和分支逻辑。
  • 返回上一步由 history module 处理,而不是
    narration
    ——参见
    pixi-vn-history
    skill 中的
    stepHistory.back()
    /
    stepHistory.canGoBack
    。

6. Input prompts

6. 输入提示

To ask the player for a value (string, number, or an HTML element like a textarea), call
narration.input.request
inside a step; the engine will not let the story continue (
narration.canContinue
is
false
) until a value is provided by the UI:
ts
() => {
    narration.dialogue = "What is your name?";
    narration.input.request({ type: "string" });
},
() => {
    narration.dialogue = `My name is ${narration.input.value}`;
},
() => {
    narration.dialogue = "How old are you?";
    narration.input.request({ type: "number" }, 18); // second arg is an optional default value
},
  • narration.input.request(info?, defaultValue?)
    —
    info
    is
    InputInfo
    minus
    isRequired
    (e.g.
    { type: "string" }
    ,
    { type: "number" }
    ,
    { type: "html textarea" }
    ); omit it for a plain text input.
  • narration.input.isRequired
    /
    narration.input.type
    tell the UI layer whether a prompt is pending and what kind, so it can render the right control.
  • narration.input.value
    holds the submitted value once the UI resolves the prompt — read it in a later step to use what the player typed.
  • narration.input.removeRequest()
    cancels a pending request without a value (rarely needed in normal authoring).
若要让玩家提供某个值(string、number,或 textarea 这样的 HTML element),请在 step 中调用
narration.input.request
;在 UI 提供该值之前,engine 不会允许故事继续 (
narration.canContinue
为
false
):
ts
() => {
    narration.dialogue = "What is your name?";
    narration.input.request({ type: "string" });
},
() => {
    narration.dialogue = `My name is ${narration.input.value}`;
},
() => {
    narration.dialogue = "How old are you?";
    narration.input.request({ type: "number" }, 18); // second arg is an optional default value
},
  • narration.input.request(info?, defaultValue?)
    ——
    info
    是
    InputInfo
    减去
    isRequired
    后的结构 (例如
    { type: "string" }
    、
    { type: "number" }
    、
    { type: "html textarea" }
    ); 若要使用普通文本输入,则可省略它。
  • narration.input.isRequired
    /
    narration.input.type
    会告知 UI layer 是否存在待处理的 prompt 以及 prompt 的类型,从而渲染正确的 control。
  • UI 解决该 prompt 后,
    narration.input.value
    会保存已提交的值——可在后续 step 中读取并使用 玩家输入的内容。
  • narration.input.removeRequest()
    可取消一个尚未提供值的待处理 request(正常创作中很少需要)。

7. Conditional branching

7. 条件分支

Because
steps
can be a function, a label's content can change based on stored state — e.g. showing different dialogue on a repeat visit:
ts
import { narration, newLabel, storage } from "@drincs/pixi-vn";

const talkAliceQuest = newLabel("talk-alice-quest", () => {
  if (storage.flags.get("test") === false) {
    return [
      () => {
        narration.dialogue = {
          character: "alice",
          text: "Hi, can you order me a book?",
        };
      },
      () => {
        narration.dialogue = { character: "mc", text: "Ok" };
      },
      async (props) => {
        storage.flags.set("test", true);
        await narration.continue(props);
      },
    ];
  }
  return [
    () => {
      narration.dialogue = { character: "alice", text: "Thanks for the book." };
    },
  ];
});
The function re-runs every time the engine needs to know the label's steps, so branch on stable storage flags/variables (see
pixi-vn-storage
) rather than on anything that changes mid-step. The last step in a branch can
await narration.continue(props)
itself if it wants to immediately auto-advance past a "setup" step instead of waiting for another player input.
由于
steps
可以是一个函数,label 的内容可以根据已存储的状态发生变化——例如,在重复访问时显示不同对话:
ts
import { narration, newLabel, storage } from "@drincs/pixi-vn";

const talkAliceQuest = newLabel("talk-alice-quest", () => {
  if (storage.flags.get("test") === false) {
    return [
      () => {
        narration.dialogue = {
          character: "alice",
          text: "Hi, can you order me a book?",
        };
      },
      () => {
        narration.dialogue = { character: "mc", text: "Ok" };
      },
      async (props) => {
        storage.flags.set("test", true);
        await narration.continue(props);
      },
    ];
  }
  return [
    () => {
      narration.dialogue = { character: "alice", text: "Thanks for the book." };
    },
  ];
});
该函数会在 engine 每次需要获取 label 的 steps 时重新运行,因此应根据稳定的 storage flags/variables (参见
pixi-vn-storage
)进行分支,而不是依据任何会在 step 中途变化的状态。 某个分支的最后一个 step 可以在自身内部
await narration.continue(props)
,以便在等待下一次玩家输入之前, 立即自动推进越过“设置”step。

8. Other useful narration bits

8. 其他有用的 narration 功能

  • Game.onEnd(async (props) => {...})
    runs once every open label's steps are exhausted and nothing else is called — the standard place to navigate to an ending screen, or to
    narration.call
    /
    jump
    back into a menu/loop label if the game should never truly "end".
  • narration.getRandomNumber(...)
    gives a seeded-safe random number (prefer this over raw
    Math.random
    so saves/rollback stay deterministic).
  • narration.queries.isLabelAlreadyCompleted(label | labelId)
    checks whether a label has fully run before — handy for "seen this scene already" branching.
  • narration.currentStepTimesCounter
    counts how many times the current step has executed (only increments when actually read, and only once per step execution); set it to
    0
    to reset. Useful for "show this the first time only" logic inside a dynamic step-list function.
  • Game.onEnd(async (props) => {...})
    会在所有已打开 label 的 steps 耗尽且没有其他调用时运行一次—— 这是跳转到 ending screen 的标准位置;如果游戏永远不应真正“结束”,也可以在这里
    narration.call
    /
    jump
    回 menu/loop label。
  • narration.getRandomNumber(...)
    会生成可安全设种的随机数(应优先于原始
    Math.random
    , 以便 saves/rollback 保持确定性)。
  • narration.queries.isLabelAlreadyCompleted(label | labelId)
    可检查某个 label 是否已完整运行过—— 便于实现“这个场景已经看过”类型的分支。
  • narration.currentStepTimesCounter
    会统计当前 step 已执行多少次(仅在实际读取时递增, 且每次 step 执行只递增一次);将其设为
    0
    即可重置。适用于动态 step-list function 内部的 “仅首次显示”逻辑。

9. Real-world project convention (official React template)

9. 实际项目约定(官方 React template)

The library itself doesn't mandate any file layout or UI wiring — the points below are how
npm create pixi-vn@latest
's official "TS narration + React" template does it, as seen in
pixi-vn-react-template
. Treat this as one convention worth following/recognizing, not a hard requirement: other narration styles (e.g. an ink-based project) will have their own skill and their own conventions.
  • File layout: each scene lives in its own
    src/content/labels/<name>.label.ts
    , exporting a
    newLabel(...)
    result (e.g.
    second.label.ts
    exports
    secondPart = newLabel("second_part", [...])
    ). Character definitions live alongside under
    src/content/characters/
    .
  • Auto-registration:
    src/content/index.ts
    does
    import.meta.glob(["./**/*.ts", "!./index.ts"], { eager: true })
    (a Vite feature) to eagerly import every module under
    content/
    for its side effects at startup. Since
    newLabel
    /character registration runs at module scope, a new label file just needs to exist under
    content/
    — no manual import list to maintain.
  • Driving narration from the UI (see
    lib/hooks/narration-hooks.ts
    and
    lib/query/narration-query.ts
    ): a "next" handler guards on
    narration.canContinue
    before calling
    await narration.continue(gameProps)
    ; a choice handler reads
    narration.choices.list
    and calls
    await narration.choices.select(item, gameProps)
    ; an input prompt reads
    narration.input.isRequired
    /
    narration.input.type
    /
    narration.input.value
    . Real code wraps all three in a loading-state guard (e.g. a
    GameStatus.setLoading(true/false)
    store) so double-clicks can't fire concurrent
    continue()
    /
    choices.select()
    calls, and a combined
    narration.canContinue && !narration.input.isRequired
    is a convenient "can the player press next" check for enabling/disabling a next button.
  • gameProps
    above is just that project's augmented
    StepLabelProps
    (translation function, router navigate, toast, etc., declared via TS module augmentation in
    pixi-vn.d.ts
    ) — it's plain application data, not something the narration module requires.
library 本身并不强制规定 file layout 或 UI wiring——以下要点是
npm create pixi-vn@latest
官方“TS narration + React”template 的做法, 正如
pixi-vn-react-template
中所见。请将其视为值得遵循或识别的一种约定,而不是硬性要求: 其他 narration 风格(例如基于 ink 的项目)会有自己的 skill 和自己的约定。
  • 文件布局:每个 scene 位于其自己的
    src/content/labels/<name>.label.ts
    ,导出
    newLabel(...)
    的结果(例如
    second.label.ts
    导出
    secondPart = newLabel("second_part", [...])
    )。 Character definitions 位于同一目录结构下的
    src/content/characters/
    。
  • 自动注册:
    src/content/index.ts
    通过
    import.meta.glob(["./**/*.ts", "!./index.ts"], { eager: true })
    (一项 Vite 功能)在启动时急切 import
    content/
    下的每个 module,以执行其 side effects。 由于
    newLabel
    /character registration 会在 module scope 运行,因此只需在
    content/
    下创建新的 label file, 无需维护手动 import list。
  • 从 UI 驱动 narration(参见
    lib/hooks/narration-hooks.ts
    和
    lib/query/narration-query.ts
    ):“下一步”handler 会先检查
    narration.canContinue
    ,再调用
    await narration.continue(gameProps)
    ;choice handler 会读取
    narration.choices.list
    并调用
    await narration.choices.select(item, gameProps)
    ;input prompt 会读取
    narration.input.isRequired
    /
    narration.input.type
    /
    narration.input.value
    。 实际代码会用 loading-state guard(例如
    GameStatus.setLoading(true/false)
    store)包裹这三种操作, 从而防止双击并发触发
    continue()
    /
    choices.select()
    调用;而
    narration.canContinue && !narration.input.isRequired
    则是启用或禁用“下一步”按钮时,判断玩家能否继续的便捷检查。
  • 这里的
    gameProps
    只是该项目扩展后的
    StepLabelProps
    (translation function、router navigate、toast 等, 通过
    pixi-vn.d.ts
    中的 TS module augmentation 声明)——它只是普通 application data,并非 narration module 的必需项。

Related skills

相关 skill

pixi-vn-getting-started, pixi-vn-characters, pixi-vn-history, pixi-vn-storage
pixi-vn-getting-started, pixi-vn-characters, pixi-vn-history, pixi-vn-storage