From 2c54498ee2f270672effdbe3e1a975ae5b27d069 Mon Sep 17 00:00:00 2001 From: Georg Tomitsch Date: Sun, 17 May 2026 15:52:41 +0200 Subject: [PATCH] Document markup and improve choice tags --- .gitignore | 7 +- CLIENT_TODO.md | 63 + MARKUP_GUIDELINES.md | 81 ++ NOTE.md | 56 - README.md | 50 +- THIRD_PARTY_AUDIT.md | 47 + THIRD_PARTY_NOTICES.md | 7 + config/engines/ink.json | 15 +- config/engines/yaml.json | 1 + config/engines/zork.json | 1 + data/ink-src/eibenreith.ink | 1760 +++++++++++++++++++++++ data/ink/eibenreith.ink.json | 1 + dist/config/game-config.d.ts | 1 + dist/config/game-config.js | 2 + dist/config/game-config.js.map | 2 +- dist/engine/ink-engine.js | 37 +- dist/engine/ink-engine.js.map | 2 +- dist/interfaces/turn-result.d.ts | 4 + dist/interfaces/turn-result.js.map | 2 +- dist/utils/tag-parser.js | 8 + dist/utils/tag-parser.js.map | 2 +- ink_inclusion.md | 2 +- public/THIRD_PARTY_NOTICES.md | 116 ++ public/css/style.css | 335 ++++- public/images/README.md | 7 +- public/images/mat.png | Bin 2274853 -> 2110663 bytes public/js/api-tts-module-base.js | 25 +- public/js/audio-manager-module.js | 85 +- public/js/browser-tts-module.js | 40 +- public/js/choice-display-module.js | 37 +- public/js/elevenlabs-tts-module.js | 5 +- public/js/game-config-module.js | 7 +- public/js/game-loop-module.js | 37 +- public/js/kokoro-tts-module.js | 48 +- public/js/localization-module.js | 11 - public/js/markup-parser-module.js | 33 +- public/js/openai-tts-module.js | 5 +- public/js/options-ui-module.js | 238 ++- public/js/persistence-manager-module.js | 6 + public/js/sentence-queue-module.js | 141 +- public/js/socket-client-module.js | 18 +- public/js/text-processor-module.js | 63 +- public/js/tts-factory-module.js | 37 +- public/js/ui-display-handler-module.js | 281 +++- public/locales/de_DE.json | 37 +- public/locales/en_US.json | 33 +- public/music/README.md | 2 + public/sounds/README.md | 10 + src/config/game-config.ts | 3 + src/engine/ink-engine.ts | 39 +- src/interfaces/turn-result.ts | 4 + src/utils/tag-parser.ts | 8 + 52 files changed, 3485 insertions(+), 377 deletions(-) create mode 100644 MARKUP_GUIDELINES.md delete mode 100644 NOTE.md create mode 100644 THIRD_PARTY_AUDIT.md create mode 100644 THIRD_PARTY_NOTICES.md create mode 100644 data/ink-src/eibenreith.ink create mode 100644 data/ink/eibenreith.ink.json create mode 100644 public/THIRD_PARTY_NOTICES.md diff --git a/.gitignore b/.gitignore index 6775b32..2d15b15 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,9 @@ -node_modules +node_modules # windsurf rules .windsurfrules + +# local inspection / generated scratch artifacts +.tmp/ +*.orig +*.bkp diff --git a/CLIENT_TODO.md b/CLIENT_TODO.md index 2c35d07..31cbfba 100644 --- a/CLIENT_TODO.md +++ b/CLIENT_TODO.md @@ -26,6 +26,10 @@ The production client must tolerate TTS being unavailable. The safe default TTS - Done: placeholder game API for new/load/save/running state. - Done: sound effect and music folders, sound effect playback, music playback, and music ducking during TTS. - Done: image markup is parsed, persisted in history, restored from save/history, and rendered as line-snapped page blocks. +- Done: Ink engine integration with source compilation, engine config, metadata handoff, choice-mode turns, one-list choice UI, and keyboard choice letters. +- Done: localized UI strings, game metadata language handoff for typography/hyphenation/TTS language, and German dialogue guillemet normalization. +- Done: localized popup queue for intended endings, unrecoverable errors, achievements, and tutorial/player alerts. +- Done: credits dialog and third-party license display. - Partial: save-game API restores story state and Ink state, but the broader save/storage model still needs hardening for all engines. - Pending: deeper automated tests for layout, playback timing, TTS provider switching, and media cue timing. @@ -74,6 +78,7 @@ The loader is deliberately the conductor, not the orchestra. Module-specific con - `options-ui-module.js`: options modal, persisted controls, provider status displays. - `ui-controller-module.js`: top-bar commands, global input behavior, game API control wiring. - `ui-display-handler-module.js`: book page display, startup prompt, unified live/history rendering, line-coordinate scrolling, image placement, and media block dispatch. +- `choice-display-module.js`: choice-mode rendering, keyboard-letter assignment, click/keyboard choice dispatch, and future choice-template hook. - `ui-input-handler-module.js`: command entry, history, fast-forward key handling. - `socket-client-module.js`: socket connection and game API request wrapper. - `game-loop-module.js`: high-level client/game flow. @@ -192,6 +197,16 @@ Canonical structural and media tags use Ink-style `#` syntax: - YAML and Zork narrative output use the same leading `#...` syntax, parsed by the server into `StoryTag` objects before the client sees them. - The browser protocol is structured `TurnResult` objects with structured tags and render blocks, not raw story markup. +Tag syntax: + +```text +#key +#key[value] +#key[value](options) +``` + +The current parser accepts the bracket/parentheses form above and colon value tags such as `#key:x` or `#action:movement`. + Markdown emphasis: ```text @@ -256,6 +271,47 @@ File names resolve relative to `public/music/`. Modes: For chapter openings, authors can place `#music[file](..., lead=N)` after `#chapter[...]` and before the first prose paragraph. The heading is rendered/spoken first, then music starts and plays alone for the lead duration, then the dropcapped paragraph continues. Music and image pauses are playback gates only; they must not stop the queue from preparing upcoming TTS in the background. +Sound effect options: + +```text +#sfx[church-bells.ogg](max=8 fade fade-duration=2) +#sfx[steam-whistle.ogg](fade-after=4 fade-duration=1.5) +``` + +Supported SFX options are `max=`, `duration=`, `max-duration=`, `limit=`, `stop-after=`, `fade-after=`, bare seconds such as `4s`, `mode=fade`, `mode=stop`, `fade`, `stop`, `cut`, and `fade-duration=`/`fade-time=`. + +Choice tags: + +```ink +* [Open the compartment door] + # letter[o] + # action[examine] +``` + +`#letter[x]`, `# letter[x]`, or `#key:x` reserves keyboard letter `X` for that choice. Explicit letters are assigned first; remaining visible choices receive `A` through `Z` in screen order, skipping reserved letters. The current UI supports up to 26 visible choices and renders them in one list. `#action[name]` or `#action:name` is stored as the choice category and reserved for later template routing. + +Future choice-template metadata should keep the same bracket tag syntax if implemented: + +```text +#action[movement] +#optional +#gated[noble] +#sort[last] +``` + +The older standalone `MARKUP_GUIDELINES.md` draft proposed colon tags, digit keys, grouping, shuffling, and parser-style reserved shortcuts. The active implementation deliberately does not use that syntax yet. Any future expansion should first extend the shared tag parser and `TurnResult.choices` contract so Ink, YAML, and Z-code all still emit the same structured choice objects. + +Game-state and popup tags: + +```text +#score[You reached the quiet ending.] +#error[The story ended unexpectedly.] +#achievement[First Steps] +#alert[Try examining objects before using them.] +``` + +`#score[...]` marks an intended ending and is surfaced as `gameState.endState.type = intended` when the turn ends. `#error[...]` marks an unrecoverable ending and is surfaced as `gameState.endState.type = error`. The Ink engine emits a synthetic `#error[...]` when it runs out of content without an explicit `#score[...]`/`#error[...]`. `#achievement[...]` and `#alert[...]` show queued localized popups while play continues. + ## TTS And Playback Specification The playback system must keep text animation and audio synchronized. @@ -345,6 +401,7 @@ Current placeholder behavior: - `loadGame(slot)` requires a placeholder save and then starts the demo game like `newGame()`. - Saves do not persist across reloads yet. - `isGameRunning()` returns true after a game starts and until the session ends. +- Turn results use `inputMode: 'end'` when no more input is accepted. End turns may include `gameState.endState` and/or `#score[...]`/`#error[...]` tags to distinguish intended endings from unrecoverable errors. UI requirements before a game starts: @@ -415,6 +472,11 @@ Longer-term goal: - [x] Added command input focus and global typing behavior. - [x] Added command mirroring through the server path for UI testing. - [x] Added fast-forward for animation and TTS fade/stop. +- [x] Added Ink source compilation and Ink engine server. +- [x] Added choice-mode UI and keyboard letter assignment from `#letter[x]` and `#key:x`. +- [x] Added localized ending, error, achievement, and alert popups from tag-channel events. +- [x] Added credits/license dialog. +- [x] Added line-addressed history scrolling model and moved the NOTE.md scrolling specification here. ### In Progress @@ -422,6 +484,7 @@ Longer-term goal: - [ ] Tighten automated checks around top-bar/options state initialization after reload. - [ ] Improve automated visual regression coverage for page scaling, dropcap line-height alignment, and paragraph indentation. - [ ] Improve automated audio tests for music ducking, sound effect timing, and fast-forward fadeout. +- [ ] Polish custom scrollbar dragging so the thumb moves freely during drag and commits the scroll target only on release. ### Pending diff --git a/MARKUP_GUIDELINES.md b/MARKUP_GUIDELINES.md new file mode 100644 index 0000000..18c88e0 --- /dev/null +++ b/MARKUP_GUIDELINES.md @@ -0,0 +1,81 @@ +# Markup Guidelines + +This file documents author-facing Ink tag conventions. The active parser normalizes tags into structured `StoryTag` objects before they reach the UI. + +## Implemented Tag Forms + +Use bracket tags for titles, filenames, and longer text: + +```ink +#chapter[Eibenreith] +#image[statue.png](square) +#music[Kaiserpunk Waltz.mp3](crossfade, loop, lead=8) +#sfx[church-bells.ogg](max=8, fade) +#score[You reached an ending.] +#achievement[First Steps] +#alert[Try examining the room.] +``` + +Use colon tags for short identifiers, categories, and choice keys: + +```ink +#action:movement +#key:l +#sort:last +#gated:noble +``` + +Bare flags are accepted as tags with no value: + +```ink +#optional +``` + +## Choice Metadata + +Choice tags are placed on the Ink choice they belong to: + +```ink +* [__Schaue__: Aus dem Fenster.] + #action:orientation + #key:l +``` + +Implemented choice metadata: + +- `#key:x`: reserves keyboard key `X` for the choice. +- `#letter[x]`: older equivalent for reserving keyboard key `X`. +- `#action:group` or `#action[group]`: stores a category/template hint. + +The current UI renders all choices in one list. Explicit keys are assigned first; choices without explicit keys receive `A` through `Z` in visible order. Digits, grouping columns, stable shuffling, `#optional`, `#gated[...]`, and `#sort[...]` are documented authoring conventions or future metadata, not fully implemented UI behavior yet. + +## Popup And End-State Tags + +These tags may appear as Ink global tags, paragraph tags, or empty tag-only lines. They are dispatched through the same tag channel as media tags. + +```ink +#score[You reached the quiet ending.] +#error[The story ended unexpectedly.] +#achievement[First Steps] +#alert[Try examining objects before using them.] +``` + +- `#score[...]`: intended ending. When the turn reaches `inputMode: end`, the UI shows a localized ending popup with the tag value as the optional message. +- `#error[...]`: unrecoverable ending. The UI shows a localized error popup with the tag value as the optional message. The Ink engine emits this automatically if Ink runs out of content without an explicit `#score[...]` or `#error[...]`. +- `#achievement[...]`: queued localized achievement popup while the game continues. +- `#alert[...]`: queued localized player hint/tutorial popup while the game continues. + +## Existing Media And Structure Tags + +```ink +#chapter[Title] +#section +#textblock +#image[filename.png](landscape) +#image[filename.png](portrait pause=2) +#image[filename.png](square lead=1.5) +#music[track.mp3](crossfade, loop, lead=4) +#sfx[file.ogg](max=8 fade fade-duration=2) +``` + +Asset filenames resolve relative to the configured image, music, and sound folders. diff --git a/NOTE.md b/NOTE.md deleted file mode 100644 index e04c3b3..0000000 --- a/NOTE.md +++ /dev/null @@ -1,56 +0,0 @@ -Now let's fix the history scrolling: -1.) Make sure