Files
rustkeep/.scratch/rustkeep-1.0/map.md
MDietermann 6014471c33 feat(kampf): verbindliche Gegner-Ansage und Zielcursor
Am Zugende legt jeder Gegner fest, was er im naechsten Zug tut; der
Kampf-Screen zeigt es unter dem Lebensbalken. Verbindlich statt Vorschau,
denn nur so entsteht ein Fenster: Wer dem Gegner zwischen Ansage und
Ausfuehrung die Ausdauer nimmt, kostet ihn den Zug.

Zielwahl bekommt einen eigenen Cursor, Tab schaltet zwischen Gegner- und
Faehigkeitsliste um — dieselbe Bedienung wie im Laden. Die Ziffern
springen nur noch aufs Ziel, ausgeloest wird mit Enter.
2026-08-12 20:49:24 +02:00

34 KiB
Raw Blame History

Map: rustkeep 1.0

Gitea-Spiegel: Issue #1. Diese Datei ist die kanonische Quelle — bei Abweichung gilt .scratch/.

Gitea hat keine nativen Parent-Child- oder Blocking-Beziehungen, deshalb steht beides dort als Konvention im Body. Zuordnung Ticket → Gitea-Issue:

Ticket Gitea Ticket Gitea
01 Effekt-Vokabular #2 06 Research ratatui #7
02 Typ-Registry #3 07 Game-State-Baum #8
03 Mod-Ladepipeline #4 08 Event-System #9
04 Fehlerstrategie #5 09 Save-Schema #10
05 Research Script-Engine #6 10 Raum-Prefab-Format #11
11 UI-Gerüst #12 12 Prototyp ratatui #13
13 Script-Engine wählen #14 14 Kampfformeln #15
15 Progression #16 16 Loot-Tabellen #17

Destination — erreicht am 2026-08-12

Alle sechzehn Tickets sind geschlossen. Damit ist die Entscheidungsphase vorbei und die Umsetzung läuft — Stand siehe „Verification" weiter unten.

Sechs Meilensteine statt neuer Tickets: M1 vertikaler Durchstich · M2 Etagengenerierung und Prefabs · M3 Kampf mit Events und Triggern · M4 Inventar, Ausrüstung, Affixe, Handel · M5 Quests und Save/Laden · M6 Inhalt für fünf Etagen und Balancing. Die Map hat sich für Entscheidungen gelohnt; dieselbe Zeremonie über Implementierungsaufgaben zu stülpen wäre Prozess um seiner selbst willen.

1.0 heißt: 5 Etagen mit Boss, Solo-Held mit Rasse und Klasse, Grid-Erkundung mit Menü-Kampf, Inventar und Ausrüstung, Quests, NPCs, Handel, Speichern/Laden, Mod-Support auf den Stufen (a) und (b).

Stufe (c) — Scripting — ist entschieden, aber nicht Teil von 1.0: Engine (Rhai) und Schnittstelle stehen, gebaut wird sie, wenn ein Modder an eine Lücke im Vokabular stößt (13).

Verification — was der Compiler bestätigt hat

Sechzehn Tickets Papier, davon hatte bis M1 nur das Screen-Modell (12) Kontakt zu einem Compiler. Diese Liste wächst mit jedem Meilenstein und ist der einzige Ort, an dem steht, welche Entscheidung gehalten hat.

M1 — vertikaler Durchstich (feat/m1-durchstich, 10 Tests)

Zusage Aus Befund
Registries neben Game, disjunkte Borrows 07 bestätigt — game.execute(cmd, &reg) und game.attribute(…, &reg) nebeneinander. Ticket 12 konnte das nicht prüfen, weil der Durchstich keine Registries lud
Zweistufige Deserialisierung, Eintrag als Fehlereinheit 04 bestätigt — ein kaputtes Feld tötet seinen Eintrag, die Nachbarn überleben
Game::attribute sammelt aus mehreren Quellen 07 bestätigt — Basis + Optionen + Ausrüstung, Stapelregel aus 01 stimmt
game/ bleibt ratatui-frei 11 bestätigt — aber der Test dafür musste nachgebessert werden
Trigger-Suche, Event-Warteschlange 08 ungeprüft — es gibt weder Trigger noch Events. M3

M2 — Etagengenerierung (feat/m2-etagen, 21 Tests)

Zusage Aus Befund
Kette i → i−1 garantiert Erreichbarkeit strukturell 10 bestätigt über 29 Seeds, per Flutfüllung gezählt
Pflichträume zuerst ist die Garantie 10 bestätigt über 39 Seeds — Eingang, Treppe, Händler immer da
Türen explizit, keine Löcher in gezeichneten Wänden 10 erst nach zwei Korrekturen — siehe unten
Seed im Save ⇒ Nachladen würfelt nicht neu 14 bestätigt — gleicher Seed, gleiche Etage
Sieg über das Fehlen der nächsten Etage 10 bestätigt — keine Fünf im Code

Zwei Befunde nachgetragen: 10 (der Korridor startete auf der Tür und grub die Wandreihe auf — 2340 Wandfelder; zwei L-Varianten reichen nicht, es braucht eine Wegsuche über den freien Raum) und 11 (das Grid braucht einen mitlaufenden Bildausschnitt, weil Etage 5 breiter ist als ein 80-Spalten-Terminal — das stand in keinem Ticket).

M3 — Kampf, Events und Trigger (feat/m3-kampf, 43 Tests)

Zusage Aus Befund
Trigger werden gesucht, nicht indiziert 08 bestätigt — die letzte offene Papierzusage. Gift tickt über einen turn_started-Trigger, ohne Script
Warteschlange statt Rekursion 08 erst nach Korrektur — die Umsetzung war ein Stack
Zug = Spieler plus alle Gegner, Initiative am Wesen 14 bestätigt, Gleichstand geht an den Spieler
Tote erst am Zugende entfernen 14 bestätigt
Gesperrte Fähigkeit ⇒ Rejected, kein Zug 14 bestätigt
Resistenz typ-indiziert, bei 0.8 gedeckelt 01/14 bestätigt, Verwundbarkeit bei −1.0 ausdrückbar
Krit-Reihenfolge ist belanglos 14 bestätigt, direkt nachgerechnet
Gegner gehören zum Raum, verschoben nicht kopiert 10 bestätigt
Trefferformel 14 war toter Code — jeder Angriff traf, siehe unten

Drei Befunde nachgetragen: 14 (die Trefferformel wurde nie aufgerufen — nur der Compiler hat es gemeldet, weil alle Tests sie direkt prüften statt über einen Angriff), 08 (die Warteschlange war Vec::pop, also LIFO — die Formulierung war richtig, die Umsetzung nicht, und kein Test hat es gemerkt) und 01 (benannte Modifikatoren wie core:poisoned hatten keine Registry; jetzt die dreizehnte).

Dazu zwei Ergänzungen am Bedingungsvokabular (health_below, chance am Trigger) und eine offene Lücke: Ein Trigger kann die Nutzlast des Events, das ihn ausgelöst hat, nicht prüfen — Check sieht nur Wesen. „Dornenpanzer reflektiert nur Feuer" ist damit nicht formulierbar. Zu entscheiden vor M4, weil dort Affix- und Gegenstands-Trigger kommen.

M4 — Beute, Inventar, Affixe und Handel (feat/m4-beute, feat/m4-handel, 56 Tests)

Zusage Aus Befund
Eine Maschinerie für Gegenstände und Affixe 16 bestätigt — dieselben Gewichte, dieselbe Tabelle-in-Tabelle
Beute erst beim Aufheben auflösen, ein Symbol für alles 16 bestätigt — die Anzeige verrät nichts, was noch nicht gewürfelt ist
Affix ist eine benannte Modifikatorliste am Exemplar 07 bestätigt — strukturell dasselbe wie eine Charakteroption, attribute() blieb unverändert
Speicherstand bleibt eine ID-Liste 07 bestätigt — ItemInstance trägt def, affixes, count
Stapelregel: gleiche def und gleiche affixes 07 war nie umgesetzt — siehe unten
Gold ist ein gewöhnlicher Gegenstand 09 bestätigt — Bezahlen läuft durch take("core:gold", …), kein Sonderfeld
Flache Dialogliste statt Knotengraph M4 bestätigt — die bedingte Option ersetzt den zweiten Knoten, ohne Knoten-IDs im Save
open_shop als elfte Aktion, nicht als Sonderfeld 01 bestätigt — game/ setzt eine Absicht, die Schleife schiebt den Screen
Warenkorb einmal bei der Erzeugung ziehen M4 bestätigt — Schließen und Öffnen liefert dasselbe Angebot

Ein Befund nachgetragen: 07/01/09 — die Stapelregel stand seit Ticket 07 auf dem Papier, aber niemand hatte sie gebaut. Der Beutezieher schob für count = "50-120" bis zu 120 einzelne Einträge ins Inventar. Gefunden nicht beim Nachdenken, sondern beim Entwerfen des Handels: „Gold abziehen" braucht einen Stapel, sonst ist Bezahlen eine Schleife über Einträge.

Und ein Absturz, den erst das Spielen gefunden hat: Die Initiativreihenfolge ist ein Schnappschuss vor dem Zug, eine geglückte Flucht leert enemies aber mitten darin — die Reihenfolge zeigt danach ins Leere. Zwei Zusagen aus 14, jede für sich richtig, die zusammen nicht geprüft worden waren. Ein leerer enemies bricht den Zug jetzt ab.

Dazu eine bewusst offene Stelle: Npc.triggers ist nicht gebaut, weil triggers_for NPCs nicht einsammelt — ein Feld ohne Leser wäre eine Zusage, die niemand einlöst. TODO steht im Code.

Nach M4 — die Ansage (58 Tests)

Kein Meilenstein, eine Nachforderung aus dem Spielen: Gegner sagen am Zugende an, was sie im nächsten tun, und der Spieler wählt sein Ziel mit einem eigenen Cursor. Zwei Entscheidungen dahinter, beide in die Tickets zurückgetragen:

Frage Entschieden Warum
Ansage verbindlich oder Vorschau? verbindlich (14) Eine Anzeige, die sich mit der Lage mitdreht, ist genau dann falsch, wenn der Spieler handelt. Verbindlich heißt störbar — das Fenster zwischen Ansage und Ausführung ist die Taktik
Zielwahl mit Pfeilen auf zwei Achsen oder mit Tab? Tab (11) Dieselbe Umschaltregel wie im Laden. Eine Bedienung für alle zweispaltigen Screens statt einer je Screen

Der Zugablauf aus 14 hat damit einen siebten Schritt, Creature ein Feld intent, und die Sperrgründe, die ability_block ohnehin berechnete, haben einen zweiten Nutzen bekommen.

Drei Befunde aus M1, die nur der Compiler liefern konnte — alle in die Tickets zurückgetragen:

  • 11: Der Grenztest in seiner wörtlichen Fassung schlägt beim eigenen Doc-Kommentar an. Muss Kommentarzeilen ausnehmen.
  • 03: Die Referenzseite war nie entschieden. Präfixiert wird nur id; Verweise stehen voll qualifiziert. Asymmetrisch — zu klären, bevor der erste Fremdmod existiert.
  • 04: Spanned zeigt auf den Eintragskopf, nicht auf das schuldige Feld. Deckt sich mit der Zusage, ist aber gut zu wissen.

Notes

Domain: Rust-Terminal-Roguelike, moddbar. Repo: MDietermann/rustkeep.

Das Glossar liegt in CONTEXT.md im Repo-Wurzelverzeichnis — kanonische Begriffe, keine Implementierung. Jede Session, die einen Begriff schärft, schreibt ihn dort fest.

Jede Session konsultiert: ponytail (dauerhaft aktiv), /grilling und /domain-modeling für Grilling-Tickets, /research für Research-Tickets, /prototype für Prototype-Tickets.

Feststehende Rahmenentscheidungen aus der Charter-Session:

Thema Entscheidung
Motivation Rust lernen und Spiel bauen — Lern-Entscheidungen bekommen eigene Tickets, wo sie echte Architekturfragen sind
Kernschleife Grid-Bewegung zum Erkunden, Kampf im Menü (Dungeon Master / Etrian Odyssey)
Held Solo, kein Party-System
Rendering ratatui + crossterm; print_box fliegt raus, sobald ratatui steht
Etagen Hybrid — Raum-Prefabs in TOML, prozedural zusammengesetzt
Daten Alles TOML, zur Laufzeit geladen. include_str! und enum ItemType fallen
Mod-IDs Namespaced (core:iron_sword), Präfix aus dem Manifest und automatisch vorangestellt. Ladereihenfolge aus einer Spielerliste, nicht aus dem Manifest (03). Das Kernspiel ist selbst ein Mod
Mod-Stufen Daten → Typen → Scripting, in dieser Reihenfolge. Scripting setzt auf der Typ-Registry auf. 13: Rhai gewählt, Umsetzung aber hinter 1.0 — gebaut wird sie, wenn das Vokabular an eine Grenze stößt
Effekte Ein generisches Effekt-System mit Wirkung auf Stats. Rasse, Klasse, Items, Statuseffekte, Quest-Belohnungen und später Scripts nutzen dasselbe Vokabular — dieses Vokabular ist die Mod-API
Quests Bedingung + Belohnung als Daten, generisches Event-/Trigger-System
Weltzustand Game-Ownership-Baum, Verweise über String-IDs (keine Indizes, 07). Registries liegt daneben, nicht darin. Kein ECS
Fehler Result statt panic! — kaputte Mod-TOMLs sind der Normalfall, nicht der Ausnahmefall
Save TOML, Mod-Liste im Header, fehlender Mod = Warnung und Verwerfen, kein harter Abbruch

Leitplanke aus Ticket 05: Scripts reden ausschließlich über das Effekt-Vokabular, nie direkt mit dem Game-State. Solange das gilt, ist die Script-Engine austauschbar — Ticket 01 entscheidet damit indirekt auch über Ticket 13.

Decisions so far

  • 01 — Effekt-Vokabular (#2) — Zwei Begriffe statt einem: Modifikator (rechnet ein Attribut um, solange er existiert) und Aktion (ändert Zustand einmalig), getrennte Maschinen und getrennte TOML-Arrays. Fester Attributkatalog ohne Namensraum; max_health ist Attribut, der Lebensstand ist Zustand. Sechs Aktionen, damage eigenständig wegen der Minderungskette. Stapeln (Basis + Σ flach) × (1 + Σ prozentual). Eine Uhr: der Zug. Sieben feste Schadenstypen, ein Minderungsmechanismus (resistance), armor nur Kurzschreibweise. Auren wohnen beim Träger. Bedingungsvokabular bewusst an Ticket 08 delegiert. Begriffe im Glossar.
  • 02 — Typ-Registry statt Enums (#3) — Es gibt keinen Item-Typ mehr: enum ItemType vermischte Slot, Verhalten und Kategorie und entfällt ersatzlos zugunsten der Felder slot, stackable, consumed_on_use. ItemWeapon und ItemArmor fallen weg, ein Gegenstand ist einheitlich. Fähigkeit kommt als dritter Begriff neben Modifikator und Aktion dazu (Angebot statt Wirkung). Fünf moddbare Registries; Slots und sogar Charakteroption-Kategorien sind Inhalt, mit pick = N. Startausrüstung ist on_create als gewöhnliche Aktionsliste. value → base_price, shop_price → price_modifier.
  • 03 — Mod-Ladepipeline und Manifest (#4) — Zwei Suchpfade: mods/ neben der Binary (enthält core/) und data_dir()/rustkeep/mods über die Crate directories; bei gleicher ID gewinnt das Profil, mit Warnzeile. core liegt als echter Mod-Ordner auf der Platte, include_str! entfällt. manifest.toml mit vier Feldern (id, name, version, depends) — TOML statt YAML, weil serde_yaml und der Fork serde_yml beide archiviert sind. Ladereihenfolge aus einer selbstheilenden Spielerliste load_order.toml; depends sortiert nicht, sondern prüft, und eine Verletzung ist harter Startfehler. Jede *.toml im Mod wird eingelesen, jede darf jede Registry-Tabelle enthalten. Präfix wird automatisch vorangestellt, Überschreiben über overrides = "core:iron_sword" am Eintrag — der landet nur unter der überschriebenen ID. Drei Phasen (Manifeste → Einlesen → Auflösen), dadurch keine Vorwärtsreferenzen; Fehlereinheit ist der Eintrag. Kein Nachladen zur Laufzeit.
  • 04 — Fehlerstrategie für kaputte Mods (#5) — Syntax ist ein Datei-Fehler, alles Semantische ein Eintrags-Fehler; damit 03s Versprechen hält, wird zweistufig deserialisiert (Vec<Spanned<toml::Value>>, dann Eintrag für Eintrag), sonst reißt ein Tippfehler die ganze items.toml mit. Genau drei harte Fehler: depends fehlend, depends verletzt, core nicht gefunden — stderr und exit(1). core ist sonst kein Sonderfall; abgesichert wird es durch einen #[test], der mods/core/ mit null Warnungen lädt. Warnungen sammeln sich in Vec<String> und landen in logs/load-<zeitstempel>.log (letzte zehn), plus eine Zeile auf dem Startbildschirm — kein Report-Screen. Meldung meinmod/items.toml:42: core:iron_sword — <message>, Mod-ID vorne, keine Spalte. Rust: handgeschriebenes enum LoadError mit impl Display statt thiserror/anyhow, load_mods() -> Result<(Registries, Vec<String>), LoadError>, Kette endet vor main mit eprintln! und exit(1). Panic-Grenze ist die Datenherkunft: TOML ist Result, eigener Code darf panicken.
  • 05 — Research: Script-Engine (#6) — Empfehlung Rhai wegen Fehlermeldungen mit Zeile und Spalte, billiger Typ-Brücke, AST-Hot-Reload und Build ohne cc. Gemessen: mlua liest unter StdLib::ALL_SAFE per io.open echte Dateien — „safe" heißt dort speichersicher, nicht fähigkeitsbeschränkt. Lua gewinnt bei der Modder-Vertrautheit; WASM scheidet für 1.0 aus. Kernfolge: reden Scripts nur über das Effekt-Vokabular, ist die Engine-Wahl umkehrbar. Entscheidung offen in Ticket 13.
  • 06 — Research: ratatui-App-Architektur (#7) — Screen-Stack über zentralem Game-State statt Elm oder Component-Trait; game/ bleibt ratatui-frei; Popups per Clear auf Sub-Rect statt Widget-Stack; blockierendes event::read() ohne Tick; Dungeon-Grid ist Eigenbau, es gibt kein Widget dafür; ratatui 0.30.2 mit MSRV 1.88; KeyEventKind::Press-Filter unter Windows Pflicht. Voller Bericht in research/06.
  • 07 — Game-State-Baum (#8) — Registries lebt neben Game, nicht darin: als Feld kollidierte &mut game.player mit &game.registries bei jedem Methodenaufruf. Verweise per String-ID, keine Indizes (die hängen an der Ladereihenfolge). Ein Creature für Held und Gegner, aber zwei Felder player und enemies statt einer Vec — Solo-Held heißt, „Gegner greift Gegner an" existiert nicht, und zwei Felder sind ein disjunkter Borrow. Ein Floor, kein Backtracking; Händler müssen deshalb auf jeder Etage vorkommen können. Instanzzustand kommt: ItemInstance { def, affixes }, und Affixe sind die sechste Registry — dadurch bleibt der Save eine ID-Liste und der Preis base_price × Π price_factor. Stapeln bei gleicher def und gleicher affixes. enum CreatureRef, dazu je ein Eingang Game::apply und Game::attribute — Letzteres auf Game, weil Auren beim Träger wohnen und ein Creature seine Attribute allein gar nicht ausrechnen kann. Kein Attribut-Cache.
  • 08 — Event- und Trigger-System (#9) — Der Trigger ist die vierte Wirkungsform neben Modifikator, Aktion und Fähigkeit: { on, condition, actions } darf überall stehen, wo modifiers und actions stehen — sonst entstünde dieselbe Maschine ein zweites Mal in 14 als on_hit. Eine Bedingungsgrammatik für Trigger und Modifikatoren: getaggtes Enum, Liste ist UND, not je Eintrag, kein ODER (zwei Einträge oder ein gemeinsames Tag). Neun feste Events ohne Namensraum, jedes mit source/target; attack_hit und damage_taken bleiben getrennt, weil der eine beim Angreifer und der andere beim Getroffenen hängt. Im Trigger ist self immer der Träger. Warteschlange statt synchron — synchrones Feuern wäre ein Borrow-Fehler (über self.quests iterieren, während self.apply läuft) — mit Zähler bei 100 Events pro Zug statt Zyklengraph. Trigger werden gesucht, nicht indiziert. Quest = Trigger mit count, siebte Registry, erledigte bleiben markiert stehen. Zwei Nachträge an geschlossene Tickets: start_quest als siebte Aktion in 01, und against: Option<CreatureRef> plus events: Vec<Event> in 07.
  • 09 — Save-Schema (#10) — Layout wird gespeichert, nicht gewuerfelt: Regenerieren aus dem Seed braeuchte dieselben Prefabs und denselben Generator-Code, sonst steht der Held nach einem Mod-Update in einer Wand — und der Raumzustand muss ohnehin mit, der Seed spart also nur 3 KB ASCII. Ein Slot, Autosave bei Etagenbetreten und Beenden, Permadeath (geloescht erst beim GameOver-Screen, nicht beim Tod). Der Save ist der Game-Baum per serde-Ableitung, eine Ausnahme fuers Kachelgitter. Schema-Version ist das einzige ausgewertete Header-Feld; abweichend ⇒ ablehnen, Datei bleibt liegen (Migration vor dem ersten ausgelieferten Save waere Code ohne Problem). Fehlende Mods: verwerfen und warnen, nie ablehnen — auch bei der Klasse. Mod-Version wird nur protokolliert, das ist die ehrliche Bilanz des Felds aus 03. Warnungen in dieselbe Sitzungsdatei wie der Mod-Ladelauf, plus Rueckfrage vor dem Betreten (der Spieler will vielleicht nachinstallieren statt weiterzuspielen). Atomar geschrieben ueber .tmp und fs::rename — die eine Stelle, an der bewusst nicht lazy entschieden wurde. Nachtraege: core:attack/core:flee implizit an 02, Confirm { on_yes: Box<Response> } an 11.
  • 10 — Raum-Prefab-Format (#11) — Freie Platzierung mit L-Korridoren; verbunden wird Raum i mit Raum i−1, weil eine Kette Erreichbarkeit strukturell garantiert statt sie nachtraeglich zu pruefen. Prefab als ASCII-Block mit Markern im Block, Tueren explizit (sonst stanzt der Generator Loecher in gezeichnete Waende); Rotation und Spiegelung erlaubt, rotate = false als Opt-out. Ein e sagt „hier steht ein Gegner", nicht welcher — das entscheidet die Etage, sonst waere jedes Prefab an eine Schwierigkeitsstufe gebunden. [[prefabs]] und [[floors]] sind die neunte und zehnte Registry; Auswahl durchgehend ueber Tags, Pflichtraeume zuerst platziert (das ist die Garantie), Flaechenpruefung beim Laden. Kampf beginnt bei room_entered, Gegner gehoeren zum Raum — das erspart Pfadsuche, Sichtlinien und Verfolgung. Floor mit rooms, player_room: Option<usize> (Korridor ist ein echter Zustand). Siegbedingung erstmals definiert: kein [[floors]] mit der naechsten Nummer = gewonnen, die Zahl „5" steht nirgends im Code. Nachtraege: room_entered an 08 (zwoelf Events), GameOver { outcome } an 14.
  • 11 — UI-Gerüst und Screen-Modell (#12) — „Aktion" hiess zwei Dinge: Action bleibt das TOML-Effektvokabular aus 01, Command wird das UI-Kommando, Eingang Game::execute(cmd, reg) — die eine Stelle, an der ein Zug endet (Modifikatoren ticken, Event-Warteschlange leerarbeiten). Zwölf Commands, alle kosten einen Zug (ein costs_turn-Flag waere eine Ausnahmeliste); Angriff und Flucht sind keine Varianten, sondern UseAbility("core:attack") und UseAbility("core:flee") — damit moddbar. Ein gemeinsamer Vec<Screen> fuer Screens und Popups; Screens geben Response zurueck statt selbst zu schieben, sonst haelt ein Screen ein &mut auf die Vec, in der er liegt. Ein Response::Command schliesst alle Overlays — dasselbe Flag, das schon fuers Rendern existiert. Zehn Screens, ListState in der Variante (der Stack erhaelt den Cursor gratis, solange der Screen liegt). Nur KeyEventKind::Press: bei Repeat bestimmte die Tastaturwiederholrate die Spielzeit; gegen zaehes Laufen hilft ein Renn-Kommando in game/. Tastenbelegung fest, Pfeile und hjkl. ratatui-Freiheit von game/ prueft ein #[test] statt eines Workspace. Nachtrag an 01: symbol traegt das Zeichen und einen Farbnamen selbst — ui/theme.rs ist Code und damit fuer Modder unerreichbar.
  • 12 — Prototyp: ratatui-Durchstich (#13) — Das Modell aus 11 traegt. Primaerquelle: Branch prototyp/12-ratatui-durchstich, cargo run --example durchstich. Bestaetigt: mod game bleibt ratatui-frei ohne Disziplin, Grid als eigenes impl Widget ueber buf[(x,y)].set_symbol(…), Vec<Screen> mit Overlay-Flag fuer Rendern und Auto-Schliessen, Response statt Stack-Mutation (kein Borrow-Aerger). Der Windows-Fallstrick ist jetzt eine Zahl: Tasten 14 → Press 7 → Zuege 7 — ohne Filter kostete jeder Schritt zwei Zuege und damit doppelte Spielzeit. Zwei Korrekturen: 11 sagte „alle Commands kosten einen Zug", richtig ist alle ausgefuehrten (Outcome { TurnPassed, Rejected(&str) }, Grund reist mit zurueck); und Command muss Clone sein, damit Confirm es herausgeben kann. Ungetestet blieb Registries neben Game — der Durchstich laedt keine.
  • 13 — Script-Engine wählen (#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 (#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 (#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 (#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.

Nachentscheidungen

Entscheidungen, die nach dem Schließen aller Tickets fällig wurden — beim Bauen, nicht beim Planen.

NPC- und Handelsmodell (während M4)

Der letzte Punkt aus „Not yet specified". Er hing an nichts Offenem mehr: Talk stand in Ticket 11s Command-Liste, npc_talked in Ticket 08s Eventliste, Dialog und Shop unter den elf Screens, und Ticket 10 ließ das Prefab seine NPCs nennen. Was fehlte, waren die Definitionen dahinter.

NPCs sind die vierzehnte Registry. Nicht creatures mit hostile = false: Ein Händler trüge dann health, initiative, cooldowns, ai und exp_reward als dauerhaft toten Ballast, und die Frage „kann ich den Händler erschlagen" wäre beantwortet, ohne dass sie jemand gestellt hat. Vereinheitlichen lohnt, wenn Felder geteilt werden, nicht wenn sie mitgeschleppt werden.

Der Warenbestand ist eine Beutetabelle. shop = "core:blacksmith_stock" verweist auf [[loot_tables]] — dieselben Gewichte, dieselbe Tabelle-in-Tabelle, dieselben Affixe. Kein neuer „Waren"-Begriff, und ein Mod hängt Waren mit demselben Handgriff ein wie Beute. Gezogen wird einmal beim Erzeugen der Etage: Bei jedem Talk neu wäre ausnutzbar (weggehen, wiederkommen, bis das gute Stück im Regal liegt) — dieselbe Klasse wie Save-Scumming, die Ticket 14 geschlossen hat. Der Unterschied zur Bodenbeute, die doch erst beim Aufheben zieht: Dort war das Argument Glück, und bei einem Laden hat Glück nichts zu suchen — ein Kauf ist kein Fund.

Preise mit Spanne, sonst ist Handel ein Exploit oder ein No-op:

kauf    = item_price × (1 − price_modifier/100)
verkauf = item_price × VERKAUF × (1 + price_modifier/100)      # VERKAUF ≈ 0.4

charisma speist price_modifier über eine Formel, die in der Preisfunktion lebt, nicht in Game::attribute — dort wäre es das einzige Attribut mit einem Sonderfall, und ein generischer Pfad mit einer Ausnahme ist die Stelle, an der später jemand rät. Der Händler kauft alles und hat unbegrenzt Gold: Die Spanne verhindert die Geldmaschine bereits, ein Goldvorrat wäre eine zweite Regel gegen dasselbe Problem.

Dialoge sind eine flache Optionsliste, kein Knotengraph. Jede Option trägt Text, condition und actions — damit sind Questvergabe, bedingte Optionen und Ladenzugang abgedeckt. Was erst ein Graph könnte, ist Erzählung, und das Story-Modul steht ausdrücklich unter Out of scope. Der Ausbau ist additiv: goto an der Option plus eine Knotenebene, die vorhandenen Optionen werden der Startknoten.

open_shop wird die elfte Aktion statt eines Sonderfelds an der Option — sie steht dann in derselben Liste wie start_quest und läuft über denselben Eingang.

Angesprochen wird auf dem eigenen Feld. Eine Regel für drei Kommandos: g hebt auf, was hier liegt, > steigt ab, wo hier eine Treppe ist, t spricht an, wer hier steht. Ein NPC blockiert nicht — er steht in einem Laden, nicht in einer Tür.

Befund nebenbei: stackable war nie implementiert. Ticket 02 gab Gegenständen das Feld, Ticket 07 die Stapelregel, Ticket 09 erklärte Gold zum gewöhnlichen Gegenstand — und loot.rs schob für count = "50-120" bis zu 120 einzelne Einträge ins Inventar. ItemInstance bekommt deshalb ein count, und die Regel aus 07 (stackable und gleiche def und gleiche affixes) wird zum ersten Mal ausgeführt statt nur beschrieben.

Not yet specified

  • Balancing — konkrete Zahlen für Attribute, Schaden, Preise und Erfahrung über fünf Etagen. Wartet auf Kampfformeln (14), Progression (15) und Loot (16); vorher gibt es nichts zu balancieren.

  • Boss-Design — ein Boss ist derzeit ein Wesen mit ai-Liste (14) in einem Prefab mit core:boss-Tag (10). Was fehlt, sind Phasen: „ab 50 % Leben ein anderes Verhalten" wäre mit der ai-Bedingungsliste sogar ausdrückbar, „ab 50 % ändern sich die Werte" nicht. Berührt Stufe (c) des Mod-Supports.

  • NPC- und Handelsmodell — entschieden während M4, siehe unten.

  • Inventar-UX — Gewicht oder Kapazität (carry_capacity ist seit 01 ein Attribut ohne Regel), Sortierung, Vergleichsansicht. 11 hat nur den Screen und die Commands festgelegt, nicht was darin steht.

  • Mod-Verteilung — durch 03 auf „Ordner ins Profil entpacken" geschrumpft, ohne Versionsprüfung. Offen ist nur noch, ob 1.0 mehr braucht.

  • Endlosmodus — das erklärte Fernziel (aufgekommen in Ticket 15). 1.0 bleibt endlich; endlos ist additiv: ein Feld repeat = true an der letzten Etage plus eine Skalierungsformel. Berührt drei getroffene Entscheidungen — die Siegbedingung (10), GameOver { outcome: Won } (14) und die floors-Registry, die einen Eintrag je Nummer hat. Die fehlende Level-Obergrenze aus 15 ist bereits darauf ausgelegt.

Out of scope

  • Story-Modul — kommt später als eigenes Modul, linear statt prozedural, ebenfalls moddbar. Eigene Map, wenn 1.0 steht.
  • Party-System — bewusst gegen Solo entschieden; ein Wechsel wäre ein Umbau der Kampfarchitektur, keine Erweiterung.
  • Multiplayer
  • Grafik jenseits des Terminals