From 34cbb7f158c96dac11c0994147ba86b725f3de4e Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Mon, 1 Jun 2026 16:58:39 +0200 Subject: [PATCH] docs: spec for use X on Y item combinations Data-driven combination recipes (requires/consume/produce/effects/response/ failText), operands from inventory or current room, order-independent lookup, generic-or-failText feedback. No parser change (on/with already fillers). Minimal matches+torch -> lit_torch demo. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../specs/2026-06-01-use-x-on-y-design.md | 206 ++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100644 Semesterprojekt/docs/superpowers/specs/2026-06-01-use-x-on-y-design.md diff --git a/Semesterprojekt/docs/superpowers/specs/2026-06-01-use-x-on-y-design.md b/Semesterprojekt/docs/superpowers/specs/2026-06-01-use-x-on-y-design.md new file mode 100644 index 0000000..173f10b --- /dev/null +++ b/Semesterprojekt/docs/superpowers/specs/2026-06-01-use-x-on-y-design.md @@ -0,0 +1,206 @@ +# Spec: Item-Kombination `use X on Y` + +Stand: 2026-06-01. Erste der verbleibenden Mechaniken nach Hauptmenü + +Speichern/Laden. Setzt das in `enhancement-ideas.md` #5b (Mechanik-Spine §4) +festgelegte Item-Kombinations-Feature um. Baut auf der bestehenden +Condition/Effect-Engine, dem datengetriebenen Loader (wie quests/endings) und +dem vorhandenen `UseCommand` auf. + +## 1. Kontext & Ziel + +v1.0 hat bewusst argloses `use X` ohne Ziel gewählt. Ziel jetzt: **`use X on Y`** +als datengetriebene **Kombinations-Mechanik** — ein Rezept kann Bedingungen +prüfen, Items verbrauchen, ein Item erzeugen, Flags setzen und einen Text +ausgeben. Deckt sowohl Transform (`matches + torch → lit_torch`) als auch +Anwenden-auf-Ziel (`use key on door → effekt`) mit *einem* Modell ab. + +Bestätigte Entscheidungen (Brainstorming 2026-06-01): +- **Allgemeine, datengetriebene Rezepte** (requires / consume / produce / effects + / response / failText) — Superset, nutzt die vorhandene Engine. +- **Operanden** lösen aus **Inventar oder aktuellem Raum** auf (`use key on door`, + `use matches on torch`). +- **Fehlschlag**: kein Rezept → generische Zeile; Rezept vorhanden aber `requires` + nicht erfüllt → optionales `failText` (Hinweis), sonst generische Zeile. +- **Demo**: ein minimales Showcase-Rezept (`matches + torch → lit_torch`) plus die + wenigen dafür nötigen Items, in bestehende Räume gestellt. + +## 2. Scope + +**In Scope:** +- `UseCommand`-Routing: 0 Args → "Use what?"; 1 Arg → bestehendes Einzel-`use`; + ≥2 Args → Kombination der ersten beiden Operanden. +- `Combination`-Datenmodell + optionale `combinations.yaml` (geladen wie + quests/endings über `readListOptional`). +- `CombinationDto` + Factory + `World.combinations` (Map mit kanonischem Paar-Key). +- `Combinations`-Engine (`game`) — Auflösung, Lookup, requires/consume/produce/ + effects/response/failText. +- `WorldValidator`-Regeln: `a`/`b`/`consume`/`produce` referenzieren existierende + Item-Ids; keine doppelten Paare. +- Minimales Demo-Rezept + Items. +- Tests: Engine, UseCommand-Routing, Loader, Validator. + +**Out of Scope:** +- Parser-Änderung — `on`/`with`/`to` sind **bereits** Filler (s. §3); keine neue + Grammatik nötig. +- Mehrteilige (>2) Rezepte / Kombination aus 3+ Items. +- Mehrwort-Item-Namen-Auflösung (Operanden lösen per Item-Id auf, wie heute). +- Größerer Content-/Rätsel-Ausbau (eigenes Teilprojekt). + +## 3. Grammatik & Parsing (keine Änderung nötig) + +`CommandParser.FILLERS` enthält bereits `to`, `with`, `on`, `at`, `the`, `a`, +`an`. Daher liefern alle drei Eingaben dieselbe Args-Liste: + +``` +use matches on torch → verb=use, args=[matches, torch] +use matches with torch → verb=use, args=[matches, torch] +use matches torch → verb=use, args=[matches, torch] +``` + +`UseCommand` verzweigt nur über die Arg-Anzahl: + +```java +if (args.isEmpty()) { io.write("Use what?"); return; } +if (args.size() >= 2) { Combinations.tryUse(ctx, args.get(0), args.get(1)); return; } +// 1 Arg: bestehende Einzel-use-Logik (Inventar dann Raum, item.use(ctx)) +``` + +Bei ≥2 Args zählen die **ersten beiden** Tokens als Operandenpaar. + +## 4. Datenmodell (model) + +```java +public record Combination( + String a, String b, // Operanden-Item-Ids (ungeordnet) + List requires, // optional; leer = immer erfüllt + List consume, // Item-Ids, die entfernt werden + String produce, // optional; Item-Id, die ins Inventar kommt (null = keins) + List effects, // optional; Flag-Mutationen + String response, // Erfolgstext + String failText // optional; Hinweis bei nicht erfüllten requires +) { + public Combination { + requires = requires == null ? List.of() : List.copyOf(requires); + consume = consume == null ? List.of() : List.copyOf(consume); + effects = effects == null ? List.of() : List.copyOf(effects); + } + /** Kanonischer, reihenfolge-unabhängiger Schlüssel für ein Item-Paar. */ + public static String key(String x, String y) { + return x.compareTo(y) <= 0 ? x + "|" + y : y + "|" + x; + } + public String key() { return key(a, b); } +} +``` + +`combinations.yaml` (Demo): +```yaml +- a: matches + b: torch + consume: [matches] + produce: lit_torch + response: | + You strike the matches; the torch catches with a low hiss and steady glow. + # requires/effects/failText optional, hier nicht nötig +``` + +`World` erhält `Map combinations` (Key = `Combination.key()`). +Backward-kompatible Konstruktor-Überladung wie bei quests/endings. + +## 5. Kombinations-Engine (game/Combinations.java) + +Statische Utility wie `Conditions`/`Effects`/`Light`: + +```java +public static void tryUse(GameContext ctx, String idX, String idY) { + Player p = ctx.getPlayer(); + // 1. Beide Operanden auflösen (Inventar zuerst, dann aktueller Raum) + if (!present(ctx, idX)) { ctx.getIo().write(noSuch(idX)); return; } + if (!present(ctx, idY)) { ctx.getIo().write(noSuch(idY)); return; } + // 2. Rezept-Lookup über kanonischen Key + Combination c = ctx.getWorld().getCombinations().get(Combination.key(idX, idY)); + if (c == null) { ctx.getIo().write("Nothing happens."); return; } + // 3. requires prüfen + if (!Conditions.all(c.requires(), ctx)) { + ctx.getIo().write(c.failText() != null ? c.failText() : "Nothing happens."); + return; + } + // 4. consume → produce → effects → response + for (String id : c.consume()) { removeFromAnywhere(ctx, id); } + if (c.produce() != null) { + Item produced = ctx.getWorld().getItems().get(c.produce()); + p.addItem(produced); + } + Effects.applyAll(c.effects(), ctx); + ctx.getIo().print(/* response als StyledText */); +} +``` + +Hilfsmethoden: +- `present(ctx, id)` → Inventar **oder** aktueller Raum enthält `id`. +- `removeFromAnywhere(ctx, id)` → `player.removeItem(id)`, sonst + `currentRoom.removeItem(id)`. +- `noSuch(id)` → "There is no '' here or in your inventory." (wie UseCommand). +- Erzeugtes Item ist in `items.yaml` definiert, aber in **keinem** Raum platziert + (existiert in `world.getItems()`, „nirgends"), bis es gefertigt wird → dann ins + Inventar. Save/Load erfasst Item-Platzierung + Switch-Zustände bereits korrekt; + ungefertigte Items bleiben unplatziert. + +## 6. Loader-Anbindung + +- `CombinationDto` (Jackson-Record): `a, b, requires, consume, produce, effects, + response, failText`. `requires`/`effects` via `ConditionDto.toModelList` / + `EffectDto.toModelList` (vorhanden). +- `WorldLoader`: `readListOptional(basePath + "/combinations.yaml", CombinationDto.class)`. +- `CombinationFactory`: DTO → `Combination`; sammelt in `Map` + per `Combination.key()`. Doppelter Key → `WorldLoadException`. +- `World.combinations` + Getter; backward-kompatible Konstruktoren. + +## 7. Validierung (WorldValidator) + +- `a`, `b`, jede `consume`-Id und (falls gesetzt) `produce` müssen in der + Item-Registry existieren → sonst `WorldLoadException` mit klarer Meldung. +- Doppelte Paar-Keys → Fehler (im Factory bereits abgefangen; Validator + dokumentiert die Regel). +- `requires`/`effects` nutzen die bestehende Condition/Effect-Validierung, falls + vorhanden; sonst rein strukturell. + +## 8. Demo-Content (minimal) + +- `items.yaml`: `matches` (plain), `torch` (plain, `light: false`), `lit_torch` + (**plain mit `light: true`**). `Light.isActiveLight` wertet ein Nicht-Switchable + mit `light: true` als **immer aktive** Lichtquelle (nur SwitchableItems müssen + „on" sein) — also ist `lit_torch` eine dauerhaft brennende Lichtquelle, ohne + Toggle. `lit_torch` wird **nicht** in einem Raum platziert (entsteht nur durchs + Rezept). +- `rooms.yaml`: `matches` und `torch` in erreichbare Räume legen. +- `combinations.yaml`: das eine Rezept aus §4. +- Spielbar nachweisbar: `take matches`, `take torch`, `use matches on torch` → + `lit_torch` im Inventar (Lichtquelle), `matches` verbraucht. + +## 9. Tests + +- **`CombinationsTest`**: ungeordneter Match (`X on Y` == `Y on X`); `requires` + erfüllt/nicht erfüllt (failText vs. generisch); consume aus Inventar **und** aus + Raum; produce landet im Inventar; effects gesetzt; kein Rezept → "Nothing + happens."; fehlender Operand → noSuch-Meldung. +- **`UseCommand`-Routing**: 0 Args → "Use what?"; 1 Arg → `item.use` (bestehend); + 2 Args → Combinations-Pfad (über Fake-/TestIO + kleine Welt). +- **Loader**: `combinations.yaml` → `World.combinations` korrekt; Key-Kanonisierung; + doppeltes Paar → Exception. +- **Validator**: unbekannte `a`/`b`/`consume`/`produce`-Id → `WorldLoadException`. +- Konvention: keine GUI-/YAML-abhängigen Domain-Tests; Engine-Tests bauen Welt + inline wie `QuestEngineTest`. + +## 10. Architektur-Werte / Risiken + +- **Datengetrieben**: Rezepte in YAML, kein Hardcoding; gleiche DTO/Domain-Trennung + wie quests/endings. +- **Geringe Kopplung**: neue `Combinations`-Utility neben `Conditions`/`Effects`; + `UseCommand` bekommt nur eine Verzweigung. Kein Parser-Eingriff. +- **Reihenfolge-Unabhängigkeit** über kanonischen Key vermeidet doppelte Rezepte. +- **Erzeugte Items** „aus dem Nichts": müssen in `items.yaml` definiert, aber + unplatziert sein — Validator stellt Existenz sicher; Save/Load erfasst sie nach + dem Fertigen automatisch. +- Risiko: `produce` eines bereits irgendwo platzierten Items würde dieselbe Instanz + ins Inventar verschieben (Items sind Singletons). Demo vermeidet das (unplatziert). + Validator/Doku weist darauf hin.