From f79382c43a4f9daaa998111eaad57edacbb04c7e Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Sun, 31 May 2026 21:39:59 +0200 Subject: [PATCH] docs: spec for quest foundation (world-state + conditions/effects) Approach A: one typed condition/effect vocabulary (GameState + Conditions + Effects) reused by five integration points (locked exits, state-dependent descriptions, conditional NPC dialogue, reactions with requires/effects, switch effects). All new YAML fields optional. Light/dark, item-combination, quest objects, endings deferred. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../2026-05-31-quest-foundation-design.md | 185 ++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 Semesterprojekt/docs/superpowers/specs/2026-05-31-quest-foundation-design.md diff --git a/Semesterprojekt/docs/superpowers/specs/2026-05-31-quest-foundation-design.md b/Semesterprojekt/docs/superpowers/specs/2026-05-31-quest-foundation-design.md new file mode 100644 index 0000000..401d272 --- /dev/null +++ b/Semesterprojekt/docs/superpowers/specs/2026-05-31-quest-foundation-design.md @@ -0,0 +1,185 @@ +# 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`).