nukcole67 downloadsWrite, playtest and debug ink interactive fiction: live play panel, saves, knot graph, web export and bilingual stories. English and Chinese interface.
Write, playtest and debug ink interactive fiction in Obsidian. Works on desktop and mobile. Interface in English and Chinese.
Think of it as Inky inside your vault: your story files sit next to your notes, character sheets and outlines, the play panel recompiles as you type, and the knot graph shows the shape of your branches. When the story is ready, export it as a single web page.

Writing a branching story is already a choose-your-own-adventure. The adventure should not be: alt-tab to Inky, copy the file over, lose the outline, forget which knot you were in, alt-tab back.
[[link]] away from the script..html, drop it on itch.io, go to bed.{tr("Hello", "你好")} once; readers switch languages mid-story without losing their place, and the plugin nags you about untranslated lines more reliably than any editor.game.md next to a story and it plays as a game: skill checks with odds on the buttons, resources, items, random tables, decks, a map, a dungeon that grows as you explore. Solo tabletop RPGs and gamebooks, written as notes.=== you ===
You have a story in your head and a vault full of notes.
* [Keep alt-tabbing between three apps] -> still_alt_tabbing
* [Install Inkwright] -> happily_ever_after
In Obsidian, open Settings → Community plugins → Browse, search for Inkwright, then install and enable it. Or open the listing in the community directory.
Requires Obsidian 1.13 or later; older versions get the previous release automatically.
.ink files in your vault with syntax highlighting.INCLUDE paths resolve relative to the main story's folder, as in Inky and inklecate. Lock a main story so the panel keeps playing it while you edit its chapters..html file next to the story, playable in any browser, with undo and progress kept in the browser, even when you re-export an updated version (the player's run is replayed on it and stops, with a note, where a choice they took is gone). Upload it anywhere (itch.io, GitHub Pages, …).VAR lang = "en" (or VAR 语言) and write text as {tr("Hello", "你好")}-style calls; the play panel and exported pages get a language switch that replays the run in the other language without losing progress. Text left outside such a call is reported as a warning.#input:name gets a text field; the line typed goes into the name VAR (a name, an oath, a diary line), and saves, undo and the language switch keep it.game.md note next to the story turns it into a game with dice checks, skills, resources, characters, items, random tables, decks, hands of cards, a map and more (see Game mode below).npm run check-ink -- <folder> --all plays every branch (both sides of every check in a game) and reports endings, runtime errors, unreached knots and choices; --random 300 plays random runs and gives the odds of each ending.
.ink file. Click the ▶ ribbon icon (or run Open player) to play it in the sidebar, or run Open player in the main area (full screen); a button on the panel's bar moves it between the two.The language variable is set by the panel; the text function is yours, so name it as you like:
VAR lang = "en"
{tr("It rained all night.", "雨下了整整一夜。")}
* [{tr("Open the door", "开门")}] -> door
=== function tr(en, zh) ===
{ lang == "zh":
~ return zh
}
~ return en
Languages are "en" and "zh". String literals in ink cannot contain ", so use “” or ' inside them.
A folder whose main story sits next to a game.md note is a game. The note's frontmatter holds the rules; the story stays plain ink. The state lives in ink variables, so saves, undo, the language switch and the web export all keep working.
---
rules: { kind: sum, dice: [6, 6] } # 2d6 + skill vs difficulty; also rollUnder (d100) and bag (chaos bag)
languages: [zh, en]
skills:
- { id: insight, name: { zh: 洞察力, en: Insight } }
- { id: strength, name: { zh: 力量, en: Strength } }
resources:
- { id: fear, name: { zh: 恐惧, en: Fear }, min: 0, max: 100, initial: 0 }
characters:
- { id: xin, name: { zh: 昕, en: Xin }, skills: { insight: 3, strength: 2 } }
---
Run Generate game engine ink to write _engine.ink from the note, INCLUDE it, and tag choices:
INCLUDE _engine.ink
* [{t("轻轻推开门", "Ease the door open")} #check:insight:10]
{ check("insight", 10) >= 3:
{t("门无声地开了。", "It swings open without a sound.")}
- else:
{t("门轴发出一声长响。", "The hinge groans.")}
~ fear += 20
}
With languages: [zh, en] the generated ink declares the language variable and its own t(zh, en) helper, so a game does not write a text function of its own.
The player picks a character, sees the odds of each check on the choice, and each roll shows up as a card in the text and in the dice log. The generated ink falls back to plain rules, so the story still runs in Inky.
What a game note can declare:
#check:skill:difficulty on a choice (:red for a one-shot check), five result bands from critical failure to critical success, skills that speak up as inner voices (#voice:skill), modifiers from resources and items.min..max; several playable characters as presets; companions with their own resources.#needs:item locks a choice until the item is carried.#fx:shake|flash|dark|pulse on a line.{roll("2d6+3")}, d66, exploding d6!, keep highest 4d6kh3 / lowest 2d20kl, dice pools 5d6>=5; {table("encounter")} reads a row from a table written inline or in another note's markdown table.draw("clues"), shuffle, deck_left; hands drawn as rows of cards, #play:hand plays the picked card. Cards can carry numbers and art and come from a folder of notes.#go:place makes a place tappable.The web export carries the engine along, so an exported game plays the same in a browser.
For coding agents and scripts, the plugin exposes app.plugins.plugins.inkwright.api, usable through the Obsidian CLI: compile a story, play it headlessly with a list of choices, read variables and visit counts, inspect the knot graph, drive the visible play panel, or export it. Everything returns JSON.
obsidian eval code='app.plugins.plugins.inkwright.api.help()'
obsidian eval code='app.plugins.plugins.inkwright.api.play("stories/main.ink",{choices:[0,"Open"]}).then(JSON.stringify)'
Inkwright makes no network requests. Stories are compiled and played locally with inkjs.
npm install
npm test # unit tests
npm run lint # Obsidian review rules
npm run build # type check + bundle main.js
npm run check-ink -- <folder> [--all] [--random N] # compile and play every story under a folder
npm run export-web -- <main.ink> <out.html> # web export from the command line
npm run gen-engine -- <main.ink> [--check] # write (or check) a game's _engine.ink
OBSIDIAN_PLUGIN_DIR=/path/to/vault/.obsidian/plugins/inkwright npm run dev # watch build into a vault
Releases are built by GitHub Actions when a version tag (e.g. 0.2.0, no v) is pushed.
* and -> can do. Inkwright is an unofficial fan tool and not affiliated with inkle.