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:
@@ -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`).
|
||||
Reference in New Issue
Block a user