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

@@ -8,10 +8,6 @@ use serde::Deserialize;
/// Fester Katalog aus Ticket 01. `level` steht hier, weil Bedingungen und
/// Formeln es lesen — die Quelle fuer Aufstiegspunkte ist aber `base.level`
/// und nicht dieser gelesene Wert (Ticket 15).
///
// TODO(M3): `resistance` fehlt — es ist typ-indiziert (sieben Schadenstypen aus
// Ticket 01) und braucht damit eine andere Form als die uebrigen.
// Kommt mit dem Kampf.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "snake_case")]
#[repr(usize)]
@@ -30,10 +26,14 @@ pub enum Attribute {
CriticalFactor,
PriceModifier,
CarryCapacity,
/// **Typ-indiziert**: Ein Modifikator darauf traegt zusaetzlich einen
/// `damage_type`. Die Basis in `Attributes` bleibt ungenutzt — Resistenz
/// kommt ausschliesslich aus Modifikatoren (Ticket 01).
Resistance,
}
impl Attribute {
pub const COUNT: usize = 14;
pub const COUNT: usize = 15;
pub const ALL: [Attribute; Self::COUNT] = [
Attribute::Strength,
@@ -50,6 +50,7 @@ impl Attribute {
Attribute::CriticalFactor,
Attribute::PriceModifier,
Attribute::CarryCapacity,
Attribute::Resistance,
];
pub fn name(self) -> &'static str {
@@ -68,6 +69,7 @@ impl Attribute {
Attribute::CriticalFactor => "Kritischer Faktor",
Attribute::PriceModifier => "Preis-Modifikator",
Attribute::CarryCapacity => "Tragfähigkeit",
Attribute::Resistance => "Resistenz",
}
}
}
@@ -114,12 +116,18 @@ impl Default for Attributes {
#[derive(Debug, Clone, Deserialize)]
pub struct Modifier {
pub attribute: Attribute,
/// Nur bei `attribute = "resistance"`: Resistenz ist typ-indiziert
/// (Ticket 01), alle uebrigen Attribute sind Skalare.
#[serde(default)]
pub damage_type: Option<super::action::DamageType>,
#[serde(default)]
pub flat: f32,
#[serde(default)]
pub percent: f32,
// TODO(M3): `duration`, `target` und `condition` aus Ticket 01/08. In M1
// sind alle Modifikatoren dauerhaft und wirken auf den Traeger.
// TODO(M4): `target` fuer Auren (`all_enemies`) und `condition` fuer
// bedingte Modifikatoren („+5 gegen Untote", Ticket 08). Beide
// brauchen `Game::attribute`s `against`-Parameter, der bereits
// in der Signatur steht.
}
/// Stapelregel aus Ticket 01: `(Basis + Σ flach) × (1 + Σ prozentual)`,
@@ -149,11 +157,17 @@ mod tests {
#[test]
fn stapelregel_erst_flach_dann_prozentual() {
let m = |attribute, flat, percent| Modifier {
attribute,
damage_type: None,
flat,
percent,
};
let mods = [
Modifier { attribute: Attribute::Strength, flat: 2.0, percent: 0.0 },
Modifier { attribute: Attribute::Strength, flat: 3.0, percent: 10.0 },
m(Attribute::Strength, 2.0, 0.0),
m(Attribute::Strength, 3.0, 10.0),
// Fremdes Attribut darf nicht mitzaehlen:
Modifier { attribute: Attribute::Damage, flat: 100.0, percent: 0.0 },
m(Attribute::Damage, 100.0, 0.0),
];
// (10 + 5) * 1.10 == 16.5, nicht 10*1.1 + 5 == 16.0
assert_eq!(stack(10.0, mods.iter(), Attribute::Strength), 16.5);