Prepare Ink Coolify release branch

This commit is contained in:
2026-05-19 15:25:23 +02:00
parent 9111dedaa2
commit 90f81ee1b7
90 changed files with 68 additions and 14215 deletions
+55 -209
View File
@@ -1,257 +1,103 @@
# AI Interactive Fiction
# AI Interactive Fiction - Ink Coolify Release
AI Interactive Fiction is a web and CLI text adventure prototype that combines a deterministic world model with LLM-assisted command interpretation and narrative output. The web client presents the story as an animated, novel-like book page with synchronized text animation, optional TTS, music, and sound effects.
This branch is the deployable Ink edition of the AI Interactive Fiction client/server. It contains the browser UI, the Ink server, the Eibenreith Ink source, compiled Ink output, media assets, fonts, locale files, and Docker/Coolify configuration.
## Quick Start
The full multi-engine development tree lives on `main`. The historical prototype is intentionally not part of this branch; it is preserved on `codex/archive-prototype` and tag `prototype-archive-2026-05-19`.
Use Node.js 22 LTS for development. The project accepts Node >= 18.17, but current development has been done on Node 22.
## Local Ink Development
Use Node.js 22 LTS.
```powershell
nvm install 22
nvm use 22
npm install
npm run build
npm run dev
```
`npm run dev` and `npm run start` use `DEFAULT_GAME_ENGINE` from `.env` to choose the active engine. Supported values are `ink`, `yaml`, and `zcode`. The engine-specific scripts remain available when you want to bypass the default.
`npm run dev` starts the Ink server through `ts-node` and watches `src/`, `data/ink-src/`, and `config/engines/ink.json`. The server compiles the configured Ink source when it starts.
Set `PORT` to choose a port; the server will try the next few ports if the requested one is already in use. Current engine defaults are YAML `3001`, Z-code `3002`, and Ink `3003` before port fallback.
## Commands
Useful commands:
```powershell
npm run dev # Start the web UI through ts-node/nodemon
npm run start # Build/run the configured default engine from dist/
npm run dev:ink # Start the Ink engine server, watch ink source, compile on restart
npm run dev:yaml # Start the YAML engine server
npm run dev:zcode # Start the Z-code engine server
npm run start:ink # Build and run the compiled Ink engine server
npm run build # Compile TypeScript
npm run test # Run Jest tests
npm run lint # Run ESLint on src/
npm run start:cli # Run the CLI interface
npm run dev:cli # Run the CLI interface through ts-node/nodemon
npm run build # Compile TypeScript to dist/
npm run start # Run the compiled Ink server
npm run dev:debug # Development server with Ink debug logging
npm run dev:inspect # Development server with Node inspector on 0.0.0.0:9231
npm run start:debug # Compiled server with Ink debug logging
npm run start:inspect # Compiled server with Node inspector on 0.0.0.0:9231
```
Each game engine also has `:debug` and `:inspect` variants. `:debug` enables engine-specific diagnostic logging. `:inspect` starts Node with the inspector and currently also enables that engine's debug flag, so it is the combined debug-plus-inspector mode.
Set `PORT` to choose the server port. The Docker image defaults to `3000`.
## Docker / Coolify Ink Deployment
## Coolify 4 Deployment
The included `Dockerfile` builds and serves the Ink engine only. Coolify can use the repository Dockerfile directly.
Configure Coolify to deploy this branch with the repository `Dockerfile`.
Set the Coolify environment variables from `coolify.env.example`; at minimum:
Recommended environment:
```text
NODE_ENV=production
DEFAULT_GAME_ENGINE=ink
PORT=3000
INK_CONFIG_FILE=./config/engines/ink.json
```
The container compiles TypeScript during image build and compiles the configured Ink source to JSON when the server starts.
Coolify can watch `release/coolify-ink` and redeploy on webhook pushes. The intended flow is:
## Configuration
1. Write Ink locally in `data/ink-src/`.
2. Test locally with `npm run dev`.
3. Commit to the development branch.
4. Merge or cherry-pick the wanted deployment state into `release/coolify-ink`.
5. Push `release/coolify-ink` to the Git remote watched by Coolify.
Environment variables are loaded from `.env`.
The container builds TypeScript during image build and compiles the configured Ink source at server startup.
- `PORT`: preferred web server port.
- `DEFAULT_GAME_ENGINE`: engine used by `npm run dev` and `npm run start`; one of `ink`, `yaml`, or `zcode`.
- `DEFAULT_WORLD_FILE`: YAML world file to load. Defaults to `./data/worlds/example_world.yml`.
- `OPENROUTER_API_KEY`: API key for LLM command interpretation.
- `OPENROUTER_MODEL`: OpenRouter model name.
## Ink Configuration
TTS provider settings are configured in the browser options menu and persisted in browser storage. Providers currently include `none`, browser speech synthesis, Kokoro, ElevenLabs, and OpenAI. Production should not assume a universal TTS default; the game or player state selects the active mode, and `none` is the safe fallback.
The active game is configured in `config/engines/ink.json`.
## Starting A Game
Important paths:
The web client no longer starts the game automatically. Browsers require a user gesture before audio playback, so the right page initially shows a start prompt and the command input is hidden. Use `new game` or `load` in the top bar to start.
- `paths.inkSource`: main Ink source file.
- `paths.inkCompiled`: compiled Ink JSON target.
- `paths.mainGameFile`: compiled Ink JSON loaded by the server.
- `paths.music`: background music directory.
- `paths.sfx`: sound effect directory.
- `paths.images`: image directory.
The placeholder server API supports:
Game metadata and language are sent to the client before game start. The client uses game language for hyphenation and TTS language hints; UI locale can still be overridden by the player.
- `newGame()`
- `loadGame(slot)`
- `saveGame(slot)`
- `hasSaveGame(slot)`
- `getSaveGames()`
- `isGameRunning()`
## Browser Client
Save slots are positive integers. Save behavior is engine-specific: the Ink client/server path persists Ink state, client history, choices, media state, and playback position for browser save/load; YAML and Z-code persistence still need regression testing and cleanup.
The client lives in `public/` and is served as native browser modules. It renders structured `TurnResult` output from the server, including paragraphs, headings, choices, media events, alerts, score messages, achievements, and errors.
## Web Client
TTS provider settings, volume controls, savegames, TTS cache, and rendered story history are stored in browser storage. Ink server state is also sent back to the browser save data so a client can recover after reload or server restart without server-side per-player sessions.
The browser app is built from native ES modules in `public/js/`. The loader dynamically imports modules, applies a cache-busting query string during development, resolves declared dependencies, and awaits module initialization in dependency order before the UI becomes usable.
## Story Tags
Major modules:
Ink tags are parsed server-side into structured output objects. The client consumes structured turn data only.
- `module-registry.js`, `base-module.js`, `loader.js`: module lifecycle, dependency graph, progress overlay, state reporting.
- `text-processor-module.js`, `paragraph-layout-module.js`, `layout-renderer-module.js`: SmartyPants, language-aware hyphenation, Knuth-Plass line breaking, DOM rendering.
- `markup-parser-module.js`: story markup fallback for chapters, sections, Markdown emphasis, right-page glossary notes, images, SFX, and music.
- `sentence-queue-module.js`, `playback-coordinator-module.js`, `animation-queue-module.js`: sentence preparation, synchronized playback, timing, fast-forward.
- `tts-factory-module.js` plus provider modules: TTS provider selection, voice settings, speed mapping, caching, and playback.
- `audio-manager-module.js`: master, speech, music, and sound effect volume, music playback, sound effects, and music ducking.
- `ui-controller-module.js`, `ui-display-handler-module.js`, `ui-input-handler-module.js`, `options-ui-module.js`: book UI, command input, options, top-bar controls, and game API calls.
- `choice-display-module.js`: choice-mode UI, click selection, keyboard-letter assignment, and future choice-template routing.
The static server sends no-cache headers for local development so stale ES modules do not mask changes. If the browser console shows `onpage-dialog.preload.js:121 Uncaught ReferenceError: browser is not defined`, ignore it; that comes from the installed ad blocker, not this project.
## Story Markup
Plain paragraphs are rendered paragraph by paragraph. Normal following paragraphs are horizontally indented and do not get a blank line between them. Special block markers change the treatment of the next paragraph.
Inline Markdown emphasis:
```text
*italic* or _italic_
**bold** or __bold__
***bold italic*** or ___bold italic___
```
Right-page glossary notes:
```text
The train stops at Eibenreith.
#gloss[Eibenreith](A fictional alpine town in the Kaiserpunk setting.)
```
Glossary markup is a normal story tag scoped to the paragraph/block it is attached to. The UI finds every matching visible instance of the term in that right-page block and adds a hover/focus note. The tag itself is not displayed, is not sent to TTS, and is ignored by choices and command history. Avoid raw Ink control characters in the explanation; `|`, `{`, and `}` must be escaped in Ink as `\|`, `\{`, and `\}` if they are needed literally.
Canonical block/media/control tags use Ink-style `#` syntax. In Ink these are real Ink tags. In YAML and Z-code narrative output, leading `#...` lines are parsed by the server into the same structured `StoryTag` objects before reaching the client. The browser only consumes structured `TurnResult` objects.
Tag format:
```text
#key
#key[value]
#key[value](options)
#key:value
```
For Ink choices, put choice-local tags under the choice they belong to. Explicit keyboard letters are supported with `# letter[x]`, `#letter[x]`, or the colon form `#key:x`; the client reserves those keys first, then assigns the remaining visible choices from `1` through `0`, then `A` through `Z` in order. `#optional` renders the choice in italic. `# action[name]` or `#action:name` is parsed as a category/template hint for future choice layouts, although the current UI displays all choices in one list.
Chapter:
```text
#chapter[The Mysterious Mansion]
The first paragraph uses a drop cap and no first-line indent.
Following paragraphs use the normal paragraph indent.
```
The heading is centered, italic, and uses the same text face as the body. The first paragraph after a chapter marker is unindented and receives the drop cap treatment.
Section or text block:
Common tags:
```text
#chapter[Title]
#section
The first paragraph starts a separated block without horizontal indent.
The following paragraph returns to the normal indent.
#image[file.png](landscape|portrait|square, pause=2)
#music[file.mp3](crossfade|queue|cut, loop=true, lead=5)
#sfx[file.ogg](duration=4, fade=true)
#gloss[Term](Explanation shown on hover.)
#score[Optional score text]
#achievement[Optional achievement text]
#alert[Optional player hint]
#error[Optional error text]
```
`#textblock` is treated the same way. The first paragraph after the marker is separated from previous content by one line of vertical space.
Images are story blocks:
Choice-local tags:
```text
#image[mansion-rain.jpg](landscape)
#image[portrait-letter.jpg](portrait pause=2)
#image[seal.png](square lead=1.5)
#key:x
#optional
#action[name]
```
Image file names are relative to `public/images/`. `landscape`/`widescreen` and `square` images are centered, near full page width, and line-snapped. `portrait` images sit beside prose at half page width. Image pauses (`pause=`, `delay=`, `lead=`, or a bare `2s`) are skippable and do not block background TTS preparation.
Sound effects are story tags:
```text
#sfx[squeaky-door.ogg]
#sfx[church-bells.ogg](max=8 fade fade-duration=2)
The door opens and the hall exhales.
```
The tag is parsed by the server into a `StoryTag` object. Sound effect paths are relative to `public/sounds/`. Optional parameters can limit playback (`max=`, `duration=`, `stop-after=`, `fade-after=`), choose the end mode (`fade` or `stop`/`cut`), and set `fade-duration=`.
Music can be placed as a block:
```text
#music[rain-theme.ogg](crossfade, loop, lead=4)
```
Music paths are relative to `public/music/`. Supported modes are `queue`, `crossfade`, and `cut`. Use `loop` or `once` to control repetition. `lead=<seconds>` delays the following text/TTS paragraph so the music can play alone before narration continues. To place that pause between a chapter heading and the dropcapped first paragraph, put the music tag after the chapter tag and before the first prose paragraph; TTS generation for the next spoken paragraph continues during the lead pause.
Game-state and player-message tags:
```text
#score[You found the quiet ending.]
#error[Ink story ended without an explicit ending tag.]
#achievement[First Steps]
#alert[Try examining objects before using them.]
```
`#score[...]` marks an intended ending and opens a localized ending popup when the turn reaches `inputMode: end`. `#error[...]` marks an unrecoverable ending and opens an error popup. If an Ink story runs out of content without an explicit `#score[...]` or `#error[...]`, the Ink engine emits an `#error[...]` tag. `#achievement[...]` and `#alert[...]` open localized queued popups while the game continues.
## Architecture Documentation
`SPECIFICATION.md` is the canonical architecture and implementation specification. `TODO.md` is the canonical progress and remaining-work list. The former loose Ink and Z-code inclusion notes have been folded into those two files.
## Assets
- `public/sounds/`: sound effects referenced by `#sfx[file]` tags.
- `public/music/`: background music referenced by `#music[file](...)` tags.
- `public/images/`: story images referenced by `#image[file](...)`.
- `public/fonts/`: font assets used by the book UI.
Keep third-party assets licensed for local redistribution, and document source and license in the folder README or alongside the file.
## Typography And Playback Behavior
The renderer is designed to behave like a scaled static book page. The page keeps its aspect ratio, and text sizes and word positions scale relative to the page instead of reflowing unpredictably at small browser sizes.
Text processing order:
1. Parse story markup and remove non-display media markers.
2. Apply Markdown emphasis spans and right-page glossary annotations.
3. Run SmartyPants for typographic punctuation.
4. Apply Hyphenopoly for the selected language.
5. Calculate line breaks with the Knuth-Plass algorithm.
6. Render absolutely positioned word spans and animate them in sync with audio or estimated duration.
When real TTS audio is available, animation duration is driven by measured audio length. With TTS disabled or unavailable, duration is estimated from text length and the persisted speed setting.
Fast-forwarding by page click or space completes the active animation and fades/stops current TTS playback so queued content can proceed.
The right page history is line-addressed rather than natively scrolled. The page has a fixed line count, all block heights snap to whole lines, and the custom scrollbar represents virtual history line position. The DOM keeps a moving window of history blocks around the active line instead of paginating the story.
## Changelog
### 2026-05-17
- Added Ink engine support with source compilation, engine config files, game metadata, locale-driven UI text, choice mode, keyboard choice letters, and one-list choice rendering.
- Added line-addressed right-page history, save/load reconstruction, image restoration, custom scrollbar plumbing, and virtual block-window rendering.
- Added story image rendering for landscape, portrait, and square images, including line-snapped sizing and portrait text exclusion.
- Added localized popups for endings, errors, achievements, and alerts through the tag channel.
- Added credits and third-party license UI.
- Added per-volume mute toggles and configurable music ducking amount.
- Added German typography handling for dialogue guillemets based on game metadata language.
### 2026-05-14
- Consolidated usage, markup, and architecture documentation into `README.md` and `TODO.md`.
- Added no-cache static serving and module URL cache busting so browser reloads pick up JS changes reliably during development.
- Fixed module loader dependency ordering so modules are initialized only after their declared dependencies are ready.
- Added the placeholder game API for `newGame`, `loadGame`, `saveGame`, `hasSaveGame`, `getSaveGames`, and `isGameRunning`.
- Changed the web UI to require a manual game start before showing the command input, which keeps browser audio autoplay restrictions manageable.
- Implemented story markup for chapters, text blocks, Markdown emphasis, image placeholders, sound effects, and music cues.
- Added music block parameters for playback mode, loop/once behavior, and lead-in delay.
- Added sound and music asset folders and playback plumbing for sound effects and background music.
- Added music ducking while TTS is active.
- Reworked book typography around Knuth-Plass line breaking, Hyphenopoly hyphenation, SmartyPants, paragraph indentation rules, drop caps, and responsive page scaling.
- Reworked TTS provider behavior, speed mapping, persistence, caching keys, top-bar/options synchronization, and OpenAI voice validation.
- Added development notes for ignoring the unrelated ad-blocker console error.
### Earlier Prototype Work
- Established the original animated fiction prototype with inkjs, SmartyPants, Hyphenopoly, Knuth-Plass line breaking, custom animation scheduling, save/load concepts, and media tags.
- Split the client from a monolithic prototype into focused modules for text processing, layout, animation, audio, persistence, TTS, and UI control.
Explicit choice keys are reserved first. Remaining choices receive keys from `1` through `0`, then `a` through `z`.