Add ink integration UI and media playback

This commit is contained in:
2026-05-15 21:23:46 +02:00
parent 44dc64f830
commit f2e786d5bc
89 changed files with 6561 additions and 556 deletions
+50 -15
View File
@@ -56,6 +56,8 @@ This means:
All engines author tags using the same `key[value]` bracket convention. For Ink this maps directly to native `# key[value]` tags. For YAML and Z-Code the server synthesises equivalent tag objects.
Ink choice text uses square brackets for its own display/control syntax. For choice-local tags, the integration therefore also accepts the prototype form `# key: value` and normalises it to the same structured tag object. Prefer `# letter: o` and `# action: examine` on choices when inkjs treats bracketed tags as choice text.
| Tag | Scope | Meaning |
|---|---|---|
| `music[filename]` | paragraph or global | Start looping music track |
@@ -70,6 +72,7 @@ All engines author tags using the same `key[value]` bracket convention. For Ink
| `title[text]` | global (first turn) | Set document/story title |
| `author[text]` | global (first turn) | Set byline |
| `action[category]` | choice | Sort this choice into a named column |
| `letter[x]` | choice | Reserve keyboard letter `x` for this choice |
### 1.3 Parsed Tag Object Shape
@@ -109,7 +112,7 @@ These work identically on all three servers:
| Direction | Event | Payload |
|---|---|---|
| client → server | `gameApi` | `{ method, args }``respond(result)` |
| server → client | `gameIntroduction` | `{ title, author, inputMode }` |
| server → client | `narrativeResponse` | `TurnResult` |
| server → client | `gameSaved` | `{ slot }` |
| server → client | `gameLoaded` | `{ slot }` |
| server → client | `error` | `{ message }` |
@@ -128,6 +131,7 @@ This is the core change. Previously `narrativeResponse` carried `{ text, gameSta
```ts
interface TurnResult {
turnId: number; // Unique ascending id within the game session
paragraphs: ParagraphResult[]; // Ordered list of story paragraphs this turn
choices: ChoiceResult[]; // Available choices (empty in text-input mode)
inputMode: 'choice' | 'text' | 'end';
@@ -149,10 +153,11 @@ interface ChoiceResult {
text: string; // Display text (SmartyPants applied)
tags: StoryTag[]; // Per-choice tags, e.g. action[examine]
category?: string; // Derived from action[...] tag for column grouping
letter?: string; // Optional reserved keyboard letter, derived from letter[x]
}
```
The old `{ text }` field is removed. For backward compatibility during transition, the server may also emit a flattened `text` field containing all paragraph texts joined with newlines.
The old flattened `{ text }` field is removed. Servers must emit `TurnResult` only.
### 2.3 `playerCommand` vs `chooseChoice`
@@ -245,6 +250,34 @@ async function handleGameApi(socket, method, args): Promise<object> {
## 5. Client-Side Changes
### 5.0 Choice UI Architecture
The first Ink integration should ship with the simplest useful choice UI:
- Display all available choices in one ordered list.
- Ignore choice grouping tags visually for now.
- Assign every visible choice a keyboard letter.
- Allow at most 26 visible choices (`A` through `Z`), which is enough for the current interaction model.
- Choices with explicit `letter[x]` tags reserve that letter first.
- Remaining choices receive letters in ascending screen order, skipping letters already reserved by tags.
- The chosen letter is shown in the UI and pressing that letter selects the same choice as clicking it.
- Letter matching is case-insensitive.
The architecture must still be ready for later choice templates. The choice renderer should normalize choices into a presentation model:
```ts
interface ChoicePresentation {
index: number;
text: string;
tags: StoryTag[];
category?: string;
letter: string;
templateCell: string; // initially always "default"
}
```
The first implemented template has exactly one full-size cell named `default`. It receives every choice whose tags do not match a registered template cell. A later template can register cells such as `examine`, `ask`, `inventory`, or `reflect`, and route choices by `action[...]`, `category`, or a future `cell[...]` tag without changing the server protocol.
### 5.1 Tag Event Dispatch
**Modified: `game-loop-module.js`** (or `socket-client-module.js`)
@@ -288,12 +321,13 @@ Remove all tag-detection regex from the text stream. The module now only applies
**Effort: ~150 lines new**
Renders choice columns, exactly as in the prototype's `createChoiceContainer`. Registered in `module-registry.js`.
Renders available choices from the canonical `TurnResult.choices` array. Registered in `module-registry.js`.
- Listens for a `story:choices` event (dispatched by `game-loop-module.js` from the `TurnResult.choices` array).
- Groups choices by `category` field into named columns with localised headers.
- Registers keyboard shortcuts (A/B/C... or 1/2/3... depending on whether choices are categorised).
- Uses a template object with one initial full-size `default` cell.
- Assigns keyboard letters from `letter[x]` tags first, then fills remaining choices with `A` through `Z` in visible order.
- On click/keypress: calls `socketClient.sendChoice(index)``gameApi { method: 'chooseChoice', args: [index] }`.
- Future grouped or column layouts should be implemented by adding more template cells and routing rules, not by changing the server protocol.
### 5.6 `ui-input-handler-module.js`
@@ -361,13 +395,14 @@ On `narrativeResponse`:
## 8. Recommended Implementation Order
1. Extract `server-base.ts`; verify YAML engine still works.
2. Define `TurnResult` + `StoryTag` interfaces.
3. Write `tag-parser.ts`; unit test it.
4. Rewrite `GameRunner` to produce `TurnResult`; update `server.ts` to emit it.
5. Update `game-loop-module.js` to consume `TurnResult` and dispatch tag events.
6. Update `audio-manager-module.js` and `ui-display-handler-module.js` to listen for tag events.
7. Remove tag parsing from `markup-parser-module.js`.
8. Build `choice-display-module.js` and `ui-input-handler` mode switching.
9. Build `InkEngine` and `server-ink.ts`; smoke-test with a compiled `.ink.json`.
10. Add Z-Code server (see `zcode_inclusion.md`).
1. Define `TurnResult` + `StoryTag` interfaces.
2. Write `tag-parser.ts`; parse the single canonical tag shape `key[value](param)`.
3. Update the client socket pipeline to consume canonical `TurnResult` objects only.
4. Dispatch structured `story:tag`, `story:choices`, and `story:input-mode` events from the socket adapter.
5. Add `choice-display-module.js` with the single-cell default template and letter assignment described in §5.0.
6. Update `audio-manager-module.js` and `ui-display-handler-module.js` to listen for `story:tag` events by translating them into the current media/display paths.
7. Convert Zork server emission from flattened text to canonical `TurnResult`.
8. Convert YAML server introduction/responses to canonical `TurnResult`.
9. Extract `server-base.ts`; verify YAML and Zork still work.
10. Build `InkEngine` and `server-ink.ts`; smoke-test with a compiled `.ink.json`.
11. Once all engines emit structured tags, remove structural tag parsing from `markup-parser-module.js`.