feat: m1 vertikaler durchstich — mod-lader, registries, attributberechnung, screen-stack

Ersetzt src/ vollstaendig: print_box, include_str! und enum ItemType fallen,
wie Ticket 01-03 es vorgezeichnet haben.

- data/: Mod-Lader mit zwei Suchpfaden, selbstheilender load_order.toml,
  drei Phasen und zweistufiger Deserialisierung (03/04)
- game/: Attributkatalog, Creature, Game::attribute ueber mehrere Quellen,
  Command mit Outcome (01/07/11/12)
- ui/: Screen-Stack mit Response, Grid-Widget, Press-Filter (06/11/12)
- mods/core/: echte TOML-Dateien auf der Platte statt include_str!

10 Tests, darunter der Borrow-Beweis fuer Registries-neben-Game, den
Ticket 12 nicht fuehren konnte.
This commit is contained in:
2026-08-12 15:39:17 +02:00
parent 9c5efab331
commit b39491916f
28 changed files with 3414 additions and 279 deletions

116
src/data/registries.rs Normal file
View File

@@ -0,0 +1,116 @@
//! Die Definitionen, die aus TOML geladen werden — Ticket 02.
//!
//! Einige Felder liest in M1 noch niemand: `base_price` wartet auf den Handel,
//! `stackable` und `consumed_on_use` aufs Inventar, `symbol` auf Gegenstaende,
//! die auf dem Boden liegen. Sie stehen trotzdem hier, weil sie **das
//! Datenmodell** sind und `mods/core` sie bereits schreibt — sie erst spaeter
//! einzufuehren hiesse, die TOML-Dateien zweimal anzufassen.
//!
//! ponytail: modulweites `allow` — es versteckt ab M2 auch echte Leichen.
//! Raus, sobald Handel und Inventar die Felder tatsaechlich lesen.
#![allow(dead_code)]
use std::collections::HashMap;
use serde::Deserialize;
use crate::game::Modifier;
/// Ein Ding im Spiel, **einheitlich strukturiert**. Es gibt keinen Item-Typ:
/// `enum ItemType` vermischte Slot, Verhalten und Kategorie (Ticket 02).
#[derive(Debug, Clone, Deserialize)]
pub struct Item {
pub id: String,
pub name: String,
#[serde(default)]
pub base_price: u32,
/// Leer = nicht ausruestbar.
pub slot: Option<String>,
#[serde(default)]
pub stackable: bool,
#[serde(default)]
pub consumed_on_use: bool,
#[serde(default = "default_symbol")]
pub symbol: char,
#[serde(default)]
pub color: Option<String>,
#[serde(default)]
pub modifiers: Vec<Modifier>,
/// Ersetzt den genannten Eintrag und uebernimmt dessen ID (Ticket 03).
#[serde(default)]
pub overrides: Option<String>,
// TODO(M3+): `actions`, `abilities`, `triggers` — brauchen erst Kampf und
// Event-System.
}
fn default_symbol() -> char {
'?'
}
/// Eine benannte Stelle am Charakter. Moddbarer Inhalt, kein fester
/// Aufzaehlungswert (Ticket 02).
#[derive(Debug, Clone, Deserialize)]
pub struct Slot {
pub id: String,
pub name: String,
#[serde(default)]
pub order: i32,
#[serde(default)]
pub overrides: Option<String>,
}
/// Kategorie von Charakteroptionen — selbst Inhalt, mit `pick = N`.
#[derive(Debug, Clone, Deserialize)]
pub struct Category {
pub id: String,
pub name: String,
#[serde(default)]
pub order: i32,
#[serde(default = "one")]
pub pick: u32,
#[serde(default)]
pub overrides: Option<String>,
}
fn one() -> u32 {
1
}
/// Eine Wahl beim Charakterbau. Rasse und Klasse sind beides davon.
#[derive(Debug, Clone, Deserialize)]
pub struct CharacterOption {
pub id: String,
pub category: String,
pub name: String,
#[serde(default)]
pub description: String,
#[serde(default)]
pub modifiers: Vec<Modifier>,
#[serde(default)]
pub overrides: Option<String>,
// TODO(M3): `abilities` und `on_create` (Ticket 02) — Faehigkeiten und
// Startausruestung brauchen Kampf bzw. Inventar.
}
/// Alle geladenen Definitionen. Liegt **neben** `Game`, nicht darin — als Feld
/// kollidierte `&mut game.player` mit `&game.registries` bei jedem
/// Methodenaufruf (Ticket 07).
#[derive(Debug, Default)]
pub struct Registries {
pub items: HashMap<String, Item>,
pub slots: HashMap<String, Slot>,
pub categories: HashMap<String, Category>,
pub options: HashMap<String, CharacterOption>,
// TODO(M2+): `affixes`, `abilities`, `quests`, `creatures`, `prefabs`,
// `floors`, `loot_tables`, `scripts` — zwoelf sind es am Ende.
}
impl Registries {
/// Slots in Anzeigereihenfolge. Der Charakterbogen schleift darueber,
/// statt ein Layout zu mauern (Ticket 02).
pub fn slots_ordered(&self) -> Vec<&Slot> {
let mut v: Vec<&Slot> = self.slots.values().collect();
v.sort_by_key(|s| (s.order, s.id.clone()));
v
}
}