wowlab_engine_domain/encounter/
definition.rs1use wowlab_types::{
2 constants::MS_PER_SECOND,
3 sim::{EnemyIdx, EnemyRole, GroupId, SimTime, SpatialTransform},
4};
5
6#[derive(Clone, Copy, Debug, PartialEq)]
8pub 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#[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#[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#[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}