essences/
combat_origin.rs

1use crate::prelude::*;
2
3use strum_macros::{Display, EnumString};
4
5/// Where one combat event came from.
6///
7/// Triggers fire on [`CombatEventOrigin::Core`] only. Without that boundary a
8/// modifier that grants a crit would re-enter the crit trigger that produced
9/// it and loop forever.
10///
11/// Events that are state transitions rather than combat actions (a global
12/// flip) carry no origin and can never be Core.
13#[derive(
14    Clone,
15    Copy,
16    Debug,
17    Default,
18    Serialize,
19    Deserialize,
20    PartialEq,
21    Eq,
22    Hash,
23    JsonSchema,
24    Tsify,
25    Display,
26    EnumString,
27)]
28#[tsify(from_wasm_abi, into_wasm_abi)]
29pub enum CombatEventOrigin {
30    /// A genuine combat action: an attack, a crit, a dodge, a heal, a mob
31    /// death. Everything the fight engine itself produces is Core.
32    #[default]
33    Core,
34    /// Produced by an Effect Stone or any other modifier. Never re-enters a
35    /// trigger.
36    Proc,
37}
38
39impl CombatEventOrigin {
40    /// Whether triggers may react to an event with this origin.
41    pub fn is_core(self) -> bool {
42        matches!(self, Self::Core)
43    }
44
45    /// Provenance of work produced under both marks at once — the primitive's
46    /// own and the scope it ran inside. Marking only upgrades: once something
47    /// is non-Core, nothing later in the cascade resets it to Core.
48    pub fn merge(self, other: Self) -> Self {
49        if self.is_core() { other } else { self }
50    }
51}
52
53#[cfg(test)]
54mod tests {
55    use super::*;
56
57    #[test]
58    fn only_core_is_core() {
59        assert!(CombatEventOrigin::Core.is_core());
60        assert!(!CombatEventOrigin::Proc.is_core());
61    }
62
63    #[test]
64    fn merge_only_upgrades() {
65        use CombatEventOrigin::{Core, Proc};
66        assert_eq!(Core.merge(Core), Core);
67        assert_eq!(Core.merge(Proc), Proc);
68        assert_eq!(Proc.merge(Core), Proc);
69        assert_eq!(Proc.merge(Proc), Proc);
70    }
71
72    #[test]
73    fn core_is_the_ambient_default() {
74        assert_eq!(CombatEventOrigin::default(), CombatEventOrigin::Core);
75    }
76
77    #[test]
78    fn round_trips_through_string_and_json() {
79        for origin in [CombatEventOrigin::Core, CombatEventOrigin::Proc] {
80            assert_eq!(
81                origin.to_string().parse::<CombatEventOrigin>().unwrap(),
82                origin
83            );
84            let json = serde_json::to_string(&origin).unwrap();
85            assert_eq!(
86                serde_json::from_str::<CombatEventOrigin>(&json).unwrap(),
87                origin
88            );
89        }
90    }
91}