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>
14 KiB
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: derold_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 jetztfoyer; dergo-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+TutorialLoaderTestan 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, dasspower_on,ghost_banishedund 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)
- Erkunden Erdgeschoss; in der kitchen
lamp+matchesnehmen;use lamp→ Lampe an (Lichtquelle). (Alternativweg:torchaus dining +use matches on torch→lit_torch.) - conservatory (dining N):
valve_wheelnehmen. - In den Keller (kitchen S→cellar_stairs — braucht Licht) → flooded_cellar (E):
use valve_wheel on valve→ Flagcellar_drained+ erhältfuse. - boiler_room (cellar_stairs W):
use fuse on generator→ Flagpower_on. (Questrestore_powerabgeschlossen → startetlay_to_rest.) grand_staircase → landingjetzt offen → Obergeschoss.- master_bedroom:
locket. study:photograph. chapel:use locket on shrine→locket_placed;use photograph on shrine(requireslocket_placed) →ghost_banished. - Zurück zur foyer:
use front_door→ Flagleft_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 taken — 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_onohneghost_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)
- spirit_at_rest — when
[flag left_manor, flag ghost_banished]· victory true · „Du trittst hinaus; hinter dir liegt das Haus endlich still. (Bester Ausgang.)" - manor_reclaimed — when
[flag left_manor, flag power_on]· victory true · „Licht in allen Fenstern, der Bann des Hauses gebrochen — du gehst erhobenen Hauptes." - 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_cellardescriptionState: vorcellar_drained„knietief im schwarzen Wasser…", danach „das abgelassene Becken gibt den schlammigen Boden frei…".chapeldescriptionState: vorghost_banished„eine kalte Gegenwart drückt…", danach „nur noch stilles Mondlicht…".- Musik (optionale
music:-Felder; Dateien extern, fehlend → still): Erdgeschossmanor-theme.ogg; Kellercellar-drone.ogg; Obergeschossmusicbox.ogg; chapellament.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 allenew WorldLoader().load()-Aufrufe in Tests die echte Welt (target/classes/world).src/test/resources/badtut/tutorial.yamlbleibt. WorldLoaderTestanpassen: Titel „Haunted Manor", Startraumfoyer, Raum-Anzahl (14), Vorhandensein zentraler Items/Exits — Form-Invarianten der echten Welt statt Toy-Werte.TutorialLoaderTestanpassen: lädt jetzt das echtetutorial.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.triggeredliefert spirit_at_rest. - Zweiter Pfad: nur
power_on+left_manor→ manor_reclaimed; nurleft_manor→ fled (prüft die Priorisierung + Lösbarkeit der Hauptkette).
- Lichtquelle besorgen, in den Keller,
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/TutorialLoaderTestsind 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/shrinesind technisch tragbar; per Beschreibung als ortsfest dargestellt. Wenn der Spieler einestaket, bricht nichts (Kombination findet das Operand auch im Inventar). Bewusst akzeptiert. - Tutorial: Start ist
foyermit derlampals gutartigem Ziel — die Schritte examine/take/use greifen auf die Lampe (deruse-Schritt = „Lampe anzünden", exakt der erste sinnvolle Zug),goführt nach Norden in die Grand Hall,go tozurück zur foyer (zwei besuchte Räume). So landet ein neuer Spieler nicht versehentlich aufuse front_door(dasfront_doorliegt zwar auch in der foyer, aber die Anweisungen lenken auf die Lampe; und Enden sind während des Tutorials ohnehin unterdrückt). Die bestehendentutorial.yaml-Texte bleiben gültig; Start-Welcome/Banner ingame.yamlan „Haunted Manor / foyer" anpassen.