essences/
fight_breakdown.rs

1//! Per-source damage/heal breakdown of one completed fight.
2//!
3//! The summary a result screen reads: how much each skill, stone, law, pet
4//! facet and over-time effect actually contributed. Two rules define what the
5//! numbers mean:
6//!
7//! 1. **Applied, not dealt.** Every amount is counted at the one HP-mutation
8//!    point of its direction (`handle_damage` / `handle_heal`), so damage is
9//!    post-mitigation and post-shield and heal is post-overheal-cap. The rows
10//!    of a side therefore sum to exactly the HP that side actually lost or
11//!    regained.
12//! 2. **Stamped at emit, never inferred.** [`CombatSource`] rides on the
13//!    `Damage` / `Heal` events themselves; the accumulator only adds up what
14//!    the emitting code already declared.
15//!
16//! Ability-stone stat modifiers are deliberately *not* a source of their own:
17//! they multiply an ability's numbers before any event exists, so a skill's row
18//! includes the modifiers socketed into it. Shape supports (Split/Chain/Pierce/
19//! Repeat/Pulse/Leech) do get their own row — they produce extra hits, which are
20//! real events — under [`CombatSource::AbilityDerived`].
21
22use crate::abilities::AbilityId;
23use crate::artifacts::ArtifactStoneTemplateId;
24use crate::cores::LawTemplateId;
25use crate::entity::EntityId;
26use crate::fighting::{EntityTeam, FightTemplateId};
27use crate::game::EntityTemplateId;
28use crate::pet_facets::PetFacet;
29use crate::prelude::*;
30use crate::stones::StoneTemplateId;
31
32/// What produced one damage or heal instance. Stamped at the emit site, where
33/// the producing id is always in scope.
34///
35/// Variant names carry a `Cast`/`Hit`/`Proc` suffix deliberately: an externally
36/// tagged enum turns each variant into a generated C#/TypeScript type of the
37/// same name, and bare `Ability` / `Stone` / `ArtifactStone` would collide with
38/// the client types already generated for those catalog entities — quicktype
39/// resolves such a clash by RENAMING the existing type, which silently breaks
40/// every hand-written reference to it.
41#[derive(
42    Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Hash, JsonSchema, Tsify, Default,
43)]
44#[tsify(from_wasm_abi, into_wasm_abi)]
45pub enum CombatSource {
46    /// Basic attack or active skill, including the ability-stone modifiers
47    /// folded into its numbers.
48    AbilityCast { ability_id: AbilityId },
49    /// An extra hit a support-stone shape produced out of `ability_id`
50    /// (Split/Chain/Pierce/Repeat/Pulse), the Leech heal such a cast fed, or a
51    /// damage-/heal-over-time tick the cast scheduled.
52    AbilityDerived { ability_id: AbilityId },
53    /// A projectile's landing. Counterattack projectiles answer
54    /// [`CombatSource::Counterattack`] instead.
55    ProjectileHit { projectile_id: Uuid },
56    /// Effect/Trigger stone proc.
57    StoneProc { stone_template_id: StoneTemplateId },
58    /// A bonus an Effect Stone or a Law ARMED earlier and that a later hit
59    /// spends (`NextAttackDamageBonus`, splash, echo, lifesteal, …). Which
60    /// stone or law armed it is not recoverable: arming writes a magnitude onto
61    /// an entity attribute, and an attribute cannot carry a template id. Kept
62    /// out of [`CombatSource::Other`] so an equipment row never lands in the
63    /// environment bucket.
64    ArmedBonus,
65    /// An Aspect rule of the worn artifact rebroadcasting an effect stone.
66    ArtifactStoneProc {
67        artifact_stone_template_id: ArtifactStoneTemplateId,
68    },
69    /// Core/law effect.
70    LawProc { law_template_id: LawTemplateId },
71    /// Pet ult cast.
72    PetUlt { ability_id: AbilityId },
73    /// Pet facet / Team Die proc.
74    PetFacetProc { facet: PetFacet },
75    /// Damage-over-time tick whose applier named no ability (environment,
76    /// legacy content). A tick applied by a cast carries that ability under
77    /// [`CombatSource::AbilityDerived`] instead.
78    Dot,
79    /// Heal-over-time tick — the healing sibling of [`CombatSource::Dot`].
80    Hot,
81    /// Passive regeneration tick.
82    Regeneration,
83    /// The target's counterattack proc.
84    Counterattack,
85    /// Perfect Guard's retaliation.
86    Retaliation,
87    /// Environment, fight-start scripts, legacy content — anything with no
88    /// producing id at its emit site.
89    #[default]
90    Other,
91}
92
93impl CombatSource {
94    /// The source a support-stone shape copy of `self` carries: an ability
95    /// keeps its identity but moves into its own derived row. Everything else
96    /// (a proc that was already derived) is unchanged — there is no second
97    /// level of "derived".
98    pub fn derived(self) -> Self {
99        match self {
100            Self::AbilityCast { ability_id } | Self::PetUlt { ability_id } => {
101                Self::AbilityDerived { ability_id }
102            }
103            other => other,
104        }
105    }
106}
107
108/// Aggregated totals for one (actor, source) pair.
109#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Tsify)]
110#[tsify(from_wasm_abi, into_wasm_abi)]
111pub struct BreakdownEntry {
112    pub source: CombatSource,
113    /// Applied damage: post-mitigation, post-shield.
114    pub damage: u64,
115    /// Applied heal: overheal above `max_hp` is not counted.
116    pub heal: u64,
117    /// Damage or heal instances that landed a non-zero amount.
118    pub hits: u32,
119    /// Of those, how many carried the `crit` marker.
120    pub crits: u32,
121}
122
123/// Everything one combatant produced during the fight.
124#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Tsify)]
125#[tsify(from_wasm_abi, into_wasm_abi)]
126pub struct ActorBreakdown {
127    /// Fight-local entity that dealt the damage / did the healing. `None` for
128    /// ownerless effects (DoT ticks, environment) — they are collected into one
129    /// unowned actor rather than dropped.
130    pub entity_id: Option<EntityId>,
131    pub entity_template_id: Option<EntityTemplateId>,
132    pub team: EntityTeam,
133    /// One row per distinct source, in first-contribution order.
134    pub entries: Vec<BreakdownEntry>,
135}
136
137impl ActorBreakdown {
138    /// Total applied damage of this actor.
139    pub fn total_damage(&self) -> u64 {
140        self.entries.iter().map(|entry| entry.damage).sum()
141    }
142
143    /// Total applied heal of this actor.
144    pub fn total_heal(&self) -> u64 {
145        self.entries.iter().map(|entry| entry.heal).sum()
146    }
147}
148
149/// The retrievable per-fight summary. Written once, at `EndFight`.
150#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Tsify)]
151#[tsify(from_wasm_abi, into_wasm_abi)]
152pub struct FightBreakdown {
153    /// Instance id of the fight this summarizes (`ActiveFight::id`), not the
154    /// template — a retry of the same stage is a different fight.
155    pub fight_instance_id: Uuid,
156    pub fight_id: FightTemplateId,
157    pub is_win: bool,
158    /// Fight-clock ticks between fight start and `EndFight`.
159    pub duration_ticks: u64,
160    /// Keyed by acting entity, so a party fight reports hero and ally
161    /// separately and the enemy side's rows are available for a
162    /// "damage taken" view.
163    pub actors: Vec<ActorBreakdown>,
164}