# 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`).
