@@ -1,7 +1,7 @@
# 07 — Game-State-Baum
Type: grilling
Status: open
Status: resolved
Blocked by: 01, 02
## Question
@@ -19,3 +19,148 @@ Entschieden ist: ein `Game`-Ownership-Baum mit Verweisen über IDs oder Indizes,
`Game` hängen), und wie wird das aufgelöst?
Ergebnis: Skizze der Structs mit Besitzverhältnissen und Referenzform.
## Answer
Begriffe im [Glossar ](../../../CONTEXT.md ). Dieses Ticket entscheidet **Besitz und Verweisform ** —
nicht die vollständige Feldliste, die kommt aus 14, 15 und 16.
### Definitionen und Zustand sind getrennt
`Registries` (aus Mods geladen, unveränderlich) lebt **neben ** `Game` , nicht darin. Signaturen werden
durchgängig `fn irgendwas(&mut self, reg: &Registries)` .
Der Grund ist ein Rust-Fakt, kein Geschmack: Läge `registries` als Feld in `Game` , kollidierte
`game.player.angreifen(&game.registries)` — ein `&mut` -Borrow auf ein Feld plus ein `&` -Borrow auf ein
anderes geht innerhalb einer Funktion, aber nicht über einen Methodenaufruf auf `&mut self` hinweg.
Das ist kein Randfall, sondern jeder zweite Aufruf im Kampf. Ein Screen bekommt entsprechend
`(&mut Game, &Registries)` , wie 06 es vorzeichnet.
Laufzeitobjekte **verweisen ** per `String` -ID, sie kopieren nicht. Kopien würden bedeuten, dass ein
`overrides` aus Ticket 03 an bereits aufgesammelten Gegenständen vorbeigeht und der Save jede
Definition mitschreiben müsste.
Keine `usize` -Indizes und keine internierten Handles: Bei ein paar hundert Registry-Einträgen und
* einer * Handlung pro Tastendruck kaufen sie Geschwindigkeit, die niemand messen kann, und bezahlen sie
mit einer Auflösungsschicht in beide Richtungen plus einer neuen Fehlerklasse — Indizes hängen an der
Ladereihenfolge, und die ändert sich bei jedem installierten Mod. Die Decke: Taucht je eine heiße
Schleife auf, wird an * dieser * Stelle interniert, nicht global vorsorglich.
### Der Baum
``` rust
struct Game {
player : Creature ,
enemies : Vec < Creature > ,
floor : Floor , // Innenbau gehoert Ticket 10
floor_number : u8 ,
quests : Vec < QuestProgress > , // Form gehoert Ticket 08
turn : u64 ,
}
struct Creature {
name : String ,
base : Attributes , // Katalog aus Ticket 01
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` für Held und Gegner. ** Das Glossar spricht durchgehend von Wesen; zwei Typen hießen,
die Kampfformeln aus 14 zweimal zu schreiben oder ein `trait Combatant` mit genau zwei
Implementierungen einzuziehen — die Abstraktion, die Ticket 02 beim `ItemType` schon abgeräumt hat.
**Held und Gegner sind zwei Felder, keine gemeinsame `Vec`. ** Der Grund ist die Charter: Solo-Held,
kein Party-System. „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` oder Herauskopieren in jeder
Kampfroutine. Zwei Felder sind ein disjunkter Feld-Borrow, den Rust erlaubt. Fällt die
Solo-Entscheidung je, ist das ohnehin ein Umbau der Kampfarchitektur — sie steht unter „Out of scope".
**Ein `Floor`, kein `Vec<Floor>`. ** Abstiegs-Roguelike ohne Rückkehr: hält den Save klein und erspart
dem Generator aus 10 die Garantie, dass Rückwege begehbar bleiben. Die Folge wird bewusst in Kauf
genommen — der Spieler kann nicht zum Händler auf Etage 2 zurück, Händler müssen also als Prefab auf
jeder Etage auftauchen können.
`options` ist kein Beiwerk: Ohne das Feld hat der Held nach dem Charakterbau keine Rasse mehr, weil
Rassen- und Klassenmodifikatoren sonst nirgends herkommen.
### Instanzzustand und die sechste Registry
Zwei Exemplare desselben Gegenstands dürfen sich unterscheiden. Eine Instanz trägt aber keine frei
zusammengesetzten Modifikatoren, sondern **Affix-IDs ** — und Affixe sind eine neue, moddbare Registry
neben den fünf aus Ticket 02:
``` toml
[ [ affixes ] ]
id = "core:flaming"
name = "Flammend" # -> "Flammendes Eisenschwert"
price_factor = 1.5
modifiers = [ { attribute = "damage" , flat = 4 } ]
```
Instanzzustand kostet an drei Stellen, und der Verweis auf Affixe macht zwei davon klein: Der **Save **
bleibt eine Liste von IDs (`{ id = "core:iron_sword", affixes = ["core:flaming"] }` ) statt
serialisierter Modifikator-Strukturen, und die **Preisbildung ** ist `base_price × Π price_factor` .
Dazu sind Affixe Inhalt — ein Modder erfindet „Vampirisch", ohne dass die Engine davon weiß.
Strukturell ist ein Affix dasselbe wie eine Charakteroption: benannte Modifikatorliste plus Metadaten.
Mit frei zusammengesetzten Modifikatoren je Exemplar könnte ein zufällig erzeugtes Schwert theoretisch
alles, praktisch aber nichts, das ein Modder benennen oder ein Händler bepreisen könnte — und die
Anzeige hätte keinen Namen für das, was da liegt.
**Stapelregel: ** `stackable` **und ** gleiche `def` **und ** gleiche `affixes` . Ein Vergleich zweier
`Vec<String>` , eine Zeile, verbietet nichts. Affixe an ausrüstbare Gegenstände zu binden wäre eine
Regel, die dem Modder erklärt, was er darf — Bevormundung, die 02 abgeräumt hat. Dass ein Trank mit
Zufallsaffix nicht mehr stapelt, ist die logische Folge und das Problem dessen, der ihn so baut.
### 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
```
`CreatureRef` fällt direkt aus der Zwei-Felder-Entscheidung: Es gibt keinen gemeinsamen Index mehr,
also braucht eine Aktion eine Adresse.
Der **Lesezugang liegt auf `Game`, nicht auf `Creature` ** . Ticket 01 hat entschieden, dass ein
Modifikator mit `target = "all_enemies"` beim Träger wohnt, damit die Aura mit ihm stirbt. Folglich
braucht das Lesen eines Gegner-Attributs Zugriff auf die Modifikatoren des Helden — ein `Creature`
kann seine eigenen Attribute gar nicht ausrechnen. Zwei Rechenwege für dieselbe Formel aus 01 wären
ein Bug, der sich als Balancing-Problem tarnt: Wer `creature.attribute()` direkt aufruft, weil es
bequemer aussieht, vergisst die Aura.
Ein Attributzugriff läuft die vier Quellen jedes Mal ab: `base` → `options` → `equipment` samt Affixen
→ befristete `modifiers` . **Kein Cache. ** Das passiert pro Tastendruck, nicht pro Frame. Die Decke:
Liest 14 dasselbe Attribut in einer Schleife zwanzigmal, wird * dort * einmal vorberechnet, statt hier
vorsorglich ein Invalidierungsproblem zu bauen.
Modifikatoren werden ausdrücklich **nicht ** beim Ausrüsten in `modifiers` hineinkopiert. Das machte
das Lesen billiger und das Schreiben gefährlich: Jedes Ablegen müsste exakt die richtigen Einträge
wieder herausoperieren, und ein Affix-Wechsel ginge an der Kopie vorbei.
### Auflagen an andere Tickets
- **08 Event- und Trigger-System**: Form von `QuestProgress` ; das Bedingungsvokabular, das 01 bereits
angefordert hat.
- **09 Save-Schema**: serialisiert `Game` . `ItemInstance` schreibt sich als `{ id, affixes }` .
- **10 Raum-Prefab-Format**: Innenbau von `Floor` — Grid, Räume, Bodenloot, Ausgänge. Dazu der
Händlerraum als Prefab, das auf jeder Etage auftauchen kann.
- **14 Kampfformeln und Rundenablauf**: Aktionskatalog und Ziel-Auflösung — und der **Aufräumzeitpunkt
für tote Gegner**. Ein `CreatureRef::Enemy(i)` überlebt das Entfernen aus der `Vec` nicht; wann
aufgeräumt wird, entscheidet der Rundenablauf.
- **16 Loot-Tabellen**: wer Affixe beim Loot vergibt und mit welcher Gewichtung.
### Folgen für bestehenden Code
`Stats` zerfällt in `Attributes` (Katalog aus 01) und Zustandsfelder — heute stehen `max_health` und
`current_health` nebeneinander in derselben Struktur. `Player` wird zu `Creature` . Neben `Item` als
Definition entsteht `ItemInstance` als Laufzeitform.