wowlab_engine_application/gear/
enchants.rs1use wowlab_engine_domain::{
4 dbc::{
5 AuraSubtypeKind, EnchantEffectKind, EnchantTriggerSemantic, SpellEffectSemanticExt,
6 enchant_effect_is, enchant_trigger_semantic, enchantment_stat_value,
7 },
8 gear::{apply_item_stat, crit_effectiveness_with_percent, gem_color_key},
9 stats::{PrimaryStats, Ratings},
10};
11use wowlab_engine_gamedata::ResolvedGameTables;
12use wowlab_engine_ports::{DataResolver, DynDataResolver, EngineError, SpellId};
13use wowlab_types::{
14 data::ItemScalingData,
15 game::{ClassId, RaceId, SpecId},
16 sim::FastSet,
17};
18
19use crate::game_data::resolve_race_passive_spells;
20
21pub(super) struct StatAccumulator<'a> {
22 pub primary: &'a mut PrimaryStats,
23 pub ratings: &'a mut Ratings,
24 pub crit_effectiveness_mult: &'a mut f64,
25 pub has_crit_effectiveness_meta: &'a mut bool,
26 pub unique_gem_colors: &'a mut FastSet<String>,
27}
28
29pub(super) struct StatScalingContext<'a> {
30 pub spec: SpecId,
31 pub level: u32,
32 pub tables: &'a ResolvedGameTables,
33 pub resolver: &'a DynDataResolver<'a>,
34 pub scaling_data: &'a ItemScalingData,
35}
36
37pub(super) async fn apply_gem_stats(
39 gem_ids: &[u32],
40 stats: &mut StatAccumulator<'_>,
41 scaling: &StatScalingContext<'_>,
42) -> Result<(), EngineError> {
43 let mut missing_properties = Vec::with_capacity(gem_ids.len());
44
45 for &gem_id in gem_ids {
47 let gem = scaling
48 .resolver
49 .get_item(wowlab_types::numeric::u32_to_i32_saturating(gem_id))
50 .await
51 .map_err(|e| {
52 EngineError::spec_construction(format!("failed to resolve gem item {gem_id}: {e}"))
53 })?;
54
55 if gem.gem_properties <= 0 {
56 continue;
57 }
58
59 if let Some(color) = gem_color_key(&gem.name) {
60 stats.unique_gem_colors.insert(color.to_string());
62 }
63
64 let Some(props) = scaling.scaling_data.gem_properties.get(&gem.gem_properties) else {
65 missing_properties.push((gem_id, gem.gem_properties));
66 continue;
67 };
68
69 apply_enchantment_stats(props.enchant_id, stats, scaling).await?;
70 }
71
72 if !missing_properties.is_empty() {
73 tracing::warn!(
74 target: "wowlab::gear",
75 ?missing_properties,
76 "gem properties rows missing; gem stats dropped"
77 );
78 }
79
80 Ok(())
81}
82
83pub(super) async fn apply_enchantment_stats(
84 enchant_id: i32,
85 stats: &mut StatAccumulator<'_>,
86 scaling: &StatScalingContext<'_>,
87) -> Result<(), EngineError> {
88 let enchant = scaling
89 .resolver
90 .get_enchantment(enchant_id)
91 .await
92 .map_err(|e| {
93 EngineError::spec_construction(format!(
94 "failed to resolve enchantment {enchant_id}: {e}"
95 ))
96 })?;
97
98 for effect in &enchant.effects {
99 if enchant_effect_is(effect.effect_type, EnchantEffectKind::Stat) {
100 if let Some(value) = enchantment_stat_value(
101 effect,
102 scaling.tables,
103 enchant.scaling_class,
104 enchant.max_level,
105 scaling.level,
106 ) {
107 apply_item_stat(
108 stats.primary,
109 stats.ratings,
110 scaling.spec,
111 effect.effect_arg,
112 value,
113 );
114 }
115 } else if enchant_effect_is(effect.effect_type, EnchantEffectKind::TriggerSpell)
116 && effect.effect_arg > 0
117 {
118 if enchant_trigger_semantic(effect.effect_arg)
119 == Some(EnchantTriggerSemantic::CritEffectivenessPerUniqueGemColor)
120 {
121 *stats.has_crit_effectiveness_meta = true;
122 continue;
123 }
124
125 let passive = scaling
126 .resolver
127 .get_spell(SpellId::new(effect.effect_arg))
128 .await?;
129
130 *stats.crit_effectiveness_mult = passive
131 .effects
132 .iter()
133 .filter(|spell_effect| {
134 spell_effect.aura_is(AuraSubtypeKind::ModCritDamageMultiplier)
135 })
136 .fold(*stats.crit_effectiveness_mult, |multiplier, aura| {
137 crit_effectiveness_with_percent(multiplier, aura.base_points)
138 });
139 }
140 }
141
142 Ok(())
143}
144
145pub(super) async fn race_crit_effectiveness_mult(
147 race: RaceId,
148 class: ClassId,
149 resolver: &DynDataResolver<'_>,
150) -> f64 {
151 let mut mult = 1.0;
152
153 for spell in resolve_race_passive_spells(resolver, Some(race), class).await {
155 for aura in spell
156 .effects
157 .iter()
158 .filter(|effect| effect.aura_is(AuraSubtypeKind::ModCritDamageMultiplier))
159 {
160 mult = crit_effectiveness_with_percent(mult, aura.base_points);
161 }
162 }
163
164 mult
165}