feat: m4 teil 1 — beute, inventar und affixe

- data/: [[loot_tables]] und [[affixes]] als Registries; LootEntry mit
  guaranteed, nothing, table-in-table, count und affixes
- game/loot.rs: EINE Ziehmaschinerie fuer Gegenstaende und Affixe,
  Tiefendeckel gegen wechselseitigen Bezug
- PickUp zieht erst beim Aufheben (Ticket 16) — moeglich nur, weil alle
  Haufen dasselbe Symbol zeigen
- UseItem/Equip/Unequip/Drop, Inventar-Screen als Registry-Schleife
- Affixe im Attributpfad, in der Resistenz und in der Trigger-Suche;
  item_label stellt Affixnamen voran, item_price multipliziert Faktoren
- Room haelt eine loot_table (nicht je Haufen eine), Prefab-loot
  ueberschreibt die Etagentabelle

53 Tests. Offen aus M4: Handel.
This commit is contained in:
2026-08-12 17:56:28 +02:00
parent cdcabcce21
commit 197f62ce33
16 changed files with 849 additions and 19 deletions

View File

@@ -15,8 +15,8 @@ use serde::de::DeserializeOwned;
use toml::Spanned;
use super::registries::{
Ability, Category, CharacterOption, CreatureDef, FloorDef, Item, ModifierDef, Prefab,
Registries, Slot,
Ability, Affix, Category, CharacterOption, CreatureDef, FloorDef, Item, LootTable, ModifierDef,
Prefab, Registries, Slot,
};
/// Genau drei harte Fehler (Ticket 04). Alles Uebrige ist Warnung plus
@@ -257,6 +257,10 @@ struct ModFile {
creatures: Vec<Spanned<toml::Value>>,
#[serde(default)]
modifiers: Vec<Spanned<toml::Value>>,
#[serde(default)]
affixes: Vec<Spanned<toml::Value>>,
#[serde(default)]
loot_tables: Vec<Spanned<toml::Value>>,
/// **Keine** Tabelle mit `[[...]]`, sondern ein einzelner Block, der
/// feldweise verschmilzt — die ausdrueckliche Ausnahme zu Ticket 03.
#[serde(default)]
@@ -294,6 +298,8 @@ impl_entry!(FloorDef);
impl_entry!(Ability);
impl_entry!(CreatureDef);
impl_entry!(ModifierDef);
impl_entry!(Affix);
impl_entry!(LootTable);
struct FileCtx<'a> {
module: &'a str,
@@ -502,6 +508,8 @@ pub fn load_mods() -> Result<(Registries, Vec<String>), LoadError> {
parse_entries(parsed.abilities, &ctx, &mut reg.abilities, &mut warnings);
parse_entries(parsed.creatures, &ctx, &mut reg.creatures, &mut warnings);
parse_entries(parsed.modifiers, &ctx, &mut reg.modifiers, &mut warnings);
parse_entries(parsed.affixes, &ctx, &mut reg.affixes, &mut warnings);
parse_entries(parsed.loot_tables, &ctx, &mut reg.loot_tables, &mut warnings);
// `[balance]` verschmilzt **feldweise** (Ticket 14): Ein Mod, der
// einen Knopf dreht, schreibt eine Zeile — und ein neuer Knopf in

View File

@@ -5,4 +5,6 @@ pub mod loader;
pub mod registries;
pub use loader::{load_mods, write_log};
pub use registries::{Balance, CreatureDef, FloorDef, Prefab, Registries};
pub use registries::{
Balance, CreatureDef, FloorDef, LootEntry, Prefab, Registries, parse_range,
};

View File

@@ -229,6 +229,90 @@ impl Default for Balance {
}
}
/// Eine benannte Modifikatorliste, die an einem **einzelnen Exemplar** haengt —
/// „Flammendes Eisenschwert" (Ticket 07).
///
/// Abstufungen sind **eigene Eintraege** mit kleinerem Gewicht (`core:flaming`
/// neben `core:greater_flaming`), keine Zahl am Exemplar: Eine Staerke je
/// Exemplar haette Save-Format, Preisformel und Stapelregel auf einmal
/// gekostet (Ticket 16).
#[derive(Debug, Clone, Deserialize)]
pub struct Affix {
pub id: String,
/// Wird dem Gegenstandsnamen vorangestellt: „Flammendes Eisenschwert".
pub name: String,
#[serde(default = "one_f32")]
pub price_factor: f32,
#[serde(default)]
pub modifiers: Vec<Modifier>,
#[serde(default)]
pub triggers: Vec<Trigger>,
#[serde(default)]
pub overrides: Option<String>,
}
fn one_f32() -> f32 {
1.0
}
/// Ein Eintrag einer Beutetabelle. Genau eine der vier Formen (Ticket 16).
#[derive(Debug, Clone, Deserialize)]
pub struct LootEntry {
#[serde(default = "one")]
pub weight: u32,
/// Wird **immer** ausgegeben und nimmt an der Gewichtung nicht teil — mit
/// Gewichten allein laesst sich „sicher" nicht ausdruecken, und Bosse
/// stehen in der Destination.
#[serde(default)]
pub guaranteed: bool,
#[serde(default)]
pub item: Option<String>,
/// Tabelle-in-Tabelle: liefert Seltenheitsstufen, ohne dass „Seltenheit"
/// ein Begriff werden muss. Zugleich der **Mod-Einhaengepunkt** — ein Mod
/// bindet `core:floor_3_table` als Untertabelle ein.
#[serde(default)]
pub table: Option<String>,
/// Dieselbe Maschinerie zieht Gegenstaende *und* Affixe.
#[serde(default)]
pub affix: Option<String>,
/// Niete. Ein gewoehnlicher Eintrag, kein eigenes `drop_chance`-Feld — die
/// Engine kennt eine Mechanik statt zweier.
#[serde(default)]
pub nothing: bool,
/// Stapelmenge, als Bereich `"1-2"`.
#[serde(default)]
pub count: Option<String>,
/// Affix-Tabelle fuer diesen Gegenstand.
#[serde(default)]
pub affixes: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct LootTable {
pub id: String,
/// Wie viele Zuege, als Bereich `"1-3"`.
#[serde(default)]
pub rolls: Option<String>,
#[serde(default)]
pub entries: Vec<LootEntry>,
#[serde(default)]
pub overrides: Option<String>,
}
/// Bereichsangabe `"1-3"` oder `"2"`. Ein Zweizeiler statt einer Grammatik.
pub fn parse_range(spec: Option<&String>) -> (u32, u32) {
let Some(s) = spec else { return (1, 1) };
match s.split_once('-') {
Some((a, b)) => (a.trim().parse().unwrap_or(1), b.trim().parse().unwrap_or(1)),
None => {
let n = s.trim().parse().unwrap_or(1);
(n, n)
}
}
}
/// Ein von Hand gezeichneter Raum als ASCII-Block (Ticket 10).
#[derive(Debug, Clone, Deserialize)]
pub struct Prefab {
@@ -283,9 +367,11 @@ pub struct FloorDef {
/// als bei Prefabs, weil eine Etage ihre Gegner benennen koennen soll.
#[serde(default)]
pub creatures: Vec<WeightedId>,
/// Beutetabelle fuer die `l`-Plaetze (Ticket 16).
#[serde(default)]
pub loot: Option<String>,
#[serde(default)]
pub overrides: Option<String>,
// TODO(M4): `loot` — die Beutetabelle fuer die `l`-Plaetze (Ticket 16).
}
#[derive(Debug, Clone, Deserialize)]
@@ -309,9 +395,11 @@ pub struct Registries {
pub abilities: HashMap<String, Ability>,
pub creatures: HashMap<String, CreatureDef>,
pub modifiers: HashMap<String, ModifierDef>,
pub affixes: HashMap<String, Affix>,
pub loot_tables: HashMap<String, LootTable>,
/// Keine Registry — ein Block mit festem Schema, feldweise verschmolzen.
pub balance: Balance,
// TODO(M4+): `affixes`, `quests`, `loot_tables`, `scripts`.
// TODO(M5+): `quests` (Ticket 08) und `scripts` (Ticket 13).
}
impl Registries {