wowlab_engine_combat/builder/combat_builder/build/
assemble.rs1use super::{
2 AuraSubtypeKind, BaseStats, CombatRuntime, CombatSystemBuilder, CooldownCategoryId,
3 EnemyActorDefinition, EnemyActorDefinitionInput, EnemyHealthInput, EnemyState, EngineError,
4 HealthModel, IntMap, PoolIndex, ResolvedEncounter, ResourceThresholdTrigger, ResourceType,
5 RuntimeStateParts, SpellIdx, resolve_resource_type,
6};
7use crate::{
8 builder::draft::{DraftDefinitions, DraftIndexes},
9 state::ProgramConfig,
10};
11
12pub(super) struct DraftAssemblyResult(
13 Result<(DraftDefinitions, DraftIndexes, CombatRuntime, ProgramConfig), EngineError>,
14);
15
16impl DraftAssemblyResult {
17 pub(super) fn into_result(
18 self,
19 ) -> Result<(DraftDefinitions, DraftIndexes, CombatRuntime, ProgramConfig), EngineError> {
20 self.0
21 }
22}
23
24fn category_members(
25 spells: &[crate::state::SpellData],
26 category: impl Fn(&crate::state::SpellData) -> Option<CooldownCategoryId>,
27) -> PoolIndex<CooldownCategoryId, crate::state::LocalSpellIdx> {
28 let mut members = PoolIndex::default();
29
30 for (index, spell) in spells.iter().enumerate() {
31 let Some(category) = category(spell) else {
32 continue;
33 };
34
35 members.register(
36 category,
37 crate::state::LocalSpellIdx::new(u8::try_from(index).expect("spell count fits in u8")),
38 );
39 }
40
41 members
42}
43
44fn mark_dynamic_max_charges(defs: &mut DraftDefinitions) {
46 let categories: Vec<i32> = defs
47 .auras
48 .iter()
49 .flat_map(|aura| aura.effects.iter().flatten())
50 .filter_map(|effect| match effect {
51 crate::BuffEffect::MaxChargesCategory(_, category) => Some(*category),
52 _ => None,
53 })
54 .collect();
55
56 if categories.is_empty() {
57 return;
58 }
59
60 for spell in &mut defs.spells {
61 let Some(charge_category) = spell.cooldown.charge_category else {
62 continue;
63 };
64
65 if categories.contains(&charge_category.raw()) {
66 spell.cooldown.dynamic_max_charges = true;
67 }
68 }
69}
70
71fn starts_active_at_encounter_start(
72 encounter: &ResolvedEncounter,
73 enemy: &wowlab_engine_ports::ResolvedEnemyEncounter,
74) -> bool {
75 if enemy.spawn_at_s().abs() > f64::EPSILON {
76 return false;
77 }
78
79 let definition = encounter.definition();
80 let Some(first_pull) = definition.pulls.first() else {
81 return false;
82 };
83 let Some(group) = definition
84 .groups
85 .get(enemy.group_id().as_usize())
86 .filter(|group| group.id == enemy.group_id())
87 else {
88 return false;
89 };
90 let Some(wave) = definition
91 .waves
92 .get(group.wave_id.as_usize())
93 .filter(|wave| wave.id == group.wave_id)
94 else {
95 return false;
96 };
97
98 group.enemy_ids.contains(&enemy.enemy())
99 && wave.group_ids.contains(&group.id)
100 && first_pull.wave_ids.contains(&wave.id)
101 && wave.pull_id == first_pull.id
102 && wave.minimum_activation_s.abs() <= f64::EPSILON
103 && wave.depends_on_groups.is_empty()
104}
105
106fn enemy_actor_definitions(encounter: &ResolvedEncounter) -> Vec<EnemyActorDefinition> {
107 encounter
108 .enemies()
109 .iter()
110 .map(|enemy| {
111 let health_model = match enemy.health() {
112 EnemyHealthInput::Auto => HealthModel::DamageDriven {
113 max_health: enemy.max_health(),
114 },
115 EnemyHealthInput::Fixed { max_health } => HealthModel::DamageDriven {
116 max_health: *max_health,
117 },
118 EnemyHealthInput::ScriptedLinear {
119 display_max_health,
120 death_at_s,
121 } => HealthModel::ScriptedLinear {
122 display_max_health: *display_max_health,
123 death_at_s: *death_at_s,
124 },
125 };
126
127 EnemyActorDefinition::new(EnemyActorDefinitionInput {
128 id: enemy.enemy(),
129 display_name: enemy.display_name().to_string(),
130 npc_id: enemy.npc_id(),
131 classification: enemy.classification(),
132 creature_type: enemy.creature_type(),
133 level: enemy.level(),
134 armor: enemy.creature_armor(),
135 armor_constant: enemy.armor_constant(),
136 auto_attack_dps: enemy.auto_attack_dps(),
137 spell_damage: enemy.spell_damage(),
138 creature_aoe_avoidance_pct: enemy.creature_aoe_avoidance_pct(),
139 attack_table: wowlab_engine_domain::encounter::EnemyAttackTable::default(),
140 health_model,
141 initial_transform: enemy.initial_transform(),
142 group_id: enemy.group_id(),
143 enemy_tags: enemy.enemy_tags().to_vec(),
144 group_tags: enemy.group_tags().to_vec(),
145 role: enemy.role(),
146 spawn_at_s: enemy.spawn_at_s(),
147 starts_active: starts_active_at_encounter_start(encounter, enemy),
148 })
149 })
150 .collect()
151}
152
153impl CombatSystemBuilder {
154 pub(super) fn assemble_draft(
156 &mut self,
157 parts: RuntimeStateParts,
158 selected_talent_spell_ids: &[u32],
159 encounter: ResolvedEncounter,
160 ) -> DraftAssemblyResult {
161 let primary_rt = resolve_resource_type(self.resource_type_id, &self.resource_name);
162 let secondary_rt = self
163 .secondary_resource_name
164 .as_deref()
165 .and_then(|name| resolve_resource_type(self.secondary_resource_type_id, name));
166
167 let mut resource_threshold_triggers = Vec::new();
168
169 for &talent_id in selected_talent_spell_ids {
170 let talent = SpellIdx::from_raw(talent_id);
171
172 for effect_index in 1..=self.game_data.max_effect_index(talent) {
173 if !wowlab_engine_domain::dbc::aura_subtype_is(
174 self.game_data.effect_aura(talent, effect_index),
175 AuraSubtypeKind::ResourceThresholdTrigger,
176 ) {
177 continue;
178 }
179
180 let raw_resource_type = self.game_data.effect_misc_value_0(talent, effect_index);
181 let Some(resource_type) = u8::try_from(raw_resource_type)
182 .ok()
183 .and_then(|value| ResourceType::try_from(value).ok())
184 .filter(|resource_type| {
185 Some(*resource_type) == primary_rt || Some(*resource_type) == secondary_rt
186 })
187 else {
188 continue;
189 };
190 let Some(triggered) = self.game_data.trigger_spell(talent, effect_index) else {
191 continue;
192 };
193 let Some(&spell_local) = parts.spell_by_id.get(&triggered.as_u32()) else {
194 continue;
195 };
196
197 resource_threshold_triggers.push(ResourceThresholdTrigger {
198 resource_type,
199 threshold: self.game_data.base_points(talent, effect_index),
200 spell_local,
201 });
202 }
203 }
204
205 let haste_regen = matches!(primary_rt, Some(ResourceType::Energy | ResourceType::Focus));
206
207 let impact_effect_count = parts.impact_effect_procs.len();
208 let spell_count = parts.spell_data.len();
209 let cooldown_categories =
210 category_members(&parts.spell_data, |spell| spell.cooldown.cooldown_category);
211 let charge_categories =
212 category_members(&parts.spell_data, |spell| spell.cooldown.charge_category);
213 let min_primary_cost = parts
214 .spell_data
215 .iter()
216 .map(|spell| {
217 spell.cost.resource_cost
218 + spell
219 .cost
220 .resource_cost_pct
221 .resolve(self.resource_max, self.resource_max)
222 })
223 .filter(|cost| *cost > 0.0)
224 .reduce(f64::min)
225 .unwrap_or(0.0);
226 let enemy_definitions = enemy_actor_definitions(&encounter);
227 let enemy_states = enemy_definitions
228 .iter()
229 .map(EnemyState::from_definition)
230 .collect();
231
232 let mut defs = DraftDefinitions::new(enemy_definitions);
233
234 defs.spells = parts.spell_data;
235 defs.empower_cast_times_ms = parts.empower_cast_times_ms;
236 defs.spell_effects = parts.spell_effects;
237 defs.auras = parts.aura_data;
238 defs.aura_bindings = std::mem::take(&mut self.aura_bindings);
239 defs.spell_bindings = std::mem::take(&mut self.spell_bindings);
240 mark_dynamic_max_charges(&mut defs);
241 defs.auto_attacks = parts.auto_attack_data;
242 defs.cast_hooks = std::mem::take(&mut self.cast_hooks);
243 defs.empower_release_hooks = std::mem::take(&mut self.empower_release_hooks);
244 defs.tick_hooks = std::mem::take(&mut self.tick_hooks);
245 defs.player_cast_hooks = std::mem::take(&mut self.player_cast_hooks);
246 defs.resource_threshold_triggers = resource_threshold_triggers;
247 defs.impact_procs = std::mem::take(&mut self.impact_procs);
248 defs.landed_impact_procs = std::mem::take(&mut self.landed_impact_procs);
249 defs.accumulating_impact_procs = std::mem::take(&mut self.accumulating_impact_procs);
250 defs.impact_effect_procs = parts.impact_effect_procs;
251 defs.talent_ranks = std::mem::take(&mut self.selected_talent_ranks);
252 defs.resource_gain_procs = std::mem::take(&mut self.resource_gain_procs);
253 defs.item_use_spells = std::mem::take(&mut self.item_use_spells);
254 defs.rppm_trackers = std::mem::take(&mut self.rppm_trackers);
255 defs.rppm_bindings = std::mem::take(&mut self.rppm_bindings);
256 defs.threshold_trackers = std::mem::take(&mut self.threshold_trackers);
257 defs.precombat_auras = std::mem::take(&mut self.precombat_auras);
258 defs.talent_aura_stacks = std::mem::take(&mut self.talent_aura_stacks);
259 defs.stealth_aura_id = self.stealth_aura_id;
260
261 let preferred_target = encounter
262 .definition()
263 .pulls
264 .first()
265 .expect("validated encounter has at least one pull")
266 .preferred_target;
267 let orchestration = crate::state::EncounterRuntime::new(encounter.definition());
268 let player_max_health = self.stats.stamina.floor()
269 * self
270 .game_data
271 .health_per_stamina(self.game_data.level())
272 .unwrap_or(0.0);
273 let mut runtime = CombatRuntime::new(
274 enemy_states,
275 crate::state::CombatRuntimeInit {
276 encounter: orchestration,
277 preferred_target,
278 player_transform: encounter.definition().initial_player_transform,
279 player_max_health,
280 has_pet: self.has_pet,
281 },
282 );
283
284 runtime.procs.impact_effect_ready_at = vec![0.0; impact_effect_count];
285 runtime
286 .procs
287 .resize_target_icds_for_known_targets(impact_effect_count);
288 runtime.procs.impact_effect_charges = defs
289 .impact_effect_procs
290 .iter()
291 .map(|proc| {
292 if proc.uses_stacks_for_charges {
293 0
294 } else {
295 proc.charges
296 }
297 })
298 .collect();
299 runtime.procs.impact_effect_rng = defs
300 .impact_effect_procs
301 .iter()
302 .map(crate::state::ImpactEffectRngState::from_proc)
303 .collect();
304 runtime.recharge_rate_mults = vec![1.0; spell_count];
305 #[cfg(feature = "rotation-optimizations")]
306 {
307 runtime.cooldown_duration_mults = vec![1.0; spell_count];
308 runtime.initial_recharge_rate_mults = vec![1.0; spell_count];
309 runtime.initial_cooldown_duration_mults = vec![1.0; spell_count];
310 }
311 runtime.pools.cooldown_ready_at.resize(spell_count);
312 runtime.pools.start_recovery_ready_at.resize(spell_count);
313 runtime.pools.cooldown_category_ready_at.resize(spell_count);
314 runtime.pools.charge_category_ready_at.resize(spell_count);
315
316 DraftAssemblyResult(Ok((
317 defs,
318 DraftIndexes {
319 spell_by_id: parts.spell_by_id,
320 aura_by_id: parts.aura_by_id,
321 aura_by_identity: parts.aura_by_identity,
322 cooldown_categories,
323 charge_categories,
324 item_rppm_indices: std::mem::take(&mut self.item_rppm_indices),
325 system_rppm_indices: IntMap::default(),
326 enchant_rppm_indices: IntMap::default(),
327 enchant_buff_spell_ids: IntMap::default(),
328 },
329 runtime,
330 ProgramConfig {
331 base_stats: BaseStats {
332 stats: std::mem::take(&mut self.stats),
333 primary_attribute: self
334 .spec_id
335 .map(wowlab_engine_domain::stats::primary_stat_for_spec),
336 base_regen: self.resource_regen,
337 resource_max: self.resource_max,
338 resource_start: self.resource_start,
339 resource_name: std::mem::take(&mut self.resource_name),
340 has_pet: self.has_pet,
341 secondary_resource_name: std::mem::take(&mut self.secondary_resource_name),
342 secondary_resource_max: self.secondary_resource_max,
343 resource_type: primary_rt,
344 secondary_resource_type: secondary_rt,
345 swing_resource_gain_mult: 1.0,
346 },
347 min_primary_cost,
348 haste_regen,
349 game_data: std::mem::take(&mut self.game_data),
350 encounter,
351 mastery_hook: self.mastery_hook,
352 mastery_crit_damage: self.mastery_crit_damage,
353 mastery_spell: self.mastery_spell,
354 spec_config: Box::new(()),
355 bugs: wowlab_engine_ports::BugToggles::default(),
356 cast_latency: wowlab_engine_ports::CastLatency {
357 queue_ms: 0,
358 gcd_ms: 0,
359 channel_ms: 0,
360 queue_window_ms: 0,
361 strict_gcd_queue: true,
362 },
363 raid_events: Vec::new(),
364 external_buffs: Vec::new(),
365 },
366 )))
367 }
368}