# 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<Condition> requires,      // optional; leer = immer erfüllt
        List<String> consume,          // Item-Ids, die entfernt werden
        String produce,                // optional; Item-Id, die ins Inventar kommt (null = keins)
        List<Effect> 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<String, Combination> 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 '<id>' 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<String, Combination>`
  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.
