Skip to content

Narrative bundle

The Narrative bundle is a short visual novel slice. The apprentice arrives at the forge, talks to Master Halden, and chooses whether to accept the lesson or practice alone first. Two endings. About 90 seconds end-to-end. Boot lands on a main menu (New Game / Continue / Settings / Credits) before the first dialogue node.

Pure DOM, no canvas, no game engine. The dialogue runner is the same shape the Top-down RPG bundle reuses for NPC chatter, so anything you build here transfers cleanly to a more complex project.

  • 14 TypeScript files across 6 scenes (main menu, settings, credits, dialogue runner, complete) plus the GameManager spine, 7 system components, and a Yarn-inspired flat dialogue node graph.
  • 3 background plates (forge, courtyard, scriptorium) with scene-swap transitions via the scene field on dialogue nodes.
  • 2 characters x 4 emotions = 8 portraits (idle / engaged / concerned / determined). Selected per node via the emotion field.
  • 3 ready-made characters in the asset bank (Wren, Iolen, Kael) with all four emotions, copy them into public/portraits/ to use them.
  • Ambient interior music loop on the Music bus plus 3 dialogue SFX cues (dialogue-blip, choice-pick, chapter-complete) layered via AudioManager bus listeners.
  • Full game shell: main menu, settings overlay (4 audio sliders + subtitle toggle), credits, complete scene.

The Narrative bundle ships with 7 systems registered:

  • EventBus carries dialogue advance and chapter complete events.
  • ProjectConfig holds typewriter speed and similar tunables.
  • StateMachine drives the menu / dialogue / settings / credits / complete flow with pushdown overlays for Settings and Credits.
  • SaveSystem persists single-slot autosave to localStorage.
  • AudioManager owns the bus tree with ducking and crossfade.
  • SettingsManager renders the audio sliders and subtitle toggle.
  • ThemeManager drives the violet-on-charcoal palette.
InputAction
Click New Game / ContinueStart dialogue from the menu
Settings / Credits on menuPushdown overlays (Back returns)
Click anywhere / Space / EnterAdvance dialogue or skip the typewriter
Click a choice buttonPick that branch
R (after the ending)Restart the chapter

src/data/story.dialog.json is a flat list of nodes. Each node is one of three shapes:

  • Linear ({ id, speaker, emotion, text, next }): runner walks next on advance.
  • Branch ({ id, speaker, emotion, text, choices: [{ label, target }] }): runner waits for a click.
  • Terminal ({ id, speaker, emotion, text, endingId }): runner fires dialogue_chapter_complete and shifts to the ending card.

Optional fields: scene swaps the backdrop on enter (must match a src/data/scenes.ts id). emotion selects the speaker’s portrait variant (idle | engaged | concerned | determined).

Open the file in Forge and it routes to the Dialog tab. You can drag nodes, add choices, swap emotions, and save back to JSON. The visual graph keeps the same flat-list shape the runtime expects.

Drop a portrait set under public/portraits/<id>/ with all four emotions named idle.png, engaged.png, concerned.png, determined.png. Then register in src/data/characters.ts:

{ id: 'wren', name: 'Wren', portraits: {
idle: '/portraits/wren/idle.png',
engaged: '/portraits/wren/engaged.png',
concerned: '/portraits/wren/concerned.png',
determined: '/portraits/wren/determined.png',
}}

Missing emotion variants fall back to idle.png silently, so you can add a new emotion on one node without committing the full set first.

If you have the agent on, ask it to generate a portrait set via forge.generate_image and chroma-key the magenta backgrounds via forge.alpha_mask_smart. See Generate art, audio, and 3D.

Drop a 1920x1080 PNG into public/backgrounds/ and append to src/data/scenes.ts:

{ id: 'rune_shrine', background: '/backgrounds/bg_rune_shrine.png' }

Reference scene: 'rune_shrine' on a node’s enter to transition. The asset bank ships 3 unused backdrops (apprentice_dormitory, rune_shrine, canyon_overlook) ready to copy in.

  • Minimal audio. AudioManager ships with bus tree, ducking, and crossfade plus an ambient interior loop and 3 dialogue SFX. Dialogue voice-over is an extension. Drop MP3s into public/audio/voice/ and key per-node via audio.playSfx('/audio/voice/halden-l1.mp3').
  • No multi-slot save. Single progress slot plus autosave. Multi-slot save UI is an extension.
  • No localization. English-only strings inline in story.dialog.json.
  • No save import / export. localStorage is the entire persistence surface.