Pixi'VN Characters
SkillAI & modelsUse when defining visual novel characters for a @drincs/pixi-vn game — creating a character with a name, color, icon and other props, registering it so it can be looked up by id, and referencing it from dialogue/narration. Load this before writing any `CharacterBaseModel` or `RegisteredCharacters` code.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Pixi'VN Characters skill
What this skill tells your AI
The instructions your AI receives, as published by drincs-productions/pixi-vn in skills/characters/SKILL.md and read by ahel’s review.
When to use this skill
Use this skill whenever a task involves creating the characters that appear in a
Pixi'VN game (name, surname, age, color, icon, and any custom fields), storing
them so the engine and narration code can find them by id, or hooking a
character into a narration.dialogue line. It does not cover how dialogue/text
itself is displayed (see the narration skill) or how variables are generally
saved (see the storage skill).
import { CharacterBaseModel, RegisteredCharacters } from "@drincs/pixi-vn";
(Also available from the narrower @drincs/pixi-vn/characters subpath — see pixi-vn-getting-started for when to prefer that.)
Full reference: https://pixi-vn.com/start/character
1. Defining a character
CharacterBaseModel is the built-in character class. Construct it with a
unique string id and a props object (name, surname, age, icon,
color — all optional):
import { CharacterBaseModel } from "@drincs/pixi-vn";
export const liam = new CharacterBaseModel("liam", {
name: "Liam",
surname: "Smith",
age: 25,
icon: "https://example.com/liam.png",
color: "#9e2e12",
});
export const emma = new CharacterBaseModel("emma", {
name: "Emma",
surname: "Johnson",
age: 23,
icon: "https://example.com/emma.png",
color: "#c23b7f",
});
The id is the value used everywhere else in the game (dialogue, lookups,
history) to reference this character, so it must be unique and stable across
saves.
name/surname/age are actually get/set accessors backed by the game's
storage (see CharacterStoredClass, which CharacterBaseModel extends, and
the stored classes doc): the value
passed in the constructor becomes the default, but setting liam.name = "..."
at runtime persists an override in storage that survives save/load and takes
priority over the default. icon and color are plain readonly fields, not
stored/overridable at runtime.
import { liam } from "@/content/characters";
console.log(liam.name); // "Liam"
liam.name = "Liam Smith";
console.log(liam.name); // "Liam Smith" — now persisted in game storage
If a character's id is ever changed between game versions, the engine does
not migrate the storage data from the old id to the new one — treat ids as
permanent once shipped.
Custom character classes with extra fields
If you need fields beyond name/surname/age/icon/color (e.g. a
sprite id or a relationship stat), this is the recommended, docs-endorsed
approach (project templates already ship it as models/Character.ts):
- Create a class
Character extends CharacterStoredClass implements CharacterInterface. - Augment the (otherwise empty)
CharacterInterfacein apixi-vn.d.tsfile viadeclare module "@drincs/pixi-vn"so everyCharacterInterfaceconsumer in the project — includingRegisteredCharacters.get()— sees the richer, project-specific type. - For any field that must persist across saves, back it with a
getter/setter that calls
this.getStorageProperty()/this.setStorageProperty()(inherited fromCharacterStoredClass), the same patternCharacterBaseModeluses internally forname/surname/age.
This is exactly what the official pixi-vn-react-template (the "TS narration
- React" project
npm create pixi-vn@latestscaffolds) ships assrc/models/Character.ts— stored getters/setters forname,surnameandage, and plain (non-stored) fields foricon/color:
// models/Character.ts
import { type CharacterInterface, CharacterStoredClass } from "@drincs/pixi-vn";
export default class Character
extends CharacterStoredClass
implements CharacterInterface
{
constructor(
id: string | { id: string; emotion: string },
props: CharacterProps,
) {
super(
typeof id === "string" ? id : id.id,
typeof id === "string" ? "" : id.emotion,
);
this.defaultName = props.name;
this.defaultSurname = props.surname;
this.defaultAge = props.age;
this.icon = props.icon;
this.color = props.color;
}
// stored — persists across save/load, same pattern as CharacterBaseModel
private defaultName?: string;
get name(): string {
return (
this.getStorageProperty<string>("name") || this.defaultName || this.id
);
}
set name(value: string | undefined) {
this.setStorageProperty<string>("name", value);
}
private defaultSurname?: string;
get surname(): string | undefined {
return this.getStorageProperty<string>("surname") || this.defaultSurname;
}
set surname(value: string | undefined) {
this.setStorageProperty<string>("surname", value);
}
private defaultAge?: number;
get age(): number | undefined {
return this.getStorageProperty<number>("age") || this.defaultAge;
}
set age(value: number | undefined) {
this.setStorageProperty<number>("age", value);
}
// not stored
readonly icon?: string;
readonly color?: string;
}
interface CharacterProps {
name?: string;
surname?: string;
age?: number;
icon?: string;
color?: string;
}
// pixi-vn.d.ts
declare module "@drincs/pixi-vn" {
interface CharacterInterface {
name: string;
surname?: string;
age?: number;
readonly icon?: string;
readonly color?: string;
}
}
2. Registering and looking up characters
Characters are looked up at runtime by id through the RegisteredCharacters
registry (a namespace, not a decorator despite its file name). Calling
RegisteredCharacters.add(...) is required — a CharacterBaseModel (or
custom class) instance that is only exported/constructed but never passed to
add will not be findable by id and will not work in dialogue lookups after a
save/load. Register every character you define once, at module load time
(recommended: import them all at project startup), then reference it by id
elsewhere:
import { RegisteredCharacters } from "@drincs/pixi-vn";
RegisteredCharacters.add(liam, emma);
// or with arrays:
RegisteredCharacters.add([liam, emma]);
Other RegisteredCharacters functions:
RegisteredCharacters.get(id)— returns the character (orundefined, logging a warning if it was never registered).RegisteredCharacters.has(id)— checks whether an id is registered.RegisteredCharacters.values()— returns all registered characters.RegisteredCharacters.keys()— returns all registered ids.RegisteredCharacters.clear()— removes all registered characters (mainly useful in tests).
const liam = RegisteredCharacters.get("liam");
Re-adding an id that's already registered overwrites the previous character (a log message notes this), so registration order matters if two files define the same id.
Real-world project convention (official React template)
The official pixi-vn-react-template (what npm create pixi-vn@latest
scaffolds for the "TS narration + React" setup) defines its characters as
export const instances in src/content/characters.ts, then registers all of
them with a single RegisteredCharacters.add([...]) call at module scope:
// src/content/characters.ts
import Character from "@/models/Character";
import { RegisteredCharacters } from "@drincs/pixi-vn";
export const mc = new Character("mc", { name: "Me" });
export const james = new Character("james", {
name: "James",
color: "#0084ac",
});
export const steph = new Character("steph", {
name: "Steph",
color: "#ac5900",
});
export const sly = new Character("sly", { name: "Sly", color: "#6d00ac" });
RegisteredCharacters.add([mc, james, steph, sly]);
This works with no manual import list to maintain because src/content/index.ts
uses import.meta.glob to import every file under content/ for its side
effects at app startup — so the module-scope RegisteredCharacters.add(...)
call above always runs. That means you never need to remember to import each
new character file yourself (dropping it anywhere under content/ is
enough), but conversely, a character defined (and .added) in a file
outside content/ that nothing else imports never gets its add call
executed, and stays silently unregistered.
This is the official template's convention, not a hard requirement of the
library — RegisteredCharacters.add can be called from any file, as long as
it's guaranteed to run before the character is looked up. Projects without
this glob-import setup must import each character file explicitly instead
(e.g. from a top-level content/index.ts or the app's entry point).
Typed ids (optional)
Character ids are typed as CharacterIdType, which is string by default.
If a project augments the empty PixivnCharacterIds interface (typically
auto-generated by the vitePluginPixivn Vite plugin's typeFilePath option),
CharacterIdType narrows to a union of known ids and typos become compile
errors. Don't assume this augmentation exists unless you see a generated
pixi-vn.gen.d.ts/similar file in the project — if not, treat ids as plain
strings.
3. Emotions
A character id may optionally be paired with an emotion by passing an object instead of a plain string as the constructor's first argument:
export const alice = new CharacterBaseModel("alice", {
name: "Alice",
icon: "https://example.com/alice.png",
color: "#9e2e12",
});
export const angryAlice = new CharacterBaseModel(
{ id: "alice", emotion: "angry" },
{ icon: "https://example.com/angryAlice.png" },
);
RegisteredCharacters.add([alice, angryAlice]);
Internally this stores the character's name/surname/age under a
separate storage key ("<id>@<emotion>") while still falling back to the
base id's stored/default values if an emotion-specific value isn't set — the
base id and its emotion variants share the same underlying character data
except where the emotion variant overrides it:
console.log(alice.name); // "Alice"
alice.name = "Eleonora";
console.log(alice.name); // "Eleonora"
console.log(angryAlice.name); // "Eleonora" — falls back to the base id's value
angryAlice.name = "Angry Eleonora";
console.log(alice.name); // "Eleonora" — unaffected
console.log(angryAlice.name); // "Angry Eleonora" — its own override now set
Use this only if you need per-emotion overrides of a character's stored fields; for swapping the character's on-screen sprite/image per emotion, that's driven by the canvas/image APIs, not this module.
4. Using a character in dialogue
Once registered, pass the character instance (or its id string) as the
character field of a dialogue, e.g. inside a label step:
narration.dialogue = {
character: liam,
text: "Which test do you want to perform?",
};
// or by id:
narration.dialogue = { character: "liam", text: "..." };
Which form to prefer depends on whether the project's @drincs/pixi-vn/vite plugin generates
typed character ids (the typeFilePath option covered in pixi-vn-getting-started, which narrows
CharacterIdType from plain string to a union of the actually-registered ids):
- Without it, prefer passing the instance (
character: liam) — a bare string id has no compile-time typo protection, while the instance is a real variable reference the compiler already checks. - With it, prefer the string-id form (
character: "liam") — it now gets the same compile-time safety (a typo like"liem"fails to typecheck against the generatedCharacterIdTypeunion) without needing to import the character module wherever a dialogue line is written, which keeps label files decoupled from character-definition files.
DialogueInterface.character accepts CharacterInterface | string; when a
string id is stored, the engine resolves it back to the registered character
object (via RegisteredCharacters.get) when the dialogue is read back — so a
character must already be registered by the time its dialogue line runs.
Reading/rendering the resolved dialogue is covered by the narration skill.
Related skills
pixi-vn-getting-started, pixi-vn-narration, pixi-vn-storage
Signals
- GitHub stars
- 145
- Forks
- 9
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
pixi-vn-characters- Source
- github.com/drincs-productions/pixi-vn