Skip to main content

wowlab_engine_domain/dbc/
spell_props.rs

1use wowlab_engine_gamedata::SpellProps;
2use wowlab_types::data::{SpellDataFlat, SpellResources};
3
4use super::{
5    DefenseType, SpellAttributeKind, cast_time_ms, rppm_crit_scaled, rppm_haste_scaled,
6    spell_attribute_is, spell_attribute_is_any, spell_target_masks,
7};
8
9const MELEE_CHAIN_TARGET_RANGE: f64 = 5.0;
10const RANGED_CHAIN_TARGET_RANGE: f64 = 7.5;
11const MAGIC_CHAIN_TARGET_RANGE: f64 = 10.0;
12const FULL_ANGLE_TO_HALF: f64 = 2.0;
13
14fn combine_dbc_mask_words(low: i32, high: i32) -> u64 {
15    let low = u64::from(u32::from_ne_bytes(low.to_ne_bytes()));
16    let high = u64::from(u32::from_ne_bytes(high.to_ne_bytes()));
17
18    low | (high << u32::BITS)
19}
20
21/// Builds resolved spell properties from flattened DBC data and resource metadata.
22#[must_use]
23pub fn spell_props_from(spell: &SpellDataFlat, resources: SpellResources) -> SpellProps {
24    let mut props = SpellProps::default();
25
26    populate_identity(&mut props, spell);
27    populate_resources_and_timing(&mut props, spell, resources);
28    populate_targeting_and_requirements(&mut props, spell);
29    populate_periodic_and_proc(&mut props, spell);
30
31    props
32}
33
34fn populate_identity(props: &mut SpellProps, spell: &SpellDataFlat) {
35    props.attributes.clone_from(&spell.attributes);
36    props.defense_type = spell.defense_type;
37    props.dispel_type = spell.dispel_type;
38    props.mechanic = spell.mechanic;
39    props.can_empower = spell.can_empower;
40    props.empower_stages.clone_from(&spell.empower_stages);
41    props.equipped_item_requirement = spell.equipped_item_requirement;
42    props.shapeshift_required_mask =
43        combine_dbc_mask_words(spell.shapeshift_mask_0, spell.shapeshift_mask_1);
44    props.shapeshift_excluded_mask =
45        combine_dbc_mask_words(spell.shapeshift_exclude_0, spell.shapeshift_exclude_1);
46    props.caster_aura_spell = spell.caster_aura_spell;
47    props.caster_aura_state = spell.caster_aura_state;
48    props.exclude_caster_aura_spell = spell.exclude_caster_aura_spell;
49    props.exclude_caster_aura_state = spell.exclude_caster_aura_state;
50    props.target_aura_spell = spell.target_aura_spell;
51    props.target_aura_state = spell.target_aura_state;
52    props.exclude_target_aura_spell = spell.exclude_target_aura_spell;
53    props.exclude_target_aura_state = spell.exclude_target_aura_state;
54    props.aura_interrupt_flags = u32::from_ne_bytes(spell.interrupt_aura_0.to_ne_bytes());
55    props.spell_interrupt_flags = u32::from_ne_bytes(spell.interrupt_flags.to_ne_bytes());
56    props.channel_interrupt_flags = u32::from_ne_bytes(spell.interrupt_channel_0.to_ne_bytes());
57}
58
59fn populate_resources_and_timing(
60    props: &mut SpellProps,
61    spell: &SpellDataFlat,
62    resources: SpellResources,
63) {
64    props.cost = resources.primary_cost.max(0.0);
65    props.optional_cost = resources.primary_optional_cost;
66    props.cost_pct = resources.primary_cost_pct;
67    props.max_cost_pct = resources.primary_max_cost_pct;
68    props.optional_cost_pct = resources.primary_optional_cost_pct;
69    props.gain = resources.primary_gain + (-resources.primary_cost).max(0.0);
70    props.health_cost = resources.health_cost;
71    props.health_cost_pct = resources.health_cost_pct;
72    props.health_max_cost_pct = resources.health_max_cost_pct;
73    props.health_optional_cost = resources.health_optional_cost;
74    props.health_optional_cost_pct = resources.health_optional_cost_pct;
75    props.primary_cost_entry = resources.primary_cost_entry;
76    props.secondary_cost_entry = resources.secondary_cost_entry;
77    props.secondary_cost = resources.secondary_cost.max(0.0);
78    props.secondary_optional_cost = resources.secondary_optional_cost.max(0.0);
79    props.secondary_gain = resources.secondary_gain + (-resources.secondary_cost).max(0.0);
80    props.cooldown_ms = spell.recovery_time.max(0);
81    props.cooldown_category = wowlab_types::data::CooldownCategoryId::from_raw(spell.category_id);
82    props.category_cooldown_ms = spell.category_recovery_time.max(0);
83    props.charge_category =
84        wowlab_types::data::CooldownCategoryId::from_raw(spell.charge_category_id);
85    props.cast_time_ms = cast_time_ms(spell);
86    props.duration_ms = spell.duration;
87    props.max_stacks = spell.max_stacks;
88    props.max_charges = spell.max_charges;
89    props.charge_cd_ms = spell.charge_recovery_time;
90    props.start_recovery_category =
91        wowlab_types::data::CooldownCategoryId::from_raw(spell.start_recovery_category);
92    props.gcd_ms = spell.start_recovery_time;
93}
94
95fn populate_targeting_and_requirements(props: &mut SpellProps, spell: &SpellDataFlat) {
96    let target_masks = spell_target_masks(spell);
97
98    props.projectile_speed = f64::from(spell.speed);
99    props.launch_delay_s = f64::from(spell.launch_delay);
100    props.hostile_min_range = f64::from(spell.range_min_0);
101    props.hostile_max_range = f64::from(spell.range_max_0);
102    props.radius = f64::from(spell.radius_max.max(spell.radius_min));
103    props.chain_target_range = match DefenseType::try_from(spell.defense_type).ok() {
104        Some(DefenseType::Melee) => MELEE_CHAIN_TARGET_RANGE,
105        Some(DefenseType::Ranged) => RANGED_CHAIN_TARGET_RANGE,
106        Some(DefenseType::None | DefenseType::Magic) | None => MAGIC_CHAIN_TARGET_RANGE,
107    };
108    props.max_affected_targets =
109        u8::try_from(spell.max_targets.clamp(0, i32::from(u8::MAX))).unwrap_or(u8::MAX);
110    props.cone_half_angle = f64::from(spell.cone_degrees).to_radians() / FULL_ANGLE_TO_HALF;
111    props.fixed_travel_time =
112        spell_attribute_is(&spell.attributes, SpellAttributeKind::FixedTravelTime);
113    props.ignores_line_of_sight =
114        spell_attribute_is(&spell.attributes, SpellAttributeKind::IgnoreLineOfSight);
115    props.explicit_target_mask = target_masks.explicit.bits();
116    props.required_explicit_target_mask = target_masks.required.bits();
117    props.gcd_haste_type = wowlab_types::combat::GcdHasteType::None;
118    props.hasted_ticks = spell.hasted_ticks;
119    props.duration_hasted = spell.duration_hasted;
120    props.mastery_affects_points =
121        spell_attribute_is(&spell.attributes, SpellAttributeKind::MasteryAffectsPoints);
122    props.cannot_crit = spell_attribute_is(&spell.attributes, SpellAttributeKind::CannotCrit);
123    props.requires_unshifted =
124        spell_attribute_is(&spell.attributes, SpellAttributeKind::NotShapeshifted);
125    props.allow_while_unshifted = spell_attribute_is(
126        &spell.attributes,
127        SpellAttributeKind::AllowWhileNotShapeshifted,
128    );
129    props.requires_stealth =
130        spell_attribute_is(&spell.attributes, SpellAttributeKind::RequiresStealth);
131    props.treat_as_periodic =
132        spell_attribute_is(&spell.attributes, SpellAttributeKind::TreatAsPeriodic);
133    props.treat_as_area_effect =
134        spell_attribute_is(&spell.attributes, SpellAttributeKind::TreatAsAreaEffect);
135    props.does_not_break_stealth =
136        spell_attribute_is(&spell.attributes, SpellAttributeKind::AllowWhileStealthed);
137    props.usable_while_casting = spell_attribute_is_any(
138        &spell.attributes,
139        &[
140            SpellAttributeKind::AllowCastWhileCasting,
141            SpellAttributeKind::AllowCastWhileChanneling,
142        ],
143    );
144    props.asynchronous_stacks = spell_attribute_is(
145        &spell.attributes,
146        SpellAttributeKind::AsynchronousStackingBuff,
147    );
148    props.disable_player_damage_multiplier = spell_attribute_is(
149        &spell.attributes,
150        SpellAttributeKind::DisablePlayerMultiplier,
151    );
152    props.disable_target_damage_multiplier = spell_attribute_is(
153        &spell.attributes,
154        SpellAttributeKind::DisableTargetMultiplier,
155    );
156    props.disable_positive_target_damage_multiplier = spell_attribute_is(
157        &spell.attributes,
158        SpellAttributeKind::DisableTargetPositiveMultiplier,
159    );
160    props.requires_main_hand =
161        spell_attribute_is(&spell.attributes, SpellAttributeKind::RequiresMainHand);
162    props.requires_off_hand =
163        spell_attribute_is(&spell.attributes, SpellAttributeKind::RequiresOffHand);
164}
165
166fn populate_periodic_and_proc(props: &mut SpellProps, spell: &SpellDataFlat) {
167    props.refresh_behavior = spell.refresh_behavior;
168    props.rolling_periodic = spell.rolling_periodic;
169    props.tick_may_crit = spell.tick_may_crit;
170    props.tick_on_application = spell.tick_on_application;
171    props.proc_chance_pct = spell.proc_chance;
172    props.proc_charges = spell.proc_charges.max(0);
173    props.proc_type_mask = u64::try_from(spell.proc_type_mask).unwrap_or(0);
174    props.proc_category_recovery_ms = spell.proc_category_recovery_ms.max(0);
175    props.rppm_base_rate = f64::from(spell.rppm_base_rate);
176    props.rppm_flags = spell.rppm_flags;
177    props.rppm_haste_scales = rppm_haste_scaled(&spell.rppm_mods);
178    props.rppm_crit_scales = rppm_crit_scaled(&spell.rppm_mods);
179    props.hit_chance_multiplier = 1.0;
180    props.target_resistance_multiplier = 1.0;
181    props.dispel_resistance_multiplier = 1.0;
182}