From 9c5efab3312fe849e96f0847a2e32dc73da091c6 Mon Sep 17 00:00:00 2001 From: MDietermann Date: Wed, 12 Aug 2026 14:18:44 +0200 Subject: [PATCH] docs: ticket 16 (loot-tabellen) aufloesen, nachtraege an 10 und 14, loot-tabelle ins glossar, destination erreicht --- .../issues/10-raum-prefab-format.md | 18 ++- .../14-kampfformeln-und-rundenablauf.md | 3 + .../rustkeep-1.0/issues/16-loot-tabellen.md | 146 +++++++++++++++++- .scratch/rustkeep-1.0/map.md | 9 +- CONTEXT.md | 17 +- 5 files changed, 188 insertions(+), 5 deletions(-) diff --git a/.scratch/rustkeep-1.0/issues/10-raum-prefab-format.md b/.scratch/rustkeep-1.0/issues/10-raum-prefab-format.md index a3e118b..e00cd8f 100644 --- a/.scratch/rustkeep-1.0/issues/10-raum-prefab-format.md +++ b/.scratch/rustkeep-1.0/issues/10-raum-prefab-format.md @@ -160,7 +160,8 @@ struct Room { prefab: PrefabId, // fuer room_entered und Trigger rect: Rect, enemies: Vec, - loot: Vec<(Pos, ItemInstance)>, + loot_table: LootTableId, // Nachtrag 16: Prefab-loot oder Etagentabelle + loot: Vec, // Haufen, erst beim Aufheben aufgeloest cleared: bool, } @@ -178,6 +179,21 @@ Ladeprüfung über die 04-Maschinerie: **`layout`-Zeilen müssen gleich lang sei kein Rechteck und die Platzierung rechnet mit Müll. Ein Zweizeiler, der eine ganze Klasse von Modder-Frust abfängt. +### Nachtrag aus Ticket 16 + +**Bodenbeute wird erst beim Aufheben gezogen**, deshalb hält `Room.loot` nur noch Positionen und der +Raum *eine* `loot_table` — alle Haufen eines Raums ziehen ohnehin aus derselben Tabelle (dem +Prefab-`loot` oder, wenn es fehlt, der Etagentabelle). Die *Zahl* der Haufen steht weiterhin bei der +Erzeugung fest, es sind die `l`-Zeichen; ihr Inhalt nicht. Der Save wird dadurch kleiner. + +Möglich ist das nur, weil **alle Haufen dasselbe generische Symbol zeigen**: Die Anzeige verrät damit +nichts, was noch nicht gewürfelt ist. Der Gewinn ist, dass ein Glücksring auch auf Beute wirkt, die auf +dieser Etage schon liegt. + +Dazu bekommt ein Prefab ein **optionales `loot`-Feld**, das die Etagentabelle für seine `l`-Plätze +ersetzt — sonst wäre ein `core:treasure`-Raum nur ein Raum mit mehr `l`-Zeichen. Es nennt eine +Tabellen-ID, keine Gegenstände, und bleibt damit über alle Tiefen verwendbar. + ### Siegbedingung — bisher nirgends definiert Kein Ticket hatte je festgelegt, was beim Abstieg von der letzten Etage passiert. diff --git a/.scratch/rustkeep-1.0/issues/14-kampfformeln-und-rundenablauf.md b/.scratch/rustkeep-1.0/issues/14-kampfformeln-und-rundenablauf.md index 0528d57..06a1cb7 100644 --- a/.scratch/rustkeep-1.0/issues/14-kampfformeln-und-rundenablauf.md +++ b/.scratch/rustkeep-1.0/issues/14-kampfformeln-und-rundenablauf.md @@ -213,6 +213,9 @@ fünfzig Zeilen. Dazu bekommt die `creatures`-Registry ein Feld `exp_reward`. Ausdrücklich **nicht** dazugekommen ist `points_per_level`: Ein Aufstieg gibt genau einen Punkt, das ist eine Regel und kein Stellknopf. +**Nachtrag aus Ticket 16:** `LUCK_FACTOR` kommt dazu — `luck` gewährt eine Chance auf einen +zusätzlichen Zug auf jeder Beutetabelle. Damit hat `[balance]` acht Felder. + ### Vier Nachträge an geschlossene Tickets | Ticket | Nachtrag | diff --git a/.scratch/rustkeep-1.0/issues/16-loot-tabellen.md b/.scratch/rustkeep-1.0/issues/16-loot-tabellen.md index e0dfdd7..f564436 100644 --- a/.scratch/rustkeep-1.0/issues/16-loot-tabellen.md +++ b/.scratch/rustkeep-1.0/issues/16-loot-tabellen.md @@ -1,7 +1,7 @@ # 16 — Loot-Tabellen Type: grilling -Status: open +Status: resolved Blocked by: 02 ## Question @@ -35,3 +35,147 @@ Seltenheit, oder gar nicht? Affixe sind eine eigene Registry (die sechste), also gilt für sie derselbe Mod-Einhängepunkt wie für Gegenstände. + +## Answer + +Begriffe im [Glossar](../../../CONTEXT.md). Der Zufall war bereits entschieden: derselbe gesetzte +Generator aus Ticket 14, dessen Seed im Save liegt — Beute würfelt beim Nachladen nicht neu. + +### Eine Maschinerie für Gegenstände und Affixe + +Zwölfte Registry `[[loot_tables]]`: + +```toml +[[loot_tables]] +id = "core:weapon_affixes" +rolls = "0-1" +entries = [ + { affix = "core:flaming", weight = 5 }, + { affix = "core:greater_flaming", weight = 1 }, # Stufe, kein Wert + { nothing = true, weight = 20 }, +] + +[[loot_tables]] +id = "core:floor_3_table" +rolls = "1-3" +entries = [ + { item = "core:health_potion", weight = 10, count = "1-2" }, + { item = "core:iron_sword", weight = 3, affixes = "core:weapon_affixes" }, + { table = "core:rare_table", weight = 1 }, + { nothing = true, weight = 20 }, + { item = "core:boss_blade", guaranteed = true }, +] +``` + +**Gewichte statt Prozent** — keine Geschmacksfrage, sondern eine Modding-Frage: Prozentwerte müssen +sich auf 100 summieren, und ein hinzugefügter Eintrag macht die Summe kaputt, woraufhin jemand alle +anderen anpassen muss. Gewichte verdünnen sich anteilig, ohne dass irgendwer rechnet. + +**Nieten sind gewöhnliche Einträge**, kein eigenes `drop_chance`-Feld. Ein Gegner, der selten etwas +fallen lässt, bekommt ein hohes `nothing`-Gewicht — die Engine kennt eine Mechanik statt zweier. + +**Tabelle-in-Tabelle** ist eine Zeile und liefert Seltenheitsstufen, ohne dass „Seltenheit" ein Begriff +werden muss. + +**`guaranteed = true`** wird immer ausgegeben und nimmt an der Gewichtung nicht teil. Mit Gewichten +allein lässt sich „sicher" nicht ausdrücken, und Bosse stehen in der Destination; Questgegenstände sind +derselbe Fall. + +**Mehrfachziehung erlaubt**, kein `unique`-Flag. Drei Heiltränke sind ein normales Ergebnis, und +stapelbare Gegenstände (02) tragen das ohne Zutun. Wer Einzelstücke will, gibt ihnen eine eigene +Tabelle mit `rolls = "1-1"`. Die Decke: Falls „nie zweimal dasselbe Schwert" nötig wird, ist `unique` +ein Flag — jetzt wäre es eine Regel ohne Fall. + +### Affix-Stufen statt Affix-Werte + +Ticket 07 hat festgelegt, dass eine `ItemInstance` **nur Affix-IDs** trägt. „Flammend" und „Stark +flammend" sind deshalb zwei Registry-Einträge mit unterschiedlichen Modifikatoren und Preisfaktoren, +keine Zahl am Exemplar. + +Eine Stärke je Exemplar hätte drei Dinge auf einmal gekostet: das Save-Format (ID-Liste), die +Preisformel `base_price × Π price_factor` und die Stapelregel — zwei Schwerter „Flammend 3" und +„Flammend 4" stapeln nicht mehr, aber wie zeigt man den Unterschied an? Mit Stufen kann der Modder +seine Abstufungen **benennen**, statt eine Zahl auszuliefern, die keine Anzeige hat. + +### `luck` hat genau eine Regel + +**Eine Chance auf einen zusätzlichen Zug — auf jeder Tabelle.** + +``` +p_extra = clamp(luck * LUCK_FACTOR, 0.0, 0.9) +``` + +Ein Knopf mehr in `[balance]`. Die Regel wirkt dreifach, ohne dreimal geschrieben zu werden: mehr +Gegenstände, mehr Affixe, und häufiger die stärkere Stufe — weil die als eigener Eintrag mit kleinem +Gewicht danebensteht. + +Der Grund gegen „Glück verschiebt zur Seltenheit" ist ein Ausschluss: **Es gibt keinen +Seltenheitsbegriff.** Das Schema kennt nur Gewichte; Seltenheit erst zu definieren und dann Gewichte +umzurechnen wäre ein zweites Konzept für dasselbe Gefühl. + +### Der Mod-Einhängepunkt existiert bereits + +Ticket 03 gilt unverändert: ganzer Eintrag wird ersetzt. Ein neuer Mechanismus wird trotzdem nicht +gebraucht — **Tabelle-in-Tabelle ist der Einhängepunkt**, zwei gewöhnliche Überschreibungen und null +neue Begriffe: + +```toml +[[loot_tables]] +id = "meinmod:floor_3" +entries = [ { table = "core:floor_3_table", weight = 100 }, + { item = "meinmod:zauberstab", weight = 5 } ] + +[[floors]] +id = "core:floor_3" +overrides = "core:floor_3" +loot = "meinmod:floor_3" +``` + +Decke: Zwei Mods, die dieselbe Etage umhängen, kollidieren — der letzte gewinnt. Das ist derselbe +Konflikt wie bei jeder anderen Überschreibung in 03. Falls er häufig wird, ist `[[loot_additions]]` der +Nachbau; ihn *vorher* zu bauen hieße, einen zweiten Mechanismus für etwas einzuführen, das der erste +schon kann. + +### Beute wird beim Aufheben gezogen + +`Room` aus Ticket 10 bekommt **eine `loot_table` und `loot: Vec`** statt fertiger Instanzen — alle +Haufen eines Raums ziehen ohnehin aus derselben Tabelle, die ID je Haufen zu wiederholen wäre +Redundanz. Aufgelöst wird erst beim Aufheben; der Save wird dadurch sogar kleiner. + +Die **Zahl** der Haufen steht bei der Erzeugung fest (die `l`-Zeichen im Prefab), ihr Inhalt nicht. +Alle zeigen **dasselbe generische Symbol** — deshalb verrät die Anzeige nichts, und ein Glücksring +wirkt auch auf das, was auf dieser Etage bereits liegt. Bei Ziehung zur Erzeugungszeit wäre der Wurf +für diese Etage schon gefallen, bevor der Spieler den Ring findet. + +Fällt der Wurf beim Aufheben auf eine Niete, **verschwindet der Haufen mit einer Meldung**. Kein +Neuwürfeln — das hängt sich bei einer reinen Nieten-Tabelle auf und bräuchte dann doch einen Zähler. +Keine Ladeprüfung „Tabellen für `l`-Plätze dürfen keine Nieten enthalten": Das wäre die Bevormundung, +die Ticket 02 abgeräumt hat. Wer leere Haufen nicht will, lässt Nieten aus der Tabelle weg. + +### Etagenskalierung — die Auflage aus 15 ist strukturell erfüllt + +Eine Tabelle je Etage, wie Ticket 10 es mit dem `loot`-Feld bereits angelegt hat. **Nirgends steht eine +Fünf**: Etagen sind Registry-Einträge, jeder bringt seine Tabelle mit, ein Mod mit Etage 6 bringt ihre +gleich mit, und der Endlosmodus wiederholt mit der letzten Etage auch deren Tabelle. + +Bedingungen am Eintrag (`floor at_least 3` auf einer gemeinsamen Tabelle) klingen flexibler, lösen das +Endlos-Problem aber gerade *nicht*: Ein Eintrag mit `at_least = 3` ist auf Etage 500 genauso gültig wie +auf Etage 3. Echte Tiefenskalierung bräuchte Gewichte als *Funktion* der Etage — eine Formel, keine +Daten, und die wird gebaut, wenn der Endlosmodus kommt. + +Ein Prefab darf über ein **optionales** `loot`-Feld seine eigene Tabelle nennen. Sonst ist ein +Schatzkammer-Raum nur ein Raum mit mehr `l`-Zeichen — mehr vom Gleichen statt etwas Besserem. Es nennt +dabei eine Tabellen-**ID**, keine Gegenstände, und bleibt damit über alle Tiefen verwendbar; dieselbe +Begründung, mit der 10 festgelegt hat, dass ein `e` nicht sagt, *welcher* Gegner. + +### Wo Beute hängt + +An der `creatures`-Registry (14), an der Etage (10) und optional am Prefab. Alle drei nennen schlicht +eine Tabellen-ID. + +### Zwei Nachträge an geschlossene Tickets + +| Ticket | Nachtrag | +|---|---| +| **10** | `Room` hält `loot_table: LootTableId` plus `loot: Vec` statt `Vec<(Pos, ItemInstance)>`; optionales `loot`-Feld am Prefab; generischer Beute-Marker im Grid | +| **14** | `LUCK_FACTOR` in `[balance]` | diff --git a/.scratch/rustkeep-1.0/map.md b/.scratch/rustkeep-1.0/map.md index edd6b04..32a0a7e 100644 --- a/.scratch/rustkeep-1.0/map.md +++ b/.scratch/rustkeep-1.0/map.md @@ -17,7 +17,13 @@ dort als Konvention im Body. Zuordnung Ticket → Gitea-Issue: | 13 Script-Engine wählen | [#14](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/14) | 14 Kampfformeln | [#15](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/15) | | 15 Progression | [#16](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/16) | 16 Loot-Tabellen | [#17](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/17) | -## Destination +## Destination — **erreicht am 2026-08-12** + +Alle sechzehn Tickets sind geschlossen. Was jetzt existiert: sechzehn Entscheidungsdokumente, ein +Glossar mit den kanonischen Begriffen, und ein 342-Zeilen-Wegwerfprototyp auf dem Branch +`prototyp/12-ratatui-durchstich`. Was **nicht** existiert: das Spiel. Der nächste Schritt ist die +Implementierung, und sie hat mit `src/` (heute `print_box`, `include_str!`, `enum ItemType`) fast +nichts gemein. Alle Architektur- und Design-Entscheidungen für rustkeep 1.0 sind getroffen — nicht gebaut, entschieden. 1.0 heißt: 5 Etagen mit Boss, Solo-Held mit Rasse und Klasse, Grid-Erkundung mit Menü-Kampf, @@ -78,6 +84,7 @@ auch über Ticket 13. - [13 — Script-Engine wählen](issues/13-script-engine-waehlen.md) ([#14](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/14)) — **Rhai**, aber die Wahl ist dank der Leitplanke aus 05 nur noch eine *Implementierungs*-Entscheidung: Scripts lesen ueber eine Handvoll Funktionen (gespiegelt an der Bedingungsgrammatik aus 08 — sie sehen nichts, was eine Bedingung nicht auch saehe) und schreiben ausschliesslich ueber Aktionen. Begruendung unbequem, aber ehrlich — **fuer ein unveroeffentlichtes Terminal-Roguelike schreibt niemand Mods**, der Lua-Vorsprung zahlt sich erst mit einer Gemeinde aus. Mods gelten als **vertrauenswuerdig** (wie bei Factorio, Minecraft, Skyrim), gehaertet wird gegen Unfaelle: acht Engine-Schalter, gegen die Doku geprueft; `set_max_operations` ausdruecklich **nicht** in `[balance]`, sonst hebt ein Mod sein eigenes Limit auf. `rand()` laeuft durch den Seed aus 14, sonst wuerfelten ausgerechnet Scripts beim Laden neu. `run_script` als neunte Aktion, Scripts als **elfte Registry**. Risiken beobachtet mit benanntem Ausstiegssignal; `smartstring` ist ein INFO-Advisory, kein Ausstiegsgrund. **Stufe (c) wandert hinter 1.0** — eine Script-Engine zu bauen, bevor jemand an eine Vokabular-Luecke gestossen ist, baut die Antwort vor der Frage. - [14 — Kampfformeln und Rundenablauf](issues/14-kampfformeln-und-rundenablauf.md) ([#15](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/15)) — **Ein Zug = Spielerhandlung plus alle Gegnerreaktionen**, sonst tickte ein Debuff bei drei Gegnern viermal. Initiative als `i32` **am Wesen** statt als Reihenfolgeliste (die waere eine zweite Indexsammlung mit demselben Ungueltigkeitsproblem); tote Gegner erst am Ende von `execute`, nach dem Leeren der Warteschlange. Trefferformel und Schadenskette mit drei Klemmbereichen — `0.8` bei Resistenz, weil `1.0` Immunitaet und damit ein Spielabbruch waere. **Die Krit-Reihenfolge-Frage loest sich auf**: multiplikativ ist kommutativ. Eigener xorshift, **Seed im Save** — Nachladen wuerfelt nicht neu. Faehigkeiten kosten Ausdauer, Abklingzeit in Zuegen, gesperrt ⇒ `Rejected` ohne Zug. **Achte Registry: Wesen** — die Luecke lag seit 02 offen; `ai` als geordnete Bedingungsliste. `[balance]` verschmilzt **feldweise**, ausdrueckliche Ausnahme zu 03 (festes Schema). Vier Nachtraege: `scale` und `flee` an 01, drei Felder an 07, zwei Events an 08, `GameOver` an 11. - [15 — Progression und Level-up](issues/15-progression-und-levelup.md) ([#16](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/16)) — Ein Aufstieg gibt **genau einen** frei verteilbaren Punkt auf die fuenf Grundwerte; `AssignPoints` ist **ein** Command mit allen Punkten (drei Stufen waeren sonst drei Zuege im Menue). **Keine Obergrenze** — `clamp(1, 50)` faellt, die Kurve begrenzt sich ueber steigende Kosten (`exp_base`, `exp_exponent` in `[balance]`). Erfahrung aus Toetungen (`exp_reward` am Wesen) und Quests (`give_exp`, zehnte Aktion), **nicht** fuers Betreten einer Etage — das belohnte das Durchrennen. **Die Zuwachs-Regel bleibt eng**: nur Aufstieg und `AssignPoints` heben den Lebensstand mit `max_health`, Ausruestung nie — sonst „heilt ein Ring beim Anlegen" (01). Rasse und Klasse bleiben **flach**, `per_level` entfaellt ersatzlos; was sie traegt, sind Faehigkeiten. `level` darf modifiziert werden, aber Punkte haengen an `base.level` — ein `+1 level`-Ring schenkt keinen. Vier Nachtraege: `give_exp` an 01, `current_exp`/`unspent_points` an **`Game`** statt ans Wesen (07 hatte die Platzierung hierher delegiert), `AssignPoints` an 11, zwei Balance-Knoepfe plus `exp_reward` an 14. +- [16 — Loot-Tabellen](issues/16-loot-tabellen.md) ([#17](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/17)) — Zwoelfte Registry `[[loot_tables]]`, **eine Maschinerie fuer Gegenstaende und Affixe**. **Gewichte statt Prozent**, weil Prozente sich auf 100 summieren muessen und ein hinzugefuegter Mod-Eintrag die Summe kaputt macht. Nieten sind gewoehnliche Eintraege (kein `drop_chance`), Tabelle-in-Tabelle liefert Seltenheitsstufen, ohne dass „Seltenheit" ein Begriff wird. **Affix-Stufen statt Affix-Werte** — eine Staerke je Exemplar haette Save-Format, Preisformel und Stapelregel aus 07 auf einmal gekostet. **`luck` hat genau eine Regel**: Chance auf einen zusaetzlichen Zug auf jeder Tabelle — wirkt dreifach (mehr Gegenstaende, mehr Affixe, haeufiger die staerkere Stufe), ohne dreimal geschrieben zu werden. Der **Mod-Einhaengepunkt existiert schon**: Tabelle-in-Tabelle plus zwei Ueberschreibungen, kein zweiter Mechanismus. Bodenbeute wird **erst beim Aufheben** gezogen (`Room` haelt eine `loot_table` plus Positionen), moeglich nur weil alle Haufen dasselbe Symbol zeigen — dadurch wirkt ein Gluecksring auch auf das, was schon liegt. `guaranteed = true` fuer Bosse, Mehrfachziehung erlaubt. Nachtraege: `Room`-Umbau und optionales Prefab-`loot` an 10, `LUCK_FACTOR` an 14. ## Not yet specified diff --git a/CONTEXT.md b/CONTEXT.md index 09029f4..bde618a 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -218,10 +218,22 @@ anderes Layout erzeugen und den Helden in eine Wand stellen. Fehlt beim Laden ein [Mod](#mod), wird der betroffene Eintrag verworfen und gewarnt — nie der ganze Speicherstand abgelehnt. +## Loot-Tabelle + +Eine gewichtete Liste, aus der gezogen wird. Ein Eintrag ist ein [Gegenstand](#gegenstand), ein +[Affix](#affix), eine andere Tabelle oder eine **Niete** — dieselbe Maschinerie zieht damit Beute +*und* Affixe. + +Gewichtet wird mit **Gewichten, nicht mit Prozenten** — Prozente müssten sich auf 100 summieren, und +ein Eintrag, den ein [Mod](#mod) hinzufügt, machte die Summe kaputt. + +Es gibt **keinen Seltenheitsbegriff**: Abstufungen sind eigene Einträge mit kleinerem Gewicht +(`core:flaming` neben `core:greater_flaming`). + ## Registry -Eine benannte Sammlung moddbarer Definitionen, geschlüsselt nach namespaced ID. Es gibt **elf**, und -das hier ist die kanonische Liste — sie ist über sechs Tickets gewachsen und stand sonst nirgends +Eine benannte Sammlung moddbarer Definitionen, geschlüsselt nach namespaced ID. Es gibt **zwölf**, und +das hier ist die kanonische Liste — sie ist über sieben Tickets gewachsen und stand sonst nirgends vollständig: | Registry | TOML-Tabelle | @@ -236,6 +248,7 @@ vollständig: | [Wesen](#wesen) | `[[creatures]]` | | [Prefabs](#prefab) | `[[prefabs]]` | | [Etagen](#etage) | `[[floors]]` | +| [Loot-Tabellen](#loot-tabelle) | `[[loot_tables]]` | | Scripts | `[[scripts]]` | Jede dieser Tabellen darf in jeder beliebigen `*.toml` eines [Mods](#mod) stehen.