Files
Jander_Semester2/Semesterprojekt/docs/superpowers/specs/2026-06-02-content-expansion-design.md
Jean-Luc Makiola 90311a7671 docs: spec for Haunted Manor content expansion (14 rooms)
Condensed 14-room manor on one spine (restore power -> escape) stacking all five
mechanics, plus an optional spirit thread feeding 3 priority endings (no
hard-fail). Pure YAML content on existing engines. Consolidates to one world
(removes the toy test fixture) + a WorldSolvableTest that walks the critical path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 08:48:52 +02:00

221 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Spec: Content-Ausbau — „Haunted Manor" (14 Räume)
Stand: 2026-06-02. Backlog #4 (+ #5b Mechanik-Spine). Erster reiner **Content**-
Ausbau: die Engines stehen alle — diese Erweiterung ist überwiegend **YAML**
(rooms/items/npcs/quests/endings/combinations), plus die nötigen Test-Anpassungen.
## 1. Kontext & Ziel
Die Welt hat heute 5 Räume und eine Mini-Demo. Ziel: eine **kondensierte, designte**
Spielwelt (~14 Räume, 3 Ebenen) mit **einem zentralen Bogen** („Strom wiederherstellen
→ entkommen") der den Spieler raumweise führt und **alle fünf Mechaniken stapelt**,
plus ein optionaler **Geister-Strang** (den ruhelosen Geist zur Ruhe betten) für die
beste Ende-Variante. **Kein Hard-Fail** (keine Tod/Lose-Enden), puzzle-fokussiert.
Bestätigte Entscheidungen (Brainstorming 2026-06-02):
- **Größe**: ~14 Räume, kondensiert/handgebaut (kein Füllmaterial).
- **Zentraler Bogen**: Strom wiederherstellen (drain → fuse → generator → `power_on`)
schaltet das Obergeschoss frei; **leichter Geister-Strang** speist die Enden.
- **Enden**: 3, priorisiert, **kein Hard-Fail**: *Into the Night* (geflohen) /
*Manor Reclaimed* (Strom + raus) / *Spirit at Rest* (Strom + Geist gebannt + raus).
- **Mechanik-Abdeckung**: Licht/Dunkelheit (Keller), verschlossene Exits (Treppe via
Strom, Haustür = Ausgang), Flags, Item-Kombination (mehrfach), Quest-Kette + Enden,
`go to <raum>`, Musik pro Raum.
## 2. Scope
**In Scope (Content, YAML in `src/main/resources/world/`):**
- `rooms.yaml`: 14 Räume, Exits (N/S/E/W), `dark`, `music`, exitLocks, descriptionStates.
- `items.yaml`: ~16 Items (siehe §5), inkl. produzierter (unplatzierter) Items.
- `combinations.yaml`: 5 Rezepte (§6).
- `npcs.yaml`: der `old_man` (Hinweis-NPC, flag-bedingte Dialoge, 1 Reaktion).
- `quests.yaml`: `restore_power` (autoStart, 2 Stages) + `lay_to_rest` (per Effekt gestartet).
- `endings.yaml`: 3 priorisierte Enden.
- `game.yaml`: Titel/Startraum/Welcome aktualisieren (Start = `foyer`).
- `tutorial.yaml`: ggf. minimal anpassen (Start ist jetzt `foyer`; der `go`-Schritt
führt nach Norden in die Grand Hall — bestehende verb-basierte Schritte bleiben gültig).
**In Scope (Tests / eine Welt):**
- **Toy-Test-Fixture entfernen** (`src/test/resources/world/*`), damit die loader-basierten
Tests die **echte ausgelieferte Welt** prüfen (heute beschattet das Fixture die
Produktionswelt → Produktion ist ungetestet). `src/test/resources/badtut/` bleibt
(separate Basis für den Malformed-Test).
- `WorldLoaderTest` + `TutorialLoaderTest` an die echte Welt anpassen (Form-Invarianten
statt Toy-Fixture-Werte).
- Neuer **`WorldSolvableTest`**: läuft den kritischen Pfad programmatisch (Bewegung +
`Combinations.tryUse` + Flag-Checks) und prüft, dass `power_on`, `ghost_banished` und
ein Sieg-Ende erreichbar sind — automatische Lösbarkeits-Garantie.
**Out of Scope:**
- Engine-/Code-Änderungen (außer Tests) — alle Mechaniken existieren bereits.
- Audiodateien (externer `music/`-Ordner; YAML referenziert nur Namen).
- Neue Mechaniken (Zahlenschloss/Code-Eingabe etc.) — bewusst vermieden (nur
vorhandene item+item-Kombinationen, Flags, Locks, Licht).
- Hard-Fail/Tod-Enden.
## 3. Weltkarte (Kompass-Exits, ≤4 pro Raum — keine UP/DOWN; `Direction` ist N/S/E/W)
Ebenen sind abstrakt über Kompassrichtungen verdrahtet (wie heute Keller=Süd etc.).
| Raum | Exits | dark | Items | NPC | Lock |
|---|---|---|---|---|---|
| **foyer** (START) | N→grand_hall | | front_door, **lamp** | | |
| **grand_hall** | S→foyer, N→dining_room, E→kitchen, W→library | | | | |
| **kitchen** | W→grand_hall, S→cellar_stairs | | matches, letter | | |
| **library** | E→grand_hall | | shovel, journal | old_man | |
| **dining_room** | S→grand_hall, N→conservatory, E→grand_staircase | | torch, portrait | | |
| **conservatory** | S→dining_room | | valve_wheel | | |
| **grand_staircase** | W→dining_room, N→landing | | | | **N→landing requires `power_on`** |
| **cellar_stairs** | N→kitchen, E→flooded_cellar, W→boiler_room | **dark** | | | |
| **flooded_cellar** | W→cellar_stairs | **dark** | valve | | |
| **boiler_room** | E→cellar_stairs | **dark** | generator | | |
| **landing** | S→grand_staircase, N→master_bedroom, E→study, W→chapel | | | | |
| **master_bedroom** | S→landing | | locket | | |
| **study** | W→landing | | photograph | | |
| **chapel** | E→landing | | shrine | | |
Alle Verbindungen **bidirektional** (Gegen-Exit am Zielraum). Der Power-Lock sitzt nur
auf dem **Aufwärts**-Exit `grand_staircase → landing`; der Rückweg `landing → grand_staircase`
ist immer offen. Keller-Räume sind `dark` → Eintritt nur mit getragener Lichtquelle
(`GoCommand`/`Light` gaten das bereits). Der Strom versorgt das **Obergeschoss** (Treppe/
Lift), nicht die Kellerverkabelung → der Keller bleibt dunkel (Lichtquelle nötig).
## 4. Kritischer Pfad (Lösbarkeit)
1. Erkunden Erdgeschoss; in der **kitchen** `lamp` + `matches` nehmen; `use lamp` → Lampe
an (Lichtquelle). (Alternativweg: `torch` aus dining + `use matches on torch``lit_torch`.)
2. **conservatory** (dining N): `valve_wheel` nehmen.
3. In den Keller (kitchen S→cellar_stairs — braucht Licht) → **flooded_cellar** (E):
`use valve_wheel on valve` → Flag `cellar_drained` + erhält `fuse`.
4. **boiler_room** (cellar_stairs W): `use fuse on generator` → Flag `power_on`.
(Quest `restore_power` abgeschlossen → startet `lay_to_rest`.)
5. `grand_staircase → landing` jetzt offen → Obergeschoss.
6. **master_bedroom**: `locket`. **study**: `photograph`. **chapel**:
`use locket on shrine``locket_placed`; `use photograph on shrine`
(requires `locket_placed`) → `ghost_banished`.
7. Zurück zur **foyer**: `use front_door` → Flag `left_manor` → Ende (Priorität, §8).
## 5. Items (~16)
| id | Typ | Ort | Zweck |
|---|---|---|---|
| lamp | switchable, light, initialState off | **foyer** (Start) | Primäre Lichtquelle (`use lamp` → an); zugleich gutartiges Tutorial-Ziel (examine/take/use) |
| matches | plain | kitchen | für lit_torch |
| letter | readable | kitchen | Lore; old_man-Reaktion |
| torch | plain | dining_room | für lit_torch |
| lit_torch | plain, light | **unplatziert** | Ergebnis matches+torch (alternative Lichtquelle) |
| shovel | plain | library | Flavor/Lore (kein kritischer Pfad) |
| journal | readable | library | Lore + Hinweise (Keller, Schrein) |
| valve_wheel | plain | conservatory | dräniert den Keller |
| valve | plain (Fixture) | flooded_cellar | Kombi-Ziel |
| fuse | plain | **unplatziert** | Ergebnis valve_wheel+valve |
| generator | plain (Fixture) | boiler_room | Kombi-Ziel → power_on |
| front_door | switchable, initialState off | foyer | `use front_door` → setFlag `left_manor` |
| locket | plain | master_bedroom | Relikt 1 |
| photograph | plain | study | Relikt 2 |
| shrine | plain (Fixture) | chapel | Kombi-Ziel der Relikte |
| portrait | readable | dining_room | Lore |
„Fixture" = ein nicht-tragbares Element nur als Kombi-Operand gedacht (technisch ein
`plain`-Item im Raum; der Spieler könnte es theoretisch `take`n — unkritisch, optional
per Flavor-Beschreibung entmutigt). `lit_torch`/`fuse` sind in `items.yaml` definiert,
aber **keinem Raum zugewiesen** (entstehen nur per Kombination; `CombinationFactory`
validiert ihre Existenz, `SaveCodec` erfasst sie nach dem Erzeugen).
## 6. Kombinationen (`combinations.yaml`, 5)
| a | b | requires | consume | produce | effects | response (Kurz) |
|---|---|---|---|---|---|---|
| matches | torch | | matches, torch | lit_torch | | „Der Streichholz entflammt die Fackel." |
| valve_wheel | valve | | | fuse | setFlag cellar_drained | „Du kurbelst das Ventil; das Wasser sinkt und gibt eine Sicherung frei." |
| fuse | generator | | fuse | | setFlag power_on | „Die Sicherung sitzt; der Generator springt an, das Haus erwacht." |
| locket | shrine | | locket | | setFlag locket_placed | „Du legst das Medaillon auf den Schrein; die Luft erstarrt." |
| photograph | shrine | flag locket_placed | photograph | | setFlag ghost_banished | erfolg: „…der Geist findet Frieden." / **failText**: „Der Schrein wirkt unvollständig — etwas fehlt noch." |
(Reihenfolge-unabhängig; Operanden aus Inventar **oder** aktuellem Raum — Fixtures
liegen im Raum, Relikte/Werkzeuge im Inventar.)
## 7. Flags, Quests, NPC
**Flags:** `cellar_drained`, `power_on`, `locket_placed`, `ghost_banished`, `left_manor`.
**`restore_power`** (autoStart):
- Stage 1 — Objective „Drain the flooded cellar." · completion: `flag cellar_drained`.
- Stage 2 — Objective „Get the manor's power running." · completion: `flag power_on`.
- onComplete: effects `[startQuest lay_to_rest, say "With the power back, the upper floor is reachable — and something stirs up there."]`.
**`lay_to_rest`** (autoStart: **false**; via startQuest):
- Stage 1 — Objective „Lay the restless spirit to rest in the chapel." · completion: `flag ghost_banished`.
**NPC `old_man`** (library): Greeting + **bedingte Dialoge**:
- ohne `cellar_drained`: Hinweis auf den gefluteten Keller / das Ventil.
- mit `power_on` ohne `ghost_banished`: Hinweis auf Obergeschoss/Schrein/Relikte.
- mit `ghost_banished`: dankbarer Abschiedssatz.
- **Reaktion**: onReceive `letter` → poignante Lore-Antwort, `consumes: letter`,
effects `[setFlag heard_tale]` (rein optional/Flavor; nicht auf dem kritischen Pfad).
## 8. Enden (`endings.yaml`, Reihenfolge = Priorität, erstes Match gewinnt)
1. **spirit_at_rest** — when `[flag left_manor, flag ghost_banished]` · victory **true** ·
„Du trittst hinaus; hinter dir liegt das Haus endlich still. (Bester Ausgang.)"
2. **manor_reclaimed** — when `[flag left_manor, flag power_on]` · victory **true** ·
„Licht in allen Fenstern, der Bann des Hauses gebrochen — du gehst erhobenen Hauptes."
3. **fled** — when `[flag left_manor]` · victory **false** ·
„Du fliehst in die Nacht. Sicher — aber das Haus behält seine Geheimnisse."
(`ghost_banished``power_on` ⇒ Obergeschoss erreicht, daher genügt in #1 die Kombination
`left_manor + ghost_banished`. Reihenfolge stellt sicher, dass das spezifischste Ende zuerst greift.)
## 9. Beschreibungs-Varianten & Musik (Politur)
- `flooded_cellar` descriptionState: vor `cellar_drained` „knietief im schwarzen Wasser…",
danach „das abgelassene Becken gibt den schlammigen Boden frei…".
- `chapel` descriptionState: vor `ghost_banished` „eine kalte Gegenwart drückt…",
danach „nur noch stilles Mondlicht…".
- Musik (optionale `music:`-Felder; Dateien extern, fehlend → still): Erdgeschoss
`manor-theme.ogg`; Keller `cellar-drone.ogg`; Obergeschoss `musicbox.ogg`;
chapel `lament.ogg`.
## 10. Tests / „eine Welt"-Konsolidierung
- **Entfernen**: `src/test/resources/world/{game,rooms,items,npcs,quests,endings,combinations,tutorial}.yaml`
(Toy-Fixture „Test Manor"). Danach lösen alle `new WorldLoader().load()`-Aufrufe in Tests
die **echte** Welt (target/classes/world). `src/test/resources/badtut/tutorial.yaml` bleibt.
- **`WorldLoaderTest`** anpassen: Titel „Haunted Manor", Startraum `foyer`, Raum-Anzahl (14),
Vorhandensein zentraler Items/Exits — Form-Invarianten der echten Welt statt Toy-Werte.
- **`TutorialLoaderTest`** anpassen: lädt jetzt das echte `tutorial.yaml` (Intro vorhanden,
≥1 Schritt, erster expect=`look`) statt der 2-Schritt-Fixture.
- **`CombinationLoadingTest`**: bleibt gültig (Produktion enthält matches+torch→lit_torch).
- **`SaveServiceTest`/`MenuTest`**: laden irgendeine Welt → unverändert grün (State-Roundtrip).
- **Neu `WorldSolvableTest`** (lädt die echte Welt; treibt den kritischen Pfad rein über
Domäne + `Combinations.tryUse`, ohne IO/Loop):
- Lichtquelle besorgen, in den Keller, `valve_wheel`+`valve``cellar_drained` + fuse,
`fuse`+`generator``power_on`; Treppe-Lock öffnet; Relikte holen,
`locket`+`shrine``locket_placed`, `photograph`+`shrine``ghost_banished`;
`use front_door`; `EndingEngine.triggered` liefert **spirit_at_rest**.
- Zweiter Pfad: nur `power_on` + `left_manor`**manor_reclaimed**; nur `left_manor`
**fled** (prüft die Priorisierung + Lösbarkeit der Hauptkette).
## 11. Architektur-Werte / Risiken
- **Reiner Content** auf vorhandenen Engines; datengetrieben (YAML), keine neue Mechanik.
- **Kondensiert/designt**: jeder Raum hat Zweck (Item, Lock, Lore oder Knotenpunkt); ≤4
Exits/Raum (Kompass-Constraint respektiert; keine UP/DOWN).
- **Lösbarkeit automatisiert** über `WorldSolvableTest` — ein gebrochener Lock/Flag, der die
Welt unlösbar macht, lässt den Build scheitern statt erst im Spiel aufzufallen.
- **Eine Welt** (Toy-Fixture entfernt) → die ausgelieferte Welt wird tatsächlich getestet;
Risiko: `WorldLoaderTest`/`TutorialLoaderTest` sind nun an den Content gekoppelt (gewollt
— sie prüfen die echte Welt). Form-Invarianten statt fragiler Exakt-Werte halten die
Kopplung beherrschbar.
- **Fixtures als `take`-bar**: `valve`/`generator`/`shrine` sind technisch tragbar; per
Beschreibung als ortsfest dargestellt. Wenn der Spieler eines `take`t, bricht nichts
(Kombination findet das Operand auch im Inventar). Bewusst akzeptiert.
- **Tutorial**: Start ist `foyer` mit der **`lamp`** als gutartigem Ziel — die Schritte
examine/take/use greifen auf die Lampe (der `use`-Schritt = „Lampe anzünden", exakt der
erste sinnvolle Zug), `go` führt nach Norden in die Grand Hall, `go to` zurück zur foyer
(zwei besuchte Räume). So landet ein neuer Spieler **nicht** versehentlich auf
`use front_door` (das `front_door` liegt zwar auch in der foyer, aber die Anweisungen
lenken auf die Lampe; und Enden sind während des Tutorials ohnehin unterdrückt). Die
bestehenden `tutorial.yaml`-Texte bleiben gültig; Start-Welcome/Banner in `game.yaml`
an „Haunted Manor / foyer" anpassen.