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>
This commit is contained in:
@@ -0,0 +1,220 @@
|
|||||||
|
# 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.
|
||||||
Reference in New Issue
Block a user