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