docs: spec for quests & quest-log (sub-project 3.2)

Condition-driven multi-stage quests on top of the foundation: Quest/QuestStage
data, QuestLog + QuestEngine (per-turn auto-advance with announcements),
START_QUEST effect, QuestView display via a console 'quests' command and a GUI
quest-box under the map. quests.yaml optional; World gains a 5-arg back-compat
constructor. Win/endings deferred to 3.3.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-31 22:20:49 +02:00
parent 59781e5cbd
commit 20d4f1470a

View File

@@ -0,0 +1,163 @@
# Spec: Quests & Quest-Log (Teilprojekt 3.2)
Stand: 2026-05-31. Baut auf dem Quest-Fundament (3.1) auf: Flags, Conditions,
Effects sind vorhanden. Liefert echte Quest-Objekte, eine Fortschritts-Engine und
die Anzeige (Konsolen-Befehl + GUI-Quest-Box unter der Karte).
## 1. Kontext & Ziel
Das Fundament erlaubt gated progression über Flags. Jetzt: **strukturierte Quests**
mit mehreren Stufen, deren Fortschritt **automatisch aus dem Weltzustand** erkannt
wird (bestätigt: Ansatz A, condition-driven), plus sichtbares **Quest-Log**.
## 2. Scope
**In Scope:**
- Quest-Datenmodell (`Quest`, `QuestStage`) + `quests.yaml`.
- Runtime: `QuestLog` (Fortschritt), `QuestEngine` (Tick pro Zug, Auto-Advance,
Ansagen), `START_QUEST`-Effekt.
- Anzeige: `QuestView`-Modell, `GameIO.setQuests/showQuests`, Konsolen-Befehl
`quests`, GUI-Quest-Box **unter der Karte**.
- Loader: `QuestDto`/`QuestStageDto`, `QuestFactory`, `quests.yaml` (optional),
`World.quests`.
- Demo-Quest in der echten Welt.
**Out of Scope (3.3 / später):**
- Win-Condition / mehrere Enden (3.3) eine Quest kann aber per `onComplete`-Effekt
ein Flag setzen, das 3.3 nutzt.
- Quest-Verzweigung, Fehlschlag-Zustände, Voraussetzungen (per `startQuest`-Effekt
ausdrückbar).
## 3. Datenmodell (model)
```java
public record Quest(String id, String title, boolean autoStart,
List<QuestStage> stages, List<Effect> onComplete) { /* List.copyOf */ }
public record QuestStage(String objective, List<Condition> completion,
List<Effect> onComplete) { /* List.copyOf */ }
```
`quests.yaml`:
```yaml
- id: restore_power
title: "Bring the Manor to Life"
autoStart: true
stages:
- objective: "Get the power running."
completion: [{ flag: power_on }]
- objective: "Earn the Old Man's brass key."
completion: [{ hasItem: key }]
onComplete:
- { setFlag: manor_secured }
```
## 4. Runtime (game)
### 4.1 QuestLog (in GameContext)
`QuestLog` (selbst-initialisiertes Feld in `GameContext`, wie `GameState` → kein
Konstruktor-Eingriff). Hält nur Laufzeit-Fortschritt:
- `Map<String, Integer> stageIndex` aktuelle Stufe je *aktiver* Quest.
- `Set<String> completed` abgeschlossene Quest-IDs.
- API: `start(id)`, `isActive(id)`, `isCompleted(id)`, `stageIndex(id)`,
`advance(id)`, `complete(id)`.
### 4.2 START_QUEST-Effekt
`Effect.Type.START_QUEST` (arg = Quest-ID). `Effects.apply` ruft
`ctx.getQuestLog().start(arg)`. `EffectDto` bekommt Feld `startQuest`. Damit können
Dialoge/Schalter/Reaktionen Quests starten.
### 4.3 QuestEngine.tick(ctx)
Pro Zug aus der `Game`-Schleife (neben `setHud`/`setMap`):
1. Auto-Start: jede `autoStart`-Quest, die weder aktiv noch abgeschlossen ist, starten.
2. Für jede **aktive** Quest die `completion` der **aktuellen** Stufe via
`Conditions.all` prüfen. Erfüllt →
- `onComplete`-Effekte der Stufe anwenden,
- Ansage (`io.print`, z. B. „✓ Objective complete: …"),
- `advance`; war es die letzte Stufe → Quest-`onComplete` anwenden, `complete`,
Ansage „Quest complete: …".
3. **Kaskade**: Schleife wiederholen, solange sich etwas geändert hat (begrenzt durch
Gesamt-Stufenzahl), damit ein Zug mehrere Stufen auslösen kann.
`tick` ist idempotent (abgeschlossene Stufen feuern nicht erneut). `QuestEngine.viewOf(ctx)`
baut die `QuestView` aus `World.getQuests()` + `QuestLog`.
## 5. Anzeige
### 5.1 Modell (io.text)
```java
public record QuestEntry(String title, String objective) {}
public record QuestView(List<QuestEntry> active, List<String> completed) {}
```
### 5.2 GameIO (additiv, wie die Karte)
```java
default void setQuests(QuestView view) { /* no-op */ } // Push pro Zug (GUI)
default void showQuests(QuestView view) { print(QuestText.render(view)); } // on demand (Konsole)
```
- **Konsole**: nutzt beide Defaults. `setQuests` = no-op; `showQuests` druckt via
`QuestText.render` (io.text), ausgelöst durch `QuestsCommand`.
- **GUI**: überschreibt `setQuests` (Quest-Box neu zeichnen) und `showQuests` (no-op,
Box ist immer sichtbar).
- `TestIO`: Defaults; `showQuests` druckt in `outputs` → testbar.
### 5.3 Konsolen-Befehl
`QuestsCommand` (`quests`, `log`, `journal`) → `io.showQuests(QuestEngine.viewOf(ctx))`.
Ausgabe: aktive Quests (Titel + aktuelles Ziel), darunter abgeschlossene (dim, mit ✓).
### 5.4 GUI-Quest-Box
Rechter Seitenbereich wird zu einem Panel: **Karte oben, Quest-Box unten**
(`BorderLayout`: Map-Scroll CENTER, Quest-Scroll SOUTH mit fester/anteiliger Höhe).
Die Quest-Box ist ein gestyltes, schreibgeschütztes Textfeld (`JTextPane`): aktive
Quests fett (Titel) + Ziel; abgeschlossene dim mit ✓. Font folgt der Auto-Skalierung
(`applyFonts`). `setQuests` aktualisiert sie pro Zug.
## 6. Loading
- `QuestDto(id, title, Boolean autoStart, List<QuestStageDto> stages, List<EffectDto> onComplete)`,
`QuestStageDto(objective, List<ConditionDto> completion, List<EffectDto> onComplete)`.
- `QuestFactory.fromDto` baut `Quest` (Conditions/Effects via vorhandene
`toModelList`); keine Resolver-Phase nötig (Flags/Items lazy, Quest-IDs sind Strings).
- `WorldLoader` liest `quests.yaml` **optional** (fehlt die Datei → leere Liste),
baut `Map<String, Quest>` und übergibt sie an `World`.
- `World` bekommt Feld `quests` + **rückwärtskompatiblen 5-Arg-Konstruktor**
(delegiert mit leerer Quest-Map), damit bestehende `new World(...)`-Aufrufe in Tests
unverändert kompilieren.
## 7. Fehlerbehandlung
| Fall | Verhalten |
|---|---|
| `quests.yaml` fehlt | leere Quest-Map, Spiel läuft ohne Quests |
| `START_QUEST` mit unbekannter Quest-ID | `QuestLog.start` legt Eintrag an; `viewOf` ignoriert IDs ohne Definition (Warnung geloggt) |
| Quest ohne Stufen | gilt sofort als abgeschlossen beim ersten Tick |
| Doppelte Quest-IDs | `WorldLoader.requireUniqueIds` erweitert um Quests |
## 8. Testing
- **QuestLog**: start/advance/complete, isActive/isCompleted.
- **QuestEngine**: Auto-Start; Stufe rückt vor, wenn Condition gilt; Ansage; Quest
schließt nach letzter Stufe ab; Kaskade (eine Stufe löst die nächste); `viewOf`
liefert aktive + abgeschlossene korrekt.
- **Effects**: `START_QUEST` startet Quest.
- **EffectDto/QuestDto**: `toModel`/`fromDto`-Mapping.
- **QuestsCommand**: über `TestIO` Ausgabe enthält aktives Ziel.
- **WorldLoader**: lädt `quests.yaml`; fehlende Datei → leer.
- **GUI**: nicht unit-getestet; manueller Smoke (`exec:java@gui`).
## 9. Demonstration
`quests.yaml` mit `restore_power` (autoStart, 2 Stufen): Strom anschalten →
Schlüssel vom alten Mann holen. Sichtbar im Konsolen-`quests`-Befehl und in der
GUI-Box; Ansagen beim Erfüllen.
## 10. Offene Detailfragen (in Implementierung)
- Höhe/Anteil der GUI-Quest-Box (ca. 30 % der Seitenleiste, scrollbar).
- Stil der Ansagen (zunächst `HEADING`).