feat: when-filter am trigger schliesst die luecke aus m3
condition fragt Wesen, when filtert das Ereignis. Getrennt, weil dieselbe Bedingungsgrammatik auch beim Auslesen eines Attributs laeuft — dort gibt es kein Event, und eine Nutzlast-Pruefung in condition waere im Trigger sinnvoll und am Modifikator immer falsch, ohne dass der Modder es sieht. Nutzlast zurueck: damage_type und prefab. critical und amount bleiben draussen, bis jemand sie fordert. Dazu Item.triggers/actions/abilities und Trigger-Suche in der Ausruestung — mods/core/items.toml belegt es mit einem Dornenpanzer, der NUR Feuer reflektiert. 45 Tests.
This commit is contained in:
@@ -39,11 +39,19 @@ pub struct Item {
|
||||
pub color: Option<String>,
|
||||
#[serde(default)]
|
||||
pub modifiers: Vec<Modifier>,
|
||||
/// Was beim Benutzen passiert (Trank, Schriftrolle).
|
||||
#[serde(default)]
|
||||
pub actions: Vec<Action>,
|
||||
/// Faehigkeiten, die der Gegenstand **gewaehrt**, solange er getragen wird.
|
||||
/// Nicht im Inventar tragbar — sonst koennte ein Magier seine Zauber
|
||||
/// verkaufen (Ticket 02).
|
||||
#[serde(default)]
|
||||
pub abilities: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub triggers: Vec<Trigger>,
|
||||
/// 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 {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
use super::action::Action;
|
||||
use super::action::{Action, DamageType};
|
||||
use super::condition::Condition;
|
||||
use super::creature::CreatureRef;
|
||||
|
||||
@@ -33,27 +33,62 @@ pub enum EventKind {
|
||||
CombatEnded,
|
||||
}
|
||||
|
||||
/// **Befund aus M3:** Ticket 08 hat Events eine Nutzlast gegeben (Schadenstyp,
|
||||
/// Prefab-ID, Menge), aber die Bedingungsgrammatik kann sie **gar nicht
|
||||
/// pruefen** — `Check` sieht nur Wesen, nie das Ereignis. Bis das geklaert ist,
|
||||
/// traegt ein Event nur, was tatsaechlich gelesen wird: wer und gegen wen.
|
||||
/// Nutzlast auf Vorrat waere ein Feld, das niemand ausliest.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Event {
|
||||
pub kind: EventKind,
|
||||
pub source: CreatureRef,
|
||||
pub target: Option<CreatureRef>,
|
||||
/// Nutzlast. Nur was ein benanntes Beispiel braucht: `damage_type` fuer
|
||||
/// „Dornenpanzer reflektiert nur Feuer", `prefab` fuer die Quest „betrete die
|
||||
/// Bibliothek". `critical` und `amount` bleiben draussen, bis jemand sie
|
||||
/// fordert (Nachtrag an Ticket 08).
|
||||
pub damage_type: Option<DamageType>,
|
||||
pub prefab: Option<String>,
|
||||
}
|
||||
|
||||
impl Event {
|
||||
pub fn new(kind: EventKind, source: CreatureRef) -> Self {
|
||||
Self { kind, source, target: None }
|
||||
Self { kind, source, target: None, damage_type: None, prefab: None }
|
||||
}
|
||||
|
||||
pub fn to(mut self, target: CreatureRef) -> Self {
|
||||
self.target = Some(target);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_damage(mut self, dt: DamageType) -> Self {
|
||||
self.damage_type = Some(dt);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn in_prefab(mut self, prefab: impl Into<String>) -> Self {
|
||||
self.prefab = Some(prefab.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Filtert das **Ereignis**, nicht ein Wesen — deshalb ein eigenes Feld und
|
||||
/// keine weitere Pruefung in `condition`: Dieselbe Bedingungsgrammatik laeuft
|
||||
/// auch beim Auslesen eines Attributs, und dort gibt es kein Event. Eine
|
||||
/// Nutzlast-Pruefung in `condition` waere im Trigger sinnvoll und am Modifikator
|
||||
/// immer falsch, ohne dass der Modder es sieht.
|
||||
///
|
||||
/// Alle gesetzten Felder muessen zutreffen (implizit UND). **Ein Wert je Feld,
|
||||
/// keine Negation** — wer Feuer *und* Eis reflektieren will, schreibt zwei
|
||||
/// Trigger. Fehlt ein Feld, wird nicht gefiltert.
|
||||
#[derive(Debug, Clone, Default, Deserialize)]
|
||||
pub struct EventFilter {
|
||||
#[serde(default)]
|
||||
pub damage_type: Option<DamageType>,
|
||||
#[serde(default)]
|
||||
pub prefab: Option<String>,
|
||||
}
|
||||
|
||||
impl EventFilter {
|
||||
pub fn matches(&self, event: &Event) -> bool {
|
||||
self.damage_type.is_none_or(|dt| event.damage_type == Some(dt))
|
||||
&& self.prefab.as_ref().is_none_or(|p| event.prefab.as_ref() == Some(p))
|
||||
}
|
||||
}
|
||||
|
||||
/// `{ on = <event>, condition = [...], actions = [...] }` — die vierte
|
||||
@@ -62,6 +97,9 @@ impl Event {
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct Trigger {
|
||||
pub on: EventKind,
|
||||
/// `condition` fragt **Wesen**, `when` filtert das **Ereignis**.
|
||||
#[serde(default)]
|
||||
pub when: EventFilter,
|
||||
#[serde(default)]
|
||||
pub condition: Vec<Condition>,
|
||||
#[serde(default)]
|
||||
@@ -103,5 +141,44 @@ actions = [ { action = "damage", amount = 4, damage_type = "fire", target = "tar
|
||||
let t: Trigger = toml::from_str("on = \"turn_started\"").expect("minimal");
|
||||
assert_eq!(t.chance, 1.0);
|
||||
assert!(t.actions.is_empty());
|
||||
assert!(t.when.matches(&Event::new(EventKind::TurnStarted, CreatureRef::Player)));
|
||||
}
|
||||
|
||||
/// `condition` fragt Wesen, `when` filtert das Ereignis — die Trennung aus
|
||||
/// dem Nachtrag an Ticket 08.
|
||||
#[test]
|
||||
fn when_filtert_die_ereignis_nutzlast() {
|
||||
let t: Trigger = toml::from_str(
|
||||
r#"
|
||||
on = "damage_taken"
|
||||
when = { damage_type = "fire" }
|
||||
"#,
|
||||
)
|
||||
.expect("Filter-Form aus dem Nachtrag");
|
||||
|
||||
let feuer = Event::new(EventKind::DamageTaken, CreatureRef::Player)
|
||||
.with_damage(DamageType::Fire);
|
||||
let eis =
|
||||
Event::new(EventKind::DamageTaken, CreatureRef::Player).with_damage(DamageType::Ice);
|
||||
let ohne = Event::new(EventKind::DamageTaken, CreatureRef::Player);
|
||||
|
||||
assert!(t.when.matches(&feuer));
|
||||
assert!(!t.when.matches(&eis), "der Dornenpanzer reflektiert nur Feuer");
|
||||
assert!(!t.when.matches(&ohne), "ohne Nutzlast passt der Filter nicht");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mehrere_felder_sind_implizit_und() {
|
||||
let f: EventFilter =
|
||||
toml::from_str(r#"damage_type = "fire""#).expect("ein Feld reicht");
|
||||
let mut passend = Event::new(EventKind::RoomEntered, CreatureRef::Player)
|
||||
.with_damage(DamageType::Fire);
|
||||
assert!(f.matches(&passend));
|
||||
|
||||
let beide: EventFilter =
|
||||
toml::from_str("damage_type = \"fire\"\nprefab = \"core:vault\"").expect("zwei Felder");
|
||||
assert!(!beide.matches(&passend), "prefab fehlt — alle Felder muessen zutreffen");
|
||||
passend = passend.in_prefab("core:vault");
|
||||
assert!(beide.matches(&passend));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,9 +238,14 @@ impl Game {
|
||||
out.extend(def.triggers.iter());
|
||||
}
|
||||
}
|
||||
// TODO(M4): Trigger an Gegenstaenden, Affixen und Charakteroptionen —
|
||||
// sie brauchen dieselben drei Zeilen, sobald die Registries
|
||||
// das Feld tragen.
|
||||
// Getragene Ausruestung — der Dornenpanzer haengt hier.
|
||||
for inst in c.equipment.values() {
|
||||
if let Some(item) = reg.items.get(&inst.def) {
|
||||
out.extend(item.triggers.iter());
|
||||
}
|
||||
// TODO(M4): Affix-Trigger des Exemplars, sobald die Affix-Registry
|
||||
// steht — dieselben zwei Zeilen.
|
||||
}
|
||||
out.retain(|t| t.on == kind);
|
||||
out
|
||||
}
|
||||
@@ -302,8 +307,12 @@ impl Game {
|
||||
// Dornenpanzer haengt (Ticket 08/14). Beide feuern **nach** dem
|
||||
// Schaden: Ein Trigger, der Brennen auflegt, ist ein eigener
|
||||
// Schadensfall mit eigener Minderungskette.
|
||||
self.events.push_back(Event::new(EventKind::AttackHit, source).to(victim));
|
||||
self.events.push_back(Event::new(EventKind::DamageTaken, victim).to(source));
|
||||
self.events.push_back(
|
||||
Event::new(EventKind::AttackHit, source).to(victim).with_damage(*damage_type),
|
||||
);
|
||||
self.events.push_back(
|
||||
Event::new(EventKind::DamageTaken, victim).to(source).with_damage(*damage_type),
|
||||
);
|
||||
self.check_death(victim, source);
|
||||
}
|
||||
|
||||
@@ -409,6 +418,9 @@ impl Game {
|
||||
let triggers = self.triggers_for(carrier, event.kind, reg);
|
||||
|
||||
for t in triggers {
|
||||
if !t.when.matches(&event) {
|
||||
continue;
|
||||
}
|
||||
let other = if carrier == event.source { event.target } else { Some(event.source) };
|
||||
if !self.conditions_hold(&t.condition, carrier, other, reg) {
|
||||
continue;
|
||||
@@ -653,8 +665,10 @@ impl Game {
|
||||
if room != self.floor.player_room {
|
||||
self.floor.player_room = room;
|
||||
if let Some(i) = room {
|
||||
self.events
|
||||
.push_back(Event::new(EventKind::RoomEntered, CreatureRef::Player));
|
||||
let prefab = self.floor.rooms[i].prefab.clone();
|
||||
self.events.push_back(
|
||||
Event::new(EventKind::RoomEntered, CreatureRef::Player).in_prefab(prefab),
|
||||
);
|
||||
self.enter_room(i);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user