Files
Jander_Semester2/Semesterprojekt/docs/superpowers/specs/2026-06-01-use-x-on-y-design.md
Jean-Luc Makiola 34cbb7f158 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) <noreply@anthropic.com>
2026-06-01 16:58:39 +02:00

9.4 KiB

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:

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)

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

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

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<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 torchlit_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.yamlWorld.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.