Skip to main content

wowlab_engine_domain/encounter/
definition.rs

1use wowlab_types::{
2    constants::MS_PER_SECOND,
3    sim::{EnemyIdx, EnemyRole, GroupId, SimTime, SpatialTransform},
4};
5
6/// Runtime health behavior resolved before combat construction.
7#[derive(Clone, Copy, Debug, PartialEq)]
8// #t(rust_non_exhaustive_on_public) exact health variants are a fixed encounter-runtime contract
9pub enum HealthModel {
10    DamageDriven {
11        max_health: f64,
12    },
13    ScriptedLinear {
14        display_max_health: f64,
15        death_at_s: f64,
16    },
17}
18
19impl HealthModel {
20    #[must_use]
21    pub const fn max_health(self) -> f64 {
22        match self {
23            Self::DamageDriven { max_health } => max_health,
24            Self::ScriptedLinear {
25                display_max_health, ..
26            } => display_max_health,
27        }
28    }
29
30    #[must_use]
31    pub fn scripted_health_at(self, spawned_at: SimTime, now: SimTime) -> Option<f64> {
32        let Self::ScriptedLinear {
33            display_max_health,
34            death_at_s,
35        } = self
36        else {
37            return None;
38        };
39        let elapsed_s = now.saturating_sub(spawned_at).as_secs_f64();
40
41        Some(display_max_health * (1.0 - elapsed_s / death_at_s).clamp(0.0, 1.0))
42    }
43}
44
45/// Target-side probabilities and reductions used by the player attack table.
46#[derive(Clone, Copy, Debug, PartialEq)]
47pub struct EnemyAttackTable {
48    pub dodge_chance: f64,
49    pub parry_chance: f64,
50    pub block_chance: f64,
51    pub critical_block_chance: f64,
52}
53
54impl Default for EnemyAttackTable {
55    fn default() -> Self {
56        const DEFAULT_DODGE_CHANCE: f64 = 0.03;
57        const DEFAULT_PARRY_CHANCE: f64 = 0.03;
58
59        Self {
60            dodge_chance: DEFAULT_DODGE_CHANCE,
61            parry_chance: DEFAULT_PARRY_CHANCE,
62            block_chance: 0.0,
63            critical_block_chance: 0.0,
64        }
65    }
66}
67
68/// Immutable, data-resolved definition for one encounter enemy.
69#[derive(Clone, Debug, PartialEq)]
70pub struct EnemyActorDefinition {
71    id: EnemyIdx,
72    display_name: String,
73    npc_id: Option<u32>,
74    classification: Option<i32>,
75    creature_type: Option<i32>,
76    level: u16,
77    armor: f64,
78    armor_constant: f64,
79    auto_attack_dps: Option<f64>,
80    auto_attack_damage: Option<f64>,
81    auto_attack_swing_ms: Option<u32>,
82    spell_damage: Option<f64>,
83    creature_aoe_avoidance_pct: f64,
84    attack_table: EnemyAttackTable,
85    health_model: HealthModel,
86    initial_transform: SpatialTransform,
87    group_id: GroupId,
88    enemy_tags: Vec<Box<str>>,
89    group_tags: Vec<Box<str>>,
90    role: EnemyRole,
91    spawn_at_s: f64,
92    starts_active: bool,
93}
94
95/// Values consumed when sealing an immutable enemy definition.
96#[derive(Debug)]
97pub struct EnemyActorDefinitionInput {
98    pub id: EnemyIdx,
99    pub display_name: String,
100    pub npc_id: Option<u32>,
101    pub classification: Option<i32>,
102    pub creature_type: Option<i32>,
103    pub level: u16,
104    pub armor: f64,
105    pub armor_constant: f64,
106    pub auto_attack_dps: Option<f64>,
107    pub spell_damage: Option<f64>,
108    pub creature_aoe_avoidance_pct: f64,
109    pub attack_table: EnemyAttackTable,
110    pub health_model: HealthModel,
111    pub initial_transform: SpatialTransform,
112    pub group_id: GroupId,
113    pub enemy_tags: Vec<Box<str>>,
114    pub group_tags: Vec<Box<str>>,
115    pub role: EnemyRole,
116    pub spawn_at_s: f64,
117    pub starts_active: bool,
118}
119
120impl EnemyActorDefinition {
121    const DEFAULT_AUTO_ATTACK_SWING_MS: u32 = 1_500;
122
123    #[must_use]
124    pub fn new(input: EnemyActorDefinitionInput) -> Self {
125        let auto_attack = input.auto_attack_dps.filter(|dps| *dps > 0.0);
126
127        Self {
128            id: input.id,
129            display_name: input.display_name,
130            npc_id: input.npc_id,
131            classification: input.classification,
132            creature_type: input.creature_type,
133            level: input.level,
134            armor: input.armor,
135            armor_constant: input.armor_constant,
136            auto_attack_dps: input.auto_attack_dps,
137            auto_attack_damage: auto_attack
138                .map(|dps| dps * f64::from(Self::DEFAULT_AUTO_ATTACK_SWING_MS) / MS_PER_SECOND),
139            auto_attack_swing_ms: auto_attack.map(|_| Self::DEFAULT_AUTO_ATTACK_SWING_MS),
140            spell_damage: input.spell_damage,
141            creature_aoe_avoidance_pct: input.creature_aoe_avoidance_pct,
142            attack_table: input.attack_table,
143            health_model: input.health_model,
144            initial_transform: input.initial_transform,
145            group_id: input.group_id,
146            enemy_tags: input.enemy_tags,
147            group_tags: input.group_tags,
148            role: input.role,
149            spawn_at_s: input.spawn_at_s,
150            starts_active: input.starts_active,
151        }
152    }
153
154    #[must_use]
155    pub const fn id(&self) -> EnemyIdx {
156        self.id
157    }
158    #[must_use]
159    pub fn display_name(&self) -> &str {
160        &self.display_name
161    }
162    #[must_use]
163    pub const fn npc_id(&self) -> Option<u32> {
164        self.npc_id
165    }
166    #[must_use]
167    pub const fn classification(&self) -> Option<i32> {
168        self.classification
169    }
170    #[must_use]
171    pub const fn creature_type(&self) -> Option<i32> {
172        self.creature_type
173    }
174    #[must_use]
175    pub const fn level(&self) -> u16 {
176        self.level
177    }
178    #[must_use]
179    pub const fn armor(&self) -> f64 {
180        self.armor
181    }
182    #[must_use]
183    pub const fn armor_constant(&self) -> f64 {
184        self.armor_constant
185    }
186    #[must_use]
187    pub const fn auto_attack_dps(&self) -> Option<f64> {
188        self.auto_attack_dps
189    }
190    #[must_use]
191    pub const fn auto_attack_damage(&self) -> Option<f64> {
192        self.auto_attack_damage
193    }
194    #[must_use]
195    pub const fn auto_attack_swing_ms(&self) -> Option<u32> {
196        self.auto_attack_swing_ms
197    }
198    #[must_use]
199    pub const fn spell_damage(&self) -> Option<f64> {
200        self.spell_damage
201    }
202    #[must_use]
203    pub const fn creature_aoe_avoidance_pct(&self) -> f64 {
204        self.creature_aoe_avoidance_pct
205    }
206    #[must_use]
207    pub const fn attack_table(&self) -> EnemyAttackTable {
208        self.attack_table
209    }
210    #[must_use]
211    pub const fn health_model(&self) -> HealthModel {
212        self.health_model
213    }
214    #[must_use]
215    pub const fn initial_transform(&self) -> SpatialTransform {
216        self.initial_transform
217    }
218    #[must_use]
219    pub const fn group_id(&self) -> GroupId {
220        self.group_id
221    }
222    #[must_use]
223    pub fn enemy_tags(&self) -> &[Box<str>] {
224        &self.enemy_tags
225    }
226    #[must_use]
227    pub fn group_tags(&self) -> &[Box<str>] {
228        &self.group_tags
229    }
230    #[must_use]
231    pub const fn role(&self) -> EnemyRole {
232        self.role
233    }
234    #[must_use]
235    pub const fn spawn_at_s(&self) -> f64 {
236        self.spawn_at_s
237    }
238    #[must_use]
239    pub const fn starts_active(&self) -> bool {
240        self.starts_active
241    }
242}