Skip to main content

wowlab_engine_combat/builder/combat_builder/build/
assemble.rs

1use 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
44/// Flags spells whose charge category a registered aura can raise at runtime (aura 411).
45fn 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    // #t(fn: rust_max_fn_lines) state assembly is one ordered lowering pass across interdependent runtime tables
155    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}