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

207 lines
9.4 KiB
Markdown

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