Files
Jander_Semester2/Semesterprojekt/docs/superpowers/specs/2026-05-31-quest-foundation-design.md
Jean-Luc Makiola f79382c43a 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) <noreply@anthropic.com>
2026-05-31 21:39:59 +02:00

186 lines
7.9 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: 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<String>`
(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<Condition>, 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<Effect>, 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<Direction, ExitLock>` | `exitLocks` | `GoCommand` | Vor Bewegung: gilt `requires` nicht → `blocked` ausgeben, nicht bewegen |
| 2 | `Room.descriptionStates: List<DescriptionState>` | `descriptionStates` | `LookCommand` | Erste passende Variante als Beschreibung, sonst `description` |
| 3 | `Npc.dialogue: List<DialogueLine>` | `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<Effect>` | `effects` | `use` | Beim Schalten **auf „on"**: `effects` anwenden |
Hilfstypen (records in `model`): `ExitLock(List<Condition> requires, String blocked)`,
`DescriptionState(List<Condition> when, String text)`,
`DialogueLine(List<Condition> 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`).