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 `SimC`'s 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                        })
525                        .collect()
526                })
527                .unwrap_or_default();
528
529            CooldownSetFlat {
530                id: set.ID,
531                chr_specialization: set.ChrSpecialization,
532                spells,
533            }
534        })
535        .collect()
536}
537
538/// Flatten every `ExpectedStat` row in the DBC dataset into `ExpectedStatFlat` records.
539#[must_use]
540pub fn transform_all_expected_stats(dbc: &DbcData) -> Vec<ExpectedStatFlat> {
541    dbc.expected_stat
542        .values()
543        .map(|row| ExpectedStatFlat {
544            id: row.ID,
545            expansion_id: row.ExpansionID,
546            lvl: row.Lvl,
547            creature_health: row.CreatureHealth,
548            player_health: row.PlayerHealth,
549            creature_auto_attack_dps: row.CreatureAutoAttackDps,
550            creature_armor: row.CreatureArmor,
551            player_mana: row.PlayerMana,
552            player_primary_stat: row.PlayerPrimaryStat,
553            player_secondary_stat: row.PlayerSecondaryStat,
554            armor_constant: row.ArmorConstant,
555            creature_spell_damage: row.CreatureSpellDamage,
556            content_set_id: row.ContentSetID,
557        })
558        .collect()
559}
560
561/// Flatten every `ExpectedStatMod` row in the DBC dataset into `ExpectedStatModFlat` records.
562#[must_use]
563pub fn transform_all_expected_stat_mods(dbc: &DbcData) -> Vec<ExpectedStatModFlat> {
564    dbc.expected_stat_mod
565        .values()
566        .map(|row| ExpectedStatModFlat {
567            id: row.ID,
568            creature_health_mod: row.CreatureHealthMod,
569            player_health_mod: row.PlayerHealthMod,
570            creature_auto_attack_dps_mod: row.CreatureAutoAttackDPSMod,
571            creature_armor_mod: row.CreatureArmorMod,
572            player_mana_mod: row.PlayerManaMod,
573            player_primary_stat_mod: row.PlayerPrimaryStatMod,
574            player_secondary_stat_mod: row.PlayerSecondaryStatMod,
575            armor_constant_mod: row.ArmorConstantMod,
576            creature_spell_damage_mod: row.CreatureSpellDamageMod,
577        })
578        .collect()
579}
580
581/// Flatten every `PowerType` row in the DBC dataset into `PowerTypeFlat` records.
582#[must_use]
583pub fn transform_all_power_types(dbc: &DbcData) -> Vec<PowerTypeFlat> {
584    dbc.power_type
585        .values()
586        .map(|row| PowerTypeFlat {
587            id: row.ID,
588            name_global_string_tag: row.NameGlobalStringTag.clone().unwrap_or_default(),
589            cost_global_string_tag: row.CostGlobalStringTag.clone().unwrap_or_default(),
590            power_type_enum: row.PowerTypeEnum,
591            min_power: row.MinPower,
592            max_base_power: row.MaxBasePower,
593            center_power: row.CenterPower,
594            default_power: row.DefaultPower,
595            display_modifier: row.DisplayModifier,
596            regen_interrupt_time_ms: row.RegenInterruptTimeMS,
597            regen_peace: row.RegenPeace,
598            regen_combat: row.RegenCombat,
599            flags: row.Flags,
600        })
601        .collect()
602}
603
604/// Flatten every `ItemArmorQuality` row in the DBC dataset into `ItemArmorQualityFlat` records.
605#[must_use]
606pub fn transform_all_item_armor_quality(dbc: &DbcData) -> Vec<ItemArmorQualityFlat> {
607    dbc.item_armor_quality
608        .values()
609        .map(|row| ItemArmorQualityFlat {
610            id: row.ID,
611            qualitymod_0: row.Qualitymod_0,
612            qualitymod_1: row.Qualitymod_1,
613            qualitymod_2: row.Qualitymod_2,
614            qualitymod_3: row.Qualitymod_3,
615            qualitymod_4: row.Qualitymod_4,
616            qualitymod_5: row.Qualitymod_5,
617            qualitymod_6: row.Qualitymod_6,
618        })
619        .collect()
620}
621
622/// Flatten every `ItemArmorShield` row in the DBC dataset into `ItemArmorShieldFlat` records.
623#[must_use]
624pub fn transform_all_item_armor_shield(dbc: &DbcData) -> Vec<ItemArmorShieldFlat> {
625    dbc.item_armor_shield
626        .values()
627        .map(|row| ItemArmorShieldFlat {
628            id: row.ID,
629            item_level: row.ItemLevel,
630            quality_0: row.Quality_0,
631            quality_1: row.Quality_1,
632            quality_2: row.Quality_2,
633            quality_3: row.Quality_3,
634            quality_4: row.Quality_4,
635            quality_5: row.Quality_5,
636            quality_6: row.Quality_6,
637        })
638        .collect()
639}
640
641/// Flatten every `ItemArmorTotal` row in the DBC dataset into `ItemArmorTotalFlat` records.
642#[must_use]
643pub fn transform_all_item_armor_total(dbc: &DbcData) -> Vec<ItemArmorTotalFlat> {
644    dbc.item_armor_total
645        .values()
646        .map(|row| ItemArmorTotalFlat {
647            id: row.ID,
648            item_level: row.ItemLevel,
649            cloth: row.Cloth,
650            leather: row.Leather,
651            mail: row.Mail,
652            plate: row.Plate,
653        })
654        .collect()
655}
656
657/// Flatten every `SpecializationSpells` row in the DBC dataset into `SpecializationSpellFlat` records.
658#[must_use]
659pub fn transform_all_specialization_spells(dbc: &DbcData) -> Vec<SpecializationSpellFlat> {
660    dbc.specialization_spells
661        .values()
662        .flatten()
663        .map(|row| SpecializationSpellFlat {
664            id: row.ID,
665            spec_id: row.SpecID,
666            spell_id: row.SpellID,
667            overrides_spell_id: row.OverridesSpellID,
668        })
669        .collect()
670}
671
672/// Flatten every `ArmorLocation` row in the DBC dataset into `ArmorLocationFlat` records.
673#[must_use]
674pub fn transform_all_armor_location(dbc: &DbcData) -> Vec<ArmorLocationFlat> {
675    dbc.armor_location
676        .values()
677        .map(|row| ArmorLocationFlat {
678            id: row.ID,
679            cloth_modifier: row.Clothmodifier,
680            leather_modifier: row.Leathermodifier,
681            chain_modifier: row.Chainmodifier,
682            plate_modifier: row.Platemodifier,
683            modifier: row.Modifier,
684        })
685        .collect()
686}
687
688#[cfg(test)]
689mod tests {
690    use googletest::prelude::*;
691
692    use super::*;
693    use crate::parsers::{
694        dbc::rows::{
695            CooldownSetRow, CooldownSetSpellRow, CraftingDataEnchantQualityRow,
696            CraftingDataItemQualityRow, CraftingDataRow, CraftingQualityRow, ItemDamageAmmoRow,
697            ItemDamageOneHandCasterRow, ItemDamageOneHandRow, ItemDamageTwoHandCasterRow,
698            ItemDamageTwoHandRow, ItemEffectRow, ItemXItemEffectRow, SpellEquippedItemsRow,
699            SpellItemEnchantmentRow,
700        },
701        transform::fixtures::{
702            dbc, item_row, skill_line_ability_row, spell_effect_row, spell_name_row,
703        },
704    };
705
706    fn enchantment_row(id: i32) -> SpellItemEnchantmentRow {
707        SpellItemEnchantmentRow {
708            ID: id,
709            Name_lang: None,
710            HordeName_lang: None,
711            Duration: 0,
712            EffectArg_0: 0,
713            EffectArg_1: 0,
714            EffectArg_2: 0,
715            Flags: 0,
716            EffectScalingPoints_0: 0.0,
717            EffectScalingPoints_1: 0.0,
718            EffectScalingPoints_2: 0.0,
719            IconFileDataID: 0,
720            ItemLevelMin: 0,
721            ItemLevelMax: 0,
722            TransmogUseConditionID: 0,
723            TransmogCost: 0,
724            EffectPointsMin_0: 0.0,
725            EffectPointsMin_1: 0.0,
726            EffectPointsMin_2: 0.0,
727            ItemVisual: 0,
728            RequiredSkillID: 0,
729            RequiredSkillRank: 0,
730            ItemLevel: 0,
731            Charges: 0,
732            Effect_0: 0,
733            Effect_1: 0,
734            Effect_2: 0,
735            ScalingClass: 0,
736            ScalingClassRestricted: 0,
737            Condition_ID: 0,
738            MinLevel: 0,
739            MaxLevel: 0,
740        }
741    }
742
743    fn equipped_items(
744        spell_id: i32,
745        inventory_type_mask: i32,
746        subclass_mask: i32,
747    ) -> SpellEquippedItemsRow {
748        SpellEquippedItemsRow {
749            ID: spell_id,
750            SpellID: spell_id,
751            EquippedItemClass: 4,
752            EquippedItemInvTypes: inventory_type_mask,
753            EquippedItemSubclass: subclass_mask,
754        }
755    }
756
757    fn add_effect(dbc: &mut DbcData, spell_id: i32, kind: i32, misc: i32, item_id: i32) {
758        let mut effect = spell_effect_row(spell_id, kind, 0);
759
760        effect.EffectMiscValue_0 = misc;
761        effect.EffectItemType = item_id;
762        dbc.spell_effect.insert(spell_id, vec![effect]);
763    }
764
765    fn add_item_enchant(
766        dbc: &mut DbcData,
767        item_id: i32,
768        item_effect_id: i32,
769        spell_id: i32,
770        enchant_id: i32,
771        inventory_type_mask: i32,
772        subclass_mask: i32,
773    ) {
774        dbc.item_x_item_effect.insert(
775            item_id,
776            vec![ItemXItemEffectRow {
777                ID: item_effect_id,
778                ItemEffectID: item_effect_id,
779                ItemID: item_id,
780            }],
781        );
782        dbc.item_effect.insert(
783            item_effect_id,
784            ItemEffectRow {
785                ID: item_effect_id,
786                LegacySlotIndex: 0,
787                TriggerType: 0,
788                Charges: 0,
789                CoolDownMSec: 0,
790                CategoryCoolDownMSec: 0,
791                SpellCategoryID: 0,
792                SpellID: spell_id,
793                ChrSpecializationID: 0,
794            },
795        );
796        add_effect(
797            dbc,
798            spell_id,
799            EFFECT_ENCHANT_ITEM_PERMANENTLY,
800            enchant_id,
801            0,
802        );
803        dbc.spell_equipped_items.insert(
804            spell_id,
805            equipped_items(spell_id, inventory_type_mask, subclass_mask),
806        );
807    }
808
809    #[gtest]
810    fn permanent_enchants_follow_all_simc_effect_paths_and_quality_joins() -> Result<()> {
811        let mut dbc = dbc();
812
813        for (spell_id, name, effect_type) in [
814            (100, "Plain Enchant", EFFECT_ENCHANT_ITEM_PERMANENTLY),
815            (200, "Created Item Enchant", EFFECT_CREATE_ITEM),
816            (
817                300,
818                "Enchant Ring - Eyes of the Eagle",
819                EFFECT_CRAFTING_DATA_ENCHANT,
820            ),
821            (400, "Forest Hunter's Armor Kit", EFFECT_CRAFTING_DATA_ITEM),
822        ] {
823            dbc.spell_name
824                .insert(spell_id, spell_name_row(spell_id, Some(name)));
825            dbc.skill_line_ability.insert(
826                spell_id,
827                vec![skill_line_ability_row(spell_id, 333, spell_id)],
828            );
829            add_effect(
830                &mut dbc,
831                spell_id,
832                effect_type,
833                spell_id + 1_000,
834                spell_id + 2_000,
835            );
836        }
837
838        dbc.spell_item_enchantment
839            .insert(1_100, enchantment_row(1_100));
840        dbc.spell_equipped_items
841            .insert(100, equipped_items(100, 1 << 16, 1 << 2));
842
843        dbc.spell_item_enchantment
844            .insert(1_200, enchantment_row(1_200));
845        add_item_enchant(&mut dbc, 2_200, 3_200, 4_200, 1_200, 1 << 13, 1 << 3);
846
847        dbc.crafting_data.insert(
848            1_300,
849            CraftingDataRow {
850                ID: 1_300,
851                Type: 0,
852                CraftingDifficultyID: 0,
853                CraftedItemID: 0,
854                ItemBonusTreeID: 0,
855            },
856        );
857        dbc.crafting_quality.insert(
858            13,
859            CraftingQualityRow {
860                ID: 13,
861                QualityTier: 2,
862                CraftingQualityAtlasSetID: 0,
863            },
864        );
865        dbc.crafting_data_enchant_quality.insert(
866            1_300,
867            vec![CraftingDataEnchantQualityRow {
868                ID: 1,
869                Rank: 13,
870                SpellItemEnchantmentID: 7_967,
871                ItemID: 0,
872                CraftingDataID: 1_300,
873            }],
874        );
875        dbc.spell_item_enchantment
876            .insert(7_967, enchantment_row(7_967));
877        dbc.spell_equipped_items
878            .insert(300, equipped_items(300, 1 << 11, 0x1f));
879
880        dbc.crafting_data.insert(
881            1_400,
882            CraftingDataRow {
883                ID: 1_400,
884                Type: 0,
885                CraftingDifficultyID: 0,
886                CraftedItemID: 0,
887                ItemBonusTreeID: 0,
888            },
889        );
890        dbc.crafting_quality.insert(
891            14,
892            CraftingQualityRow {
893                ID: 14,
894                QualityTier: 2,
895                CraftingQualityAtlasSetID: 0,
896            },
897        );
898        let mut quality_item = item_row(2_400, 8, 8, 0);
899
900        quality_item.CraftingQualityID = 14;
901        dbc.item.insert(2_400, quality_item);
902        dbc.crafting_data_item_quality.insert(
903            1_400,
904            vec![CraftingDataItemQualityRow {
905                ID: 2,
906                ItemID: 2_400,
907                CraftingDataID: 1_400,
908            }],
909        );
910        dbc.spell_item_enchantment
911            .insert(8_159, enchantment_row(8_159));
912        add_item_enchant(&mut dbc, 2_400, 3_400, 4_400, 8_159, 1 << 7, 0x1e);
913
914        let result = transform_all_permanent_enchants(&dbc);
915
916        verify_that!(
917            result,
918            unordered_elements_are![
919                eq(&PermanentEnchantFlat {
920                    enchant_id: 1_100,
921                    rank: 0,
922                    item_class: 4,
923                    inventory_type_mask: 1 << 16,
924                    subclass_mask: 1 << 2,
925                    tokenized_name: "plain_enchant".to_string(),
926                }),
927                eq(&PermanentEnchantFlat {
928                    enchant_id: 1_200,
929                    rank: 0,
930                    item_class: 4,
931                    inventory_type_mask: 1 << 13,
932                    subclass_mask: 1 << 3,
933                    tokenized_name: "created_item_enchant".to_string(),
934                }),
935                eq(&PermanentEnchantFlat {
936                    enchant_id: 7_967,
937                    rank: 2,
938                    item_class: 4,
939                    inventory_type_mask: 1 << 11,
940                    subclass_mask: 0x1f,
941                    tokenized_name: "enchant_ring__eyes_of_the_eagle".to_string(),
942                }),
943                eq(&PermanentEnchantFlat {
944                    enchant_id: 8_159,
945                    rank: 2,
946                    item_class: 4,
947                    inventory_type_mask: 1 << 7,
948                    subclass_mask: 0x1e,
949                    tokenized_name: "forest_hunters_armor_kit".to_string(),
950                }),
951            ]
952        )
953    }
954
955    #[gtest]
956    fn item_damage_scaling_merges_with_offsets() -> Result<()> {
957        let mut dbc = dbc();
958
959        dbc.item_damage_one_hand.insert(
960            10,
961            ItemDamageOneHandRow {
962                ID: 10,
963                ItemLevel: 100,
964                Quality_0: 0.0,
965                Quality_1: 0.0,
966                Quality_2: 0.0,
967                Quality_3: 0.0,
968                Quality_4: 0.0,
969                Quality_5: 0.0,
970                Quality_6: 0.0,
971            },
972        );
973        dbc.item_damage_two_hand.insert(
974            10,
975            ItemDamageTwoHandRow {
976                ID: 10,
977                ItemLevel: 100,
978                Quality_0: 0.0,
979                Quality_1: 0.0,
980                Quality_2: 0.0,
981                Quality_3: 0.0,
982                Quality_4: 0.0,
983                Quality_5: 0.0,
984                Quality_6: 0.0,
985            },
986        );
987        dbc.item_damage_ammo.insert(
988            10,
989            ItemDamageAmmoRow {
990                ID: 10,
991                ItemLevel: 100,
992                Quality_0: 0.0,
993                Quality_1: 0.0,
994                Quality_2: 0.0,
995                Quality_3: 0.0,
996                Quality_4: 0.0,
997                Quality_5: 0.0,
998                Quality_6: 0.0,
999            },
1000        );
1001        dbc.item_damage_one_hand_caster.insert(
1002            10,
1003            ItemDamageOneHandCasterRow {
1004                ID: 10,
1005                ItemLevel: 100,
1006                Quality_0: 0.0,
1007                Quality_1: 0.0,
1008                Quality_2: 0.0,
1009                Quality_3: 0.0,
1010                Quality_4: 0.0,
1011                Quality_5: 0.0,
1012                Quality_6: 0.0,
1013            },
1014        );
1015        dbc.item_damage_two_hand_caster.insert(
1016            10,
1017            ItemDamageTwoHandCasterRow {
1018                ID: 10,
1019                ItemLevel: 100,
1020                Quality_0: 0.0,
1021                Quality_1: 0.0,
1022                Quality_2: 0.0,
1023                Quality_3: 0.0,
1024                Quality_4: 0.0,
1025                Quality_5: 0.0,
1026                Quality_6: 0.0,
1027            },
1028        );
1029
1030        let result = transform_all_item_damage_scaling(&dbc);
1031
1032        verify_that!(result, len(eq(5)))?;
1033        verify_that!(
1034            result,
1035            contains(matches_pattern!(ItemDamageScalingFlat {
1036                id: eq(&10),
1037                weapon_type: eq("one_hand"),
1038                ..
1039            }))
1040        )?;
1041        verify_that!(
1042            result,
1043            contains(matches_pattern!(ItemDamageScalingFlat {
1044                id: eq(&100_010),
1045                weapon_type: eq("two_hand"),
1046                ..
1047            }))
1048        )?;
1049        verify_that!(
1050            result,
1051            contains(matches_pattern!(ItemDamageScalingFlat {
1052                id: eq(&200_010),
1053                weapon_type: eq("ammo"),
1054                ..
1055            }))
1056        )?;
1057        verify_that!(
1058            result,
1059            contains(matches_pattern!(ItemDamageScalingFlat {
1060                id: eq(&300_010),
1061                weapon_type: eq("one_hand_caster"),
1062                ..
1063            }))
1064        )?;
1065
1066        verify_that!(
1067            result,
1068            contains(matches_pattern!(ItemDamageScalingFlat {
1069                id: eq(&400_010),
1070                weapon_type: eq("two_hand_caster"),
1071                ..
1072            }))
1073        )
1074    }
1075
1076    fn cooldown_set_spell(cooldown_set_id: i32, spell_id: i32) -> CooldownSetSpellRow {
1077        CooldownSetSpellRow {
1078            ID: 0,
1079            CooldownSetID: cooldown_set_id,
1080            SpellID: spell_id,
1081            Category: 7,
1082            PlayerConditionID: 0,
1083            OrderIndex: 0,
1084            Flags: 0,
1085        }
1086    }
1087
1088    #[gtest]
1089    fn cooldown_sets_join_spells() -> Result<()> {
1090        let mut dbc = dbc();
1091
1092        dbc.cooldown_set.insert(
1093            1,
1094            CooldownSetRow {
1095                ID: 1,
1096                ChrSpecialization: 63,
1097                Field: 0,
1098            },
1099        );
1100        dbc.cooldown_set.insert(
1101            2,
1102            CooldownSetRow {
1103                ID: 2,
1104                ChrSpecialization: 64,
1105                Field: 0,
1106            },
1107        );
1108        dbc.cooldown_set_spell.insert(
1109            1,
1110            vec![cooldown_set_spell(1, 100), cooldown_set_spell(1, 200)],
1111        );
1112
1113        let result = transform_all_cooldown_sets(&dbc);
1114
1115        verify_that!(
1116            result,
1117            contains(matches_pattern!(CooldownSetFlat {
1118                id: eq(&1),
1119                spells: all!(
1120                    len(eq(2)),
1121                    contains(matches_pattern!(CooldownSetSpellEntry {
1122                        spell_id: eq(&100),
1123                        category: eq(&7),
1124                        ..
1125                    }))
1126                ),
1127                ..
1128            }))
1129        )?;
1130
1131        verify_that!(
1132            result,
1133            contains(matches_pattern!(CooldownSetFlat {
1134                id: eq(&2),
1135                spells: len(eq(0)),
1136                ..
1137            }))
1138        )
1139    }
1140}