1use anyhow::bail;
4use proc_macro2::TokenStream;
5use quote::{format_ident, quote};
6use wowlab_manifest_schema::{DamageSchoolKind, ScalarField, ScalarRef, SpellScalarField};
7
8use crate::rust_source::{lit_f64, lit_int, lit_str};
9
10pub(crate) fn damage_flags(school: DamageSchoolKind) -> TokenStream {
12 match school {
13 DamageSchoolKind::Physical => quote!(wowlab_engine_combat::DamageFlags::PHYSICAL),
14 DamageSchoolKind::Magic => quote!(wowlab_engine_combat::DamageFlags::empty()),
15 }
16}
17
18pub(crate) fn spell_idx(spell_id: u32) -> TokenStream {
20 let spell_id = lit_int(spell_id);
21
22 quote!(wowlab_types::sim::SpellIdx::from_raw(#spell_id))
23}
24
25pub(crate) fn effect_lookup(data: &TokenStream, spell_id: u32, effect_index: u8) -> TokenStream {
26 let spell_id = spell_idx(spell_id);
27 let effect_index = lit_int(effect_index);
28
29 quote! {
30 wowlab_engine_domain::dbc::EffectLookup::new(
31 #data,
32 wowlab_types::sim::EffectRef::new(#spell_id, #effect_index),
33 )
34 }
35}
36
37pub(crate) fn require_get(field: &str, argument: &TokenStream) -> TokenStream {
39 let method = format_ident!("require_{field}");
40
41 quote!(data.#method(#argument)?)
42}
43
44pub(crate) fn scalar_ref_call(scalar: &ScalarRef) -> anyhow::Result<TokenStream> {
46 scalar_ref_call_with_data(scalar, "e!(data))
47}
48
49pub(crate) fn scalar_ref_call_with_data(
51 scalar: &ScalarRef,
52 data: &TokenStream,
53) -> anyhow::Result<TokenStream> {
54 let (mut value, multiplier, offset, round_decimals) = match scalar {
55 ScalarRef::Literal(value) => {
56 let value = lit_f64(*value);
57
58 return Ok(quote!(#value));
59 }
60 ScalarRef::EffectRef {
61 spell_id,
62 effect,
63 field,
64 multiplier,
65 offset,
66 round_decimals,
67 } => {
68 let spell_id = spell_idx(*spell_id);
69 let effect = lit_int(effect);
70 let method = match field {
71 ScalarField::BasePoints => format_ident!("base_points"),
72 ScalarField::ApCoef => format_ident!("ap_coef"),
73 ScalarField::SpCoef => format_ident!("sp_coef"),
74 ScalarField::Amplitude => format_ident!("amplitude"),
75 ScalarField::Period => format_ident!("period"),
76 ScalarField::Coefficient => format_ident!("coefficient"),
77 other => bail!("unknown scalar field: {other:?}"),
78 };
79
80 (
81 quote!(#data.#method(#spell_id, #effect)),
82 multiplier,
83 offset,
84 round_decimals,
85 )
86 }
87 ScalarRef::SpellRef {
88 spell_id,
89 field,
90 multiplier,
91 offset,
92 round_decimals,
93 } => {
94 let spell_id = spell_idx(*spell_id);
95 let (method, cast) = match field {
96 SpellScalarField::DurationMs => (format_ident!("require_aura_duration_ms"), true),
97 SpellScalarField::MaxStacks => (format_ident!("require_aura_max_stacks"), true),
98 SpellScalarField::Cooldown => (format_ident!("require_cooldown_s"), false),
99 SpellScalarField::Cost => (format_ident!("require_cost"), false),
100 SpellScalarField::CastTimeMs => (format_ident!("require_cast_time_ms"), true),
101 SpellScalarField::Charges => (format_ident!("require_charges"), true),
102 SpellScalarField::ChargeCd => (format_ident!("require_charge_cd_s"), false),
103 SpellScalarField::GcdMs => (format_ident!("require_gcd_ms"), true),
104 SpellScalarField::ProcCategoryRecoveryMs => {
105 (format_ident!("require_proc_category_recovery_ms"), true)
106 }
107 other => bail!("unknown spell scalar field: {other:?}"),
108 };
109 let call = quote!(#data.#method(#spell_id)?);
110
111 (
112 if cast { quote!(f64::from(#call)) } else { call },
113 multiplier,
114 offset,
115 round_decimals,
116 )
117 }
118 };
119
120 if let Some(multiplier) = multiplier {
121 let multiplier = lit_f64(*multiplier);
122
123 value = quote!((#value) * #multiplier);
124 }
125
126 if let Some(offset) = offset {
127 let offset = lit_f64(*offset);
128
129 value = quote!((#value) + #offset);
130 }
131
132 if let Some(decimals) = round_decimals {
133 let scale = lit_f64(10_f64.powi(i32::from(*decimals)));
134
135 value = quote!(((#value) * #scale).round() / #scale);
136 }
137
138 Ok(value)
139}
140
141pub(crate) fn validated_milliseconds_call(
143 value: &ScalarRef,
144 owner_id: u32,
145 field: &str,
146) -> anyhow::Result<TokenStream> {
147 validated_scalar_call("validated_milliseconds", value, owner_id, field)
148}
149
150fn validated_scalar_call(
151 validator: &str,
152 value: &ScalarRef,
153 owner_id: u32,
154 field: &str,
155) -> anyhow::Result<TokenStream> {
156 let validator = format_ident!("{validator}");
157 let value = scalar_ref_call(value)?;
158 let owner_id = lit_int(owner_id);
159 let field = lit_str(field);
160
161 Ok(quote! {
162 wowlab_engine_combat::#validator(#value, #owner_id, #field)?
163 })
164}
165
166pub(crate) fn validated_nonnegative_call(
168 value: &ScalarRef,
169 owner_id: u32,
170 field: &str,
171) -> anyhow::Result<TokenStream> {
172 validated_scalar_call("validated_nonnegative", value, owner_id, field)
173}
174
175pub(crate) fn validated_max_stacks_call(
177 value: &ScalarRef,
178 aura_id: u32,
179 field: &str,
180) -> anyhow::Result<TokenStream> {
181 validated_scalar_call("validated_max_stacks", value, aura_id, field)
182}
183
184pub(crate) fn validated_channel_tick_count_call(
186 value: &ScalarRef,
187 spell_id: u32,
188 field: &str,
189) -> anyhow::Result<TokenStream> {
190 validated_scalar_call("validated_channel_tick_count", value, spell_id, field)
191}
192
193pub(crate) fn validated_charges_call(
195 value: &ScalarRef,
196 spell_id: u32,
197 field: &str,
198) -> anyhow::Result<TokenStream> {
199 validated_scalar_call("validated_charges", value, spell_id, field)
200}
201
202#[cfg(test)]
203mod tests {
204 use googletest::prelude::*;
205
206 use super::*;
207
208 #[gtest]
209 fn scalar_transform_emits_multiplier_then_offset_then_rounding() -> Result<()> {
210 let scalar = ScalarRef::EffectRef {
211 spell_id: 42,
212 effect: 1,
213 field: ScalarField::BasePoints,
214 multiplier: Some(10.0),
215 offset: Some(0.004),
216 round_decimals: Some(2),
217 };
218
219 let expected = quote!(
220 ((((data.base_points(wowlab_types::sim::SpellIdx::from_raw(42), 1)) * 10.0) + 0.004)
221 * 100.0)
222 .round()
223 / 100.0
224 )
225 .to_string();
226
227 verify_that!(
228 scalar_ref_call(&scalar).or_fail()?.to_string(),
229 eq(&expected)
230 )
231 }
232
233 #[gtest]
234 fn spell_property_ref_emits_required_accessor_and_affine_transform() -> Result<()> {
235 let scalar = ScalarRef::SpellRef {
236 spell_id: 42,
237 field: SpellScalarField::DurationMs,
238 multiplier: Some(0.8),
239 offset: Some(1.0),
240 round_decimals: Some(0),
241 };
242
243 let emitted = scalar_ref_call(&scalar).or_fail()?.to_string();
244
245 verify_that!(
246 emitted,
247 all!(
248 contains_substring("data . require_aura_duration_ms"),
249 contains_substring("* 0.8"),
250 contains_substring("+ 1.0"),
251 contains_substring(". round ()")
252 )
253 )
254 }
255
256 #[gtest]
257 fn scalar_ref_accepts_an_explicit_game_data_receiver() -> Result<()> {
258 let scalar = ScalarRef::EffectRef {
259 spell_id: 42,
260 effect: 1,
261 field: ScalarField::BasePoints,
262 multiplier: None,
263 offset: None,
264 round_decimals: None,
265 };
266
267 verify_that!(
268 scalar_ref_call_with_data(&scalar, "e!(params.game_data))
269 .or_fail()?
270 .to_string(),
271 eq(
272 "params . game_data . base_points (wowlab_types :: sim :: SpellIdx :: from_raw (42) , 1)"
273 )
274 )
275 }
276}