Official docs: pixi-vn.com/ink/character. Assumes
Pixi'VN characters are already defined via CharacterBaseModel
RegisteredCharacters.add(...)— this skill only covers referencing them from.ink.
When to use this skill
Use whenever an .ink line needs to be spoken by a registered character, needs that character's
display name printed inline, needs an emotion-specific character variant, or needs to change a
character's name from inside the story (a naming scene, a nickname reveal, ...).
Attributing a line to a character
mc: Hello, I'm Liam.
<character_id>: <text> — Pixi'VN splits on the first ": " (colon, then a space) in the line and
checks whether the token before it is a known character id. A known id is either one explicitly passed
to importInkText(text, { characters: [...] }) / vitePluginInk({ characters: [...] }), or (the
common case) already present in RegisteredCharacters at conversion time. If the token doesn't match a
known character, the whole line is left as plain narrator text — including the colon — so a typo'd
character id silently becomes garbled narration instead of an error. Because the split requires the
literal ": ", mc:Hello (no space) is not recognized as dialogue attribution.
import { CharacterBaseModel, RegisteredCharacters } from "@drincs/pixi-vn";
export const mc = new CharacterBaseModel("mc", { name: "Liam" });
RegisteredCharacters.add(mc);
If characters are registered after the ink importing/build step runs (a common ordering issue with
the Vite plugin, since vitePluginInk's JSON export happens independently of when your content/
files execute), pass their ids explicitly via vitePluginInk({ characters: [...] }) or
importInkText(text, { characters: [...] }) so the split still recognizes them.
Printing a character's name inline: [characterId]
Hello, [mc].
This is not native ink's {variable} interpolation — it's Pixi'VN's
text-replacement mechanism ([key] in square brackets), applied here to
substitute a character id with that character's current name. It needs one text-replace handler
registered, using the built-in "characterId" validation mode — every official template ships this by
default:
import { TextReplaces } from "@drincs/pixi-vn-ink";
import { RegisteredCharacters } from "@drincs/pixi-vn/characters";
TextReplaces.add((key) => RegisteredCharacters.get(key)?.name, {
name: "character name",
validation: "characterId",
type: "after-translation",
i18nInterpolation: true,
description: "Replaces a character ID with the character's name in the game.",
});
validation: "characterId" means this handler only fires for keys that are currently a registered
character id (checked against RegisteredCharacters) — no regex needed. Because the character's
name can change at runtime (see rename below), [mc] always reflects the current name, not the one
at story-write time.
Character emotions: id@emotion
mc@happy: Hi, I'm Liam. I'm very happy today.
<character_id>@<emotion>: <text> — the whole mc@happy token is checked against
RegisteredCharacters exactly like a plain id above; it resolves because Pixi'VN characters can be
registered under a composite { id, emotion } key (internally addressed the same way, id@emotion):
import { CharacterBaseModel, RegisteredCharacters } from "@drincs/pixi-vn";
export const mc = new CharacterBaseModel("mc", { name: "Liam" });
export const mcHappy = new CharacterBaseModel({ id: "mc", emotion: "happy" }, { name: "Liam happy" });
RegisteredCharacters.add([mc, mcHappy]);
An emotion variant is a full second CharacterBaseModel registration — typically with its own name
override (here identical) plus whatever sprite/portrait data your project's rendering layer keys off of
(see pixi-vn-ink-canvas for showing an emotion-specific sprite alongside the line).
Referencing mc@sad without registering that variant behaves like any other unknown id: the line falls
back to plain narrator text.
Renaming a character from ink: # rename
=== start ===
mc: Hello, I'm [mc].
# request input string
mc: My name is:
# rename mc {_input_value_}
mc: My name is [mc]
-> DONE
# rename <characterId> <newName> is not a built-in hashtag command — every official template
registers it itself, because "which field(s) does renaming touch" is project-specific:
import { characterIdsEnum } from "@/pixi-vn.keys.gen"; // generated by vitePluginPixivn's typeFilePath
import { HashtagCommands } from "@drincs/pixi-vn-ink";
import { RegisteredCharacters } from "@drincs/pixi-vn/characters";
import zod from "zod";
HashtagCommands.add(
async (script) => {
const character = RegisteredCharacters.get(script[1]);
if (character) character.name = script[2];
return true;
},
{
name: "character rename",
description: "Renames a character in the game.\n\n```ink\n# rename <characterId> <newName>\n```",
validation: zod.tuple([zod.literal("rename"), zod.enum(characterIdsEnum), zod.string()]),
},
);
See pixi-vn-ink-hashtag-commands for the add vs addMapper choice and
why this is registered as a plain add handler (it mutates game state directly rather than returning a
PixiVNJsonOperation).
Related
- pixi-vn-ink-text-replacement — the general
[key]mechanism[characterId]is built on. - pixi-vn-ink-hashtag-commands — how
# rename(and any other custom command) is registered. - pixi-vn-ink — knots-as-labels, storage mapping, installation.
- pixi-vn.com/start/character — defining
CharacterBaseModels in JS/TS in the first place.