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
does not scroll! This should have no scrollbar or scrolling css whatsoever. It's fixed size. The #story_scrollbar should have a fixed height of 100% of #page_right and be placed absolutely over the parent, so it cannot be influenced by anything next or below it and it does not let clicks bubble through it to anything else below (no unpausing story on scrollbar usage). Neither #story or anything inside it should have a scrollbar!
-2.) Every text or image block that is inserted into the dom should update it's entry in the historyDB to include it's height in lines. Every time this happens the height is added to a game engine value that stores the length of the game history in lines. The #page_rigiht is considered to have a fixed height in lines (calculate id). The #story_scrollbar is calculated using the position and size in lines of the current page view to the game history. For this calculation the history ends at the latestRenderedBlockId, not the latestBlockId.
-3.) When a new block is put into the dom, the #story div is moved up (dynamically with an ease in/ease out animation) within the #page_rigt div until its bottom edge aligns with the bottom edge of it's parent (that's scrolling to the bottom). Any time this position changes the scrollbar is updated accordingly.
-4.) Scrolling using the arrow up or arrow down keys or the mousewheel moves the pane smoothly up and down until it's end. There should be a value storing the block currently shown in the center of the page. The system dynamically adds and removes blocks to the top and bottom of the #paragraphs list so that there are 20 blocks above and 20 blocks below the central block (if available) at all times.
-5.) Any kind of manual scrolling, be it via mousewheel, arrow keys up/down, or using the scrollbar immediately disables autoplay.
-6.) Inserting images should first insert the image covered, then scroll down, and only once the scrolling animation finished start the revealing fade in of the image which should take 2 seconds.
-
-
-Scrolling using the arrow keys or mousewheel works fine now, But I found the following bugs already:
-1.) Autoplay does not disable on scrolling.
-2.) Restoring the chapter beginning shows the chapter heading as a normal line of text and no longer produces the drop-cap.
-3.) Loading a game that was saved while choices were present does not restore the choice dialog.
-4.) The game now seems to always scroll to the center of the page to add/animate in new content. Please keep this position at the page bottom as it was, only adding as much space as needed for the new content (like before).
-5.) Make sure autoscrolling or manual scrolling always stops at the nearest line boundary, so no cut of lines can be visible.
-6.) Suggest a plan how to fix the scrollbar controls. They look correct now. But moving them with the mouse feels clunky and somethimes it triggers strange behavior. We need a solution where the bar can be moved freele up and down without doing anything yet. Once released the content between the current position and the target position including the necessary margins are loaded before the scrolling is animated and then the now unneccessary blocks unloaded.
-
-
-That sounds still too complicated! Why invalidate the layout index?
-Assume the following:
-1.) The #right_page div has a size relative to the window. There is ONE line height value, which is a divisor or the page height/the page height has a fixed number of lines: Line height = Page height/fixed number of lines.
-2.) All content has an exact multiple of line height as height all margins and paddings included.
-3.) Therefore any coordinates or pixel sizes of the virtual content pane can be derived mathematically from line coordinates.
-4.) Scrolling means translating the content vertically (with ease in/eas out animation) to the closest position where the page edges aligns with line edges.
-5.) Since stored content does not change line numbers after creation, and (visible) content is never added in between existing blocks, updating the cumulative values should be unnecessary.
-6.) Scrolling to the bottom means to scroll to the position where the bottom edge of the last line of the last element aligns with the bottom edge of the page.
-7.) Scrolling to the top means to scroll to the position where the top edge of the first line of the first block aligns with the top edge of the page.
-8.) Scrolling to the bottom to insert new content means the same as 6. but with the new content already added invisibly to the block history (advancing the current block counter), scrolling to the position and only then activating the fade in animation.
-9.) In the case of portrait format images next to text the cumulative line positions can overlap but still should border on the line edges.
-10.) Scrolling to a random position means to first load all content between the starting point and the target point + additional blocks in the movement direction into the right_page. Then scrolling so the bottom edge of the requested line aligns with the bottom edge of the page, if there is enough content above it otherwise it's the same as 7.) scrolling to the top. After the scroll has finished blocks a certain distance from the reached position are unloded.
-
-
-Looks like you partially work with outdated specs: Here the last version:
-1.) The #right_page div has a size relative to the window. There is ONE line height value, which is a divisor or the page height/the page height has a fixed number of lines: Line height = Page height/fixed number of lines.
-2.) All content has an exact multiple of line height as height all margins and paddings included.
-3.) Therefore any coordinates or pixel sizes of the virtual content pane can be derived mathematically from line coordinates.
-4.) Scrolling means translating the content vertically (with ease in/eas out animation) to the closest position where the page edges aligns with line edges.
-5.) Since stored content does not change line numbers after creation, and (visible) content is never added in between existing blocks, updating the cumulative values should be unnecessary.
-6.) Scrolling to the bottom means to scroll to the position where the bottom edge of the last line of the last element aligns with the bottom edge of the page.
-7.) Scrolling to the top means to scroll to the position where the top edge of the first line of the first block aligns with the top edge of the page.
-8.) Scrolling to the bottom to insert new content means the same as 6. but with the new content already added invisibly to the block history (advancing the current block counter), scrolling to the position and only then activating the fade in animation.
-9.) In the case of portrait format images next to text the cumulative line positions can overlap but still should border on the line edges.
-10.) Scrolling to a random position means to first load all content between the starting point and the target point + additional blocks in the movement direction into the right_page. Then scrolling so the bottom edge of the requested line aligns with the bottom edge of the page, if there is enough content above it otherwise it's the same as 7.) scrolling to the top. After the scroll has finished blocks a certain distance from the reached position are unloded.
-
-Put that wherever you keep your project specs but refine it with the following information:
-DO NOT do pagination. The 41 blocks mean there should be one line that is the current position. If there is enough lines content before it this position is always asumed to be the last line of the page (line 25). Whichever block this line belongs to is the active block. The system should always keep 20 blocks before the active block, the active block, and 20 blocks after the active block loaded. That's where the 41 blocks come from. The moment scrolling shifts the active line into a new block in any direction one other block is to be loaded (in that direction) and one is to be unloaded (opposite this direction). If the coordinate is not reached by traversal but jumped to 20 blocks before the active block, the active block, all blocks between the starting block and the target block, the target block and 20 blocks after the target block should be loaded so the whole range can be traversed. If this exceeds a sensible amount of blocks in the dom, let's say 150 blocsk in total, all content on the page is faded out and unloaded, then the target block and 20 blocks before and after it (as available) are loaded before the content of the page is faded in again.
-
-Please give me feedback whether you understand how I imagine this to work, If you agree this is feasable and then apply it to whatever your notes you keep about the project specifications.
-
-What works and what doesnt:
-1.) up arrow and down arrow perfectly scrolls up and down line by line, but pressing the button again while it is still scrolling leads to stuttering movement. Also all formats and layouts seem to be correct.
-2,) Using the mousewheel or page up and down does move the content in the right direction but seemingly a random number of lines (about 1-5) not 24 or whatever the mousewheel speed says. Find out why? How is wheel speed translated into a scroll command?
-3.) Home loads a completely new page which takes some time and then flickers into existence ... then it correctly scrolls to the top.
-4.) End removes all content from the page, takes some time then you can see the scroll bar go to the bottom, but content does not re-appear before or after the scroll. Only manually scrolling using the arrow key shows the content again.
-5.) Scrolling correctly cancels playback and animation. Resuming correctly scrolls to the bottom. Sometimes it resumes play as intended, but under certain conditions new TTS audio is played, but no new text is added. The game just always scrolls down to the last visible block and stays there, while the story and audio continues.
-Do not fix it yet. Explain to me step by step why? Do not guess. Do not invent an explanation! Trace the what the program actually does from triggering event to end state and explain to me why this should work, cannot work, is complete or was left in an unfinished state!
diff --git a/README.md b/README.md
index d5ce624..c10d20f 100644
--- a/README.md
+++ b/README.md
@@ -20,6 +20,9 @@ The web app starts on `http://localhost:3000` when launched through `src/index.t
```powershell
npm run dev # Start the web UI through ts-node/nodemon
+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:zork # Start the Z-code/Zork engine server
npm run start # Run the compiled web server from dist/
npm run build # Compile TypeScript
npm run test # Run Jest tests
@@ -28,6 +31,8 @@ npm run start:cli # Run the CLI interface
npm run dev:cli # Run the CLI interface through ts-node/nodemon
```
+Each game engine also has an inspect command for debugger work: `npm run dev:ink:inspect`, `npm run dev:yaml:inspect`, and `npm run dev:zork:inspect`.
+
## Configuration
Environment variables are loaded from `.env`.
@@ -67,6 +72,7 @@ Major modules:
- `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.
@@ -82,7 +88,18 @@ Inline Markdown emphasis:
***bold italic*** or ___bold italic___
```
-Canonical block/media tags use Ink-style `#` syntax. In Ink these are real Ink tags. In YAML and Zork 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.
+Canonical block/media/control tags use Ink-style `#` syntax. In Ink these are real Ink tags. In YAML and Zork 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 letters first, then assigns the remaining visible choices from `A` through `Z` in order. `# 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:
@@ -108,23 +125,25 @@ The following paragraph returns to the normal indent.
`#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 parsed for future rendering:
+Images are story blocks:
```text
#image[mansion-rain.jpg](landscape)
#image[portrait-letter.jpg](portrait pause=2)
+#image[seal.png](square lead=1.5)
```
-Image file names are relative to `public/images/`. `widescreen` is accepted as an alias for `landscape`.
+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/`.
+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:
@@ -134,6 +153,17 @@ Music can be placed as a block:
Music paths are relative to `public/music/`. Supported modes are `queue`, `crossfade`, and `cut`. Use `loop` or `once` to control repetition. `lead=
` 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.
+
## Assets
- `public/sounds/`: sound effects referenced by `#sfx[file]` tags.
@@ -160,8 +190,20 @@ When real TTS audio is available, animation duration is driven by measured audio
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 `CLIENT_TODO.md`.
diff --git a/THIRD_PARTY_AUDIT.md b/THIRD_PARTY_AUDIT.md
new file mode 100644
index 0000000..e83c84a
--- /dev/null
+++ b/THIRD_PARTY_AUDIT.md
@@ -0,0 +1,47 @@
+# Third-Party Library Audit
+
+Date: 2026-05-17
+
+## Summary
+
+The project currently uses the expected browser-side typography/story libraries plus additional runtime packages:
+
+- inkjs
+- SmartyPants.js
+- Hyphenopoly
+- Knuth-Plass line breaking support (`knuth-and-plass.js`, `linebreak.js`, `linked-list.js`)
+- Kokoro JS browser bundle
+- Server/runtime npm packages: Express, Socket.IO, OpenAI SDK, Axios, cors, dotenv, js-yaml, ifvms
+- EB Garamond font files
+
+## Browser-vendored files
+
+| Component | Files | Upstream/latest check | Local status |
+| --- | --- | --- | --- |
+| SmartyPants.js | `public/js/smartypants.js` | Local header says `smartypants.js 0.0.6`; npm `smartypants` latest is `0.2.2`. The old `smartypants.js` package name is unpublished from npm. | Not byte-identical to npm `smartypants` 0.0.5, 0.0.9, or 0.2.2. Treat as modified/older vendor code. |
+| Hyphenopoly browser files | `public/js/Hyphenopoly.js`, `public/js/Hyphenopoly_Loader.js`, `public/js/hyphenopoly.module.js`, `public/js/patterns/*.wasm` | Browser header says `5.2.0-beta.1`; npm dependency is `6.0.0`; npm latest is `6.1.0`. | `Hyphenopoly.js` is effectively 5.2.0-beta.1 after line-ending normalization. `Hyphenopoly_Loader.js` has a small local/prototype difference in `H.hide`. Browser copy is older than package/latest. |
+| Knuth-Plass adapter | `public/js/knuth-and-plass.js` | No authoritative upstream identified from headers or npm metadata. | Modified from the prototype copy and currently application-owned adapter code. |
+| Line breaking support | `public/js/linebreak.js`, `public/js/linked-list.js` | No authoritative upstream identified from headers. Not the npm `linebreak` package 1.1.0. | Identical to prototype copies. `linked-list.js` still has a suspicious `get last() { return this.last; }` accessor inherited from the prototype. |
+| Kokoro JS browser bundle | `public/js/kokoro-js.js` | npm `kokoro-js` latest is `1.2.1`; installed is `1.2.0`. | Byte-identical to `kokoro-js@1.2.0/dist/kokoro.web.js`; not latest. |
+
+## Direct runtime npm packages
+
+| Package | Installed | Latest checked | License | Status |
+| --- | --- | --- | --- | --- |
+| `inkjs` | 2.4.0 | 2.4.0 | MIT | Current. |
+| `hyphenopoly` | 6.0.0 | 6.1.0 | MIT | Not latest. Browser vendored files are older than this dependency. |
+| `kokoro-js` | 1.2.0 | 1.2.1 | Apache-2.0 | Not latest. |
+| `ifvms` | 1.1.6 | 1.1.6 | MIT | Current. |
+| `openai` | 4.91.0 | 6.38.0 | Apache-2.0 | Not latest major. |
+| `socket.io` | 4.8.1 | 4.8.3 | MIT | Not latest patch. |
+| `express` | 5.1.0 | 5.2.1 | MIT | Not latest patch. |
+| `axios` | 1.8.4 | 1.16.1 | MIT | Not latest. |
+| `cors` | 2.8.5 | 2.8.6 | MIT | Not latest patch. |
+| `dotenv` | 16.4.7 | 17.4.2 | BSD-2-Clause | Not latest major. |
+| `js-yaml` | 4.1.0 | 4.1.1 | MIT | Not latest patch. |
+
+## Notices
+
+The UI-readable license and credit notice is `public/THIRD_PARTY_NOTICES.md`.
+
+The root `THIRD_PARTY_NOTICES.md` points to that served file so the repository has an obvious project-level notice entry.
diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md
new file mode 100644
index 0000000..60c790b
--- /dev/null
+++ b/THIRD_PARTY_NOTICES.md
@@ -0,0 +1,7 @@
+# Third-Party Notices
+
+The browser-visible third-party notices and license text live at:
+
+`public/THIRD_PARTY_NOTICES.md`
+
+That file is served by the game UI and is the source used by the in-game credits dialog.
diff --git a/config/engines/ink.json b/config/engines/ink.json
index e26400a..e4dc66b 100644
--- a/config/engines/ink.json
+++ b/config/engines/ink.json
@@ -1,10 +1,10 @@
{
"engine": "ink",
- "locale": "en_US",
+ "locale": "de_DE",
"paths": {
- "mainGameFile": "data/ink/kaiserpunk.ink.json",
- "inkSource": "data/ink-src/kaiserpunk.ink",
- "inkCompiled": "data/ink/kaiserpunk.ink.json",
+ "mainGameFile": "data/ink/eibenreith.ink.json",
+ "inkSource": "data/ink-src/eibenreith.ink",
+ "inkCompiled": "data/ink/eibenreith.ink.json",
"music": "public/music",
"sfx": "public/sounds",
"images": "public/images"
@@ -12,8 +12,9 @@
"metadata": {
"title": "Eibenreith",
"author": "Georg Tomitsch",
- "subtitle": "A Kaiserpunk Adventure",
- "version": "0.0.1",
- "copyright": "2026 by Bad Tools Studio"
+ "subtitle": "Ein Kaiserpunk Abenteuer",
+ "version": "0.0.2",
+ "language": "de_DE",
+ "copyright": "© 2026 Bad Tools Studio"
}
}
diff --git a/config/engines/yaml.json b/config/engines/yaml.json
index e9923a6..a865863 100644
--- a/config/engines/yaml.json
+++ b/config/engines/yaml.json
@@ -12,6 +12,7 @@
"author": "AI Interactive Fiction",
"subtitle": "An open-world text adventure",
"version": "1.0.0",
+ "language": "en_US",
"copyright": "Prototype content for local development."
}
}
diff --git a/config/engines/zork.json b/config/engines/zork.json
index 3f7b3b4..71c6c12 100644
--- a/config/engines/zork.json
+++ b/config/engines/zork.json
@@ -13,6 +13,7 @@
"author": "Infocom",
"subtitle": "A narrated Z-code adventure",
"version": "1.0.0",
+ "language": "en_US",
"copyright": "Use only with a legally supplied Z-code story file."
}
}
diff --git a/data/ink-src/eibenreith.ink b/data/ink-src/eibenreith.ink
new file mode 100644
index 0000000..0205e41
--- /dev/null
+++ b/data/ink-src/eibenreith.ink
@@ -0,0 +1,1760 @@
+// Eibenreith.ink
+// Deutscher Intro-Entwurf für ein choice-basiertes Horror-Textspiel in Ink.
+// Prinzip: freie Aktionswahl im Abteil; im Hintergrund wird Valerie in sinnvoller Reihenfolge definiert.
+// Erzählung: zweite Person Singular. Figurenrede: historisch/formales „Sie“.
+
+VAR birth_class = "unset"
+VAR title_part = ""
+VAR given_names = ""
+VAR common_name = ""
+VAR surname = ""
+
+VAR religion_stance = "unset"
+VAR supernatural_belief = "unset"
+VAR supernatural_senses = "unset"
+
+VAR body_detail = "unset"
+VAR hair_colour = "unset"
+VAR hairstyle = "unset"
+VAR complexion_detail = "unset"
+VAR face_detail = "unset"
+VAR outfit_detail = "unset"
+VAR appearance_done = false
+
+VAR baggage_style = "unset"
+VAR viktor_relation = "unset"
+
+VAR saw_window = false
+VAR observed_viktor = false
+VAR glanced_mirror_early = false
+
+VAR tut_choice_intro = false
+VAR tut_optional_intro = false
+VAR tut_character_intro = false
+VAR tut_dialog_intro = false
+VAR tut_manners_intro = false
+VAR tut_gated_intro = false
+
+VAR lover = 0
+VAR sapphic = 0
+VAR detective = 0
+VAR careless = 0
+VAR eccentric = 0
+VAR class_confidence = 0
+VAR medium_reputation = 0
+VAR court_loyalty = 0
+VAR viktor_trust = 0
+VAR viktor_suspicion = 0
+VAR supernatural_exposure = 0
+
+-> intro_train
+
+=== intro_train ===
+
+Der Zug hatte Wien hinter sich gelassen, doch Wien hatte dich noch nicht freigegeben. #chapter[Eibenreith] #music[Kaiserpunk Waltz.mp3](crossfade, loop, lead=8)
+
+Es hing noch am schwarzen Glanz deiner Reisestiefel, am Schnitt deines Mantels, am engen kleinen Gefängnis deiner Handschuhe. Es lag im Siegel des Schreibens, das in deinem Ridikül ruhte, im Geruch von Kohlenrauch, der sich selbst in die Polster der ersten Klasse geschlichen hatte, und in der Tatsache, dass Herr Viktor Nowak dir gegenübersaß, als wäre dieses Abteil kein mit Samt, Messing und poliertem Holz ausgekleideter Reiseraum, sondern ein provisorisches Amtszimmer auf Rädern.
+
+-> train_compartment
+
+=== train_compartment ===
+
+{not tut_choice_intro:
+ #alert[Optionen beginnen mit einem fett gesetzten Aktionswort. Dieses Wort zeigt, was du tust: schauen, untersuchen, lesen, bestimmen, fragen, antworten oder warten. Manche klassische Aktionen haben feste Tasten, etwa L für Schaue und X für Untersuche.]
+ ~ tut_choice_intro = true
+}
+
+{ saw_window and observed_viktor and birth_class != "unset" and religion_stance != "unset" and supernatural_senses != "unset" and appearance_done:
+ -> first_viktor_exchange
+}
+
+* {not saw_window} [___Schaue___: _Aus dem Fenster._] #action:orientation #optional #key:l
+ ~ saw_window = true
+ -> look_out_window
+
+* {not observed_viktor} [___Untersuche___: _Viktor._] #action:orientation #optional #key:x
+ ~ observed_viktor = true
+ -> observe_viktor
+
+* {birth_class == "unset"} [__Bestimme__: Deine Herkunft anhand des Abteils.] #action:thinking
+ -> define_class_and_name
+
+* {religion_stance == "unset"} [__Lies__: Das Schreiben in deinem Ridikül.] #action:orientation
+ -> define_religion_and_supernatural
+
+* {birth_class == "unset" and not glanced_mirror_early} [__Bestimme__: Dein Spiegelbild.] #action:thinking
+ ~ glanced_mirror_early = true
+ Der Zug fährt in einen Tunnel. Für mehrere Sekunden gibt dir das Abteilfenster nur eine dunkle Andeutung zurück: Hut, Handschuhe, Gesicht, die schmale Linie des Kragens.
+
+ Das reicht nicht. Nicht für die Frau, die in Eibenreith aussteigen wird. Ein Spiegelbild ohne Namen ist noch keine Person; es ist nur ein Schatten, der gesellschaftlich korrekt sitzt.
+
+ -> train_compartment
+
+* {birth_class != "unset" and not appearance_done} [__Bestimme__: Dein Spiegelbild.] #action:thinking
+ -> define_appearance
+
+-
+-> first_viktor_exchange
+
+=== look_out_window ===
+
+{not tut_optional_intro:
+ #alert[Kursiv gesetzte Optionen sind Erkundungen. Sie liefern Beobachtungen, Stimmung oder Hinweise und treiben die Szene meist nicht sofort unwiderruflich weiter.]
+ ~ tut_optional_intro = true
+}
+
+Draußen zerfallen die letzten Ränder der Stadt in winterbraune Felder und Dörfer, deren Kirchtürme gegen den Pfiff der Lokomotive nichts auszurichten haben. Die Schienen nehmen sich das Land, ohne um Erlaubnis zu fragen. Dämme schneiden durch Obstgärten. Telegraphenstangen gleiten in regelmäßigen Abständen vorbei, eine nach der anderen, wie Gedanken, die man zu rasch verworfen hat. #sfx[steam-whistle.ogg]
+
+Du hattest erwartet, dass sich die Eisenbahn wie ein Sieg des Jahrhunderts anfühlen würde.
+
+Stattdessen fühlt sie sich wie ein Streit an. #image[suedbahn.png](landscape)
+
+Die Maschine wirft sich mit einer Gewalt nach Süden, die gute Gesellschaft niemals offen bewundert hätte. Die Lampen zittern in ihren Fassungen. Deine Tasse schlägt leise gegen die Untertasse. Jenseits der Scheibe beginnt das Land zu steigen, zuerst beinahe höflich, dann mit festerem Willen, bis die Bahnlinie selbst mit den Bergen zu verhandeln scheint: durch Steinbögen, schwarze Tunnel und Viadukte, die mit dem ganzen Selbstvertrauen kaiserlicher Ingenieurskunst über Schluchten gesetzt sind.
+
+-> train_compartment
+
+=== observe_viktor ===
+
+Viktor hat noch kein einziges Mal beeindruckt gewirkt.
+
+Seine Zivilkleidung ist korrekt genug, um keinen Widerspruch hervorzurufen: dunkler Gehrock, nüchterne Weste, Handschuhe, tadelloser Kragen, dazu die Haltung eines Mannes, der selbst im Sitzen nie ganz aufhört, im Dienst zu sein. Doch kein Schneider der Monarchie kann Disziplin verbergen.
+
+* [___Untersuche___: _Viktors Haltung._] #action:orientation #optional #key:x
+ Sie bleibt in seinen Schultern, in der Sparsamkeit seiner Bewegungen, in der Art, wie er selbst im Sitzen nie ganz aufhört, einen Raum zu sichern.
+
+* [___Schaue___: _Viktors Blick nach._] #action:orientation #optional #key:l
+ Seine Augen messen Türen, Fenster, Gepäcknetz, Korridor, dein Gesicht und wieder die Tür. Nicht gierig. Nicht unhöflich. Nur vollständig.
+
+* [___Untersuche___: _Viktors Kleidung._] #action:orientation #optional #key:x
+ Auf dem Papier ist er dein Sekretär und Reisebegleiter.
+
+ In Wahrheit ist er ein Offizier, den man einer heiklen Angelegenheit beigegeben hat; aus Kanälen, die Namen haben, aber sie nicht unnötig gebrauchen. Rittmeister Viktor Alois Nowak, auch wenn auf Jagdhaus Hohenreith niemand Anlass haben soll, ihn so zu nennen.
+
+-
+Eure Gastgeber haben um ein Medium ersucht. Die Kabinettskanzlei hat dich geschickt. Das Militär hat ihn geschickt, damit aus dir kein Skandal wird, ehe du nützlich werden kannst.
+
+-> train_compartment
+
+=== define_class_and_name ===
+
+{not tut_character_intro:
+ #alert[Optionen mit Bestimme formen Valerie. Sie sind Teil des Charakteraufbaus: Herkunft, Glaube, Fähigkeiten, Aussehen und Auftreten beeinflussen spätere Möglichkeiten.]
+ ~ tut_character_intro = true
+}
+
+Das Abteil beantwortet eine Frage, noch ehe Viktor sie stellen kann.
+
+* [__Bestimme__: Das Abteil wirkt, als sei es für Menschen gebaut, die nie darüber nachdenken müssen, ob sie hineingehören.] #action:thinking
+ #class:noble
+ ~ birth_class = "noble"
+ ~ class_confidence += 2
+ ~ court_loyalty += 1
+
+ Nicht der Luxus beunruhigt dich. Luxus ist nur Holz, Stoff, Messing, Bedienung, Stille. Entscheidend ist, ob die Diener zweimal hinsehen, ob der Schaffner die Stimme senkt, ob ein anderer Reisender deine Handschuhe prüft und beschließt, nicht nach deinem Auftrag zu fragen.
+
+ Du wurdest unter Menschen geboren, die solche Dinge früher verstanden als Freundlichkeit.
+
+ Du hast früh gelernt, dass jedes Zimmer einen Hof enthält, auch wenn kein Kaiser anwesend ist. Ein Mädchen deines Ranges wird darin unterrichtet, einzutreten, sich zu verneigen, vorgestellt, platziert und wieder vergessen zu werden; nur genug zu sprechen, mehr zu verstehen, als es zugibt, und zu wissen, dass ein Familienname zugleich Schlüssel und Kette sein kann.
+
+ Deine eigene Familie besitzt keinen großen Sitz, keine Schar von Verwaltern, kein altes Recht, Provinzen zu befehlen. Doch dein Name öffnete Türen in Wiener Salons, und sobald du in diesen Zimmern warst, lerntest du, Menschen Geschichten wiederholen zu lassen, die sie nur hatten andeuten wollen.
+
+ Dein Ruf als Medium ist nicht vom Himmel gefallen. Er wurde zusammengesetzt aus Halblicht, richtigen Vermutungen, sorgsamen Pausen und der Bereitschaft besser geborener Toren, Aufführung für Offenbarung zu halten.
+
+ Bevor der Hof dich benutzen konnte, musste die Gesellschaft dich erst erfinden.
+
+ -> choose_name_noble
+
+* [__Bestimme__: Du berechnest den Preis jedes Details, noch ehe du dich daran hindern kannst.] #action:thinking
+ #class:middle
+ ~ birth_class = "middle"
+ ~ class_confidence += 1
+
+ Die Polster, die Lampen, das polierte Furnier, die stille Bedienung an den Stationen: Nichts davon ist Zauberei. Es ist bezahlt. Verbucht. Irgendwo von jemandem aufgeschrieben, der Tinte an den Manschetten hat und eine Frau daheim, die weiß, wie lange Kerzen brennen dürfen, ehe das Haushaltsgeld Einspruch erhebt.
+
+ Du wurdest nicht für dieses Abteil geboren, aber nahe genug, um seine Regeln zu studieren.
+
+ Du wurdest in jenem breiten, unruhigen Gebiet zwischen Ehrerbietung und Ehrgeiz geboren. Deine Familie hatte Bücher, Rechnungen, Anstand, vielleicht ein Klavier, das niemand gut genug spielte, vielleicht einen Vater mit Amt, eine Mutter mit Besuchern, Brüder, von denen Aufstieg erwartet wurde, und Töchter, die nicht zeigen durften, dass auch sie danach hungerten.
+
+ Du lerntest Rechnen vor Etikette, Etikette vor Französisch und Französisch vor der Einsicht, wie leicht sich Männer von einer ruhigen Frauenstimme erklären lassen. Du stiegst auf, weil du zuhörtest. Du stiegst auf, weil du verstandest, dass Betrug, Glaube, Medizin, Tratsch, Politik und Kummer dieselben Türen in den menschlichen Geist benutzen.
+
+ Der Hof gibt ungern zu, dass er bürgerliche Tüchtigkeit braucht. Er borgt sie lieber aus, kleidet sie ordentlich ein und nennt sie Diskretion.
+
+ -> choose_name_middle
+
+* [__Bestimme__: Dir fällt zuerst auf, wie sauber alles ist.] #action:thinking
+ #class:working
+ ~ birth_class = "working"
+ ~ class_confidence -= 1
+
+ Der Samt sieht weich genug aus, um Fingerabdrücke zu verschlucken. Die Messingbeschläge sind von Händen poliert worden, die niemals hier sitzen werden. Das kleine Vorhangband ist dort abgenützt, wo andere Reisende, alle sicherer als du, es ohne Dankbarkeit berührt haben.
+
+ Du wurdest nicht auf dieser Seite des Dienstes geboren.
+
+ Du wurdest unter Menschen geboren, die wenig besaßen außer Verpflichtungen. Arbeit hatte einen Klang, bevor sie eine Bedeutung hatte: Wasser, Besen, Schritte, Atem, das Klirren von Geschirr, der Husten von Männern, die aus kalten Höfen kamen, Frauen, die Münzen unter dem Atem zählten. Du lerntest früh, dass die Hochgeborenen nicht aufmerksamer sind als andere. Sie müssen nur seltener aufmerksam sein.
+
+ Das war dein erster Vorteil.
+
+ Eine Dienstmagd weiß, welche Tür wichtig ist, weil sie die anderen benutzt. Eine Näherin lernt Körper, weil sie sie misst. Eine Zofe lernt Geheimnisse, weil feine Leute ihre Seelen wie Handschuhe liegen lassen, gewiss, dass niemand unter ihnen Hände hat.
+
+ Du stiegst auf durch Begabung, Protektion, Nachahmung, Nervenstärke und die furchtbare Bequemlichkeit, für harmlos gehalten zu werden. Als Wien zu flüstern begann, du sähest mehr als anständige Leute sehen, hattest du schon Jahre damit verbracht, das zu sehen, was anständige Leute übersahen.
+
+ -> choose_name_working
+
+=== choose_name_noble ===
+
+Wien kannte dich unter dem Namen, den die Gesellschaft brauchbar gemacht hatte.
+
+* [__Bestimme__: Valerie Eleonore Josepha] #action:thinking
+ ~ given_names = "Valerie Eleonore Josepha"
+ ~ common_name = "Valerie"
+* [__Bestimme__: Helene Cäcilie Franziska] #action:thinking
+ ~ given_names = "Helene Cäcilie Franziska"
+ ~ common_name = "Helene"
+* [__Bestimme__: Clara Theresia Leopoldine] #action:thinking
+ ~ given_names = "Clara Theresia Leopoldine"
+ ~ common_name = "Clara"
+* [__Bestimme__: Sophie Eleonore Auguste] #action:thinking
+ ~ given_names = "Sophie Eleonore Auguste"
+ ~ common_name = "Sophie"
+* [__Bestimme__: Mathilde Josepha Henriette] #action:thinking
+ ~ given_names = "Mathilde Josepha Henriette"
+ ~ common_name = "Mathilde"
+* [__Bestimme__: Therese Valerie Franziska] #action:thinking
+ ~ given_names = "Therese Valerie Franziska"
+ ~ common_name = "Therese"
+* [__Bestimme__: Ilona Theresia Eleonore] #action:thinking
+ ~ given_names = "Ilona Theresia Eleonore"
+ ~ common_name = "Ilona"
+* [__Bestimme__: Zdenka Eleonore Josepha] #action:thinking
+ ~ given_names = "Zdenka Eleonore Josepha"
+ ~ common_name = "Zdenka"
+-
+-> choose_surname_noble
+
+=== choose_surname_noble ===
+
+Dein Titel ist durch Geburt und durch die vorsichtige Bescheidenheit deiner Familie bestimmt: keine Gräfin, keine Fürstin, keiner jener glänzenden Namen, die Botschafter und Gläubiger wie Staub anziehen.
+
+Eine Freiin. Baronial. Brauchbar. Zugelassen, aber nicht thronend.
+
+* [__Bestimme__: Freiin von Rauhenfels] #action:thinking
+ ~ title_part = "Freiin von"
+ ~ surname = "Rauhenfels"
+* [__Bestimme__: Freiin von Traunegg] #action:thinking
+ ~ title_part = "Freiin von"
+ ~ surname = "Traunegg"
+* [__Bestimme__: Freiin von Ebenwald] #action:thinking
+ ~ title_part = "Freiin von"
+ ~ surname = "Ebenwald"
+* [__Bestimme__: Freiin von Arnsberg] #action:thinking
+ ~ title_part = "Freiin von"
+ ~ surname = "Arnsberg"
+* [__Bestimme__: Freiin von Reichenau] #action:thinking
+ ~ title_part = "Freiin von"
+ ~ surname = "Reichenau"
+* [__Bestimme__: Freiin von Waldstätten] #action:thinking
+ ~ title_part = "Freiin von"
+ ~ surname = "Waldstätten"
+-
+-> assemble_full_name
+
+=== choose_name_middle ===
+
+Die Salons, die zuerst über dich lachten und dich dann wieder einluden, lernten deinen Namen, bevor sie lernten, was er gekostet hatte.
+
+* [__Bestimme__: Clara Eleonore] #action:thinking
+ ~ given_names = "Clara Eleonore"
+ ~ common_name = "Clara"
+* [__Bestimme__: Anna Katharina] #action:thinking
+ ~ given_names = "Anna Katharina"
+ ~ common_name = "Anna"
+* [__Bestimme__: Helene Theresia] #action:thinking
+ ~ given_names = "Helene Theresia"
+ ~ common_name = "Helene"
+* [__Bestimme__: Rosa Franziska] #action:thinking
+ ~ given_names = "Rosa Franziska"
+ ~ common_name = "Rosa"
+* [__Bestimme__: Johanna Elise] #action:thinking
+ ~ given_names = "Johanna Elise"
+ ~ common_name = "Johanna"
+* [__Bestimme__: Katharina Sophie] #action:thinking
+ ~ given_names = "Katharina Sophie"
+ ~ common_name = "Katharina"
+* [__Bestimme__: Therese Leopoldine] #action:thinking
+ ~ given_names = "Therese Leopoldine"
+ ~ common_name = "Therese"
+* [__Bestimme__: Magdalena Cäcilie] #action:thinking
+ ~ given_names = "Magdalena Cäcilie"
+ ~ common_name = "Magdalena"
+-
+-> choose_surname_middle
+
+=== choose_surname_middle ===
+
+Dein Familienname enthält kein Partikel, das den Aufstieg abfedert. Er muss allein aufrecht stehen.
+
+* [__Bestimme__: Leitner] #action:thinking
+ ~ title_part = "Fräulein"
+ ~ surname = "Leitner"
+* [__Bestimme__: Wagner] #action:thinking
+ ~ title_part = "Fräulein"
+ ~ surname = "Wagner"
+* [__Bestimme__: Kellner] #action:thinking
+ ~ title_part = "Fräulein"
+ ~ surname = "Kellner"
+* [__Bestimme__: Baumgartner] #action:thinking
+ ~ title_part = "Fräulein"
+ ~ surname = "Baumgartner"
+* [__Bestimme__: Fischer] #action:thinking
+ ~ title_part = "Fräulein"
+ ~ surname = "Fischer"
+* [__Bestimme__: Schmid] #action:thinking
+ ~ title_part = "Fräulein"
+ ~ surname = "Schmid"
+* [__Bestimme__: Pichler] #action:thinking
+ ~ title_part = "Fräulein"
+ ~ surname = "Pichler"
+* [__Bestimme__: Rosenfeld] #action:thinking
+ ~ title_part = "Fräulein"
+ ~ surname = "Rosenfeld"
+-
+-> assemble_full_name
+
+=== choose_name_working ===
+
+Der Name, den du nach oben trugst, wurde vielleicht in der Aussprache verändert, aber nie ganz von seinem Ursprung gereinigt.
+
+* [__Bestimme__: Anna] #action:thinking
+ ~ given_names = "Anna"
+ ~ common_name = "Anna"
+* [__Bestimme__: Klara] #action:thinking
+ ~ given_names = "Klara"
+ ~ common_name = "Klara"
+* [__Bestimme__: Agnes] #action:thinking
+ ~ given_names = "Agnes"
+ ~ common_name = "Agnes"
+* [__Bestimme__: Leni] #action:thinking
+ ~ given_names = "Leni"
+ ~ common_name = "Leni"
+* [__Bestimme__: Rosa] #action:thinking
+ ~ given_names = "Rosa"
+ ~ common_name = "Rosa"
+* [__Bestimme__: Gertrud] #action:thinking
+ ~ given_names = "Gertrud"
+ ~ common_name = "Gertrud"
+* [__Bestimme__: Elisabeth] #action:thinking
+ ~ given_names = "Elisabeth"
+ ~ common_name = "Elisabeth"
+* [__Bestimme__: Franziska] #action:thinking
+ ~ given_names = "Franziska"
+ ~ common_name = "Franziska"
+-
+-> choose_surname_working
+
+=== choose_surname_working ===
+
+Ein einfacher Name kann in Wien eine Last sein. Er sagt den Leuten, wie wenig Achtung sie vorgeben müssen, ehe du gesprochen hast.
+
+* [__Bestimme__: Pichler] #action:thinking
+ ~ title_part = "Fräulein"
+ ~ surname = "Pichler"
+* [__Bestimme__: Huber] #action:thinking
+ ~ title_part = "Fräulein"
+ ~ surname = "Huber"
+* [__Bestimme__: Maier] #action:thinking
+ ~ title_part = "Fräulein"
+ ~ surname = "Maier"
+* [__Bestimme__: Gruber] #action:thinking
+ ~ title_part = "Fräulein"
+ ~ surname = "Gruber"
+* [__Bestimme__: Schuster] #action:thinking
+ ~ title_part = "Fräulein"
+ ~ surname = "Schuster"
+* [__Bestimme__: Krenn] #action:thinking
+ ~ title_part = "Fräulein"
+ ~ surname = "Krenn"
+* [__Bestimme__: Wolf] #action:thinking
+ ~ title_part = "Fräulein"
+ ~ surname = "Wolf"
+* [__Bestimme__: Moser] #action:thinking
+ ~ title_part = "Fräulein"
+ ~ surname = "Moser"
+-
+-> assemble_full_name
+
+=== assemble_full_name ===
+
+{birth_class == "noble":
+ Auf Visitenkarten, in Briefen, in den vorsichtigen Mündern der Dienerschaft bist du {given_names} {title_part} {surname}.
+- else:
+ In Bahndokumenten, Hotelbüchern und auf den Zungen von Menschen, die noch nicht entschieden haben, wie viel Achtung du verdienst, bist du {title_part} {given_names} {surname}.
+}
+
+Aber in der privaten Kammer, in der ein Name zuerst beantwortet wird, ehe er gespielt werden muss, bist du {common_name}.
+
+-> train_compartment
+
+=== define_religion_and_supernatural ===
+
+Du berührst das Ridikül, ohne es sofort zu öffnen.
+
+Das Schreiben darin nennt dich nicht Ermittlerin. Es nennt dich, in einer Prosa trocken genug, um durch beliebig viele Ämter zu gelangen, eine Frau, deren ungewöhnlicher spiritistischer Ruf sie für eine heikle Haushaltsangelegenheit empfehle. Die Formulierung ist erlesen. Sie bejaht nicht und verneint nicht. Sie erlaubt allen Beteiligten, später zu glauben, sie hätten nichts Ungehöriges geglaubt.
+
+Die gräfliche Familie auf Jagdhaus Hohenreith hat um Diskretion ersucht. Wien hat mit einem versiegelten Schreiben geantwortet, mit einer Frau, der man nachsagt, sie spreche mit dem Verborgenen, und mit einem Mann ihr gegenüber, der eigene Befehle hat.
+
+Das Schreiben nennt keine Kirche. Gerade das macht die Kirche anwesend.
+
+* [__Bestimme__: Der Glaube ist dir wirklich heilig.] #action:thinking
+ ~ religion_stance = "devout_catholic"
+ Gott ist kein Gesprächsgegenstand für Abteile. Er ist kein Talent, kein Ruf, keine gesellschaftliche Bequemlichkeit. Du glaubst nicht kindlich, aber tief: an Sünde, Gnade, Sakrament, Versuchung und an die gefährliche Nähe der unsichtbaren Welt.
+
+* [__Bestimme__: Du bist katholisch, wie man in Wien katholisch ist.] #action:thinking
+ ~ religion_stance = "social_catholic"
+ Du kennst die Feste, die Gebete, das Gewicht der Beichte und die Macht eines Pfarrers über Menschen, die behaupten, ihn nicht zu fürchten. Dein Glaube ist nicht leer; aber er ist ebenso Gewohnheit wie Überzeugung, ebenso Ordnung wie Trost.
+
+* [__Bestimme__: Die Kirche ist für dich eine Behörde mit Weihrauch.] #action:thinking
+ ~ religion_stance = "josephinian_sceptic"
+ Du achtest Register, Schulen, Spitäler, Archive und jene Nützlichkeit, die Institutionen gelegentlich gegen ihren eigenen Dünkel entwickeln. Aber Priester erklären zu oft, was sie zuerst besitzen möchten: Schuld, Frauen, Armut und Angst.
+
+* [__Bestimme__: Wenn Seelen fortbestehen, hat Rom nicht das Monopol auf ihre Stimmen.] #action:thinking
+ ~ religion_stance = "spiritist_syncretic"
+ Heiligenbilder, Totenmessen, Séancen, Ahnungen, Tischklopfen, Träume: Die sauberen Grenzen dazwischen scheinen dir eher von Männern gezogen als von der Ewigkeit selbst. Was überlebt, spricht vielleicht in Formen, die keine Kanzlei genehmigt hat.
+
+* [__Bestimme__: Der Glaube hat dich geformt, bevor du alt genug warst, dich gegen ihn zu wehren.] #action:thinking
+ ~ religion_stance = "wounded_catholic"
+ Du kennst die Gebete zu gut, um sie einfach abzulegen, und die Schuld zu gut, um sie fromm zu nennen. Katholische Bilder erreichen dich nicht als Dekoration. Sie greifen nach Stellen, die du lieber versiegelt hieltest.
+
+-
+
+Vor dieser Reise, vor diesem Zug, bevor die Berge beginnen, Stück für Stück den Himmel zu nehmen, hat der Glaube bereits seine Stellung in dir bezogen. Nun bleibt die andere Frage: was du aus dem machst, was die Leute über deinen Ruf sagen.
+
+* [__Bestimme__: Die Toten schweigen nicht. Die Lebenden hören nur schlecht zu.] #action:thinking
+ #supernatural:believer
+ ~ supernatural_belief = "believer"
+ ~ medium_reputation += 1
+ ~ supernatural_exposure += 1
+
+ Du hast den Unglauben der Gebildeten immer für eine provinzielle Anmaßung gehalten. Es gibt Druck in Zimmern, in denen Kummer gewesen ist. Es gibt Worte, die Menschen sprechen, bevor sie wissen, dass sie gesprochen haben. Es gibt Träume, die mit Schlamm am Saum eintreffen.
+
+ Vielleicht ist die Welt nicht heimgesucht. Vielleicht ist sie nur überfüllt.
+
+* [__Bestimme__: Das Übernatürliche ist meistens Schmerz, Betrug, Fieber, Erbschaft oder schlechte Lüftung.] #action:thinking
+ #supernatural:sceptic
+ #route:detective
+ ~ supernatural_belief = "sceptic"
+ ~ detective += 1
+
+ Das Wort Geist verdeckt zu viel und erklärt zu wenig. Du hast anständige Menschen ein Echo eine Botschaft nennen hören, einen Zufall ein Zeichen, eine zitternde Hand eine himmlische Berührung. Männer der Wissenschaft können Toren sein, doch Toren mit Kerzen und Tischchen sind keine Verbesserung.
+
+ Falls Hohenreith Gespenster hat, erwartest du von ihnen Buchhaltung, Briefe, Fußspuren und einen Nutznießer.
+
+* [__Täusche__: Glaube ist ein Kostüm, das Männer dir selbst reichen.] #action:social
+ #supernatural:performer
+ ~ supernatural_belief = "performer"
+ ~ medium_reputation += 2
+
+ Du hast früh entdeckt, dass Männer, die dem Verstand einer Frau misstrauen, manchmal ihre Nerven verehren. Eine Schlussfolgerung aus Beweisen reizt sie. Eine Vision, mit gesenkten Wimpern gehaucht, lässt sie näher rücken.
+
+ Nun gut. Sollen sie näher rücken.
+
+* [__Bestimme__: Du hast gelernt, nicht zu früh zu entscheiden.] #action:thinking
+ #supernatural:undecided
+ ~ supernatural_belief = "undecided"
+
+ Es gibt Dinge, die du erklären kannst, Dinge, die du noch nicht erklären kannst, und Dinge, denen Erklärung schadet, ehe sie hilft. Du hast einen Beruf daraus gemacht, an Schwellen zu stehen, mit einem Gesicht, das gefasst genug ist, damit beide Seiten weitersprechen.
+
+ Hohenreith wird dir zeigen müssen, welche Art von Fall es ist.
+
+-
+
+Glaube ist eine Sache. Erfahrung eine andere.
+
+Man nennt eine Frau empfindsam, wenn ihre Wahrnehmungen wie eine Krankheit klingen sollen. Man nennt sie hysterisch, wenn diese Wahrnehmungen unbequem werden. Man nennt sie inspiriert, wenn man sie braucht, und labil, wenn man sie nicht mehr braucht.
+
+Unter Ruf und Aufführung hat die Erinnerung ihre eigene Aussage.
+
+* [__Bestimme__: Es gab Augenblicke, die du nicht wegerklären kannst.] #action:thinking
+ #powers:genuine
+ ~ supernatural_senses = "genuine"
+ ~ supernatural_exposure += 2
+
+ Einmal, als Kind, wusstest du es, bevor das Telegramm kam. Einmal, in einem überfüllten Zimmer, trat der Kummer einer Fremden mit solcher Gewalt in dich ein, dass deine eigenen Knie nachgaben. Einmal sahst du in einem Spiegel eine Tür hinter dir, die nicht im Raum war, als du dich umdrehtest.
+
+ Danach lerntest du Vorsicht. Es ist unklug für eine Frau, Dinge zu wissen, bevor ein Mann ihre Meinung erbeten hat.
+
+* [__Bestimme__: Alles, was du tust, lässt sich durch Beobachtung, Zeitpunkt und Nervenstärke erklären.] #action:thinking
+ #powers:faked
+ #route:detective
+ ~ supernatural_senses = "faked"
+ ~ detective += 1
+
+ Du bemerkst Ringe, die zu frisch abgenommen wurden, Trauerhandschuhe, die zu sorgfältig getragen werden, Briefe, so oft gefaltet und wieder gefaltet, bis der Knick die Besessenheit des Lesers verrät. Du hörst, wenn Diener Gäste falsch benennen, wenn Mütter vor den Zimmern ihrer Töchter innehalten, wenn Offiziere lügen, indem sie zu genau werden.
+
+ Die Toten haben dir nie etwas gesagt. Die Lebenden können nicht aufhören, dir alles zu verraten.
+
+* [__Bestimme__: Etwas geschieht, aber niemals, wenn man es ruft.] #action:thinking
+ #powers:ambiguous
+ ~ supernatural_senses = "ambiguous"
+ ~ supernatural_exposure += 1
+
+ Dein Ruf beruht auf Beherrschung. Die Wahrheit, falls es Wahrheit ist, hat keinen Respekt vor Terminen.
+
+ Manchmal verändert ein Zimmer den Druck um dich. Manchmal bekommt ein Gesicht einen alten Ausdruck, den kein Lebender ihm beigebracht hat. Manchmal kommen Namen vor den Vorstellungen. Aber je fester du danach greifst, desto gewöhnlicher wird die Welt.
+
+* [__Bestimme__: Du hast die ersten Zeichen begraben.] #action:thinking
+ #powers:repressed
+ #route:eccentric
+ ~ supernatural_senses = "repressed"
+ ~ eccentric += 1
+
+ Es gibt Kindheitserinnerungen, die hinter Höflichkeit versiegelt sind: ein Kinderzimmerspiegel, zur Wand gedreht; eine Amme, ohne Zeugnis entlassen; die Hand deiner Mutter um dein Handgelenk, so fest, dass die Knochen sich beschwerten.
+
+ Danach wurdest du auf Arten sonderbar, die die Gesellschaft leichter bewundern als verstehen konnte.
+
+-
+-> train_compartment
+
+=== define_appearance ===
+
+Der Zug fährt in einen Tunnel. Für mehrere Sekunden gibt dir das Abteilfenster nur dein eigenes Spiegelbild zurück.
+
+Jetzt, nachdem dein Name im Raum steht, kann das Glas mehr zeigen als eine Dame im richtigen Abteil. Es zeigt die Frau, die in Eibenreith aussteigen wird.
+
+* [__Bestimme__: Klein, schmal und beweglich.] #action:thinking
+ ~ body_detail = "small_slender"
+ Unter Mantel, Rock und Sitzhaltung bleibt etwas Knappes, Schnelles an dir: schmale Schultern, feine Handgelenke, ein Körper, der in Türen verschwindet und in Gesprächen unterschätzt wird.
+
+* [__Bestimme__: Mittelgroß, weich gebaut, mit ruhiger körperlicher Gegenwart.] #action:thinking
+ ~ body_detail = "medium_soft"
+ Du wirkst nicht zerbrechlich. Die Taille ist geformt, nicht mädchenhaft; die Schultern sind ruhig, die Hände gepflegt, die Anwesenheit fester, als manche Männer an einer jungen Frau bequem finden.
+
+* [__Bestimme__: Hochgewachsen und schlank.] #action:thinking
+ ~ body_detail = "tall_slender"
+ Die Sitzbank macht dich weniger lang, als du bist. Aufrecht stehend würdest du mehr Raum einnehmen, als deine Rolle verspricht: lange Linien, schmale Hände, eine Haltung, die Disziplin beinahe wie Herkunft aussehen lässt.
+
+* [__Bestimme__: Kompakt und kräftiger, als die Kleidung zugibt.] #action:thinking
+ ~ body_detail = "compact_strong"
+ Reisekleidung und Korsett ordnen dich, aber sie verleugnen nicht alles. In deinen Unterarmen, im Nacken, in der Art, wie du ein Gleichgewicht hältst, liegt mehr Kraft, als man einer Dame höflich zutraut.
+
+* [__Bestimme__: Zierlich, mit einem Anschein von Empfindlichkeit.] #action:thinking
+ ~ body_detail = "delicate"
+ Du wirkst zart genug, dass Ärzte, Tanten und taktlose Herren zu rasch glauben, deinen Zustand deuten zu dürfen. Dass Zartheit nicht dasselbe ist wie Schwäche, bleibt eine nützliche Verwechslung.
+
+-
+
+Die dunkle Scheibe hält nun Haar und Hut fest.
+
+* [__Bestimme__: Dunkelbraunes Haar, fast schwarz im schwachen Licht.] #action:thinking
+ ~ hair_colour = "dark_brown"
+ Dunkelbraunes Haar rahmt Stirn und Schläfen, im Tunnel beinahe schwarz, im Lampenlicht mit einem wärmeren Schimmer.
+
+* [__Bestimme__: Kastanienbraunes Haar mit warmem Glanz.] #action:thinking
+ ~ hair_colour = "chestnut"
+ Kastanienbraunes Haar fängt jedes bisschen Licht, das die Lampe hergibt, und macht dein Gesicht weicher, als dein Blick es erlaubt.
+
+* [__Bestimme__: Dunkelblondes bis aschbraunes Haar.] #action:thinking
+ ~ hair_colour = "dark_blond_ash"
+ Dunkelblondes, aschbraun wirkendes Haar gibt dir etwas Zurückhaltendes und Nordisches, besonders unter dem dunklen Hut.
+
+* [__Bestimme__: Hellbraunes Haar mit goldenen Strähnen.] #action:thinking
+ ~ hair_colour = "light_brown_gold"
+ Hellbraunes Haar mit goldenen Strähnen wirkt im Abteil beinahe zu warm für diese Reise, als habe Wien einen letzten Rest Nachmittag darin vergessen.
+
+* [__Bestimme__: Rotbraunes Haar, sorgfältig gebändigt.] #action:thinking
+ ~ hair_colour = "auburn"
+ Rotbraunes Haar ist nie ganz unauffällig, auch wenn Nadeln, Hut und Sitte es zur Ordnung zwingen.
+
+-
+
+Die Frisur ist nicht bloß Geschmack. Eine Frau trägt auch ihre Beherrschung auf dem Kopf.
+
+* [__Bestimme__: Ein glatter Mittelscheitel und ein tiefer Knoten im Nacken.] #action:thinking
+ ~ hairstyle = "centre_part_low_bun"
+ Der Mittelscheitel ist sauber, der Knoten tief und fest. Keine Locke bittet um Nachsicht. Es ist eine Frisur für Frauen, die lieber richtig als reizend erscheinen.
+
+* [__Bestimme__: Weiche Stirnwellen und ein sorgfältiger Chignon.] #action:thinking
+ ~ hairstyle = "soft_waves_chignon"
+ Die Stirnwellen sind weich gelegt, der Chignon sitzt sauber unter dem Hut. Das wirkt weiblicher, zugänglicher, aber nicht weniger berechnet.
+
+* [__Bestimme__: Geflochtene Partien, im Nacken zu einem schweren Knoten gefasst.] #action:thinking
+ ~ hairstyle = "braided_low_knot"
+ Geflochtene Partien führen das Haar zur Ordnung zurück. Der Knoten im Nacken ist schwerer, ländlicher vielleicht, aber unter städtischer Hand gezähmt.
+
+* [__Bestimme__: Etwas Höhe über der Stirn, modischer und selbstbewusster.] #action:thinking
+ ~ hairstyle = "raised_front_modern"
+ Das Haar hebt sich über der Stirn mit jener neuen Fülle, die ältere Damen für Eitelkeit und jüngere für Freiheit halten. Unter dem Reisehut bleibt es gerade noch anständig.
+
+-
+
+Das Gesicht unter dem Hut ist deutlich genug, um nicht mehr ausweichen zu können.
+
+* [__Bestimme__: Helle Haut, kühle Augen, scharfe Brauen.] #action:thinking
+ ~ complexion_detail = "fair_cool"
+ ~ face_detail = "cool_sharp"
+ Dein Teint ist hell und kühl, die Brauen geben dem Gesicht mehr Bestimmtheit, als ein flüchtiger Blick erwartet. Man könnte dich empfindlich nennen, bis man die Augen genauer ansieht.
+
+* [__Bestimme__: Rosiger Teint, weicher Mund, wacher Blick.] #action:thinking
+ ~ complexion_detail = "rosy"
+ ~ face_detail = "soft_alert"
+ Du hast mehr Farbe im Gesicht, als die meisten Damen im Winter zeigen möchten. Der Mund wirkt weicher als der Blick; zusammen macht dich das zugänglicher, aber nicht einfacher.
+
+* [__Bestimme__: Warmer Teint, dunklere Augen, ruhige Miene.] #action:thinking
+ ~ complexion_detail = "warm"
+ ~ face_detail = "calm_dark_eyes"
+ Der Teint ist wärmer, die Augen dunkler, die Miene ruhiger. Du wirkst weniger ätherisch als gegenwärtig: eine Frau aus Fleisch, Gedächtnis und Absicht.
+
+* [__Bestimme__: Blasse Haut, feine Züge, ein fast zu kontrolliertes Gesicht.] #action:thinking
+ ~ complexion_detail = "pale_fine"
+ ~ face_detail = "fine_controlled"
+ Die Züge sind fein, fast zerbrechlich, doch die Kontrolle darin ist zu sichtbar, um harmlos zu wirken. Wer dich für schwach hält, möchte es zu gerne.
+
+* [__Bestimme__: Ein markanteres Gesicht mit gerader Nase und festem Kinn.] #action:thinking
+ ~ complexion_detail = "clear"
+ ~ face_detail = "marked_jaw"
+ Die Nase ist gerade, das Kinn fester, die Linien weniger gefällig als ein Porträtmaler sie gern hätte. Das Gesicht verrät eher Willen als Sanftmut.
+
+-
+
+Der Rest der Spiegelung ist Kostüm, Rüstung und Beweismittel.
+
+{birth_class == "noble":
+ Die Kleidung muss genug Rang zeigen, um glaubwürdig zu sein, und genug Zurückhaltung, um nicht nach Provinztheater zu riechen.
+- else:
+ {birth_class == "middle":
+ Die Kleidung muss eine höhere Welt betreten können, ohne zu schreien, dass sie dafür gearbeitet hat.
+ - else:
+ Die Kleidung muss beweisen, dass man dich in die erste Klasse setzen konnte, ohne dass der Stoff gegen dich aussagt.
+ }
+}
+
+* [__Bestimme__: Ein dunkel anthrazitfarbenes Reisekostüm mit pflaumenfarbenem Samtkragen.] #action:thinking
+ ~ outfit_detail = "charcoal_plum_velvet"
+ Du trägst ein geschneidertes Reisekostüm aus dunkler anthrazitfarbener Wolle. Am Kragen und an den Manschetten liegt ein pflaumenfarbener Samtton, gedämpft genug für den Tag, teuer genug für Menschen mit Augen.
+
+* [__Bestimme__: Ein schwarzbraunes Wollkostüm mit elfenbeinfarbener Bluse und schmaler Spitze.] #action:thinking
+ ~ outfit_detail = "black_brown_ivory_lace"
+ Der Rock ist dunkel und schwer genug für die Reise, die Jacke streng, die elfenbeinfarbene Bluse am Hals hochgeschlossen. Die Spitze ist schmal, sauber und gefährlich nahe an Frömmigkeit.
+
+* [__Bestimme__: Ein graublaues Reisekostüm mit kurzem Mantel und praktischen Knöpfen.] #action:thinking
+ ~ outfit_detail = "blue_grey_practical"
+ Graublaue Wolle, ein kurzer Mantel, ein Rock, der beim Aussteigen nicht sofort Verrat übt, und Knöpfe, die mehr nach Zweck als nach Schmuck aussehen. Korrekt, städtisch, brauchbar.
+
+* [__Bestimme__: Ein dunkles grünes Kostüm mit schwarzem Besatz und passendem Hut.] #action:thinking
+ ~ outfit_detail = "dark_green_black_trim"
+ Das Grün ist so dunkel, dass es erst im Licht der Fenster sichtbar wird. Schwarzer Besatz, passende Handschuhe, ein Hut mit kleiner Feder: nicht laut, aber schwer zu vergessen.
+
+* [__Bestimme__: Ein schwarzes Reisekleid mit Schleier, zu ernst für bloße Mode.] #action:thinking
+ ~ outfit_detail = "black_veil_severe"
+ Das Schwarz ist nicht Trauer, jedenfalls nicht offiziell. Ein schmaler Schleier, dunkle Handschuhe, glatter Rock, hohe Knopfleiste. Es ist die Art Kleidung, in der skeptische Männer leichter an Ahnungen glauben.
+
+-
+~ appearance_done = true
+
+Als die Berge zurückkehren, wirken sie näher.
+
+-> train_compartment
+
+=== first_viktor_exchange ===
+
+{not tut_dialog_intro:
+ #alert[Dialogoptionen stehen in Anführungszeichen. Das Aktionswort sagt, ob du antwortest, fragst, mitteilst, spottest, schweigst oder sozial handelst.]
+ ~ tut_dialog_intro = true
+}
+
+Er faltet die Zeitung zusammen, obwohl du sehr sicher bist, dass er nicht gelesen hat.
+
+„Sie sind sehr still gewesen, gnädiges Fräulein. Für eine Dame auf ihrer ersten amtlichen Reise beweisen Sie bemerkenswerte Zurückhaltung.“
+
+Die Anrede ist technisch richtig, falls du adelig bist, zu hoch gegriffen, falls du es nicht bist, und vollkommen geeignet, weil er noch nicht weiß, welcher Teil von dir brauchbar ist, welcher Verkleidung, und welcher Gefahr.
+
+{birth_class == "noble":
+ -> viktor_class_noble
+- else:
+ {birth_class == "middle":
+ -> viktor_class_middle
+ - else:
+ -> viktor_class_working
+ }
+}
+
+=== viktor_class_noble ===
+
+Viktor wartet auf die Antwort, die seine Bemerkung verlangt. Der Zug ruckt einmal, dann findet er wieder in seinen harten, selbstgewissen Rhythmus.
+
+* [__Sage__: „Zurückhaltung ist keine Tugend, Herr Nowak. Oft ist sie nur gute Erziehung mit geschlossenem Mund.“] #action:conversation
+ #route:eccentric
+ ~ eccentric += 1
+ „Zurückhaltung ist keine Tugend, Herr Nowak. Oft ist sie nur gute Erziehung mit geschlossenem Mund.“
+
+ Seine Brauen bewegen sich kaum merklich.
+
+ „Dann hat gute Erziehung also militärischen Nutzen“, sagt er.
+
+ ** [__Sage__: „Nur, wenn sie richtig kommandiert wird.“] #action:conversation
+ #route:lover
+ ~ lover += 1
+ ~ viktor_suspicion += 1
+ „Nur, wenn sie richtig kommandiert wird.“
+
+ „Sie gedenken, sie selbst zu kommandieren?“
+
+ Die Antwort bleibt im Winkel deines Handschuhs und in der Ruhe deines Blickes liegen.
+
+ ** [__Sage__: „Nur, wenn Männer Schweigen mit Gehorsam verwechseln.“] #action:conversation
+ #route:sapphic
+ ~ sapphic += 1
+ ~ viktor_suspicion += 1
+ „Nur, wenn Männer Schweigen mit Gehorsam verwechseln.“
+
+ „Das ist eine ehrgeizige Unterscheidung.“
+
+ Diese Unterscheidung hat viele Frauen davor bewahrt, zu früh verstanden zu werden.
+
+ ** [__Sage__: „Ich bevorzuge jede Disziplin, die keinen Makel in den Akten hinterlässt.“] #action:conversation
+ #route:detective
+ ~ detective += 1
+ ~ viktor_trust += 1
+ „Ich bevorzuge jede Disziplin, die keinen Makel in den Akten hinterlässt.“
+
+ „Eine nützliche Vorliebe“, sagt er. „Wenn sie aufrichtig ist.“
+
+ Die Frage der Aufrichtigkeit bleibt vorläufig sein Problem.
+
+ --
+ -> viktor_mission_briefing
+
+* [__Sage__: „Sie brauchen nicht zu prüfen, ob ich stillsitzen kann, Herr Nowak. Ich wurde von Leuten erzogen, die weniger Geduld und schärfere Augen hatten.“] #action:conversation
+ #route:detective
+ ~ detective += 1
+ ~ viktor_trust += 1
+ „Sie brauchen nicht zu prüfen, ob ich stillsitzen kann, Herr Nowak. Ich wurde von Leuten erzogen, die weniger Geduld und schärfere Augen hatten.“
+
+ „Eine Familienerziehung also.“
+
+ ** [__Sage__: „Eher ein Familienurteil.“] #action:conversation
+ #route:eccentric
+ ~ eccentric += 1
+ „Eher ein Familienurteil.“
+
+ „Sie sprechen, als wäre Geburt ein Gefängnis.“
+
+ Die polierten Möbel antworten ihm besser, als es ein Einwand könnte.
+
+ ** [__Sage__: „Eine Erziehung in Zimmern, in denen selbst jeder Stuhl Rang besitzt.“] #action:conversation
+ ~ class_confidence += 1
+ „Eine Erziehung in Zimmern, in denen selbst jeder Stuhl Rang besitzt.“
+
+ „Dann wird Hohenreith Sie vielleicht nicht überraschen.“
+
+ Die Möglichkeit, dass Hohenreith bessere Geheimnisse als Stühle besitzt, darf unausgesprochen bleiben.
+
+ --
+ -> viktor_mission_briefing
+
+* [__Sage__: „Wenn dies bemerkenswerte Zurückhaltung ist, Herr Nowak, fürchte ich, Sie haben bisher vor allem Offiziere begleitet.“] #action:conversation
+ #route:lover
+ ~ lover += 1
+ ~ viktor_suspicion += 1
+ „Wenn dies bemerkenswerte Zurückhaltung ist, Herr Nowak, fürchte ich, Sie haben bisher vor allem Offiziere begleitet.“
+
+ Sein Mundwinkel verändert sich so wenig, dass man es beinahe übersehen könnte.
+
+ „Offiziere langweilen sich nicht so leicht.“
+
+ ** [__Sage__: „Oder weniger ehrlich darin.“] #action:conversation
+ #route:lover
+ ~ lover += 1
+ „Oder weniger ehrlich darin.“
+
+ „Sie beschuldigen die Armee der Eitelkeit.“
+
+ Der Vorwurf der Beständigkeit ist der am wenigsten zu leugnende.
+
+ ** [__Sage__: „Dann muss ich mich bemühen, die Armee nicht zu enttäuschen.“] #action:conversation
+ #route:careless
+ ~ careless += 1
+ „Dann muss ich mich bemühen, die Armee nicht zu enttäuschen.“
+
+ „Genau das zu verhindern, ist mir aufgetragen worden.“
+
+ --
+ -> viktor_mission_briefing
+
+=== viktor_class_middle ===
+
+Viktors Bemerkung bleibt zwischen euch liegen wie ein höflicher Vorwurf.
+
+* [__Sage__: „Zurückhaltung fällt leichter, wenn man gelernt hat, dass jeder Fehler von jemandem behalten wird, der besser gestellt ist.“] #action:conversation
+ #route:detective
+ ~ detective += 1
+ ~ viktor_trust += 1
+ „Zurückhaltung fällt leichter, wenn man gelernt hat, dass jeder Fehler von jemandem behalten wird, der besser gestellt ist.“
+
+ Viktor beobachtet dich genauer.
+
+ „Eine bittere Lektion.“
+
+ ** [__Sage__: „Eine nützliche. Bitterkeit ist nur der Geschmack, den Belehrung zurücklässt.“] #action:conversation
+ #route:eccentric
+ ~ eccentric += 1
+ „Eine nützliche. Bitterkeit ist nur der Geschmack, den Belehrung zurücklässt.“
+
+ „Sie sammeln Redewendungen wie Waffen.“
+
+ Der Satz liegt leicht genug da, dass er entscheiden muss, ob er Zierrat oder Waffe ist.
+
+ ** [__Sage__: „Eine gewöhnliche. Manche Menschen bemerken Unrecht erst, wenn es ihr eigenes Stockwerk erreicht.“] #action:conversation
+ #route:sapphic
+ ~ sapphic += 1
+ „Eine gewöhnliche. Manche Menschen bemerken Unrecht erst, wenn es ihr eigenes Stockwerk erreicht.“
+
+ „Sie haben also Stockwerke studiert?“
+
+ Schwellen, hast du gelernt, sind ehrlicher als Stockwerke; sie geben zu, dass Durchgang ein Vorrecht ist.
+
+ --
+ -> viktor_mission_briefing
+
+* [__Sage__: „Wenn ich schweige, Herr Nowak, so deshalb, weil Männer sich schneller erklären, wenn ihnen die Stille missfällt.“] #action:conversation
+ #route:lover
+ ~ lover += 1
+ ~ medium_reputation += 1
+ „Wenn ich schweige, Herr Nowak, so deshalb, weil Männer sich schneller erklären, wenn ihnen die Stille missfällt.“
+
+ „Eine Methode?“
+
+ ** [__Sage__: „Eine Höflichkeit. Ich lasse sie mit ihrem Lieblingsthema beginnen.“] #action:conversation
+ #route:lover
+ ~ lover += 1
+ „Eine Höflichkeit. Ich lasse sie mit ihrem Lieblingsthema beginnen.“
+
+ „Mit sich selbst.“
+
+ Seine eigene Antwort vollendet den Grundsatz sauber genug, dass weitere Belehrung Eitelkeit wäre.
+
+ ** [__Sage__: „Ein Versuch. Er hat verlässliche Ergebnisse geliefert.“] #action:conversation
+ #route:detective
+ ~ detective += 1
+ „Ein Versuch. Er hat verlässliche Ergebnisse geliefert.“
+
+ „Dann bin ich Teil Ihres Versuches.“
+
+ Er hat lange genug dir gegenüber gesessen, um Beweisstück zu werden.
+
+ --
+ -> viktor_mission_briefing
+
+* [__Sage__: „Ich überlegte, ob Ihre Sorge amtlich, persönlich oder bloß männlich ist.“] #action:conversation
+ #route:eccentric
+ ~ eccentric += 1
+ ~ viktor_suspicion += 1
+ „Ich überlegte, ob Ihre Sorge amtlich, persönlich oder bloß männlich ist.“
+
+ Seine Augen verhärten sich um genau einen Grad.
+
+ „Heute ist sie amtlich.“
+
+ ** [__Sage__: „Wie bequem. Die beiden anderen dürfen die Verantwortung leugnen.“] #action:conversation
+ #route:eccentric
+ ~ eccentric += 1
+ „Wie bequem. Die beiden anderen dürfen die Verantwortung leugnen.“
+
+ „Ich rate Ihnen, den Witz in Hohenreith nicht zu Ihrem ersten Werkzeug zu machen.“
+
+ Die Herabstufung des Witzes zum zweiten Werkzeug bleibt theoretisch genug, um ungefährlich zu sein.
+
+ ** [__Sage__: „Dann werde ich diese amtliche Sorge mit dem Respekt behandeln, der Papier gebührt.“] #action:conversation
+ #route:detective
+ ~ detective += 1
+ „Dann werde ich diese amtliche Sorge mit dem Respekt behandeln, der Papier gebührt.“
+
+ „Papier hat Armeen in Bewegung gesetzt.“
+
+ Papier hat auch Fehler begraben, aber nicht jede Verbesserung muss ausgesprochen werden.
+
+ --
+ -> viktor_mission_briefing
+
+=== viktor_class_working ===
+
+Viktors Höflichkeit ist glatt genug, um keine Fingerabdrücke zu behalten. Darunter hörst du die Frage, wie sehr dieses Abteil dich verbessert hat.
+
+* [__Sage__: „Zurückhaltung ist, was die Leute loben, wenn sie die Mühe dahinter nicht sehen wollen.“] #action:conversation
+ #route:detective
+ ~ detective += 1
+ ~ viktor_trust += 1
+ „Zurückhaltung ist, was die Leute loben, wenn sie die Mühe dahinter nicht sehen wollen.“
+
+ Die Zeitung in Viktors Hand knittert einmal.
+
+ „Sie haben etwas dagegen, gelobt zu werden?“
+
+ ** [__Sage__: „Nur, wenn es billig geschieht.“] #action:conversation
+ #route:eccentric
+ ~ eccentric += 1
+ „Nur, wenn es billig geschieht.“
+
+ „Das dürfte schwer zu vermeiden sein.“
+
+ Wenn Hohenreith dich billig loben will, wird es die Ökonomie der Enttäuschung kennenlernen müssen.
+
+ ** [__Sage__: „Nur, wenn es die Person verbirgt, die die Arbeit getan hat.“] #action:conversation
+ #route:sapphic
+ ~ sapphic += 1
+ „Nur, wenn es die Person verbirgt, die die Arbeit getan hat.“
+
+ Er betrachtet dich, als wäre die Antwort von weiter hinten im Zug gekommen als aus der ersten Klasse.
+
+ --
+ -> viktor_mission_briefing
+
+* [__Sage__: „Ich schweige, weil man Frauen meiner Herkunft meist entweder dankbar oder unsichtbar wünscht.“] #action:conversation
+ #route:eccentric
+ ~ eccentric += 1
+ ~ viktor_suspicion += 1
+ „Ich schweige, weil man Frauen meiner Herkunft meist entweder dankbar oder unsichtbar wünscht.“
+
+ „Ich habe Sie nicht um Dankbarkeit gebeten.“
+
+ ** [__Sage__: „Nein. Sie haben verlangt, dass ich lenkbar sei.“] #action:conversation
+ #route:eccentric
+ ~ eccentric += 1
+ „Nein. Sie haben verlangt, dass ich lenkbar sei.“
+
+ „Ich habe nichts dergleichen verlangt.“
+
+ Rang braucht, anders als Männer, selten direkt zu fragen. Die Möbel fragen an seiner Stelle.
+
+ ** [__Sage__: „Dann werde ich meine Dankbarkeit aufschieben, bis Sie sie verdienen.“] #action:conversation
+ #route:lover
+ ~ lover += 1
+ „Dann werde ich meine Dankbarkeit aufschieben, bis Sie sie verdienen.“
+
+ Eine Pause. Dann, sehr trocken: „Eine großzügige Abmachung.“
+
+ --
+ -> viktor_mission_briefing
+
+* [__Sage__: „Ich versuchte, die Polster nicht so zu berühren, als könnten sie mich anklagen.“] #action:conversation
+ #route:careless
+ ~ careless += 1
+ ~ viktor_relation = "dependence"
+ „Ich versuchte, die Polster nicht so zu berühren, als könnten sie mich anklagen.“
+
+ Etwas wie Sorge huscht über sein Gesicht, zu spät als Ärger verkleidet.
+
+ „Die Polster haben Minister überstanden. Sie werden auch Sie überstehen.“
+
+ ** [__Sage__: „Dann bin ich schon mutiger, als ich es eben war.“] #action:conversation
+ #route:careless
+ ~ careless += 1
+ „Dann bin ich schon mutiger, als ich es eben war.“
+
+ „Mut, an Polstern gemessen, ist kein militärischer Maßstab.“
+
+ ** [__Sage__: „Ich werde es als Beruhigung auffassen, obgleich Sie es wie einen Verweis vorgetragen haben.“] #action:conversation
+ #route:lover
+ ~ lover += 1
+ „Ich werde es als Beruhigung auffassen, obgleich Sie es wie einen Verweis vorgetragen haben.“
+
+ „Mit beidem habe ich Übung.“
+
+ --
+ -> viktor_mission_briefing
+
+=== viktor_mission_briefing ===
+
+Der Zug tritt aus dem Tunnel in einen blassen Nachmittag aus dunklen Tannen und weißem Fels. Tief unten zeigt sich Wasser nur in Blitzen. Das Tal ist kein Anblick aus einem Salonbild mehr. Es hat Tiefe genug, um Dinge zu verbergen.
+
+Viktor öffnet eine Ledermappe und nimmt ein Memorandum heraus. Er reicht es dir nicht sofort.
+
+„Wenn wir die Bahn verlassen“, sagt er, „werden wir von einer Kutsche aus Hohenreith erwartet. Von diesem Augenblick an sind Äußerlichkeiten von Bedeutung. Ihren Gastgebern wurde mitgeteilt, dass ich bei Korrespondenz, Reiseangelegenheiten und praktischen Vorkehrungen behilflich bin. Mit militärischen Definitionen muss man sie nicht behelligen.“
+
+* [__Sage__: „Und die Dorfbewohner?“] #action:conversation
+ „Und die Dorfbewohner?“
+
+ „Die Dorfbewohner müssen mit nichts behelligt werden.“
+
+ Da ist sie: die Monarchie im Kleinen. Ein Mann, eine Mappe, ein verschlossener Satz.
+
+* [__Sage__: „Wie barmherzig. Der Staat hat ihnen das Vokabular erspart.“] #action:conversation
+ #route:eccentric
+ ~ eccentric += 1
+ ~ viktor_suspicion += 1
+ „Wie barmherzig. Der Staat hat ihnen das Vokabular erspart.“
+
+ „Der Staat hat ihnen Unruhe erspart“, sagt Viktor.
+
+ Die staatliche Gewohnheit, Unwissenheit mit Ruhe zu verwechseln, muss nicht ausgesprochen werden, um anwesend zu sein.
+
+* [__Sage__: „Sie meinen, sie sollen nicht wissen, ob ich Gast, Werkzeug oder Warnung bin.“] #action:conversation
+ #route:detective
+ ~ detective += 1
+ ~ viktor_trust += 1
+ „Sie meinen, sie sollen nicht wissen, ob ich Gast, Werkzeug oder Warnung bin.“
+
+ „Ich meine, sie sollen nur wissen, was die Lage beruhigt.“
+
+ Die Antwort beruhigt nichts. Das ist kein Widerspruch.
+
+-
+
+„Man wird Sie nach dem Stand anreden, den Sie vorweisen“, fährt er fort. „Der Haushalt des Grafen wird den Rang beachten. Die Dienerschaft wird beachten, was der Haushalt beachtet. Die Dorfbewohner mögen weniger beachten und mehr behalten. Ich rate zur Zurückhaltung.“
+
+Der Rat ist vernünftig. Das macht ihn nicht weniger ärgerlich.
+
+* [__Sage__: „Wären Herren weniger leicht zu lenken, Herr Nowak, so bedürften Damen weniger Methoden.“] #action:conversation
+ #route:lover
+ ~ lover += 1
+ ~ viktor_relation = "provocation"
+ ~ viktor_trust -= 1
+ ~ viktor_suspicion += 1
+ „Wären Herren weniger leicht zu lenken, Herr Nowak, so bedürften Damen weniger Methoden.“
+
+ Zum ersten Mal erreicht Belustigung beinahe seinen Mund.
+
+ „Eine gefährliche Lehre.“
+
+ ** [__Sage__: „Eine praktische.“] #action:conversation
+ „Eine praktische.“
+
+ „Sie gedenken, sie in Hohenreith anzuwenden?“
+
+ *** [__Sage__: „Nur dort, wo die Pflicht gegen die Monarchie Opfer verlangt.“] #action:conversation
+ #route:lover
+ ~ lover += 1
+ „Nur dort, wo die Pflicht gegen die Monarchie Opfer verlangt.“
+
+ Er blickt auf das Memorandum hinunter, aber nicht schnell genug, um zu verbergen, dass er dich neu einschätzt.
+
+ *** [__Sage__: „Nur dort, wo Männer Begehren mit Urteil verwechseln.“] #action:conversation
+ #route:lover
+ ~ lover += 1
+ „Nur dort, wo Männer Begehren mit Urteil verwechseln.“
+
+ „Das mag mehr Gebiet umfassen, als die Karten zugeben.“
+
+ ---
+
+ ** [__Sage__: „Gefährliche Lehren reisen am besten in guten Handschuhen.“] #action:conversation
+ #route:eccentric
+ ~ eccentric += 1
+ „Gefährliche Lehren reisen am besten in guten Handschuhen.“
+
+ „Sie gedenken, Hohenreith durch Charme zum Geständnis zu bringen?“
+
+ Wenn Hohenreith darauf besteht, bezaubert zu werden, wird es kaum deine Schuld sein.
+
+ --
+
+* [__Sage__: „Wenn Sie wünschen, dass ich harmlos erscheine, müssen Sie aufhören, mich wie ein Kerkermeister zu warnen.“] #action:conversation
+ #route:sapphic
+ ~ sapphic += 1
+ ~ viktor_relation = "tension"
+ ~ viktor_suspicion += 1
+ „Wenn Sie wünschen, dass ich harmlos erscheine, müssen Sie aufhören, mich wie ein Kerkermeister zu warnen.“
+
+ Sein Blick schärft sich.
+
+ „Ich bin nicht Ihr Kerkermeister.“
+
+ ** [__Sage__: „Nein. Ein Kerkermeister ist wenigstens ehrlich, was den Schlüssel betrifft.“] #action:conversation
+ „Nein. Ein Kerkermeister ist wenigstens ehrlich, was den Schlüssel betrifft.“
+
+ Die Worte überraschen dich dadurch, dass sie eine Spur hinterlassen. Vielleicht nicht bei ihm. Bei dir. Je näher dich der Zug an Amalias Welt trägt, obwohl du ihr Gesicht noch nicht kennst, desto unerträglicher scheint es, dass jedes weibliche Leben dort von Männern bewacht sein könnte, die Bewachung Sorge nennen.
+
+ Viktor faltet das Memorandum einmal, genau.
+
+ ** [__Sage__: „Dann stellen Sie sich nicht zwischen mich und jede verschlossene Tür, noch ehe ich die Klinke berührt habe.“] #action:conversation
+ ~ viktor_trust += 1
+ „Dann stellen Sie sich nicht zwischen mich und jede verschlossene Tür, noch ehe ich die Klinke berührt habe.“
+
+ „Manche Türen sind nicht ohne Grund verschlossen.“
+
+ Jeder Grund, der eines Schlosses würdig ist, sollte die Unverschämtheit der Prüfung überstehen.
+
+ Viktor faltet das Memorandum einmal, genau.
+
+ --
+
+* [__Sage__: „Dann wollen wir genau sein. Was wissen sie, was vermuten sie, und was ist mir zu prüfen gestattet?“] #action:conversation
+ #route:detective
+ ~ detective += 1
+ ~ viktor_relation = "professional"
+ ~ viktor_trust += 1
+ „Dann wollen wir genau sein. Was wissen sie, was vermuten sie, und was ist mir zu prüfen gestattet?“
+
+ Er nickt kaum merklich, als hättest du die einzige Antwort gewählt, die Erwachsenen zusteht.
+
+ „Sie wissen, dass Sie empfohlen kommen. Sie vermuten, dass Sie imstande sein könnten, die Störungen ohne Polizei, Priester oder Presse beizulegen. Ihnen ist gestattet, Betrug, Zwang, Gefährdung der öffentlichen Ordnung oder glaubwürdige, derzeit nicht einzuordnende Erscheinungen zu prüfen.“
+
+ ** [__Sage__: „Glaubwürdige, derzeit nicht einzuordnende Erscheinungen.“] #action:conversation
+ „Glaubwürdige, derzeit nicht einzuordnende Erscheinungen.“
+
+ „So lautet die Wendung.“
+
+ Die Formulierung setzt sich in deinem Geist fest wie ein bürokratisches Gespenst.
+
+ „Die ungefährlichste Art“, sagt er.
+
+ ** [__Sage__: „Und wenn die Erscheinungen einzuordnen sind?“] #action:conversation
+ „Und wenn die Erscheinungen einzuordnen sind?“
+
+ „Dann ordnen wir sie ein, ehe andere es tun.“
+
+ Der Satz hat die Kälte eines Berichts, der rasch über einem Grab geschrieben wurde.
+
+ --
+
+* [__Sage__: „Ich werde mein Möglichstes tun, nicht in Ohnmacht zu fallen, es sei denn, es erweist sich als nützlich.“] #action:conversation
+ #route:careless
+ ~ careless += 1
+ ~ viktor_relation = "dependence"
+ ~ viktor_trust -= 1
+ „Ich werde mein Möglichstes tun, nicht in Ohnmacht zu fallen, es sei denn, es erweist sich als nützlich.“
+
+ Etwas in seinem Gesicht spannt sich; nicht Verachtung genau, eher Bereitschaft.
+
+ „Ich würde es vorziehen, wenn Sie überhaupt nicht in Ohnmacht fielen.“
+
+ ** [__Sage__: „Wie unkavalierhaft.“] #action:conversation
+ „Wie unkavalierhaft.“
+
+ „Wie praktisch.“
+
+ Die Last der Sachlichkeit wandert zu ihm hinüber, so anmutig wie ein Ohnmachtssofa, das in ein Feldlazarett geschleppt wird.
+
+ Seine Antwort verzögert sich um einen halben Atemzug.
+
+ „Gerade das, gnädiges Fräulein, bereitet mir Sorge.“
+
+ ** [__Sage__: „Dann müssen Sie nahe genug bleiben, um mich aufzufangen.“] #action:conversation
+ #route:lover
+ ~ lover += 1
+ „Dann müssen Sie nahe genug bleiben, um mich aufzufangen.“
+
+ „Meine Befehle erwähnten keine theatralischen Zusammenbrüche.“
+
+ Diese Auslassung spricht nicht für die Gründlichkeit seiner Vorgesetzten.
+
+ --
+
+* [__Sage__: „Zurückhaltung ist, was furchtsame Leute Gehorsam nennen, nachdem sie vergessen haben, wer sie dazu erzogen hat.“] #action:conversation
+ #route:eccentric
+ ~ eccentric += 1
+ ~ viktor_relation = "challenge"
+ ~ viktor_suspicion += 2
+ „Zurückhaltung ist, was furchtsame Leute Gehorsam nennen, nachdem sie vergessen haben, wer sie dazu erzogen hat.“
+
+ Viktor betrachtet dich, als hätte man eine unbekannte Waffe im Gepäck gefunden.
+
+ „Sie haben Freude daran, sich Feinde zu machen.“
+
+ ** [__Sage__: „Nein. Ich missbillige nur die Bequemlichkeit, Dummköpfe unentschieden bleiben zu lassen.“] #action:conversation
+ „Nein. Ich missbillige nur die Bequemlichkeit, Dummköpfe unentschieden bleiben zu lassen.“
+
+ „In Hohenreith könnte diese Abneigung kostspielig werden.“
+
+ Wenn der Graf Fügsamkeit wollte, hätte er jemand Günstigeren einladen können.
+
+ ** [__Sage__: „Feinde sind nur Menschen, die ehrlich genug sind, am richtigen Platz zu stehen.“] #action:conversation
+ „Feinde sind nur Menschen, die ehrlich genug sind, am richtigen Platz zu stehen.“
+
+ „Sie sprechen, als sei Streit eine Art Haushaltsführung.“
+
+ Streit war immer eine Form von Haushaltsführung; man entdeckt, was wohin gehört.
+
+ --
+
+-
+
+Die Räder nehmen eine Kurve. Das Abteil neigt sich. Für einen Augenblick hält euch dieselbe schmale Schräglage.
+
+Viktor gibt dir endlich das Memorandum.
+
+Das Schriftstück ist nicht lang. Das ist Teil seiner Bedrohlichkeit. Lange Schriftstücke laden zum Widerspruch ein; kurze tragen Autorität.
+
+Ein gräflicher Haushalt. Ein Jagdhaus in der Obersteiermark, nicht der Hauptsitz der Familie. Berichte über Störungen unter Dienerschaft und Dorfbewohnern. Kein Einschreiten der Polizei erbeten. Keine öffentliche kirchliche Untersuchung erwünscht. Keine Presse. Keine Korrespondenz außerhalb genehmigter Kanäle. Deine Anwesenheit ist als diskrete Konsultation auf Wunsch der Familie zu erklären. Herr Nowak dient zur Unterstützung praktischer Angelegenheiten.
+
+Niemand hat das Wort Geist geschrieben.
+
+Niemand hat das Wort Betrug geschrieben.
+
+Niemand hat das Wort Tochter geschrieben.
+
+Doch die Auslassungen ordnen sich auf der Seite an wie Möbel um eine Leiche.
+
+* [__Sage__: „Es gibt noch eine weitere Weisung.“] #action:conversation
+ „Es gibt noch eine weitere Weisung.“
+
+ Viktor fragt nicht, woher du es weißt.
+
+ „Es gibt immer noch eine weitere Weisung“, sagt er.
+
+ ** [__Sage__: „Für Sie.“] #action:conversation
+ „Für Sie.“
+
+ „Ja.“
+
+ *** [__Sage__: „Über mich.“] #action:conversation
+ „Über mich.“
+
+ „Teilweise.“
+
+ *** [__Sage__: „Darüber, ob ich Betrügerin, Närrin oder nützliches Tier bin.“] #action:conversation
+ ~ viktor_suspicion += 1
+ „Darüber, ob ich Betrügerin, Närrin oder nützliches Tier bin.“
+
+ „Teilweise“, sagt er, und diesmal hat die Ehrlichkeit eine Schneide.
+
+ ---
+
+ ** [__Sage__: „Über mich.“] #action:conversation
+ „Über mich.“
+
+ „Teilweise.“
+
+ ** [__Sage__: „Darüber, ob ich Betrügerin, Närrin oder nützliches Tier bin.“] #action:conversation
+ ~ viktor_suspicion += 1
+ „Darüber, ob ich Betrügerin, Närrin oder nützliches Tier bin.“
+
+ „Teilweise“, sagt er, und diesmal hat die Ehrlichkeit eine Schneide.
+
+ --
+
+* [__Sage__: „Ihre Fassung ist kürzer als Ihr Schweigen. Das bedeutet, es gibt noch eine weitere Weisung.“] #action:conversation
+ #route:detective
+ ~ detective += 1
+ ~ viktor_trust += 1
+ „Ihre Fassung ist kürzer als Ihr Schweigen. Das bedeutet, es gibt noch eine weitere Weisung.“
+
+ Viktor fragt nicht, woher du es weißt.
+
+ „Es gibt immer noch eine weitere Weisung“, sagt er.
+
+ ** [__Sage__: „Für Sie.“] #action:conversation
+ „Für Sie.“
+
+ „Ja.“
+
+ *** [__Sage__: „Über mich.“] #action:conversation
+ „Über mich.“
+
+ „Teilweise.“
+
+ *** [__Sage__: „Darüber, ob ich Betrügerin, Närrin oder nützliches Tier bin.“] #action:conversation
+ ~ viktor_suspicion += 1
+ „Darüber, ob ich Betrügerin, Närrin oder nützliches Tier bin.“
+
+ „Teilweise“, sagt er, und diesmal hat die Ehrlichkeit eine Schneide.
+
+ ---
+
+ ** [__Sage__: „Über mich.“] #action:conversation
+ „Über mich.“
+
+ „Teilweise.“
+
+ ** [__Sage__: „Darüber, ob ich Betrügerin, Närrin oder nützliches Tier bin.“] #action:conversation
+ ~ viktor_suspicion += 1
+ „Darüber, ob ich Betrügerin, Närrin oder nützliches Tier bin.“
+
+ „Teilweise“, sagt er, und diesmal hat die Ehrlichkeit eine Schneide.
+
+ --
+
+* [__Sage__: „Wie rührend. Wien vertraut uns beiden so wenig, dass es das Misstrauen aufteilen musste.“] #action:conversation
+ #route:eccentric
+ ~ eccentric += 1
+ ~ viktor_suspicion += 1
+ „Wie rührend. Wien vertraut uns beiden so wenig, dass es das Misstrauen aufteilen musste.“
+
+ Viktor fragt nicht, woher du es weißt.
+
+ „Es gibt immer noch eine weitere Weisung“, sagt er.
+
+ ** [__Sage__: „Für Sie.“] #action:conversation
+ „Für Sie.“
+
+ „Ja.“
+
+ *** [__Sage__: „Über mich.“] #action:conversation
+ „Über mich.“
+
+ „Teilweise.“
+
+ *** [__Sage__: „Darüber, ob ich Betrügerin, Närrin oder nützliches Tier bin.“] #action:conversation
+ ~ viktor_suspicion += 1
+ „Darüber, ob ich Betrügerin, Närrin oder nützliches Tier bin.“
+
+ „Teilweise“, sagt er, und diesmal hat die Ehrlichkeit eine Schneide.
+
+ ---
+
+ ** [__Sage__: „Über mich.“] #action:conversation
+ „Über mich.“
+
+ „Teilweise.“
+
+ ** [__Sage__: „Darüber, ob ich Betrügerin, Närrin oder nützliches Tier bin.“] #action:conversation
+ ~ viktor_suspicion += 1
+ „Darüber, ob ich Betrügerin, Närrin oder nützliches Tier bin.“
+
+ „Teilweise“, sagt er, und diesmal hat die Ehrlichkeit eine Schneide.
+
+ --
+
+-
+
+Der Zug beginnt langsamer zu werden. Der Rhythmus verändert sich zuerst im Boden, dann im Fenster, dann im Körper. Häuser sammeln sich neben der Strecke. Ein Stationsdach erscheint zwischen treibendem Rauch und den dunklen Kämmen bewaldeter Hänge. #sfx[steam-whistle.ogg]
+
+* [__Sage__: „Dann werde ich versuchen, der Tinte wert zu sein.“] #action:conversation
+ „Dann werde ich versuchen, der Tinte wert zu sein.“
+
+ „Das hoffe ich aufrichtig.“
+
+* [__Sage__: „Dann werde ich die Weisung so kunstvoll enttäuschen, wie es die Umstände erlauben.“] #action:conversation
+ #route:eccentric
+ ~ eccentric += 1
+ „Dann werde ich die Weisung so kunstvoll enttäuschen, wie es die Umstände erlauben.“
+
+ „Ich hoffe aufrichtig, dass Sie es nicht tun.“
+
+* [__Sage__: „Dann behalten Sie Ihre zweite Weisung, Herr Nowak. Ich bevorzuge Quellen erster Hand.“] #action:conversation
+ #route:detective
+ ~ detective += 1
+ „Dann behalten Sie Ihre zweite Weisung, Herr Nowak. Ich bevorzuge Quellen erster Hand.“
+
+ „Eine Vorliebe, die im kaiserlichen Dienst nicht immer gewährt wird.“
+
+-
+
+Du kannst nicht entscheiden, ob seine Antwort eine Beleidigung, ein Gebet oder sein erster ehrlicher Satz ist.
+
+-> railway_station
+
+=== railway_station ===
+
+Die Station ist klein genug, dass der Zug kurz verlegen wirkt, als er dort hält. #chapter[The Station] #image[muerzzuschlag.png](portrait)
+
+Ein Gepäckträger mit einer zu großen Kappe eilt über den Bahnsteig. Eine Frau mit Korb tritt vor dem Dampf zurück wie vor einem Tier. Irgendwo jenseits des Stationsgebäudes stampft ein Kutschpferd im gefrorenen Schlamm. Das Schild gibt dem Ort einen Namen, den du im Fahrplan gesehen hast und an den du dich nicht mit Zuneigung erinnern wirst.
+
+Dein Gepäck wird in Etappen ausgeladen.
+
+* [__Bestimme__: Eine disziplinierte amtliche Zusammenstellung.] #action:thinking
+ ~ baggage_style = "official"
+ ~ detective += 1
+ Zuerst kommt ein nüchterner Reisekoffer mit vom Gebrauch stumpfen Messingecken, dann eine Aktenmappe, dann eine Hutschachtel, dann der schmale schwarze Kasten, dessen Inhalt sowohl einen Priester als auch einen Taschenspieler in Verlegenheit bringen würde, falls einer von beiden ihn ohne Phantasie durchsuchte.
+
+* [__Bestimme__: Das Gepäck einer eleganten Dame.] #action:thinking
+ ~ baggage_style = "elegant"
+ ~ class_confidence += 1
+ Zuerst kommt ein großer Koffer aus dunklem Leder, dann ein kleinerer für Wäsche, dann eine runde Hutschachtel, ein Reise-Necessaire und ein Ridikül, das du zu nahe bei der Hand behältst, als dass ein Gepäckträger seine Bedeutung missverstehen dürfte.
+
+* [__Bestimme__: Das Gepäck einer Darstellerin.] #action:thinking
+ ~ baggage_style = "performer"
+ ~ medium_reputation += 1
+ Zuerst kommt ein respektabler Koffer, dann eine Hutschachtel, dann ein Reisekasten mit Handschuhen, Schleiern, Bändern, Visitenkarten und kleinen Gegenständen, mit denen man ein Zimmer überreden kann, an Kräfte zu glauben, die längst anwesend sind.
+
+* [__Bestimme__: Eine praktische Auswahl, die zu viel Vorbereitung verrät.] #action:thinking
+ ~ baggage_style = "practical"
+ ~ detective += 1
+ Zuerst kommt ein abgenützter, an den Ecken verstärkter Koffer, dann eine Ledertasche mit Notizheften, Bleistiften, gefalteten Karten, Ersatzhandschuhen, einer Handlampe und genug kleinen Notwendigkeiten, um jeden zu beleidigen, der Frauen lieber dekorativ hat.
+
+* [__Bestimme__: Ein übertriebener Haufen, der jede Tarnung erschwert.] #action:thinking
+ ~ baggage_style = "excessive"
+ ~ careless += 1
+ Zuerst kommt ein Koffer, dann ein zweiter, dann eine Hutschachtel, dann eine Reisedecke, dann ein Toilettenkasten, dann der schmale schwarze Kasten, dann ein kleineres Paket, von dem du vergessen hattest, dass es das Packen überlebt hat. Am Ende sieht selbst Viktor einen Augenblick lang zahlenmäßig unterlegen aus.
+
+-
+
+Viktor überwacht die Umladung mit knapper Höflichkeit. Er trägt nicht wie ein Diener. Er weist an wie ein Mann, der vorgibt, nicht zu befehlen.
+
+Die kleine Szene vor dem Waggon ist harmlos genug, um gefährlich zu sein. Ein Gepäckträger wartet mit geneigtem Kopf. Der Kutscher steht einige Schritte entfernt. Viktor ist nah genug, um dir beim Aussteigen die Hand zu reichen, aber nicht so nah, dass er es ohne dein stilles Einverständnis täte. Drei Männer, drei Stände, drei verschiedene Arten von Nützlichkeit.
+
+Was hier geschieht, wird niemand in einem Bericht erwähnen. Gerade deshalb wird es behalten.
+
+{not tut_manners_intro:
+ #alert[Soziale Optionen prüfen Benehmen, Rang und Timing. Nicht jede unhöfliche Wahl ist falsch, aber jede Wahl verrät, wie Valerie sich in der Ordnung ihrer Zeit bewegt.]
+ ~ tut_manners_intro = true
+}
+
+{(birth_class == "noble" or birth_class == "working") and not tut_gated_intro:
+ #alert[Freigeschaltete Optionen zeigen ihre Voraussetzung nach einem Mittelpunkt, zum Beispiel Adel oder Unterschicht. Solche Optionen erscheinen nur, wenn deine bisherigen Entscheidungen sie erlauben.]
+ ~ tut_gated_intro = true
+}
+
+* {birth_class == "noble"} [__Warte__ · **Adel**: Bis Viktor seine Hand anbietet.] #action:social #gated:noble #key:z
+ #manners:excellent
+ ~ class_confidence += 2
+ ~ court_loyalty += 1
+ Du wartest einen Atemzug, bis Viktor seine Hand anbietet, und nimmst sie dann, als wäre dies keine Hilfe, sondern die Ordnung der Welt.
+
+ Du gibst ihm nicht dein Gewicht. Nur deine Hand. Genau genug, dass er dienen darf, ohne Diener zu werden. Der Gepäckträger senkt den Blick ein wenig tiefer. Der Kutscher sieht, was er sehen muss: eine Dame, die ihren Rang nicht beweist, weil Beweise für Leute ohne Rang sind.
+
+* [__Danke__: Viktor mit einem Nicken und lass den Gepäckträger das Gepäck nehmen.] #action:social
+ #manners:good
+ ~ viktor_trust += 1
+ Du nimmst Viktors angebotene Hand knapp und sicher, dankst ihm mit einem Nicken und lässt den Gepäckträger das Gepäck nehmen.
+
+ Es ist gutes Benehmen ohne Prunk: nicht zu vertraut gegenüber Viktor, nicht zu freundlich gegenüber dem Gepäckträger, nicht so kalt, dass es nach Unsicherheit riecht. Mittelstand könnte dies lernen. Adel könnte es billigen. Dienstboten würden erkennen, dass du ihre Arbeit nicht mit Herablassung verwechselst.
+
+* [__Bitte__: Den Gepäckträger, zuerst den kleineren Kasten zu nehmen.] #action:social
+ #route:detective
+ #manners:practical
+ ~ detective += 1
+ Du steigst selbst aus, bevor Viktor sich entscheiden kann, und bittest den Gepäckträger sachlich, zuerst den kleineren Kasten zu nehmen.
+
+ Das ist nicht ganz falsch, aber auch nicht ganz richtig. Viktor bemerkt die kleine Missachtung der erwarteten Form. Der Gepäckträger gehorcht erleichtert, weil klare Anweisungen leichter zu tragen sind als feine Ungewissheit. Der Kutscher ordnet dich eher der Nützlichkeit als dem Rang zu.
+
+* [__Täusche__: Prüfe Viktor mit einem zu langen Lächeln.] #action:social
+ #route:lover
+ #manners:provocative
+ ~ lover += 1
+ ~ viktor_suspicion += 1
+ Du lässt Viktor zu lange mit ausgestreckter Hand warten und lächelst erst dann, als hättest du ihn absichtlich geprüft.
+
+ Es ist fast ein Fauxpas, gerettet durch Anmut und die Tatsache, dass Männer Demütigungen leichter verzeihen, wenn sie sich wie Aufmerksamkeit anfühlen. Viktor hilft dir hinunter. Seine Hand bleibt vollkommen korrekt. Sein Blick nicht ganz.
+
+* [__Berühre__: Greife selbst nach einem Koffer.] #action:object
+ #route:careless
+ #manners:awkward
+ ~ careless += 1
+ Du entschuldigst dich beim Gepäckträger dafür, dass deine Sachen Mühe machen, und greifst selbst nach einem Koffer.
+
+ Der Gepäckträger erstarrt, als hättest du ihm eine philosophische Frage gestellt. Viktor tritt sofort dazwischen, höflich genug, um die Rettung wie Zufall aussehen zu lassen. Du hast gegen keine Moral verstoßen, nur gegen die unsichtbare Arbeitsteilung, auf der diese kleine Welt ruht.
+
+* {birth_class == "working"} [__Nimm__ · **Unterschicht**: Dem Gepäckträger beinahe den Koffer aus der Hand.] #action:object #gated:working #key:t
+ #manners:fauxpas
+ ~ class_confidence -= 1
+ ~ careless += 1
+ Du springst hinunter, bevor jemand dir helfen kann, und nimmst dem Gepäckträger beinahe den Koffer aus der Hand.
+
+ Für eine Sekunde bist du schneller als deine Verkleidung. Der Gepäckträger hält fest, Viktor greift nach deinem Ellbogen, der Kutscher sieht weg, weil Wegsehen manchmal die höflichste Form von Zeugenschaft ist. Es ist kein Unglück. Nur ein Riss, klein genug, um ihn mit Haltung zu schließen.
+
+-
+
+Die Kutsche aus Hohenreith wartet jenseits des Stationshofes: dunkelgrüner Lack, schwarze Räder, das gräfliche Wappen dezent auf der Tür, zwei Pferde bereits unruhig im Geschirr. Der Kutscher nimmt den Hut ab, als er dich sieht. Nicht zu tief. Tief genug für Rang, nicht tief genug für Ehrfurcht. #sfx[horse-neigh.ogg]
+
+„Gnädiges Fräulein? Herr Sekretär?“
+
+{birth_class == "noble":
+ Man hat ihm genug gesagt, um dich einzuordnen. Das ist eine Höflichkeit. Es ist auch eine Warnung.
+- else:
+ Er zögert bei dir um das kleinste Maß. Das Zögern ist keine Unhöflichkeit. Es ist Berechnung. Erste Klasse, Hofschreiben, kein Titel außer Fräulein, und ein Mann neben dir, der aussieht, als hätte er Menschen für weniger verhaften lassen als Starren.
+}
+
+Viktor antwortet, bevor du es kannst.
+
+„Vom Jagdhaus Hohenreith?“
+
+„Jawohl, Herr Sekretär. Der Weg ist befahrbar. Wenn der Nebel nicht dichter wird, sollten wir Eibenreith vor Einbruch der Dunkelheit erreichen.“
+
+Das Wort tritt ohne Zeremonie in die Luft.
+
+Eibenreith.
+
+Nicht Hohenreith, der Name, der in sauberer Hand auf der Einladung steht. Eibenreith: das Dorf darunter. Ein kleinerer Name. Älter im Mund. Ein Name mit Wurzeln statt Briefpapier.
+
+-> coach_journey
+
+=== coach_journey ===
+
+Die Kutsche lässt die Station hinter sich und damit das letzte leicht erkennbare Zeichen der Monarchie. #chapter[The Graben] #music[Kaiserpunk Jodler.mp3](crossfade, loop, lead=4)
+
+Zuerst folgt der Weg einem Tal, in dem Telegraphendraht ihm noch Gesellschaft leistet und der Fluss in einem hellen, steinigen Bett läuft. Sägewerke, umzäunte Wiesen und Bauernhäuser erscheinen und verschwinden hinter Fichtenbeständen. Die Berge steigen nicht auf einmal. Sie rücken zuständigkeitsweise vor. Ein bewaldeter Hang beansprucht den linken Himmel, dann schließt eine graue Wand aus Kalk den Norden, dann sammelt sich im Osten ein weiterer Rücken, bis selbst die Wolken in Dienst getreten scheinen.
+
+Der Kutscher nennt Orte, wenn Viktor fragt, doch die Namen sind örtlich und praktisch, gedacht für Männer, die wissen, welche Brücke bei Hochwasser nachgibt und welcher Hof störrische Pferde hält. Irgendwo hinter den sichtbaren Rücken, sagt er, liegt der große weiße Rücken des Hochschwab. Nach Osten, jenseits von Wald und Pass, hält die Hohe Veitsch ihr eigenes Wetter. Er sagt das nicht wie ein Führer, sondern wie ein Mann, der Nachbarn erklärt, die vielleicht guter Laune sind und vielleicht nicht.
+
+Das Haupttal verengt sich.
+
+Der Weg biegt davon in einen Seitengraben, und die Veränderung ist augenblicklich. Der Klang ändert sich. Die Räder klingen nicht mehr gegen offene Entfernung, sondern mahlen zwischen Böschungen, Wurzeln und nassem Stein. Die Luft riecht nach Lauberde, Harz und kaltem Wasser. Eiben erscheinen zwischen den Fichten in dunkler, unwahrscheinlicher Geduld, ihre Nadeln zu schwarz für den Nachmittag.
+
+„Eibenreither Graben“, sagt der Kutscher und bekreuzigt sich so rasch, dass die Geste auch einem Schlagloch gegolten haben könnte.
+
+Viktor bemerkt es. Natürlich bemerkt er es.
+
+„Schlechter Weg?“, fragt er.
+
+„Alter Weg“, sagt der Kutscher.
+
+Eine Weile spricht niemand.
+
+Du beobachtest die Bäume.
+
+Es gibt Wälder, die zu Geschichten einladen, weil sie hübsch sind, und Wälder, die Geschichten zurückweisen, weil das, was dort geschah, keine Zeugen brauchte. Dieser gehört zur zweiten Art. Seine Stämme stehen eng, nicht wild, sondern mit der Haltung einer Menge, die Platz macht für etwas, das vor langer Zeit durch sie getragen wurde. Der Schnee in den Mulden ist nicht rein. Er hat Nadeln gesammelt, Rinde und einen gelblichen Fleck dort, wo Wasser von unten aufgestiegen ist.
+
+An einem Hang oberhalb des Weges, halb vom Unterholz verschluckt, erblickst du Stein.
+
+Ein Wegheiligtum vielleicht. Ein Grenzzeichen. Eine Figur. Die Kutsche ist schon vorbei, bevor deine Augen sich auf ihre Form einigen können. Für einen Augenblick bleibt der Eindruck eines Frauenkopfes zurück, geneigt nicht im Gebet, sondern im Lauschen. #image[statue.png](square)
+
+{supernatural_senses == "genuine" or supernatural_senses == "ambiguous" or supernatural_senses == "repressed":
+ Dein Nacken zieht sich zusammen.
+
+ Nicht Furcht. Wiedererkennen wäre schlimmer.
+- else:
+ Du sagst dir, dass alter Stein, durch bewegte Zweige gesehen, zu allem wird, wozu der Geist feig genug ist.
+}
+
+Viktor hat sich leicht demselben Hang zugewandt.
+
+„Haben Sie etwas gesehen?“
+
+* [__Sage__: „Vielleicht eine Frau im Wald. Oder ein Stein, der eine sein wollte.“] #action:conversation
+ #route:eccentric
+ #statue_hint
+ ~ eccentric += 1
+ ~ viktor_suspicion += 1
+ „Vielleicht eine Frau im Wald. Oder ein Stein, der eine sein wollte.“
+
+ Er betrachtet die vorbeiziehenden Bäume.
+
+ „Ein Wegheiligtum?“
+
+ ** [__Sage__: „Wenn es ein Heiligtum ist, so ist es seit langem nicht mehr geliebt worden.“] #action:conversation
+ „Wenn es ein Heiligtum ist, so ist es seit langem nicht mehr geliebt worden.“
+
+ „Sie sprechen, als bemerkten Steine Vernachlässigung.“
+
+ Soldaten bemerken Vernachlässigung ebenfalls. Sein Schweigen gesteht genug zu.
+
+ Er antwortet nicht.
+
+ ** [__Sage__: „Nein. Heiligtümer wenden sich den Gläubigen zu. Dieses Ding lauschte seitwärts.“] #action:conversation
+ ~ supernatural_exposure += 1
+ „Nein. Heiligtümer wenden sich den Gläubigen zu. Dieses Ding lauschte seitwärts.“
+
+ Viktors Hand ruht am Halteriemen der Kutsche, still und bereit.
+
+ --
+
+* [__Sage__: „Ein Grenz- oder Wegzeichen. Ich wüsste gern, wohin dieser Pfad führt.“] #action:conversation
+ #route:detective
+ #statue_hint
+ ~ detective += 1
+ ~ viktor_trust += 1
+ „Ein Grenz- oder Wegzeichen. Ich wüsste gern, wohin dieser Pfad führt.“
+
+ „Sie haben einen Pfad gesehen?“
+
+ ** [__Sage__: „Nicht deutlich. Genug, um später danach zu fragen.“] #action:conversation
+ „Nicht deutlich. Genug, um später danach zu fragen.“
+
+ Viktor blickt durch das kleine rückwärtige Fenster. Die Biegung hat den Hang bereits ausgelöscht.
+
+ „Fragen Sie vorsichtig. Orte, die man nicht erwähnt, sind oft aufschlussreicher als jene, die man empfiehlt.“
+
+ ** [__Sage__: „Nur die Andeutung eines Pfades. Wenn er existiert, erhält jemand seine Abwesenheit aufrecht.“] #action:conversation
+ #route:detective
+ ~ detective += 1
+ „Nur die Andeutung eines Pfades. Wenn er existiert, erhält jemand seine Abwesenheit aufrecht.“
+
+ „Sie lassen Abwesenheiten kostspielig klingen.“
+
+ Das sind sie meistens; Abwesenheit ist teuer, wenn jemand sie pflegt.
+
+ --
+
+* [__Sage__: „Nur Bäume. Die Art, bei der man für Herren mit Revolvern dankbar wird.“] #action:conversation
+ #route:careless
+ ~ careless += 1
+ ~ viktor_relation = "dependence"
+ „Nur Bäume. Die Art, bei der man für Herren mit Revolvern dankbar wird.“
+
+ Sein Ausdruck verdunkelt sich um einen amtlichen Grad.
+
+ „Ein Revolver ist ein schlechtes Werkzeug gegen Bäume.“
+
+ ** [__Sage__: „Dann werde ich mich darauf verlassen, dass Ihre Unterhaltung sie einschüchtert.“] #action:conversation
+ „Dann werde ich mich darauf verlassen, dass Ihre Unterhaltung sie einschüchtert.“
+
+ Der Kutscher tut, als höre er nichts. Seine Schultern jedoch hören alles.
+
+ ** [__Sage__: „Wie bedauerlich. Sie wirkten so berufsmäßig beruhigend.“] #action:conversation
+ #route:lover
+ ~ lover += 1
+ „Wie bedauerlich. Sie wirkten so berufsmäßig beruhigend.“
+
+ „Ich bevorzuge Feinde, die sich zu erkennen geben.“
+
+ --
+
+* [__Sage__: „Würden Sie mir glauben, wenn ich sagte, ich hätte etwas gesehen?“] #action:conversation
+ #route:lover
+ ~ lover += 1
+ ~ viktor_suspicion += 1
+ „Würden Sie mir glauben, wenn ich sagte, ich hätte etwas gesehen?“
+
+ „Das hinge davon ab, welchen Vorteil Sie sich von der Antwort versprechen.“
+
+ ** [__Sage__: „Herr Nowak. Sie verletzen mich.“] #action:conversation
+ „Herr Nowak. Sie verletzen mich.“
+
+ „Noch nicht.“
+
+ Es ist das Erste, was er an diesem Tag gesagt hat, das beinahe wie ein Flirt klingt, wenn auch vielleicht nur deshalb, weil Gefahr ein Talent dafür hat, wärmere Kleider zu borgen.
+
+ ** [__Sage__: „Dann beobachten Sie den Hang, nicht meine Absichten. Eines von beidem könnte nützlich sein.“] #action:conversation
+ ~ viktor_trust += 1
+ „Dann beobachten Sie den Hang, nicht meine Absichten. Eines von beidem könnte nützlich sein.“
+
+ Er gehorcht, ohne zuzugeben, dass er es getan hat.
+
+ --
+
+* [__Sage__: „Nein.“] #action:conversation
+ #route:sapphic
+ ~ sapphic += 1
+ „Nein.“
+
+ Die Verneinung kommt zu rasch, und ihr hört es beide.
+
+ Du denkst nicht mehr an den Stein. Du denkst an die junge Frau, die irgendwo vor euch wartet: die Tochter des Grafen, der Grund, der sorgsam nicht im Memorandum steht, die Fremde, deren Haushalt dich unter einem Titel herbeigerufen hat, der zugleich lächerlich und nützlich ist.
+
+ ** [__Sage__: „Es war nur Schatten.“] #action:conversation
+ „Es war nur Schatten.“
+
+ Wenn dieser Ort Frauen in Stein hält, denkst du, was tut er dann mit ihnen in den Häusern?
+
+ ** [__Sage__: „Oder, falls ich etwas sah, ziehe ich es vor, es mir nicht erklären zu lassen, bevor ich verstehe, warum es von Bedeutung ist.“] #action:conversation
+ #route:detective
+ ~ detective += 1
+ „Oder, falls ich etwas sah, ziehe ich es vor, es mir nicht erklären zu lassen, bevor ich verstehe, warum es von Bedeutung ist.“
+
+ Wenn dieser Ort Frauen in Stein hält, denkst du, was tut er dann mit ihnen in den Häusern?
+
+ --
+
+-
+
+Der Graben öffnet sich widerwillig.
+
+Zuerst kommt der Geruch von Rauch. Dann ein Dach, niedrig und dunkel vom Wetter. Dann ein zweites. Dann ein Kirchturm, nicht hoch, nicht anmutig, sondern breitschultrig und blass vor dem Hang dahinter. Seine Mauern wirken älter als das Dorf um sie her und weniger sicher ihres Sieges. Die Fenster sind klein. Die Kirchhofmauer hält die Straße auf Abstand, als bräuchten die Toten Schutz vor den Lebenden oder die Lebenden vor etwas anderem. #chapter[Eibenreith Village] #sfx[church-bells.ogg](max=8, fade) #image[eibenreith.png](landscape)
+
+Eibenreith erscheint nicht, wie ein Dorf auf einem Bild erscheint, auf einmal und zur Bewunderung geordnet, sondern in Bruchstücken.
+
+Eine Frau mit einem dunklen Kopftuch hält mit einem Eimer in der Hand inne. Ein Bub hört auf, Gänse zu treiben, und lässt sie um seine Stiefel klagen. Zwei Männer vor einem Schuppen beenden im selben Augenblick ihr Gespräch, ohne einander anzusehen. Vorhänge rühren sich an Fenstern, hinter denen niemand zugibt zu stehen. Ein Schmiedeschild bewegt sich leicht in Luft, die du nicht fühlen kannst. Wasser läuft irgendwo unter Brettern, unter Stein, unter der Straße selbst, schnell, kalt und verborgen.
+
+Die Häuser sind nicht arm, nicht eigentlich. Viele sind fest, weißgekalkt, geschindelt, erhalten mit der störrischen Anständigkeit von Menschen, die reparieren, was sie nicht ersetzen können. Und doch stört etwas in ihrer Anordnung das Auge. Sie wenden sich der Kirche zu, aber nicht ganz. Sie halten die Straße, aber lehnen sich von ihr weg. Sie lassen zwischen Hof, Zaun und Holzstoß schmale Durchgänge, in denen sich Schatten zu früh sammelt.
+
+Die Kutsche wird langsamer.
+
+Niemand läuft herbei, um sie zu begrüßen.
+
+Niemand muss das. Die Nachricht ist bereits ins Dorf eingetreten, auf Wegen schneller als Bahn, Telegraph oder kaiserliches Siegel.
+
+Du sitzt sehr gerade, während Eibenreith dich zum ersten Mal betrachtet.
+
+Der Kutscher hält vor dem Wirtshaus oder vielleicht nur vor dem Gebäude, das in einem besseren Dorf eines gewesen wäre. Ein Knecht aus dem Dorf tritt aus dem Schatten des Tors. Viktor öffnet die Kutschentür von innen nicht sofort; der Kutscher steigt ab, um den Schlag zu öffnen. Der Knecht sieht auf dein Gepäck, dann auf deine Handschuhe, dann auf Viktor.
+
+Wieder stellt die Welt eine Frage, ohne sie auszusprechen: Wer darf dir helfen, wer muss dir helfen, und wem erlaubst du, dabei wichtig zu wirken?
+
+* {birth_class == "noble"} [__Warte__ · **Adel**: Bis der Kutscher den Schlag öffnet und Viktor zuerst aussteigt.] #action:social #gated:noble #key:z
+ #manners:excellent
+ ~ class_confidence += 2
+ Du wartest, bis der Kutscher den Schlag öffnet und Viktor zuerst aussteigt; erst dann reichst du Viktor die behandschuhte Hand.
+
+ Es geschieht langsam genug, dass alle Beteiligten ihre Rolle finden. Der Kutscher ist Dienst, Viktor ist Begleitung, der Knecht ist noch nicht wichtig genug, um dich zu berühren. Dein Fuß erreicht den Boden, als hätte die Straße sich dafür bereitgehalten.
+
+* [__Danke__: Dem Kutscher mit einem knappen Blick, nachdem Viktor dir geholfen hat.] #action:social
+ #manners:good
+ ~ viktor_trust += 1
+ Du lässt Viktor aussteigen, nimmst seine Hand beim Abtreten und dankst dem Kutscher erst danach mit einem knappen Blick.
+
+ Der Ablauf ist korrekt genug, um keine Geschichte zu erzeugen. In einem Dorf, das von Geschichten lebt, ist das ein kleiner Sieg.
+
+* [__Weise den Knecht an__: Welches Gepäck zuerst abgeladen werden soll.] #action:social
+ #route:detective
+ #manners:practical
+ ~ detective += 1
+ Du gibst dem Knecht eine klare Anweisung, welches Gepäck zuerst abgeladen werden soll, bevor er danach fragen kann.
+
+ Er gehorcht sofort. Viktor registriert die Zweckmäßigkeit. Der Kutscher registriert die Ungewöhnlichkeit. Eine Dame, die Gepäckreihenfolgen kennt, ist entweder sehr erfahren, sehr nervös oder beides.
+
+* [__Danke__: Dem Kutscher mit einem zu freundlichen Lächeln.] #action:social
+ #route:lover
+ #manners:too_warm
+ ~ lover += 1
+ Du bietest dem Kutscher ein sichtbares Lächeln und ein zu freundliches „Danke“ an.
+
+ Der Mann senkt den Blick, verwirrt und geschmeichelt. Viktor wird stiller. Freundlichkeit über Standesgrenzen hinweg kann Güte sein, Taktik oder Unachtsamkeit. Auf dem Dorf wird niemand lange brauchen, eine vierte Möglichkeit zu erfinden.
+
+* [__Gehe__: Zu früh aus der Kutsche.] #action:movement
+ #route:careless
+ #manners:awkward
+ ~ careless += 1
+ ~ viktor_relation = "dependence"
+ Du steigst zu früh aus, trittst beinahe in den Straßenschlamm und fängst dich an Viktors Arm.
+
+ Er hält dich ohne sichtbare Anstrengung fest. Für einen Augenblick sieht das Dorf genau das, was es am liebsten sieht: eine Dame, gerettet durch einen Mann. Es ist lächerlich nützlich und nützlich lächerlich.
+
+* {birth_class == "working"} [__Gehe__ · **Unterschicht**: Allein aus der Kutsche.] #action:movement #gated:working
+ #manners:fauxpas
+ ~ class_confidence -= 1
+ Du steigst allein aus, nimmst deinen Rock hoch genug, um den Schlamm zu sehen, und sagst dem Knecht, er solle mit dem schweren Koffer vorsichtig sein.
+
+ Es ist praktisch, schnell und völlig falsch. Nicht, weil du unrecht hast, sondern weil du recht hast wie jemand, der selbst schon getragen hat. Der Knecht erkennt es. Viktor auch.
+
+-
+
+Neben dir senkt Viktor die Stimme.
+
+„Vergessen Sie nicht: In Hohenreith wird jede Höflichkeit etwas bedeuten. Hier wird es jedes Schweigen tun.“
+
+* [__Sage__: „Dann werden wir bereits empfangen.“] #action:conversation
+ #route:detective
+ ~ detective += 1
+ „Dann werden wir bereits empfangen.“
+
+ „Ja“, sagt er. „Und geprüft.“
+
+* [__Sage__: „Sie lassen es klingen, als stünde das Dorf über dem Grafen.“] #action:conversation
+ #route:eccentric
+ ~ eccentric += 1
+ „Sie lassen es klingen, als stünde das Dorf über dem Grafen.“
+
+ „Nein“, sagt Viktor. „Nur, als hätte es vielleicht mehr als einen überlebt.“
+
+* [__Sage__: „Wie glücklich, dass ich mehrere Arten des Schweigens eingepackt habe.“] #action:conversation
+ #route:lover
+ ~ lover += 1
+ „Wie glücklich, dass ich mehrere Arten des Schweigens eingepackt habe.“
+
+ Sein Mund bewegt sich beinahe. „Verwenden Sie zuerst das schlichteste.“
+
+* [__Sage__: „Ich mag es nicht, von Leuten beobachtet zu werden, die sich nicht vorstellen.“] #action:conversation
+ #route:careless
+ ~ careless += 1
+ „Ich mag es nicht, von Leuten beobachtet zu werden, die sich nicht vorstellen.“
+
+ „Das“, sagt er, „wird sich heute kaum bessern.“
+
+* [__Sage__: „Wenn Amalia ihr ganzes Leben unter diesem Blick gelebt hat, beginne ich zu verstehen, weshalb man nach Geistern sandte.“] #action:conversation
+ #route:sapphic
+ ~ sapphic += 1
+ „Wenn Amalia ihr ganzes Leben unter diesem Blick gelebt hat, beginne ich zu verstehen, weshalb man nach Geistern sandte.“
+
+ Viktor sieht dich an, doch welche Antwort er auch erwägt, er behält sie hinter den Zähnen.
+
+-
+
+Die Pferde ziehen die Kutsche an der Kirchhofmauer vorbei. Darüber, auf dem alten Putz neben dem Tor, blickt eine verblasste gemalte Frau unter einem abblätternden blauen Mantel herab. Ihre Hände sind zum Gebet gefaltet. Ihre Augen, vom Wetter beschädigt, zeigen nicht mehr in dieselbe Richtung.
+
+{religion_stance == "devout_catholic":
+ Für einen Atemzug stört dich nicht, dass das Bild alt ist. Es stört dich, dass es nicht mehr ganz heilig wirkt.
+- else:
+ {religion_stance == "josephinian_sceptic":
+ Für einen Atemzug wirkt das Bild weniger wie Andacht als wie Verwaltung: ein aufgemaltes Siegel über etwas, das man nicht fortschaffen konnte.
+ - else:
+ {religion_stance == "wounded_catholic":
+ Für einen Atemzug trifft dich das gemalte Gesicht an einer Stelle, die du lieber Schuld als Erinnerung nennen würdest.
+ - else:
+ Für einen Atemzug, als die Räder über ein verborgenes Wasserrinnsal fahren, wirkt das gemalte Gesicht weniger wie die Heilige Mutter als wie eine Maske, die etwas aufgesetzt wurde, das länger gewartet hatte.
+ }
+ }
+}
+
+Dann fährt die Kutsche in das eigentliche Dorf hinein, und die Straße biegt zu der unsichtbaren Höhe, auf der Jagdhaus Hohenreith über Eibenreith unter seinem neueren Namen steht.
+
+#score[Du hast Eibenreith erreicht.]
+-> END
diff --git a/data/ink/eibenreith.ink.json b/data/ink/eibenreith.ink.json
new file mode 100644
index 0000000..5fca3a8
--- /dev/null
+++ b/data/ink/eibenreith.ink.json
@@ -0,0 +1 @@
+{"inkVersion":21,"root":[[{"->":"intro_train"},["done",{"#n":"g-0"}],null],"done",{"intro_train":["^Der Zug hatte Wien hinter sich gelassen, doch Wien hatte dich noch nicht freigegeben. ","#","^chapter[Eibenreith] ","/#","#","^music[Kaiserpunk Waltz.mp3](crossfade, loop, lead=8)","/#","\n","^Es hing noch am schwarzen Glanz deiner Reisestiefel, am Schnitt deines Mantels, am engen kleinen Gefängnis deiner Handschuhe. Es lag im Siegel des Schreibens, das in deinem Ridikül ruhte, im Geruch von Kohlenrauch, der sich selbst in die Polster der ersten Klasse geschlichen hatte, und in der Tatsache, dass Herr Viktor Nowak dir gegenübersaß, als wäre dieses Abteil kein mit Samt, Messing und poliertem Holz ausgekleideter Reiseraum, sondern ein provisorisches Amtszimmer auf Rädern.","\n",{"->":"train_compartment"},null],"train_compartment":[["ev",{"VAR?":"tut_choice_intro"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n","#","^alert[Optionen beginnen mit einem fett gesetzten Aktionswort. Dieses Wort zeigt, was du tust: schauen, untersuchen, lesen, bestimmen, fragen, antworten oder warten. Manche klassische Aktionen haben feste Tasten, etwa L für Schaue und X für Untersuche.]","/#","ev",true,"/ev",{"VAR=":"tut_choice_intro","re":true},{"->":".^.^.^.5"},null]}],"nop","\n","ev",{"VAR?":"saw_window"},{"VAR?":"observed_viktor"},"&&",{"VAR?":"birth_class"},"str","^unset","/str","!=","&&",{"VAR?":"religion_stance"},"str","^unset","/str","!=","&&",{"VAR?":"supernatural_senses"},"str","^unset","/str","!=","&&",{"VAR?":"appearance_done"},"&&","/ev",[{"->":".^.b","c":true},{"b":["\n",{"->":"first_viktor_exchange"},{"->":".^.^.^.33"},null]}],"nop","\n","ev","str","^___Schaue___: _Aus dem Fenster._","/str",{"VAR?":"saw_window"},"!","/ev",{"*":".^.c-0","flg":21},"ev","str","^___Untersuche___: _Viktor._","/str",{"VAR?":"observed_viktor"},"!","/ev",{"*":".^.c-1","flg":21},"ev","str","^__Bestimme__: Deine Herkunft anhand des Abteils.","/str",{"VAR?":"birth_class"},"str","^unset","/str","==","/ev",{"*":".^.c-2","flg":21},"ev","str","^__Lies__: Das Schreiben in deinem Ridikül.","/str",{"VAR?":"religion_stance"},"str","^unset","/str","==","/ev",{"*":".^.c-3","flg":21},"ev","str","^__Bestimme__: Dein Spiegelbild.","/str",{"VAR?":"birth_class"},"str","^unset","/str","==",{"VAR?":"glanced_mirror_early"},"!","&&","/ev",{"*":".^.c-4","flg":21},"ev","str","^__Bestimme__: Dein Spiegelbild.","/str",{"VAR?":"birth_class"},"str","^unset","/str","!=",{"VAR?":"appearance_done"},"!","&&","/ev",{"*":".^.c-5","flg":21},{"c-0":["^ ","#","^action:orientation ","/#","#","^optional ","/#","#","^key:l","/#","\n","ev",true,"/ev",{"VAR=":"saw_window","re":true},{"->":"look_out_window"},{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","#","^action:orientation ","/#","#","^optional ","/#","#","^key:x","/#","\n","ev",true,"/ev",{"VAR=":"observed_viktor","re":true},{"->":"observe_viktor"},{"->":".^.^.g-0"},{"#f":5}],"c-2":["^ ","#","^action:thinking","/#","\n",{"->":"define_class_and_name"},{"->":".^.^.g-0"},{"#f":5}],"c-3":["^ ","#","^action:orientation","/#","\n",{"->":"define_religion_and_supernatural"},{"->":".^.^.g-0"},{"#f":5}],"c-4":["^ ","#","^action:thinking","/#","\n","ev",true,"/ev",{"VAR=":"glanced_mirror_early","re":true},"^Der Zug fährt in einen Tunnel. Für mehrere Sekunden gibt dir das Abteilfenster nur eine dunkle Andeutung zurück: Hut, Handschuhe, Gesicht, die schmale Linie des Kragens.","\n","^Das reicht nicht. Nicht für die Frau, die in Eibenreith aussteigen wird. Ein Spiegelbild ohne Namen ist noch keine Person; es ist nur ein Schatten, der gesellschaftlich korrekt sitzt.","\n",{"->":".^.^.^"},{"->":".^.^.g-0"},{"#f":5}],"c-5":["^ ","#","^action:thinking","/#","\n",{"->":"define_appearance"},{"->":".^.^.g-0"},{"#f":5}],"g-0":[{"->":"first_viktor_exchange"},null]}],null],"look_out_window":["ev",{"VAR?":"tut_optional_intro"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n","#","^alert[Kursiv gesetzte Optionen sind Erkundungen. Sie liefern Beobachtungen, Stimmung oder Hinweise und treiben die Szene meist nicht sofort unwiderruflich weiter.]","/#","ev",true,"/ev",{"VAR=":"tut_optional_intro","re":true},{"->":".^.^.^.5"},null]}],"nop","\n","^Draußen zerfallen die letzten Ränder der Stadt in winterbraune Felder und Dörfer, deren Kirchtürme gegen den Pfiff der Lokomotive nichts auszurichten haben. Die Schienen nehmen sich das Land, ohne um Erlaubnis zu fragen. Dämme schneiden durch Obstgärten. Telegraphenstangen gleiten in regelmäßigen Abständen vorbei, eine nach der anderen, wie Gedanken, die man zu rasch verworfen hat. ","#","^sfx[steam-whistle.ogg]","/#","\n","^Du hattest erwartet, dass sich die Eisenbahn wie ein Sieg des Jahrhunderts anfühlen würde.","\n","^Stattdessen fühlt sie sich wie ein Streit an. ","#","^image[suedbahn.png](landscape)","/#","\n","^Die Maschine wirft sich mit einer Gewalt nach Süden, die gute Gesellschaft niemals offen bewundert hätte. Die Lampen zittern in ihren Fassungen. Deine Tasse schlägt leise gegen die Untertasse. Jenseits der Scheibe beginnt das Land zu steigen, zuerst beinahe höflich, dann mit festerem Willen, bis die Bahnlinie selbst mit den Bergen zu verhandeln scheint: durch Steinbögen, schwarze Tunnel und Viadukte, die mit dem ganzen Selbstvertrauen kaiserlicher Ingenieurskunst über Schluchten gesetzt sind.","\n",{"->":"train_compartment"},null],"observe_viktor":[["^Viktor hat noch kein einziges Mal beeindruckt gewirkt.","\n","^Seine Zivilkleidung ist korrekt genug, um keinen Widerspruch hervorzurufen: dunkler Gehrock, nüchterne Weste, Handschuhe, tadelloser Kragen, dazu die Haltung eines Mannes, der selbst im Sitzen nie ganz aufhört, im Dienst zu sein. Doch kein Schneider der Monarchie kann Disziplin verbergen.","\n","ev","str","^___Untersuche___: _Viktors Haltung._","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^___Schaue___: _Viktors Blick nach._","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^___Untersuche___: _Viktors Kleidung._","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["^ ","#","^action:orientation ","/#","#","^optional ","/#","#","^key:x","/#","\n","^Sie bleibt in seinen Schultern, in der Sparsamkeit seiner Bewegungen, in der Art, wie er selbst im Sitzen nie ganz aufhört, einen Raum zu sichern.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","#","^action:orientation ","/#","#","^optional ","/#","#","^key:l","/#","\n","^Seine Augen messen Türen, Fenster, Gepäcknetz, Korridor, dein Gesicht und wieder die Tür. Nicht gierig. Nicht unhöflich. Nur vollständig.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-2":["^ ","#","^action:orientation ","/#","#","^optional ","/#","#","^key:x","/#","\n","^Auf dem Papier ist er dein Sekretär und Reisebegleiter.","\n","^In Wahrheit ist er ein Offizier, den man einer heiklen Angelegenheit beigegeben hat; aus Kanälen, die Namen haben, aber sie nicht unnötig gebrauchen. Rittmeister Viktor Alois Nowak, auch wenn auf Jagdhaus Hohenreith niemand Anlass haben soll, ihn so zu nennen.","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^Eure Gastgeber haben um ein Medium ersucht. Die Kabinettskanzlei hat dich geschickt. Das Militär hat ihn geschickt, damit aus dir kein Skandal wird, ehe du nützlich werden kannst.","\n",{"->":"train_compartment"},null]}],null],"define_class_and_name":[["ev",{"VAR?":"tut_character_intro"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n","#","^alert[Optionen mit Bestimme formen Valerie. Sie sind Teil des Charakteraufbaus: Herkunft, Glaube, Fähigkeiten, Aussehen und Auftreten beeinflussen spätere Möglichkeiten.]","/#","ev",true,"/ev",{"VAR=":"tut_character_intro","re":true},{"->":".^.^.^.5"},null]}],"nop","\n","^Das Abteil beantwortet eine Frage, noch ehe Viktor sie stellen kann.","\n","ev","str","^__Bestimme__: Das Abteil wirkt, als sei es für Menschen gebaut, die nie darüber nachdenken müssen, ob sie hineingehören.","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^__Bestimme__: Du berechnest den Preis jedes Details, noch ehe du dich daran hindern kannst.","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^__Bestimme__: Dir fällt zuerst auf, wie sauber alles ist.","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["^ ","#","^action:thinking","/#","\n","#","^class:noble","/#","ev","str","^noble","/str","/ev",{"VAR=":"birth_class","re":true},"ev",{"VAR?":"class_confidence"},2,"+",{"VAR=":"class_confidence","re":true},"/ev","ev",{"VAR?":"court_loyalty"},1,"+",{"VAR=":"court_loyalty","re":true},"/ev","^Nicht der Luxus beunruhigt dich. Luxus ist nur Holz, Stoff, Messing, Bedienung, Stille. Entscheidend ist, ob die Diener zweimal hinsehen, ob der Schaffner die Stimme senkt, ob ein anderer Reisender deine Handschuhe prüft und beschließt, nicht nach deinem Auftrag zu fragen.","\n","^Du wurdest unter Menschen geboren, die solche Dinge früher verstanden als Freundlichkeit.","\n","^Du hast früh gelernt, dass jedes Zimmer einen Hof enthält, auch wenn kein Kaiser anwesend ist. Ein Mädchen deines Ranges wird darin unterrichtet, einzutreten, sich zu verneigen, vorgestellt, platziert und wieder vergessen zu werden; nur genug zu sprechen, mehr zu verstehen, als es zugibt, und zu wissen, dass ein Familienname zugleich Schlüssel und Kette sein kann.","\n","^Deine eigene Familie besitzt keinen großen Sitz, keine Schar von Verwaltern, kein altes Recht, Provinzen zu befehlen. Doch dein Name öffnete Türen in Wiener Salons, und sobald du in diesen Zimmern warst, lerntest du, Menschen Geschichten wiederholen zu lassen, die sie nur hatten andeuten wollen.","\n","^Dein Ruf als Medium ist nicht vom Himmel gefallen. Er wurde zusammengesetzt aus Halblicht, richtigen Vermutungen, sorgsamen Pausen und der Bereitschaft besser geborener Toren, Aufführung für Offenbarung zu halten.","\n","^Bevor der Hof dich benutzen konnte, musste die Gesellschaft dich erst erfinden.","\n",{"->":"choose_name_noble"},{"#f":5}],"c-1":["^ ","#","^action:thinking","/#","\n","#","^class:middle","/#","ev","str","^middle","/str","/ev",{"VAR=":"birth_class","re":true},"ev",{"VAR?":"class_confidence"},1,"+",{"VAR=":"class_confidence","re":true},"/ev","^Die Polster, die Lampen, das polierte Furnier, die stille Bedienung an den Stationen: Nichts davon ist Zauberei. Es ist bezahlt. Verbucht. Irgendwo von jemandem aufgeschrieben, der Tinte an den Manschetten hat und eine Frau daheim, die weiß, wie lange Kerzen brennen dürfen, ehe das Haushaltsgeld Einspruch erhebt.","\n","^Du wurdest nicht für dieses Abteil geboren, aber nahe genug, um seine Regeln zu studieren.","\n","^Du wurdest in jenem breiten, unruhigen Gebiet zwischen Ehrerbietung und Ehrgeiz geboren. Deine Familie hatte Bücher, Rechnungen, Anstand, vielleicht ein Klavier, das niemand gut genug spielte, vielleicht einen Vater mit Amt, eine Mutter mit Besuchern, Brüder, von denen Aufstieg erwartet wurde, und Töchter, die nicht zeigen durften, dass auch sie danach hungerten.","\n","^Du lerntest Rechnen vor Etikette, Etikette vor Französisch und Französisch vor der Einsicht, wie leicht sich Männer von einer ruhigen Frauenstimme erklären lassen. Du stiegst auf, weil du zuhörtest. Du stiegst auf, weil du verstandest, dass Betrug, Glaube, Medizin, Tratsch, Politik und Kummer dieselben Türen in den menschlichen Geist benutzen.","\n","^Der Hof gibt ungern zu, dass er bürgerliche Tüchtigkeit braucht. Er borgt sie lieber aus, kleidet sie ordentlich ein und nennt sie Diskretion.","\n",{"->":"choose_name_middle"},{"#f":5}],"c-2":["^ ","#","^action:thinking","/#","\n","#","^class:working","/#","ev","str","^working","/str","/ev",{"VAR=":"birth_class","re":true},"ev",{"VAR?":"class_confidence"},1,"-",{"VAR=":"class_confidence","re":true},"/ev","^Der Samt sieht weich genug aus, um Fingerabdrücke zu verschlucken. Die Messingbeschläge sind von Händen poliert worden, die niemals hier sitzen werden. Das kleine Vorhangband ist dort abgenützt, wo andere Reisende, alle sicherer als du, es ohne Dankbarkeit berührt haben.","\n","^Du wurdest nicht auf dieser Seite des Dienstes geboren.","\n","^Du wurdest unter Menschen geboren, die wenig besaßen außer Verpflichtungen. Arbeit hatte einen Klang, bevor sie eine Bedeutung hatte: Wasser, Besen, Schritte, Atem, das Klirren von Geschirr, der Husten von Männern, die aus kalten Höfen kamen, Frauen, die Münzen unter dem Atem zählten. Du lerntest früh, dass die Hochgeborenen nicht aufmerksamer sind als andere. Sie müssen nur seltener aufmerksam sein.","\n","^Das war dein erster Vorteil.","\n","^Eine Dienstmagd weiß, welche Tür wichtig ist, weil sie die anderen benutzt. Eine Näherin lernt Körper, weil sie sie misst. Eine Zofe lernt Geheimnisse, weil feine Leute ihre Seelen wie Handschuhe liegen lassen, gewiss, dass niemand unter ihnen Hände hat.","\n","^Du stiegst auf durch Begabung, Protektion, Nachahmung, Nervenstärke und die furchtbare Bequemlichkeit, für harmlos gehalten zu werden. Als Wien zu flüstern begann, du sähest mehr als anständige Leute sehen, hattest du schon Jahre damit verbracht, das zu sehen, was anständige Leute übersahen.","\n",{"->":"choose_name_working"},{"#f":5}]}],null],"choose_name_noble":[["^Wien kannte dich unter dem Namen, den die Gesellschaft brauchbar gemacht hatte.","\n","ev","str","^__Bestimme__: Valerie Eleonore Josepha","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^__Bestimme__: Helene Cäcilie Franziska","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^__Bestimme__: Clara Theresia Leopoldine","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^__Bestimme__: Sophie Eleonore Auguste","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^__Bestimme__: Mathilde Josepha Henriette","/str","/ev",{"*":".^.c-4","flg":20},"ev","str","^__Bestimme__: Therese Valerie Franziska","/str","/ev",{"*":".^.c-5","flg":20},"ev","str","^__Bestimme__: Ilona Theresia Eleonore","/str","/ev",{"*":".^.c-6","flg":20},"ev","str","^__Bestimme__: Zdenka Eleonore Josepha","/str","/ev",{"*":".^.c-7","flg":20},{"c-0":["^ ","#","^action:thinking","/#","\n","ev","str","^Valerie Eleonore Josepha","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Valerie","/str","/ev",{"VAR=":"common_name","re":true},{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","#","^action:thinking","/#","\n","ev","str","^Helene Cäcilie Franziska","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Helene","/str","/ev",{"VAR=":"common_name","re":true},{"->":".^.^.g-0"},{"#f":5}],"c-2":["^ ","#","^action:thinking","/#","\n","ev","str","^Clara Theresia Leopoldine","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Clara","/str","/ev",{"VAR=":"common_name","re":true},{"->":".^.^.g-0"},{"#f":5}],"c-3":["^ ","#","^action:thinking","/#","\n","ev","str","^Sophie Eleonore Auguste","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Sophie","/str","/ev",{"VAR=":"common_name","re":true},{"->":".^.^.g-0"},{"#f":5}],"c-4":["^ ","#","^action:thinking","/#","\n","ev","str","^Mathilde Josepha Henriette","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Mathilde","/str","/ev",{"VAR=":"common_name","re":true},{"->":".^.^.g-0"},{"#f":5}],"c-5":["^ ","#","^action:thinking","/#","\n","ev","str","^Therese Valerie Franziska","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Therese","/str","/ev",{"VAR=":"common_name","re":true},{"->":".^.^.g-0"},{"#f":5}],"c-6":["^ ","#","^action:thinking","/#","\n","ev","str","^Ilona Theresia Eleonore","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Ilona","/str","/ev",{"VAR=":"common_name","re":true},{"->":".^.^.g-0"},{"#f":5}],"c-7":["^ ","#","^action:thinking","/#","\n","ev","str","^Zdenka Eleonore Josepha","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Zdenka","/str","/ev",{"VAR=":"common_name","re":true},{"->":".^.^.g-0"},{"#f":5}],"g-0":[{"->":"choose_surname_noble"},null]}],null],"choose_surname_noble":[["^Dein Titel ist durch Geburt und durch die vorsichtige Bescheidenheit deiner Familie bestimmt: keine Gräfin, keine Fürstin, keiner jener glänzenden Namen, die Botschafter und Gläubiger wie Staub anziehen.","\n","^Eine Freiin. Baronial. Brauchbar. Zugelassen, aber nicht thronend.","\n","ev","str","^__Bestimme__: Freiin von Rauhenfels","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^__Bestimme__: Freiin von Traunegg","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^__Bestimme__: Freiin von Ebenwald","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^__Bestimme__: Freiin von Arnsberg","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^__Bestimme__: Freiin von Reichenau","/str","/ev",{"*":".^.c-4","flg":20},"ev","str","^__Bestimme__: Freiin von Waldstätten","/str","/ev",{"*":".^.c-5","flg":20},{"c-0":["^ ","#","^action:thinking","/#","\n","ev","str","^Freiin von","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Rauhenfels","/str","/ev",{"VAR=":"surname","re":true},{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","#","^action:thinking","/#","\n","ev","str","^Freiin von","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Traunegg","/str","/ev",{"VAR=":"surname","re":true},{"->":".^.^.g-0"},{"#f":5}],"c-2":["^ ","#","^action:thinking","/#","\n","ev","str","^Freiin von","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Ebenwald","/str","/ev",{"VAR=":"surname","re":true},{"->":".^.^.g-0"},{"#f":5}],"c-3":["^ ","#","^action:thinking","/#","\n","ev","str","^Freiin von","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Arnsberg","/str","/ev",{"VAR=":"surname","re":true},{"->":".^.^.g-0"},{"#f":5}],"c-4":["^ ","#","^action:thinking","/#","\n","ev","str","^Freiin von","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Reichenau","/str","/ev",{"VAR=":"surname","re":true},{"->":".^.^.g-0"},{"#f":5}],"c-5":["^ ","#","^action:thinking","/#","\n","ev","str","^Freiin von","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Waldstätten","/str","/ev",{"VAR=":"surname","re":true},{"->":".^.^.g-0"},{"#f":5}],"g-0":[{"->":"assemble_full_name"},null]}],null],"choose_name_middle":[["^Die Salons, die zuerst über dich lachten und dich dann wieder einluden, lernten deinen Namen, bevor sie lernten, was er gekostet hatte.","\n","ev","str","^__Bestimme__: Clara Eleonore","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^__Bestimme__: Anna Katharina","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^__Bestimme__: Helene Theresia","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^__Bestimme__: Rosa Franziska","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^__Bestimme__: Johanna Elise","/str","/ev",{"*":".^.c-4","flg":20},"ev","str","^__Bestimme__: Katharina Sophie","/str","/ev",{"*":".^.c-5","flg":20},"ev","str","^__Bestimme__: Therese Leopoldine","/str","/ev",{"*":".^.c-6","flg":20},"ev","str","^__Bestimme__: Magdalena Cäcilie","/str","/ev",{"*":".^.c-7","flg":20},{"c-0":["^ ","#","^action:thinking","/#","\n","ev","str","^Clara Eleonore","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Clara","/str","/ev",{"VAR=":"common_name","re":true},{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","#","^action:thinking","/#","\n","ev","str","^Anna Katharina","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Anna","/str","/ev",{"VAR=":"common_name","re":true},{"->":".^.^.g-0"},{"#f":5}],"c-2":["^ ","#","^action:thinking","/#","\n","ev","str","^Helene Theresia","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Helene","/str","/ev",{"VAR=":"common_name","re":true},{"->":".^.^.g-0"},{"#f":5}],"c-3":["^ ","#","^action:thinking","/#","\n","ev","str","^Rosa Franziska","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Rosa","/str","/ev",{"VAR=":"common_name","re":true},{"->":".^.^.g-0"},{"#f":5}],"c-4":["^ ","#","^action:thinking","/#","\n","ev","str","^Johanna Elise","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Johanna","/str","/ev",{"VAR=":"common_name","re":true},{"->":".^.^.g-0"},{"#f":5}],"c-5":["^ ","#","^action:thinking","/#","\n","ev","str","^Katharina Sophie","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Katharina","/str","/ev",{"VAR=":"common_name","re":true},{"->":".^.^.g-0"},{"#f":5}],"c-6":["^ ","#","^action:thinking","/#","\n","ev","str","^Therese Leopoldine","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Therese","/str","/ev",{"VAR=":"common_name","re":true},{"->":".^.^.g-0"},{"#f":5}],"c-7":["^ ","#","^action:thinking","/#","\n","ev","str","^Magdalena Cäcilie","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Magdalena","/str","/ev",{"VAR=":"common_name","re":true},{"->":".^.^.g-0"},{"#f":5}],"g-0":[{"->":"choose_surname_middle"},null]}],null],"choose_surname_middle":[["^Dein Familienname enthält kein Partikel, das den Aufstieg abfedert. Er muss allein aufrecht stehen.","\n","ev","str","^__Bestimme__: Leitner","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^__Bestimme__: Wagner","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^__Bestimme__: Kellner","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^__Bestimme__: Baumgartner","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^__Bestimme__: Fischer","/str","/ev",{"*":".^.c-4","flg":20},"ev","str","^__Bestimme__: Schmid","/str","/ev",{"*":".^.c-5","flg":20},"ev","str","^__Bestimme__: Pichler","/str","/ev",{"*":".^.c-6","flg":20},"ev","str","^__Bestimme__: Rosenfeld","/str","/ev",{"*":".^.c-7","flg":20},{"c-0":["^ ","#","^action:thinking","/#","\n","ev","str","^Fräulein","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Leitner","/str","/ev",{"VAR=":"surname","re":true},{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","#","^action:thinking","/#","\n","ev","str","^Fräulein","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Wagner","/str","/ev",{"VAR=":"surname","re":true},{"->":".^.^.g-0"},{"#f":5}],"c-2":["^ ","#","^action:thinking","/#","\n","ev","str","^Fräulein","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Kellner","/str","/ev",{"VAR=":"surname","re":true},{"->":".^.^.g-0"},{"#f":5}],"c-3":["^ ","#","^action:thinking","/#","\n","ev","str","^Fräulein","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Baumgartner","/str","/ev",{"VAR=":"surname","re":true},{"->":".^.^.g-0"},{"#f":5}],"c-4":["^ ","#","^action:thinking","/#","\n","ev","str","^Fräulein","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Fischer","/str","/ev",{"VAR=":"surname","re":true},{"->":".^.^.g-0"},{"#f":5}],"c-5":["^ ","#","^action:thinking","/#","\n","ev","str","^Fräulein","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Schmid","/str","/ev",{"VAR=":"surname","re":true},{"->":".^.^.g-0"},{"#f":5}],"c-6":["^ ","#","^action:thinking","/#","\n","ev","str","^Fräulein","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Pichler","/str","/ev",{"VAR=":"surname","re":true},{"->":".^.^.g-0"},{"#f":5}],"c-7":["^ ","#","^action:thinking","/#","\n","ev","str","^Fräulein","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Rosenfeld","/str","/ev",{"VAR=":"surname","re":true},{"->":".^.^.g-0"},{"#f":5}],"g-0":[{"->":"assemble_full_name"},null]}],null],"choose_name_working":[["^Der Name, den du nach oben trugst, wurde vielleicht in der Aussprache verändert, aber nie ganz von seinem Ursprung gereinigt.","\n","ev","str","^__Bestimme__: Anna","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^__Bestimme__: Klara","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^__Bestimme__: Agnes","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^__Bestimme__: Leni","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^__Bestimme__: Rosa","/str","/ev",{"*":".^.c-4","flg":20},"ev","str","^__Bestimme__: Gertrud","/str","/ev",{"*":".^.c-5","flg":20},"ev","str","^__Bestimme__: Elisabeth","/str","/ev",{"*":".^.c-6","flg":20},"ev","str","^__Bestimme__: Franziska","/str","/ev",{"*":".^.c-7","flg":20},{"c-0":["^ ","#","^action:thinking","/#","\n","ev","str","^Anna","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Anna","/str","/ev",{"VAR=":"common_name","re":true},{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","#","^action:thinking","/#","\n","ev","str","^Klara","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Klara","/str","/ev",{"VAR=":"common_name","re":true},{"->":".^.^.g-0"},{"#f":5}],"c-2":["^ ","#","^action:thinking","/#","\n","ev","str","^Agnes","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Agnes","/str","/ev",{"VAR=":"common_name","re":true},{"->":".^.^.g-0"},{"#f":5}],"c-3":["^ ","#","^action:thinking","/#","\n","ev","str","^Leni","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Leni","/str","/ev",{"VAR=":"common_name","re":true},{"->":".^.^.g-0"},{"#f":5}],"c-4":["^ ","#","^action:thinking","/#","\n","ev","str","^Rosa","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Rosa","/str","/ev",{"VAR=":"common_name","re":true},{"->":".^.^.g-0"},{"#f":5}],"c-5":["^ ","#","^action:thinking","/#","\n","ev","str","^Gertrud","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Gertrud","/str","/ev",{"VAR=":"common_name","re":true},{"->":".^.^.g-0"},{"#f":5}],"c-6":["^ ","#","^action:thinking","/#","\n","ev","str","^Elisabeth","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Elisabeth","/str","/ev",{"VAR=":"common_name","re":true},{"->":".^.^.g-0"},{"#f":5}],"c-7":["^ ","#","^action:thinking","/#","\n","ev","str","^Franziska","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Franziska","/str","/ev",{"VAR=":"common_name","re":true},{"->":".^.^.g-0"},{"#f":5}],"g-0":[{"->":"choose_surname_working"},null]}],null],"choose_surname_working":[["^Ein einfacher Name kann in Wien eine Last sein. Er sagt den Leuten, wie wenig Achtung sie vorgeben müssen, ehe du gesprochen hast.","\n","ev","str","^__Bestimme__: Pichler","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^__Bestimme__: Huber","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^__Bestimme__: Maier","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^__Bestimme__: Gruber","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^__Bestimme__: Schuster","/str","/ev",{"*":".^.c-4","flg":20},"ev","str","^__Bestimme__: Krenn","/str","/ev",{"*":".^.c-5","flg":20},"ev","str","^__Bestimme__: Wolf","/str","/ev",{"*":".^.c-6","flg":20},"ev","str","^__Bestimme__: Moser","/str","/ev",{"*":".^.c-7","flg":20},{"c-0":["^ ","#","^action:thinking","/#","\n","ev","str","^Fräulein","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Pichler","/str","/ev",{"VAR=":"surname","re":true},{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","#","^action:thinking","/#","\n","ev","str","^Fräulein","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Huber","/str","/ev",{"VAR=":"surname","re":true},{"->":".^.^.g-0"},{"#f":5}],"c-2":["^ ","#","^action:thinking","/#","\n","ev","str","^Fräulein","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Maier","/str","/ev",{"VAR=":"surname","re":true},{"->":".^.^.g-0"},{"#f":5}],"c-3":["^ ","#","^action:thinking","/#","\n","ev","str","^Fräulein","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Gruber","/str","/ev",{"VAR=":"surname","re":true},{"->":".^.^.g-0"},{"#f":5}],"c-4":["^ ","#","^action:thinking","/#","\n","ev","str","^Fräulein","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Schuster","/str","/ev",{"VAR=":"surname","re":true},{"->":".^.^.g-0"},{"#f":5}],"c-5":["^ ","#","^action:thinking","/#","\n","ev","str","^Fräulein","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Krenn","/str","/ev",{"VAR=":"surname","re":true},{"->":".^.^.g-0"},{"#f":5}],"c-6":["^ ","#","^action:thinking","/#","\n","ev","str","^Fräulein","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Wolf","/str","/ev",{"VAR=":"surname","re":true},{"->":".^.^.g-0"},{"#f":5}],"c-7":["^ ","#","^action:thinking","/#","\n","ev","str","^Fräulein","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Moser","/str","/ev",{"VAR=":"surname","re":true},{"->":".^.^.g-0"},{"#f":5}],"g-0":[{"->":"assemble_full_name"},null]}],null],"assemble_full_name":["ev",{"VAR?":"birth_class"},"str","^noble","/str","==","/ev",[{"->":".^.b","c":true},{"b":["\n","^Auf Visitenkarten, in Briefen, in den vorsichtigen Mündern der Dienerschaft bist du ","ev",{"VAR?":"given_names"},"out","/ev","^ ","ev",{"VAR?":"title_part"},"out","/ev","^ ","ev",{"VAR?":"surname"},"out","/ev","^.","\n",{"->":".^.^.^.9"},null]}],[{"->":".^.b"},{"b":["\n","^In Bahndokumenten, Hotelbüchern und auf den Zungen von Menschen, die noch nicht entschieden haben, wie viel Achtung du verdienst, bist du ","ev",{"VAR?":"title_part"},"out","/ev","^ ","ev",{"VAR?":"given_names"},"out","/ev","^ ","ev",{"VAR?":"surname"},"out","/ev","^.","\n",{"->":".^.^.^.9"},null]}],"nop","\n","^Aber in der privaten Kammer, in der ein Name zuerst beantwortet wird, ehe er gespielt werden muss, bist du ","ev",{"VAR?":"common_name"},"out","/ev","^.","\n",{"->":"train_compartment"},null],"define_religion_and_supernatural":[["^Du berührst das Ridikül, ohne es sofort zu öffnen.","\n","^Das Schreiben darin nennt dich nicht Ermittlerin. Es nennt dich, in einer Prosa trocken genug, um durch beliebig viele Ämter zu gelangen, eine Frau, deren ungewöhnlicher spiritistischer Ruf sie für eine heikle Haushaltsangelegenheit empfehle. Die Formulierung ist erlesen. Sie bejaht nicht und verneint nicht. Sie erlaubt allen Beteiligten, später zu glauben, sie hätten nichts Ungehöriges geglaubt.","\n","^Die gräfliche Familie auf Jagdhaus Hohenreith hat um Diskretion ersucht. Wien hat mit einem versiegelten Schreiben geantwortet, mit einer Frau, der man nachsagt, sie spreche mit dem Verborgenen, und mit einem Mann ihr gegenüber, der eigene Befehle hat.","\n","^Das Schreiben nennt keine Kirche. Gerade das macht die Kirche anwesend.","\n","ev","str","^__Bestimme__: Der Glaube ist dir wirklich heilig.","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^__Bestimme__: Du bist katholisch, wie man in Wien katholisch ist.","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^__Bestimme__: Die Kirche ist für dich eine Behörde mit Weihrauch.","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^__Bestimme__: Wenn Seelen fortbestehen, hat Rom nicht das Monopol auf ihre Stimmen.","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^__Bestimme__: Der Glaube hat dich geformt, bevor du alt genug warst, dich gegen ihn zu wehren.","/str","/ev",{"*":".^.c-4","flg":20},{"c-0":["^ ","#","^action:thinking","/#","\n","ev","str","^devout_catholic","/str","/ev",{"VAR=":"religion_stance","re":true},"^Gott ist kein Gesprächsgegenstand für Abteile. Er ist kein Talent, kein Ruf, keine gesellschaftliche Bequemlichkeit. Du glaubst nicht kindlich, aber tief: an Sünde, Gnade, Sakrament, Versuchung und an die gefährliche Nähe der unsichtbaren Welt.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","#","^action:thinking","/#","\n","ev","str","^social_catholic","/str","/ev",{"VAR=":"religion_stance","re":true},"^Du kennst die Feste, die Gebete, das Gewicht der Beichte und die Macht eines Pfarrers über Menschen, die behaupten, ihn nicht zu fürchten. Dein Glaube ist nicht leer; aber er ist ebenso Gewohnheit wie Überzeugung, ebenso Ordnung wie Trost.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-2":["^ ","#","^action:thinking","/#","\n","ev","str","^josephinian_sceptic","/str","/ev",{"VAR=":"religion_stance","re":true},"^Du achtest Register, Schulen, Spitäler, Archive und jene Nützlichkeit, die Institutionen gelegentlich gegen ihren eigenen Dünkel entwickeln. Aber Priester erklären zu oft, was sie zuerst besitzen möchten: Schuld, Frauen, Armut und Angst.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-3":["^ ","#","^action:thinking","/#","\n","ev","str","^spiritist_syncretic","/str","/ev",{"VAR=":"religion_stance","re":true},"^Heiligenbilder, Totenmessen, Séancen, Ahnungen, Tischklopfen, Träume: Die sauberen Grenzen dazwischen scheinen dir eher von Männern gezogen als von der Ewigkeit selbst. Was überlebt, spricht vielleicht in Formen, die keine Kanzlei genehmigt hat.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-4":["^ ","#","^action:thinking","/#","\n","ev","str","^wounded_catholic","/str","/ev",{"VAR=":"religion_stance","re":true},"^Du kennst die Gebete zu gut, um sie einfach abzulegen, und die Schuld zu gut, um sie fromm zu nennen. Katholische Bilder erreichen dich nicht als Dekoration. Sie greifen nach Stellen, die du lieber versiegelt hieltest.","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^Vor dieser Reise, vor diesem Zug, bevor die Berge beginnen, Stück für Stück den Himmel zu nehmen, hat der Glaube bereits seine Stellung in dir bezogen. Nun bleibt die andere Frage: was du aus dem machst, was die Leute über deinen Ruf sagen.","\n","ev","str","^__Bestimme__: Die Toten schweigen nicht. Die Lebenden hören nur schlecht zu.","/str","/ev",{"*":".^.c-5","flg":20},"ev","str","^__Bestimme__: Das Übernatürliche ist meistens Schmerz, Betrug, Fieber, Erbschaft oder schlechte Lüftung.","/str","/ev",{"*":".^.c-6","flg":20},"ev","str","^__Täusche__: Glaube ist ein Kostüm, das Männer dir selbst reichen.","/str","/ev",{"*":".^.c-7","flg":20},"ev","str","^__Bestimme__: Du hast gelernt, nicht zu früh zu entscheiden.","/str","/ev",{"*":".^.c-8","flg":20},{"c-5":["^ ","#","^action:thinking","/#","\n","#","^supernatural:believer","/#","ev","str","^believer","/str","/ev",{"VAR=":"supernatural_belief","re":true},"ev",{"VAR?":"medium_reputation"},1,"+",{"VAR=":"medium_reputation","re":true},"/ev","ev",{"VAR?":"supernatural_exposure"},1,"+",{"VAR=":"supernatural_exposure","re":true},"/ev","^Du hast den Unglauben der Gebildeten immer für eine provinzielle Anmaßung gehalten. Es gibt Druck in Zimmern, in denen Kummer gewesen ist. Es gibt Worte, die Menschen sprechen, bevor sie wissen, dass sie gesprochen haben. Es gibt Träume, die mit Schlamm am Saum eintreffen.","\n","^Vielleicht ist die Welt nicht heimgesucht. Vielleicht ist sie nur überfüllt.","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-6":["^ ","#","^action:thinking","/#","\n","#","^supernatural:sceptic","/#","#","^route:detective","/#","ev","str","^sceptic","/str","/ev",{"VAR=":"supernatural_belief","re":true},"ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","^Das Wort Geist verdeckt zu viel und erklärt zu wenig. Du hast anständige Menschen ein Echo eine Botschaft nennen hören, einen Zufall ein Zeichen, eine zitternde Hand eine himmlische Berührung. Männer der Wissenschaft können Toren sein, doch Toren mit Kerzen und Tischchen sind keine Verbesserung.","\n","^Falls Hohenreith Gespenster hat, erwartest du von ihnen Buchhaltung, Briefe, Fußspuren und einen Nutznießer.","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-7":["^ ","#","^action:social","/#","\n","#","^supernatural:performer","/#","ev","str","^performer","/str","/ev",{"VAR=":"supernatural_belief","re":true},"ev",{"VAR?":"medium_reputation"},2,"+",{"VAR=":"medium_reputation","re":true},"/ev","^Du hast früh entdeckt, dass Männer, die dem Verstand einer Frau misstrauen, manchmal ihre Nerven verehren. Eine Schlussfolgerung aus Beweisen reizt sie. Eine Vision, mit gesenkten Wimpern gehaucht, lässt sie näher rücken.","\n","^Nun gut. Sollen sie näher rücken.","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-8":["^ ","#","^action:thinking","/#","\n","#","^supernatural:undecided","/#","ev","str","^undecided","/str","/ev",{"VAR=":"supernatural_belief","re":true},"^Es gibt Dinge, die du erklären kannst, Dinge, die du noch nicht erklären kannst, und Dinge, denen Erklärung schadet, ehe sie hilft. Du hast einen Beruf daraus gemacht, an Schwellen zu stehen, mit einem Gesicht, das gefasst genug ist, damit beide Seiten weitersprechen.","\n","^Hohenreith wird dir zeigen müssen, welche Art von Fall es ist.","\n",{"->":".^.^.^.g-1"},{"#f":5}]}],"g-1":["^Glaube ist eine Sache. Erfahrung eine andere.","\n","^Man nennt eine Frau empfindsam, wenn ihre Wahrnehmungen wie eine Krankheit klingen sollen. Man nennt sie hysterisch, wenn diese Wahrnehmungen unbequem werden. Man nennt sie inspiriert, wenn man sie braucht, und labil, wenn man sie nicht mehr braucht.","\n","^Unter Ruf und Aufführung hat die Erinnerung ihre eigene Aussage.","\n","ev","str","^__Bestimme__: Es gab Augenblicke, die du nicht wegerklären kannst.","/str","/ev",{"*":".^.c-9","flg":20},"ev","str","^__Bestimme__: Alles, was du tust, lässt sich durch Beobachtung, Zeitpunkt und Nervenstärke erklären.","/str","/ev",{"*":".^.c-10","flg":20},"ev","str","^__Bestimme__: Etwas geschieht, aber niemals, wenn man es ruft.","/str","/ev",{"*":".^.c-11","flg":20},"ev","str","^__Bestimme__: Du hast die ersten Zeichen begraben.","/str","/ev",{"*":".^.c-12","flg":20},{"c-9":["^ ","#","^action:thinking","/#","\n","#","^powers:genuine","/#","ev","str","^genuine","/str","/ev",{"VAR=":"supernatural_senses","re":true},"ev",{"VAR?":"supernatural_exposure"},2,"+",{"VAR=":"supernatural_exposure","re":true},"/ev","^Einmal, als Kind, wusstest du es, bevor das Telegramm kam. Einmal, in einem überfüllten Zimmer, trat der Kummer einer Fremden mit solcher Gewalt in dich ein, dass deine eigenen Knie nachgaben. Einmal sahst du in einem Spiegel eine Tür hinter dir, die nicht im Raum war, als du dich umdrehtest.","\n","^Danach lerntest du Vorsicht. Es ist unklug für eine Frau, Dinge zu wissen, bevor ein Mann ihre Meinung erbeten hat.","\n",{"->":".^.^.^.g-2"},{"#f":5}],"c-10":["^ ","#","^action:thinking","/#","\n","#","^powers:faked","/#","#","^route:detective","/#","ev","str","^faked","/str","/ev",{"VAR=":"supernatural_senses","re":true},"ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","^Du bemerkst Ringe, die zu frisch abgenommen wurden, Trauerhandschuhe, die zu sorgfältig getragen werden, Briefe, so oft gefaltet und wieder gefaltet, bis der Knick die Besessenheit des Lesers verrät. Du hörst, wenn Diener Gäste falsch benennen, wenn Mütter vor den Zimmern ihrer Töchter innehalten, wenn Offiziere lügen, indem sie zu genau werden.","\n","^Die Toten haben dir nie etwas gesagt. Die Lebenden können nicht aufhören, dir alles zu verraten.","\n",{"->":".^.^.^.g-2"},{"#f":5}],"c-11":["^ ","#","^action:thinking","/#","\n","#","^powers:ambiguous","/#","ev","str","^ambiguous","/str","/ev",{"VAR=":"supernatural_senses","re":true},"ev",{"VAR?":"supernatural_exposure"},1,"+",{"VAR=":"supernatural_exposure","re":true},"/ev","^Dein Ruf beruht auf Beherrschung. Die Wahrheit, falls es Wahrheit ist, hat keinen Respekt vor Terminen.","\n","^Manchmal verändert ein Zimmer den Druck um dich. Manchmal bekommt ein Gesicht einen alten Ausdruck, den kein Lebender ihm beigebracht hat. Manchmal kommen Namen vor den Vorstellungen. Aber je fester du danach greifst, desto gewöhnlicher wird die Welt.","\n",{"->":".^.^.^.g-2"},{"#f":5}],"c-12":["^ ","#","^action:thinking","/#","\n","#","^powers:repressed","/#","#","^route:eccentric","/#","ev","str","^repressed","/str","/ev",{"VAR=":"supernatural_senses","re":true},"ev",{"VAR?":"eccentric"},1,"+",{"VAR=":"eccentric","re":true},"/ev","^Es gibt Kindheitserinnerungen, die hinter Höflichkeit versiegelt sind: ein Kinderzimmerspiegel, zur Wand gedreht; eine Amme, ohne Zeugnis entlassen; die Hand deiner Mutter um dein Handgelenk, so fest, dass die Knochen sich beschwerten.","\n","^Danach wurdest du auf Arten sonderbar, die die Gesellschaft leichter bewundern als verstehen konnte.","\n",{"->":".^.^.^.g-2"},{"#f":5}]}],"g-2":[{"->":"train_compartment"},null]}],null],"define_appearance":[["^Der Zug fährt in einen Tunnel. Für mehrere Sekunden gibt dir das Abteilfenster nur dein eigenes Spiegelbild zurück.","\n","^Jetzt, nachdem dein Name im Raum steht, kann das Glas mehr zeigen als eine Dame im richtigen Abteil. Es zeigt die Frau, die in Eibenreith aussteigen wird.","\n","ev","str","^__Bestimme__: Klein, schmal und beweglich.","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^__Bestimme__: Mittelgroß, weich gebaut, mit ruhiger körperlicher Gegenwart.","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^__Bestimme__: Hochgewachsen und schlank.","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^__Bestimme__: Kompakt und kräftiger, als die Kleidung zugibt.","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^__Bestimme__: Zierlich, mit einem Anschein von Empfindlichkeit.","/str","/ev",{"*":".^.c-4","flg":20},{"c-0":["^ ","#","^action:thinking","/#","\n","ev","str","^small_slender","/str","/ev",{"VAR=":"body_detail","re":true},"^Unter Mantel, Rock und Sitzhaltung bleibt etwas Knappes, Schnelles an dir: schmale Schultern, feine Handgelenke, ein Körper, der in Türen verschwindet und in Gesprächen unterschätzt wird.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","#","^action:thinking","/#","\n","ev","str","^medium_soft","/str","/ev",{"VAR=":"body_detail","re":true},"^Du wirkst nicht zerbrechlich. Die Taille ist geformt, nicht mädchenhaft; die Schultern sind ruhig, die Hände gepflegt, die Anwesenheit fester, als manche Männer an einer jungen Frau bequem finden.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-2":["^ ","#","^action:thinking","/#","\n","ev","str","^tall_slender","/str","/ev",{"VAR=":"body_detail","re":true},"^Die Sitzbank macht dich weniger lang, als du bist. Aufrecht stehend würdest du mehr Raum einnehmen, als deine Rolle verspricht: lange Linien, schmale Hände, eine Haltung, die Disziplin beinahe wie Herkunft aussehen lässt.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-3":["^ ","#","^action:thinking","/#","\n","ev","str","^compact_strong","/str","/ev",{"VAR=":"body_detail","re":true},"^Reisekleidung und Korsett ordnen dich, aber sie verleugnen nicht alles. In deinen Unterarmen, im Nacken, in der Art, wie du ein Gleichgewicht hältst, liegt mehr Kraft, als man einer Dame höflich zutraut.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-4":["^ ","#","^action:thinking","/#","\n","ev","str","^delicate","/str","/ev",{"VAR=":"body_detail","re":true},"^Du wirkst zart genug, dass Ärzte, Tanten und taktlose Herren zu rasch glauben, deinen Zustand deuten zu dürfen. Dass Zartheit nicht dasselbe ist wie Schwäche, bleibt eine nützliche Verwechslung.","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^Die dunkle Scheibe hält nun Haar und Hut fest.","\n","ev","str","^__Bestimme__: Dunkelbraunes Haar, fast schwarz im schwachen Licht.","/str","/ev",{"*":".^.c-5","flg":20},"ev","str","^__Bestimme__: Kastanienbraunes Haar mit warmem Glanz.","/str","/ev",{"*":".^.c-6","flg":20},"ev","str","^__Bestimme__: Dunkelblondes bis aschbraunes Haar.","/str","/ev",{"*":".^.c-7","flg":20},"ev","str","^__Bestimme__: Hellbraunes Haar mit goldenen Strähnen.","/str","/ev",{"*":".^.c-8","flg":20},"ev","str","^__Bestimme__: Rotbraunes Haar, sorgfältig gebändigt.","/str","/ev",{"*":".^.c-9","flg":20},{"c-5":["^ ","#","^action:thinking","/#","\n","ev","str","^dark_brown","/str","/ev",{"VAR=":"hair_colour","re":true},"^Dunkelbraunes Haar rahmt Stirn und Schläfen, im Tunnel beinahe schwarz, im Lampenlicht mit einem wärmeren Schimmer.","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-6":["^ ","#","^action:thinking","/#","\n","ev","str","^chestnut","/str","/ev",{"VAR=":"hair_colour","re":true},"^Kastanienbraunes Haar fängt jedes bisschen Licht, das die Lampe hergibt, und macht dein Gesicht weicher, als dein Blick es erlaubt.","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-7":["^ ","#","^action:thinking","/#","\n","ev","str","^dark_blond_ash","/str","/ev",{"VAR=":"hair_colour","re":true},"^Dunkelblondes, aschbraun wirkendes Haar gibt dir etwas Zurückhaltendes und Nordisches, besonders unter dem dunklen Hut.","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-8":["^ ","#","^action:thinking","/#","\n","ev","str","^light_brown_gold","/str","/ev",{"VAR=":"hair_colour","re":true},"^Hellbraunes Haar mit goldenen Strähnen wirkt im Abteil beinahe zu warm für diese Reise, als habe Wien einen letzten Rest Nachmittag darin vergessen.","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-9":["^ ","#","^action:thinking","/#","\n","ev","str","^auburn","/str","/ev",{"VAR=":"hair_colour","re":true},"^Rotbraunes Haar ist nie ganz unauffällig, auch wenn Nadeln, Hut und Sitte es zur Ordnung zwingen.","\n",{"->":".^.^.^.g-1"},{"#f":5}]}],"g-1":["^Die Frisur ist nicht bloß Geschmack. Eine Frau trägt auch ihre Beherrschung auf dem Kopf.","\n","ev","str","^__Bestimme__: Ein glatter Mittelscheitel und ein tiefer Knoten im Nacken.","/str","/ev",{"*":".^.c-10","flg":20},"ev","str","^__Bestimme__: Weiche Stirnwellen und ein sorgfältiger Chignon.","/str","/ev",{"*":".^.c-11","flg":20},"ev","str","^__Bestimme__: Geflochtene Partien, im Nacken zu einem schweren Knoten gefasst.","/str","/ev",{"*":".^.c-12","flg":20},"ev","str","^__Bestimme__: Etwas Höhe über der Stirn, modischer und selbstbewusster.","/str","/ev",{"*":".^.c-13","flg":20},{"c-10":["^ ","#","^action:thinking","/#","\n","ev","str","^centre_part_low_bun","/str","/ev",{"VAR=":"hairstyle","re":true},"^Der Mittelscheitel ist sauber, der Knoten tief und fest. Keine Locke bittet um Nachsicht. Es ist eine Frisur für Frauen, die lieber richtig als reizend erscheinen.","\n",{"->":".^.^.^.g-2"},{"#f":5}],"c-11":["^ ","#","^action:thinking","/#","\n","ev","str","^soft_waves_chignon","/str","/ev",{"VAR=":"hairstyle","re":true},"^Die Stirnwellen sind weich gelegt, der Chignon sitzt sauber unter dem Hut. Das wirkt weiblicher, zugänglicher, aber nicht weniger berechnet.","\n",{"->":".^.^.^.g-2"},{"#f":5}],"c-12":["^ ","#","^action:thinking","/#","\n","ev","str","^braided_low_knot","/str","/ev",{"VAR=":"hairstyle","re":true},"^Geflochtene Partien führen das Haar zur Ordnung zurück. Der Knoten im Nacken ist schwerer, ländlicher vielleicht, aber unter städtischer Hand gezähmt.","\n",{"->":".^.^.^.g-2"},{"#f":5}],"c-13":["^ ","#","^action:thinking","/#","\n","ev","str","^raised_front_modern","/str","/ev",{"VAR=":"hairstyle","re":true},"^Das Haar hebt sich über der Stirn mit jener neuen Fülle, die ältere Damen für Eitelkeit und jüngere für Freiheit halten. Unter dem Reisehut bleibt es gerade noch anständig.","\n",{"->":".^.^.^.g-2"},{"#f":5}]}],"g-2":["^Das Gesicht unter dem Hut ist deutlich genug, um nicht mehr ausweichen zu können.","\n","ev","str","^__Bestimme__: Helle Haut, kühle Augen, scharfe Brauen.","/str","/ev",{"*":".^.c-14","flg":20},"ev","str","^__Bestimme__: Rosiger Teint, weicher Mund, wacher Blick.","/str","/ev",{"*":".^.c-15","flg":20},"ev","str","^__Bestimme__: Warmer Teint, dunklere Augen, ruhige Miene.","/str","/ev",{"*":".^.c-16","flg":20},"ev","str","^__Bestimme__: Blasse Haut, feine Züge, ein fast zu kontrolliertes Gesicht.","/str","/ev",{"*":".^.c-17","flg":20},"ev","str","^__Bestimme__: Ein markanteres Gesicht mit gerader Nase und festem Kinn.","/str","/ev",{"*":".^.c-18","flg":20},{"c-14":["^ ","#","^action:thinking","/#","\n","ev","str","^fair_cool","/str","/ev",{"VAR=":"complexion_detail","re":true},"ev","str","^cool_sharp","/str","/ev",{"VAR=":"face_detail","re":true},"^Dein Teint ist hell und kühl, die Brauen geben dem Gesicht mehr Bestimmtheit, als ein flüchtiger Blick erwartet. Man könnte dich empfindlich nennen, bis man die Augen genauer ansieht.","\n",{"->":".^.^.^.g-3"},{"#f":5}],"c-15":["^ ","#","^action:thinking","/#","\n","ev","str","^rosy","/str","/ev",{"VAR=":"complexion_detail","re":true},"ev","str","^soft_alert","/str","/ev",{"VAR=":"face_detail","re":true},"^Du hast mehr Farbe im Gesicht, als die meisten Damen im Winter zeigen möchten. Der Mund wirkt weicher als der Blick; zusammen macht dich das zugänglicher, aber nicht einfacher.","\n",{"->":".^.^.^.g-3"},{"#f":5}],"c-16":["^ ","#","^action:thinking","/#","\n","ev","str","^warm","/str","/ev",{"VAR=":"complexion_detail","re":true},"ev","str","^calm_dark_eyes","/str","/ev",{"VAR=":"face_detail","re":true},"^Der Teint ist wärmer, die Augen dunkler, die Miene ruhiger. Du wirkst weniger ätherisch als gegenwärtig: eine Frau aus Fleisch, Gedächtnis und Absicht.","\n",{"->":".^.^.^.g-3"},{"#f":5}],"c-17":["^ ","#","^action:thinking","/#","\n","ev","str","^pale_fine","/str","/ev",{"VAR=":"complexion_detail","re":true},"ev","str","^fine_controlled","/str","/ev",{"VAR=":"face_detail","re":true},"^Die Züge sind fein, fast zerbrechlich, doch die Kontrolle darin ist zu sichtbar, um harmlos zu wirken. Wer dich für schwach hält, möchte es zu gerne.","\n",{"->":".^.^.^.g-3"},{"#f":5}],"c-18":["^ ","#","^action:thinking","/#","\n","ev","str","^clear","/str","/ev",{"VAR=":"complexion_detail","re":true},"ev","str","^marked_jaw","/str","/ev",{"VAR=":"face_detail","re":true},"^Die Nase ist gerade, das Kinn fester, die Linien weniger gefällig als ein Porträtmaler sie gern hätte. Das Gesicht verrät eher Willen als Sanftmut.","\n",{"->":".^.^.^.g-3"},{"#f":5}]}],"g-3":["^Der Rest der Spiegelung ist Kostüm, Rüstung und Beweismittel.","\n","ev",{"VAR?":"birth_class"},"str","^noble","/str","==","/ev",[{"->":".^.b","c":true},{"b":["\n","^Die Kleidung muss genug Rang zeigen, um glaubwürdig zu sein, und genug Zurückhaltung, um nicht nach Provinztheater zu riechen.","\n",{"->":".^.^.^.11"},null]}],[{"->":".^.b"},{"b":["\n","ev",{"VAR?":"birth_class"},"str","^middle","/str","==","/ev",[{"->":".^.b","c":true},{"b":["\n","^Die Kleidung muss eine höhere Welt betreten können, ohne zu schreien, dass sie dafür gearbeitet hat.","\n",{"->":".^.^.^.10"},null]}],[{"->":".^.b"},{"b":["\n","^Die Kleidung muss beweisen, dass man dich in die erste Klasse setzen konnte, ohne dass der Stoff gegen dich aussagt.","\n",{"->":".^.^.^.10"},null]}],"nop","\n",{"->":".^.^.^.11"},null]}],"nop","\n","ev","str","^__Bestimme__: Ein dunkel anthrazitfarbenes Reisekostüm mit pflaumenfarbenem Samtkragen.","/str","/ev",{"*":".^.c-19","flg":20},"ev","str","^__Bestimme__: Ein schwarzbraunes Wollkostüm mit elfenbeinfarbener Bluse und schmaler Spitze.","/str","/ev",{"*":".^.c-20","flg":20},"ev","str","^__Bestimme__: Ein graublaues Reisekostüm mit kurzem Mantel und praktischen Knöpfen.","/str","/ev",{"*":".^.c-21","flg":20},"ev","str","^__Bestimme__: Ein dunkles grünes Kostüm mit schwarzem Besatz und passendem Hut.","/str","/ev",{"*":".^.c-22","flg":20},"ev","str","^__Bestimme__: Ein schwarzes Reisekleid mit Schleier, zu ernst für bloße Mode.","/str","/ev",{"*":".^.c-23","flg":20},{"c-19":["^ ","#","^action:thinking","/#","\n","ev","str","^charcoal_plum_velvet","/str","/ev",{"VAR=":"outfit_detail","re":true},"^Du trägst ein geschneidertes Reisekostüm aus dunkler anthrazitfarbener Wolle. Am Kragen und an den Manschetten liegt ein pflaumenfarbener Samtton, gedämpft genug für den Tag, teuer genug für Menschen mit Augen.","\n",{"->":".^.^.^.g-4"},{"#f":5}],"c-20":["^ ","#","^action:thinking","/#","\n","ev","str","^black_brown_ivory_lace","/str","/ev",{"VAR=":"outfit_detail","re":true},"^Der Rock ist dunkel und schwer genug für die Reise, die Jacke streng, die elfenbeinfarbene Bluse am Hals hochgeschlossen. Die Spitze ist schmal, sauber und gefährlich nahe an Frömmigkeit.","\n",{"->":".^.^.^.g-4"},{"#f":5}],"c-21":["^ ","#","^action:thinking","/#","\n","ev","str","^blue_grey_practical","/str","/ev",{"VAR=":"outfit_detail","re":true},"^Graublaue Wolle, ein kurzer Mantel, ein Rock, der beim Aussteigen nicht sofort Verrat übt, und Knöpfe, die mehr nach Zweck als nach Schmuck aussehen. Korrekt, städtisch, brauchbar.","\n",{"->":".^.^.^.g-4"},{"#f":5}],"c-22":["^ ","#","^action:thinking","/#","\n","ev","str","^dark_green_black_trim","/str","/ev",{"VAR=":"outfit_detail","re":true},"^Das Grün ist so dunkel, dass es erst im Licht der Fenster sichtbar wird. Schwarzer Besatz, passende Handschuhe, ein Hut mit kleiner Feder: nicht laut, aber schwer zu vergessen.","\n",{"->":".^.^.^.g-4"},{"#f":5}],"c-23":["^ ","#","^action:thinking","/#","\n","ev","str","^black_veil_severe","/str","/ev",{"VAR=":"outfit_detail","re":true},"^Das Schwarz ist nicht Trauer, jedenfalls nicht offiziell. Ein schmaler Schleier, dunkle Handschuhe, glatter Rock, hohe Knopfleiste. Es ist die Art Kleidung, in der skeptische Männer leichter an Ahnungen glauben.","\n",{"->":".^.^.^.g-4"},{"#f":5}]}],"g-4":["ev",true,"/ev",{"VAR=":"appearance_done","re":true},"^Als die Berge zurückkehren, wirken sie näher.","\n",{"->":"train_compartment"},null]}],null],"first_viktor_exchange":["ev",{"VAR?":"tut_dialog_intro"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n","#","^alert[Dialogoptionen stehen in Anführungszeichen. Das Aktionswort sagt, ob du antwortest, fragst, mitteilst, spottest, schweigst oder sozial handelst.]","/#","ev",true,"/ev",{"VAR=":"tut_dialog_intro","re":true},{"->":".^.^.^.5"},null]}],"nop","\n","^Er faltet die Zeitung zusammen, obwohl du sehr sicher bist, dass er nicht gelesen hat.","\n","^„Sie sind sehr still gewesen, gnädiges Fräulein. Für eine Dame auf ihrer ersten amtlichen Reise beweisen Sie bemerkenswerte Zurückhaltung.“","\n","^Die Anrede ist technisch richtig, falls du adelig bist, zu hoch gegriffen, falls du es nicht bist, und vollkommen geeignet, weil er noch nicht weiß, welcher Teil von dir brauchbar ist, welcher Verkleidung, und welcher Gefahr.","\n","ev",{"VAR?":"birth_class"},"str","^noble","/str","==","/ev",[{"->":".^.b","c":true},{"b":["\n",{"->":"viktor_class_noble"},{"->":".^.^.^.22"},null]}],[{"->":".^.b"},{"b":["\n","ev",{"VAR?":"birth_class"},"str","^middle","/str","==","/ev",[{"->":".^.b","c":true},{"b":["\n",{"->":"viktor_class_middle"},{"->":".^.^.^.10"},null]}],[{"->":".^.b"},{"b":["\n",{"->":"viktor_class_working"},{"->":".^.^.^.10"},null]}],"nop","\n",{"->":".^.^.^.22"},null]}],"nop","\n",null],"viktor_class_noble":[["^Viktor wartet auf die Antwort, die seine Bemerkung verlangt. Der Zug ruckt einmal, dann findet er wieder in seinen harten, selbstgewissen Rhythmus.","\n","ev","str","^__Sage__: „Zurückhaltung ist keine Tugend, Herr Nowak. Oft ist sie nur gute Erziehung mit geschlossenem Mund.“","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^__Sage__: „Sie brauchen nicht zu prüfen, ob ich stillsitzen kann, Herr Nowak. Ich wurde von Leuten erzogen, die weniger Geduld und schärfere Augen hatten.“","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^__Sage__: „Wenn dies bemerkenswerte Zurückhaltung ist, Herr Nowak, fürchte ich, Sie haben bisher vor allem Offiziere begleitet.“","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["^ ","#","^action:conversation","/#","\n","#","^route:eccentric","/#","ev",{"VAR?":"eccentric"},1,"+",{"VAR=":"eccentric","re":true},"/ev","^„Zurückhaltung ist keine Tugend, Herr Nowak. Oft ist sie nur gute Erziehung mit geschlossenem Mund.“","\n","^Seine Brauen bewegen sich kaum merklich.","\n","^„Dann hat gute Erziehung also militärischen Nutzen“, sagt er.","\n",["ev","str","^__Sage__: „Nur, wenn sie richtig kommandiert wird.“","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^__Sage__: „Nur, wenn Männer Schweigen mit Gehorsam verwechseln.“","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^__Sage__: „Ich bevorzuge jede Disziplin, die keinen Makel in den Akten hinterlässt.“","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["^ ","#","^action:conversation","/#","\n","#","^route:lover","/#","ev",{"VAR?":"lover"},1,"+",{"VAR=":"lover","re":true},"/ev","ev",{"VAR?":"viktor_suspicion"},1,"+",{"VAR=":"viktor_suspicion","re":true},"/ev","^„Nur, wenn sie richtig kommandiert wird.“","\n","^„Sie gedenken, sie selbst zu kommandieren?“","\n","^Die Antwort bleibt im Winkel deines Handschuhs und in der Ruhe deines Blickes liegen.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","#","^action:conversation","/#","\n","#","^route:sapphic","/#","ev",{"VAR?":"sapphic"},1,"+",{"VAR=":"sapphic","re":true},"/ev","ev",{"VAR?":"viktor_suspicion"},1,"+",{"VAR=":"viktor_suspicion","re":true},"/ev","^„Nur, wenn Männer Schweigen mit Gehorsam verwechseln.“","\n","^„Das ist eine ehrgeizige Unterscheidung.“","\n","^Diese Unterscheidung hat viele Frauen davor bewahrt, zu früh verstanden zu werden.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-2":["^ ","#","^action:conversation","/#","\n","#","^route:detective","/#","ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","ev",{"VAR?":"viktor_trust"},1,"+",{"VAR=":"viktor_trust","re":true},"/ev","^„Ich bevorzuge jede Disziplin, die keinen Makel in den Akten hinterlässt.“","\n","^„Eine nützliche Vorliebe“, sagt er. „Wenn sie aufrichtig ist.“","\n","^Die Frage der Aufrichtigkeit bleibt vorläufig sein Problem.","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":[{"->":"viktor_mission_briefing"},null]}],{"#f":5}],"c-1":["^ ","#","^action:conversation","/#","\n","#","^route:detective","/#","ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","ev",{"VAR?":"viktor_trust"},1,"+",{"VAR=":"viktor_trust","re":true},"/ev","^„Sie brauchen nicht zu prüfen, ob ich stillsitzen kann, Herr Nowak. Ich wurde von Leuten erzogen, die weniger Geduld und schärfere Augen hatten.“","\n","^„Eine Familienerziehung also.“","\n",["ev","str","^__Sage__: „Eher ein Familienurteil.“","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^__Sage__: „Eine Erziehung in Zimmern, in denen selbst jeder Stuhl Rang besitzt.“","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","#","^action:conversation","/#","\n","#","^route:eccentric","/#","ev",{"VAR?":"eccentric"},1,"+",{"VAR=":"eccentric","re":true},"/ev","^„Eher ein Familienurteil.“","\n","^„Sie sprechen, als wäre Geburt ein Gefängnis.“","\n","^Die polierten Möbel antworten ihm besser, als es ein Einwand könnte.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","#","^action:conversation","/#","\n","ev",{"VAR?":"class_confidence"},1,"+",{"VAR=":"class_confidence","re":true},"/ev","^„Eine Erziehung in Zimmern, in denen selbst jeder Stuhl Rang besitzt.“","\n","^„Dann wird Hohenreith Sie vielleicht nicht überraschen.“","\n","^Die Möglichkeit, dass Hohenreith bessere Geheimnisse als Stühle besitzt, darf unausgesprochen bleiben.","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":[{"->":"viktor_mission_briefing"},null]}],{"#f":5}],"c-2":["^ ","#","^action:conversation","/#","\n","#","^route:lover","/#","ev",{"VAR?":"lover"},1,"+",{"VAR=":"lover","re":true},"/ev","ev",{"VAR?":"viktor_suspicion"},1,"+",{"VAR=":"viktor_suspicion","re":true},"/ev","^„Wenn dies bemerkenswerte Zurückhaltung ist, Herr Nowak, fürchte ich, Sie haben bisher vor allem Offiziere begleitet.“","\n","^Sein Mundwinkel verändert sich so wenig, dass man es beinahe übersehen könnte.","\n","^„Offiziere langweilen sich nicht so leicht.“","\n",["ev","str","^__Sage__: „Oder weniger ehrlich darin.“","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^__Sage__: „Dann muss ich mich bemühen, die Armee nicht zu enttäuschen.“","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","#","^action:conversation","/#","\n","#","^route:lover","/#","ev",{"VAR?":"lover"},1,"+",{"VAR=":"lover","re":true},"/ev","^„Oder weniger ehrlich darin.“","\n","^„Sie beschuldigen die Armee der Eitelkeit.“","\n","^Der Vorwurf der Beständigkeit ist der am wenigsten zu leugnende.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","#","^action:conversation","/#","\n","#","^route:careless","/#","ev",{"VAR?":"careless"},1,"+",{"VAR=":"careless","re":true},"/ev","^„Dann muss ich mich bemühen, die Armee nicht zu enttäuschen.“","\n","^„Genau das zu verhindern, ist mir aufgetragen worden.“","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":[{"->":"viktor_mission_briefing"},null]}],{"#f":5}]}],null],"viktor_class_middle":[["^Viktors Bemerkung bleibt zwischen euch liegen wie ein höflicher Vorwurf.","\n","ev","str","^__Sage__: „Zurückhaltung fällt leichter, wenn man gelernt hat, dass jeder Fehler von jemandem behalten wird, der besser gestellt ist.“","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^__Sage__: „Wenn ich schweige, Herr Nowak, so deshalb, weil Männer sich schneller erklären, wenn ihnen die Stille missfällt.“","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^__Sage__: „Ich überlegte, ob Ihre Sorge amtlich, persönlich oder bloß männlich ist.“","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["^ ","#","^action:conversation","/#","\n","#","^route:detective","/#","ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","ev",{"VAR?":"viktor_trust"},1,"+",{"VAR=":"viktor_trust","re":true},"/ev","^„Zurückhaltung fällt leichter, wenn man gelernt hat, dass jeder Fehler von jemandem behalten wird, der besser gestellt ist.“","\n","^Viktor beobachtet dich genauer.","\n","^„Eine bittere Lektion.“","\n",["ev","str","^__Sage__: „Eine nützliche. Bitterkeit ist nur der Geschmack, den Belehrung zurücklässt.“","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^__Sage__: „Eine gewöhnliche. Manche Menschen bemerken Unrecht erst, wenn es ihr eigenes Stockwerk erreicht.“","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","#","^action:conversation","/#","\n","#","^route:eccentric","/#","ev",{"VAR?":"eccentric"},1,"+",{"VAR=":"eccentric","re":true},"/ev","^„Eine nützliche. Bitterkeit ist nur der Geschmack, den Belehrung zurücklässt.“","\n","^„Sie sammeln Redewendungen wie Waffen.“","\n","^Der Satz liegt leicht genug da, dass er entscheiden muss, ob er Zierrat oder Waffe ist.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","#","^action:conversation","/#","\n","#","^route:sapphic","/#","ev",{"VAR?":"sapphic"},1,"+",{"VAR=":"sapphic","re":true},"/ev","^„Eine gewöhnliche. Manche Menschen bemerken Unrecht erst, wenn es ihr eigenes Stockwerk erreicht.“","\n","^„Sie haben also Stockwerke studiert?“","\n","^Schwellen, hast du gelernt, sind ehrlicher als Stockwerke; sie geben zu, dass Durchgang ein Vorrecht ist.","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":[{"->":"viktor_mission_briefing"},null]}],{"#f":5}],"c-1":["^ ","#","^action:conversation","/#","\n","#","^route:lover","/#","ev",{"VAR?":"lover"},1,"+",{"VAR=":"lover","re":true},"/ev","ev",{"VAR?":"medium_reputation"},1,"+",{"VAR=":"medium_reputation","re":true},"/ev","^„Wenn ich schweige, Herr Nowak, so deshalb, weil Männer sich schneller erklären, wenn ihnen die Stille missfällt.“","\n","^„Eine Methode?“","\n",["ev","str","^__Sage__: „Eine Höflichkeit. Ich lasse sie mit ihrem Lieblingsthema beginnen.“","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^__Sage__: „Ein Versuch. Er hat verlässliche Ergebnisse geliefert.“","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","#","^action:conversation","/#","\n","#","^route:lover","/#","ev",{"VAR?":"lover"},1,"+",{"VAR=":"lover","re":true},"/ev","^„Eine Höflichkeit. Ich lasse sie mit ihrem Lieblingsthema beginnen.“","\n","^„Mit sich selbst.“","\n","^Seine eigene Antwort vollendet den Grundsatz sauber genug, dass weitere Belehrung Eitelkeit wäre.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","#","^action:conversation","/#","\n","#","^route:detective","/#","ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","^„Ein Versuch. Er hat verlässliche Ergebnisse geliefert.“","\n","^„Dann bin ich Teil Ihres Versuches.“","\n","^Er hat lange genug dir gegenüber gesessen, um Beweisstück zu werden.","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":[{"->":"viktor_mission_briefing"},null]}],{"#f":5}],"c-2":["^ ","#","^action:conversation","/#","\n","#","^route:eccentric","/#","ev",{"VAR?":"eccentric"},1,"+",{"VAR=":"eccentric","re":true},"/ev","ev",{"VAR?":"viktor_suspicion"},1,"+",{"VAR=":"viktor_suspicion","re":true},"/ev","^„Ich überlegte, ob Ihre Sorge amtlich, persönlich oder bloß männlich ist.“","\n","^Seine Augen verhärten sich um genau einen Grad.","\n","^„Heute ist sie amtlich.“","\n",["ev","str","^__Sage__: „Wie bequem. Die beiden anderen dürfen die Verantwortung leugnen.“","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^__Sage__: „Dann werde ich diese amtliche Sorge mit dem Respekt behandeln, der Papier gebührt.“","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","#","^action:conversation","/#","\n","#","^route:eccentric","/#","ev",{"VAR?":"eccentric"},1,"+",{"VAR=":"eccentric","re":true},"/ev","^„Wie bequem. Die beiden anderen dürfen die Verantwortung leugnen.“","\n","^„Ich rate Ihnen, den Witz in Hohenreith nicht zu Ihrem ersten Werkzeug zu machen.“","\n","^Die Herabstufung des Witzes zum zweiten Werkzeug bleibt theoretisch genug, um ungefährlich zu sein.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","#","^action:conversation","/#","\n","#","^route:detective","/#","ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","^„Dann werde ich diese amtliche Sorge mit dem Respekt behandeln, der Papier gebührt.“","\n","^„Papier hat Armeen in Bewegung gesetzt.“","\n","^Papier hat auch Fehler begraben, aber nicht jede Verbesserung muss ausgesprochen werden.","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":[{"->":"viktor_mission_briefing"},null]}],{"#f":5}]}],null],"viktor_class_working":[["^Viktors Höflichkeit ist glatt genug, um keine Fingerabdrücke zu behalten. Darunter hörst du die Frage, wie sehr dieses Abteil dich verbessert hat.","\n","ev","str","^__Sage__: „Zurückhaltung ist, was die Leute loben, wenn sie die Mühe dahinter nicht sehen wollen.“","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^__Sage__: „Ich schweige, weil man Frauen meiner Herkunft meist entweder dankbar oder unsichtbar wünscht.“","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^__Sage__: „Ich versuchte, die Polster nicht so zu berühren, als könnten sie mich anklagen.“","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["^ ","#","^action:conversation","/#","\n","#","^route:detective","/#","ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","ev",{"VAR?":"viktor_trust"},1,"+",{"VAR=":"viktor_trust","re":true},"/ev","^„Zurückhaltung ist, was die Leute loben, wenn sie die Mühe dahinter nicht sehen wollen.“","\n","^Die Zeitung in Viktors Hand knittert einmal.","\n","^„Sie haben etwas dagegen, gelobt zu werden?“","\n",["ev","str","^__Sage__: „Nur, wenn es billig geschieht.“","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^__Sage__: „Nur, wenn es die Person verbirgt, die die Arbeit getan hat.“","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","#","^action:conversation","/#","\n","#","^route:eccentric","/#","ev",{"VAR?":"eccentric"},1,"+",{"VAR=":"eccentric","re":true},"/ev","^„Nur, wenn es billig geschieht.“","\n","^„Das dürfte schwer zu vermeiden sein.“","\n","^Wenn Hohenreith dich billig loben will, wird es die Ökonomie der Enttäuschung kennenlernen müssen.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","#","^action:conversation","/#","\n","#","^route:sapphic","/#","ev",{"VAR?":"sapphic"},1,"+",{"VAR=":"sapphic","re":true},"/ev","^„Nur, wenn es die Person verbirgt, die die Arbeit getan hat.“","\n","^Er betrachtet dich, als wäre die Antwort von weiter hinten im Zug gekommen als aus der ersten Klasse.","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":[{"->":"viktor_mission_briefing"},null]}],{"#f":5}],"c-1":["^ ","#","^action:conversation","/#","\n","#","^route:eccentric","/#","ev",{"VAR?":"eccentric"},1,"+",{"VAR=":"eccentric","re":true},"/ev","ev",{"VAR?":"viktor_suspicion"},1,"+",{"VAR=":"viktor_suspicion","re":true},"/ev","^„Ich schweige, weil man Frauen meiner Herkunft meist entweder dankbar oder unsichtbar wünscht.“","\n","^„Ich habe Sie nicht um Dankbarkeit gebeten.“","\n",["ev","str","^__Sage__: „Nein. Sie haben verlangt, dass ich lenkbar sei.“","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^__Sage__: „Dann werde ich meine Dankbarkeit aufschieben, bis Sie sie verdienen.“","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","#","^action:conversation","/#","\n","#","^route:eccentric","/#","ev",{"VAR?":"eccentric"},1,"+",{"VAR=":"eccentric","re":true},"/ev","^„Nein. Sie haben verlangt, dass ich lenkbar sei.“","\n","^„Ich habe nichts dergleichen verlangt.“","\n","^Rang braucht, anders als Männer, selten direkt zu fragen. Die Möbel fragen an seiner Stelle.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","#","^action:conversation","/#","\n","#","^route:lover","/#","ev",{"VAR?":"lover"},1,"+",{"VAR=":"lover","re":true},"/ev","^„Dann werde ich meine Dankbarkeit aufschieben, bis Sie sie verdienen.“","\n","^Eine Pause. Dann, sehr trocken: „Eine großzügige Abmachung.“","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":[{"->":"viktor_mission_briefing"},null]}],{"#f":5}],"c-2":["^ ","#","^action:conversation","/#","\n","#","^route:careless","/#","ev",{"VAR?":"careless"},1,"+",{"VAR=":"careless","re":true},"/ev","ev","str","^dependence","/str","/ev",{"VAR=":"viktor_relation","re":true},"^„Ich versuchte, die Polster nicht so zu berühren, als könnten sie mich anklagen.“","\n","^Etwas wie Sorge huscht über sein Gesicht, zu spät als Ärger verkleidet.","\n","^„Die Polster haben Minister überstanden. Sie werden auch Sie überstehen.“","\n",["ev","str","^__Sage__: „Dann bin ich schon mutiger, als ich es eben war.“","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^__Sage__: „Ich werde es als Beruhigung auffassen, obgleich Sie es wie einen Verweis vorgetragen haben.“","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","#","^action:conversation","/#","\n","#","^route:careless","/#","ev",{"VAR?":"careless"},1,"+",{"VAR=":"careless","re":true},"/ev","^„Dann bin ich schon mutiger, als ich es eben war.“","\n","^„Mut, an Polstern gemessen, ist kein militärischer Maßstab.“","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","#","^action:conversation","/#","\n","#","^route:lover","/#","ev",{"VAR?":"lover"},1,"+",{"VAR=":"lover","re":true},"/ev","^„Ich werde es als Beruhigung auffassen, obgleich Sie es wie einen Verweis vorgetragen haben.“","\n","^„Mit beidem habe ich Übung.“","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":[{"->":"viktor_mission_briefing"},null]}],{"#f":5}]}],null],"viktor_mission_briefing":[["^Der Zug tritt aus dem Tunnel in einen blassen Nachmittag aus dunklen Tannen und weißem Fels. Tief unten zeigt sich Wasser nur in Blitzen. Das Tal ist kein Anblick aus einem Salonbild mehr. Es hat Tiefe genug, um Dinge zu verbergen.","\n","^Viktor öffnet eine Ledermappe und nimmt ein Memorandum heraus. Er reicht es dir nicht sofort.","\n","^„Wenn wir die Bahn verlassen“, sagt er, „werden wir von einer Kutsche aus Hohenreith erwartet. Von diesem Augenblick an sind Äußerlichkeiten von Bedeutung. Ihren Gastgebern wurde mitgeteilt, dass ich bei Korrespondenz, Reiseangelegenheiten und praktischen Vorkehrungen behilflich bin. Mit militärischen Definitionen muss man sie nicht behelligen.“","\n","ev","str","^__Sage__: „Und die Dorfbewohner?“","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^__Sage__: „Wie barmherzig. Der Staat hat ihnen das Vokabular erspart.“","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^__Sage__: „Sie meinen, sie sollen nicht wissen, ob ich Gast, Werkzeug oder Warnung bin.“","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["^ ","#","^action:conversation","/#","\n","^„Und die Dorfbewohner?“","\n","^„Die Dorfbewohner müssen mit nichts behelligt werden.“","\n","^Da ist sie: die Monarchie im Kleinen. Ein Mann, eine Mappe, ein verschlossener Satz.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","#","^action:conversation","/#","\n","#","^route:eccentric","/#","ev",{"VAR?":"eccentric"},1,"+",{"VAR=":"eccentric","re":true},"/ev","ev",{"VAR?":"viktor_suspicion"},1,"+",{"VAR=":"viktor_suspicion","re":true},"/ev","^„Wie barmherzig. Der Staat hat ihnen das Vokabular erspart.“","\n","^„Der Staat hat ihnen Unruhe erspart“, sagt Viktor.","\n","^Die staatliche Gewohnheit, Unwissenheit mit Ruhe zu verwechseln, muss nicht ausgesprochen werden, um anwesend zu sein.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-2":["^ ","#","^action:conversation","/#","\n","#","^route:detective","/#","ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","ev",{"VAR?":"viktor_trust"},1,"+",{"VAR=":"viktor_trust","re":true},"/ev","^„Sie meinen, sie sollen nicht wissen, ob ich Gast, Werkzeug oder Warnung bin.“","\n","^„Ich meine, sie sollen nur wissen, was die Lage beruhigt.“","\n","^Die Antwort beruhigt nichts. Das ist kein Widerspruch.","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^„Man wird Sie nach dem Stand anreden, den Sie vorweisen“, fährt er fort. „Der Haushalt des Grafen wird den Rang beachten. Die Dienerschaft wird beachten, was der Haushalt beachtet. Die Dorfbewohner mögen weniger beachten und mehr behalten. Ich rate zur Zurückhaltung.“","\n","^Der Rat ist vernünftig. Das macht ihn nicht weniger ärgerlich.","\n","ev","str","^__Sage__: „Wären Herren weniger leicht zu lenken, Herr Nowak, so bedürften Damen weniger Methoden.“","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^__Sage__: „Wenn Sie wünschen, dass ich harmlos erscheine, müssen Sie aufhören, mich wie ein Kerkermeister zu warnen.“","/str","/ev",{"*":".^.c-4","flg":20},"ev","str","^__Sage__: „Dann wollen wir genau sein. Was wissen sie, was vermuten sie, und was ist mir zu prüfen gestattet?“","/str","/ev",{"*":".^.c-5","flg":20},"ev","str","^__Sage__: „Ich werde mein Möglichstes tun, nicht in Ohnmacht zu fallen, es sei denn, es erweist sich als nützlich.“","/str","/ev",{"*":".^.c-6","flg":20},"ev","str","^__Sage__: „Zurückhaltung ist, was furchtsame Leute Gehorsam nennen, nachdem sie vergessen haben, wer sie dazu erzogen hat.“","/str","/ev",{"*":".^.c-7","flg":20},{"c-3":["^ ","#","^action:conversation","/#","\n","#","^route:lover","/#","ev",{"VAR?":"lover"},1,"+",{"VAR=":"lover","re":true},"/ev","ev","str","^provocation","/str","/ev",{"VAR=":"viktor_relation","re":true},"ev",{"VAR?":"viktor_trust"},1,"-",{"VAR=":"viktor_trust","re":true},"/ev","ev",{"VAR?":"viktor_suspicion"},1,"+",{"VAR=":"viktor_suspicion","re":true},"/ev","^„Wären Herren weniger leicht zu lenken, Herr Nowak, so bedürften Damen weniger Methoden.“","\n","^Zum ersten Mal erreicht Belustigung beinahe seinen Mund.","\n","^„Eine gefährliche Lehre.“","\n",["ev","str","^__Sage__: „Eine praktische.“","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^__Sage__: „Gefährliche Lehren reisen am besten in guten Handschuhen.“","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","#","^action:conversation","/#","\n","^„Eine praktische.“","\n","^„Sie gedenken, sie in Hohenreith anzuwenden?“","\n",["ev","str","^__Sage__: „Nur dort, wo die Pflicht gegen die Monarchie Opfer verlangt.“","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^__Sage__: „Nur dort, wo Männer Begehren mit Urteil verwechseln.“","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","#","^action:conversation","/#","\n","#","^route:lover","/#","ev",{"VAR?":"lover"},1,"+",{"VAR=":"lover","re":true},"/ev","^„Nur dort, wo die Pflicht gegen die Monarchie Opfer verlangt.“","\n","^Er blickt auf das Memorandum hinunter, aber nicht schnell genug, um zu verbergen, dass er dich neu einschätzt.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","#","^action:conversation","/#","\n","#","^route:lover","/#","ev",{"VAR?":"lover"},1,"+",{"VAR=":"lover","re":true},"/ev","^„Nur dort, wo Männer Begehren mit Urteil verwechseln.“","\n","^„Das mag mehr Gebiet umfassen, als die Karten zugeben.“","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":[{"->":".^.^.^.^.g-0"},null]}],{"#f":5}],"c-1":["^ ","#","^action:conversation","/#","\n","#","^route:eccentric","/#","ev",{"VAR?":"eccentric"},1,"+",{"VAR=":"eccentric","re":true},"/ev","^„Gefährliche Lehren reisen am besten in guten Handschuhen.“","\n","^„Sie gedenken, Hohenreith durch Charme zum Geständnis zu bringen?“","\n","^Wenn Hohenreith darauf besteht, bezaubert zu werden, wird es kaum deine Schuld sein.","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":[{"->":".^.^.^.^.^.g-1"},null]}],{"#f":5}],"c-4":["^ ","#","^action:conversation","/#","\n","#","^route:sapphic","/#","ev",{"VAR?":"sapphic"},1,"+",{"VAR=":"sapphic","re":true},"/ev","ev","str","^tension","/str","/ev",{"VAR=":"viktor_relation","re":true},"ev",{"VAR?":"viktor_suspicion"},1,"+",{"VAR=":"viktor_suspicion","re":true},"/ev","^„Wenn Sie wünschen, dass ich harmlos erscheine, müssen Sie aufhören, mich wie ein Kerkermeister zu warnen.“","\n","^Sein Blick schärft sich.","\n","^„Ich bin nicht Ihr Kerkermeister.“","\n",["ev","str","^__Sage__: „Nein. Ein Kerkermeister ist wenigstens ehrlich, was den Schlüssel betrifft.“","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^__Sage__: „Dann stellen Sie sich nicht zwischen mich und jede verschlossene Tür, noch ehe ich die Klinke berührt habe.“","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","#","^action:conversation","/#","\n","^„Nein. Ein Kerkermeister ist wenigstens ehrlich, was den Schlüssel betrifft.“","\n","^Die Worte überraschen dich dadurch, dass sie eine Spur hinterlassen. Vielleicht nicht bei ihm. Bei dir. Je näher dich der Zug an Amalias Welt trägt, obwohl du ihr Gesicht noch nicht kennst, desto unerträglicher scheint es, dass jedes weibliche Leben dort von Männern bewacht sein könnte, die Bewachung Sorge nennen.","\n","^Viktor faltet das Memorandum einmal, genau.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","#","^action:conversation","/#","\n","ev",{"VAR?":"viktor_trust"},1,"+",{"VAR=":"viktor_trust","re":true},"/ev","^„Dann stellen Sie sich nicht zwischen mich und jede verschlossene Tür, noch ehe ich die Klinke berührt habe.“","\n","^„Manche Türen sind nicht ohne Grund verschlossen.“","\n","^Jeder Grund, der eines Schlosses würdig ist, sollte die Unverschämtheit der Prüfung überstehen.","\n","^Viktor faltet das Memorandum einmal, genau.","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":[{"->":".^.^.^.^.^.g-1"},null]}],{"#f":5}],"c-5":["^ ","#","^action:conversation","/#","\n","#","^route:detective","/#","ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","ev","str","^professional","/str","/ev",{"VAR=":"viktor_relation","re":true},"ev",{"VAR?":"viktor_trust"},1,"+",{"VAR=":"viktor_trust","re":true},"/ev","^„Dann wollen wir genau sein. Was wissen sie, was vermuten sie, und was ist mir zu prüfen gestattet?“","\n","^Er nickt kaum merklich, als hättest du die einzige Antwort gewählt, die Erwachsenen zusteht.","\n","^„Sie wissen, dass Sie empfohlen kommen. Sie vermuten, dass Sie imstande sein könnten, die Störungen ohne Polizei, Priester oder Presse beizulegen. Ihnen ist gestattet, Betrug, Zwang, Gefährdung der öffentlichen Ordnung oder glaubwürdige, derzeit nicht einzuordnende Erscheinungen zu prüfen.“","\n",["ev","str","^__Sage__: „Glaubwürdige, derzeit nicht einzuordnende Erscheinungen.“","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^__Sage__: „Und wenn die Erscheinungen einzuordnen sind?“","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","#","^action:conversation","/#","\n","^„Glaubwürdige, derzeit nicht einzuordnende Erscheinungen.“","\n","^„So lautet die Wendung.“","\n","^Die Formulierung setzt sich in deinem Geist fest wie ein bürokratisches Gespenst.","\n","^„Die ungefährlichste Art“, sagt er.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","#","^action:conversation","/#","\n","^„Und wenn die Erscheinungen einzuordnen sind?“","\n","^„Dann ordnen wir sie ein, ehe andere es tun.“","\n","^Der Satz hat die Kälte eines Berichts, der rasch über einem Grab geschrieben wurde.","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":[{"->":".^.^.^.^.^.g-1"},null]}],{"#f":5}],"c-6":["^ ","#","^action:conversation","/#","\n","#","^route:careless","/#","ev",{"VAR?":"careless"},1,"+",{"VAR=":"careless","re":true},"/ev","ev","str","^dependence","/str","/ev",{"VAR=":"viktor_relation","re":true},"ev",{"VAR?":"viktor_trust"},1,"-",{"VAR=":"viktor_trust","re":true},"/ev","^„Ich werde mein Möglichstes tun, nicht in Ohnmacht zu fallen, es sei denn, es erweist sich als nützlich.“","\n","^Etwas in seinem Gesicht spannt sich; nicht Verachtung genau, eher Bereitschaft.","\n","^„Ich würde es vorziehen, wenn Sie überhaupt nicht in Ohnmacht fielen.“","\n",["ev","str","^__Sage__: „Wie unkavalierhaft.“","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^__Sage__: „Dann müssen Sie nahe genug bleiben, um mich aufzufangen.“","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","#","^action:conversation","/#","\n","^„Wie unkavalierhaft.“","\n","^„Wie praktisch.“","\n","^Die Last der Sachlichkeit wandert zu ihm hinüber, so anmutig wie ein Ohnmachtssofa, das in ein Feldlazarett geschleppt wird.","\n","^Seine Antwort verzögert sich um einen halben Atemzug.","\n","^„Gerade das, gnädiges Fräulein, bereitet mir Sorge.“","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","#","^action:conversation","/#","\n","#","^route:lover","/#","ev",{"VAR?":"lover"},1,"+",{"VAR=":"lover","re":true},"/ev","^„Dann müssen Sie nahe genug bleiben, um mich aufzufangen.“","\n","^„Meine Befehle erwähnten keine theatralischen Zusammenbrüche.“","\n","^Diese Auslassung spricht nicht für die Gründlichkeit seiner Vorgesetzten.","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":[{"->":".^.^.^.^.^.g-1"},null]}],{"#f":5}],"c-7":["^ ","#","^action:conversation","/#","\n","#","^route:eccentric","/#","ev",{"VAR?":"eccentric"},1,"+",{"VAR=":"eccentric","re":true},"/ev","ev","str","^challenge","/str","/ev",{"VAR=":"viktor_relation","re":true},"ev",{"VAR?":"viktor_suspicion"},2,"+",{"VAR=":"viktor_suspicion","re":true},"/ev","^„Zurückhaltung ist, was furchtsame Leute Gehorsam nennen, nachdem sie vergessen haben, wer sie dazu erzogen hat.“","\n","^Viktor betrachtet dich, als hätte man eine unbekannte Waffe im Gepäck gefunden.","\n","^„Sie haben Freude daran, sich Feinde zu machen.“","\n",["ev","str","^__Sage__: „Nein. Ich missbillige nur die Bequemlichkeit, Dummköpfe unentschieden bleiben zu lassen.“","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^__Sage__: „Feinde sind nur Menschen, die ehrlich genug sind, am richtigen Platz zu stehen.“","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","#","^action:conversation","/#","\n","^„Nein. Ich missbillige nur die Bequemlichkeit, Dummköpfe unentschieden bleiben zu lassen.“","\n","^„In Hohenreith könnte diese Abneigung kostspielig werden.“","\n","^Wenn der Graf Fügsamkeit wollte, hätte er jemand Günstigeren einladen können.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","#","^action:conversation","/#","\n","^„Feinde sind nur Menschen, die ehrlich genug sind, am richtigen Platz zu stehen.“","\n","^„Sie sprechen, als sei Streit eine Art Haushaltsführung.“","\n","^Streit war immer eine Form von Haushaltsführung; man entdeckt, was wohin gehört.","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":[{"->":".^.^.^.^.^.g-1"},null]}],{"#f":5}]}],"g-1":["^Die Räder nehmen eine Kurve. Das Abteil neigt sich. Für einen Augenblick hält euch dieselbe schmale Schräglage.","\n","^Viktor gibt dir endlich das Memorandum.","\n","^Das Schriftstück ist nicht lang. Das ist Teil seiner Bedrohlichkeit. Lange Schriftstücke laden zum Widerspruch ein; kurze tragen Autorität.","\n","^Ein gräflicher Haushalt. Ein Jagdhaus in der Obersteiermark, nicht der Hauptsitz der Familie. Berichte über Störungen unter Dienerschaft und Dorfbewohnern. Kein Einschreiten der Polizei erbeten. Keine öffentliche kirchliche Untersuchung erwünscht. Keine Presse. Keine Korrespondenz außerhalb genehmigter Kanäle. Deine Anwesenheit ist als diskrete Konsultation auf Wunsch der Familie zu erklären. Herr Nowak dient zur Unterstützung praktischer Angelegenheiten.","\n","^Niemand hat das Wort Geist geschrieben.","\n","^Niemand hat das Wort Betrug geschrieben.","\n","^Niemand hat das Wort Tochter geschrieben.","\n","^Doch die Auslassungen ordnen sich auf der Seite an wie Möbel um eine Leiche.","\n","ev","str","^__Sage__: „Es gibt noch eine weitere Weisung.“","/str","/ev",{"*":".^.c-8","flg":20},"ev","str","^__Sage__: „Ihre Fassung ist kürzer als Ihr Schweigen. Das bedeutet, es gibt noch eine weitere Weisung.“","/str","/ev",{"*":".^.c-9","flg":20},"ev","str","^__Sage__: „Wie rührend. Wien vertraut uns beiden so wenig, dass es das Misstrauen aufteilen musste.“","/str","/ev",{"*":".^.c-10","flg":20},{"c-8":["^ ","#","^action:conversation","/#","\n","^„Es gibt noch eine weitere Weisung.“","\n","^Viktor fragt nicht, woher du es weißt.","\n","^„Es gibt immer noch eine weitere Weisung“, sagt er.","\n",["ev","str","^__Sage__: „Für Sie.“","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^__Sage__: „Über mich.“","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^__Sage__: „Darüber, ob ich Betrügerin, Närrin oder nützliches Tier bin.“","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["^ ","#","^action:conversation","/#","\n","^„Für Sie.“","\n","^„Ja.“","\n",["ev","str","^__Sage__: „Über mich.“","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^__Sage__: „Darüber, ob ich Betrügerin, Närrin oder nützliches Tier bin.“","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","#","^action:conversation","/#","\n","^„Über mich.“","\n","^„Teilweise.“","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","#","^action:conversation","/#","\n","ev",{"VAR?":"viktor_suspicion"},1,"+",{"VAR=":"viktor_suspicion","re":true},"/ev","^„Darüber, ob ich Betrügerin, Närrin oder nützliches Tier bin.“","\n","^„Teilweise“, sagt er, und diesmal hat die Ehrlichkeit eine Schneide.","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":[{"->":".^.^.^.^.g-0"},null]}],{"#f":5}],"c-1":["^ ","#","^action:conversation","/#","\n","^„Über mich.“","\n","^„Teilweise.“","\n",{"->":".^.^.g-0"},{"#f":5}],"c-2":["^ ","#","^action:conversation","/#","\n","ev",{"VAR?":"viktor_suspicion"},1,"+",{"VAR=":"viktor_suspicion","re":true},"/ev","^„Darüber, ob ich Betrügerin, Närrin oder nützliches Tier bin.“","\n","^„Teilweise“, sagt er, und diesmal hat die Ehrlichkeit eine Schneide.","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":[{"->":".^.^.^.^.^.g-2"},null]}],{"#f":5}],"c-9":["^ ","#","^action:conversation","/#","\n","#","^route:detective","/#","ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","ev",{"VAR?":"viktor_trust"},1,"+",{"VAR=":"viktor_trust","re":true},"/ev","^„Ihre Fassung ist kürzer als Ihr Schweigen. Das bedeutet, es gibt noch eine weitere Weisung.“","\n","^Viktor fragt nicht, woher du es weißt.","\n","^„Es gibt immer noch eine weitere Weisung“, sagt er.","\n",["ev","str","^__Sage__: „Für Sie.“","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^__Sage__: „Über mich.“","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^__Sage__: „Darüber, ob ich Betrügerin, Närrin oder nützliches Tier bin.“","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["^ ","#","^action:conversation","/#","\n","^„Für Sie.“","\n","^„Ja.“","\n",["ev","str","^__Sage__: „Über mich.“","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^__Sage__: „Darüber, ob ich Betrügerin, Närrin oder nützliches Tier bin.“","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","#","^action:conversation","/#","\n","^„Über mich.“","\n","^„Teilweise.“","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","#","^action:conversation","/#","\n","ev",{"VAR?":"viktor_suspicion"},1,"+",{"VAR=":"viktor_suspicion","re":true},"/ev","^„Darüber, ob ich Betrügerin, Närrin oder nützliches Tier bin.“","\n","^„Teilweise“, sagt er, und diesmal hat die Ehrlichkeit eine Schneide.","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":[{"->":".^.^.^.^.g-0"},null]}],{"#f":5}],"c-1":["^ ","#","^action:conversation","/#","\n","^„Über mich.“","\n","^„Teilweise.“","\n",{"->":".^.^.g-0"},{"#f":5}],"c-2":["^ ","#","^action:conversation","/#","\n","ev",{"VAR?":"viktor_suspicion"},1,"+",{"VAR=":"viktor_suspicion","re":true},"/ev","^„Darüber, ob ich Betrügerin, Närrin oder nützliches Tier bin.“","\n","^„Teilweise“, sagt er, und diesmal hat die Ehrlichkeit eine Schneide.","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":[{"->":".^.^.^.^.^.g-2"},null]}],{"#f":5}],"c-10":["^ ","#","^action:conversation","/#","\n","#","^route:eccentric","/#","ev",{"VAR?":"eccentric"},1,"+",{"VAR=":"eccentric","re":true},"/ev","ev",{"VAR?":"viktor_suspicion"},1,"+",{"VAR=":"viktor_suspicion","re":true},"/ev","^„Wie rührend. Wien vertraut uns beiden so wenig, dass es das Misstrauen aufteilen musste.“","\n","^Viktor fragt nicht, woher du es weißt.","\n","^„Es gibt immer noch eine weitere Weisung“, sagt er.","\n",["ev","str","^__Sage__: „Für Sie.“","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^__Sage__: „Über mich.“","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^__Sage__: „Darüber, ob ich Betrügerin, Närrin oder nützliches Tier bin.“","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["^ ","#","^action:conversation","/#","\n","^„Für Sie.“","\n","^„Ja.“","\n",["ev","str","^__Sage__: „Über mich.“","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^__Sage__: „Darüber, ob ich Betrügerin, Närrin oder nützliches Tier bin.“","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","#","^action:conversation","/#","\n","^„Über mich.“","\n","^„Teilweise.“","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","#","^action:conversation","/#","\n","ev",{"VAR?":"viktor_suspicion"},1,"+",{"VAR=":"viktor_suspicion","re":true},"/ev","^„Darüber, ob ich Betrügerin, Närrin oder nützliches Tier bin.“","\n","^„Teilweise“, sagt er, und diesmal hat die Ehrlichkeit eine Schneide.","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":[{"->":".^.^.^.^.g-0"},null]}],{"#f":5}],"c-1":["^ ","#","^action:conversation","/#","\n","^„Über mich.“","\n","^„Teilweise.“","\n",{"->":".^.^.g-0"},{"#f":5}],"c-2":["^ ","#","^action:conversation","/#","\n","ev",{"VAR?":"viktor_suspicion"},1,"+",{"VAR=":"viktor_suspicion","re":true},"/ev","^„Darüber, ob ich Betrügerin, Närrin oder nützliches Tier bin.“","\n","^„Teilweise“, sagt er, und diesmal hat die Ehrlichkeit eine Schneide.","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":[{"->":".^.^.^.^.^.g-2"},null]}],{"#f":5}]}],"g-2":["^Der Zug beginnt langsamer zu werden. Der Rhythmus verändert sich zuerst im Boden, dann im Fenster, dann im Körper. Häuser sammeln sich neben der Strecke. Ein Stationsdach erscheint zwischen treibendem Rauch und den dunklen Kämmen bewaldeter Hänge. ","#","^sfx[steam-whistle.ogg]","/#","\n","ev","str","^__Sage__: „Dann werde ich versuchen, der Tinte wert zu sein.“","/str","/ev",{"*":".^.c-11","flg":20},"ev","str","^__Sage__: „Dann werde ich die Weisung so kunstvoll enttäuschen, wie es die Umstände erlauben.“","/str","/ev",{"*":".^.c-12","flg":20},"ev","str","^__Sage__: „Dann behalten Sie Ihre zweite Weisung, Herr Nowak. Ich bevorzuge Quellen erster Hand.“","/str","/ev",{"*":".^.c-13","flg":20},{"c-11":["^ ","#","^action:conversation","/#","\n","^„Dann werde ich versuchen, der Tinte wert zu sein.“","\n","^„Das hoffe ich aufrichtig.“","\n",{"->":".^.^.^.g-3"},{"#f":5}],"c-12":["^ ","#","^action:conversation","/#","\n","#","^route:eccentric","/#","ev",{"VAR?":"eccentric"},1,"+",{"VAR=":"eccentric","re":true},"/ev","^„Dann werde ich die Weisung so kunstvoll enttäuschen, wie es die Umstände erlauben.“","\n","^„Ich hoffe aufrichtig, dass Sie es nicht tun.“","\n",{"->":".^.^.^.g-3"},{"#f":5}],"c-13":["^ ","#","^action:conversation","/#","\n","#","^route:detective","/#","ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","^„Dann behalten Sie Ihre zweite Weisung, Herr Nowak. Ich bevorzuge Quellen erster Hand.“","\n","^„Eine Vorliebe, die im kaiserlichen Dienst nicht immer gewährt wird.“","\n",{"->":".^.^.^.g-3"},{"#f":5}]}],"g-3":["^Du kannst nicht entscheiden, ob seine Antwort eine Beleidigung, ein Gebet oder sein erster ehrlicher Satz ist.","\n",{"->":"railway_station"},null]}],null],"railway_station":[["^Die Station ist klein genug, dass der Zug kurz verlegen wirkt, als er dort hält. ","#","^chapter[The Station] ","/#","#","^image[muerzzuschlag.png](portrait)","/#","\n","^Ein Gepäckträger mit einer zu großen Kappe eilt über den Bahnsteig. Eine Frau mit Korb tritt vor dem Dampf zurück wie vor einem Tier. Irgendwo jenseits des Stationsgebäudes stampft ein Kutschpferd im gefrorenen Schlamm. Das Schild gibt dem Ort einen Namen, den du im Fahrplan gesehen hast und an den du dich nicht mit Zuneigung erinnern wirst.","\n","^Dein Gepäck wird in Etappen ausgeladen.","\n","ev","str","^__Bestimme__: Eine disziplinierte amtliche Zusammenstellung.","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^__Bestimme__: Das Gepäck einer eleganten Dame.","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^__Bestimme__: Das Gepäck einer Darstellerin.","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^__Bestimme__: Eine praktische Auswahl, die zu viel Vorbereitung verrät.","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^__Bestimme__: Ein übertriebener Haufen, der jede Tarnung erschwert.","/str","/ev",{"*":".^.c-4","flg":20},{"c-0":["^ ","#","^action:thinking","/#","\n","ev","str","^official","/str","/ev",{"VAR=":"baggage_style","re":true},"ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","^Zuerst kommt ein nüchterner Reisekoffer mit vom Gebrauch stumpfen Messingecken, dann eine Aktenmappe, dann eine Hutschachtel, dann der schmale schwarze Kasten, dessen Inhalt sowohl einen Priester als auch einen Taschenspieler in Verlegenheit bringen würde, falls einer von beiden ihn ohne Phantasie durchsuchte.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","#","^action:thinking","/#","\n","ev","str","^elegant","/str","/ev",{"VAR=":"baggage_style","re":true},"ev",{"VAR?":"class_confidence"},1,"+",{"VAR=":"class_confidence","re":true},"/ev","^Zuerst kommt ein großer Koffer aus dunklem Leder, dann ein kleinerer für Wäsche, dann eine runde Hutschachtel, ein Reise-Necessaire und ein Ridikül, das du zu nahe bei der Hand behältst, als dass ein Gepäckträger seine Bedeutung missverstehen dürfte.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-2":["^ ","#","^action:thinking","/#","\n","ev","str","^performer","/str","/ev",{"VAR=":"baggage_style","re":true},"ev",{"VAR?":"medium_reputation"},1,"+",{"VAR=":"medium_reputation","re":true},"/ev","^Zuerst kommt ein respektabler Koffer, dann eine Hutschachtel, dann ein Reisekasten mit Handschuhen, Schleiern, Bändern, Visitenkarten und kleinen Gegenständen, mit denen man ein Zimmer überreden kann, an Kräfte zu glauben, die längst anwesend sind.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-3":["^ ","#","^action:thinking","/#","\n","ev","str","^practical","/str","/ev",{"VAR=":"baggage_style","re":true},"ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","^Zuerst kommt ein abgenützter, an den Ecken verstärkter Koffer, dann eine Ledertasche mit Notizheften, Bleistiften, gefalteten Karten, Ersatzhandschuhen, einer Handlampe und genug kleinen Notwendigkeiten, um jeden zu beleidigen, der Frauen lieber dekorativ hat.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-4":["^ ","#","^action:thinking","/#","\n","ev","str","^excessive","/str","/ev",{"VAR=":"baggage_style","re":true},"ev",{"VAR?":"careless"},1,"+",{"VAR=":"careless","re":true},"/ev","^Zuerst kommt ein Koffer, dann ein zweiter, dann eine Hutschachtel, dann eine Reisedecke, dann ein Toilettenkasten, dann der schmale schwarze Kasten, dann ein kleineres Paket, von dem du vergessen hattest, dass es das Packen überlebt hat. Am Ende sieht selbst Viktor einen Augenblick lang zahlenmäßig unterlegen aus.","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^Viktor überwacht die Umladung mit knapper Höflichkeit. Er trägt nicht wie ein Diener. Er weist an wie ein Mann, der vorgibt, nicht zu befehlen.","\n","^Die kleine Szene vor dem Waggon ist harmlos genug, um gefährlich zu sein. Ein Gepäckträger wartet mit geneigtem Kopf. Der Kutscher steht einige Schritte entfernt. Viktor ist nah genug, um dir beim Aussteigen die Hand zu reichen, aber nicht so nah, dass er es ohne dein stilles Einverständnis täte. Drei Männer, drei Stände, drei verschiedene Arten von Nützlichkeit.","\n","^Was hier geschieht, wird niemand in einem Bericht erwähnen. Gerade deshalb wird es behalten.","\n","ev",{"VAR?":"tut_manners_intro"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n","#","^alert[Soziale Optionen prüfen Benehmen, Rang und Timing. Nicht jede unhöfliche Wahl ist falsch, aber jede Wahl verrät, wie Valerie sich in der Ordnung ihrer Zeit bewegt.]","/#","ev",true,"/ev",{"VAR=":"tut_manners_intro","re":true},{"->":".^.^.^.11"},null]}],"nop","\n","ev",{"VAR?":"birth_class"},"str","^noble","/str","==",{"VAR?":"birth_class"},"str","^working","/str","==","||",{"VAR?":"tut_gated_intro"},"!","&&","/ev",[{"->":".^.b","c":true},{"b":["\n","#","^alert[Freigeschaltete Optionen zeigen ihre Voraussetzung nach einem Mittelpunkt, zum Beispiel Adel oder Unterschicht. Solche Optionen erscheinen nur, wenn deine bisherigen Entscheidungen sie erlauben.]","/#","ev",true,"/ev",{"VAR=":"tut_gated_intro","re":true},{"->":".^.^.^.30"},null]}],"nop","\n","ev","str","^__Warte__ · **Adel**: Bis Viktor seine Hand anbietet.","/str",{"VAR?":"birth_class"},"str","^noble","/str","==","/ev",{"*":".^.c-5","flg":21},"ev","str","^__Danke__: Viktor mit einem Nicken und lass den Gepäckträger das Gepäck nehmen.","/str","/ev",{"*":".^.c-6","flg":20},"ev","str","^__Bitte__: Den Gepäckträger, zuerst den kleineren Kasten zu nehmen.","/str","/ev",{"*":".^.c-7","flg":20},"ev","str","^__Täusche__: Prüfe Viktor mit einem zu langen Lächeln.","/str","/ev",{"*":".^.c-8","flg":20},"ev","str","^__Berühre__: Greife selbst nach einem Koffer.","/str","/ev",{"*":".^.c-9","flg":20},"ev","str","^__Nimm__ · **Unterschicht**: Dem Gepäckträger beinahe den Koffer aus der Hand.","/str",{"VAR?":"birth_class"},"str","^working","/str","==","/ev",{"*":".^.c-10","flg":21},{"c-5":["^ ","#","^action:social ","/#","#","^gated:noble ","/#","#","^key:z","/#","\n","#","^manners:excellent","/#","ev",{"VAR?":"class_confidence"},2,"+",{"VAR=":"class_confidence","re":true},"/ev","ev",{"VAR?":"court_loyalty"},1,"+",{"VAR=":"court_loyalty","re":true},"/ev","^Du wartest einen Atemzug, bis Viktor seine Hand anbietet, und nimmst sie dann, als wäre dies keine Hilfe, sondern die Ordnung der Welt.","\n","^Du gibst ihm nicht dein Gewicht. Nur deine Hand. Genau genug, dass er dienen darf, ohne Diener zu werden. Der Gepäckträger senkt den Blick ein wenig tiefer. Der Kutscher sieht, was er sehen muss: eine Dame, die ihren Rang nicht beweist, weil Beweise für Leute ohne Rang sind.","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-6":["^ ","#","^action:social","/#","\n","#","^manners:good","/#","ev",{"VAR?":"viktor_trust"},1,"+",{"VAR=":"viktor_trust","re":true},"/ev","^Du nimmst Viktors angebotene Hand knapp und sicher, dankst ihm mit einem Nicken und lässt den Gepäckträger das Gepäck nehmen.","\n","^Es ist gutes Benehmen ohne Prunk: nicht zu vertraut gegenüber Viktor, nicht zu freundlich gegenüber dem Gepäckträger, nicht so kalt, dass es nach Unsicherheit riecht. Mittelstand könnte dies lernen. Adel könnte es billigen. Dienstboten würden erkennen, dass du ihre Arbeit nicht mit Herablassung verwechselst.","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-7":["^ ","#","^action:social","/#","\n","#","^route:detective","/#","#","^manners:practical","/#","ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","^Du steigst selbst aus, bevor Viktor sich entscheiden kann, und bittest den Gepäckträger sachlich, zuerst den kleineren Kasten zu nehmen.","\n","^Das ist nicht ganz falsch, aber auch nicht ganz richtig. Viktor bemerkt die kleine Missachtung der erwarteten Form. Der Gepäckträger gehorcht erleichtert, weil klare Anweisungen leichter zu tragen sind als feine Ungewissheit. Der Kutscher ordnet dich eher der Nützlichkeit als dem Rang zu.","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-8":["^ ","#","^action:social","/#","\n","#","^route:lover","/#","#","^manners:provocative","/#","ev",{"VAR?":"lover"},1,"+",{"VAR=":"lover","re":true},"/ev","ev",{"VAR?":"viktor_suspicion"},1,"+",{"VAR=":"viktor_suspicion","re":true},"/ev","^Du lässt Viktor zu lange mit ausgestreckter Hand warten und lächelst erst dann, als hättest du ihn absichtlich geprüft.","\n","^Es ist fast ein Fauxpas, gerettet durch Anmut und die Tatsache, dass Männer Demütigungen leichter verzeihen, wenn sie sich wie Aufmerksamkeit anfühlen. Viktor hilft dir hinunter. Seine Hand bleibt vollkommen korrekt. Sein Blick nicht ganz.","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-9":["^ ","#","^action:object","/#","\n","#","^route:careless","/#","#","^manners:awkward","/#","ev",{"VAR?":"careless"},1,"+",{"VAR=":"careless","re":true},"/ev","^Du entschuldigst dich beim Gepäckträger dafür, dass deine Sachen Mühe machen, und greifst selbst nach einem Koffer.","\n","^Der Gepäckträger erstarrt, als hättest du ihm eine philosophische Frage gestellt. Viktor tritt sofort dazwischen, höflich genug, um die Rettung wie Zufall aussehen zu lassen. Du hast gegen keine Moral verstoßen, nur gegen die unsichtbare Arbeitsteilung, auf der diese kleine Welt ruht.","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-10":["^ ","#","^action:object ","/#","#","^gated:working ","/#","#","^key:t","/#","\n","#","^manners:fauxpas","/#","ev",{"VAR?":"class_confidence"},1,"-",{"VAR=":"class_confidence","re":true},"/ev","ev",{"VAR?":"careless"},1,"+",{"VAR=":"careless","re":true},"/ev","^Du springst hinunter, bevor jemand dir helfen kann, und nimmst dem Gepäckträger beinahe den Koffer aus der Hand.","\n","^Für eine Sekunde bist du schneller als deine Verkleidung. Der Gepäckträger hält fest, Viktor greift nach deinem Ellbogen, der Kutscher sieht weg, weil Wegsehen manchmal die höflichste Form von Zeugenschaft ist. Es ist kein Unglück. Nur ein Riss, klein genug, um ihn mit Haltung zu schließen.","\n",{"->":".^.^.^.g-1"},{"#f":5}]}],"g-1":["^Die Kutsche aus Hohenreith wartet jenseits des Stationshofes: dunkelgrüner Lack, schwarze Räder, das gräfliche Wappen dezent auf der Tür, zwei Pferde bereits unruhig im Geschirr. Der Kutscher nimmt den Hut ab, als er dich sieht. Nicht zu tief. Tief genug für Rang, nicht tief genug für Ehrfurcht. ","#","^sfx[horse-neigh.ogg]","/#","\n","^„Gnädiges Fräulein? Herr Sekretär?“","\n","ev",{"VAR?":"birth_class"},"str","^noble","/str","==","/ev",[{"->":".^.b","c":true},{"b":["\n","^Man hat ihm genug gesagt, um dich einzuordnen. Das ist eine Höflichkeit. Es ist auch eine Warnung.","\n",{"->":".^.^.^.16"},null]}],[{"->":".^.b"},{"b":["\n","^Er zögert bei dir um das kleinste Maß. Das Zögern ist keine Unhöflichkeit. Es ist Berechnung. Erste Klasse, Hofschreiben, kein Titel außer Fräulein, und ein Mann neben dir, der aussieht, als hätte er Menschen für weniger verhaften lassen als Starren.","\n",{"->":".^.^.^.16"},null]}],"nop","\n","^Viktor antwortet, bevor du es kannst.","\n","^„Vom Jagdhaus Hohenreith?“","\n","^„Jawohl, Herr Sekretär. Der Weg ist befahrbar. Wenn der Nebel nicht dichter wird, sollten wir Eibenreith vor Einbruch der Dunkelheit erreichen.“","\n","^Das Wort tritt ohne Zeremonie in die Luft.","\n","^Eibenreith.","\n","^Nicht Hohenreith, der Name, der in sauberer Hand auf der Einladung steht. Eibenreith: das Dorf darunter. Ein kleinerer Name. Älter im Mund. Ein Name mit Wurzeln statt Briefpapier.","\n",{"->":"coach_journey"},null]}],null],"coach_journey":[["^Die Kutsche lässt die Station hinter sich und damit das letzte leicht erkennbare Zeichen der Monarchie. ","#","^chapter[The Graben] ","/#","#","^music[Kaiserpunk Jodler.mp3](crossfade, loop, lead=4)","/#","\n","^Zuerst folgt der Weg einem Tal, in dem Telegraphendraht ihm noch Gesellschaft leistet und der Fluss in einem hellen, steinigen Bett läuft. Sägewerke, umzäunte Wiesen und Bauernhäuser erscheinen und verschwinden hinter Fichtenbeständen. Die Berge steigen nicht auf einmal. Sie rücken zuständigkeitsweise vor. Ein bewaldeter Hang beansprucht den linken Himmel, dann schließt eine graue Wand aus Kalk den Norden, dann sammelt sich im Osten ein weiterer Rücken, bis selbst die Wolken in Dienst getreten scheinen.","\n","^Der Kutscher nennt Orte, wenn Viktor fragt, doch die Namen sind örtlich und praktisch, gedacht für Männer, die wissen, welche Brücke bei Hochwasser nachgibt und welcher Hof störrische Pferde hält. Irgendwo hinter den sichtbaren Rücken, sagt er, liegt der große weiße Rücken des Hochschwab. Nach Osten, jenseits von Wald und Pass, hält die Hohe Veitsch ihr eigenes Wetter. Er sagt das nicht wie ein Führer, sondern wie ein Mann, der Nachbarn erklärt, die vielleicht guter Laune sind und vielleicht nicht.","\n","^Das Haupttal verengt sich.","\n","^Der Weg biegt davon in einen Seitengraben, und die Veränderung ist augenblicklich. Der Klang ändert sich. Die Räder klingen nicht mehr gegen offene Entfernung, sondern mahlen zwischen Böschungen, Wurzeln und nassem Stein. Die Luft riecht nach Lauberde, Harz und kaltem Wasser. Eiben erscheinen zwischen den Fichten in dunkler, unwahrscheinlicher Geduld, ihre Nadeln zu schwarz für den Nachmittag.","\n","^„Eibenreither Graben“, sagt der Kutscher und bekreuzigt sich so rasch, dass die Geste auch einem Schlagloch gegolten haben könnte.","\n","^Viktor bemerkt es. Natürlich bemerkt er es.","\n","^„Schlechter Weg?“, fragt er.","\n","^„Alter Weg“, sagt der Kutscher.","\n","^Eine Weile spricht niemand.","\n","^Du beobachtest die Bäume.","\n","^Es gibt Wälder, die zu Geschichten einladen, weil sie hübsch sind, und Wälder, die Geschichten zurückweisen, weil das, was dort geschah, keine Zeugen brauchte. Dieser gehört zur zweiten Art. Seine Stämme stehen eng, nicht wild, sondern mit der Haltung einer Menge, die Platz macht für etwas, das vor langer Zeit durch sie getragen wurde. Der Schnee in den Mulden ist nicht rein. Er hat Nadeln gesammelt, Rinde und einen gelblichen Fleck dort, wo Wasser von unten aufgestiegen ist.","\n","^An einem Hang oberhalb des Weges, halb vom Unterholz verschluckt, erblickst du Stein.","\n","^Ein Wegheiligtum vielleicht. Ein Grenzzeichen. Eine Figur. Die Kutsche ist schon vorbei, bevor deine Augen sich auf ihre Form einigen können. Für einen Augenblick bleibt der Eindruck eines Frauenkopfes zurück, geneigt nicht im Gebet, sondern im Lauschen. ","#","^image[statue.png](square)","/#","\n","ev",{"VAR?":"supernatural_senses"},"str","^genuine","/str","==",{"VAR?":"supernatural_senses"},"str","^ambiguous","/str","==","||",{"VAR?":"supernatural_senses"},"str","^repressed","/str","==","||","/ev",[{"->":".^.b","c":true},{"b":["\n","^Dein Nacken zieht sich zusammen.","\n","^Nicht Furcht. Wiedererkennen wäre schlimmer.","\n",{"->":".^.^.^.58"},null]}],[{"->":".^.b"},{"b":["\n","^Du sagst dir, dass alter Stein, durch bewegte Zweige gesehen, zu allem wird, wozu der Geist feig genug ist.","\n",{"->":".^.^.^.58"},null]}],"nop","\n","^Viktor hat sich leicht demselben Hang zugewandt.","\n","^„Haben Sie etwas gesehen?“","\n","ev","str","^__Sage__: „Vielleicht eine Frau im Wald. Oder ein Stein, der eine sein wollte.“","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^__Sage__: „Ein Grenz- oder Wegzeichen. Ich wüsste gern, wohin dieser Pfad führt.“","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^__Sage__: „Nur Bäume. Die Art, bei der man für Herren mit Revolvern dankbar wird.“","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^__Sage__: „Würden Sie mir glauben, wenn ich sagte, ich hätte etwas gesehen?“","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^__Sage__: „Nein.“","/str","/ev",{"*":".^.c-4","flg":20},{"c-0":["^ ","#","^action:conversation","/#","\n","#","^route:eccentric","/#","#","^statue_hint","/#","ev",{"VAR?":"eccentric"},1,"+",{"VAR=":"eccentric","re":true},"/ev","ev",{"VAR?":"viktor_suspicion"},1,"+",{"VAR=":"viktor_suspicion","re":true},"/ev","^„Vielleicht eine Frau im Wald. Oder ein Stein, der eine sein wollte.“","\n","^Er betrachtet die vorbeiziehenden Bäume.","\n","^„Ein Wegheiligtum?“","\n",["ev","str","^__Sage__: „Wenn es ein Heiligtum ist, so ist es seit langem nicht mehr geliebt worden.“","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^__Sage__: „Nein. Heiligtümer wenden sich den Gläubigen zu. Dieses Ding lauschte seitwärts.“","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","#","^action:conversation","/#","\n","^„Wenn es ein Heiligtum ist, so ist es seit langem nicht mehr geliebt worden.“","\n","^„Sie sprechen, als bemerkten Steine Vernachlässigung.“","\n","^Soldaten bemerken Vernachlässigung ebenfalls. Sein Schweigen gesteht genug zu.","\n","^Er antwortet nicht.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","#","^action:conversation","/#","\n","ev",{"VAR?":"supernatural_exposure"},1,"+",{"VAR=":"supernatural_exposure","re":true},"/ev","^„Nein. Heiligtümer wenden sich den Gläubigen zu. Dieses Ding lauschte seitwärts.“","\n","^Viktors Hand ruht am Halteriemen der Kutsche, still und bereit.","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":[{"->":".^.^.^.^.g-0"},null]}],{"#f":5}],"c-1":["^ ","#","^action:conversation","/#","\n","#","^route:detective","/#","#","^statue_hint","/#","ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","ev",{"VAR?":"viktor_trust"},1,"+",{"VAR=":"viktor_trust","re":true},"/ev","^„Ein Grenz- oder Wegzeichen. Ich wüsste gern, wohin dieser Pfad führt.“","\n","^„Sie haben einen Pfad gesehen?“","\n",["ev","str","^__Sage__: „Nicht deutlich. Genug, um später danach zu fragen.“","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^__Sage__: „Nur die Andeutung eines Pfades. Wenn er existiert, erhält jemand seine Abwesenheit aufrecht.“","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","#","^action:conversation","/#","\n","^„Nicht deutlich. Genug, um später danach zu fragen.“","\n","^Viktor blickt durch das kleine rückwärtige Fenster. Die Biegung hat den Hang bereits ausgelöscht.","\n","^„Fragen Sie vorsichtig. Orte, die man nicht erwähnt, sind oft aufschlussreicher als jene, die man empfiehlt.“","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","#","^action:conversation","/#","\n","#","^route:detective","/#","ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","^„Nur die Andeutung eines Pfades. Wenn er existiert, erhält jemand seine Abwesenheit aufrecht.“","\n","^„Sie lassen Abwesenheiten kostspielig klingen.“","\n","^Das sind sie meistens; Abwesenheit ist teuer, wenn jemand sie pflegt.","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":[{"->":".^.^.^.^.g-0"},null]}],{"#f":5}],"c-2":["^ ","#","^action:conversation","/#","\n","#","^route:careless","/#","ev",{"VAR?":"careless"},1,"+",{"VAR=":"careless","re":true},"/ev","ev","str","^dependence","/str","/ev",{"VAR=":"viktor_relation","re":true},"^„Nur Bäume. Die Art, bei der man für Herren mit Revolvern dankbar wird.“","\n","^Sein Ausdruck verdunkelt sich um einen amtlichen Grad.","\n","^„Ein Revolver ist ein schlechtes Werkzeug gegen Bäume.“","\n",["ev","str","^__Sage__: „Dann werde ich mich darauf verlassen, dass Ihre Unterhaltung sie einschüchtert.“","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^__Sage__: „Wie bedauerlich. Sie wirkten so berufsmäßig beruhigend.“","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","#","^action:conversation","/#","\n","^„Dann werde ich mich darauf verlassen, dass Ihre Unterhaltung sie einschüchtert.“","\n","^Der Kutscher tut, als höre er nichts. Seine Schultern jedoch hören alles.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","#","^action:conversation","/#","\n","#","^route:lover","/#","ev",{"VAR?":"lover"},1,"+",{"VAR=":"lover","re":true},"/ev","^„Wie bedauerlich. Sie wirkten so berufsmäßig beruhigend.“","\n","^„Ich bevorzuge Feinde, die sich zu erkennen geben.“","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":[{"->":".^.^.^.^.g-0"},null]}],{"#f":5}],"c-3":["^ ","#","^action:conversation","/#","\n","#","^route:lover","/#","ev",{"VAR?":"lover"},1,"+",{"VAR=":"lover","re":true},"/ev","ev",{"VAR?":"viktor_suspicion"},1,"+",{"VAR=":"viktor_suspicion","re":true},"/ev","^„Würden Sie mir glauben, wenn ich sagte, ich hätte etwas gesehen?“","\n","^„Das hinge davon ab, welchen Vorteil Sie sich von der Antwort versprechen.“","\n",["ev","str","^__Sage__: „Herr Nowak. Sie verletzen mich.“","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^__Sage__: „Dann beobachten Sie den Hang, nicht meine Absichten. Eines von beidem könnte nützlich sein.“","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","#","^action:conversation","/#","\n","^„Herr Nowak. Sie verletzen mich.“","\n","^„Noch nicht.“","\n","^Es ist das Erste, was er an diesem Tag gesagt hat, das beinahe wie ein Flirt klingt, wenn auch vielleicht nur deshalb, weil Gefahr ein Talent dafür hat, wärmere Kleider zu borgen.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","#","^action:conversation","/#","\n","ev",{"VAR?":"viktor_trust"},1,"+",{"VAR=":"viktor_trust","re":true},"/ev","^„Dann beobachten Sie den Hang, nicht meine Absichten. Eines von beidem könnte nützlich sein.“","\n","^Er gehorcht, ohne zuzugeben, dass er es getan hat.","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":[{"->":".^.^.^.^.g-0"},null]}],{"#f":5}],"c-4":["^ ","#","^action:conversation","/#","\n","#","^route:sapphic","/#","ev",{"VAR?":"sapphic"},1,"+",{"VAR=":"sapphic","re":true},"/ev","^„Nein.“","\n","^Die Verneinung kommt zu rasch, und ihr hört es beide.","\n","^Du denkst nicht mehr an den Stein. Du denkst an die junge Frau, die irgendwo vor euch wartet: die Tochter des Grafen, der Grund, der sorgsam nicht im Memorandum steht, die Fremde, deren Haushalt dich unter einem Titel herbeigerufen hat, der zugleich lächerlich und nützlich ist.","\n",["ev","str","^__Sage__: „Es war nur Schatten.“","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^__Sage__: „Oder, falls ich etwas sah, ziehe ich es vor, es mir nicht erklären zu lassen, bevor ich verstehe, warum es von Bedeutung ist.“","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","#","^action:conversation","/#","\n","^„Es war nur Schatten.“","\n","^Wenn dieser Ort Frauen in Stein hält, denkst du, was tut er dann mit ihnen in den Häusern?","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","#","^action:conversation","/#","\n","#","^route:detective","/#","ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","^„Oder, falls ich etwas sah, ziehe ich es vor, es mir nicht erklären zu lassen, bevor ich verstehe, warum es von Bedeutung ist.“","\n","^Wenn dieser Ort Frauen in Stein hält, denkst du, was tut er dann mit ihnen in den Häusern?","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":[{"->":".^.^.^.^.g-0"},null]}],{"#f":5}],"g-0":["^Der Graben öffnet sich widerwillig.","\n","^Zuerst kommt der Geruch von Rauch. Dann ein Dach, niedrig und dunkel vom Wetter. Dann ein zweites. Dann ein Kirchturm, nicht hoch, nicht anmutig, sondern breitschultrig und blass vor dem Hang dahinter. Seine Mauern wirken älter als das Dorf um sie her und weniger sicher ihres Sieges. Die Fenster sind klein. Die Kirchhofmauer hält die Straße auf Abstand, als bräuchten die Toten Schutz vor den Lebenden oder die Lebenden vor etwas anderem. ","#","^chapter[Eibenreith Village] ","/#","#","^sfx[church-bells.ogg](max=8, fade) ","/#","#","^image[eibenreith.png](landscape)","/#","\n","^Eibenreith erscheint nicht, wie ein Dorf auf einem Bild erscheint, auf einmal und zur Bewunderung geordnet, sondern in Bruchstücken.","\n","^Eine Frau mit einem dunklen Kopftuch hält mit einem Eimer in der Hand inne. Ein Bub hört auf, Gänse zu treiben, und lässt sie um seine Stiefel klagen. Zwei Männer vor einem Schuppen beenden im selben Augenblick ihr Gespräch, ohne einander anzusehen. Vorhänge rühren sich an Fenstern, hinter denen niemand zugibt zu stehen. Ein Schmiedeschild bewegt sich leicht in Luft, die du nicht fühlen kannst. Wasser läuft irgendwo unter Brettern, unter Stein, unter der Straße selbst, schnell, kalt und verborgen.","\n","^Die Häuser sind nicht arm, nicht eigentlich. Viele sind fest, weißgekalkt, geschindelt, erhalten mit der störrischen Anständigkeit von Menschen, die reparieren, was sie nicht ersetzen können. Und doch stört etwas in ihrer Anordnung das Auge. Sie wenden sich der Kirche zu, aber nicht ganz. Sie halten die Straße, aber lehnen sich von ihr weg. Sie lassen zwischen Hof, Zaun und Holzstoß schmale Durchgänge, in denen sich Schatten zu früh sammelt.","\n","^Die Kutsche wird langsamer.","\n","^Niemand läuft herbei, um sie zu begrüßen.","\n","^Niemand muss das. Die Nachricht ist bereits ins Dorf eingetreten, auf Wegen schneller als Bahn, Telegraph oder kaiserliches Siegel.","\n","^Du sitzt sehr gerade, während Eibenreith dich zum ersten Mal betrachtet.","\n","^Der Kutscher hält vor dem Wirtshaus oder vielleicht nur vor dem Gebäude, das in einem besseren Dorf eines gewesen wäre. Ein Knecht aus dem Dorf tritt aus dem Schatten des Tors. Viktor öffnet die Kutschentür von innen nicht sofort; der Kutscher steigt ab, um den Schlag zu öffnen. Der Knecht sieht auf dein Gepäck, dann auf deine Handschuhe, dann auf Viktor.","\n","^Wieder stellt die Welt eine Frage, ohne sie auszusprechen: Wer darf dir helfen, wer muss dir helfen, und wem erlaubst du, dabei wichtig zu wirken?","\n","ev","str","^__Warte__ · **Adel**: Bis der Kutscher den Schlag öffnet und Viktor zuerst aussteigt.","/str",{"VAR?":"birth_class"},"str","^noble","/str","==","/ev",{"*":".^.c-5","flg":21},"ev","str","^__Danke__: Dem Kutscher mit einem knappen Blick, nachdem Viktor dir geholfen hat.","/str","/ev",{"*":".^.c-6","flg":20},"ev","str","^__Weise den Knecht an__: Welches Gepäck zuerst abgeladen werden soll.","/str","/ev",{"*":".^.c-7","flg":20},"ev","str","^__Danke__: Dem Kutscher mit einem zu freundlichen Lächeln.","/str","/ev",{"*":".^.c-8","flg":20},"ev","str","^__Gehe__: Zu früh aus der Kutsche.","/str","/ev",{"*":".^.c-9","flg":20},"ev","str","^__Gehe__ · **Unterschicht**: Allein aus der Kutsche.","/str",{"VAR?":"birth_class"},"str","^working","/str","==","/ev",{"*":".^.c-10","flg":21},{"c-5":["^ ","#","^action:social ","/#","#","^gated:noble ","/#","#","^key:z","/#","\n","#","^manners:excellent","/#","ev",{"VAR?":"class_confidence"},2,"+",{"VAR=":"class_confidence","re":true},"/ev","^Du wartest, bis der Kutscher den Schlag öffnet und Viktor zuerst aussteigt; erst dann reichst du Viktor die behandschuhte Hand.","\n","^Es geschieht langsam genug, dass alle Beteiligten ihre Rolle finden. Der Kutscher ist Dienst, Viktor ist Begleitung, der Knecht ist noch nicht wichtig genug, um dich zu berühren. Dein Fuß erreicht den Boden, als hätte die Straße sich dafür bereitgehalten.","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-6":["^ ","#","^action:social","/#","\n","#","^manners:good","/#","ev",{"VAR?":"viktor_trust"},1,"+",{"VAR=":"viktor_trust","re":true},"/ev","^Du lässt Viktor aussteigen, nimmst seine Hand beim Abtreten und dankst dem Kutscher erst danach mit einem knappen Blick.","\n","^Der Ablauf ist korrekt genug, um keine Geschichte zu erzeugen. In einem Dorf, das von Geschichten lebt, ist das ein kleiner Sieg.","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-7":["^ ","#","^action:social","/#","\n","#","^route:detective","/#","#","^manners:practical","/#","ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","^Du gibst dem Knecht eine klare Anweisung, welches Gepäck zuerst abgeladen werden soll, bevor er danach fragen kann.","\n","^Er gehorcht sofort. Viktor registriert die Zweckmäßigkeit. Der Kutscher registriert die Ungewöhnlichkeit. Eine Dame, die Gepäckreihenfolgen kennt, ist entweder sehr erfahren, sehr nervös oder beides.","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-8":["^ ","#","^action:social","/#","\n","#","^route:lover","/#","#","^manners:too_warm","/#","ev",{"VAR?":"lover"},1,"+",{"VAR=":"lover","re":true},"/ev","^Du bietest dem Kutscher ein sichtbares Lächeln und ein zu freundliches „Danke“ an.","\n","^Der Mann senkt den Blick, verwirrt und geschmeichelt. Viktor wird stiller. Freundlichkeit über Standesgrenzen hinweg kann Güte sein, Taktik oder Unachtsamkeit. Auf dem Dorf wird niemand lange brauchen, eine vierte Möglichkeit zu erfinden.","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-9":["^ ","#","^action:movement","/#","\n","#","^route:careless","/#","#","^manners:awkward","/#","ev",{"VAR?":"careless"},1,"+",{"VAR=":"careless","re":true},"/ev","ev","str","^dependence","/str","/ev",{"VAR=":"viktor_relation","re":true},"^Du steigst zu früh aus, trittst beinahe in den Straßenschlamm und fängst dich an Viktors Arm.","\n","^Er hält dich ohne sichtbare Anstrengung fest. Für einen Augenblick sieht das Dorf genau das, was es am liebsten sieht: eine Dame, gerettet durch einen Mann. Es ist lächerlich nützlich und nützlich lächerlich.","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-10":["^ ","#","^action:movement ","/#","#","^gated:working","/#","\n","#","^manners:fauxpas","/#","ev",{"VAR?":"class_confidence"},1,"-",{"VAR=":"class_confidence","re":true},"/ev","^Du steigst allein aus, nimmst deinen Rock hoch genug, um den Schlamm zu sehen, und sagst dem Knecht, er solle mit dem schweren Koffer vorsichtig sein.","\n","^Es ist praktisch, schnell und völlig falsch. Nicht, weil du unrecht hast, sondern weil du recht hast wie jemand, der selbst schon getragen hat. Der Knecht erkennt es. Viktor auch.","\n",{"->":".^.^.^.g-1"},{"#f":5}]}],"g-1":["^Neben dir senkt Viktor die Stimme.","\n","^„Vergessen Sie nicht: In Hohenreith wird jede Höflichkeit etwas bedeuten. Hier wird es jedes Schweigen tun.“","\n","ev","str","^__Sage__: „Dann werden wir bereits empfangen.“","/str","/ev",{"*":".^.c-11","flg":20},"ev","str","^__Sage__: „Sie lassen es klingen, als stünde das Dorf über dem Grafen.“","/str","/ev",{"*":".^.c-12","flg":20},"ev","str","^__Sage__: „Wie glücklich, dass ich mehrere Arten des Schweigens eingepackt habe.“","/str","/ev",{"*":".^.c-13","flg":20},"ev","str","^__Sage__: „Ich mag es nicht, von Leuten beobachtet zu werden, die sich nicht vorstellen.“","/str","/ev",{"*":".^.c-14","flg":20},"ev","str","^__Sage__: „Wenn Amalia ihr ganzes Leben unter diesem Blick gelebt hat, beginne ich zu verstehen, weshalb man nach Geistern sandte.“","/str","/ev",{"*":".^.c-15","flg":20},{"c-11":["^ ","#","^action:conversation","/#","\n","#","^route:detective","/#","ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","^„Dann werden wir bereits empfangen.“","\n","^„Ja“, sagt er. „Und geprüft.“","\n",{"->":".^.^.^.g-2"},{"#f":5}],"c-12":["^ ","#","^action:conversation","/#","\n","#","^route:eccentric","/#","ev",{"VAR?":"eccentric"},1,"+",{"VAR=":"eccentric","re":true},"/ev","^„Sie lassen es klingen, als stünde das Dorf über dem Grafen.“","\n","^„Nein“, sagt Viktor. „Nur, als hätte es vielleicht mehr als einen überlebt.“","\n",{"->":".^.^.^.g-2"},{"#f":5}],"c-13":["^ ","#","^action:conversation","/#","\n","#","^route:lover","/#","ev",{"VAR?":"lover"},1,"+",{"VAR=":"lover","re":true},"/ev","^„Wie glücklich, dass ich mehrere Arten des Schweigens eingepackt habe.“","\n","^Sein Mund bewegt sich beinahe. „Verwenden Sie zuerst das schlichteste.“","\n",{"->":".^.^.^.g-2"},{"#f":5}],"c-14":["^ ","#","^action:conversation","/#","\n","#","^route:careless","/#","ev",{"VAR?":"careless"},1,"+",{"VAR=":"careless","re":true},"/ev","^„Ich mag es nicht, von Leuten beobachtet zu werden, die sich nicht vorstellen.“","\n","^„Das“, sagt er, „wird sich heute kaum bessern.“","\n",{"->":".^.^.^.g-2"},{"#f":5}],"c-15":["^ ","#","^action:conversation","/#","\n","#","^route:sapphic","/#","ev",{"VAR?":"sapphic"},1,"+",{"VAR=":"sapphic","re":true},"/ev","^„Wenn Amalia ihr ganzes Leben unter diesem Blick gelebt hat, beginne ich zu verstehen, weshalb man nach Geistern sandte.“","\n","^Viktor sieht dich an, doch welche Antwort er auch erwägt, er behält sie hinter den Zähnen.","\n",{"->":".^.^.^.g-2"},{"#f":5}]}],"g-2":["^Die Pferde ziehen die Kutsche an der Kirchhofmauer vorbei. Darüber, auf dem alten Putz neben dem Tor, blickt eine verblasste gemalte Frau unter einem abblätternden blauen Mantel herab. Ihre Hände sind zum Gebet gefaltet. Ihre Augen, vom Wetter beschädigt, zeigen nicht mehr in dieselbe Richtung.","\n","ev",{"VAR?":"religion_stance"},"str","^devout_catholic","/str","==","/ev",[{"->":".^.b","c":true},{"b":["\n","^Für einen Atemzug stört dich nicht, dass das Bild alt ist. Es stört dich, dass es nicht mehr ganz heilig wirkt.","\n",{"->":".^.^.^.11"},null]}],[{"->":".^.b"},{"b":["\n","ev",{"VAR?":"religion_stance"},"str","^josephinian_sceptic","/str","==","/ev",[{"->":".^.b","c":true},{"b":["\n","^Für einen Atemzug wirkt das Bild weniger wie Andacht als wie Verwaltung: ein aufgemaltes Siegel über etwas, das man nicht fortschaffen konnte.","\n",{"->":".^.^.^.10"},null]}],[{"->":".^.b"},{"b":["\n","ev",{"VAR?":"religion_stance"},"str","^wounded_catholic","/str","==","/ev",[{"->":".^.b","c":true},{"b":["\n","^Für einen Atemzug trifft dich das gemalte Gesicht an einer Stelle, die du lieber Schuld als Erinnerung nennen würdest.","\n",{"->":".^.^.^.10"},null]}],[{"->":".^.b"},{"b":["\n","^Für einen Atemzug, als die Räder über ein verborgenes Wasserrinnsal fahren, wirkt das gemalte Gesicht weniger wie die Heilige Mutter als wie eine Maske, die etwas aufgesetzt wurde, das länger gewartet hatte.","\n",{"->":".^.^.^.10"},null]}],"nop","\n",{"->":".^.^.^.10"},null]}],"nop","\n",{"->":".^.^.^.11"},null]}],"nop","\n","^Dann fährt die Kutsche in das eigentliche Dorf hinein, und die Straße biegt zu der unsichtbaren Höhe, auf der Jagdhaus Hohenreith über Eibenreith unter seinem neueren Namen steht.","\n","#","^score[Du hast Eibenreith erreicht.]","/#","end",null]}],null],"global decl":["ev","str","^unset","/str",{"VAR=":"birth_class"},"str","^","/str",{"VAR=":"title_part"},"str","^","/str",{"VAR=":"given_names"},"str","^","/str",{"VAR=":"common_name"},"str","^","/str",{"VAR=":"surname"},"str","^unset","/str",{"VAR=":"religion_stance"},"str","^unset","/str",{"VAR=":"supernatural_belief"},"str","^unset","/str",{"VAR=":"supernatural_senses"},"str","^unset","/str",{"VAR=":"body_detail"},"str","^unset","/str",{"VAR=":"hair_colour"},"str","^unset","/str",{"VAR=":"hairstyle"},"str","^unset","/str",{"VAR=":"complexion_detail"},"str","^unset","/str",{"VAR=":"face_detail"},"str","^unset","/str",{"VAR=":"outfit_detail"},false,{"VAR=":"appearance_done"},"str","^unset","/str",{"VAR=":"baggage_style"},"str","^unset","/str",{"VAR=":"viktor_relation"},false,{"VAR=":"saw_window"},false,{"VAR=":"observed_viktor"},false,{"VAR=":"glanced_mirror_early"},false,{"VAR=":"tut_choice_intro"},false,{"VAR=":"tut_optional_intro"},false,{"VAR=":"tut_character_intro"},false,{"VAR=":"tut_dialog_intro"},false,{"VAR=":"tut_manners_intro"},false,{"VAR=":"tut_gated_intro"},0,{"VAR=":"lover"},0,{"VAR=":"sapphic"},0,{"VAR=":"detective"},0,{"VAR=":"careless"},0,{"VAR=":"eccentric"},0,{"VAR=":"class_confidence"},0,{"VAR=":"medium_reputation"},0,{"VAR=":"court_loyalty"},0,{"VAR=":"viktor_trust"},0,{"VAR=":"viktor_suspicion"},0,{"VAR=":"supernatural_exposure"},"/ev","end",null]}],"listDefs":{}}
\ No newline at end of file
diff --git a/dist/config/game-config.d.ts b/dist/config/game-config.d.ts
index a6d838f..8597569 100644
--- a/dist/config/game-config.d.ts
+++ b/dist/config/game-config.d.ts
@@ -5,6 +5,7 @@ export interface GameMetadata {
subtitle?: string;
version?: string;
copyright?: string;
+ language?: string;
}
export interface GamePaths {
mainGameFile: string;
diff --git a/dist/config/game-config.js b/dist/config/game-config.js
index e31d763..e4788de 100644
--- a/dist/config/game-config.js
+++ b/dist/config/game-config.js
@@ -30,6 +30,7 @@ function fallbackConfig(engine) {
subtitle: 'An open-world text adventure',
version: '1.0.0',
copyright: '',
+ language: 'en_US',
},
};
}
@@ -56,6 +57,7 @@ function loadGameConfig(configPath, engine) {
metadata: {
...fallback.metadata,
...(parsed.metadata ?? {}),
+ language: parsed.metadata?.language ?? parsed.locale ?? fallback.metadata.language,
},
};
}
diff --git a/dist/config/game-config.js.map b/dist/config/game-config.js.map
index 2d52eee..6581257 100644
--- a/dist/config/game-config.js.map
+++ b/dist/config/game-config.js.map
@@ -1 +1 @@
-{"version":3,"file":"game-config.js","sourceRoot":"","sources":["../../src/config/game-config.ts"],"names":[],"mappings":"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"}
\ No newline at end of file
+{"version":3,"file":"game-config.js","sourceRoot":"","sources":["../../src/config/game-config.ts"],"names":[],"mappings":"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"}
\ No newline at end of file
diff --git a/dist/engine/ink-engine.js b/dist/engine/ink-engine.js
index 5ed3e1c..43f49c7 100644
--- a/dist/engine/ink-engine.js
+++ b/dist/engine/ink-engine.js
@@ -120,10 +120,12 @@ class InkEngine {
}
const paragraphs = [];
const globalTags = [];
+ const turnTags = [];
while (this.story.canContinue) {
const rawText = this.story.Continue();
const text = String(rawText || '').trim();
const tags = (0, tag_parser_1.parseTags)(this.story.currentTags || []);
+ turnTags.push(...tags);
tags
.filter((tag) => tag.key === 'title' || tag.key === 'author')
.forEach((tag) => globalTags.push(tag));
@@ -137,7 +139,7 @@ class InkEngine {
const choices = this.story.currentChoices.map((choice) => {
const tags = (0, tag_parser_1.parseTags)(choice.tags || []);
const category = (0, tag_parser_1.getTagValue)(tags, 'action');
- const letter = (0, tag_parser_1.getTagValue)(tags, 'letter');
+ const letter = (0, tag_parser_1.getTagValue)(tags, 'letter') || (0, tag_parser_1.getTagValue)(tags, 'key');
return {
index: choice.index,
text: String(choice.text || '').trim(),
@@ -146,12 +148,43 @@ class InkEngine {
letter,
};
});
+ const inputMode = choices.length > 0 ? 'choice' : 'end';
+ const gameState = {};
+ if (inputMode === 'end') {
+ const errorTag = turnTags.find((tag) => tag.key === 'error');
+ const scoreTag = turnTags.find((tag) => tag.key === 'score');
+ if (!errorTag && !scoreTag) {
+ const message = 'Ink story ended without an explicit #score ending tag.';
+ const generatedErrorTag = { key: 'error', value: message };
+ globalTags.push(generatedErrorTag);
+ turnTags.push(generatedErrorTag);
+ }
+ const finalErrorTag = turnTags.find((tag) => tag.key === 'error');
+ const finalScoreTag = turnTags.find((tag) => tag.key === 'score');
+ if (finalErrorTag) {
+ gameState.endState = {
+ type: 'error',
+ message: finalErrorTag.value || finalErrorTag.param,
+ };
+ }
+ else if (finalScoreTag) {
+ const numericScore = Number(finalScoreTag?.value);
+ if (Number.isFinite(numericScore)) {
+ gameState.score = numericScore;
+ }
+ gameState.endState = {
+ type: 'intended',
+ message: finalScoreTag.value || finalScoreTag.param,
+ };
+ }
+ }
return {
turnId: this.nextTurnId++,
paragraphs,
choices,
- inputMode: choices.length > 0 ? 'choice' : 'end',
+ inputMode,
globalTags: globalTags.length > 0 ? globalTags : undefined,
+ gameState: Object.keys(gameState).length > 0 ? gameState : undefined,
};
}
}
diff --git a/dist/engine/ink-engine.js.map b/dist/engine/ink-engine.js.map
index edd1e7c..1420e7c 100644
--- a/dist/engine/ink-engine.js.map
+++ b/dist/engine/ink-engine.js.map
@@ -1 +1 @@
-{"version":3,"file":"ink-engine.js","sourceRoot":"","sources":["../../src/engine/ink-engine.ts"],"names":[],"mappings":"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"}
\ No newline at end of file
+{"version":3,"file":"ink-engine.js","sourceRoot":"","sources":["../../src/engine/ink-engine.ts"],"names":[],"mappings":";;;;;;AA4BA,4CA8CC;AA1ED,2BAAwE;AACxE,gDAAwB;AACxB,iCAA8B;AAM9B,oDAA6D;AAE7D,MAAM,EAAE,QAAQ,EAAE,GAAG,OAAO,CAAC,YAAY,CAUgB,CAAC;AAQ1D,SAAgB,gBAAgB,CAAC,UAAkB,EAAE,UAAkB;IACrE,MAAM,cAAc,GAAG,cAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IAChD,MAAM,cAAc,GAAG,cAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IAChD,IAAI,CAAC,IAAA,eAAU,EAAC,cAAc,CAAC,EAAE,CAAC;QAChC,MAAM,IAAI,KAAK,CAAC,8BAA8B,cAAc,EAAE,CAAC,CAAC;IAClE,CAAC;IAED,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,MAAM,MAAM,GAAG,IAAA,iBAAY,EAAC,cAAc,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;IAC3E,MAAM,SAAS,GAAG,cAAI,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC;IAC/C,MAAM,WAAW,GAAG;QAClB,kBAAkB,EAAE,CAAC,QAAgB,EAAE,EAAE,CACvC,cAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,cAAI,CAAC,OAAO,CAAC,SAAS,EAAE,QAAQ,CAAC;QAC1E,mBAAmB,EAAE,CAAC,QAAgB,EAAE,EAAE,CACxC,IAAA,iBAAY,EAAC,cAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,cAAI,CAAC,OAAO,CAAC,SAAS,EAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;aAC3F,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC;KAC5B,CAAC;IACF,MAAM,QAAQ,GAAG,IAAI,QAAQ,CAAC,MAAM,EAAE;QACpC,cAAc,EAAE,cAAc;QAC9B,WAAW;QACX,YAAY,EAAE,CAAC,OAAe,EAAE,IAAY,EAAE,EAAE;YAC9C,IAAI,IAAI,KAAK,CAAC,EAAE,CAAC;gBACf,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YACzB,CAAC;iBAAM,CAAC;gBACN,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YACvB,CAAC;QACH,CAAC;KACF,CAAC,CAAC;IAEH,MAAM,KAAK,GAAG,QAAQ,CAAC,OAAO,EAAE,CAAC;IACjC,IAAI,CAAC,KAAK,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAChC,MAAM,IAAI,KAAK,CAAC,4BAA4B,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACnE,CAAC;IAED,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACxB,QAAQ,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,OAAO,EAAE,CAAC,CAAC,CAAC;IAClE,CAAC;IAED,IAAA,cAAS,EAAC,cAAI,CAAC,OAAO,CAAC,cAAc,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC7D,IAAA,kBAAa,EAAC,cAAc,EAAE,KAAK,CAAC,MAAM,EAAE,EAAE,MAAM,CAAC,CAAC;IACtD,OAAO;QACL,UAAU,EAAE,cAAc;QAC1B,UAAU,EAAE,cAAc;QAC1B,YAAY,EAAE,QAAQ,CAAC,MAAM;KAC9B,CAAC;AACJ,CAAC;AAED,MAAa,SAAS;IAIpB,YAA6B,SAAiB;QAAjB,cAAS,GAAT,SAAS,CAAQ;QAHtC,UAAK,GAAiB,IAAI,CAAC;QAC3B,eAAU,GAAG,CAAC,CAAC;IAE0B,CAAC;IAElD,SAAS;QACP,IAAI,CAAC,IAAI,CAAC,KAAK;YAAE,OAAO,KAAK,CAAC;QAC9B,OAAO,IAAI,CAAC,KAAK,CAAC,WAAW,IAAI,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,MAAM,GAAG,CAAC,CAAC;IACxE,CAAC;IAED,OAAO;QACL,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;QAC9B,IAAI,CAAC,UAAU,GAAG,CAAC,CAAC;QACpB,OAAO,IAAI,CAAC,aAAa,EAAE,CAAC;IAC9B,CAAC;IAED,YAAY,CAAC,WAAmB;QAC9B,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;YAChB,MAAM,IAAI,KAAK,CAAC,qBAAqB,CAAC,CAAC;QACzC,CAAC;QAED,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,KAAK,WAAW,CAAC,CAAC;QACpF,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,MAAM,IAAI,KAAK,CAAC,cAAc,WAAW,mBAAmB,CAAC,CAAC;QAChE,CAAC;QAED,IAAI,CAAC,KAAK,CAAC,iBAAiB,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAC3C,OAAO,IAAI,CAAC,aAAa,EAAE,CAAC;IAC9B,CAAC;IAED,QAAQ;QACN,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;YAChB,MAAM,IAAI,KAAK,CAAC,6BAA6B,CAAC,CAAC;QACjD,CAAC;QACD,OAAO,IAAI,CAAC,SAAS,CAAC;YACpB,QAAQ,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,EAAE;YACnC,UAAU,EAAE,IAAI,CAAC,UAAU;SAC5B,CAAC,CAAC;IACL,CAAC;IAED,QAAQ,CAAC,UAAkB;QACzB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;QAC9B,IAAI,QAAQ,GAAG,UAAU,CAAC;QAC1B,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;YACtC,IAAI,MAAM,IAAI,OAAO,MAAM,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;gBAClD,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC;gBAC3B,IAAI,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,CAAC;oBACxC,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC;gBACnD,CAAC;YACH,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,kDAAkD;QACpD,CAAC;QACD,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QACpC,OAAO,IAAI,CAAC,aAAa,EAAE,CAAC;IAC9B,CAAC;IAEO,SAAS;QACf,MAAM,YAAY,GAAG,cAAI,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QAClD,IAAI,CAAC,IAAA,eAAU,EAAC,YAAY,CAAC,EAAE,CAAC;YAC9B,MAAM,IAAI,KAAK,CAAC,6BAA6B,YAAY,EAAE,CAAC,CAAC;QAC/D,CAAC;QAED,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,IAAA,iBAAY,EAAC,YAAY,EAAE,MAAM,CAAC,CAAC,CAAC;QACjE,OAAO,IAAI,aAAK,CAAC,SAAS,CAAC,CAAC;IAC9B,CAAC;IAEO,aAAa;QACnB,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;YAChB,MAAM,IAAI,KAAK,CAAC,qBAAqB,CAAC,CAAC;QACzC,CAAC;QAED,MAAM,UAAU,GAA6B,EAAE,CAAC;QAChD,MAAM,UAAU,GAAe,EAAE,CAAC;QAClC,MAAM,QAAQ,GAAe,EAAE,CAAC;QAEhC,OAAO,IAAI,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC;YAC9B,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC;YACtC,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;YAC1C,MAAM,IAAI,GAAG,IAAA,sBAAS,EAAC,IAAI,CAAC,KAAK,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC;YACrD,QAAQ,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC;YAEvB,IAAI;iBACD,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,KAAK,OAAO,IAAI,GAAG,CAAC,GAAG,KAAK,QAAQ,CAAC;iBAC5D,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;YAE1C,IAAI,IAAI,EAAE,CAAC;gBACT,UAAU,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;YAClC,CAAC;iBAAM,CAAC;gBACN,IAAI,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;YAC9C,CAAC;QACH,CAAC;QAED,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,MAAM,EAAgB,EAAE;YACrE,MAAM,IAAI,GAAG,IAAA,sBAAS,EAAC,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;YAC1C,MAAM,QAAQ,GAAG,IAAA,wBAAW,EAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;YAC7C,MAAM,MAAM,GAAG,IAAA,wBAAW,EAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,IAAA,wBAAW,EAAC,IAAI,EAAE,KAAK,CAAC,CAAC;YACvE,OAAO;gBACL,KAAK,EAAE,MAAM,CAAC,KAAK;gBACnB,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE;gBACtC,IAAI;gBACJ,QAAQ;gBACR,MAAM;aACP,CAAC;QACJ,CAAC,CAAC,CAAC;QACH,MAAM,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC;QACxD,MAAM,SAAS,GAA4B,EAAE,CAAC;QAE9C,IAAI,SAAS,KAAK,KAAK,EAAE,CAAC;YACxB,MAAM,QAAQ,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,KAAK,OAAO,CAAC,CAAC;YAC7D,MAAM,QAAQ,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,KAAK,OAAO,CAAC,CAAC;YAE7D,IAAI,CAAC,QAAQ,IAAI,CAAC,QAAQ,EAAE,CAAC;gBAC3B,MAAM,OAAO,GAAG,wDAAwD,CAAC;gBACzE,MAAM,iBAAiB,GAAa,EAAE,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC;gBACrE,UAAU,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;gBACnC,QAAQ,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;YACnC,CAAC;YAED,MAAM,aAAa,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,KAAK,OAAO,CAAC,CAAC;YAClE,MAAM,aAAa,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,KAAK,OAAO,CAAC,CAAC;YAClE,IAAI,aAAa,EAAE,CAAC;gBAClB,SAAS,CAAC,QAAQ,GAAG;oBACnB,IAAI,EAAE,OAAO;oBACb,OAAO,EAAE,aAAa,CAAC,KAAK,IAAI,aAAa,CAAC,KAAK;iBACpD,CAAC;YACJ,CAAC;iBAAM,IAAI,aAAa,EAAE,CAAC;gBACzB,MAAM,YAAY,GAAG,MAAM,CAAC,aAAa,EAAE,KAAK,CAAC,CAAC;gBAClD,IAAI,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC;oBAClC,SAAS,CAAC,KAAK,GAAG,YAAY,CAAC;gBACjC,CAAC;gBACD,SAAS,CAAC,QAAQ,GAAG;oBACnB,IAAI,EAAE,UAAU;oBAChB,OAAO,EAAE,aAAa,CAAC,KAAK,IAAI,aAAa,CAAC,KAAK;iBACpD,CAAC;YACJ,CAAC;QACH,CAAC;QAED,OAAO;YACL,MAAM,EAAE,IAAI,CAAC,UAAU,EAAE;YACzB,UAAU;YACV,OAAO;YACP,SAAS;YACT,UAAU,EAAE,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,SAAS;YAC1D,SAAS,EAAE,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS;SACrE,CAAC;IACJ,CAAC;CACF;AArJD,8BAqJC"}
\ No newline at end of file
diff --git a/dist/interfaces/turn-result.d.ts b/dist/interfaces/turn-result.d.ts
index c844010..7bb8c0d 100644
--- a/dist/interfaces/turn-result.d.ts
+++ b/dist/interfaces/turn-result.d.ts
@@ -26,6 +26,10 @@ export interface TurnResult {
score?: number;
moves?: number;
statusLine?: string;
+ endState?: {
+ type: 'intended' | 'error';
+ message?: string;
+ };
};
suggestions?: string[];
}
diff --git a/dist/interfaces/turn-result.js.map b/dist/interfaces/turn-result.js.map
index 6472681..b0d8ec6 100644
--- a/dist/interfaces/turn-result.js.map
+++ b/dist/interfaces/turn-result.js.map
@@ -1 +1 @@
-{"version":3,"file":"turn-result.js","sourceRoot":"","sources":["../../src/interfaces/turn-result.ts"],"names":[],"mappings":";;AAyCA,4CA6BC;AAtED;;GAEG;AACH,oDAA+C;AAsC/C,SAAgB,gBAAgB,CAAC,IAAY,EAAE,OAAmB,EAAE;IAClE,OAAO,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;SACtB,OAAO,CAAC,QAAQ,EAAE,IAAI,CAAC;SACvB,KAAK,CAAC,QAAQ,CAAC;SACf,GAAG,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC;SACpC,MAAM,CAAC,OAAO,CAAC;SACf,GAAG,CAAC,CAAC,SAAS,EAAE,EAAE;QACjB,MAAM,KAAK,GAAG,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACpC,MAAM,aAAa,GAAe,CAAC,GAAG,IAAI,CAAC,CAAC;QAC5C,MAAM,SAAS,GAAa,EAAE,CAAC;QAC/B,IAAI,aAAa,GAAG,IAAI,CAAC;QAEzB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;YAC5B,MAAM,QAAQ,GAAG,aAAa,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAA,qBAAQ,EAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;YAErF,IAAI,QAAQ,EAAE,CAAC;gBACb,aAAa,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YAC/B,CAAC;iBAAM,CAAC;gBACN,aAAa,GAAG,KAAK,CAAC;gBACtB,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACvB,CAAC;QACH,CAAC;QAED,OAAO;YACL,IAAI,EAAE,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE;YACjC,IAAI,EAAE,aAAa;SACpB,CAAC;IACJ,CAAC,CAAC,CAAC;AACP,CAAC"}
\ No newline at end of file
+{"version":3,"file":"turn-result.js","sourceRoot":"","sources":["../../src/interfaces/turn-result.ts"],"names":[],"mappings":";;AA6CA,4CA6BC;AA1ED;;GAEG;AACH,oDAA+C;AA0C/C,SAAgB,gBAAgB,CAAC,IAAY,EAAE,OAAmB,EAAE;IAClE,OAAO,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;SACtB,OAAO,CAAC,QAAQ,EAAE,IAAI,CAAC;SACvB,KAAK,CAAC,QAAQ,CAAC;SACf,GAAG,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC;SACpC,MAAM,CAAC,OAAO,CAAC;SACf,GAAG,CAAC,CAAC,SAAS,EAAE,EAAE;QACjB,MAAM,KAAK,GAAG,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACpC,MAAM,aAAa,GAAe,CAAC,GAAG,IAAI,CAAC,CAAC;QAC5C,MAAM,SAAS,GAAa,EAAE,CAAC;QAC/B,IAAI,aAAa,GAAG,IAAI,CAAC;QAEzB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;YAC5B,MAAM,QAAQ,GAAG,aAAa,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAA,qBAAQ,EAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;YAErF,IAAI,QAAQ,EAAE,CAAC;gBACb,aAAa,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YAC/B,CAAC;iBAAM,CAAC;gBACN,aAAa,GAAG,KAAK,CAAC;gBACtB,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACvB,CAAC;QACH,CAAC;QAED,OAAO;YACL,IAAI,EAAE,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE;YACjC,IAAI,EAAE,aAAa;SACpB,CAAC;IACJ,CAAC,CAAC,CAAC;AACP,CAAC"}
\ No newline at end of file
diff --git a/dist/utils/tag-parser.js b/dist/utils/tag-parser.js
index 72c458e..50e6ab7 100644
--- a/dist/utils/tag-parser.js
+++ b/dist/utils/tag-parser.js
@@ -25,6 +25,14 @@ function parseTag(raw) {
tag.param = bracketMatch[3].trim();
return tag;
}
+ const colonMatch = text.match(/^([A-Za-z][\w-]*)\s*:\s*(.*?)\s*(?:\(([^)]*)\))?$/);
+ if (colonMatch) {
+ const tag = { key: normalizeKey(colonMatch[1]) };
+ tag.value = colonMatch[2].trim();
+ if (typeof colonMatch[3] !== 'undefined')
+ tag.param = colonMatch[3].trim();
+ return tag;
+ }
const bareMatch = text.match(/^[A-Za-z][\w-]*$/);
if (bareMatch) {
return { key: normalizeKey(text) };
diff --git a/dist/utils/tag-parser.js.map b/dist/utils/tag-parser.js.map
index a968669..6115676 100644
--- a/dist/utils/tag-parser.js.map
+++ b/dist/utils/tag-parser.js.map
@@ -1 +1 @@
-{"version":3,"file":"tag-parser.js","sourceRoot":"","sources":["../../src/utils/tag-parser.ts"],"names":[],"mappings":";;AAaA,4BAkBC;AAED,8BAMC;AAED,kCAGC;AA1CD,MAAM,kBAAkB,GAA2B;IACjD,KAAK,EAAE,KAAK;IACZ,SAAS,EAAE,OAAO;IAClB,SAAS,EAAE,SAAS;CACrB,CAAC;AAEF,SAAS,YAAY,CAAC,GAAW;IAC/B,MAAM,UAAU,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,eAAe,EAAE,GAAG,CAAC,CAAC;IAC1E,OAAO,kBAAkB,CAAC,UAAU,CAAC,IAAI,UAAU,CAAC;AACtD,CAAC;AAED,SAAgB,QAAQ,CAAC,GAAW;IAClC,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;IAC3D,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IAEvB,MAAM,YAAY,GAAG,IAAI,CAAC,KAAK,CAAC,qDAAqD,CAAC,CAAC;IACvF,IAAI,YAAY,EAAE,CAAC;QACjB,MAAM,GAAG,GAAa,EAAE,GAAG,EAAE,YAAY,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC7D,IAAI,OAAO,YAAY,CAAC,CAAC,CAAC,KAAK,WAAW;YAAE,GAAG,CAAC,KAAK,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAC/E,IAAI,OAAO,YAAY,CAAC,CAAC,CAAC,KAAK,WAAW;YAAE,GAAG,CAAC,KAAK,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAC/E,OAAO,GAAG,CAAC;IACb,CAAC;IAED,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,kBAAkB,CAAC,CAAC;IACjD,IAAI,SAAS,EAAE,CAAC;QACd,OAAO,EAAE,GAAG,EAAE,YAAY,CAAC,IAAI,CAAC,EAAE,CAAC;IACrC,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAgB,SAAS,CAAC,OAA8B;IACtD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC;QAAE,OAAO,EAAE,CAAC;IAEvC,OAAO,OAAO;SACX,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,IAAI,EAAE,CAAC,CAAC,CAAC;SACzC,MAAM,CAAC,CAAC,GAAG,EAAmB,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;AACpD,CAAC;AAED,SAAgB,WAAW,CAAC,IAAgB,EAAE,GAAW;IACvD,MAAM,aAAa,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC;IACxC,OAAO,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,KAAK,aAAa,CAAC,EAAE,KAAK,CAAC;AAC9D,CAAC"}
\ No newline at end of file
+{"version":3,"file":"tag-parser.js","sourceRoot":"","sources":["../../src/utils/tag-parser.ts"],"names":[],"mappings":";;AAaA,4BA0BC;AAED,8BAMC;AAED,kCAGC;AAlDD,MAAM,kBAAkB,GAA2B;IACjD,KAAK,EAAE,KAAK;IACZ,SAAS,EAAE,OAAO;IAClB,SAAS,EAAE,SAAS;CACrB,CAAC;AAEF,SAAS,YAAY,CAAC,GAAW;IAC/B,MAAM,UAAU,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,eAAe,EAAE,GAAG,CAAC,CAAC;IAC1E,OAAO,kBAAkB,CAAC,UAAU,CAAC,IAAI,UAAU,CAAC;AACtD,CAAC;AAED,SAAgB,QAAQ,CAAC,GAAW;IAClC,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;IAC3D,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IAEvB,MAAM,YAAY,GAAG,IAAI,CAAC,KAAK,CAAC,qDAAqD,CAAC,CAAC;IACvF,IAAI,YAAY,EAAE,CAAC;QACjB,MAAM,GAAG,GAAa,EAAE,GAAG,EAAE,YAAY,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC7D,IAAI,OAAO,YAAY,CAAC,CAAC,CAAC,KAAK,WAAW;YAAE,GAAG,CAAC,KAAK,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAC/E,IAAI,OAAO,YAAY,CAAC,CAAC,CAAC,KAAK,WAAW;YAAE,GAAG,CAAC,KAAK,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAC/E,OAAO,GAAG,CAAC;IACb,CAAC;IAED,MAAM,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,mDAAmD,CAAC,CAAC;IACnF,IAAI,UAAU,EAAE,CAAC;QACf,MAAM,GAAG,GAAa,EAAE,GAAG,EAAE,YAAY,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC3D,GAAG,CAAC,KAAK,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QACjC,IAAI,OAAO,UAAU,CAAC,CAAC,CAAC,KAAK,WAAW;YAAE,GAAG,CAAC,KAAK,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAC3E,OAAO,GAAG,CAAC;IACb,CAAC;IAED,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,kBAAkB,CAAC,CAAC;IACjD,IAAI,SAAS,EAAE,CAAC;QACd,OAAO,EAAE,GAAG,EAAE,YAAY,CAAC,IAAI,CAAC,EAAE,CAAC;IACrC,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAgB,SAAS,CAAC,OAA8B;IACtD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC;QAAE,OAAO,EAAE,CAAC;IAEvC,OAAO,OAAO;SACX,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,IAAI,EAAE,CAAC,CAAC,CAAC;SACzC,MAAM,CAAC,CAAC,GAAG,EAAmB,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;AACpD,CAAC;AAED,SAAgB,WAAW,CAAC,IAAgB,EAAE,GAAW;IACvD,MAAM,aAAa,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC;IACxC,OAAO,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,KAAK,aAAa,CAAC,EAAE,KAAK,CAAC;AAC9D,CAAC"}
\ No newline at end of file
diff --git a/ink_inclusion.md b/ink_inclusion.md
index 9dbbb86..0850fea 100644
--- a/ink_inclusion.md
+++ b/ink_inclusion.md
@@ -56,7 +56,7 @@ 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.
+Ink choice text uses square brackets for its own display/control syntax, but choice-local tags still use the normal tag syntax on their own tag lines below the choice. The currently implemented parser accepts `# letter[o]` and `# action[examine]`; the earlier prototype-style `# key: value` notation is not part of the active syntax.
| Tag | Scope | Meaning |
|---|---|---|
diff --git a/public/THIRD_PARTY_NOTICES.md b/public/THIRD_PARTY_NOTICES.md
new file mode 100644
index 0000000..76635e6
--- /dev/null
+++ b/public/THIRD_PARTY_NOTICES.md
@@ -0,0 +1,116 @@
+# Third-Party Notices
+
+This application includes or interfaces with the following third-party libraries, tools, fonts, and services.
+
+## Browser-vendored libraries
+
+| Component | Local files | Local version | Latest checked | License | Status |
+| --- | --- | --- | --- | --- | --- |
+| SmartyPants.js | `public/js/smartypants.js` | Header says 0.0.6 | npm `smartypants` 0.2.2 | BSD-3-Clause | Local file is not identical to npm `smartypants` 0.0.5, 0.0.9, or 0.2.2. It appears to be an older browser bundle with local or unreleased changes. |
+| Hyphenopoly | `public/js/Hyphenopoly.js`, `public/js/Hyphenopoly_Loader.js`, `public/js/hyphenopoly.module.js`, `public/js/patterns/*.wasm` | Browser file header says 5.2.0-beta.1 | npm `hyphenopoly` 6.1.0 | MIT | `Hyphenopoly.js` matches the 5.2.0-beta.1 npm file after line-ending normalization. Loader differs by a small local/prototype change. Package dependency is 6.0.0, so the browser vendored copy is older than both the installed package and latest npm. |
+| Knuth-Plass line breaking adapter | `public/js/knuth-and-plass.js` | No upstream version header | Unknown | Unknown/inherited prototype code | Local file differs from the prototype file and is application-adapted. Exact upstream could not be identified from file headers or npm metadata. |
+| Line breaking support | `public/js/linebreak.js`, `public/js/linked-list.js` | No upstream version header | Unknown | Unknown/inherited prototype code | Files are identical to the prototype copies. Exact upstream could not be identified from file headers. |
+| Kokoro JS browser bundle | `public/js/kokoro-js.js` | 1.2.0 | npm `kokoro-js` 1.2.1 | Apache-2.0 | Local file is byte-identical to `kokoro-js` 1.2.0 `dist/kokoro.web.js`; not latest. |
+
+## Direct npm runtime dependencies
+
+| Package | Installed | Latest checked | License | Credit |
+| --- | --- | --- | --- | --- |
+| `inkjs` | 2.4.0 | 2.4.0 | MIT | Yannick Lohse; based on ink by Inkle |
+| `hyphenopoly` | 6.0.0 | 6.1.0 | MIT | Mathias Nater |
+| `kokoro-js` | 1.2.0 | 1.2.1 | Apache-2.0 | hexgrad, Xenova |
+| `ifvms` | 1.1.6 | 1.1.6 | MIT | Dannii Willis |
+| `openai` | 4.91.0 | 6.38.0 | Apache-2.0 | OpenAI |
+| `socket.io` | 4.8.1 | 4.8.3 | MIT | Socket.IO contributors |
+| `express` | 5.1.0 | 5.2.1 | MIT | Express contributors |
+| `axios` | 1.8.4 | 1.16.1 | MIT | Axios contributors |
+| `cors` | 2.8.5 | 2.8.6 | MIT | Troy Goode |
+| `dotenv` | 16.4.7 | 17.4.2 | BSD-2-Clause | dotenv contributors |
+| `js-yaml` | 4.1.0 | 4.1.1 | MIT | Vladimir Zapparov and contributors |
+
+## Fonts and services
+
+| Component | Use | License/Credit |
+| --- | --- | --- |
+| EB Garamond | UI and book text font | SIL Open Font License 1.1 |
+| OpenAI / ChatGPT / Codex / GPT-image-2 | Coding assistance, writing assistance, generated images | OpenAI |
+| Claude Code | Coding assistance | Anthropic |
+| Suno | Music generation | Suno |
+
+## Creative credits
+
+Produced by Bad Tools Studio.
+
+Runtime server programming: Georg Tomitsch, OpenAI Codex
+
+Game engine: ink by Inkle; inkjs by Yannick Lohse
+
+Client and UI programming: Georg Tomitsch, OpenAI Codex, Claude Code
+
+UI visual design: Georg Tomitsch
+
+Story: Georg Tomitsch
+
+Writing: Georg Tomitsch, ChatGPT
+
+Music: Georg Tomitsch, Suno
+
+Art direction: Georg Tomitsch
+
+Images: OpenAI GPT-image-2
+
+## Links
+
+- Inkle ink: https://www.inklestudios.com/ink/
+- inkjs: https://www.npmjs.com/package/inkjs
+- SmartyPants.js: https://github.com/othree/smartypants.js
+- Hyphenopoly: https://mnater.github.io/Hyphenopoly/
+- Kokoro JS: https://github.com/hexgrad/kokoro
+- ifvms.js: https://github.com/curiousdannii/ifvms.js
+- OpenAI: https://openai.com/
+- ChatGPT: https://chatgpt.com/
+- Claude Code: https://www.anthropic.com/claude-code
+- Suno: https://suno.com/
+
+## License Texts
+
+### MIT License
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of software and associated documentation files licensed under the MIT License, to deal in the software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the software, and to permit persons to whom the software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+### BSD 2-Clause License
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the copyright notice, this list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+
+### BSD 3-Clause License
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the copyright notice, this list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+
+### Apache License 2.0
+
+Licensed under the Apache License, Version 2.0. You may not use files licensed under the Apache License except in compliance with the License. You may obtain a copy of the License at:
+
+https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software distributed under the Apache License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+
+### SIL Open Font License 1.1
+
+EB Garamond is distributed under the SIL Open Font License, Version 1.1. The license permits use, study, modification and redistribution of the font, with reserved font name restrictions and the requirement that derivative fonts remain under the same license.
+
+Full license: https://openfontlicense.org/
diff --git a/public/css/style.css b/public/css/style.css
index c9f9617..1b9b2e5 100644
--- a/public/css/style.css
+++ b/public/css/style.css
@@ -116,17 +116,17 @@ body.switched {
--book-height: 799px;
--book-scale: 1;
--page-line-count: 25;
- --book-page-top: 7.35%;
- --book-page-bottom: 14.15%;
+ --book-page-top: 4.00%;
+ --book-page-bottom: 19%;
--book-page-height: calc(100% - var(--book-page-top) - var(--book-page-bottom));
- --book-left-page-left: 10.7%;
- --book-left-page-width: 33.75%;
- --book-right-page-right: 14.85%;
- --book-right-page-width: 32.65%;
+ --book-left-page-left: 14%;
+ --book-left-page-width: 34.9%;
+ --book-right-page-right: 14%;
+ --book-right-page-width: 34%;
--book-page-perspective: 3200px;
--book-left-page-transform: none;
--book-right-page-transform: none;
- --story-line-height: calc((var(--book-height) * 0.785) / var(--page-line-count));
+ --story-line-height: calc((var(--book-height) * 0.77) / var(--page-line-count));
--story-font-size: calc(var(--story-line-height) / 1.45);
font-size: calc(var(--book-height)/(34 * 1.5));
--img-aspect-ratio: 1.779;
@@ -342,13 +342,14 @@ ol.choice {
position: absolute;
right: 0;
left: 0;
- top: -0.6rem;
+ top: 0.45rem;
user-select: none;
transition: color 0.6s, background 0.6s;
+ font-size: 0.82rem;
}
#controls [disabled] {
- color: #999;
+ color: #8f806a;
}
#controls input[type=range] {
@@ -458,7 +459,8 @@ ol.choice {
margin: 0 auto;
padding: 0;
overflow: hidden;
- background: transparent;
+ background: rgba(218, 188, 130, 0.18);
+ background-blend-mode: multiply;
}
.story-image-block img {
@@ -466,8 +468,9 @@ ol.choice {
width: 100%;
height: 100%;
object-fit: contain;
- mix-blend-mode: multiply;
- filter: contrast(1.05);
+ mix-blend-mode: color-burn;
+ /* filter: grayscale(0.72) sepia(0.18) contrast(1.22) brightness(0.96); */
+ opacity: 0.94;
}
.story-image-landscape,
@@ -641,7 +644,7 @@ ol.choice {
top: 0px;
left: 0px;
border: 1px none red;
- background-color: #fff;
+ background-color: #d8bf89;
box-shadow: 2px 2px 2px rgba(0,0,0,0.3);
}
@@ -693,28 +696,243 @@ ol.choice {
color: rgba(0, 0, 0, 0.62);
}
-#lighting {
- position: absolute;
- top: -35%;
- left: -35%;
- width: 180%;
- height: 180%;
- animation: gradient-animation-shrink 1s 1;
- background: radial-gradient(circle, rgba(255,240,182,0.1) 0%, rgba(255,237,165,0.2) 20%, rgba(0,0,0,0.9) 65%, rgba(0,0,0,0.9) 100%);
- mix-blend-mode: color-burn;
- pointer-events: none; /* makes the element ignore mouse events, and pass them to elements underneath */
- z-index: 999; /* should be high enough to be on top of other elements */
+#credits_button {
+ border: 0;
+ padding: 0;
+ margin: 0;
+ background: transparent;
+ color: rgba(0, 0, 0, 0.72);
+ font: inherit;
+ font-style: italic;
+ cursor: pointer;
+ pointer-events: auto;
}
-@keyframes gradient-animation-grow {
- 0% { width: 180%; height: 180%; left: -35%; top: -35%; }
- 100% { width: 170%; height: 170%; left: -33%; top: -33%; }
-}
+#credits_button:hover {
+ color: rgba(0, 0, 0, 0.95);
+}
-@keyframes gradient-animation-shrink {
- 0% { width: 170%; height: 170%; left: -33%; top: -33%; }
- 100% { width: 180%; height: 180%; left: -35%; top: -35%; }
-}
+.credits-modal {
+ position: fixed;
+ inset: 0;
+ z-index: 1200;
+ display: none;
+ align-items: center;
+ justify-content: center;
+ padding: 4vh 4vw;
+ background: rgba(10, 7, 4, 0.52);
+}
+
+.credits-modal.visible {
+ display: flex;
+}
+
+.credits-dialog {
+ width: min(48rem, 92vw);
+ height: min(44rem, 88vh);
+ background: rgba(229, 205, 155, 0.96);
+ color: rgba(24, 18, 12, 0.92);
+ border: 1px solid rgba(42, 31, 18, 0.45);
+ box-shadow: 0 1.2rem 3rem rgba(0, 0, 0, 0.45);
+ padding: 1.2rem;
+ display: flex;
+ flex-direction: column;
+ gap: 0.85rem;
+}
+
+.credits-dialog-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 1rem;
+ border-bottom: 1px solid rgba(42, 31, 18, 0.28);
+ padding-bottom: 0.55rem;
+}
+
+.credits-dialog-header h2 {
+ margin: 0;
+ font-size: 1.35rem;
+ font-style: italic;
+}
+
+#credits_close {
+ border: 1px solid rgba(42, 31, 18, 0.5);
+ background: rgba(255, 246, 220, 0.35);
+ color: rgba(24, 18, 12, 0.9);
+ font: inherit;
+ padding: 0.18rem 0.7rem;
+ cursor: pointer;
+}
+
+.credits-logo-row {
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 0.55rem;
+}
+
+.credits-logo-row a {
+ color: rgba(31, 23, 15, 0.88);
+ text-decoration: none;
+}
+
+.credits-logo-row img {
+ display: block;
+ width: 1.3rem;
+ height: 1.3rem;
+}
+
+.credits-wordmark {
+ border: 1px solid rgba(42, 31, 18, 0.34);
+ padding: 0.08rem 0.45rem;
+ background: rgba(255, 246, 220, 0.24);
+ font-style: italic;
+}
+
+.story-popup-modal {
+ position: fixed;
+ inset: 0;
+ z-index: 1250;
+ display: none;
+ align-items: center;
+ justify-content: center;
+ padding: 4vh 4vw;
+ background: rgba(10, 7, 4, 0.42);
+}
+
+.story-popup-modal.visible {
+ display: flex;
+}
+
+.story-popup-dialog {
+ width: min(30rem, 88vw);
+ max-height: min(28rem, 80vh);
+ overflow: auto;
+ background: rgba(228, 202, 149, 0.97);
+ color: rgba(22, 16, 10, 0.94);
+ border: 1px solid rgba(42, 31, 18, 0.5);
+ box-shadow: 0 1.1rem 2.6rem rgba(0, 0, 0, 0.42);
+ padding: 1.15rem 1.3rem 1rem;
+ display: flex;
+ flex-direction: column;
+ gap: 0.8rem;
+ font-family: 'EB Garamond', var(--book-font), serif;
+}
+
+.story-popup-dialog h2 {
+ margin: 0;
+ font-size: 1.45rem;
+ font-style: italic;
+ line-height: 1.15;
+}
+
+#story_popup_message {
+ white-space: pre-wrap;
+ line-height: 1.35;
+ font-size: 1.05rem;
+}
+
+#story_popup_ok {
+ align-self: flex-end;
+ border: 1px solid rgba(42, 31, 18, 0.5);
+ background: rgba(255, 246, 220, 0.36);
+ color: rgba(24, 18, 12, 0.92);
+ font: inherit;
+ padding: 0.18rem 0.85rem;
+ cursor: pointer;
+}
+
+.credits-content {
+ flex: 1;
+ overflow: auto;
+ margin: 0;
+ padding: 0.8rem;
+ background: rgba(255, 246, 220, 0.2);
+ border: 1px solid rgba(42, 31, 18, 0.22);
+ white-space: pre-wrap;
+ font-family: "EB Garamond", serif;
+ font-size: 0.92rem;
+ line-height: 1.28;
+}
+
+#lighting {
+ position: fixed;
+ inset: 0;
+ pointer-events: none;
+ z-index: 999;
+ overflow: hidden;
+}
+
+#lighting::before,
+#lighting::after {
+ content: "";
+ position: absolute;
+ inset: -18%;
+ pointer-events: none;
+}
+
+#lighting::before {
+ background:
+ radial-gradient(circle at 16% 5%,
+ rgba(255, 214, 150, 0.36) 0%,
+ rgba(210, 126, 64, 0.20) 14%,
+ rgba(120, 65, 38, 0.08) 34%,
+ rgba(60, 36, 24, 0.025) 58%,
+ rgba(60, 36, 24, 0) 76%);
+ mix-blend-mode: soft-light;
+ opacity: 0.42;
+ animation: candle-flicker 6.8s infinite ease-in-out;
+ will-change: opacity, transform, filter;
+}
+
+#lighting::after {
+ background:
+ radial-gradient(ellipse at center,
+ rgba(0, 0, 0, 0) 0%,
+ rgba(0, 0, 0, 0.035) 50%,
+ rgba(0, 0, 0, 0.18) 100%);
+ mix-blend-mode: multiply;
+ opacity: 0.24;
+}
+
+@keyframes candle-flicker {
+ 0% {
+ opacity: 0.36;
+ filter: brightness(0.99) sepia(0.08) saturate(1.02);
+ transform: translate3d(0, 0, 0) scale(1);
+ }
+ 18% {
+ opacity: 0.46;
+ filter: brightness(1.06) sepia(0.12) saturate(1.07);
+ transform: translate3d(0.18%, -0.08%, 0) scale(1.008);
+ }
+ 31% {
+ opacity: 0.39;
+ filter: brightness(1.01) sepia(0.09) saturate(1.04);
+ transform: translate3d(-0.08%, 0.08%, 0) scale(1.003);
+ }
+ 49% {
+ opacity: 0.5;
+ filter: brightness(1.08) sepia(0.14) saturate(1.08);
+ transform: translate3d(0.24%, -0.13%, 0) scale(1.011);
+ }
+ 64% {
+ opacity: 0.41;
+ filter: brightness(1.02) sepia(0.1) saturate(1.04);
+ transform: translate3d(-0.12%, 0.1%, 0) scale(1.004);
+ }
+ 81% {
+ opacity: 0.47;
+ filter: brightness(1.065) sepia(0.13) saturate(1.07);
+ transform: translate3d(0.14%, -0.06%, 0) scale(1.008);
+ }
+ 100% {
+ opacity: 0.36;
+ filter: brightness(1) sepia(0.08) saturate(1.02);
+ transform: translate3d(0, 0, 0) scale(1);
+ }
+}
+
/* Command history */
#command_history {
@@ -723,10 +941,10 @@ ol.choice {
font-size: 1rem;
line-height: 1.2;
margin-bottom: 1rem;
- border-top: 1px solid #d1c8b9;
+ border-top: 1px solid #b69b68;
padding-top: 0.6rem;
scrollbar-width: thin;
- scrollbar-color: #8b7765 rgba(255, 255, 255, 0.1);
+ scrollbar-color: #7b654a rgba(151, 111, 64, 0.12);
}
body:not([data-game-running="true"]) #command_history {
@@ -754,11 +972,11 @@ body:not([data-game-running="true"]) #command_history {
}
#command_history::-webkit-scrollbar-track {
- background: rgba(255, 255, 255, 0.1);
+ background: rgba(151, 111, 64, 0.12);
}
#command_history::-webkit-scrollbar-thumb {
- background-color: #8b7765;
+ background-color: #7b654a;
border-radius: 4px;
}
@@ -918,7 +1136,7 @@ html[data-process-state="playing-ready"] * {
/* Placeholder styling - lighter and italic, with padding to avoid cursor overlap */
#player_input::placeholder {
- color: #aaa;
+ color: #8f806a;
font-style: italic;
padding-left: 15px; /* Add padding to move placeholder text to the right */
}
@@ -1044,7 +1262,7 @@ body:not([data-game-running="true"]) #command_input {
#start_prompt {
position: absolute;
inset: 0;
- display: none;
+ display: flex;
align-items: center;
justify-content: center;
text-align: center;
@@ -1053,11 +1271,13 @@ body:not([data-game-running="true"]) #command_input {
font-size: 34px;
font-style: italic;
color: rgba(45, 34, 24, 0.78);
+ opacity: 0;
pointer-events: none;
+ transition: opacity 260ms ease;
}
body:not([data-game-running="true"]) #start_prompt {
- display: flex;
+ opacity: 1;
}
/* Options Modal Styling */
@@ -1214,6 +1434,43 @@ body:not([data-game-running="true"]) #start_prompt {
text-align: right;
}
+.option-item .game-language-value {
+ min-width: 8rem;
+ text-align: right;
+ font-style: italic;
+}
+
+.volume-option {
+ justify-content: flex-start;
+}
+
+.volume-option label {
+ flex: 1 1 auto;
+}
+
+.volume-toggle {
+ width: 1.7rem;
+ height: 1.4rem;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border: 1px solid black;
+ border-radius: 0.25rem;
+ background: transparent;
+ color: rgba(0,0,0,0.9);
+ cursor: pointer;
+ font-size: 0.85rem;
+ line-height: 1;
+}
+
+.volume-toggle:hover {
+ background-color: rgba(0,0,0,0.06);
+}
+
+.volume-toggle.is-muted {
+ opacity: 0.55;
+}
+
.provider-status-list {
margin: 0.5rem 0 1rem;
border: 1px solid #e9ddc8;
diff --git a/public/images/README.md b/public/images/README.md
index 3aaf264..d1b55f4 100644
--- a/public/images/README.md
+++ b/public/images/README.md
@@ -7,7 +7,8 @@ Image block markup:
```text
#image[image-name.jpg](landscape)
-#image[image-name.jpg](portrait)
+#image[image-name.jpg](portrait pause=2)
+#image[image-name.jpg](square lead=1.5)
```
Sizes:
@@ -16,7 +17,9 @@ Sizes:
- `portrait`: 16:9, half page width, height snapped to whole line heights, with following prose flowing beside it.
- `square`: 1:1, centered, near full page width, height snapped to whole line heights.
-Image markup is parsed and queued by the story markup system, but final image rendering is still future work. Keep assets ready for that renderer by using browser-friendly formats such as `.jpg`, `.png`, `.webp`, or `.avif`.
+Images are inserted as story blocks, saved in browser history, restored on load/history scrolling, and revealed after the page scrolls to their line-snapped position. Optional `pause=`, `delay=`, `lead=`, or bare seconds such as `2s` delay the next spoken paragraph; the pause is skippable and does not block background TTS preparation.
+
+Use browser-friendly formats such as `.jpg`, `.png`, `.webp`, or `.avif`.
Use file names that are stable and story-facing, for example `mansion-door-rain.webp` rather than temporary export names.
diff --git a/public/images/mat.png b/public/images/mat.png
index 97c0942..5403566 100644
Binary files a/public/images/mat.png and b/public/images/mat.png differ
diff --git a/public/js/api-tts-module-base.js b/public/js/api-tts-module-base.js
index 2cc5323..bad06f3 100644
--- a/public/js/api-tts-module-base.js
+++ b/public/js/api-tts-module-base.js
@@ -9,7 +9,7 @@ export class ApiTTSModuleBase extends TTSHandlerModule {
super(id, name);
// Declare proper dependencies according to architecture principles
- this.dependencies = ['persistence-manager', 'localization'];
+ this.dependencies = ['persistence-manager', 'localization', 'game-config'];
// Basic voice options
this.voiceOptions = {
@@ -86,7 +86,7 @@ export class ApiTTSModuleBase extends TTSHandlerModule {
return;
}
- if (['masterVolume', 'ttsVolume', 'master_volume', 'tts_volume'].includes(key)) {
+ if (['masterVolume', 'ttsVolume', 'masterVolumeEnabled', 'ttsVolumeEnabled', 'master_volume', 'tts_volume'].includes(key)) {
this.applyCurrentVolume();
}
});
@@ -129,9 +129,9 @@ export class ApiTTSModuleBase extends TTSHandlerModule {
*/
async setupVoiceFromPreferences() {
const persistenceManager = this.getModule('persistence-manager');
- const localization = this.getModule('localization');
+ const gameConfig = this.getModule('game-config');
- if (!persistenceManager || !localization) {
+ if (!persistenceManager) {
console.error(`${this.name}: Required dependencies not found`);
return false;
}
@@ -140,7 +140,7 @@ export class ApiTTSModuleBase extends TTSHandlerModule {
const preferredVoiceId = persistenceManager.getPreference('tts', `${this.id}_voice`, '');
// Get current locale
- const currentLocale = localization.getLocale();
+ const currentLocale = gameConfig?.getLocale?.() || 'en_US';
// If we have a preferred voice ID, use it
if (preferredVoiceId && this.voices.some(v => v.id === preferredVoiceId)) {
@@ -194,7 +194,7 @@ export class ApiTTSModuleBase extends TTSHandlerModule {
* @param {string} text - The text to synthesize.
* @returns {Promise} - A promise that resolves with the audio data object.
*/
- async generateSpeechAudio(text) {
+ async generateSpeechAudio(text, options = {}) {
// To be implemented by subclasses
return { success: false, reason: 'not_implemented' };
}
@@ -206,10 +206,11 @@ export class ApiTTSModuleBase extends TTSHandlerModule {
* @returns {Promise} - Resolves when audio finishes playing
*/
async speakPreloaded(preloadData, callback = null) {
+ const completionCallback = typeof callback === 'function' ? callback : null;
if (!preloadData || !preloadData.audioData) {
console.error(`${this.name}: Invalid preloaded data`);
const result = { success: false, reason: 'invalid_data' };
- if (callback) callback(result);
+ if (completionCallback) completionCallback(result);
return result;
}
@@ -242,7 +243,7 @@ export class ApiTTSModuleBase extends TTSHandlerModule {
}
URL.revokeObjectURL(audioUrl);
- if (callback) callback(result);
+ if (completionCallback) completionCallback(result);
resolve(result);
};
this.currentPlaybackFinish = finish;
@@ -284,13 +285,15 @@ export class ApiTTSModuleBase extends TTSHandlerModule {
'masterVolume',
persistenceManager.getPreference('audio', 'master_volume', 1.0)
);
+ const masterEnabled = persistenceManager.getPreference('audio', 'masterVolumeEnabled', true) !== false;
const ttsVolume = persistenceManager.getPreference(
'audio',
'ttsVolume',
persistenceManager.getPreference('audio', 'tts_volume', 1.0)
);
+ const ttsEnabled = persistenceManager.getPreference('audio', 'ttsVolumeEnabled', true) !== false;
- return Math.max(0, Math.min(1, masterVolume * ttsVolume));
+ return Math.max(0, Math.min(1, (masterEnabled ? masterVolume : 0) * (ttsEnabled ? ttsVolume : 0)));
}
/**
@@ -418,14 +421,14 @@ export class ApiTTSModuleBase extends TTSHandlerModule {
* @param {string} text - Text to preload
* @returns {Promise} - Preloaded speech data
*/
- async preloadSpeech(text) {
+ async preloadSpeech(text, options = {}) {
if (!this.isReady) {
return { success: false, reason: 'not_ready' };
}
try {
// Generate speech
- const result = await this.generateSpeechAudio(text);
+ const result = await this.generateSpeechAudio(text, options);
if (!result.success) {
return { success: false, reason: 'generation_failed' };
diff --git a/public/js/audio-manager-module.js b/public/js/audio-manager-module.js
index 7b7ffcd..44fa6b8 100644
--- a/public/js/audio-manager-module.js
+++ b/public/js/audio-manager-module.js
@@ -10,6 +10,7 @@ class AudioManagerModule extends BaseModule {
this.sounds = new Map();
this.sfxCache = new Map();
this.currentAudio = null;
+ this.currentAudioRole = null;
this.currentLoop = null;
this.currentMusic = null;
this.queuedMusic = null;
@@ -17,6 +18,12 @@ class AudioManagerModule extends BaseModule {
this.musicVolume = 1.0;
this.sfxVolume = 1.0;
this.ttsVolume = 1.0;
+ this.masterVolumeEnabled = true;
+ this.musicVolumeEnabled = true;
+ this.sfxVolumeEnabled = true;
+ this.ttsVolumeEnabled = true;
+ this.musicDuckingAmount = 0.3;
+ this.musicDuckingEnabled = true;
this.musicDuckingFactor = 1.0;
this.musicFadeToken = 0;
this.activeTtsPlaybackCount = 0;
@@ -79,6 +86,12 @@ class AudioManagerModule extends BaseModule {
this.musicVolume = this.clampVolume(persistenceManager.getPreference('audio', 'musicVolume', this.musicVolume));
this.sfxVolume = this.clampVolume(persistenceManager.getPreference('audio', 'sfxVolume', this.sfxVolume));
this.ttsVolume = this.clampVolume(persistenceManager.getPreference('audio', 'ttsVolume', this.ttsVolume));
+ this.masterVolumeEnabled = persistenceManager.getPreference('audio', 'masterVolumeEnabled', this.masterVolumeEnabled) !== false;
+ this.musicVolumeEnabled = persistenceManager.getPreference('audio', 'musicVolumeEnabled', this.musicVolumeEnabled) !== false;
+ this.sfxVolumeEnabled = persistenceManager.getPreference('audio', 'sfxVolumeEnabled', this.sfxVolumeEnabled) !== false;
+ this.ttsVolumeEnabled = persistenceManager.getPreference('audio', 'ttsVolumeEnabled', this.ttsVolumeEnabled) !== false;
+ this.musicDuckingAmount = this.clampVolume(persistenceManager.getPreference('audio', 'musicDuckingAmount', this.musicDuckingAmount));
+ this.musicDuckingEnabled = persistenceManager.getPreference('audio', 'musicDuckingEnabled', this.musicDuckingEnabled) !== false;
}
setupEventListeners() {
@@ -108,6 +121,12 @@ class AudioManagerModule extends BaseModule {
if (key === 'musicVolume') this.setMusicVolume(value);
if (key === 'sfxVolume') this.setSfxVolume(value);
if (key === 'ttsVolume') this.setTtsVolume(value);
+ if (key === 'masterVolumeEnabled') this.setVolumeEnabled('master', value);
+ if (key === 'musicVolumeEnabled') this.setVolumeEnabled('music', value);
+ if (key === 'sfxVolumeEnabled') this.setVolumeEnabled('sfx', value);
+ if (key === 'ttsVolumeEnabled') this.setVolumeEnabled('tts', value);
+ if (key === 'musicDuckingAmount') this.setMusicDuckingAmount(value);
+ if (key === 'musicDuckingEnabled') this.setMusicDuckingEnabled(value);
});
this.addEventListener(document, 'tts:playback-start', () => {
@@ -204,6 +223,7 @@ class AudioManagerModule extends BaseModule {
this.currentLoop = audio;
} else {
this.currentAudio = audio.cloneNode(true);
+ this.currentAudioRole = 'sfx';
this.setMediaVolume(this.currentAudio, this.getSfxVolume());
this.currentAudio.play().catch(error => {
console.error('Error playing audio:', error);
@@ -241,6 +261,7 @@ class AudioManagerModule extends BaseModule {
return this.currentLoop;
} else {
this.currentAudio = new Audio(url);
+ this.currentAudioRole = 'sfx';
this.setMediaVolume(this.currentAudio, this.getSfxVolume());
this.currentAudio.play().catch(error => {
console.error('Error playing audio:', error);
@@ -269,6 +290,7 @@ class AudioManagerModule extends BaseModule {
this.currentAudio.pause();
this.currentAudio.currentTime = 0;
this.currentAudio = null;
+ this.currentAudioRole = null;
}
if (this.currentLoop) {
@@ -306,6 +328,7 @@ class AudioManagerModule extends BaseModule {
*/
setTtsVolume(volume) {
this.ttsVolume = this.clampVolume(volume);
+ this.updateVolumes();
}
/**
@@ -325,6 +348,33 @@ class AudioManagerModule extends BaseModule {
this.sfxVolume = this.clampVolume(volume);
this.updateVolumes();
}
+
+ setVolumeEnabled(kind, enabled) {
+ const value = enabled !== false;
+ if (kind === 'master') this.masterVolumeEnabled = value;
+ if (kind === 'music') this.musicVolumeEnabled = value;
+ if (kind === 'sfx') this.sfxVolumeEnabled = value;
+ if (kind === 'tts') this.ttsVolumeEnabled = value;
+ this.updateVolumes();
+ }
+
+ setMusicDuckingAmount(amount) {
+ this.musicDuckingAmount = this.clampVolume(amount);
+ if (this.musicDuckingFactor !== 1.0) {
+ this.duckMusicForSpeech();
+ } else {
+ this.updateVolumes();
+ }
+ }
+
+ setMusicDuckingEnabled(enabled) {
+ this.musicDuckingEnabled = enabled !== false;
+ if (this.musicDuckingFactor !== 1.0) {
+ this.duckMusicForSpeech();
+ } else {
+ this.updateVolumes();
+ }
+ }
/**
* Update all volume levels based on current settings
@@ -332,11 +382,11 @@ class AudioManagerModule extends BaseModule {
updateVolumes() {
this.sounds.forEach(audio => {
const isMusic = audio.loop;
- this.setMediaVolume(audio, this.masterVolume * (isMusic ? this.musicVolume : this.sfxVolume));
+ this.setMediaVolume(audio, isMusic ? this.getMusicVolume() : this.getSfxVolume());
});
if (this.currentAudio) {
- this.setMediaVolume(this.currentAudio, this.masterVolume * this.sfxVolume);
+ this.setMediaVolume(this.currentAudio, this.currentAudioRole === 'tts' ? this.getTtsVolume() : this.getSfxVolume());
}
if (this.currentLoop) {
@@ -358,20 +408,29 @@ class AudioManagerModule extends BaseModule {
}
getSfxVolume() {
- return this.masterVolume * this.sfxVolume;
+ return this.getMasterVolume() * (this.sfxVolumeEnabled ? this.sfxVolume : 0);
}
getMusicVolume() {
- return this.masterVolume * this.musicVolume * this.musicDuckingFactor;
+ return this.getUnduckedMusicVolume() * this.musicDuckingFactor;
}
getUnduckedMusicVolume() {
- return this.masterVolume * this.musicVolume;
+ return this.getMasterVolume() * (this.musicVolumeEnabled ? this.musicVolume : 0);
+ }
+
+ getMasterVolume() {
+ return this.masterVolumeEnabled ? this.masterVolume : 0;
+ }
+
+ getTtsVolume() {
+ return this.getMasterVolume() * (this.ttsVolumeEnabled ? this.ttsVolume : 0);
}
duckMusicForSpeech() {
console.log('AudioManager: Ducking music for TTS playback');
- this.fadeMusicTo(0.3, 500);
+ const factor = this.musicDuckingEnabled ? 1 - this.musicDuckingAmount : 1;
+ this.fadeMusicTo(factor, 500);
}
restoreMusicAfterSpeech() {
@@ -564,6 +623,7 @@ class AudioManagerModule extends BaseModule {
const audio = template.cloneNode(true);
this.setMediaVolume(audio, this.getSfxVolume());
this.currentAudio = audio;
+ this.currentAudioRole = 'sfx';
const maxDuration = Math.max(0, Number(options.maxDurationSeconds || options.maxDuration || 0)) * 1000;
const endMode = String(options.endMode || options.mode || 'stop').toLowerCase().startsWith('fade') ? 'fade' : 'stop';
const fadeDuration = Math.max(100, Number(options.fadeDurationSeconds || options.fadeDuration || 2) * 1000);
@@ -572,6 +632,7 @@ class AudioManagerModule extends BaseModule {
if (maxTimer) clearTimeout(maxTimer);
if (this.currentAudio === audio) {
this.currentAudio = null;
+ this.currentAudioRole = null;
}
}, { once: true });
await audio.play();
@@ -588,6 +649,7 @@ class AudioManagerModule extends BaseModule {
audio.pause();
audio.currentTime = 0;
if (this.currentAudio === audio) this.currentAudio = null;
+ if (this.currentAudio === null) this.currentAudioRole = null;
}
}, timeoutDuration);
}
@@ -614,6 +676,7 @@ class AudioManagerModule extends BaseModule {
audio.pause();
audio.currentTime = 0;
if (this.currentAudio === audio) this.currentAudio = null;
+ if (this.currentAudio === null) this.currentAudioRole = null;
resolve(true);
};
requestAnimationFrame(step);
@@ -832,6 +895,10 @@ class AudioManagerModule extends BaseModule {
audio.pause();
audio.currentTime = 0;
this.setMediaVolume(audio, initialVolume); // Reset volume for future use
+ if (this.currentAudio === audio) {
+ this.currentAudio = null;
+ this.currentAudioRole = null;
+ }
resolve();
} else {
this.setMediaVolume(audio, currentVolume);
@@ -860,6 +927,7 @@ class AudioManagerModule extends BaseModule {
if (this.currentAudio) {
this.currentAudio.pause();
this.currentAudio.currentTime = 0;
+ this.currentAudioRole = null;
}
// Create new audio element
@@ -880,13 +948,14 @@ class AudioManagerModule extends BaseModule {
}
// Apply master volume and speech volume
- this.setMediaVolume(audio, this.masterVolume * speechVolume * this.ttsVolume);
+ this.setMediaVolume(audio, speechVolume * this.getTtsVolume());
// Set up cleanup
audio.onended = () => {
URL.revokeObjectURL(audioUrl);
if (this.currentAudio === audio) {
this.currentAudio = null;
+ this.currentAudioRole = null;
}
if (options.onComplete && typeof options.onComplete === 'function') {
options.onComplete();
@@ -902,6 +971,7 @@ class AudioManagerModule extends BaseModule {
URL.revokeObjectURL(audioUrl);
if (this.currentAudio === audio) {
this.currentAudio = null;
+ this.currentAudioRole = null;
}
if (options.onError && typeof options.onError === 'function') {
options.onError(error);
@@ -915,6 +985,7 @@ class AudioManagerModule extends BaseModule {
// Store as current audio
this.currentAudio = audio;
+ this.currentAudioRole = 'tts';
// Play the audio
await audio.play();
diff --git a/public/js/browser-tts-module.js b/public/js/browser-tts-module.js
index 156c900..f635a31 100644
--- a/public/js/browser-tts-module.js
+++ b/public/js/browser-tts-module.js
@@ -9,7 +9,7 @@ export class BrowserTTSModule extends TTSHandlerModule {
super('browser-tts', 'Browser TTS');
// Declare proper dependencies according to architecture principles
- this.dependencies = ['persistence-manager', 'localization'];
+ this.dependencies = ['persistence-manager', 'localization', 'game-config'];
// Voice options
this.voiceOptions = {
@@ -57,6 +57,13 @@ export class BrowserTTSModule extends TTSHandlerModule {
console.error('Browser TTS: Localization dependency not found');
return false;
}
+
+ this.addEventListener(document, 'preference-updated', (event) => {
+ const { category, key } = event.detail || {};
+ if (category === 'audio' && ['masterVolume', 'ttsVolume', 'masterVolumeEnabled', 'ttsVolumeEnabled'].includes(key) && this.currentUtterance) {
+ this.currentUtterance.volume = this.getPlaybackVolume();
+ }
+ });
// Check if browser supports speech synthesis
if (!window.speechSynthesis) {
@@ -163,9 +170,9 @@ export class BrowserTTSModule extends TTSHandlerModule {
*/
async setupVoiceFromPreferences() {
const persistenceManager = this.getModule('persistence-manager');
- const localization = this.getModule('localization');
+ const gameConfig = this.getModule('game-config');
- if (!persistenceManager || !localization || this.voices.length === 0) {
+ if (!persistenceManager || this.voices.length === 0) {
return false;
}
@@ -173,7 +180,7 @@ export class BrowserTTSModule extends TTSHandlerModule {
const preferredVoiceId = persistenceManager.getPreference('tts', 'browser_voice', '');
// Get current locale
- const currentLocale = localization.getLocale();
+ const currentLocale = gameConfig?.getLocale?.() || 'en_US';
// If we have a preferred voice ID, use it
if (preferredVoiceId && this.voices.some(v => v.id === preferredVoiceId)) {
@@ -200,11 +207,11 @@ export class BrowserTTSModule extends TTSHandlerModule {
return this.selectDefaultVoice();
}
- // Extract language code from locale (e.g., 'en-US' -> 'en')
- const langCode = locale.split('-')[0].toLowerCase();
+ const normalizedLocale = String(locale).replace('_', '-').toLowerCase();
+ const langCode = normalizedLocale.split('-')[0];
// First try to find a voice that exactly matches the locale
- let matchedVoice = this.voices.find(v => v.language && v.language.toLowerCase() === locale.toLowerCase());
+ let matchedVoice = this.voices.find(v => v.language && v.language.toLowerCase() === normalizedLocale);
// If not found, try to find a voice for the language
if (!matchedVoice && this.voicesByLang[langCode]) {
@@ -220,6 +227,21 @@ export class BrowserTTSModule extends TTSHandlerModule {
// Fall back to default voice
return this.selectDefaultVoice();
}
+
+ getPlaybackVolume() {
+ const persistenceManager = this.getModule('persistence-manager');
+ if (!persistenceManager) {
+ return this.voiceOptions.volume || 1.0;
+ }
+
+ const masterVolume = persistenceManager.getPreference('audio', 'masterVolume', 1.0);
+ const ttsVolume = persistenceManager.getPreference('audio', 'ttsVolume', 1.0);
+ const masterEnabled = persistenceManager.getPreference('audio', 'masterVolumeEnabled', true) !== false;
+ const ttsEnabled = persistenceManager.getPreference('audio', 'ttsVolumeEnabled', true) !== false;
+ const configuredVolume = this.voiceOptions.volume || 1.0;
+
+ return Math.max(0, Math.min(1, configuredVolume * (masterEnabled ? masterVolume : 0) * (ttsEnabled ? ttsVolume : 0)));
+ }
/**
* Select a default voice
@@ -342,7 +364,7 @@ export class BrowserTTSModule extends TTSHandlerModule {
utterance.rate = this.voiceOptions.speed || 1.0;
utterance.pitch = this.voiceOptions.pitch || 1.0;
- utterance.volume = this.voiceOptions.volume || 1.0;
+ utterance.volume = this.getPlaybackVolume();
// Set up event handlers
utterance.onstart = this.utteranceHandlers.start;
@@ -484,7 +506,7 @@ export class BrowserTTSModule extends TTSHandlerModule {
* @returns {boolean} - Success status (always false)
*/
speakPreloaded(preloadData, callback = null) {
- if (callback) {
+ if (typeof callback === 'function') {
callback({ success: false, reason: 'not_supported' });
}
return false;
diff --git a/public/js/choice-display-module.js b/public/js/choice-display-module.js
index bba4234..e05a340 100644
--- a/public/js/choice-display-module.js
+++ b/public/js/choice-display-module.js
@@ -8,8 +8,9 @@ class ChoiceDisplayModule extends BaseModule {
constructor() {
super('choice-display', 'Choice Display');
- this.dependencies = ['socket-client'];
+ this.dependencies = ['socket-client', 'markup-parser'];
this.socketClient = null;
+ this.markupParser = null;
this.container = null;
this.choices = [];
this.inputMode = 'text';
@@ -37,12 +38,14 @@ class ChoiceDisplayModule extends BaseModule {
'assignLetters',
'selectChoice',
'getTagValue',
- 'getTemplateCell'
+ 'getTemplateCell',
+ 'renderChoiceText'
]);
}
async initialize() {
this.socketClient = this.getModule('socket-client');
+ this.markupParser = this.getModule('markup-parser');
this.setupContainer();
this.addEventListener(document, 'story:choices', (event) => {
@@ -157,9 +160,14 @@ class ChoiceDisplayModule extends BaseModule {
.trim()
.charAt(0)
.toUpperCase();
- if (alphabet.includes(explicit) && !reserved.has(explicit)) {
- choice.letter = explicit;
- reserved.add(explicit);
+ const keyExplicit = String(choice.letter || this.getTagValue(choice.tags, 'key') || '')
+ .trim()
+ .charAt(0)
+ .toUpperCase();
+ const reservedLetter = explicit || keyExplicit;
+ if (alphabet.includes(reservedLetter) && !reserved.has(reservedLetter)) {
+ choice.letter = reservedLetter;
+ reserved.add(reservedLetter);
}
});
@@ -225,7 +233,7 @@ class ChoiceDisplayModule extends BaseModule {
const button = document.createElement('button');
button.type = 'button';
button.className = 'choice-button';
- button.innerHTML = `${choice.letter} ${this.escapeHtml(choice.text)} `;
+ button.innerHTML = `${this.escapeHtml(choice.letter)} ${this.renderChoiceText(choice.text)} `;
button.addEventListener('click', () => this.selectChoice(choice.index));
item.appendChild(button);
list.appendChild(item);
@@ -265,6 +273,23 @@ class ChoiceDisplayModule extends BaseModule {
.replace(/>/g, '>')
.replace(/"/g, '"');
}
+
+ renderChoiceText(text) {
+ if (!this.markupParser) {
+ this.markupParser = this.getModule('markup-parser');
+ }
+ if (this.markupParser && typeof this.markupParser.markdownToHtml === 'function') {
+ return this.markupParser.markdownToHtml(String(text || ''));
+ }
+
+ return this.escapeHtml(text)
+ .replace(/\*\*\*([^*]+?)\*\*\*/g, '$1 ')
+ .replace(/___([^_]+?)___/g, '$1 ')
+ .replace(/\*\*([^*]+?)\*\*/g, '$1 ')
+ .replace(/__([^_]+?)__/g, '$1 ')
+ .replace(/\*([^*\s][^*]*?)\*/g, '$1 ')
+ .replace(/_([^_\s][^_]*?)_/g, '$1 ');
+ }
}
const choiceDisplay = new ChoiceDisplayModule();
diff --git a/public/js/elevenlabs-tts-module.js b/public/js/elevenlabs-tts-module.js
index e080bd5..f8c388b 100644
--- a/public/js/elevenlabs-tts-module.js
+++ b/public/js/elevenlabs-tts-module.js
@@ -177,7 +177,7 @@ export class ElevenLabsTTSModule extends ApiTTSModuleBase {
* @param {string} text - Text to generate speech for
* @returns {Promise} - Audio data object
*/
- async generateSpeechAudio(text) {
+ async generateSpeechAudio(text, options = {}) {
// Don't attempt to call the API if no API key is set or text is empty
if (!text || !this.apiKey) {
return { success: false, reason: 'missing_api_key_or_text' };
@@ -208,7 +208,8 @@ export class ElevenLabsTTSModule extends ApiTTSModuleBase {
'xi-api-key': this.apiKey,
'Accept': 'audio/mpeg'
},
- body: JSON.stringify(payload)
+ body: JSON.stringify(payload),
+ signal: options.signal
});
if (!response.ok) {
diff --git a/public/js/game-config-module.js b/public/js/game-config-module.js
index 4f12821..9e36fc1 100644
--- a/public/js/game-config-module.js
+++ b/public/js/game-config-module.js
@@ -24,11 +24,6 @@ class GameConfigModule extends BaseModule {
this.reportProgress(20, 'Loading game configuration');
this.config = await this.loadConfig();
- const localization = this.getModule('localization');
- if (localization && this.config?.locale) {
- await localization.applyServerLocale(this.config.locale);
- }
-
this.applyDocumentMetadata();
document.dispatchEvent(new CustomEvent('game:config', {
detail: this.config
@@ -88,7 +83,7 @@ class GameConfigModule extends BaseModule {
}
getLocale() {
- return this.config?.locale || 'en_US';
+ return this.config?.metadata?.language || this.config?.locale || 'en_US';
}
}
diff --git a/public/js/game-loop-module.js b/public/js/game-loop-module.js
index 1041eeb..7dbfbff 100644
--- a/public/js/game-loop-module.js
+++ b/public/js/game-loop-module.js
@@ -14,6 +14,8 @@ class GameLoopModule extends BaseModule {
// Game state
this.gameState = {
started: false,
+ startedOnce: false,
+ ended: false,
canLoad: false,
currentRoom: null,
inventory: [],
@@ -71,6 +73,15 @@ class GameLoopModule extends BaseModule {
document.addEventListener('ui:game:restart', () => this.requestStartGame());
document.addEventListener('ui:game:save', () => this.requestSaveGame());
document.addEventListener('ui:game:load', () => this.requestLoadGame());
+ document.addEventListener('story:input-mode', (event) => {
+ if (event.detail !== 'end') {
+ return;
+ }
+ this.gameState.started = false;
+ this.gameState.ended = true;
+ this.gameState.canSave = false;
+ this.updateUIState();
+ });
}
setupSocketEventListeners() {
@@ -142,6 +153,10 @@ class GameLoopModule extends BaseModule {
]);
this.gameState.started = Boolean(running?.result);
+ if (this.gameState.started) {
+ this.gameState.startedOnce = true;
+ this.gameState.ended = false;
+ }
this.gameState.canSave = this.gameState.started;
this.gameState.canLoad = Boolean(hasSave?.result);
this.updateUIState();
@@ -177,9 +192,9 @@ class GameLoopModule extends BaseModule {
// Update UI components based on game state
const state = {
canRestart: true,
- canSave: Boolean(this.gameState.started),
+ canSave: Boolean(this.gameState.canSave && this.gameState.started),
canLoad: Boolean(this.gameState.canLoad),
- gameStarted: Boolean(this.gameState.started)
+ gameStarted: Boolean(this.gameState.started || this.gameState.startedOnce || this.gameState.ended)
};
document.body.dataset.gameRunning = state.gameStarted ? 'true' : 'false';
uiController.updateButtonStates(state);
@@ -192,6 +207,11 @@ class GameLoopModule extends BaseModule {
const socketClient = this.getModule('socket-client');
if (!socketClient) return;
+ this.gameState.started = true;
+ this.gameState.startedOnce = true;
+ this.gameState.ended = false;
+ this.gameState.canSave = true;
+ this.updateUIState();
await this.resetClientPlaybackAndDisplay();
const storyHistory = this.getModule('story-history');
if (storyHistory && typeof storyHistory.startNewGame === 'function') {
@@ -200,9 +220,14 @@ class GameLoopModule extends BaseModule {
const response = await socketClient.newGame();
if (!response?.success) {
console.error('GameLoop: newGame failed', response);
+ this.gameState.started = false;
+ this.gameState.canSave = false;
+ this.updateUIState();
return;
}
this.gameState.started = true;
+ this.gameState.startedOnce = true;
+ this.gameState.ended = false;
this.gameState.canSave = true;
this.gameState.canLoad = Boolean(response.canLoad);
this.updateUIState();
@@ -250,6 +275,12 @@ class GameLoopModule extends BaseModule {
return;
}
+ this.gameState.started = true;
+ this.gameState.startedOnce = true;
+ this.gameState.ended = false;
+ this.gameState.canSave = true;
+ this.gameState.canLoad = true;
+ this.updateUIState();
await this.resetClientPlaybackAndDisplay();
document.dispatchEvent(new CustomEvent('story:history-restoring', {
detail: { active: true, reason: 'load-game' }
@@ -285,6 +316,8 @@ class GameLoopModule extends BaseModule {
}
if (response?.success) {
this.gameState.started = true;
+ this.gameState.startedOnce = true;
+ this.gameState.ended = false;
this.gameState.canSave = true;
this.gameState.canLoad = true;
this.updateUIState();
diff --git a/public/js/kokoro-tts-module.js b/public/js/kokoro-tts-module.js
index fe2b6ad..ee6a6a5 100644
--- a/public/js/kokoro-tts-module.js
+++ b/public/js/kokoro-tts-module.js
@@ -9,7 +9,7 @@ export class KokoroTTSModule extends TTSHandlerModule {
super('kokoro-tts', 'Kokoro TTS');
// Declare proper dependencies according to architecture principles
- this.dependencies = ['persistence-manager', 'localization'];
+ this.dependencies = ['persistence-manager', 'localization', 'game-config'];
// State
this.iframe = null;
@@ -59,6 +59,13 @@ export class KokoroTTSModule extends TTSHandlerModule {
return false;
}
+ this.addEventListener(document, 'preference-updated', (event) => {
+ const { category, key } = event.detail || {};
+ if (category === 'audio' && ['masterVolume', 'ttsVolume', 'masterVolumeEnabled', 'ttsVolumeEnabled'].includes(key) && this.currentAudio) {
+ this.currentAudio.volume = this.getPlaybackVolume();
+ }
+ });
+
const ttsEnabled = persistenceManager.getPreference('tts', 'enabled', false);
const preferredHandler = persistenceManager.getPreference('tts', 'preferred_handler', 'none');
if (!ttsEnabled || preferredHandler !== this.id) {
@@ -256,8 +263,8 @@ export class KokoroTTSModule extends TTSHandlerModule {
}
// Get current locale
- const localization = this.getModule('localization');
- const locale = localization ? localization.getLocale() : null;
+ const gameConfig = this.getModule('game-config');
+ const locale = gameConfig?.getLocale?.() || 'en_US';
// Get preferred voice from preferences
const preferredVoiceId = persistenceManager.getPreference('tts', 'kokoro_voice', '');
@@ -367,6 +374,20 @@ export class KokoroTTSModule extends TTSHandlerModule {
this.setOptions({ volume: Math.max(0, Math.min(1, options.volume)) });
}
}
+
+ getPlaybackVolume() {
+ const persistenceManager = this.getModule('persistence-manager');
+ if (!persistenceManager) {
+ return this.options.volume;
+ }
+
+ const masterVolume = persistenceManager.getPreference('audio', 'masterVolume', 1.0);
+ const ttsVolume = persistenceManager.getPreference('audio', 'ttsVolume', 1.0);
+ const masterEnabled = persistenceManager.getPreference('audio', 'masterVolumeEnabled', true) !== false;
+ const ttsEnabled = persistenceManager.getPreference('audio', 'ttsVolumeEnabled', true) !== false;
+
+ return Math.max(0, Math.min(1, this.options.volume * (masterEnabled ? masterVolume : 0) * (ttsEnabled ? ttsVolume : 0)));
+ }
/**
* Get available voices
@@ -431,9 +452,10 @@ export class KokoroTTSModule extends TTSHandlerModule {
* @returns {boolean} - Success status
*/
speakPreloaded(preloadData, callback = null) {
+ const completionCallback = typeof callback === 'function' ? callback : null;
if (!this.isReady || !preloadData || !preloadData.audioData) {
- if (callback) {
- callback({ success: false, reason: 'invalid_data' });
+ if (completionCallback) {
+ completionCallback({ success: false, reason: 'invalid_data' });
}
return false;
}
@@ -446,22 +468,22 @@ export class KokoroTTSModule extends TTSHandlerModule {
const audioUrl = URL.createObjectURL(audioBlob);
const audio = new Audio(audioUrl);
- audio.volume = this.options.volume;
+ audio.volume = this.getPlaybackVolume();
audio.playbackRate = this.options.rate;
// Set up event handlers
audio.onended = () => {
this.isSpeaking = false;
- if (callback) {
- callback({ success: true });
+ if (completionCallback) {
+ completionCallback({ success: true });
}
URL.revokeObjectURL(audioUrl);
};
audio.onerror = (error) => {
this.isSpeaking = false;
- if (callback) {
- callback({ success: false, reason: 'playback_error', error });
+ if (completionCallback) {
+ completionCallback({ success: false, reason: 'playback_error', error });
}
URL.revokeObjectURL(audioUrl);
};
@@ -475,8 +497,8 @@ export class KokoroTTSModule extends TTSHandlerModule {
}));
}).catch(error => {
this.isSpeaking = false;
- if (callback) {
- callback({ success: false, reason: 'playback_error', error });
+ if (completionCallback) {
+ completionCallback({ success: false, reason: 'playback_error', error });
}
URL.revokeObjectURL(audioUrl);
});
@@ -513,7 +535,7 @@ export class KokoroTTSModule extends TTSHandlerModule {
// Create and play audio
const audio = new Audio(audioUrl);
- audio.volume = this.options.volume;
+ audio.volume = this.getPlaybackVolume();
audio.playbackRate = this.options.rate;
// Set up event handlers
diff --git a/public/js/localization-module.js b/public/js/localization-module.js
index 58aa9b7..a00b455 100644
--- a/public/js/localization-module.js
+++ b/public/js/localization-module.js
@@ -26,7 +26,6 @@ class LocalizationModule extends BaseModule {
// Bind methods
this.bindMethods([
'setLocale',
- 'applyServerLocale',
'normalizeLocale',
'getLocale',
'translate',
@@ -145,7 +144,6 @@ class LocalizationModule extends BaseModule {
const persistenceManager = this.getModule('persistence-manager');
if (persistenceManager) {
persistenceManager.updatePreference('app', 'locale', normalizedLocale);
- persistenceManager.updatePreference('tts', 'language', normalizedLocale);
if (userInitiated) {
persistenceManager.updatePreference('app', 'localeUserOverride', true);
}
@@ -171,15 +169,6 @@ class LocalizationModule extends BaseModule {
}
}
- async applyServerLocale(locale) {
- const persistenceManager = this.getModule('persistence-manager');
- const userOverride = persistenceManager?.getPreference('app', 'localeUserOverride', false);
- if (userOverride) {
- return false;
- }
- return this.setLocale(locale, { userInitiated: false });
- }
-
normalizeLocale(locale) {
const normalized = String(locale || this.defaultLocale).trim().replace('-', '_').toLowerCase();
if (normalized.startsWith('de')) return 'de_DE';
diff --git a/public/js/markup-parser-module.js b/public/js/markup-parser-module.js
index 81621e9..bf507e6 100644
--- a/public/js/markup-parser-module.js
+++ b/public/js/markup-parser-module.js
@@ -7,7 +7,7 @@ import { BaseModule } from './base-module.js';
class MarkupParserModule extends BaseModule {
constructor() {
super('markup-parser', 'Markup Parser');
- this.dependencies = [];
+ this.dependencies = ['game-config'];
this.assetRoots = {
images: '/images/',
music: '/music/',
@@ -24,6 +24,9 @@ class MarkupParserModule extends BaseModule {
'markdownToHtml',
'markdownToPlainText',
'smartypants',
+ 'applyLocaleTypography',
+ 'getTypographyLocale',
+ 'normalizeDialogueQuotes',
'escapeHtml',
'normalizeParagraph',
'buildParagraphBlock',
@@ -225,12 +228,38 @@ class MarkupParserModule extends BaseModule {
}
smartypants(text) {
- return String(text)
+ const result = String(text)
.replace(/---/g, '\u2014')
.replace(/--/g, '\u2013')
.replace(/\.\.\./g, '\u2026')
.replace(/(^|[\s([{\u2014])"([^"]*)"/g, '$1\u201c$2\u201d')
.replace(/(^|[\s([{\u2014])'([^']*)'/g, '$1\u2018$2\u2019');
+
+ return this.applyLocaleTypography(result);
+ }
+
+ applyLocaleTypography(text) {
+ const locale = this.getTypographyLocale();
+ if (locale.startsWith('de')) {
+ return this.normalizeDialogueQuotes(text);
+ }
+
+ return text;
+ }
+
+ getTypographyLocale() {
+ const gameConfig = this.getModule('game-config') || window.GameConfig;
+ const locale = gameConfig?.getLocale?.()
+ || gameConfig?.getConfig?.()?.metadata?.language
+ || 'en_US';
+
+ return String(locale).trim().toLowerCase().replace('_', '-');
+ }
+
+ normalizeDialogueQuotes(text) {
+ return String(text || '')
+ .replace(/&(ldquo|bdquo|laquo|raquo);([^&\n]+?)&(rdquo|ldquo|laquo|raquo);/gi, '»$2«')
+ .replace(/["\u201c\u201e\u201d\u00ab\u00bb]([^"\u201c\u201e\u201d\u00ab\u00bb\n]+?)["\u201c\u201d\u201e\u00ab\u00bb]/g, '»$1«');
}
escapeHtml(text) {
diff --git a/public/js/openai-tts-module.js b/public/js/openai-tts-module.js
index 0ed4491..f7ece7e 100644
--- a/public/js/openai-tts-module.js
+++ b/public/js/openai-tts-module.js
@@ -173,7 +173,7 @@ export class OpenAITTSModule extends ApiTTSModuleBase {
* @param {string} text - Text to generate speech for
* @returns {Promise} - Audio data object
*/
- async generateSpeechAudio(text) {
+ async generateSpeechAudio(text, options = {}) {
if (!this.isReady || !this.apiKey) {
return { success: false, reason: 'not_ready' };
}
@@ -198,7 +198,8 @@ export class OpenAITTSModule extends ApiTTSModuleBase {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`
},
- body: JSON.stringify(payload)
+ body: JSON.stringify(payload),
+ signal: options.signal
});
if (!response.ok) {
diff --git a/public/js/options-ui-module.js b/public/js/options-ui-module.js
index d6bf4ef..94b0800 100644
--- a/public/js/options-ui-module.js
+++ b/public/js/options-ui-module.js
@@ -17,7 +17,8 @@ class OptionsUIModule extends BaseModule {
'persistence-manager',
'localization',
'tts-factory',
- 'audio-manager'
+ 'audio-manager',
+ 'game-config'
];
// Modal element
@@ -38,6 +39,9 @@ class OptionsUIModule extends BaseModule {
'populateVoices',
'populateLanguages',
'loadPreferences',
+ 'createVolumeControl',
+ 'updateVolumeToggleButtons',
+ 'updateVolumeToggleButton',
'showReloadNotice',
'toggle',
'setupEventListeners',
@@ -170,6 +174,7 @@ class OptionsUIModule extends BaseModule {
// Create body
const body = document.createElement('div');
body.className = 'modal-body';
+ const localization = this.getModule('localization');
// Create sections
// App Settings Section (Language and Speed)
@@ -193,6 +198,23 @@ class OptionsUIModule extends BaseModule {
}, null, languageContainer);
appSettingsSection.appendChild(languageContainer);
+
+ const gameLanguageContainer = document.createElement('div');
+ gameLanguageContainer.className = 'option-item';
+
+ const gameLanguageLabel = document.createElement('label');
+ gameLanguageLabel.textContent = this.t('options.gameLanguage') + ':';
+ gameLanguageContainer.appendChild(gameLanguageLabel);
+
+ const gameLanguageValue = document.createElement('span');
+ gameLanguageValue.className = 'game-language-value';
+ const gameConfig = this.getModule('game-config');
+ const gameLocale = gameConfig?.getLocale?.() || 'en_US';
+ gameLanguageValue.textContent = localization?.getLanguageName?.(gameLocale) || gameLocale;
+ this.elements.gameLanguage = gameLanguageValue;
+ gameLanguageContainer.appendChild(gameLanguageValue);
+
+ appSettingsSection.appendChild(gameLanguageContainer);
// Speed
const speedContainer = document.createElement('div');
@@ -296,125 +318,11 @@ class OptionsUIModule extends BaseModule {
audioTitle.textContent = this.t('options.audio');
audioSection.appendChild(audioTitle);
- // Master Volume
- const masterVolumeContainer = document.createElement('div');
- masterVolumeContainer.className = 'option-item';
-
- const masterVolumeLabel = document.createElement('label');
- masterVolumeLabel.textContent = this.t('options.masterVolume') + ':';
- masterVolumeContainer.appendChild(masterVolumeLabel);
-
- const masterVolumeValue = document.createElement('span');
- masterVolumeValue.className = 'slider-value';
- masterVolumeValue.textContent = '100%';
- this.elements.masterVolumeValue = masterVolumeValue;
- masterVolumeContainer.appendChild(masterVolumeValue);
-
- this.elements.masterVolume = createUIElement('input', {
- type: 'range',
- min: 0,
- max: 100,
- value: 100,
- 'data-pref-bind': 'audio.masterVolume',
- 'data-pref-transform': 'range:0,1'
- }, null, masterVolumeContainer);
-
- // Update displayed value when slider changes
- this.elements.masterVolume.addEventListener('input', () => {
- this.elements.masterVolumeValue.textContent = `${this.elements.masterVolume.value}%`;
- });
-
- audioSection.appendChild(masterVolumeContainer);
-
- // Speech Volume
- const ttsVolumeContainer = document.createElement('div');
- ttsVolumeContainer.className = 'option-item';
-
- const ttsVolumeLabel = document.createElement('label');
- ttsVolumeLabel.textContent = this.t('options.speechVolume') + ':';
- ttsVolumeContainer.appendChild(ttsVolumeLabel);
-
- const ttsVolumeValue = document.createElement('span');
- ttsVolumeValue.className = 'slider-value';
- ttsVolumeValue.textContent = '100%';
- this.elements.ttsVolumeValue = ttsVolumeValue;
- ttsVolumeContainer.appendChild(ttsVolumeValue);
-
- this.elements.ttsVolume = createUIElement('input', {
- type: 'range',
- min: 0,
- max: 100,
- value: 100,
- 'data-pref-bind': 'audio.ttsVolume',
- 'data-pref-transform': 'range:0,1'
- }, null, ttsVolumeContainer);
-
- // Update displayed value when slider changes
- this.elements.ttsVolume.addEventListener('input', () => {
- this.elements.ttsVolumeValue.textContent = `${this.elements.ttsVolume.value}%`;
- });
-
- audioSection.appendChild(ttsVolumeContainer);
-
- // Music Volume
- const musicVolumeContainer = document.createElement('div');
- musicVolumeContainer.className = 'option-item';
-
- const musicVolumeLabel = document.createElement('label');
- musicVolumeLabel.textContent = this.t('options.musicVolume') + ':';
- musicVolumeContainer.appendChild(musicVolumeLabel);
-
- const musicVolumeValue = document.createElement('span');
- musicVolumeValue.className = 'slider-value';
- musicVolumeValue.textContent = '100%';
- this.elements.musicVolumeValue = musicVolumeValue;
- musicVolumeContainer.appendChild(musicVolumeValue);
-
- this.elements.musicVolume = createUIElement('input', {
- type: 'range',
- min: 0,
- max: 100,
- value: 70,
- 'data-pref-bind': 'audio.musicVolume',
- 'data-pref-transform': 'range:0,1'
- }, null, musicVolumeContainer);
-
- // Update displayed value when slider changes
- this.elements.musicVolume.addEventListener('input', () => {
- this.elements.musicVolumeValue.textContent = `${this.elements.musicVolume.value}%`;
- });
-
- audioSection.appendChild(musicVolumeContainer);
-
- // SFX Volume
- const sfxVolumeContainer = document.createElement('div');
- sfxVolumeContainer.className = 'option-item';
-
- const sfxVolumeLabel = document.createElement('label');
- sfxVolumeLabel.textContent = this.t('options.sfxVolume') + ':';
- sfxVolumeContainer.appendChild(sfxVolumeLabel);
-
- const sfxVolumeValue = document.createElement('span');
- sfxVolumeValue.className = 'slider-value';
- sfxVolumeValue.textContent = '100%';
- this.elements.sfxVolumeValue = sfxVolumeValue;
- sfxVolumeContainer.appendChild(sfxVolumeValue);
-
- this.elements.sfxVolume = createUIElement('input', {
- type: 'range',
- min: 0,
- max: 100,
- value: 100,
- 'data-pref-bind': 'audio.sfxVolume',
- 'data-pref-transform': 'range:0,1'
- }, null, sfxVolumeContainer);
-
- // Update displayed value when slider changes
- this.elements.sfxVolume.addEventListener('input', () => {
- this.elements.sfxVolumeValue.textContent = `${this.elements.sfxVolume.value}%`;
- });
-
- audioSection.appendChild(sfxVolumeContainer);
+ audioSection.appendChild(this.createVolumeControl('masterVolume', 'masterVolumeEnabled', 'options.masterVolume', 'options.muteMasterVolume', 'options.unmuteMasterVolume', 100));
+ audioSection.appendChild(this.createVolumeControl('ttsVolume', 'ttsVolumeEnabled', 'options.speechVolume', 'options.muteSpeechVolume', 'options.unmuteSpeechVolume', 100));
+ audioSection.appendChild(this.createVolumeControl('musicVolume', 'musicVolumeEnabled', 'options.musicVolume', 'options.muteMusicVolume', 'options.unmuteMusicVolume', 70));
+ audioSection.appendChild(this.createVolumeControl('sfxVolume', 'sfxVolumeEnabled', 'options.sfxVolume', 'options.muteSfxVolume', 'options.unmuteSfxVolume', 100));
+ audioSection.appendChild(this.createVolumeControl('musicDuckingAmount', 'musicDuckingEnabled', 'options.musicDucking', 'options.disableMusicDucking', 'options.enableMusicDucking', 30));
body.appendChild(audioSection);
@@ -437,6 +345,69 @@ class OptionsUIModule extends BaseModule {
// Add modal to document
document.body.appendChild(this.modal);
}
+
+ createVolumeControl(valueKey, enabledKey, labelKey, muteTitleKey, unmuteTitleKey, defaultPercent) {
+ const container = document.createElement('div');
+ container.className = 'option-item volume-option';
+
+ const label = document.createElement('label');
+ label.textContent = this.t(labelKey) + ':';
+ container.appendChild(label);
+
+ const toggle = document.createElement('button');
+ toggle.type = 'button';
+ toggle.className = 'volume-toggle';
+ toggle.dataset.prefCategory = 'audio';
+ toggle.dataset.prefKey = enabledKey;
+ toggle.dataset.muteTitleKey = muteTitleKey;
+ toggle.dataset.unmuteTitleKey = unmuteTitleKey;
+ toggle.addEventListener('click', () => {
+ const current = this.getPreference('audio', enabledKey, true) !== false;
+ this.updatePreference('audio', enabledKey, !current);
+ this.updateVolumeToggleButton(toggle);
+ });
+ container.appendChild(toggle);
+
+ const value = document.createElement('span');
+ value.className = 'slider-value';
+ value.textContent = `${defaultPercent}%`;
+ this.elements[`${valueKey}Value`] = value;
+ container.appendChild(value);
+
+ const slider = createUIElement('input', {
+ type: 'range',
+ min: 0,
+ max: 100,
+ value: defaultPercent,
+ 'data-pref-bind': `audio.${valueKey}`,
+ 'data-pref-transform': 'range:0,1'
+ }, null, container);
+ this.elements[valueKey] = slider;
+ slider.addEventListener('input', () => {
+ value.textContent = `${slider.value}%`;
+ });
+
+ this.updateVolumeToggleButton(toggle);
+ return container;
+ }
+
+ updateVolumeToggleButtons() {
+ if (!this.modal) return;
+ this.modal.querySelectorAll('.volume-toggle').forEach(button => {
+ this.updateVolumeToggleButton(button);
+ });
+ }
+
+ updateVolumeToggleButton(button) {
+ if (!button) return;
+ const enabled = this.getPreference(button.dataset.prefCategory, button.dataset.prefKey, true) !== false;
+ button.classList.toggle('is-muted', !enabled);
+ button.innerHTML = enabled ? '🔊' : '🔇';
+ const titleKey = enabled ? button.dataset.muteTitleKey : button.dataset.unmuteTitleKey;
+ const title = this.t(titleKey);
+ button.title = title;
+ button.setAttribute('aria-label', title);
+ }
/**
* Create API settings controls
@@ -707,7 +678,7 @@ class OptionsUIModule extends BaseModule {
languageOptions,
'code',
'name',
- this.getPreference('app', 'locale', 'en-us')
+ this.getPreference('app', 'locale', localization.getLocale?.() || 'en_US')
);
}
@@ -883,7 +854,22 @@ class OptionsUIModule extends BaseModule {
audioManager.setSfxVolume(value);
} else if (key === 'ttsVolume') {
audioManager.setTtsVolume(value);
+ } else if (key === 'masterVolumeEnabled') {
+ audioManager.setVolumeEnabled('master', value);
+ } else if (key === 'musicVolumeEnabled') {
+ audioManager.setVolumeEnabled('music', value);
+ } else if (key === 'sfxVolumeEnabled') {
+ audioManager.setVolumeEnabled('sfx', value);
+ } else if (key === 'ttsVolumeEnabled') {
+ audioManager.setVolumeEnabled('tts', value);
+ } else if (key === 'musicDuckingAmount') {
+ audioManager.setMusicDuckingAmount(value);
+ } else if (key === 'musicDuckingEnabled') {
+ audioManager.setMusicDuckingEnabled(value);
}
+
+ this.updateVolumeDisplays();
+ this.updateVolumeToggleButtons();
}
// Handle TTS settings side effects
@@ -905,8 +891,6 @@ class OptionsUIModule extends BaseModule {
ttsFactory.configure({ voice: value });
} else if (key === 'speed') {
ttsFactory.configure({ speed: value });
- } else if (key === 'language') {
- ttsFactory.configure({ language: value });
} else if (key === 'enabled') {
if (!value) {
ttsFactory.disableAfterCurrentPlayback();
@@ -939,11 +923,6 @@ class OptionsUIModule extends BaseModule {
if (localization) {
localization.setLocale(value);
}
- const ttsFactory = this.getModule('tts-factory');
- if (ttsFactory) {
- ttsFactory.configure({ language: value });
- }
- this.updatePreference('tts', 'language', value);
}
});
}
@@ -969,6 +948,9 @@ class OptionsUIModule extends BaseModule {
if (this.elements.sfxVolume && this.elements.sfxVolumeValue) {
this.elements.sfxVolumeValue.textContent = `${this.elements.sfxVolume.value}%`;
}
+ if (this.elements.musicDuckingAmount && this.elements.musicDuckingAmountValue) {
+ this.elements.musicDuckingAmountValue.textContent = `${this.elements.musicDuckingAmount.value}%`;
+ }
}
}
diff --git a/public/js/persistence-manager-module.js b/public/js/persistence-manager-module.js
index a178587..1ac14ab 100644
--- a/public/js/persistence-manager-module.js
+++ b/public/js/persistence-manager-module.js
@@ -42,9 +42,15 @@ class PersistenceManagerModule extends BaseModule {
},
audio: {
masterVolume: 1.0,
+ masterVolumeEnabled: true,
ttsVolume: 1.0,
+ ttsVolumeEnabled: true,
musicVolume: 0.7,
+ musicVolumeEnabled: true,
sfxVolume: 1.0,
+ sfxVolumeEnabled: true,
+ musicDuckingAmount: 0.3,
+ musicDuckingEnabled: true,
},
app: {
locale: null,
diff --git a/public/js/sentence-queue-module.js b/public/js/sentence-queue-module.js
index d3ff054..f5235c6 100644
--- a/public/js/sentence-queue-module.js
+++ b/public/js/sentence-queue-module.js
@@ -4,6 +4,8 @@
*/
import { BaseModule } from './base-module.js';
+const TTS_GENERATION_TIMEOUT_MS = 60000;
+
class SentenceQueueModule extends BaseModule {
constructor() {
super('sentence-queue', 'Sentence Queue');
@@ -22,6 +24,8 @@ class SentenceQueueModule extends BaseModule {
this.inputMode = 'text';
this.lastContinueAt = 0;
this.pauseBeforeNextReason = null;
+ this.ttsGenerationTimeoutMs = TTS_GENERATION_TIMEOUT_MS;
+ this.generationRequests = new Map();
// Bind methods
this.bindMethods([
@@ -34,6 +38,11 @@ class SentenceQueueModule extends BaseModule {
'getCacheKey',
'getPreparedSentence',
'prefetchAhead',
+ 'prepareSpeechMetadata',
+ 'normalizeTtsText',
+ 'runTtsPreloadWithTimeout',
+ 'cancelBlockingGeneration',
+ 'cancelGenerationRequests',
'isSpeechItem',
'getMediaPauseSeconds',
'readFirstFiniteNumber',
@@ -86,6 +95,7 @@ class SentenceQueueModule extends BaseModule {
this.addEventListener(document, 'ui:command', (event) => {
if (event.detail?.type === 'continue') {
this.lastContinueAt = performance.now();
+ this.cancelBlockingGeneration('user-fast-forward');
}
});
return true;
@@ -200,13 +210,21 @@ class SentenceQueueModule extends BaseModule {
* @param {string} text - Text to prepare speech for
* @returns {Promise} - Speech metadata object
*/
- async prepareSpeechMetadata(text) {
+ async prepareSpeechMetadata(text, context = {}) {
const ttsFactory = this.getModule('tts-factory');
if (!ttsFactory) {
throw new Error("TTS dependencies not found");
}
+ const ttsText = this.normalizeTtsText(text);
+ if (!ttsText) {
+ console.warn('SentenceQueue: Empty TTS text after normalization, using estimated silent timing', {
+ sentenceId: context.sentenceId || null
+ });
+ return this.estimateSpeechDuration(text);
+ }
+
// Check if TTS is enabled via active handler
const activeHandler = ttsFactory.getActiveHandler();
const isTtsEnabled = activeHandler !== null;
@@ -218,20 +236,28 @@ class SentenceQueueModule extends BaseModule {
try {
// Preload the speech to get metadata
- const result = await ttsFactory.preloadSpeech(text);
+ const result = await this.runTtsPreloadWithTimeout(ttsFactory, ttsText, context);
if (!result.success) {
- console.warn("SentenceQueue: Speech preload failed, using estimated duration");
+ console.warn("SentenceQueue: Speech preload failed, using estimated duration", {
+ reason: result.reason || 'unknown',
+ sentenceId: context.sentenceId || null,
+ textPreview: ttsText.slice(0, 80)
+ });
return this.estimateSpeechDuration(text);
}
// Create a speech metadata object
return {
- text: text,
+ text: ttsText,
duration: result.duration || this.estimateSpeechDuration(text).duration,
handler: ttsFactory.getActiveHandler() ? ttsFactory.getActiveHandler().id : null,
+ audioData: result.audioData || null,
play: async () => {
- return ttsFactory.speak(text);
+ if (result.audioData && typeof ttsFactory.speakPreloaded === 'function') {
+ return ttsFactory.speakPreloaded(result);
+ }
+ return ttsFactory.speak(ttsText);
},
stop: () => {
return ttsFactory.stop();
@@ -243,6 +269,94 @@ class SentenceQueueModule extends BaseModule {
return this.estimateSpeechDuration(text);
}
}
+
+ normalizeTtsText(text) {
+ return String(text || '')
+ .replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, ' ')
+ .replace(/\s+/g, ' ')
+ .trim();
+ }
+
+ runTtsPreloadWithTimeout(ttsFactory, text, context = {}) {
+ const sentenceId = context.sentenceId || context.id || `tts-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
+ const requestId = `${sentenceId}:${context.prefetch ? 'prefetch' : 'blocking'}:${Date.now()}`;
+ const controller = new AbortController();
+ const startedAt = performance.now();
+
+ return new Promise((resolve) => {
+ let settled = false;
+ const finish = (result) => {
+ if (settled) return;
+ settled = true;
+ clearTimeout(timeoutId);
+ this.generationRequests.delete(requestId);
+ resolve(result);
+ };
+
+ const timeoutId = setTimeout(() => {
+ console.warn('SentenceQueue: TTS generation timed out; continuing without audio', {
+ sentenceId,
+ timeoutMs: this.ttsGenerationTimeoutMs,
+ textPreview: text.slice(0, 120)
+ });
+ controller.abort('tts-generation-timeout');
+ finish({ success: false, reason: 'tts_generation_timeout', timedOut: true });
+ }, this.ttsGenerationTimeoutMs);
+
+ this.generationRequests.set(requestId, {
+ controller,
+ sentenceId,
+ blocking: context.blocking !== false,
+ startedAt,
+ textPreview: text.slice(0, 120),
+ finish
+ });
+
+ Promise.resolve(ttsFactory.preloadSpeech(text, { signal: controller.signal }))
+ .then(result => finish(result || { success: false, reason: 'empty_tts_result' }))
+ .catch(error => {
+ if (controller.signal.aborted) {
+ console.warn('SentenceQueue: TTS generation cancelled; continuing without audio', {
+ sentenceId,
+ reason: controller.signal.reason || 'aborted',
+ elapsedMs: Math.round(performance.now() - startedAt)
+ });
+ finish({ success: false, reason: 'tts_generation_aborted', error });
+ } else {
+ console.warn('SentenceQueue: TTS generation failed; continuing without audio', {
+ sentenceId,
+ error
+ });
+ finish({ success: false, reason: 'tts_generation_error', error });
+ }
+ });
+ });
+ }
+
+ cancelBlockingGeneration(reason = 'cancelled') {
+ this.cancelGenerationRequests(reason, request => request.blocking === true);
+ }
+
+ cancelGenerationRequests(reason = 'cancelled', predicate = () => true) {
+ for (const [requestId, request] of this.generationRequests.entries()) {
+ if (!predicate(request)) continue;
+ console.warn('SentenceQueue: Cancelling TTS generation request', {
+ requestId,
+ sentenceId: request.sentenceId,
+ reason,
+ elapsedMs: Math.round(performance.now() - request.startedAt),
+ textPreview: request.textPreview
+ });
+ try {
+ request.controller.abort(reason);
+ } catch (error) {
+ console.warn('SentenceQueue: Failed to abort TTS generation request', { requestId, error });
+ }
+ if (typeof request.finish === 'function') {
+ request.finish({ success: false, reason: 'tts_generation_cancelled' });
+ }
+ }
+ }
/**
* Estimate speech duration based on character count
@@ -314,7 +428,12 @@ class SentenceQueueModule extends BaseModule {
await audioManager.preloadMediaCues(metadata.cueMarkers || []);
}
- const ttsData = await this.prepareSpeechMetadata(text);
+ const ttsData = await this.prepareSpeechMetadata(text, {
+ sentenceId: id,
+ blockId: metadata.blockId ?? null,
+ turnId: metadata.turnId ?? null,
+ blocking: true
+ });
console.log(`SentenceQueue: Prepared speech "${text.substring(0, 50)}..." - TTS duration: ${ttsData.duration}ms`);
@@ -557,7 +676,14 @@ class SentenceQueueModule extends BaseModule {
console.log(`Process state: ${state}`, { reason: 'prefetch-start', sentenceId: nextItem.id, queueIndex: index });
const promise = (this.isSpeechItem(nextItem)
- ? this.prepareSpeechMetadata(nextItem.text || '')
+ ? this.prepareSpeechMetadata(nextItem.text || '', {
+ sentenceId: nextItem.id,
+ blockId: nextItem.blockId ?? null,
+ turnId: nextItem.turnId ?? null,
+ queueIndex: index,
+ prefetch: true,
+ blocking: false
+ })
: Promise.resolve(null))
.then(() => {
console.log('SentenceQueue: Prefetched queued speech/media', { sentenceId: nextItem.id, queueIndex: index });
@@ -781,6 +907,7 @@ class SentenceQueueModule extends BaseModule {
clear() {
this.sentenceQueue = [];
this.isProcessing = false;
+ this.cancelGenerationRequests('sentence-queue-cleared');
this.prefetchingSpeech.clear();
document.dispatchEvent(new CustomEvent('tts:queue-empty', {
detail: { reason: 'sentence-queue-cleared' }
diff --git a/public/js/socket-client-module.js b/public/js/socket-client-module.js
index 5e385a4..5abac53 100644
--- a/public/js/socket-client-module.js
+++ b/public/js/socket-client-module.js
@@ -214,10 +214,20 @@ class SocketClientModule extends BaseModule {
this.receivedParagraphCounter = 0;
}
- if (Array.isArray(data.globalTags) && data.globalTags.length > 0) {
+ const globalTags = Array.isArray(data.globalTags) ? data.globalTags : [];
+ const endState = data.gameState?.endState || null;
+ if (endState && !globalTags.some((tag) => tag?.key === 'score' || tag?.key === 'error')) {
+ globalTags.push({
+ key: endState.type === 'error' ? 'error' : 'score',
+ value: endState.message || ''
+ });
+ }
+
+ if (globalTags.length > 0) {
document.dispatchEvent(new CustomEvent('story:global-tags', {
- detail: data.globalTags
+ detail: globalTags
}));
+ this.dispatchTurnTags(globalTags, null);
}
document.dispatchEvent(new CustomEvent('story:turn-start', {
@@ -245,6 +255,10 @@ class SocketClientModule extends BaseModule {
document.dispatchEvent(new CustomEvent('story:process-state', {
detail: { state: 'ready', reason: 'choice-only-turn', turnId }
}));
+ } else if (turnBlocks.length === 0 && inputMode === 'end') {
+ document.dispatchEvent(new CustomEvent('story:process-state', {
+ detail: { state: 'ready', reason: 'empty-end-turn', turnId }
+ }));
}
}
diff --git a/public/js/text-processor-module.js b/public/js/text-processor-module.js
index 6aae666..e717df9 100644
--- a/public/js/text-processor-module.js
+++ b/public/js/text-processor-module.js
@@ -13,8 +13,7 @@ class TextProcessorModule extends BaseModule {
this.hyphenatorReady = false;
this.locale = 'en-us';
- // Add localization as a dependency
- this.dependencies = ['localization'];
+ this.dependencies = ['localization', 'game-config'];
// Bind methods using parent's bindMethods utility
this.bindMethods([
@@ -24,9 +23,11 @@ class TextProcessorModule extends BaseModule {
'isHyphenationAvailable',
'hyphenate',
'setLocale',
- 'handleLocaleChanged',
'loadHyphenopolyLoader',
- 'normalizeHyphenationLocale'
+ 'normalizeHyphenationLocale',
+ 'applyLocaleTypography',
+ 'getTypographyLocale',
+ 'normalizeDialogueQuotes'
]);
}
@@ -38,18 +39,15 @@ class TextProcessorModule extends BaseModule {
try {
this.reportProgress(10, "Initializing text processor");
- // Get locale from Localization module if available
- const localizationModule = this.getModule('localization');
- if (!localizationModule) {
- console.error("Localization module not found, required dependency missing");
- this.reportProgress(100, "Text processor initialization failed - missing localization");
- return false;
- }
-
- this.locale = localizationModule.getLocale();
-
- // Register for locale changes using the proper event pattern
- this.addEventListener(document, 'locale-changed', this.handleLocaleChanged);
+ const gameConfig = this.getModule('game-config');
+ this.locale = gameConfig?.getLocale?.() || 'en_US';
+
+ this.addEventListener(document, 'game:config', (event) => {
+ const gameLocale = event.detail?.metadata?.language || event.detail?.locale;
+ if (gameLocale) {
+ this.setLocale(gameLocale);
+ }
+ });
this.reportProgress(30, `Locale set to ${this.locale}`);
@@ -92,16 +90,6 @@ class TextProcessorModule extends BaseModule {
}
}
- /**
- * Handle locale changed event
- * @param {CustomEvent} event - The locale-changed event
- */
- handleLocaleChanged(event) {
- if (event && event.detail && event.detail.locale) {
- this.setLocale(event.detail.locale);
- }
- }
-
/**
* Set the locale for the text processor
* @param {string} locale - The locale to set
@@ -299,6 +287,10 @@ class TextProcessorModule extends BaseModule {
result = this.smartyPants(result);
}
+ if (opts.smartypants) {
+ result = this.applyLocaleTypography(result);
+ }
+
// Apply hyphenation if available and requested
if (opts.hyphenate && this.isHyphenationAvailable()) {
result = this.hyphenate(result, opts.hyphenSelector);
@@ -306,6 +298,25 @@ class TextProcessorModule extends BaseModule {
return result;
}
+
+ applyLocaleTypography(text) {
+ const locale = this.getTypographyLocale();
+ if (locale.startsWith('de')) {
+ return this.normalizeDialogueQuotes(text);
+ }
+
+ return text;
+ }
+
+ getTypographyLocale() {
+ return String(this.locale || 'en_US').trim().toLowerCase().replace('_', '-');
+ }
+
+ normalizeDialogueQuotes(text) {
+ return String(text || '')
+ .replace(/&(ldquo|bdquo|laquo|raquo);([^&\n]+?)&(rdquo|ldquo|laquo|raquo);/gi, '»$2«')
+ .replace(/["\u201c\u201e\u201d\u00ab\u00bb]([^"\u201c\u201e\u201d\u00ab\u00bb\n]+?)["\u201c\u201d\u201e\u00ab\u00bb]/g, '»$1«');
+ }
}
// Create the singleton instance
diff --git a/public/js/tts-factory-module.js b/public/js/tts-factory-module.js
index 0dfa5c3..e0c4ea2 100644
--- a/public/js/tts-factory-module.js
+++ b/public/js/tts-factory-module.js
@@ -14,6 +14,7 @@ class TTSFactoryModule extends BaseModule {
this.dependencies = [
'persistence-manager',
'localization',
+ 'game-config',
'browser-tts', // Browser TTS handler
'kokoro-tts', // Kokoro TTS handler
'elevenlabs-tts',// ElevenLabs TTS handler
@@ -24,7 +25,7 @@ class TTSFactoryModule extends BaseModule {
this.activeHandler = null;
this.ttsAvailable = false;
this.speed = 1; // Speech speed multiplier. 1.0 is normal speed.
- this.language = 'en-us';
+ this.language = 'en_US';
this.voice = '';
this.volume = 1.0;
@@ -224,9 +225,10 @@ class TTSFactoryModule extends BaseModule {
}
});
- document.addEventListener('locale-changed', (event) => {
- if (event.detail?.locale) {
- this.configure({ language: event.detail.locale });
+ document.addEventListener('game:config', (event) => {
+ const language = event.detail?.metadata?.language || event.detail?.locale;
+ if (language) {
+ this.configure({ language, persistLanguage: false });
}
});
@@ -403,7 +405,7 @@ class TTSFactoryModule extends BaseModule {
'preferred_handler': 'none', // Development default: TTS disabled
'enabled': false, // TTS disabled by default
'voice': '', // Empty default - will be selected based on handler
- 'language': 'en-US', // Default language
+ 'language': 'en_US', // Legacy stored value; game metadata now owns active TTS language
'volume': 1.0, // Default volume
'elevenlabs_api_key': '', // Empty API key by default
'elevenlabs_api_url': 'https://api.elevenlabs.io/v1', // Default ElevenLabs API URL
@@ -433,7 +435,8 @@ class TTSFactoryModule extends BaseModule {
// Load other preferences we need for initialization
const preferredHandler = persistenceManager.getPreference('tts', 'preferred_handler');
console.log(`TTS Factory: Loaded preferred handler: ${preferredHandler || 'none'}`);
- this.language = persistenceManager.getPreference('tts', 'language', defaults.language);
+ const gameConfig = this.getModule('game-config');
+ this.language = String(gameConfig?.getLocale?.() || defaults.language).replace('_', '-').toLowerCase();
this.voice = persistenceManager.getPreference('tts', 'voice', defaults.voice);
this.volume = persistenceManager.getPreference('tts', 'volume', defaults.volume);
@@ -698,7 +701,7 @@ class TTSFactoryModule extends BaseModule {
if (persistenceManager) {
persistenceManager.updatePreference('tts', 'preferred_handler', id);
this.voice = persistenceManager.getPreference('tts', 'voice', this.voice || '');
- this.language = persistenceManager.getPreference('tts', 'language', this.language || 'en-us');
+ this.language = String(this.getModule('game-config')?.getLocale?.() || this.language || 'en_US').replace('_', '-').toLowerCase();
this.speed = persistenceManager.getPreference('tts', 'speed', this.speed || 1.0);
}
@@ -788,7 +791,7 @@ class TTSFactoryModule extends BaseModule {
// Not cached, generate and cache
if (typeof handler.preloadSpeech === 'function') {
console.log(`TTS Factory: Generating and caching speech for hash ${hash}`);
- const preloadData = await handler.preloadSpeech(text);
+ const preloadData = await handler.preloadSpeech(text, options);
if (preloadData && preloadData.success) {
// Cache the speech
await this.cacheSpeech(hash, preloadData.audioData, preloadData.duration);
@@ -822,7 +825,7 @@ class TTSFactoryModule extends BaseModule {
* @param {number} [priority=5] - Priority for preloading (1-10, higher is more important)
* @returns {Promise} - Preloaded speech data
*/
- async preloadSpeech(text, priority = 5) {
+ async preloadSpeech(text, options = {}) {
// Check if we have an active handler
if (!this.activeHandler || !this.ttsAvailable) {
console.warn('TTS Factory: Cannot preload speech - no active handler or TTS not available');
@@ -855,7 +858,7 @@ class TTSFactoryModule extends BaseModule {
// If the handler has a preloadSpeech method, use it
if (typeof this.handlers[this.activeHandler].preloadSpeech === 'function') {
- const preloadData = await this.handlers[this.activeHandler].preloadSpeech(text);
+ const preloadData = await this.handlers[this.activeHandler].preloadSpeech(text, options);
// Cache the generated speech data (extract audioData from result object)
if (preloadData && preloadData.audioData) {
@@ -1169,9 +1172,9 @@ class TTSFactoryModule extends BaseModule {
}
if (typeof options.language === 'string' && options.language) {
- this.language = options.language.toLowerCase();
+ this.language = options.language.replace('_', '-').toLowerCase();
voiceOptions.language = this.language;
- if (persistenceManager) {
+ if (persistenceManager && options.persistLanguage === true) {
persistenceManager.updatePreference('tts', 'language', this.language);
}
}
@@ -1215,7 +1218,7 @@ class TTSFactoryModule extends BaseModule {
* @param {string} text - Text to preload
* @returns {Promise} - Resolves with preloaded speech data
*/
- async preloadSpeech(text) {
+ async preloadSpeech(text, options = {}) {
if (!this.activeHandler) {
console.warn("TTS Factory: No active TTS handler for preload");
return null;
@@ -1243,7 +1246,7 @@ class TTSFactoryModule extends BaseModule {
// If the handler has a preloadSpeech method, use it
if (typeof this.handlers[this.activeHandler].preloadSpeech === 'function') {
- const preloadData = await this.handlers[this.activeHandler].preloadSpeech(text);
+ const preloadData = await this.handlers[this.activeHandler].preloadSpeech(text, options);
// Cache the generated speech data (extract audioData from result object)
if (preloadData && preloadData.audioData) {
@@ -1313,7 +1316,11 @@ class TTSFactoryModule extends BaseModule {
// If the handler has a speakPreloaded method, use it
if (typeof this.handlers[this.activeHandler].speakPreloaded === 'function') {
- return await this.handlers[this.activeHandler].speakPreloaded(preloadData, options);
+ return await this.handlers[this.activeHandler].speakPreloaded(preloadData, result => {
+ document.dispatchEvent(new CustomEvent('tts:speechCompleted', {
+ detail: { success: result?.success === true, error: result?.error }
+ }));
+ });
} else {
console.warn(`TTS Factory: Handler ${this.activeHandler} does not support speaking preloaded data`);
return false;
diff --git a/public/js/ui-display-handler-module.js b/public/js/ui-display-handler-module.js
index 1d5aacf..1ab1a02 100644
--- a/public/js/ui-display-handler-module.js
+++ b/public/js/ui-display-handler-module.js
@@ -48,6 +48,10 @@ class UIDisplayHandlerModule extends BaseModule {
this.lastManualScrollAt = 0;
this.layoutFlowLine = 0;
this.layoutExclusions = [];
+ this.notificationQueue = [];
+ this.notificationActive = false;
+ this.pendingTerminalNotifications = [];
+ this.latestInputMode = 'text';
// Resources to preload
this.cssPath = '/css/style.css';
@@ -121,7 +125,19 @@ class UIDisplayHandlerModule extends BaseModule {
'measureText',
'loadCSS',
'showChoices',
- 'preloadImages'
+ 'preloadImages',
+ 'createCreditsDialog',
+ 'openCreditsDialog',
+ 'closeCreditsDialog',
+ 'loadCreditsText',
+ 'createNotificationDialog',
+ 'handleStoryTag',
+ 'getTagMessage',
+ 'showNotification',
+ 'displayNextNotification',
+ 'queueTerminalNotification',
+ 'flushTerminalNotifications',
+ 'closeNotification'
]);
console.log('UIDisplayHandler: Constructor initialized');
@@ -173,6 +189,15 @@ class UIDisplayHandlerModule extends BaseModule {
this.addEventListener(document, 'story:history-updated', (event) => {
this.updateStoryScrollbar(event.detail || {});
});
+ this.addEventListener(document, 'story:tag', (event) => {
+ this.handleStoryTag(event.detail);
+ });
+ this.addEventListener(document, 'story:turn-start', () => {
+ this.latestInputMode = 'text';
+ });
+ this.addEventListener(document, 'story:input-mode', (event) => {
+ this.latestInputMode = event.detail || 'text';
+ });
this.addEventListener(document, 'wheel', this.handleHistoryWheel, { passive: false });
this.addEventListener(document, 'keydown', (event) => {
const tagName = String(event.target?.tagName || '').toLowerCase();
@@ -213,6 +238,9 @@ class UIDisplayHandlerModule extends BaseModule {
? this.t('title.continueHint')
: this.t('title.fastForwardHint');
}
+ if (state === 'ready' && this.latestInputMode === 'end') {
+ this.flushTerminalNotifications();
+ }
});
if (window.ResizeObserver && this.paragraphContainer) {
@@ -472,6 +500,9 @@ class UIDisplayHandlerModule extends BaseModule {
lighting.id = 'lighting';
document.body.appendChild(lighting);
}
+
+ this.createCreditsDialog();
+ this.createNotificationDialog();
console.log('UIDisplayHandler: All containers initialized');
this.applyGameConfig(this.gameConfig?.getConfig?.());
@@ -497,7 +528,27 @@ class UIDisplayHandlerModule extends BaseModule {
metadata.version ? this.t('title.version', { version: metadata.version }) : '',
metadata.copyright || ''
].filter(Boolean);
- legalElement.textContent = items.join(' · ');
+ legalElement.innerHTML = '';
+ const legalText = document.createElement('span');
+ legalText.id = 'game_legal_text';
+ legalText.textContent = items.join(' | ');
+ legalElement.appendChild(legalText);
+
+ const creditsButton = document.createElement('button');
+ creditsButton.id = 'credits_button';
+ creditsButton.type = 'button';
+ creditsButton.textContent = this.t('credits.button');
+ creditsButton.title = this.t('credits.buttonTitle');
+ creditsButton.addEventListener('click', (event) => {
+ event.preventDefault();
+ event.stopPropagation();
+ this.openCreditsDialog();
+ });
+
+ if (items.length > 0) {
+ legalElement.appendChild(document.createTextNode(' | '));
+ }
+ legalElement.appendChild(creditsButton);
}
}
@@ -522,6 +573,9 @@ class UIDisplayHandlerModule extends BaseModule {
setText('options', 'topbar.options');
setText('remark_text', 'title.fastForwardHint');
setText('start_prompt', 'title.startPrompt');
+ setText('credits_dialog_title', 'credits.title');
+ setText('credits_close', 'credits.close');
+ setText('story_popup_ok', 'popup.ok');
setTitle('speech', 'topbar.speechTitle');
setTitle('autoplay', 'topbar.autoplayTitle');
setTitle('rewind', 'topbar.newGameTitle');
@@ -533,6 +587,224 @@ class UIDisplayHandlerModule extends BaseModule {
if (input) input.setAttribute('placeholder', this.t('input.placeholder'));
this.applyGameConfig(this.gameConfig?.getConfig?.());
}
+
+ createCreditsDialog() {
+ if (document.getElementById('credits_modal')) {
+ return;
+ }
+
+ const modal = document.createElement('div');
+ modal.id = 'credits_modal';
+ modal.className = 'credits-modal';
+ modal.setAttribute('aria-hidden', 'true');
+ modal.innerHTML = `
+
+ `;
+
+ document.body.appendChild(modal);
+
+ modal.addEventListener('click', (event) => {
+ if (event.target === modal) {
+ this.closeCreditsDialog();
+ }
+ });
+
+ const closeButton = document.getElementById('credits_close');
+ if (closeButton) {
+ closeButton.addEventListener('click', () => this.closeCreditsDialog());
+ }
+ }
+
+ async openCreditsDialog() {
+ const modal = document.getElementById('credits_modal');
+ const content = document.getElementById('credits_content');
+ if (!modal || !content) {
+ return;
+ }
+
+ modal.classList.add('visible');
+ modal.setAttribute('aria-hidden', 'false');
+
+ if (!content.dataset.loaded) {
+ content.textContent = this.t('credits.loading');
+ content.textContent = await this.loadCreditsText();
+ content.dataset.loaded = 'true';
+ }
+ }
+
+ closeCreditsDialog() {
+ const modal = document.getElementById('credits_modal');
+ if (!modal) {
+ return;
+ }
+ modal.classList.remove('visible');
+ modal.setAttribute('aria-hidden', 'true');
+ }
+
+ async loadCreditsText() {
+ try {
+ const response = await fetch('/THIRD_PARTY_NOTICES.md', { cache: 'no-cache' });
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status}`);
+ }
+ return await response.text();
+ } catch (error) {
+ console.warn('UIDisplayHandler: Failed to load credits notices', error);
+ return this.t('credits.loadFailed');
+ }
+ }
+
+ createNotificationDialog() {
+ if (document.getElementById('story_popup_modal')) {
+ return;
+ }
+
+ const modal = document.createElement('div');
+ modal.id = 'story_popup_modal';
+ modal.className = 'story-popup-modal';
+ modal.setAttribute('aria-hidden', 'true');
+ modal.innerHTML = `
+
+ `;
+
+ document.body.appendChild(modal);
+ modal.addEventListener('click', (event) => {
+ event.preventDefault();
+ event.stopPropagation();
+ if (event.target === modal) {
+ this.closeNotification();
+ }
+ });
+ modal.addEventListener('pointerdown', (event) => {
+ event.stopPropagation();
+ });
+
+ const okButton = document.getElementById('story_popup_ok');
+ if (okButton) {
+ okButton.addEventListener('click', (event) => {
+ event.preventDefault();
+ event.stopPropagation();
+ this.closeNotification();
+ });
+ }
+ }
+
+ handleStoryTag(tag) {
+ const key = String(tag?.key || '').toLowerCase();
+ if (!['score', 'error', 'achievement', 'alert'].includes(key)) {
+ return;
+ }
+
+ const message = this.getTagMessage(tag);
+ if (key === 'score') {
+ this.queueTerminalNotification(
+ 'ending',
+ this.t('popup.endingTitle'),
+ message || this.t('popup.defaultEnding')
+ );
+ } else if (key === 'error') {
+ this.queueTerminalNotification(
+ 'error',
+ this.t('popup.errorTitle'),
+ message || this.t('popup.defaultError')
+ );
+ } else if (key === 'achievement') {
+ this.showNotification(
+ 'achievement',
+ this.t('popup.achievementTitle'),
+ message || this.t('popup.defaultAchievement')
+ );
+ } else if (key === 'alert') {
+ this.showNotification(
+ 'alert',
+ this.t('popup.alertTitle'),
+ message || this.t('popup.defaultAlert')
+ );
+ }
+ }
+
+ getTagMessage(tag) {
+ return [tag?.value, tag?.param]
+ .map((part) => String(part || '').trim())
+ .filter(Boolean)
+ .join('\n');
+ }
+
+ showNotification(kind, title, message) {
+ this.notificationQueue.push({ kind, title, message });
+ this.displayNextNotification();
+ }
+
+ queueTerminalNotification(kind, title, message) {
+ this.pendingTerminalNotifications.push({ kind, title, message });
+ if (this.latestInputMode === 'end') {
+ this.flushTerminalNotifications();
+ }
+ }
+
+ flushTerminalNotifications() {
+ if (this.pendingTerminalNotifications.length === 0) {
+ return;
+ }
+ this.pendingTerminalNotifications.splice(0).forEach((notification) => {
+ this.showNotification(notification.kind, notification.title, notification.message);
+ });
+ }
+
+ displayNextNotification() {
+ if (this.notificationActive || this.notificationQueue.length === 0) {
+ return;
+ }
+
+ const next = this.notificationQueue.shift();
+ const modal = document.getElementById('story_popup_modal');
+ const title = document.getElementById('story_popup_title');
+ const message = document.getElementById('story_popup_message');
+ const okButton = document.getElementById('story_popup_ok');
+ if (!modal || !title || !message) {
+ return;
+ }
+
+ modal.dataset.kind = next.kind;
+ title.textContent = next.title;
+ message.textContent = next.message;
+ if (okButton) {
+ okButton.textContent = this.t('popup.ok');
+ setTimeout(() => okButton.focus(), 0);
+ }
+ this.notificationActive = true;
+ modal.classList.add('visible');
+ modal.setAttribute('aria-hidden', 'false');
+ }
+
+ closeNotification() {
+ const modal = document.getElementById('story_popup_modal');
+ if (!modal) {
+ this.notificationActive = false;
+ return;
+ }
+ modal.classList.remove('visible');
+ modal.setAttribute('aria-hidden', 'true');
+ this.notificationActive = false;
+ setTimeout(() => this.displayNextNotification(), 0);
+ }
/**
* Measure text width using canvas
@@ -1927,6 +2199,11 @@ class UIDisplayHandlerModule extends BaseModule {
this.container.appendChild(this.paragraphContainer);
}
this.renderedItems = [];
+ this.notificationQueue = [];
+ this.pendingTerminalNotifications = [];
+ this.notificationActive = false;
+ document.getElementById('story_popup_modal')?.classList.remove('visible');
+ document.getElementById('story_popup_modal')?.setAttribute('aria-hidden', 'true');
this.historyWindowStartId = 1;
this.historyWindowEndId = 0;
this.storyTopLine = 0;
diff --git a/public/locales/de_DE.json b/public/locales/de_DE.json
index cdea2a2..373c505 100644
--- a/public/locales/de_DE.json
+++ b/public/locales/de_DE.json
@@ -1,8 +1,8 @@
{
- "title.byAuthor": "von {{author}}",
+ "title.byAuthor": "{{author}}",
"title.version": "Version {{version}}",
- "title.fastForwardHint": "auf die Seite klicken oder Leertaste druecken, um die Textanimation vorzuspulen",
- "title.continueHint": "auf die Seite klicken oder Leertaste druecken, um fortzufahren",
+ "title.fastForwardHint": "auf die Seite klicken oder Leertaste drücken, um die Textanimation vorzuspulen",
+ "title.continueHint": "auf die Seite klicken oder Leertaste drücken, um fortzufahren",
"title.startPrompt": "Klicke auf Neues Spiel oder Laden, um das Spiel zu starten",
"topbar.speech": "Sprache",
"topbar.autoplay": "Auto",
@@ -21,7 +21,8 @@
"options.title": "Optionen",
"options.close": "Schliessen",
"options.applicationSettings": "Anwendung",
- "options.language": "Sprache",
+ "options.language": "UI-Sprache",
+ "options.gameLanguage": "Spieltext-Sprache",
"options.speech": "Sprachausgabe",
"options.enableSpeech": "Sprachausgabe aktivieren",
"options.provider": "Anbieter",
@@ -33,8 +34,34 @@
"options.speechVolume": "Sprachlautstaerke",
"options.musicVolume": "Musiklautstaerke",
"options.sfxVolume": "Effektlautstaerke",
+ "options.musicDucking": "Musikabsenkung",
+ "options.muteMasterVolume": "Gesamtaudio ausschalten",
+ "options.unmuteMasterVolume": "Gesamtaudio einschalten",
+ "options.muteSpeechVolume": "Sprachausgabe ausschalten",
+ "options.unmuteSpeechVolume": "Sprachausgabe einschalten",
+ "options.muteMusicVolume": "Musik ausschalten",
+ "options.unmuteMusicVolume": "Musik einschalten",
+ "options.muteSfxVolume": "Soundeffekte ausschalten",
+ "options.unmuteSfxVolume": "Soundeffekte einschalten",
+ "options.disableMusicDucking": "Musikabsenkung ausschalten",
+ "options.enableMusicDucking": "Musikabsenkung einschalten",
"options.elevenLabsSettings": "ElevenLabs API-Einstellungen",
"options.openAiSettings": "OpenAI API-Einstellungen",
"options.apiKey": "API-Schluessel",
- "options.apiUrl": "API-URL"
+ "options.apiUrl": "API-URL",
+ "credits.button": "Credits",
+ "credits.buttonTitle": "Credits und Lizenzen anzeigen",
+ "credits.title": "Credits und Lizenzen",
+ "credits.close": "Schliessen",
+ "credits.loading": "Credits werden geladen...",
+ "credits.loadFailed": "Credits und Lizenzhinweise konnten nicht geladen werden.",
+ "popup.ok": "OK",
+ "popup.endingTitle": "Ende erreicht",
+ "popup.errorTitle": "Spiel beendet",
+ "popup.achievementTitle": "Errungenschaft",
+ "popup.alertTitle": "Hinweis",
+ "popup.defaultEnding": "Du hast ein Ende erreicht.",
+ "popup.defaultError": "Das Spiel wurde wegen eines nicht behebbaren Fehlers beendet.",
+ "popup.defaultAchievement": "Errungenschaft freigeschaltet.",
+ "popup.defaultAlert": "Hinweis"
}
diff --git a/public/locales/en_US.json b/public/locales/en_US.json
index 95a6993..a460c21 100644
--- a/public/locales/en_US.json
+++ b/public/locales/en_US.json
@@ -1,5 +1,5 @@
{
- "title.byAuthor": "by {{author}}",
+ "title.byAuthor": "{{author}}",
"title.version": "Version {{version}}",
"title.fastForwardHint": "click on page or press spacebar to fast forward text animation",
"title.continueHint": "click on page or press spacebar to continue",
@@ -21,7 +21,8 @@
"options.title": "Options",
"options.close": "Close",
"options.applicationSettings": "Application Settings",
- "options.language": "Language",
+ "options.language": "UI Language",
+ "options.gameLanguage": "Game Text Language",
"options.speech": "Speech",
"options.enableSpeech": "Enable text to speech",
"options.provider": "Provider",
@@ -33,8 +34,34 @@
"options.speechVolume": "Speech Volume",
"options.musicVolume": "Music Volume",
"options.sfxVolume": "Sound Effects Volume",
+ "options.musicDucking": "Music Ducking",
+ "options.muteMasterVolume": "Disable master audio",
+ "options.unmuteMasterVolume": "Enable master audio",
+ "options.muteSpeechVolume": "Disable speech audio",
+ "options.unmuteSpeechVolume": "Enable speech audio",
+ "options.muteMusicVolume": "Disable music audio",
+ "options.unmuteMusicVolume": "Enable music audio",
+ "options.muteSfxVolume": "Disable sound effects",
+ "options.unmuteSfxVolume": "Enable sound effects",
+ "options.disableMusicDucking": "Disable music ducking",
+ "options.enableMusicDucking": "Enable music ducking",
"options.elevenLabsSettings": "ElevenLabs API Settings",
"options.openAiSettings": "OpenAI API Settings",
"options.apiKey": "API Key",
- "options.apiUrl": "API URL"
+ "options.apiUrl": "API URL",
+ "credits.button": "credits",
+ "credits.buttonTitle": "Show credits and third-party licenses",
+ "credits.title": "Credits and Licenses",
+ "credits.close": "Close",
+ "credits.loading": "Loading credits...",
+ "credits.loadFailed": "Credits and license notices could not be loaded.",
+ "popup.ok": "OK",
+ "popup.endingTitle": "Ending reached",
+ "popup.errorTitle": "Game ended",
+ "popup.achievementTitle": "Achievement",
+ "popup.alertTitle": "Hint",
+ "popup.defaultEnding": "You reached an ending.",
+ "popup.defaultError": "The game ended because of an unrecoverable error.",
+ "popup.defaultAchievement": "Achievement unlocked.",
+ "popup.defaultAlert": "Hint"
}
diff --git a/public/music/README.md b/public/music/README.md
index dfd8aa5..ed6d139 100644
--- a/public/music/README.md
+++ b/public/music/README.md
@@ -23,6 +23,8 @@ Supported playback flags:
Music volume is controlled by master volume and music volume in the options menu. While TTS is playing, music is ducked to 70% of its configured volume and restored when TTS playback is idle.
+The ducking amount is configurable in the options menu and can be disabled with the music-ducking mute toggle. Music state is saved with browser savegames when a track is active, including the current playback position, and restored with a fade-in on load.
+
Browsers require a user interaction before audio can play, so music should begin after `new game` or `load`, not during passive page load.
Document third-party source and license information here or next to the file.
diff --git a/public/sounds/README.md b/public/sounds/README.md
index bc50c8a..7903884 100644
--- a/public/sounds/README.md
+++ b/public/sounds/README.md
@@ -7,6 +7,7 @@ Use a sound effect story tag:
```text
#sfx[squeaky-door.ogg]
+#sfx[church-bells.ogg](max=8 fade fade-duration=2)
The old door opens into the dark.
```
@@ -16,6 +17,15 @@ Supported browser-friendly formats are recommended: `.ogg`, `.mp3`, and `.wav`.
Sound effect loudness is controlled by the master volume and sound effects volume sliders in the options menu.
+Supported timing/end options:
+
+- `max=`, `duration=`, `max-duration=`, `limit=`, `stop-after=`: stop after this many seconds.
+- `fade-after=`: fade after this many seconds.
+- bare seconds such as `4s`: shorthand for a maximum duration.
+- `fade`, `fadeout`, `fade-out`, or `mode=fade`: fade out when the limit is reached.
+- `stop`, `cut`, `halt`, or `mode=stop`: stop immediately when the limit is reached.
+- `fade-duration=` or `fade-time=`: fade length in seconds.
+
Document third-party source and license information here or next to the file.
Current assets:
diff --git a/src/config/game-config.ts b/src/config/game-config.ts
index b6deb15..a752013 100644
--- a/src/config/game-config.ts
+++ b/src/config/game-config.ts
@@ -9,6 +9,7 @@ export interface GameMetadata {
subtitle?: string;
version?: string;
copyright?: string;
+ language?: string;
}
export interface GamePaths {
@@ -52,6 +53,7 @@ function fallbackConfig(engine: EngineName): GameEngineConfig {
subtitle: 'An open-world text adventure',
version: '1.0.0',
copyright: '',
+ language: 'en_US',
},
};
}
@@ -81,6 +83,7 @@ export function loadGameConfig(configPath: string, engine: EngineName): GameEngi
metadata: {
...fallback.metadata,
...(parsed.metadata ?? {}),
+ language: parsed.metadata?.language ?? parsed.locale ?? fallback.metadata.language,
},
};
}
diff --git a/src/engine/ink-engine.ts b/src/engine/ink-engine.ts
index 50a9dec..ee20151 100644
--- a/src/engine/ink-engine.ts
+++ b/src/engine/ink-engine.ts
@@ -150,11 +150,13 @@ export class InkEngine {
const paragraphs: TurnResult['paragraphs'] = [];
const globalTags: StoryTag[] = [];
+ const turnTags: StoryTag[] = [];
while (this.story.canContinue) {
const rawText = this.story.Continue();
const text = String(rawText || '').trim();
const tags = parseTags(this.story.currentTags || []);
+ turnTags.push(...tags);
tags
.filter((tag) => tag.key === 'title' || tag.key === 'author')
@@ -170,7 +172,7 @@ export class InkEngine {
const choices = this.story.currentChoices.map((choice): ChoiceResult => {
const tags = parseTags(choice.tags || []);
const category = getTagValue(tags, 'action');
- const letter = getTagValue(tags, 'letter');
+ const letter = getTagValue(tags, 'letter') || getTagValue(tags, 'key');
return {
index: choice.index,
text: String(choice.text || '').trim(),
@@ -179,13 +181,46 @@ export class InkEngine {
letter,
};
});
+ const inputMode = choices.length > 0 ? 'choice' : 'end';
+ const gameState: TurnResult['gameState'] = {};
+
+ if (inputMode === 'end') {
+ const errorTag = turnTags.find((tag) => tag.key === 'error');
+ const scoreTag = turnTags.find((tag) => tag.key === 'score');
+
+ if (!errorTag && !scoreTag) {
+ const message = 'Ink story ended without an explicit #score ending tag.';
+ const generatedErrorTag: StoryTag = { key: 'error', value: message };
+ globalTags.push(generatedErrorTag);
+ turnTags.push(generatedErrorTag);
+ }
+
+ const finalErrorTag = turnTags.find((tag) => tag.key === 'error');
+ const finalScoreTag = turnTags.find((tag) => tag.key === 'score');
+ if (finalErrorTag) {
+ gameState.endState = {
+ type: 'error',
+ message: finalErrorTag.value || finalErrorTag.param,
+ };
+ } else if (finalScoreTag) {
+ const numericScore = Number(finalScoreTag?.value);
+ if (Number.isFinite(numericScore)) {
+ gameState.score = numericScore;
+ }
+ gameState.endState = {
+ type: 'intended',
+ message: finalScoreTag.value || finalScoreTag.param,
+ };
+ }
+ }
return {
turnId: this.nextTurnId++,
paragraphs,
choices,
- inputMode: choices.length > 0 ? 'choice' : 'end',
+ inputMode,
globalTags: globalTags.length > 0 ? globalTags : undefined,
+ gameState: Object.keys(gameState).length > 0 ? gameState : undefined,
};
}
}
diff --git a/src/interfaces/turn-result.ts b/src/interfaces/turn-result.ts
index e2b9146..bf6521e 100644
--- a/src/interfaces/turn-result.ts
+++ b/src/interfaces/turn-result.ts
@@ -35,6 +35,10 @@ export interface TurnResult {
score?: number;
moves?: number;
statusLine?: string;
+ endState?: {
+ type: 'intended' | 'error';
+ message?: string;
+ };
};
suggestions?: string[];
}
diff --git a/src/utils/tag-parser.ts b/src/utils/tag-parser.ts
index 70c5149..7871676 100644
--- a/src/utils/tag-parser.ts
+++ b/src/utils/tag-parser.ts
@@ -23,6 +23,14 @@ export function parseTag(raw: string): StoryTag | null {
return tag;
}
+ const colonMatch = text.match(/^([A-Za-z][\w-]*)\s*:\s*(.*?)\s*(?:\(([^)]*)\))?$/);
+ if (colonMatch) {
+ const tag: StoryTag = { key: normalizeKey(colonMatch[1]) };
+ tag.value = colonMatch[2].trim();
+ if (typeof colonMatch[3] !== 'undefined') tag.param = colonMatch[3].trim();
+ return tag;
+ }
+
const bareMatch = text.match(/^[A-Za-z][\w-]*$/);
if (bareMatch) {
return { key: normalizeKey(text) };