# Spec: Quest-Fundament – World-State & Bedingungen/Effekte (Teilprojekt 3.1) Stand: 2026-05-31. Erstes Stück des Quest-Systems (Teilprojekt 3) aus [enhancement-ideas.md](../../enhancement-ideas.md). Liefert die Zustands- und Bedingungs-/Effekt-Engine, auf der Quests (#3.2) und Enden (#3.3) aufbauen. ## 1. Kontext & Ziel Das NPC-System ist heute statisch: fester `greeting`, Reaktionen (`consumes`/`gives`/`response`) ohne Bedingungen, kein Weltzustand. Ziel: **gated progression** datengetrieben ermöglichen – verschlossene Türen, zustandsabhängige Beschreibungen, reaktive NPCs, Schalter, die Weltzustand setzen – über **eine** kleine, wiederverwendbare Bedingungs-/Effekt-Sprache (Ansatz A). Scope **core-only** (bestätigt): Licht/Dunkelheit und Item-Kombination werden bewusst auf spätere kleine Folge-Specs verschoben. ## 2. Scope **In Scope:** - **`GameState`** – Flag-Speicher (benannte Booleans). - **Condition/Effect-Modell** + Auswerter (`Conditions`) + Anwender (`Effects`). - Fünf Integrationspunkte: verschlossene Exits, zustandsabhängige Raumbeschreibungen, bedingte NPC-Dialoge, Reaktionen mit `requires`/`effects`, Schalter mit `effects`. - Loader/DTO/Factory/Resolver-Erweiterungen, alle neuen YAML-Felder **optional** (Rückwärtskompatibilität). **Out of Scope (eigene spätere Specs):** - Licht & Dunkelheit; Item-Kombination (`use X on Y`). - Quest-Objekte, Quest-Log, GUI-Quest-Box (Teilprojekt #3.2). - Win-Condition / mehrere Enden (#3.3). - `startFlags`-Seeding (YAGNI – Flags starten leer = alle false, was der Ausgangslage „Strom aus, Keller geflutet" entspricht). ## 3. Kern-Engine (Ansatz A) ### 3.1 GameState `GameState` (Package `game`) hält die gesetzten Flags als `Set` (vorhanden = true). API: `isSet(name)`, `set(name)`, `clear(name)`, `all()`. `GameContext` bekommt ein **selbst-initialisiertes** Feld `private final GameState state = new GameState();` (per `@Getter` lesbar). Da das Feld initialisiert ist, bleibt der `@RequiredArgsConstructor` (world, player, io) **unverändert** – keine Anpassung der bestehenden Konstruktionsstellen nötig. ### 3.2 Condition (Daten in `model`, Auswerter in `game`) ```java public record Condition(Type type, String arg) { public enum Type { FLAG, NOT_FLAG, HAS_ITEM } } ``` `Conditions.all(List, GameContext)` → true, wenn alle gelten (leere Liste = true): - `FLAG` → `state.isSet(arg)` - `NOT_FLAG` → `!state.isSet(arg)` - `HAS_ITEM` → `player.hasItem(arg)` ### 3.3 Effect (Daten in `model`, Anwender in `game`) ```java public record Effect(Type type, String arg) { public enum Type { SET_FLAG, CLEAR_FLAG, GIVE_ITEM, REMOVE_ITEM, SAY } } ``` `Effects.applyAll(List, GameContext)` wendet jeden an: - `SET_FLAG`/`CLEAR_FLAG` → `state.set/clear(arg)` - `GIVE_ITEM` → Item aus `world.getItems().get(arg)` ins Inventar (unbekannt → Warnung geloggt, übersprungen) - `REMOVE_ITEM` → `player.removeItem(arg)` - `SAY` → `io.write(arg)` ## 4. Integrationspunkte Jeder trägt nur `requires` (Conditions) und/oder `effects` und ruft die Helfer. | # | Träger (model) | YAML-Feld | Konsument | Verhalten | |---|---|---|---|---| | 1 | `Room.exitLocks: EnumMap` | `exitLocks` | `GoCommand` | Vor Bewegung: gilt `requires` nicht → `blocked` ausgeben, nicht bewegen | | 2 | `Room.descriptionStates: List` | `descriptionStates` | `LookCommand` | Erste passende Variante als Beschreibung, sonst `description` | | 3 | `Npc.dialogue: List` | `dialogue` | `TalkCommand` | Erste passende Zeile, sonst `greeting` | | 4 | `NpcReaction.requires/effects` | `requires`, `effects` | `GiveCommand` | `requires` prüfen; bei Erfolg consume/give wie bisher + `effects` | | 5 | `SwitchableItem.effects: List` | `effects` | `use` | Beim Schalten **auf „on"**: `effects` anwenden | Hilfstypen (records in `model`): `ExitLock(List requires, String blocked)`, `DescriptionState(List when, String text)`, `DialogueLine(List when, String text)`. ### YAML-Beispiele ```yaml # rooms.yaml exits: { east: cellar } exitLocks: - direction: east requires: [{ flag: power_on }] blocked: "The cellar door won't budge without power." descriptionStates: - when: [{ flag: power_on }] text: "The cellar, now lit, reveals a workbench." # npcs.yaml dialogue: - when: [{ flag: power_on }] text: '"You did it — the lights are back!"' reactions: - onReceive: fuse requires: [{ flag: cellar_drained }] response: "That'll hold now." effects: - { removeItem: fuse } - { setFlag: fuse_installed } # items.yaml - id: breaker type: switchable name: Breaker onText: "You flip the breaker. The manor hums to life." offText: "You switch the breaker off." state: false effects: - { setFlag: power_on } ``` ## 5. Loading / DTO / Mapping - Neue DTOs (`loader.dto`): `ConditionDto(flag, notFlag, hasItem)`, `EffectDto(setFlag, clearFlag, giveItem, removeItem, say)`, `ExitLockDto`, `DescriptionStateDto`, `DialogueLineDto`. Jeweils `toModel()`. - Erweiterte DTOs: `RoomDto` (+`exitLocks`, +`descriptionStates`), `NpcDto` (+`dialogue`), `ReactionDto` (+`requires`, +`effects`), `ItemDto` (+`effects`). - `ConditionDto.toModel()`: genau ein Feld gesetzt → entsprechende `Condition`, sonst `WorldLoadException`. Analog `EffectDto`. - Verdrahtung: `ReferenceResolver.resolveRooms` baut `exitLocks` (Direction via `Direction.fromString`) und `descriptionStates`; `resolveNpcs` baut `dialogue` und erweitert die `NpcReaction` um `requires`/`effects`; `ItemFactory` setzt `SwitchableItem.effects`. - **Item-Referenzen in Conditions/Effects** (`hasItem`/`giveItem`/`removeItem`) werden **nicht** beim Laden aufgelöst, sondern zur Laufzeit (lazy) – kein Eingriff in die strikte Resolver-Validierung; `GIVE_ITEM` unbekannt ist laufzeit-tolerant (Warnung). ## 6. Fehlerbehandlung | Fall | Verhalten | |---|---| | Condition/Effect-DTO ohne gesetztes Feld | `WorldLoadException` beim Laden | | `exitLocks` mit unbekannter Richtung | `WorldLoadException` (`Direction.fromString`) | | `GIVE_ITEM` mit unbekannter Item-ID | Warnung geloggt, Effekt übersprungen | | Neue Felder fehlen (alte YAMLs) | null → als leere Liste behandelt, lädt normal | | Schalter `off`→`on` mehrfach | `effects` werden bei jedem Übergang auf „on" angewendet (idempotent bei `setFlag`) | ## 7. Rückwärtskompatibilität Alle neuen Felder sind optional (nullable). Bestehende `rooms.yaml`, `npcs.yaml`, `items.yaml` und alle Test-Fixtures laden unverändert; die 90 bestehenden Tests bleiben grün. `NpcReaction` behält `consumes`/`gives`/`response`. ## 8. Testing - **Unit:** `GameState` (set/clear/isSet); `Conditions.all` (FLAG/NOT_FLAG/HAS_ITEM, AND, leer=true); `Effects.applyAll` (alle fünf Typen, unbekanntes Item tolerant); `ConditionDto.toModel`/`EffectDto.toModel` (inkl. Fehlerfall). - **Integration (in-Code-Welt wie bestehende Command-Tests):** - `GoCommand`: gesperrter Exit blockt ohne Flag, passiert mit Flag. - `LookCommand`: Beschreibung wechselt mit Flag. - `TalkCommand`: Dialogzeile wechselt mit Flag, sonst `greeting`. - `GiveCommand`: Reaktion mit `requires` blockt/erlaubt; `effects` setzen Flag. - `SwitchableItem.use`: setzt Flag beim Einschalten. - **Loader:** ein Round-Trip-Test, der ein YAML mit allen neuen Feldern lädt; ein Test, dass alte YAMLs ohne neue Felder weiterhin laden. ## 9. Demonstration (am Ende, minimal) Eine kleine, sichere Erweiterung der echten Welt-YAMLs, um die Kette end-to-end spielbar zu zeigen (z. B. Schalter setzt ein Flag, das eine Tür öffnet und eine Beschreibung ändert) – ohne bestehende Tests zu berühren. ## 10. Offene Detailfragen (in Implementierung entscheidbar) - `SAY`-Effekt: schlicht `io.write` oder gestylt (`StyledText`) – zunächst `write`. - Ob `ExitLock`/`DescriptionState`/`DialogueLine` als records in `model` oder `model.state` liegen – im Plan festgelegt (`model`).