compat: C_Spell.GetSpellEffectMechanics, the per-effect sibling

`SuperCleveRoidMacros/ClassicAPI.lua:267` calls it unguarded, so its absence was a raise a
session. It is not a second spelling of `GetSpellMechanicByID`, which this tree already answers:
that one reads `Spell.dbc`'s spell-level `Mechanic`, and vanilla stores some mechanics on an
*effect* instead. The consumer's own comment names the case — Rake's bleed is effect-level, so
the spell-level column says 0 while this answers `{0, 15, 0}` — and a macro conditional asking
whether its target is bleeding gets the wrong answer from the spell-level field alone.

`SpellDisplay::effect_mechanic` was already parsed for the cast-immunity scan, so this is a field
on the probe payload and a reader, not a new lookup. The three-way answer is its sibling's, for
the sibling's reason: nothing at all for an id no spell owns, three zeros for a real spell that
carries no mechanic anywhere, the ids otherwise — collapsing those two would reclassify crowd
control as ordinary damage, which is the hand-kept table this function exists to replace.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
2026-10-16 18:45:25 -06:00
co-authored by Claude Opus 5
parent d25ef0f1d9
commit 751cac062b
5 changed files with 99 additions and 1 deletions
@@ -245,6 +245,39 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<Table> {
})?,
)?;
// GetSpellEffectMechanics(spellID) → `{m1, m2, m3}`, the per-effect `EffectMechanic[3]`
// column, where `GetSpellMechanicByID` above reads only the spell-level `Mechanic` one.
//
// The two are not the same question and the consumer says why
// (`SuperCleveRoidMacros/ClassicAPI.lua:259-263`): "vanilla stores some mechanics on an
// effect instead — Rake's bleed is effect-level, so GetSpellMechanicByID returns 0 but this
// returns {0,15,0}". A macro conditional that asks whether its target is bleeding gets the
// wrong answer from the spell-level field alone.
//
// Same three-way answer as its sibling, for the same reason: **nothing** for an id no spell
// owns, and a table of three numbers otherwise, with `0` in a slot meaning that effect
// carries no mechanic. A table of zeros and no table at all must stay distinguishable.
t.set(
"GetSpellEffectMechanics",
lua.create_function(|lua, spell_id: Option<u32>| {
let Some(spell_id) = spell_id else {
return Ok(Value::Nil);
};
let model = lua.app_data_ref::<Model>().expect("model app_data");
let Some(probe) = model.spell_mechanic_probe.as_ref() else {
return Ok(Value::Nil);
};
let Some(x) = probe(spell_id) else {
return Ok(Value::Nil);
};
let out = lua.create_table()?;
for (i, m) in x.effect_mechanics.iter().enumerate() {
out.set(i + 1, *m)?;
}
Ok(Value::Table(out))
})?,
)?;
// GetSpellPowerCost(spellID) → an **array of cost rows**, not a number: the consumer reads
// `costs[1].cost` (`SuperCleveRoidMacros/Core.lua:670-672`), so the shape is a list even
// though 1.12 has exactly one cost per spell — there is no second power type to report.
@@ -609,6 +642,63 @@ mod tests {
use crate::script::{SpellBookState, SpellSlotView, SpellTabView, UiScript};
/// `GetSpellMechanicByID`'s three-way answer, which is the consumer's own spec: nothing for an
/// `GetSpellEffectMechanics` reads the **per-effect** column, which is a different question
/// from its spell-level sibling and the reason the consumer calls both: vanilla stores some
/// mechanics on an effect instead, so a spell whose `Mechanic` is 0 can still carry one
/// (`SuperCleveRoidMacros/ClassicAPI.lua:259-263` names Rake's bleed, `{0, 15, 0}`). An id no
/// spell owns stays distinguishable from a spell whose three effects carry none.
#[test]
fn the_per_effect_mechanics_are_a_table_of_three_and_absent_for_an_unknown_spell() {
let mut s = UiScript::new().unwrap();
s.set_spell_mechanic_probe(Box::new(move |id| match id {
// Rake: nothing at the spell level, a bleed on the second effect.
1822 => Some(crate::script::SpellDbcExtras {
mechanic: 0,
mechanic_name: None,
effect_mechanics: [0, 15, 0],
mana_cost: 0,
power_type: 0,
duration_ms: 0,
icon_id: 0,
}),
// A real spell with no mechanic anywhere.
133 => Some(crate::script::SpellDbcExtras {
mechanic: 0,
mechanic_name: None,
effect_mechanics: [0; 3],
mana_cost: 0,
power_type: 0,
duration_ms: 0,
icon_id: 0,
}),
_ => None,
}));
assert_eq!(
s.eval::<Vec<u32>>("return C_Spell.GetSpellEffectMechanics(1822)")
.unwrap(),
vec![0, 15, 0],
"the effect-level bleed the spell-level column does not carry"
);
assert_eq!(
s.eval::<Vec<u32>>("return C_Spell.GetSpellEffectMechanics(133)")
.unwrap(),
vec![0, 0, 0],
"a real spell with no mechanic is three zeros, not nothing"
);
assert!(
s.eval::<Option<mlua::Table>>("return C_Spell.GetSpellEffectMechanics(999999)")
.unwrap()
.is_none(),
"an id no spell owns answers nothing at all"
);
// And the spell-level getter still disagrees with it, which is the whole point.
assert_eq!(
s.eval::<i64>("local m = C_Spell.GetSpellMechanicByID(1822) return m")
.unwrap(),
0
);
}
/// id no spell owns, `0` for a real spell with no mechanic, the id otherwise. It replaces a
/// hand-kept spellID → mechanic table, so collapsing "unknown" into "none" would quietly
/// reclassify crowd control as ordinary damage.
@@ -618,6 +708,7 @@ mod tests {
let extras = |mechanic: u32, name: Option<&str>, cost: u32| crate::script::SpellDbcExtras {
mechanic,
mechanic_name: name.map(str::to_string),
effect_mechanics: [0; 3],
mana_cost: cost,
power_type: 0,
duration_ms: 0,
@@ -545,6 +545,7 @@ mod tests {
let extras = |duration_ms: u32| crate::script::SpellDbcExtras {
mechanic: 0,
mechanic_name: None,
effect_mechanics: [0; 3],
mana_cost: 0,
power_type: 0,
duration_ms,
+5
View File
@@ -84,6 +84,11 @@ pub struct SpellDbcExtras {
/// That mechanic's enUS `SpellMechanic.dbc` name, lower-case as the data holds it; `None` for
/// mechanic 0 and for an id with no row.
pub mechanic_name: Option<String>,
/// `EffectMechanic[3]`, the per-effect column, where [`Self::mechanic`] is the spell-level
/// one. Vanilla puts some mechanics on an effect instead of the spell — Rake's bleed is the
/// consumer's own example — so a caller that reads only the spell-level field misses them.
/// `0` in a slot means that effect carries no mechanic.
pub effect_mechanics: [u32; 3],
/// `manaCost` (column 32): the flat cost, in [`Self::power_type`]'s unit.
pub mana_cost: u32,
/// `powerType`: 0 mana, 1 rage, 3 energy — the unit [`Self::mana_cost`] is counted in.