Checkpoint before character generator cleanup

This commit is contained in:
2026-05-25 16:21:44 +02:00
parent eef90f3471
commit 40ef48b279
80 changed files with 21597 additions and 744 deletions
+92 -9
View File
@@ -8,6 +8,8 @@ This file documents author-facing Ink tag conventions. The active parser normali
All Ink source and UI localization files must be UTF-8 and use real German characters directly: `ä`, `ö`, `ü`, `Ä`, `Ö`, `Ü`, `ß`, `„…“`. Do not replace them with `ae`, `oe`, `ue`, `ss`, or ASCII quotation marks as an encoding workaround.
Mandatory editing rules for AI/Codex work: no authored text changes through regex, bulk replacement, or scripted rewrites. Text edits must use `apply_patch`. PowerShell commands must set UTF-8 output before reading or displaying text. Before large text work, create a git check-in. Edit and inspect entries one by one, sequentially, without generated shortcuts.
Eibenreith uses a bucket architecture. This is mandatory for authored content, not a suggestion. The active choice surface is collected in this priority order:
1. Moment bucket
@@ -25,12 +27,28 @@ Every room is declared through `enter_room(location, entry, look, exits, bucket)
Bucket content must be written as complete conditioned weaves directly inside the bucket. Do not split ordinary bucket choices into one-line `_choices` knots or pass-through knots. A separate knot is allowed only when the content is genuinely reused or when the exception is explicitly justified, such as the identity papers tunnel in `character_generator.ink`.
The canonical atom header format is:
```ink
* {condition_one}
{condition_two}
[__Verb Charakter__: "..."]
Text and nested weave content.
-> TURN
```
Each precondition gets its own header line directly after the choice marker. The choice text gets its own header line after the preconditions. Do not put naked condition lines before the choice; they compile, but they do not gate the choice. Do not put visibility conditions inside the branch body.
Valid examples:
```ink
=== train_washroom_bucket ===
* {lacks(face_def)} [AUTO: Spiegelbild im Waschraum] #auto
* {lacks(face_def)}
[AUTO: Spiegelbild im Waschraum] #auto
-> washroom_mirror_character_generator
* {lacks(washroom_toilet_used)} [__Benutze__: Die Toilette. #key:t]
* {lacks(washroom_toilet_used)}
[__Benutze__: Die Toilette. #key:t]
~ mark(washroom_toilet_used)
Du schiebst den Riegel vor und nimmst dir die Zeit, die eine Reise dem Körper selten höflich anbietet.
-> TURN
@@ -84,6 +102,7 @@ Author-facing helper functions live in `data/ink-src/eibenreith/helpers.ink` and
- `mark`, `has`, `lacks`: exact checklist facts.
- `tutorial`: returns true once and marks the tutorial as shown.
- `claim_choice_gate_if(gate, available)`: transient choice-surface arbitration. Use only to allow at most one valid choice from a prioritized family, especially `#auto` groups.
- `timer_start(timer_id, turns)`, `timer_due(timer_id)`, `timer_due_if(timer_id, available)`, `timer_claim(timer_id)`: named turn timers for delayed events.
- `rel_*`: relationship counters and two-value relationship-axis queries.
Relationship counters use only the standard value pairs declared in `characters.ink`:
@@ -113,7 +132,8 @@ Use a named Ink callback when later content remembers one concrete earlier choic
* (send_carriage_ahead_from_village) [__Verfüge__: Die Kutsche mit Gepäck und Nachricht vorausschicken.]
...
* {village_detour_exits.send_carriage_ahead_from_village} [__Erinnere__: An die vorausgeschickte Kutsche.]
* {village_detour_exits.send_carriage_ahead_from_village}
[__Erinnere__: An die vorausgeschickte Kutsche.]
```
Use a separate semantic `LIST` with the `state_*` helpers whenever a tracker expresses a linear process. This also applies to small two-state processes such as "begun" and "completed"; if completion implies beginning, it is a progress tracker, not a pair of loose facts.
@@ -137,11 +157,13 @@ Use route and relationship helpers only as heuristics. They should color tone, s
Use it when several choices can be valid at the same time but the surface must offer only one of them. The main use case is prioritized `#auto` families:
```ink
+ {claim_choice_gate_if(return_auto, reunion(viktor) && state_reached(freshen_up_done))} [AUTO: Viktors Rückkehr nach Frischmachen] #auto
+ {claim_choice_gate_if(return_auto, reunion(viktor) && state_reached(freshen_up_done))}
[AUTO: Viktors Rückkehr nach Frischmachen] #auto
...
-> TURN
+ {claim_choice_gate_if(return_auto, reunion(viktor) && state_reached(explore_train_done))} [AUTO: Viktors Rückkehr nach Erkundung] #auto
+ {claim_choice_gate_if(return_auto, reunion(viktor) && state_reached(explore_train_done))}
[AUTO: Viktors Rückkehr nach Erkundung] #auto
...
-> TURN
```
@@ -168,13 +190,71 @@ Use:
`first_meeting`, `reunion`, and `parting` are transition checks. They are true only for the first choice surface after the transition happened. The next turn clears them centrally in `provide_choices`. Authors must not call `contact_clear_transitions()` or any other cleanup helper from content. This makes the transitions suitable for immediate one-shot auto reactions:
```ink
+ {claim_choice_gate_if(return_auto, reunion(viktor) && state_reached(freshen_up_done))} [AUTO: Viktors Rückkehr nach Frischmachen] #auto
+ {claim_choice_gate_if(return_auto, reunion(viktor) && state_reached(freshen_up_done))}
[AUTO: Viktors Rückkehr nach Frischmachen] #auto
...
-> TURN
```
Companions are characters in the `companions` list. When Valerie traverses with `loc_move_to(...)`, companions automatically move to the new location before contact is updated. `companion_join(character)` and `companion_leave(character)` only change whether a character follows Valerie; they are not story-memory flags. Character starting positions and initial companion state belong in episode setup. If several characters are placed manually before play resumes, call `contact_sync()` once after setup to establish contact without firing a meeting or reunion reaction.
Episode setup may install a companion transition bucket through `enter_episode(value, slot, start_bucket, end_bucket, episode_bucket, companion_transition_bucket)`. `enter_room(...)` plays this bucket centrally after movement and before room content is installed. Use it for prose that describes how current companions traverse with Valerie, so individual exits do not need companion boilerplate. Such buckets should usually contain only conditioned prose:
```ink
=== train_companion_transition_bucket ===
{
- accompanied_by(viktor):
{
- traversal_between(loc_train_home_corridor, loc_train_clergy_corridor):
Viktor folgt dir über die schwankende Verbindung.
}
}
->->
```
Use `traversal_from(location)`, `traversal_to(location)`, or `traversal_between(origin, destination)` only inside companion transition buckets. Normal room, episode, and game buckets should gate on `loc(...)`, `present(...)`, `reunion(...)`, and semantic story state instead.
## Room Look Lifecycle
Room look content belongs in the room's look bucket passed to `enter_room(...)`. The Ink room engine exposes that bucket only after Valerie has left a room and re-entered it. The look choice then disappears after it is used and becomes available again on the next re-entry.
Authors do not add flags for this and do not call a cleanup helper. The shared room engine compares the current room-entry turn with Ink's visit tracking for the active look bucket, so any look bucket selected during the current visit is automatically hidden until the room is re-entered. Room look choices must keep using the `#key:l` convention:
```ink
=== train_compartment_look ===
+ [__Schaue__: Im Abteil umher. #key:l]
...
-> TURN
-> DONE
```
## Turn Timers
Use named timers for delayed events that should advance when any ordinary choice is taken, including unrelated dynamic bucket content. Timer IDs are values in the global `Timer` LIST.
```ink
~ timer_start(train_lunch_order, 3)
+ {timer_due_if(train_lunch_order, state_between(lunch_ordered, lunch_served))}
[AUTO: Der Kellner bringt die Bestellung] #auto
~ state_reach(lunch_served)
-> TURN
```
`timer_start(timer_id, turns)` first removes that timer name from all countdown, ready, and claimed buckets, then starts it again. Durations `1..10` count later player choices; the choice that starts the timer does not consume one of those turns.
When a timer expires, it stays in the ready bucket until claimed. `timer_due(timer_id)` returns true if the timer is ready and moves it to claimed; further checks in the same turn still return true. Claimed timers are cleared automatically on the next `TURN`. Use `timer_due_if(timer_id, available)` when additional story conditions must be true before the timer is claimed.
If chosen content makes the delayed event happen early, use `timer_claim(timer_id)` as part of that event, not as cleanup:
```ink
+ {state_between(lunch_ordered, lunch_served)}
[__Warte__: Auf die Bestellung.]
~ timer_claim(train_lunch_order)
~ state_reach(lunch_served)
-> TURN
```
## Implemented Tag Forms
Use bracket tags for titles, filenames, and longer text:
@@ -266,13 +346,16 @@ The current UI renders all non-auto choices in one visible list. Choices are fir
Auto choices are ordinary Ink choices with a developer-facing choice text in `[...]`. The UI does not show that text in normal play, but Inky needs it for local testing and the text makes the source readable. Ink owns availability and once-only behavior; the UI owns automatic selection and timing. Supported forms:
```ink
* {condition} [AUTO: Ereignisname] #auto
* {condition}
[AUTO: Ereignisname] #auto
-> event
* {condition} [AUTO: Ereignisname] #auto(2)
* {condition}
[AUTO: Ereignisname] #auto(2)
-> event
* {condition} [AUTO: Ereignisname] #auto:tunnel(2)
* {condition}
[AUTO: Ereignisname] #auto:tunnel(2)
-> event
```