Vierzehnte Registry: NPCs sind keine Wesen — kein Kampfzustand, keine Initiative, keine Attribute. Ihre Antwortliste ist flach; was erst nach einer Antwort erscheint, ist eine Option mit einer Bedingung. - open_shop als elfte Aktion (game/ setzt eine Absicht, die UI schiebt) - Talk/Choose/Buy/Sell, Talk wirkt auf das eigene Feld wie PickUp - Preisspanne statt Goldkonto beim Haendler, Charisma in der Preisfunktion - Warenkorb einmal bei der Etagenerzeugung gezogen - Stapelregel aus Ticket 07 endlich gebaut: 50-120 Gold war 120 Eintraege - Dialog- und Shop-Screen, NPC-Symbol im Grid
33 KiB
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, ®) und game.attribute(…, ®) 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.
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.
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:
Spannedzeigt 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_healthist Attribut, der Lebensstand ist Zustand. Sechs Aktionen,damageeigenständig wegen der Minderungskette. Stapeln(Basis + Σ flach) × (1 + Σ prozentual). Eine Uhr: der Zug. Sieben feste Schadenstypen, ein Minderungsmechanismus (resistance),armornur 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 ItemTypevermischte Slot, Verhalten und Kategorie und entfällt ersatzlos zugunsten der Felderslot,stackable,consumed_on_use.ItemWeaponundItemArmorfallen 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, mitpick = N. Startausrüstung iston_createals gewöhnliche Aktionsliste.value→base_price,shop_price→price_modifier. - 03 — Mod-Ladepipeline und Manifest (#4) — Zwei Suchpfade:
mods/neben der Binary (enthältcore/) unddata_dir()/rustkeep/modsüber die Cratedirectories; bei gleicher ID gewinnt das Profil, mit Warnzeile.coreliegt als echter Mod-Ordner auf der Platte,include_str!entfällt.manifest.tomlmit vier Feldern (id,name,version,depends) — TOML statt YAML, weilserde_yamlund der Forkserde_ymlbeide archiviert sind. Ladereihenfolge aus einer selbstheilenden Spielerlisteload_order.toml;dependssortiert nicht, sondern prüft, und eine Verletzung ist harter Startfehler. Jede*.tomlim Mod wird eingelesen, jede darf jede Registry-Tabelle enthalten. Präfix wird automatisch vorangestellt, Überschreiben überoverrides = "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 ganzeitems.tomlmit. Genau drei harte Fehler:dependsfehlend,dependsverletzt,corenicht gefunden — stderr undexit(1).coreist sonst kein Sonderfall; abgesichert wird es durch einen#[test], dermods/core/mit null Warnungen lädt. Warnungen sammeln sich inVec<String>und landen inlogs/load-<zeitstempel>.log(letzte zehn), plus eine Zeile auf dem Startbildschirm — kein Report-Screen. Meldungmeinmod/items.toml:42: core:iron_sword — <message>, Mod-ID vorne, keine Spalte. Rust: handgeschriebenesenum LoadErrormitimpl Displaystattthiserror/anyhow,load_mods() -> Result<(Registries, Vec<String>), LoadError>, Kette endet vormainmiteprintln!undexit(1). Panic-Grenze ist die Datenherkunft: TOML istResult, 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 unterStdLib::ALL_SAFEperio.openechte 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 perClearauf Sub-Rectstatt Widget-Stack; blockierendesevent::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) —
Registrieslebt nebenGame, nicht darin: als Feld kollidierte&mut game.playermit&game.registriesbei jedem Methodenaufruf. Verweise perString-ID, keine Indizes (die hängen an der Ladereihenfolge). EinCreaturefür Held und Gegner, aber zwei Felderplayerundenemiesstatt einerVec— Solo-Held heißt, „Gegner greift Gegner an" existiert nicht, und zwei Felder sind ein disjunkter Borrow. EinFloor, 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 Preisbase_price × Π price_factor. Stapeln bei gleicherdefund gleicheraffixes.enum CreatureRef, dazu je ein EingangGame::applyundGame::attribute— Letzteres aufGame, weil Auren beim Träger wohnen und einCreatureseine 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, womodifiersundactionsstehen — sonst entstünde dieselbe Maschine ein zweites Mal in 14 alson_hit. Eine Bedingungsgrammatik für Trigger und Modifikatoren: getaggtes Enum, Liste ist UND,notje Eintrag, kein ODER (zwei Einträge oder ein gemeinsames Tag). Neun feste Events ohne Namensraum, jedes mitsource/target;attack_hitunddamage_takenbleiben getrennt, weil der eine beim Angreifer und der andere beim Getroffenen hängt. Im Trigger istselfimmer der Träger. Warteschlange statt synchron — synchrones Feuern wäre ein Borrow-Fehler (überself.questsiterieren, währendself.applyläuft) — mit Zähler bei 100 Events pro Zug statt Zyklengraph. Trigger werden gesucht, nicht indiziert. Quest = Trigger mitcount, siebte Registry, erledigte bleiben markiert stehen. Zwei Nachträge an geschlossene Tickets:start_questals siebte Aktion in 01, undagainst: Option<CreatureRef>plusevents: 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 derGame-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.tmpundfs::rename— die eine Stelle, an der bewusst nicht lazy entschieden wurde. Nachtraege:core:attack/core:fleeimplizit an 02,Confirm { on_yes: Box<Response> }an 11. - 10 — Raum-Prefab-Format (#11) — Freie Platzierung mit L-Korridoren; verbunden wird Raum
imit Raumi−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 = falseals Opt-out. Einesagt „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 beiroom_entered, Gegner gehoeren zum Raum — das erspart Pfadsuche, Sichtlinien und Verfolgung.Floormitrooms,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_enteredan 08 (zwoelf Events),GameOver { outcome }an 14. - 11 — UI-Gerüst und Screen-Modell (#12) — „Aktion" hiess zwei Dinge:
Actionbleibt das TOML-Effektvokabular aus 01,Commandwird das UI-Kommando, EingangGame::execute(cmd, reg)— die eine Stelle, an der ein Zug endet (Modifikatoren ticken, Event-Warteschlange leerarbeiten). Zwölf Commands, alle kosten einen Zug (eincosts_turn-Flag waere eine Ausnahmeliste); Angriff und Flucht sind keine Varianten, sondernUseAbility("core:attack")undUseAbility("core:flee")— damit moddbar. Ein gemeinsamerVec<Screen>fuer Screens und Popups; Screens gebenResponsezurueck statt selbst zu schieben, sonst haelt ein Screen ein&mutauf dieVec, in der er liegt. EinResponse::Commandschliesst alle Overlays — dasselbe Flag, das schon fuers Rendern existiert. Zehn Screens,ListStatein der Variante (der Stack erhaelt den Cursor gratis, solange der Screen liegt). NurKeyEventKind::Press: beiRepeatbestimmte die Tastaturwiederholrate die Spielzeit; gegen zaehes Laufen hilft ein Renn-Kommando ingame/. Tastenbelegung fest, Pfeile undhjkl. ratatui-Freiheit vongame/prueft ein#[test]statt eines Workspace. Nachtrag an 01:symboltraegt das Zeichen und einen Farbnamen selbst —ui/theme.rsist 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 gamebleibt ratatui-frei ohne Disziplin, Grid als eigenesimpl Widgetueberbuf[(x,y)].set_symbol(…),Vec<Screen>mit Overlay-Flag fuer Rendern und Auto-Schliessen,Responsestatt 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); undCommandmussClonesein, damitConfirmes herausgeben kann. Ungetestet bliebRegistriesnebenGame— 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_operationsausdruecklich 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_scriptals neunte Aktion, Scripts als elfte Registry. Risiken beobachtet mit benanntem Ausstiegssignal;smartstringist 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
i32am Wesen statt als Reihenfolgeliste (die waere eine zweite Indexsammlung mit demselben Ungueltigkeitsproblem); tote Gegner erst am Ende vonexecute, nach dem Leeren der Warteschlange. Trefferformel und Schadenskette mit drei Klemmbereichen —0.8bei Resistenz, weil1.0Immunitaet 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 ⇒Rejectedohne Zug. Achte Registry: Wesen — die Luecke lag seit 02 offen;aials geordnete Bedingungsliste.[balance]verschmilzt feldweise, ausdrueckliche Ausnahme zu 03 (festes Schema). Vier Nachtraege:scaleundfleean 01, drei Felder an 07, zwei Events an 08,GameOveran 11. - 15 — Progression und Level-up (#16) — Ein Aufstieg gibt genau einen frei verteilbaren Punkt auf die fuenf Grundwerte;
AssignPointsist 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_exponentin[balance]). Erfahrung aus Toetungen (exp_rewardam Wesen) und Quests (give_exp, zehnte Aktion), nicht fuers Betreten einer Etage — das belohnte das Durchrennen. Die Zuwachs-Regel bleibt eng: nur Aufstieg undAssignPointsheben den Lebensstand mitmax_health, Ausruestung nie — sonst „heilt ein Ring beim Anlegen" (01). Rasse und Klasse bleiben flach,per_levelentfaellt ersatzlos; was sie traegt, sind Faehigkeiten.leveldarf modifiziert werden, aber Punkte haengen anbase.level— ein+1 level-Ring schenkt keinen. Vier Nachtraege:give_expan 01,current_exp/unspent_pointsanGamestatt ans Wesen (07 hatte die Platzierung hierher delegiert),AssignPointsan 11, zwei Balance-Knoepfe plusexp_rewardan 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 (keindrop_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.luckhat 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 (Roomhaelt eineloot_tableplus Positionen), moeglich nur weil alle Haufen dasselbe Symbol zeigen — dadurch wirkt ein Gluecksring auch auf das, was schon liegt.guaranteed = truefuer Bosse, Mehrfachziehung erlaubt. Nachtraege:Room-Umbau und optionales Prefab-lootan 10,LUCK_FACTORan 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 mitcore:boss-Tag (10). Was fehlt, sind Phasen: „ab 50 % Leben ein anderes Verhalten" wäre mit derai-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_capacityist 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 = truean der letzten Etage plus eine Skalierungsformel. Berührt drei getroffene Entscheidungen — die Siegbedingung (10),GameOver { outcome: Won }(14) und diefloors-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