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

7.9 KiB
Raw Blame History

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

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):

  • FLAGstate.isSet(arg)
  • NOT_FLAG!state.isSet(arg)
  • HAS_ITEMplayer.hasItem(arg)

3.3 Effect (Daten in model, Anwender in game)

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_FLAGstate.set/clear(arg)
  • GIVE_ITEM → Item aus world.getItems().get(arg) ins Inventar (unbekannt → Warnung geloggt, übersprungen)
  • REMOVE_ITEMplayer.removeItem(arg)
  • SAYio.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

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