Files
rustkeep/.scratch/rustkeep-1.0/map.md

20 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

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, 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).

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.

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 — Warenbestand, Dialogdaten. Wartet auf das Event-System (08). Die Preisformel steht bereits in 07; weil 07 Backtracking ausgeschlossen hat, muss ein Händler auf jeder Etage erreichbar sein.
  • 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.

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