Skip to main content

wowlab_parsers/parsers/transform/
scaling.rs

1// #t(file: rust_inline_test_module_size) private DBC join fixtures cover the full scaling and permanent-enchant transform matrix beside the implementation
2
3const TWO_HAND_ID_OFFSET: i32 = 100_000;
4const AMMO_ID_OFFSET: i32 = 200_000;
5const ONE_HAND_CASTER_ID_OFFSET: i32 = 300_000;
6const TWO_HAND_CASTER_ID_OFFSET: i32 = 400_000;
7const EFFECT_CREATE_ITEM: i32 = 24;
8const EFFECT_ENCHANT_ITEM_PERMANENTLY: i32 = 53;
9const EFFECT_CRAFTING_DATA_ENCHANT: i32 = 301;
10const EFFECT_CRAFTING_DATA_ITEM: i32 = 288;
11const PERMANENT_ENCHANT_SKILL_LINES: [i32; 9] = [164, 165, 171, 197, 202, 333, 755, 773, 960];
12
13use wowlab_types::data::{
14    ArmorLocationFlat, CooldownSetFlat, CooldownSetSpellEntry, CurveFlat, CurvePointFlat,
15    EnchantmentFlat, ExpectedStatFlat, ExpectedStatModFlat, GemPropertiesFlat,
16    ItemArmorQualityFlat, ItemArmorShieldFlat, ItemArmorTotalFlat, ItemBonusFlat,
17    ItemDamageScalingFlat, ItemOffsetCurveFlat, ItemScalingConfigFlat, ItemSquishEraFlat,
18    PermanentEnchantFlat, PowerTypeFlat, RandPropPointsFlat, SpecializationSpellFlat,
19};
20
21#[cfg(feature = "dbc")]
22use super::super::dbc::DbcData;
23
24/// Flatten every `ItemBonus` row in the DBC dataset into `ItemBonusFlat` records.
25#[must_use]
26pub fn transform_all_item_bonuses(dbc: &DbcData) -> Vec<ItemBonusFlat> {
27    dbc.item_bonus
28        .values()
29        .flatten()
30        .map(|row| ItemBonusFlat {
31            id: row.ID,
32            value_0: row.Value_0,
33            value_1: row.Value_1,
34            value_2: row.Value_2,
35            value_3: row.Value_3,
36            parent_item_bonus_list_id: row.ParentItemBonusListID,
37            bonus_type: row.Type,
38            order_index: row.OrderIndex,
39        })
40        .collect()
41}
42
43/// Flatten every `Curve` row in the DBC dataset into `CurveFlat` records.
44#[must_use]
45pub fn transform_all_curves(dbc: &DbcData) -> Vec<CurveFlat> {
46    dbc.curve
47        .values()
48        .map(|row| CurveFlat {
49            id: row.ID,
50            curve_type: row.Type,
51            flags: row.Flags,
52        })
53        .collect()
54}
55
56/// Flatten every `CurvePoint` row in the DBC dataset into `CurvePointFlat` records.
57#[must_use]
58pub fn transform_all_curve_points(dbc: &DbcData) -> Vec<CurvePointFlat> {
59    dbc.curve_point
60        .values()
61        .flatten()
62        .map(|row| CurvePointFlat {
63            id: row.ID,
64            curve_id: row.CurveID,
65            order_index: row.OrderIndex,
66            pos_0: row.Pos_0,
67            pos_1: row.Pos_1,
68            pos_pre_squish_0: row.PosPreSquish_0,
69            pos_pre_squish_1: row.PosPreSquish_1,
70        })
71        .collect()
72}
73
74/// Flatten every `RandPropPoints` row in the DBC dataset into `RandPropPointsFlat` records.
75#[must_use]
76pub fn transform_all_rand_prop_points(dbc: &DbcData) -> Vec<RandPropPointsFlat> {
77    dbc.rand_prop_points
78        .values()
79        .map(|row| RandPropPointsFlat {
80            id: row.ID,
81            damage_replace_stat_f: row.DamageReplaceStatF,
82            damage_secondary_f: row.DamageSecondaryF,
83            damage_replace_stat: row.DamageReplaceStat,
84            damage_secondary: row.DamageSecondary,
85            epic_f_0: row.EpicF_0,
86            epic_f_1: row.EpicF_1,
87            epic_f_2: row.EpicF_2,
88            epic_f_3: row.EpicF_3,
89            epic_f_4: row.EpicF_4,
90            superior_f_0: row.SuperiorF_0,
91            superior_f_1: row.SuperiorF_1,
92            superior_f_2: row.SuperiorF_2,
93            superior_f_3: row.SuperiorF_3,
94            superior_f_4: row.SuperiorF_4,
95            good_f_0: row.GoodF_0,
96            good_f_1: row.GoodF_1,
97            good_f_2: row.GoodF_2,
98            good_f_3: row.GoodF_3,
99            good_f_4: row.GoodF_4,
100            epic_0: row.Epic_0,
101            epic_1: row.Epic_1,
102            epic_2: row.Epic_2,
103            epic_3: row.Epic_3,
104            epic_4: row.Epic_4,
105            superior_0: row.Superior_0,
106            superior_1: row.Superior_1,
107            superior_2: row.Superior_2,
108            superior_3: row.Superior_3,
109            superior_4: row.Superior_4,
110            good_0: row.Good_0,
111            good_1: row.Good_1,
112            good_2: row.Good_2,
113            good_3: row.Good_3,
114            good_4: row.Good_4,
115        })
116        .collect()
117}
118
119/// Flatten every `ItemScalingConfig` row in the DBC dataset into `ItemScalingConfigFlat` records.
120#[must_use]
121pub fn transform_all_item_scaling_configs(dbc: &DbcData) -> Vec<ItemScalingConfigFlat> {
122    dbc.item_scaling_config
123        .values()
124        .map(|row| ItemScalingConfigFlat {
125            id: row.ID,
126            item_offset_curve_id: row.ItemOffsetCurveID,
127            item_level: row.ItemLevel,
128            required_level: row.RequiredLevel,
129            item_squish_era_id: row.ItemSquishEraID,
130            flags: row.Flags,
131        })
132        .collect()
133}
134
135/// Flatten every `ItemOffsetCurve` row in the DBC dataset into `ItemOffsetCurveFlat` records.
136#[must_use]
137pub fn transform_all_item_offset_curves(dbc: &DbcData) -> Vec<ItemOffsetCurveFlat> {
138    dbc.item_offset_curve
139        .values()
140        .map(|row| ItemOffsetCurveFlat {
141            id: row.ID,
142            curve_id: row.CurveID,
143            offset: row.Offset,
144        })
145        .collect()
146}
147
148/// Flatten every `ItemSquishEra` row in the DBC dataset into `ItemSquishEraFlat` records.
149#[must_use]
150pub fn transform_all_item_squish_eras(dbc: &DbcData) -> Vec<ItemSquishEraFlat> {
151    dbc.item_squish_era
152        .values()
153        .map(|row| ItemSquishEraFlat {
154            id: row.ID,
155            patch: row.Patch,
156            curve_id: row.CurveID,
157            flags: row.Flags,
158        })
159        .collect()
160}
161
162/// Flatten every `SpellItemEnchantment` row in the DBC dataset into `EnchantmentFlat` records.
163#[must_use]
164pub fn transform_all_enchantments(dbc: &DbcData) -> Vec<EnchantmentFlat> {
165    dbc.spell_item_enchantment
166        .values()
167        .map(|row| EnchantmentFlat {
168            id: row.ID,
169            name: row.Name_lang.clone().unwrap_or_default(),
170            duration: row.Duration,
171            flags: row.Flags,
172            icon_file_data_id: row.IconFileDataID,
173            item_level_min: row.ItemLevelMin,
174            item_level_max: row.ItemLevelMax,
175            item_level: row.ItemLevel,
176            charges: row.Charges,
177            scaling_class: row.ScalingClass,
178            scaling_class_restricted: row.ScalingClassRestricted,
179            condition_id: row.Condition_ID,
180            min_level: row.MinLevel,
181            max_level: row.MaxLevel,
182            required_skill_id: row.RequiredSkillID,
183            required_skill_rank: row.RequiredSkillRank,
184            transmog_use_condition_id: row.TransmogUseConditionID,
185            transmog_cost: row.TransmogCost,
186            item_visual: row.ItemVisual,
187            effect_0: row.Effect_0,
188            effect_1: row.Effect_1,
189            effect_2: row.Effect_2,
190            effect_arg_0: row.EffectArg_0,
191            effect_arg_1: row.EffectArg_1,
192            effect_arg_2: row.EffectArg_2,
193            effect_points_min_0: row.EffectPointsMin_0,
194            effect_points_min_1: row.EffectPointsMin_1,
195            effect_points_min_2: row.EffectPointsMin_2,
196            effect_scaling_points_0: row.EffectScalingPoints_0,
197            effect_scaling_points_1: row.EffectScalingPoints_1,
198            effect_scaling_points_2: row.EffectScalingPoints_2,
199        })
200        .collect()
201}
202
203fn permanent_enchant_token(name: &str) -> String {
204    name.trim_start_matches(['_', '+'])
205        .chars()
206        .filter(|&character| {
207            character.is_ascii_alphanumeric() || matches!(character, '_' | '+' | '.' | '%' | ' ')
208        })
209        .map(|character| {
210            if character == ' ' {
211                '_'
212            } else {
213                character.to_ascii_lowercase()
214            }
215        })
216        .collect()
217}
218
219fn permanent_enchant_effect(
220    dbc: &DbcData,
221    spell_id: i32,
222    effect_type: i32,
223) -> Option<&crate::parsers::dbc::rows::SpellEffectRow> {
224    dbc.spell_effect
225        .get(&spell_id)?
226        .iter()
227        .find(|effect| effect.Effect == effect_type)
228}
229
230fn insert_permanent_enchant(
231    dbc: &DbcData,
232    entries: &mut std::collections::BTreeSet<(String, i32, i32, i32, i32, i32)>,
233    tokenized_name: &str,
234    rank: i32,
235    enchant_id: i32,
236    restriction_spell_id: i32,
237) {
238    if !dbc.spell_item_enchantment.contains_key(&enchant_id) {
239        return;
240    }
241
242    let Some(restriction) = dbc.spell_equipped_items.get(&restriction_spell_id) else {
243        return;
244    };
245
246    entries.insert((
247        tokenized_name.to_string(),
248        rank,
249        enchant_id,
250        restriction.EquippedItemClass,
251        restriction.EquippedItemInvTypes,
252        restriction.EquippedItemSubclass,
253    ));
254}
255
256fn insert_item_permanent_enchants(
257    dbc: &DbcData,
258    entries: &mut std::collections::BTreeSet<(String, i32, i32, i32, i32, i32)>,
259    tokenized_name: &str,
260    rank: i32,
261    item_id: i32,
262) {
263    let Some(item_effects) = dbc.item_x_item_effect.get(&item_id) else {
264        return;
265    };
266
267    for mapping in item_effects {
268        let Some(item_effect) = dbc.item_effect.get(&mapping.ItemEffectID) else {
269            continue;
270        };
271        let Some(effect) =
272            permanent_enchant_effect(dbc, item_effect.SpellID, EFFECT_ENCHANT_ITEM_PERMANENTLY)
273        else {
274            continue;
275        };
276
277        insert_permanent_enchant(
278            dbc,
279            entries,
280            tokenized_name,
281            rank,
282            effect.EffectMiscValue_0,
283            item_effect.SpellID,
284        );
285    }
286}
287
288/// Build the permanent-enchant index from profession spells and their recipe outputs.
289#[must_use]
290pub fn transform_all_permanent_enchants(dbc: &DbcData) -> Vec<PermanentEnchantFlat> {
291    let mut entries = std::collections::BTreeSet::new();
292
293    for ability in dbc.skill_line_ability.values().flatten() {
294        if !PERMANENT_ENCHANT_SKILL_LINES.contains(&ability.SkillLine) {
295            continue;
296        }
297
298        let Some(name) = dbc
299            .spell_name
300            .get(&ability.Spell)
301            .and_then(|spell| spell.Name_lang.as_deref())
302        else {
303            continue;
304        };
305        let tokenized_name = permanent_enchant_token(name);
306
307        if let Some(effect) =
308            permanent_enchant_effect(dbc, ability.Spell, EFFECT_ENCHANT_ITEM_PERMANENTLY)
309        {
310            insert_permanent_enchant(
311                dbc,
312                &mut entries,
313                &tokenized_name,
314                0,
315                effect.EffectMiscValue_0,
316                ability.Spell,
317            );
318        } else if let Some(effect) =
319            permanent_enchant_effect(dbc, ability.Spell, EFFECT_CREATE_ITEM)
320        {
321            insert_item_permanent_enchants(
322                dbc,
323                &mut entries,
324                &tokenized_name,
325                0,
326                effect.EffectItemType,
327            );
328        } else if let Some(effect) =
329            permanent_enchant_effect(dbc, ability.Spell, EFFECT_CRAFTING_DATA_ENCHANT)
330        {
331            if !dbc.crafting_data.contains_key(&effect.EffectMiscValue_0) {
332                continue;
333            }
334
335            for quality in dbc
336                .crafting_data_enchant_quality
337                .get(&effect.EffectMiscValue_0)
338                .into_iter()
339                .flatten()
340            {
341                let Some(rank) = dbc.crafting_quality.get(&quality.Rank) else {
342                    continue;
343                };
344
345                insert_permanent_enchant(
346                    dbc,
347                    &mut entries,
348                    &tokenized_name,
349                    rank.QualityTier,
350                    quality.SpellItemEnchantmentID,
351                    ability.Spell,
352                );
353            }
354        } else if let Some(effect) =
355            permanent_enchant_effect(dbc, ability.Spell, EFFECT_CRAFTING_DATA_ITEM)
356        {
357            if !dbc.crafting_data.contains_key(&effect.EffectMiscValue_0) {
358                continue;
359            }
360
361            for quality in dbc
362                .crafting_data_item_quality
363                .get(&effect.EffectMiscValue_0)
364                .into_iter()
365                .flatten()
366            {
367                let Some(item) = dbc.item.get(&quality.ItemID) else {
368                    continue;
369                };
370                let Some(rank) = dbc.crafting_quality.get(&item.CraftingQualityID) else {
371                    continue;
372                };
373
374                insert_item_permanent_enchants(
375                    dbc,
376                    &mut entries,
377                    &tokenized_name,
378                    rank.QualityTier,
379                    quality.ItemID,
380                );
381            }
382        }
383    }
384
385    entries
386        .into_iter()
387        .map(
388            |(tokenized_name, rank, enchant_id, item_class, inventory_type_mask, subclass_mask)| {
389                PermanentEnchantFlat {
390                    enchant_id,
391                    rank,
392                    item_class,
393                    inventory_type_mask,
394                    subclass_mask,
395                    tokenized_name,
396                }
397            },
398        )
399        .collect()
400}
401
402/// Flatten every `GemProperties` row in the DBC dataset into `GemPropertiesFlat` records.
403#[must_use]
404pub fn transform_all_gem_properties(dbc: &DbcData) -> Vec<GemPropertiesFlat> {
405    dbc.gem_properties
406        .values()
407        .map(|row| GemPropertiesFlat {
408            id: row.ID,
409            enchant_id: row.Enchant_ID,
410            gem_type: row.Type,
411        })
412        .collect()
413}
414
415/// Merge the OneHand/TwoHand/Ammo/caster damage tables, offsetting IDs by 100k per weapon type to avoid collisions.
416#[must_use]
417pub fn transform_all_item_damage_scaling(dbc: &DbcData) -> Vec<ItemDamageScalingFlat> {
418    let one_hand = dbc
419        .item_damage_one_hand
420        .values()
421        .map(|row| ItemDamageScalingFlat {
422            id: row.ID,
423            weapon_type: "one_hand".to_string(),
424            item_level: row.ItemLevel,
425            quality_0: row.Quality_0,
426            quality_1: row.Quality_1,
427            quality_2: row.Quality_2,
428            quality_3: row.Quality_3,
429            quality_4: row.Quality_4,
430            quality_5: row.Quality_5,
431            quality_6: row.Quality_6,
432        });
433
434    let two_hand = dbc
435        .item_damage_two_hand
436        .values()
437        .map(|row| ItemDamageScalingFlat {
438            id: row.ID + TWO_HAND_ID_OFFSET,
439            weapon_type: "two_hand".to_string(),
440            item_level: row.ItemLevel,
441            quality_0: row.Quality_0,
442            quality_1: row.Quality_1,
443            quality_2: row.Quality_2,
444            quality_3: row.Quality_3,
445            quality_4: row.Quality_4,
446            quality_5: row.Quality_5,
447            quality_6: row.Quality_6,
448        });
449
450    let ammo = dbc
451        .item_damage_ammo
452        .values()
453        .map(|row| ItemDamageScalingFlat {
454            id: row.ID + AMMO_ID_OFFSET,
455            weapon_type: "ammo".to_string(),
456            item_level: row.ItemLevel,
457            quality_0: row.Quality_0,
458            quality_1: row.Quality_1,
459            quality_2: row.Quality_2,
460            quality_3: row.Quality_3,
461            quality_4: row.Quality_4,
462            quality_5: row.Quality_5,
463            quality_6: row.Quality_6,
464        });
465
466    let one_hand_caster =
467        dbc.item_damage_one_hand_caster
468            .values()
469            .map(|row| ItemDamageScalingFlat {
470                id: row.ID + ONE_HAND_CASTER_ID_OFFSET,
471                weapon_type: "one_hand_caster".to_string(),
472                item_level: row.ItemLevel,
473                quality_0: row.Quality_0,
474                quality_1: row.Quality_1,
475                quality_2: row.Quality_2,
476                quality_3: row.Quality_3,
477                quality_4: row.Quality_4,
478                quality_5: row.Quality_5,
479                quality_6: row.Quality_6,
480            });
481
482    let two_hand_caster =
483        dbc.item_damage_two_hand_caster
484            .values()
485            .map(|row| ItemDamageScalingFlat {
486                id: row.ID + TWO_HAND_CASTER_ID_OFFSET,
487                weapon_type: "two_hand_caster".to_string(),
488                item_level: row.ItemLevel,
489                quality_0: row.Quality_0,
490                quality_1: row.Quality_1,
491                quality_2: row.Quality_2,
492                quality_3: row.Quality_3,
493                quality_4: row.Quality_4,
494                quality_5: row.Quality_5,
495                quality_6: row.Quality_6,
496            });
497
498    one_hand
499        .chain(two_hand)
500        .chain(ammo)
501        .chain(one_hand_caster)
502        .chain(two_hand_caster)
503        .collect()
504}
505
506/// Join `CooldownSet` with `CooldownSetSpell`.
507#[must_use]
508pub fn transform_all_cooldown_sets(dbc: &DbcData) -> Vec<CooldownSetFlat> {
509    dbc.cooldown_set
510        .values()
511        .map(|set| {
512            let spells: Vec<CooldownSetSpellEntry> = dbc
513                .cooldown_set_spell
514                .get(&set.ID)
515                .map(|spell_rows| {
516                    spell_rows
517                        .iter()
518                        .map(|s| CooldownSetSpellEntry {
519                            spell_id: s.SpellID,
520                            category: s.Category,
521                            player_condition_id: s.PlayerConditionID,
522                            order_index: s.OrderIndex,
523                            flags: s.Flags,
524                            spell_category_id: s.SpellCategoryID,
525                            equip_slot: s.EquipSlot,
526                            field_12_1_0_68209_009: s.Field_12_1_0_68209_009,
527                        })
528                        .collect()
529                })
530                .unwrap_or_default();
531
532            CooldownSetFlat {
533                id: set.ID,
534                chr_specialization: set.ChrSpecialization,
535                player_condition_id: set.PlayerConditionID,
536                spells,
537            }
538        })
539        .collect()
540}
541
542/// Flatten every `ExpectedStat` row in the DBC dataset into `ExpectedStatFlat` records.
543#[must_use]
544pub fn transform_all_expected_stats(dbc: &DbcData) -> Vec<ExpectedStatFlat> {
545    dbc.expected_stat
546        .values()
547        .map(|row| ExpectedStatFlat {
548            id: row.ID,
549            expansion_id: row.ExpansionID,
550            lvl: row.Lvl,
551            creature_health: row.CreatureHealth,
552            player_health: row.PlayerHealth,
553            creature_auto_attack_dps: row.CreatureAutoAttackDps,
554            creature_armor: row.CreatureArmor,
555            player_mana: row.PlayerMana,
556            player_primary_stat: row.PlayerPrimaryStat,
557            player_secondary_stat: row.PlayerSecondaryStat,
558            armor_constant: row.ArmorConstant,
559            creature_spell_damage: row.CreatureSpellDamage,
560            content_set_id: row.ContentSetID,
561        })
562        .collect()
563}
564
565/// Flatten every `ExpectedStatMod` row in the DBC dataset into `ExpectedStatModFlat` records.
566#[must_use]
567pub fn transform_all_expected_stat_mods(dbc: &DbcData) -> Vec<ExpectedStatModFlat> {
568    dbc.expected_stat_mod
569        .values()
570        .map(|row| ExpectedStatModFlat {
571            id: row.ID,
572            creature_health_mod: row.CreatureHealthMod,
573            player_health_mod: row.PlayerHealthMod,
574            creature_auto_attack_dps_mod: row.CreatureAutoAttackDPSMod,
575            creature_armor_mod: row.CreatureArmorMod,
576            player_mana_mod: row.PlayerManaMod,
577            player_primary_stat_mod: row.PlayerPrimaryStatMod,
578            player_secondary_stat_mod: row.PlayerSecondaryStatMod,
579            armor_constant_mod: row.ArmorConstantMod,
580            creature_spell_damage_mod: row.CreatureSpellDamageMod,
581        })
582        .collect()
583}
584
585/// Flatten every `PowerType` row in the DBC dataset into `PowerTypeFlat` records.
586#[must_use]
587pub fn transform_all_power_types(dbc: &DbcData) -> Vec<PowerTypeFlat> {
588    dbc.power_type
589        .values()
590        .map(|row| PowerTypeFlat {
591            id: row.ID,
592            name_global_string_tag: row.NameGlobalStringTag.clone().unwrap_or_default(),
593            cost_global_string_tag: row.CostGlobalStringTag.clone().unwrap_or_default(),
594            power_type_enum: row.PowerTypeEnum,
595            min_power: row.MinPower,
596            max_base_power: row.MaxBasePower,
597            center_power: row.CenterPower,
598            default_power: row.DefaultPower,
599            display_modifier: row.DisplayModifier,
600            regen_interrupt_time_ms: row.RegenInterruptTimeMS,
601            regen_peace: row.RegenPeace,
602            regen_combat: row.RegenCombat,
603            flags: row.Flags,
604        })
605        .collect()
606}
607
608/// Flatten every `ItemArmorQuality` row in the DBC dataset into `ItemArmorQualityFlat` records.
609#[must_use]
610pub fn transform_all_item_armor_quality(dbc: &DbcData) -> Vec<ItemArmorQualityFlat> {
611    dbc.item_armor_quality
612        .values()
613        .map(|row| ItemArmorQualityFlat {
614            id: row.ID,
615            qualitymod_0: row.Qualitymod_0,
616            qualitymod_1: row.Qualitymod_1,
617            qualitymod_2: row.Qualitymod_2,
618            qualitymod_3: row.Qualitymod_3,
619            qualitymod_4: row.Qualitymod_4,
620            qualitymod_5: row.Qualitymod_5,
621            qualitymod_6: row.Qualitymod_6,
622        })
623        .collect()
624}
625
626/// Flatten every `ItemArmorShield` row in the DBC dataset into `ItemArmorShieldFlat` records.
627#[must_use]
628pub fn transform_all_item_armor_shield(dbc: &DbcData) -> Vec<ItemArmorShieldFlat> {
629    dbc.item_armor_shield
630        .values()
631        .map(|row| ItemArmorShieldFlat {
632            id: row.ID,
633            item_level: row.ItemLevel,
634            quality_0: row.Quality_0,
635            quality_1: row.Quality_1,
636            quality_2: row.Quality_2,
637            quality_3: row.Quality_3,
638            quality_4: row.Quality_4,
639            quality_5: row.Quality_5,
640            quality_6: row.Quality_6,
641        })
642        .collect()
643}
644
645/// Flatten every `ItemArmorTotal` row in the DBC dataset into `ItemArmorTotalFlat` records.
646#[must_use]
647pub fn transform_all_item_armor_total(dbc: &DbcData) -> Vec<ItemArmorTotalFlat> {
648    dbc.item_armor_total
649        .values()
650        .map(|row| ItemArmorTotalFlat {
651            id: row.ID,
652            item_level: row.ItemLevel,
653            cloth: row.Cloth,
654            leather: row.Leather,
655            mail: row.Mail,
656            plate: row.Plate,
657        })
658        .collect()
659}
660
661/// Flatten every `SpecializationSpells` row in the DBC dataset into `SpecializationSpellFlat` records.
662#[must_use]
663pub fn transform_all_specialization_spells(dbc: &DbcData) -> Vec<SpecializationSpellFlat> {
664    dbc.specialization_spells
665        .values()
666        .flatten()
667        .map(|row| SpecializationSpellFlat {
668            id: row.ID,
669            spec_id: row.SpecID,
670            spell_id: row.SpellID,
671            overrides_spell_id: row.OverridesSpellID,
672        })
673        .collect()
674}
675
676/// Flatten every `ArmorLocation` row in the DBC dataset into `ArmorLocationFlat` records.
677#[must_use]
678pub fn transform_all_armor_location(dbc: &DbcData) -> Vec<ArmorLocationFlat> {
679    dbc.armor_location
680        .values()
681        .map(|row| ArmorLocationFlat {
682            id: row.ID,
683            cloth_modifier: row.Clothmodifier,
684            leather_modifier: row.Leathermodifier,
685            chain_modifier: row.Chainmodifier,
686            plate_modifier: row.Platemodifier,
687            modifier: row.Modifier,
688        })
689        .collect()
690}
691
692#[cfg(test)]
693mod tests {
694    use googletest::prelude::*;
695
696    use super::*;
697    use crate::parsers::{
698        dbc::rows::{
699            CooldownSetRow, CooldownSetSpellRow, CraftingDataEnchantQualityRow,
700            CraftingDataItemQualityRow, CraftingDataRow, CraftingQualityRow, ItemDamageAmmoRow,
701            ItemDamageOneHandCasterRow, ItemDamageOneHandRow, ItemDamageTwoHandCasterRow,
702            ItemDamageTwoHandRow, ItemEffectRow, ItemXItemEffectRow, SpellEquippedItemsRow,
703            SpellItemEnchantmentRow,
704        },
705        transform::fixtures::{
706            dbc, item_row, skill_line_ability_row, spell_effect_row, spell_name_row,
707        },
708    };
709
710    fn enchantment_row(id: i32) -> SpellItemEnchantmentRow {
711        SpellItemEnchantmentRow {
712            ID: id,
713            Name_lang: None,
714            HordeName_lang: None,
715            Duration: 0,
716            EffectArg_0: 0,
717            EffectArg_1: 0,
718            EffectArg_2: 0,
719            Flags: 0,
720            EffectScalingPoints_0: 0.0,
721            EffectScalingPoints_1: 0.0,
722            EffectScalingPoints_2: 0.0,
723            IconFileDataID: 0,
724            ItemLevelMin: 0,
725            ItemLevelMax: 0,
726            TransmogUseConditionID: 0,
727            TransmogCost: 0,
728            EffectPointsMin_0: 0.0,
729            EffectPointsMin_1: 0.0,
730            EffectPointsMin_2: 0.0,
731            ItemVisual: 0,
732            RequiredSkillID: 0,
733            RequiredSkillRank: 0,
734            ItemLevel: 0,
735            Charges: 0,
736            Effect_0: 0,
737            Effect_1: 0,
738            Effect_2: 0,
739            ScalingClass: 0,
740            ScalingClassRestricted: 0,
741            Condition_ID: 0,
742            MinLevel: 0,
743            MaxLevel: 0,
744        }
745    }
746
747    fn equipped_items(
748        spell_id: i32,
749        inventory_type_mask: i32,
750        subclass_mask: i32,
751    ) -> SpellEquippedItemsRow {
752        SpellEquippedItemsRow {
753            ID: spell_id,
754            SpellID: spell_id,
755            EquippedItemClass: 4,
756            EquippedItemInvTypes: inventory_type_mask,
757            EquippedItemSubclass: subclass_mask,
758        }
759    }
760
761    fn add_effect(dbc: &mut DbcData, spell_id: i32, kind: i32, misc: i32, item_id: i32) {
762        let mut effect = spell_effect_row(spell_id, kind, 0);
763
764        effect.EffectMiscValue_0 = misc;
765        effect.EffectItemType = item_id;
766        dbc.spell_effect.insert(spell_id, vec![effect]);
767    }
768
769    fn add_item_enchant(
770        dbc: &mut DbcData,
771        item_id: i32,
772        item_effect_id: i32,
773        spell_id: i32,
774        enchant_id: i32,
775        inventory_type_mask: i32,
776        subclass_mask: i32,
777    ) {
778        dbc.item_x_item_effect.insert(
779            item_id,
780            vec![ItemXItemEffectRow {
781                ID: item_effect_id,
782                ItemEffectID: item_effect_id,
783                ItemID: item_id,
784            }],
785        );
786        dbc.item_effect.insert(
787            item_effect_id,
788            ItemEffectRow {
789                ID: item_effect_id,
790                LegacySlotIndex: 0,
791                TriggerType: 0,
792                Charges: 0,
793                CoolDownMSec: 0,
794                CategoryCoolDownMSec: 0,
795                SpellCategoryID: 0,
796                SpellID: spell_id,
797                ChrSpecializationID: 0,
798            },
799        );
800        add_effect(
801            dbc,
802            spell_id,
803            EFFECT_ENCHANT_ITEM_PERMANENTLY,
804            enchant_id,
805            0,
806        );
807        dbc.spell_equipped_items.insert(
808            spell_id,
809            equipped_items(spell_id, inventory_type_mask, subclass_mask),
810        );
811    }
812
813    #[gtest]
814    fn permanent_enchants_follow_all_effect_paths_and_quality_joins() -> Result<()> {
815        let mut dbc = dbc();
816
817        for (spell_id, name, effect_type) in [
818            (100, "Plain Enchant", EFFECT_ENCHANT_ITEM_PERMANENTLY),
819            (200, "Created Item Enchant", EFFECT_CREATE_ITEM),
820            (
821                300,
822                "Enchant Ring - Eyes of the Eagle",
823                EFFECT_CRAFTING_DATA_ENCHANT,
824            ),
825            (400, "Forest Hunter's Armor Kit", EFFECT_CRAFTING_DATA_ITEM),
826        ] {
827            dbc.spell_name
828                .insert(spell_id, spell_name_row(spell_id, Some(name)));
829            dbc.skill_line_ability.insert(
830                spell_id,
831                vec![skill_line_ability_row(spell_id, 333, spell_id)],
832            );
833            add_effect(
834                &mut dbc,
835                spell_id,
836                effect_type,
837                spell_id + 1_000,
838                spell_id + 2_000,
839            );
840        }
841
842        dbc.spell_item_enchantment
843            .insert(1_100, enchantment_row(1_100));
844        dbc.spell_equipped_items
845            .insert(100, equipped_items(100, 1 << 16, 1 << 2));
846
847        dbc.spell_item_enchantment
848            .insert(1_200, enchantment_row(1_200));
849        add_item_enchant(&mut dbc, 2_200, 3_200, 4_200, 1_200, 1 << 13, 1 << 3);
850
851        dbc.crafting_data.insert(
852            1_300,
853            CraftingDataRow {
854                ID: 1_300,
855                Type: 0,
856                CraftingDifficultyID: 0,
857                CraftedItemID: 0,
858                ItemBonusTreeID: 0,
859            },
860        );
861        dbc.crafting_quality.insert(
862            13,
863            CraftingQualityRow {
864                ID: 13,
865                QualityTier: 2,
866                CraftingQualityAtlasSetID: 0,
867            },
868        );
869        dbc.crafting_data_enchant_quality.insert(
870            1_300,
871            vec![CraftingDataEnchantQualityRow {
872                ID: 1,
873                Rank: 13,
874                SpellItemEnchantmentID: 7_967,
875                ItemID: 0,
876                CraftingDataID: 1_300,
877            }],
878        );
879        dbc.spell_item_enchantment
880            .insert(7_967, enchantment_row(7_967));
881        dbc.spell_equipped_items
882            .insert(300, equipped_items(300, 1 << 11, 0x1f));
883
884        dbc.crafting_data.insert(
885            1_400,
886            CraftingDataRow {
887                ID: 1_400,
888                Type: 0,
889                CraftingDifficultyID: 0,
890                CraftedItemID: 0,
891                ItemBonusTreeID: 0,
892            },
893        );
894        dbc.crafting_quality.insert(
895            14,
896            CraftingQualityRow {
897                ID: 14,
898                QualityTier: 2,
899                CraftingQualityAtlasSetID: 0,
900            },
901        );
902        let mut quality_item = item_row(2_400, 8, 8, 0);
903
904        quality_item.CraftingQualityID = 14;
905        dbc.item.insert(2_400, quality_item);
906        dbc.crafting_data_item_quality.insert(
907            1_400,
908            vec![CraftingDataItemQualityRow {
909                ID: 2,
910                ItemID: 2_400,
911                CraftingDataID: 1_400,
912            }],
913        );
914        dbc.spell_item_enchantment
915            .insert(8_159, enchantment_row(8_159));
916        add_item_enchant(&mut dbc, 2_400, 3_400, 4_400, 8_159, 1 << 7, 0x1e);
917
918        let result = transform_all_permanent_enchants(&dbc);
919
920        verify_that!(
921            result,
922            unordered_elements_are![
923                eq(&PermanentEnchantFlat {
924                    enchant_id: 1_100,
925                    rank: 0,
926                    item_class: 4,
927                    inventory_type_mask: 1 << 16,
928                    subclass_mask: 1 << 2,
929                    tokenized_name: "plain_enchant".to_string(),
930                }),
931                eq(&PermanentEnchantFlat {
932                    enchant_id: 1_200,
933                    rank: 0,
934                    item_class: 4,
935                    inventory_type_mask: 1 << 13,
936                    subclass_mask: 1 << 3,
937                    tokenized_name: "created_item_enchant".to_string(),
938                }),
939                eq(&PermanentEnchantFlat {
940                    enchant_id: 7_967,
941                    rank: 2,
942                    item_class: 4,
943                    inventory_type_mask: 1 << 11,
944                    subclass_mask: 0x1f,
945                    tokenized_name: "enchant_ring__eyes_of_the_eagle".to_string(),
946                }),
947                eq(&PermanentEnchantFlat {
948                    enchant_id: 8_159,
949                    rank: 2,
950                    item_class: 4,
951                    inventory_type_mask: 1 << 7,
952                    subclass_mask: 0x1e,
953                    tokenized_name: "forest_hunters_armor_kit".to_string(),
954                }),
955            ]
956        )
957    }
958
959    #[gtest]
960    fn item_damage_scaling_merges_with_offsets() -> Result<()> {
961        let mut dbc = dbc();
962
963        dbc.item_damage_one_hand.insert(
964            10,
965            ItemDamageOneHandRow {
966                ID: 10,
967                ItemLevel: 100,
968                Quality_0: 0.0,
969                Quality_1: 0.0,
970                Quality_2: 0.0,
971                Quality_3: 0.0,
972                Quality_4: 0.0,
973                Quality_5: 0.0,
974                Quality_6: 0.0,
975            },
976        );
977        dbc.item_damage_two_hand.insert(
978            10,
979            ItemDamageTwoHandRow {
980                ID: 10,
981                ItemLevel: 100,
982                Quality_0: 0.0,
983                Quality_1: 0.0,
984                Quality_2: 0.0,
985                Quality_3: 0.0,
986                Quality_4: 0.0,
987                Quality_5: 0.0,
988                Quality_6: 0.0,
989            },
990        );
991        dbc.item_damage_ammo.insert(
992            10,
993            ItemDamageAmmoRow {
994                ID: 10,
995                ItemLevel: 100,
996                Quality_0: 0.0,
997                Quality_1: 0.0,
998                Quality_2: 0.0,
999                Quality_3: 0.0,
1000                Quality_4: 0.0,
1001                Quality_5: 0.0,
1002                Quality_6: 0.0,
1003            },
1004        );
1005        dbc.item_damage_one_hand_caster.insert(
1006            10,
1007            ItemDamageOneHandCasterRow {
1008                ID: 10,
1009                ItemLevel: 100,
1010                Quality_0: 0.0,
1011                Quality_1: 0.0,
1012                Quality_2: 0.0,
1013                Quality_3: 0.0,
1014                Quality_4: 0.0,
1015                Quality_5: 0.0,
1016                Quality_6: 0.0,
1017            },
1018        );
1019        dbc.item_damage_two_hand_caster.insert(
1020            10,
1021            ItemDamageTwoHandCasterRow {
1022                ID: 10,
1023                ItemLevel: 100,
1024                Quality_0: 0.0,
1025                Quality_1: 0.0,
1026                Quality_2: 0.0,
1027                Quality_3: 0.0,
1028                Quality_4: 0.0,
1029                Quality_5: 0.0,
1030                Quality_6: 0.0,
1031            },
1032        );
1033
1034        let result = transform_all_item_damage_scaling(&dbc);
1035
1036        verify_that!(result, len(eq(5)))?;
1037        verify_that!(
1038            result,
1039            contains(matches_pattern!(ItemDamageScalingFlat {
1040                id: eq(&10),
1041                weapon_type: eq("one_hand"),
1042                ..
1043            }))
1044        )?;
1045        verify_that!(
1046            result,
1047            contains(matches_pattern!(ItemDamageScalingFlat {
1048                id: eq(&100_010),
1049                weapon_type: eq("two_hand"),
1050                ..
1051            }))
1052        )?;
1053        verify_that!(
1054            result,
1055            contains(matches_pattern!(ItemDamageScalingFlat {
1056                id: eq(&200_010),
1057                weapon_type: eq("ammo"),
1058                ..
1059            }))
1060        )?;
1061        verify_that!(
1062            result,
1063            contains(matches_pattern!(ItemDamageScalingFlat {
1064                id: eq(&300_010),
1065                weapon_type: eq("one_hand_caster"),
1066                ..
1067            }))
1068        )?;
1069
1070        verify_that!(
1071            result,
1072            contains(matches_pattern!(ItemDamageScalingFlat {
1073                id: eq(&400_010),
1074                weapon_type: eq("two_hand_caster"),
1075                ..
1076            }))
1077        )
1078    }
1079
1080    fn cooldown_set_spell(cooldown_set_id: i32, spell_id: i32) -> CooldownSetSpellRow {
1081        CooldownSetSpellRow {
1082            ID: 0,
1083            CooldownSetID: cooldown_set_id,
1084            SpellID: spell_id,
1085            Category: 7,
1086            PlayerConditionID: 0,
1087            OrderIndex: 0,
1088            Flags: 0,
1089            SpellCategoryID: 30,
1090            EquipSlot: 13,
1091            Field_12_1_0_68209_009: 1,
1092        }
1093    }
1094
1095    #[gtest]
1096    fn cooldown_sets_join_spells() -> Result<()> {
1097        let mut dbc = dbc();
1098
1099        dbc.cooldown_set.insert(
1100            1,
1101            CooldownSetRow {
1102                ID: 1,
1103                ChrSpecialization: 63,
1104                Field: 0,
1105                PlayerConditionID: 160_935,
1106            },
1107        );
1108        dbc.cooldown_set.insert(
1109            2,
1110            CooldownSetRow {
1111                ID: 2,
1112                ChrSpecialization: 64,
1113                Field: 0,
1114                PlayerConditionID: 0,
1115            },
1116        );
1117        dbc.cooldown_set_spell.insert(
1118            1,
1119            vec![cooldown_set_spell(1, 100), cooldown_set_spell(1, 200)],
1120        );
1121
1122        let result = transform_all_cooldown_sets(&dbc);
1123
1124        verify_that!(
1125            result,
1126            contains(matches_pattern!(CooldownSetFlat {
1127                id: eq(&1),
1128                player_condition_id: eq(&160_935),
1129                spells: all!(
1130                    len(eq(2)),
1131                    contains(matches_pattern!(CooldownSetSpellEntry {
1132                        spell_id: eq(&100),
1133                        category: eq(&7),
1134                        spell_category_id: eq(&30),
1135                        equip_slot: eq(&13),
1136                        field_12_1_0_68209_009: eq(&1),
1137                        ..
1138                    }))
1139                ),
1140                ..
1141            }))
1142        )?;
1143
1144        verify_that!(
1145            result,
1146            contains(matches_pattern!(CooldownSetFlat {
1147                id: eq(&2),
1148                spells: len(eq(0)),
1149                ..
1150            }))
1151        )
1152    }
1153}