Spell Data
The SpellDataFlat type and its SpellEffect rows. The field groups the engine reads to resolve costs, coefficients, timing, and aura behaviour
A spell in WoW is not one row. The client splits a single ability across a dozen tables: its name, its timing, its costs, its school, and a variable number of effects each with their own coefficients. The transform layer joins all of that back together into one struct, SpellDataFlat, so the rest of the system can treat a spell as a single value. The simplest mental model: SpellDataFlat is "everything the engine could ever want to know about one spell, flattened into one record."
This is the largest and most consulted type in the data layer, so it is worth walking through its field groups rather than dumping the whole struct. Because SpellDataFlat derives serde snake_case, the exact same struct deserializes from a CSV-derived transform, a Supabase JSON row, or a JS-bridge value. That single-shape property is what lets three very different backends feed one engine; I return to it in Data Resolution.
Field groups
Rather than 80-odd fields in arbitrary order, the struct is organized into logical groups. These are the groups the engine actually reads during resolve_game_data (covered in Codegen).
A couple of fields carry contracts that are not obvious from their names, and getting them wrong silently produces wrong cooldowns:
- Cooldown is two columns. The engine must use
max(recovery_time, category_recovery_time). Picking either one alone is wrong for spells that put their cooldown on a shared category. start_recovery_timeis the GCD, not a cooldown.- A non-zero
interrupt_channel_0means the spell is a channel, which changes how its "cast time" is interpreted. For channels the duration field is the channel length, not the cast bar.
The resolution pass spells the cooldown rule out, with one extra wrinkle: a single-charge spell can hide its real cooldown in charge_recovery_time instead:
Effects
The payload of a spell lives in its effects. effects is a Vec<SpellEffect>, and each SpellEffect is one DBC effect row carrying its own type, aura sub-type, base points, and the two coefficients that drive damage:
A few of those fields carry conventions worth calling out. index is 0-based in the flat row even though the same effect is $s1 (1-based) in descriptions. effect is the effect type id and aura is the aura sub-type id. coefficient and variance are the direct-damage roll, while bonus_coefficient is the spell-power coefficient and bonus_coefficient_from_ap is the attack-power coefficient.
The coefficient naming is the one trap here. When the resolution pass builds the engine's damage view it reads bonus_coefficient as the SP coefficient and bonus_coefficient_from_ap as the AP coefficient. An effect whose direct coefficients are both zero but which redirects to a trigger spell is followed down a bounded chain to find the real payload. I cover that trigger-chain walk in Codegen.
There is a subtle index mismatch to keep in your head. The flat SpellEffect.index is 0-based, but the resolver's get_spell_effect(spell_id, effect_index) takes a 1-based index because that is the author-facing convention used in manifests and overrides. The local resolvers translate between the two via validate_effect_index, which subtracts one and treats index 0 as "not found":
Mixing the two conventions is the single easiest way to read the wrong effect.
The remaining sub-types are small: PowerCostEntry { power_type, cost, cost_pct, optional_cost } describes one resource cost, EmpowerStage { stage, duration_ms } one empower tier, and LearnSpell { learn_spell_id, overrides_spell_id } the learn/override linkage. None of them need their own page; they exist only to keep the per-spell record self-contained.
Étapes suivantes
