feat: m3 kampf, events und trigger

Der Dungeon hat jetzt Gegner, und man kann sterben.

- game/action.rs: sieben Schadenstypen, fuenf der zehn Aktionen, Target
- game/condition.rs: Bedingungsgrammatik (Liste = UND, not je Eintrag)
- game/event.rs: zwoelf Events, Trigger als vierte Wirkungsform
- game/combat.rs: Treffer- und Schadensformel mit drei Klemmbereichen
- Game::apply, drain_events (FIFO), Initiative, Rundenablauf aus Ticket 14
- data/: abilities-, creatures- und modifiers-Registry, [balance] feldweise
- ui/: Combat-Screen mit Faehigkeitsmenue und Sperrgrund

Drei Befunde:
- Die Trefferformel war toter Code — jeder Angriff traf. Nur der Compiler
  hat es gemeldet ('hit_chance is never used'). Test haelt sie am Leben.
- Die Warteschlange war ein Stack (pop statt pop_front), obwohl Ticket 08
  ausdruecklich 'haengt es hinten an' sagt. Jetzt VecDeque.
- Benannte Modifikatoren (core:poisoned) hatten keine Registry — Ticket 01
  laesst apply_modifier per ID referenzieren, aber keine der zwoelf
  definierte sie. Das ist die dreizehnte.

43 Tests.
This commit is contained in:
2026-08-12 17:03:00 +02:00
parent 8bcd53efa2
commit 217a7acbbc
20 changed files with 1990 additions and 75 deletions

View File

@@ -14,7 +14,10 @@ use serde::Deserialize;
use serde::de::DeserializeOwned;
use toml::Spanned;
use super::registries::{Category, CharacterOption, FloorDef, Item, Prefab, Registries, Slot};
use super::registries::{
Ability, Category, CharacterOption, CreatureDef, FloorDef, Item, ModifierDef, Prefab,
Registries, Slot,
};
/// Genau drei harte Fehler (Ticket 04). Alles Uebrige ist Warnung plus
/// Verwerfen des **einzelnen Eintrags**.
@@ -248,6 +251,16 @@ struct ModFile {
prefabs: Vec<Spanned<toml::Value>>,
#[serde(default)]
floors: Vec<Spanned<toml::Value>>,
#[serde(default)]
abilities: Vec<Spanned<toml::Value>>,
#[serde(default)]
creatures: Vec<Spanned<toml::Value>>,
#[serde(default)]
modifiers: Vec<Spanned<toml::Value>>,
/// **Keine** Tabelle mit `[[...]]`, sondern ein einzelner Block, der
/// feldweise verschmilzt — die ausdrueckliche Ausnahme zu Ticket 03.
#[serde(default)]
balance: Option<toml::Value>,
}
/// Damit die vier Registry-Typen durch dieselbe Einlese-Schleife passen.
@@ -278,6 +291,9 @@ impl_entry!(Category);
impl_entry!(CharacterOption);
impl_entry!(Prefab);
impl_entry!(FloorDef);
impl_entry!(Ability);
impl_entry!(CreatureDef);
impl_entry!(ModifierDef);
struct FileCtx<'a> {
module: &'a str,
@@ -451,6 +467,7 @@ pub fn load_mods() -> Result<(Registries, Vec<String>), LoadError> {
// Phase 2 — Einlesen in Ladereihenfolge
let mut reg = Registries::default();
let mut balance_raw = toml::value::Table::new();
for id in &order {
let Some(info) = found.iter().find(|m| &m.id == id) else { continue };
@@ -482,15 +499,44 @@ pub fn load_mods() -> Result<(Registries, Vec<String>), LoadError> {
parse_entries(parsed.options, &ctx, &mut reg.options, &mut warnings);
parse_entries(parsed.prefabs, &ctx, &mut reg.prefabs, &mut warnings);
parse_entries(parsed.floors, &ctx, &mut reg.floors, &mut warnings);
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);
// `[balance]` verschmilzt **feldweise** (Ticket 14): Ein Mod, der
// einen Knopf dreht, schreibt eine Zeile — und ein neuer Knopf in
// der naechsten Version bricht ihn nicht.
if let Some(table) = parsed.balance {
merge_balance(table, &mut balance_raw);
}
}
}
// `[balance]` erst am Ende deserialisieren: Fehlende Felder fallen dann auf
// die Engine-Standards zurueck, nicht auf die des zuletzt geladenen Mods.
match toml::Value::Table(balance_raw).try_into() {
Ok(b) => reg.balance = b,
Err(e) => warnings.push(format!("[balance]: {} — Standardwerte gelten", e.message())),
}
// Phase 3 — Referenzen aufloesen
resolve(&mut reg, &mut warnings);
Ok((reg, warnings))
}
/// Feldweises Verschmelzen. Die Ausnahme bleibt eng, weil ihre Begruendung eng
/// ist: `[balance]` hat ein **festes, der Engine bekanntes Schema** — genau das
/// fehlte bei Inhalt mit offenen Feldern, und das war Ticket 03s Argument gegen
/// feldweises Patchen.
fn merge_balance(from: toml::Value, into: &mut toml::value::Table) {
if let toml::Value::Table(t) = from {
for (k, v) in t {
into.insert(k, v);
}
}
}
fn toml_files(dir: &Path) -> Vec<PathBuf> {
let mut files: Vec<PathBuf> = fs::read_dir(dir)
.into_iter()