07 — Game-State-Baum #8

Closed
opened 2026-08-11 13:13:26 +00:00 by RDietermann · 1 comment
Owner

Teil der Map #1 · Type: wayfinder:grilling · Blocked by: #2, #3

Wie sieht der Weltzustand als Rust-Datenstruktur aus? Ownership, Referenzform, Borrow-Checker-Konflikte.

Volle Fragestellung (kanonisch): .scratch/rustkeep-1.0/issues/07-game-state-baum.md

Teil der Map #1 · **Type: `wayfinder:grilling`** · **Blocked by: #2, #3** Wie sieht der Weltzustand als Rust-Datenstruktur aus? Ownership, Referenzform, Borrow-Checker-Konflikte. Volle Fragestellung (kanonisch): `.scratch/rustkeep-1.0/issues/07-game-state-baum.md`
RDietermann added the wayfinder:grilling label 2026-08-11 13:13:26 +00:00
Author
Owner

Aufgeloest in f25f393. Kanonische Fassung: .scratch/rustkeep-1.0/issues/07-game-state-baum.md

Dieses Ticket entscheidet Besitz und Verweisform — nicht die vollstaendige Feldliste, die kommt aus 14, 15 und 16.

Definitionen und Zustand sind getrennt

Registries lebt neben Game, nicht darin. Der Grund ist ein Rust-Fakt, kein Geschmack: als Feld kollidierte game.player.angreifen(&game.registries) — &mut auf ein Feld plus & auf ein anderes geht innerhalb einer Funktion, aber nicht ueber einen Methodenaufruf auf &mut self hinweg. Das ist jeder zweite Aufruf im Kampf. Signaturen werden durchgaengig fn irgendwas(&mut self, reg: &Registries), ein Screen bekommt (&mut Game, &Registries).

Verweise per String-ID, keine Indizes: Bei ein paar hundert Eintraegen und einer Handlung pro Tastendruck kaufen Indizes Geschwindigkeit, die niemand messen kann, und haengen dafuer an der Ladereihenfolge — die sich bei jedem installierten Mod aendert.

Der Baum

struct Game {
    player: Creature,
    enemies: Vec<Creature>,
    floor: Floor,                // Innenbau gehoert #11
    floor_number: u8,
    quests: Vec<QuestProgress>,  // Form gehoert #9
    turn: u64,
}

struct Creature {
    name: String,
    base: Attributes,
    health: f32, stamina: f32,          // Zustand, getrennt von den Attributen
    options: Vec<OptionId>,             // Rasse, Klasse
    inventory: Vec<ItemInstance>,
    equipment: HashMap<SlotId, ItemInstance>,
    tags: Vec<String>,
    modifiers: Vec<ActiveModifier>,     // nur befristete
}

struct ItemInstance { def: ItemId, affixes: Vec<AffixId> }
enum CreatureRef { Player, Enemy(usize) }

Ein Creature fuer Held und Gegner — zwei Typen hiessen, die Kampfformeln zweimal zu schreiben oder ein trait Combatant mit genau zwei Implementierungen einzuziehen.

Aber zwei Felder, keine gemeinsame Vec. Der Grund ist die Charter: Solo-Held. "Gegner greift Gegner an" existiert nicht, also kauft die gemeinsame Vec eine Symmetrie, die das Spiel nie braucht, und bezahlt sie mit split_at_mut in jeder Kampfroutine. Zwei Felder sind ein disjunkter Feld-Borrow, den Rust erlaubt.

Ein Floor, kein Backtracking. Haelt den Save klein, erspart dem Generator begehbare Rueckwege — Folge: Haendler muessen auf jeder Etage auftauchen koennen.

Instanzzustand und die sechste Registry

Zwei Exemplare desselben Gegenstands duerfen sich unterscheiden, aber eine Instanz traegt Affix-IDs, keine freien Modifikatoren. Affixe sind eine neue moddbare Registry:

[[affixes]]
id = "core:flaming"
name = "Flammend"
price_factor = 1.5
modifiers = [ { attribute = "damage", flat = 4 } ]

Damit bleibt der Save eine ID-Liste und der Preis base_price × Π price_factor. Mit freien Modifikatoren koennte ein Zufallsschwert theoretisch alles, praktisch aber nichts, das ein Modder benennen oder ein Haendler bepreisen koennte.

Stapelregel: stackable und gleiche def und gleiche affixes.

Ein Eingang, ein Ausgang

Game::apply(&mut self, action: &Action, source: CreatureRef, target: CreatureRef, reg: &Registries)
Game::attribute(&self, who: CreatureRef, attr: Attribute, reg: &Registries) -> f32

Der Lesezugang liegt auf Game, nicht auf Creature: Eine Aura mit target = "all_enemies" wohnt beim Traeger (#2), also braucht das Lesen eines Gegner-Attributs die Modifikatoren des Helden — ein Creature kann seine eigenen Attribute gar nicht ausrechnen. Zwei Rechenwege waeren ein Bug, der sich als Balancing-Problem tarnt.

Vier Quellen bei jedem Zugriff: base -> options -> equipment samt Affixen -> befristete modifiers. Kein Cache — das passiert pro Tastendruck, nicht pro Frame. Modifikatoren werden ausdruecklich nicht beim Ausruesten hineinkopiert: das machte das Lesen billiger und das Schreiben gefaehrlich.

Auflagen

  • #9 (08 Events): Form von QuestProgress; Handler laufen durch Game::apply
  • #10 (09 Save): serialisiert Game, ItemInstance als { id, affixes }, options muss mit
  • #11 (10 Prefabs): Innenbau von Floor; Haendlerraum auf jeder Etage; Bodenloot ist ItemInstance
  • #15 (14 Kampf): Aktionskatalog, Ziel-Aufloesung — und wann tote Gegner aufgeraeumt werden, denn CreatureRef::Enemy(i) ueberlebt das Entfernen aus der Vec nicht
  • #17 (16 Loot): wer Affixe vergibt und mit welcher Gewichtung

Folgen fuer bestehenden Code

Stats zerfaellt in Attributes und Zustandsfelder, Player wird zu Creature, neben Item entsteht ItemInstance.

**Aufgeloest** in f25f393. Kanonische Fassung: `.scratch/rustkeep-1.0/issues/07-game-state-baum.md` Dieses Ticket entscheidet **Besitz und Verweisform** — nicht die vollstaendige Feldliste, die kommt aus 14, 15 und 16. ### Definitionen und Zustand sind getrennt `Registries` lebt **neben** `Game`, nicht darin. Der Grund ist ein Rust-Fakt, kein Geschmack: als Feld kollidierte `game.player.angreifen(&game.registries)` — `&mut` auf ein Feld plus `&` auf ein anderes geht innerhalb einer Funktion, aber nicht ueber einen Methodenaufruf auf `&mut self` hinweg. Das ist jeder zweite Aufruf im Kampf. Signaturen werden durchgaengig `fn irgendwas(&mut self, reg: &Registries)`, ein Screen bekommt `(&mut Game, &Registries)`. Verweise per `String`-ID, keine Indizes: Bei ein paar hundert Eintraegen und *einer* Handlung pro Tastendruck kaufen Indizes Geschwindigkeit, die niemand messen kann, und haengen dafuer an der Ladereihenfolge — die sich bei jedem installierten Mod aendert. ### Der Baum ```rust struct Game { player: Creature, enemies: Vec<Creature>, floor: Floor, // Innenbau gehoert #11 floor_number: u8, quests: Vec<QuestProgress>, // Form gehoert #9 turn: u64, } struct Creature { name: String, base: Attributes, health: f32, stamina: f32, // Zustand, getrennt von den Attributen options: Vec<OptionId>, // Rasse, Klasse inventory: Vec<ItemInstance>, equipment: HashMap<SlotId, ItemInstance>, tags: Vec<String>, modifiers: Vec<ActiveModifier>, // nur befristete } struct ItemInstance { def: ItemId, affixes: Vec<AffixId> } enum CreatureRef { Player, Enemy(usize) } ``` **Ein `Creature`** fuer Held und Gegner — zwei Typen hiessen, die Kampfformeln zweimal zu schreiben oder ein `trait Combatant` mit genau zwei Implementierungen einzuziehen. **Aber zwei Felder, keine gemeinsame `Vec`.** Der Grund ist die Charter: Solo-Held. "Gegner greift Gegner an" existiert nicht, also kauft die gemeinsame `Vec` eine Symmetrie, die das Spiel nie braucht, und bezahlt sie mit `split_at_mut` in jeder Kampfroutine. Zwei Felder sind ein disjunkter Feld-Borrow, den Rust erlaubt. **Ein `Floor`**, kein Backtracking. Haelt den Save klein, erspart dem Generator begehbare Rueckwege — Folge: Haendler muessen auf jeder Etage auftauchen koennen. ### Instanzzustand und die sechste Registry Zwei Exemplare desselben Gegenstands duerfen sich unterscheiden, aber eine Instanz traegt **Affix-IDs**, keine freien Modifikatoren. Affixe sind eine neue moddbare Registry: ```toml [[affixes]] id = "core:flaming" name = "Flammend" price_factor = 1.5 modifiers = [ { attribute = "damage", flat = 4 } ] ``` Damit bleibt der Save eine ID-Liste und der Preis `base_price × Π price_factor`. Mit freien Modifikatoren koennte ein Zufallsschwert theoretisch alles, praktisch aber nichts, das ein Modder benennen oder ein Haendler bepreisen koennte. Stapelregel: `stackable` **und** gleiche `def` **und** gleiche `affixes`. ### Ein Eingang, ein Ausgang ```rust Game::apply(&mut self, action: &Action, source: CreatureRef, target: CreatureRef, reg: &Registries) Game::attribute(&self, who: CreatureRef, attr: Attribute, reg: &Registries) -> f32 ``` Der Lesezugang liegt auf `Game`, nicht auf `Creature`: Eine Aura mit `target = "all_enemies"` wohnt beim Traeger (#2), also braucht das Lesen eines Gegner-Attributs die Modifikatoren des Helden — ein `Creature` kann seine eigenen Attribute gar nicht ausrechnen. Zwei Rechenwege waeren ein Bug, der sich als Balancing-Problem tarnt. Vier Quellen bei jedem Zugriff: `base` -> `options` -> `equipment` samt Affixen -> befristete `modifiers`. **Kein Cache** — das passiert pro Tastendruck, nicht pro Frame. Modifikatoren werden ausdruecklich nicht beim Ausruesten hineinkopiert: das machte das Lesen billiger und das Schreiben gefaehrlich. ### Auflagen - **#9** (08 Events): Form von `QuestProgress`; Handler laufen durch `Game::apply` - **#10** (09 Save): serialisiert `Game`, `ItemInstance` als `{ id, affixes }`, `options` muss mit - **#11** (10 Prefabs): Innenbau von `Floor`; Haendlerraum auf jeder Etage; Bodenloot ist `ItemInstance` - **#15** (14 Kampf): Aktionskatalog, Ziel-Aufloesung — und **wann tote Gegner aufgeraeumt werden**, denn `CreatureRef::Enemy(i)` ueberlebt das Entfernen aus der `Vec` nicht - **#17** (16 Loot): wer Affixe vergibt und mit welcher Gewichtung ### Folgen fuer bestehenden Code `Stats` zerfaellt in `Attributes` und Zustandsfelder, `Player` wird zu `Creature`, neben `Item` entsteht `ItemInstance`.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: RDietermann/rustkeep#8