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>
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
requiresnicht erfüllt → optionalesfailText(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 + optionalecombinations.yaml(geladen wie quests/endings überreadListOptional).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/producereferenzieren existierende Item-Ids; keine doppelten Paare.- Minimales Demo-Rezept + Items.
- Tests: Engine, UseCommand-Routing, Loader, Validator.
Out of Scope:
- Parser-Änderung —
on/with/tosind 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ältid.removeFromAnywhere(ctx, id)→player.removeItem(id), sonstcurrentRoom.removeItem(id).noSuch(id)→ "There is no '' here or in your inventory." (wie UseCommand).- Erzeugtes Item ist in
items.yamldefiniert, aber in keinem Raum platziert (existiert inworld.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/effectsviaConditionDto.toModelList/EffectDto.toModelList(vorhanden).WorldLoader:readListOptional(basePath + "/combinations.yaml", CombinationDto.class).CombinationFactory: DTO →Combination; sammelt inMap<String, Combination>perCombination.key(). Doppelter Key →WorldLoadException.World.combinations+ Getter; backward-kompatible Konstruktoren.
7. Validierung (WorldValidator)
a,b, jedeconsume-Id und (falls gesetzt)producemüssen in der Item-Registry existieren → sonstWorldLoadExceptionmit klarer Meldung.- Doppelte Paar-Keys → Fehler (im Factory bereits abgefangen; Validator dokumentiert die Regel).
requires/effectsnutzen 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 mitlight: true).Light.isActiveLightwertet ein Nicht-Switchable mitlight: trueals immer aktive Lichtquelle (nur SwitchableItems müssen „on" sein) — also istlit_torcheine dauerhaft brennende Lichtquelle, ohne Toggle.lit_torchwird nicht in einem Raum platziert (entsteht nur durchs Rezept).rooms.yaml:matchesundtorchin erreichbare Räume legen.combinations.yaml: das eine Rezept aus §4.- Spielbar nachweisbar:
take matches,take torch,use matches on torch→lit_torchim Inventar (Lichtquelle),matchesverbraucht.
9. Tests
CombinationsTest: ungeordneter Match (X on Y==Y on X);requireserfü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.combinationskorrekt; 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 nebenConditions/Effects;UseCommandbekommt nur eine Verzweigung. Kein Parser-Eingriff. - Reihenfolge-Unabhängigkeit über kanonischen Key vermeidet doppelte Rezepte.
- Erzeugte Items „aus dem Nichts": müssen in
items.yamldefiniert, aber unplatziert sein — Validator stellt Existenz sicher; Save/Load erfasst sie nach dem Fertigen automatisch. - Risiko:
produceeines bereits irgendwo platzierten Items würde dieselbe Instanz ins Inventar verschieben (Items sind Singletons). Demo vermeidet das (unplatziert). Validator/Doku weist darauf hin.