Skip to main content

wowlab_engine_combat/builder/combat_builder/build/
assemble.rs

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