--- name: pixi-vn-ink description: "Use when installing/configuring @drincs/pixi-vn-ink, wiring vitePluginInk and setupInkHmrListener, debugging ink via the Vite dev-server logs/API or the exported JSON, generating ink translation files, or writing/reading core ink syntax (knots, diverts, choices, weave, variables, lists) as it applies to a Pixi'VN project. Index and entry point of the pixi-vn-ink skill set: start here, then follow the skill map to the right pixi-vn-ink-* skill — install it together with the rest of this skill set, not alone." --- # Pixi'VN + ink: skill index and getting started Official docs: [pixi-vn.com/ink](https://pixi-vn.com/ink). Full native-ink language spec (Inkle): bundled at `reference/writing-with-ink.md` in this skill, annotated inline wherever Pixi'VN behaves differently — read this file first, reach for that one only for exhaustive syntax detail or a long worked example (Tower of Hanoi, the crime-scene mystery, ...). ## When to use this skill Use this skill whenever the task touches an `.ink` file or the `@drincs/pixi-vn-ink` package: setting up the Vite plugin, debugging why an `.ink` edit isn't showing up or a hashtag command isn't firing, generating translation files, or writing/reviewing plain ink narrative structure (knots, diverts, choices, weave, variables, lists) — the parts of ink that aren't owned by a more specific skill in this set. This is the index of the `pixi-vn-ink` skill set. It is split into modules the same way `@drincs/pixi-vn`'s is, but **unlike that skill set, install every `pixi-vn-ink*` module together** — they all teach one coherent "how to write ink for Pixi'VN" skill, and several depend on the vocabulary/setup this module establishes (knots-as-labels, the storage mapping, the hashtag-command mechanism). Installing with `npx skills add DRincs-Productions/pixi-vn-ink --all` is the supported path; don't cherry-pick a subset. ## Skill map: which skill to load Find the task below, then load the matching skill before writing ink or hashtag-command code. | Task | Skill | | --- | --- | | Install/configure `@drincs/pixi-vn-ink`, Vite plugin, debugging, translation files, core ink syntax (knots, diverts, choices, variables) | **pixi-vn-ink** (this skill) | | Attribute dialogue to a character, `[characterId]`, `id@emotion`, `# rename` | [pixi-vn-ink-characters](../characters/SKILL.md) | | Prompt the player for input (`# request input`) | [pixi-vn-ink-input](../input/SKILL.md) | | `# pause` and `<># continue` | [pixi-vn-ink-pause-continue](../pause-continue/SKILL.md) | | Markdown/HTML/Tailwind in dialogue text, escaping symbols | [pixi-vn-ink-markup](../markup/SKILL.md) | | Show/edit/remove/animate canvas elements (`# show`, `# edit`, `# remove`, ...) | [pixi-vn-ink-canvas](../canvas/SKILL.md) | | Play/pause/edit/stop sounds and channels | [pixi-vn-ink-sound](../sound/SKILL.md) | | Load/lazy-load assets and bundles (`# load`/`# lazyload`) | [pixi-vn-ink-assets](../assets/SKILL.md) | | Inline `[key]` substitution via `TextReplaces.add` | [pixi-vn-ink-text-replacement](../text-replacement/SKILL.md) | | Custom logic: write your own hashtag command instead of an ink function | [pixi-vn-ink-hashtag-commands](../hashtag-commands/SKILL.md) | | Upgrade the package to a newer version | [pixi-vn-ink-migration](../migration/SKILL.md) | ### Core Pixi'VN skills Ink content calls into the exact same engine APIs as plain TypeScript narration, so for anything outside ink itself (`Game.init`, canvas, sound, storage, saves, history, UI, testing, ...) use the core `@drincs/pixi-vn` skill set — start from its index skill, **pixi-vn**, which maps every task to a skill. Install: `npx skills@latest add DRincs-Productions/pixi-vn@latest --skill '*' -g -y`. For exact signatures and types of the engine API, see the JSDoc reference: [jsdocs.pixi-vn.com/pixi-vn](https://jsdocs.pixi-vn.com/pixi-vn). For this library's own API, see [jsdocs.pixi-vn.com/pixi-vn-ink](https://jsdocs.pixi-vn.com/pixi-vn-ink). ## Installation ```npm npm install @drincs/pixi-vn @drincs/pixi-vn-ink ``` Peer dependencies: `@drincs/pixi-vn` (>=1.9.0) and `zod` (>=4.4.0, used by `HashtagCommands`/ `TextReplaces` validation). `vite` is an optional peer — only needed for the Vite plugin below, which is the recommended setup for every real project. ### (Recommended) the Vite plugin: `@drincs/pixi-vn-ink/vite` ```ts title="vite.config.ts" import { defineConfig } from "vite"; import { vitePluginInk } from "@drincs/pixi-vn-ink/vite"; export default defineConfig({ plugins: [ // other plugins (vitePluginPixivn, react(), ...) vitePluginInk({ inkGlob: "./ink/**/*.ink", inkJsonOutputPattern: "./public/ink-json/[path][name].gen.json", inkJsonManifestPath: "./src/assets/ink-manifest.gen.json", }), ], }); ``` This is the exact configuration the official `pixi-vn-react-template` ships. What each option does: - **`inkGlob`** — which `.ink` files to track (glob, resolved from Vite `root`). The plugin **imports them itself**: it generates a virtual module, `virtual:pixi-vn-ink`, exporting the raw text of every matched file as `string[]` — no hand-written `import.meta.glob` helper needed. - **`inkJsonOutputPattern`** — when set, every matched `.ink` file is compiled with `convertInkToJson` and written to disk at the rendered path (placeholders: `[name]`, `[ext]`, `[extname]`, `[file]`, `[path]`, `[dir]`). **This is the actual game optimization**: ink source is parsed once, at build/dev time, into `PixiVNJson`; the shipped game loads pre-compiled JSON instead of running the ink compiler in the player's browser on every load. It also makes the virtual module additionally export `inkJsons` (the compiled `PixiVNJson[]`, used below). - **`inkJsonManifestPath`** — where the generated `manifest.json` (listing every exported JSON file's URL, for bulk loading) is written; defaults to `manifest.json` inside the output directory if omitted. Wire it up once in the app entry point — with this configuration, **no manual `importInkText` call is needed**: initial load and every future hot-reload are both handled by the listener. ```ts title="main.ts" import { setupInkHmrListener } from "@drincs/pixi-vn-ink/vite-listener"; await setupInkHmrListener(); ``` `setupInkHmrListener()` imports the initial compiled JSON from the virtual module on startup, and (only while the Vite dev server is attached) subscribes to an `ink-updated` HMR event so editing any tracked `.ink` file live-reloads its compiled content without a full page reload. This is also the path a production build takes (`import.meta.hot` is simply absent there, so only the one-time initial import runs) — the same call works unmodified in dev and in a built game. The lower-level primitives this wraps still exist and are occasionally useful directly: `importInkText(text | text[])` (from `@drincs/pixi-vn-ink`) parses raw ink source strings and registers their knots; `importJson(pixiVNJson | pixiVNJson[])` imports already-compiled `PixiVNJson`. Reach for them only outside the Vite-plugin flow (e.g. a headless script, or loading ink fetched from a remote source at runtime) — a normal app just calls `setupInkHmrListener()` and is done. ### VS Code Install the [ink extension](https://marketplace.visualstudio.com/items?itemName=drincs-productions.pixi-vn-ink-vscode) (syntax highlighting, a `pixi-vn` engine mode that suppresses warnings for Pixi'VN-only syntax, live preview). Recommended `settings.json`, matching the official template: ```json title=".vscode/settings.json" { "ink.mainFile": "start.ink", "ink.rootFolder": "ink", "ink.markup": "Markdown", "ink.engine": "pixi-vn" } ``` ## Debugging: check the Vite dev-server logs and API, and the exported JSON **Whenever `vite`/`npm run dev` is running, use it as a source of truth before guessing.** Most ink-authoring mistakes in a Pixi'VN project surface here immediately — don't rely on story-runtime behavior alone to tell whether a `.ink` edit is correct. ### Terminal logs On every `.ink` save (and on `vite build`), `vitePluginInk` logs, prefixed `(pixi-vn-ink)`: - **Ink compile errors/warnings** — from the ink compiler itself (`InkCompiler.compile`), with `file:line`. - **Unknown hashtag commands** — `# some_typo ...` that matched no registered handler. This is the single most common authoring mistake; if a `#` line silently does nothing in-game, check here first. - **Hashtag key-schema issues** — a recognized command whose object-shaped argument (e.g. `edit`'s properties, `animate`'s `options`) doesn't match that command's own schema. - **Unknown divert targets** — `-> some_label` where `some_label` matches no known ink knot **or** JS/TS label (see below). - A summary line per export: `N file(s) exported: M label(s), K hashtag-command(s), J text-replace(s)`. ### The dev-server API While the dev server is up, three read-only-or-more endpoints are queryable directly (e.g. `curl`, `fetch`) — useful for checking exactly what's registered instead of trusting memory or a stale doc: ```bash curl http://localhost:5173/__pixi-vn-ink/hashtag-commands # every registered hashtag command: name, description, validation rule curl http://localhost:5173/__pixi-vn-ink/text-replaces # every registered [key] text-replace handler, same shape curl http://localhost:5173/__pixi-vn-ink/info # { version, schemaUrl } for the running @drincs/pixi-vn-ink ``` Each `validation` is serialized as `{ type: "regexp", source, flags }`, `{ type: "zod", schema }` (a JSON Schema), or `{ type: "literal", value }` — enough to reconstruct exactly which `# ...` shapes a handler accepts without reading its source file. ### The exported JSON With `inkJsonOutputPattern` set, **open the generated `.json` file(s)** after editing `.ink` — that file *is* what the game will actually run, already validated against the `PixiVNJson` schema (a schema mismatch — most often a custom hashtag command returning a malformed operation — is warned in the terminal with the offending ink source line attached). Reading the compiled JSON next to the `.ink` source is the fastest way to confirm "does what I wrote behave the way I intended", especially for a custom hashtag command or text replace whose output isn't otherwise directly visible. ## Generating the translation file [Translation](https://pixi-vn.com/start/translate) wiring for ink goes through `onInkTranslate`: ```ts title="lib/hooks/ink-hooks.tsx" import { onInkTranslate } from "@drincs/pixi-vn-ink"; import { useTranslation } from "react-i18next"; export default function useInkInitialization() { const { t } = useTranslation(["narration"]); useEffect(() => onInkTranslate(t), [t]); return null; } ``` To generate the translation-file skeleton (every key ink narration text needs an entry for), use `generateJsonInkTranslation(pixivnJson, targetObject, options?)` against each compiled ink JSON — this is what every official template's settings-screen "download translations" button does: ```ts title="lib/i18n.ts" import { generateJsonInkTranslation } from "@drincs/pixi-vn-ink"; async function generateResourceToTranslate(lng: string) { const res = { ...(await getLocalesResource(lng)) }; res.narration ??= {}; const manifest = await import("@/assets/ink-manifest.gen.json"); // written by inkJsonManifestPath for (const path of manifest.default) { const element = await fetch(path).then((r) => r.json()); element && (await generateJsonInkTranslation(element, res.narration)); } return res; } export async function downloadResourceToTranslate() { const lng = i18n.options.fallbackLng?.toString() || "en"; const data = await generateResourceToTranslate(lng); const blob = new Blob([JSON.stringify(data)], { type: "application/json" }); const a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = `strings_${lng}.json`; a.click(); } ``` `generateJsonInkTranslation` walks the JSON's labels/steps, collecting every dialogue string (running each through `HashtagCommands.run` first, so operation-only lines don't pollute the translation file) and adds a `defaultValue` entry (`"copy_key"` by default, or `"empty_string"`) for any key not already present in the target object — safe to call repeatedly as new ink content is written. ## Writing core ink for Pixi'VN Everything Pixi'VN doesn't give its own dedicated module — content, comments, choices, knots, diverts, glue, weave, gathers, variable text, `VAR`/`CONST`/`temp`, `LIST`, functions, tunnels — is plain, standard ink. The full syntax reference is `reference/writing-with-ink.md`; this section covers only what's specific to writing it *for Pixi'VN*. ### Knots are Pixi'VN labels — in both directions Creating a `knot` with ink registers a `label` internally, addressed exactly like a TS-authored one: - `-> knot` (divert) is [`jump`](https://pixi-vn.com/start/labels#jump-to-a-label). - `-> knot ->` (tunnel) is [`call`](https://pixi-vn.com/start/labels#call-to-a-label); `->->` and `-> DONE` are equivalent ways to close it. Because both live in the same registry, **a divert can target a label defined in JS/TS**, and JS/TS can call into a knot — this is the intended way to mix the two: ```ink title="ink/start.ink" === start === The story continues in code. -> minigame_result ``` ```ts title="content/labels/minigame-result.label.ts" import { newLabel } from "@drincs/pixi-vn"; export const minigameResult = newLabel("minigame_result", [ () => { /* ... */ }, ]); ``` ```ts title="some ink label's JS counterpart" import { narration } from "@drincs/pixi-vn"; await narration.call("start", {}); // "start" is the ink knot above ``` ### Diverting in the same step By default a divert runs in the *next* step, so the diverted content only appears once the flow continues. Put the divert directly after the text with no line break, and it runs in the **same** step, so both are shown together — the same glue-adjacent rule as native ink: ```ink === start === It is, first and foremost... -> add2 // next step: "...a story about the twelve labors of Hercules." shown separately === start === It is, first and foremost...-> add2 // same step: shown as one continuous block ``` ### Threads work differently in Pixi'VN ink Native ink Threads (fork the story, merge multiple sources' choice lists into one) have **no Pixi'VN equivalent**. `<-` is simply repurposed to mean the same thing as a tunnel, `-> knot ->` — it calls the target and returns. Don't expect combined choice lists or "shared but not forked" globals when porting a script that used `<-` for real threading. ### Variables live in Pixi'VN storage `VAR`/`CONST` are [regular storage](https://pixi-vn.com/start/storage); `temp` is [temporary storage](https://pixi-vn.com/start/storage#temporary-storage) — same underlying mechanism, just scoped to the current knot/stitch. The variable name **is** the storage key, so `storage.get("myVariable")`/`storage.set(...)` from JS/TS reads and writes the exact same value an ink `~ myVariable = ...` line does. `LIST name = a, b, c` stores both a list definition and each entry under its **qualified key**, `name.entryName` — see [LIST entries need their qualified name](#list-entries-need-their-qualified-name) below; this is the single most common porting bug. ### Functions: define in JS/TS (prefer a hashtag command), not ink `=== function name(...) === / ~ return ...` is parsed but **silently ignored** at runtime. See **pixi-vn-ink-hashtag-commands** — a hashtag command is the preferred way to add custom logic; a plain JS function exposed via `StepLabelProps` and called from ink with `~ myFunc(...)` also works but is more limited (no optional parameters) and doesn't integrate with the dev-server introspection above. ### Not yet supported in Pixi'VN ink Standard, documented parts of native ink that the Pixi'VN runtime does not implement yet — they parse without error but won't behave as native ink/Inky would: - `CHOICE_COUNT()`, `TURNS()`, `TURNS_SINCE()`, `SEED_RANDOM()` - `LIST_RANDOM()`, `LIST_COUNT()`, `LIST_INVERT()`, `LIST_ALL()`, `LIST_RANGE()`, `LIST_MIN()`, `LIST_MAX()`, `LIST_VALUE()` `RANDOM(min, max)`, `POW`, `INT`/`FLOOR`/`FLOAT`, and basic `LIST` set/get/`has`/`?`/`==`/`++`/`--` operations *are* implemented and safe to use. Check [pixi-vn.com/ink](https://pixi-vn.com/ink)'s "Upcoming features" section before assuming this list has changed. ### LIST entries need their qualified name **Native ink** treats a list entry's short name (`sword`) and its qualified name (`items.sword`) as interchangeable. **Pixi'VN does not** — always use the qualified form. A bare `sword` silently resolves to an unrelated plain storage key named `"sword"`, not the list member, with no warning: ```ink LIST items = sword, key, potion ~ myItem = items.sword // ✅ correct ~ myItem = sword // ❌ silently wrong — reads/writes a plain "sword" storage flag instead ``` ### Syntax Pixi'VN ignores entirely - **`INCLUDE other.ink`** — use multiple `.ink` files matched by `inkGlob` (or, manually, `importInkText([sourceA, sourceB, ...])`) instead. - **`=== function ... ===`** — see above. - **Narration outside any knot** — every top-level line that isn't a `VAR`/`CONST` declaration is ignored, including a top-level `-> start` divert; a knot only ever runs when JS/TS explicitly starts it (`narration.call`/`jump`, an ink divert/call from another knot, or a choice). ### Differences from native ink — quick cheat-sheet | Native ink (Inky) | Pixi'VN ink | | --- | --- | | `sword` and `items.sword` are equivalent list-entry references | Only `items.sword` (qualified) works | | `<-` forks a Thread, merging choice lists from multiple sources | `<-` just **calls** a knot, like `-> knot ->` | | `{ shuffle: - A - B }` shows exactly one line as its own paragraph | Multiple shuffled lines are joined into **one** dialogue block | | A one-time choice attached to a weave is invalidated by *reaching* it via divert | Invalidated only by actually **selecting** the choice (see below) | | `INCLUDE file.ink` | Ignored — use `inkGlob` / `importInkText([...])` | | `=== function ... ===` runs in-language | Ignored — use a hashtag command or `StepLabelProps` | | `CHOICE_COUNT`, `TURNS`, `TURNS_SINCE`, `SEED_RANDOM`, most `LIST_*()` | Not implemented yet | The one-time-choice/weave interaction, worked through: given ```ink -> start === start === * [1] -> shove * (shove) [2] 2 * {shove} [3] -> END - -> start -> DONE ``` after choosing `1` once, revisiting `start`: **native ink** hides choice `2` (it treats reaching `shove` via divert as equivalent to choosing it); **Pixi'VN ink** still offers both `2` and `3`. To get native-ink parity, invalidate the choice explicitly: `* (shove) {!shove} [2] 2`. ## Related - Every other `pixi-vn-ink-*` skill in this set (install together — see above). - The `@drincs/pixi-vn` Agent Skills (`npx skills add DRincs-Productions/pixi-vn`; index skill **pixi-vn**) — `Game.init`, plain TS narration, canvas, sound, storage, characters, saves, UI. Ink content calls into the exact same APIs those skills document. - [jsdocs.pixi-vn.com/pixi-vn](https://jsdocs.pixi-vn.com/pixi-vn) — the engine's JSDoc API reference. - [jsdocs.pixi-vn.com/pixi-vn-ink](https://jsdocs.pixi-vn.com/pixi-vn-ink) — this library's JSDoc API reference (`importInkText`, `HashtagCommands`, `onInkTranslate`, the Vite plugin, ...). - [pixi-vn.com/ink](https://pixi-vn.com/ink) — the rendered version of this integration's docs. - [inkle/ink](https://github.com/inkle/ink) and [inkjs](https://github.com/inkle/inkjs) — the language and the JS runtime this integration is built on.