Skip to main content

wowlab_types/types/data/
loot.rs

1use serde::{Deserialize, Serialize};
2use wowlab_engine_macros::CopyInsert;
3
4#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
5#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
6#[serde(rename_all = "lowercase")]
7#[non_exhaustive]
8pub enum InstanceKind {
9    Raid,
10    Dungeon,
11    #[default]
12    World,
13}
14
15impl InstanceKind {
16    #[must_use]
17    pub const fn as_str(&self) -> &'static str {
18        match self {
19            Self::Raid => "raid",
20            Self::Dungeon => "dungeon",
21            Self::World => "world",
22        }
23    }
24}
25
26/// Difficulty kinds derived from `Difficulty.InstanceType`.
27#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
28#[serde(rename_all = "lowercase")]
29#[non_exhaustive]
30pub enum DifficultyKind {
31    Raid,
32    Dungeon,
33    Pvp,
34    World,
35    Scenario,
36    #[default]
37    Unknown,
38}
39
40impl DifficultyKind {
41    #[must_use]
42    pub const fn as_str(&self) -> &'static str {
43        match self {
44            Self::Raid => "raid",
45            Self::Dungeon => "dungeon",
46            Self::Pvp => "pvp",
47            Self::World => "world",
48            Self::Scenario => "scenario",
49            Self::Unknown => "unknown",
50        }
51    }
52}
53
54#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
55#[serde(rename_all = "lowercase")]
56#[non_exhaustive]
57pub enum DropSourceKind {
58    #[default]
59    Raid,
60    Dungeon,
61}
62
63impl DropSourceKind {
64    #[must_use]
65    pub const fn as_str(&self) -> &'static str {
66        match self {
67            Self::Raid => "raid",
68            Self::Dungeon => "dungeon",
69        }
70    }
71}
72
73/// Per-difficulty key; wire form is exact addon `PascalCase` strings.
74#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
75#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
76#[non_exhaustive]
77pub enum DifficultyKey {
78    #[serde(rename = "LFR")]
79    Lfr,
80    #[default]
81    Normal,
82    Heroic,
83    Mythic,
84    #[serde(rename = "MythicPlus_2_3")]
85    MythicPlusTwoThree,
86    #[serde(rename = "MythicPlus_4")]
87    MythicPlusFour,
88    #[serde(rename = "MythicPlus_5")]
89    MythicPlusFive,
90    #[serde(rename = "MythicPlus_6_7")]
91    MythicPlusSixSeven,
92    #[serde(rename = "MythicPlus_8_9")]
93    MythicPlusEightNine,
94    #[serde(rename = "MythicPlus_10Plus")]
95    MythicPlusTenPlus,
96    #[serde(rename = "MythicPlus_VaultBR")]
97    MythicPlusVaultBr,
98}
99
100impl DifficultyKey {
101    #[must_use]
102    pub const fn as_str(&self) -> &'static str {
103        match self {
104            Self::Lfr => "LFR",
105            Self::Normal => "Normal",
106            Self::Heroic => "Heroic",
107            Self::Mythic => "Mythic",
108            Self::MythicPlusTwoThree => "MythicPlus_2_3",
109            Self::MythicPlusFour => "MythicPlus_4",
110            Self::MythicPlusFive => "MythicPlus_5",
111            Self::MythicPlusSixSeven => "MythicPlus_6_7",
112            Self::MythicPlusEightNine => "MythicPlus_8_9",
113            Self::MythicPlusTenPlus => "MythicPlus_10Plus",
114            Self::MythicPlusVaultBr => "MythicPlus_VaultBR",
115        }
116    }
117
118    // #t(fn: rust_magic_numbers) positional sort ordinals; naming each adds no clarity
119    #[must_use]
120    pub const fn order(self) -> u8 {
121        match self {
122            Self::Lfr => 0,
123            Self::Normal => 1,
124            Self::Heroic => 2,
125            Self::Mythic => 3,
126            Self::MythicPlusTwoThree => 4,
127            Self::MythicPlusFour => 5,
128            Self::MythicPlusFive => 6,
129            Self::MythicPlusSixSeven => 7,
130            Self::MythicPlusEightNine => 8,
131            Self::MythicPlusTenPlus => 9,
132            Self::MythicPlusVaultBr => 10,
133        }
134    }
135}
136
137#[derive(Clone, Debug, Default, Deserialize, Serialize)]
138#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
139pub struct TierInstanceEntry {
140    pub instance_id: i32,
141    pub instance_name: String,
142    pub kind: InstanceKind,
143    pub order_index: i32,
144    pub map_id: i32,
145    pub is_mythic_plus: bool,
146}
147
148#[derive(Clone, CopyInsert, Debug, Default, Deserialize, Serialize)]
149pub struct JournalTierFlat {
150    pub id: i32,
151    pub name: String,
152    pub expansion: i32,
153    pub player_condition_id: i32,
154    #[copy(json)]
155    pub instances: Vec<TierInstanceEntry>,
156}
157
158#[derive(Clone, Debug, Default, Deserialize, Serialize)]
159#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
160pub struct EncounterEntry {
161    pub id: i32,
162    pub name: String,
163    pub order_index: i32,
164    pub dungeon_encounter_id: i32,
165    pub ui_map_id: i32,
166    pub image_file_data_id: i32,
167    pub image_storage_path: String,
168    pub creature_display_info_id: i32,
169    pub ui_model_scene_id: i32,
170}
171
172#[derive(Clone, CopyInsert, Debug, Default, Deserialize, Serialize)]
173pub struct JournalInstanceFlat {
174    pub id: i32,
175    pub name: String,
176    pub description: String,
177    pub map_id: i32,
178    pub background_file_data_id: i32,
179    pub background_storage_path: String,
180    pub button_file_data_id: i32,
181    pub button_storage_path: String,
182    pub button_small_file_data_id: i32,
183    pub lore_file_data_id: i32,
184    pub lore_storage_path: String,
185    pub loadscreen_file_data_id: i32,
186    pub loadscreen_storage_path: String,
187    pub loadscreen_seo_storage_path: String,
188    #[copy(enum_str)]
189    pub kind: InstanceKind,
190    pub is_mythic_plus: bool,
191    pub is_current_season: bool,
192    pub tier_id: i32,
193    pub tier_name: String,
194    pub tier_order_index: i32,
195    #[copy(json)]
196    pub encounters: Vec<EncounterEntry>,
197}
198
199#[derive(Clone, CopyInsert, Debug, Default, Deserialize, Serialize)]
200#[copy_insert(patch_first)]
201pub struct CraftedProductFlat {
202    /// Bigserial PK; emit 0, Postgres assigns.
203    #[copy(skip)]
204    pub id: i64,
205    pub crafting_data_id: i32,
206    pub crafted_item_id: i32,
207    pub crafted_item_name: String,
208    pub quality_tier: i32,
209    pub quality_item_id: i32,
210    pub quality_item_name: String,
211    pub quality_item_level: i32,
212    pub profession_id: i32,
213    pub profession_name: String,
214    pub profession_enum_value: i32,
215    pub item_bonus_tree_id: i32,
216}
217
218#[derive(Clone, Debug, Default, Deserialize, Serialize)]
219#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
220pub struct PvpTierRewardEntry {
221    pub tier_index: i32,
222    pub tier_name: String,
223    pub min_rating: i32,
224    pub max_rating: i32,
225    pub bracket_id: i32,
226    pub end_of_match_ilvl: i32,
227    pub weekly_cache_ilvl: i32,
228}
229
230/// The single current `PvpSeason` row with embedded tier rewards.
231#[derive(Clone, CopyInsert, Debug, Default, Deserialize, Serialize)]
232pub struct PvpSeasonFlat {
233    pub id: i32,
234    pub milestone_season: i32,
235    pub alliance_achievement_id: i32,
236    pub horde_achievement_id: i32,
237    #[copy(json)]
238    pub tier_rewards: Vec<PvpTierRewardEntry>,
239}
240
241#[derive(Clone, Debug, Default, Deserialize, Serialize)]
242#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
243pub struct KeyLevelRewardEntry {
244    pub key_level: i32,
245    pub activity_tier_id: i32,
246    pub weekly_reward_level: i32,
247    pub end_of_run_reward_level: i32,
248}
249
250#[derive(Clone, Debug, Default, Deserialize, Serialize)]
251#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
252pub struct TrackedDungeonEntry {
253    pub map_challenge_mode_id: i32,
254    pub map_id: i32,
255    pub instance_name: String,
256}
257
258#[derive(Clone, Debug, Default, Deserialize, Serialize)]
259#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
260pub struct TrackedAffixEntry {
261    pub keystone_affix_id: i32,
262    pub bonus_rating: i32,
263}
264
265#[derive(Clone, Debug, Default, Deserialize, Serialize)]
266#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
267pub struct KeyFloorEntry {
268    pub key_floor: i32,
269    pub player_condition_id: i32,
270}
271
272/// The current synthesized M+ season with embedded rewards and tracked data.
273#[derive(Clone, Debug, Default, Deserialize, Serialize)]
274pub struct MythicPlusSeasonFlat {
275    pub id: i32,
276    pub milestone_season: i32,
277    pub start_time_event: i32,
278    pub expansion_level: i32,
279    pub heroic_lfg_dungeon_min_gear: i32,
280    pub display_season_id: i32,
281    pub display_season_name: String,
282    pub display_season_index: i32,
283    pub delves_season_id: i32,
284    pub item_group_ilvl_scaling_id: i32,
285    pub bonus_list_groups: Vec<i32>,
286    pub crest_currencies: Vec<i32>,
287    pub valorstones_currency_id: i32,
288    pub tracked_dungeons: Vec<TrackedDungeonEntry>,
289    pub tracked_affixes: Vec<TrackedAffixEntry>,
290    pub key_floors: Vec<KeyFloorEntry>,
291    pub key_rewards: Vec<KeyLevelRewardEntry>,
292}
293
294/// 1:1 plain port of `CurrencyTypes` joined with `CurrencyCategory.Name_lang`.
295#[derive(Clone, CopyInsert, Debug, Default, Deserialize, Serialize)]
296pub struct Currency {
297    pub id: i32,
298    pub name: String,
299    pub category_id: i32,
300    pub category_name: String,
301    pub icon_file_id: i32,
302    pub max_qty: i32,
303    pub max_earnable_per_week: i32,
304    pub quality: i32,
305    pub order_index: i32,
306    pub flags: i32,
307}
308
309#[derive(Clone, CopyInsert, Debug, Default, Deserialize, Serialize)]
310pub struct CurrencyCategory {
311    pub id: i32,
312    pub name: String,
313    pub flags: i32,
314    pub expansion_id: i32,
315    pub parent_category_id: i32,
316}
317
318#[derive(Clone, CopyInsert, Debug, Default, Deserialize, Serialize)]
319pub struct ItemBonusListGroup {
320    pub id: i32,
321    pub sequence_spell_id: i32,
322    pub player_condition_id: i32,
323    pub item_extended_cost_id: i32,
324    pub item_logical_cost_group_id: i32,
325    pub item_group_ilvl_scaling_id: i32,
326}
327
328#[derive(Clone, CopyInsert, Debug, Default, Deserialize, Serialize)]
329pub struct ItemGroupIlvlScalingEntry {
330    pub id: i32,
331    pub currency_type_id: i32,
332    pub item_id: i32,
333    pub cost_scaling: f32,
334    pub flags: i32,
335    pub player_condition_id: i32,
336    pub conditional_cost_scaling: f32,
337    pub item_group_ilvl_scaling_id: i32,
338}
339
340#[derive(Clone, CopyInsert, Debug, Default, Deserialize, Serialize)]
341pub struct ItemLevelSelector {
342    pub id: i32,
343    pub min_item_level: i32,
344    pub item_level_selector_quality_set_id: i32,
345    pub azerite_unlock_mapping_set_id: i32,
346}
347
348#[derive(Clone, CopyInsert, Debug, Default, Deserialize, Serialize)]
349pub struct ItemLevelSelectorQualitySet {
350    pub id: i32,
351    pub ilvl_rare: i32,
352    pub ilvl_epic: i32,
353}
354
355#[derive(Clone, CopyInsert, Debug, Default, Deserialize, Serialize)]
356pub struct ItemLevelSelectorQuality {
357    pub id: i32,
358    pub quality_item_bonus_list_id: i32,
359    pub quality: i32,
360    pub parent_ils_quality_set_id: i32,
361}
362
363#[derive(Clone, CopyInsert, Debug, Default, Deserialize, Serialize)]
364pub struct ItemConversionFlat {
365    pub id: i32,
366    pub item_bonus_tree_id: i32,
367    pub item_logical_cost_group_id: i32,
368    pub alternate_item_logical_cost_group_id: i32,
369    pub player_condition_id: i32,
370    pub items: Vec<i32>,
371}
372
373#[derive(Clone, CopyInsert, Debug, Default, Deserialize, Serialize)]
374pub struct ChallengeModeItemBonusOverride {
375    pub id: i32,
376    pub item_bonus_tree_group_id: i32,
377    pub dst_item_bonus_tree_id: i32,
378    pub value: i32,
379    pub required_time_event_passed: i32,
380    pub required_time_event_not_passed: i32,
381    pub src_item_bonus_tree_id: i32,
382}
383
384#[derive(Clone, CopyInsert, Debug, Default, Deserialize, Serialize)]
385pub struct ItemBonusSequenceSpell {
386    pub id: i32,
387    pub spell_id: i32,
388    pub item_id: i32,
389}
390
391#[derive(Clone, CopyInsert, Debug, Default, Deserialize, Serialize)]
392pub struct DelvesSeason {
393    pub id: i32,
394    pub faction_id: i32,
395}
396
397/// Per-(item, `source_kind`, difficulty) resolved `BonusID` record.
398#[derive(Clone, CopyInsert, Debug, Default, Deserialize, Serialize)]
399#[copy_insert(patch_first)]
400pub struct ItemDropScalingFlat {
401    /// Bigserial PK; emit 0, Postgres assigns.
402    #[copy(skip)]
403    pub id: i64,
404    pub item_id: i32,
405    #[copy(enum_str)]
406    pub source_kind: DropSourceKind,
407    #[copy(enum_str)]
408    pub difficulty_key: DifficultyKey,
409    /// `Type=49` upgrade bonus list ID; `0` = none.
410    pub upgrade_id: i32,
411    /// `Type=4` flair bonus list ID; `0` = none, never set without `upgrade_id`.
412    pub flair_id: i32,
413}
414
415#[derive(Clone, CopyInsert, Debug, Default, Deserialize, Serialize)]
416pub struct PvpTier {
417    pub id: i32,
418    pub name: String,
419    pub min_rating: i32,
420    pub max_rating: i32,
421    pub prev_tier: i32,
422    pub next_tier: i32,
423    pub bracket_id: i32,
424    pub rank: i32,
425    pub rank_icon: i32,
426}
427
428/// 1:1 port of `Difficulty` plus a derived `kind` discriminator.
429#[derive(Clone, CopyInsert, Debug, Default, Deserialize, Serialize)]
430pub struct Difficulty {
431    pub id: i32,
432    pub name: String,
433    pub instance_type: i32,
434    pub order_index: i32,
435    pub min_players: i32,
436    pub max_players: i32,
437    pub flags: i32,
438    pub item_context: i32,
439    #[copy(enum_str)]
440    pub kind: DifficultyKind,
441}
442
443/// 1:1 port of `Profession` with name resolved from `ProfessionEnumValue`.
444#[derive(Clone, CopyInsert, Debug, Default, Deserialize, Serialize)]
445pub struct Profession {
446    pub id: i32,
447    pub skill_line_id: i32,
448    pub profession_enum_value: i32,
449    pub name: String,
450}
451
452#[cfg(test)]
453mod tests {
454    use googletest::prelude::*;
455    use rstest::rstest;
456
457    use super::*;
458
459    #[gtest]
460    #[rstest]
461    #[case::raid(InstanceKind::Raid, "raid")]
462    #[case::dungeon(InstanceKind::Dungeon, "dungeon")]
463    #[case::world(InstanceKind::World, "world")]
464    fn instance_kind_as_str(#[case] kind: InstanceKind, #[case] expected: &str) -> Result<()> {
465        verify_that!(kind.as_str(), eq(expected))
466    }
467
468    #[gtest]
469    #[rstest]
470    #[case::raid(DifficultyKind::Raid, "raid")]
471    #[case::dungeon(DifficultyKind::Dungeon, "dungeon")]
472    #[case::pvp(DifficultyKind::Pvp, "pvp")]
473    #[case::world(DifficultyKind::World, "world")]
474    #[case::scenario(DifficultyKind::Scenario, "scenario")]
475    #[case::unknown(DifficultyKind::Unknown, "unknown")]
476    fn difficulty_kind_as_str(#[case] kind: DifficultyKind, #[case] expected: &str) -> Result<()> {
477        verify_that!(kind.as_str(), eq(expected))
478    }
479
480    #[gtest]
481    #[rstest]
482    #[case::raid(DropSourceKind::Raid, "raid")]
483    #[case::dungeon(DropSourceKind::Dungeon, "dungeon")]
484    fn drop_source_kind_as_str(#[case] kind: DropSourceKind, #[case] expected: &str) -> Result<()> {
485        verify_that!(kind.as_str(), eq(expected))
486    }
487
488    #[gtest]
489    #[rstest]
490    #[case::lfr(DifficultyKey::Lfr, "LFR")]
491    #[case::normal(DifficultyKey::Normal, "Normal")]
492    #[case::heroic(DifficultyKey::Heroic, "Heroic")]
493    #[case::mythic(DifficultyKey::Mythic, "Mythic")]
494    #[case::mplus_2_3(DifficultyKey::MythicPlusTwoThree, "MythicPlus_2_3")]
495    #[case::mplus_4(DifficultyKey::MythicPlusFour, "MythicPlus_4")]
496    #[case::mplus_5(DifficultyKey::MythicPlusFive, "MythicPlus_5")]
497    #[case::mplus_6_7(DifficultyKey::MythicPlusSixSeven, "MythicPlus_6_7")]
498    #[case::mplus_8_9(DifficultyKey::MythicPlusEightNine, "MythicPlus_8_9")]
499    #[case::mplus_10plus(DifficultyKey::MythicPlusTenPlus, "MythicPlus_10Plus")]
500    #[case::mplus_vault_br(DifficultyKey::MythicPlusVaultBr, "MythicPlus_VaultBR")]
501    fn difficulty_key_as_str(#[case] key: DifficultyKey, #[case] expected: &str) -> Result<()> {
502        verify_that!(key.as_str(), eq(expected))
503    }
504
505    #[gtest]
506    #[rstest]
507    #[case::lfr(DifficultyKey::Lfr, 0)]
508    #[case::normal(DifficultyKey::Normal, 1)]
509    #[case::heroic(DifficultyKey::Heroic, 2)]
510    #[case::mythic(DifficultyKey::Mythic, 3)]
511    #[case::mplus_2_3(DifficultyKey::MythicPlusTwoThree, 4)]
512    #[case::mplus_4(DifficultyKey::MythicPlusFour, 5)]
513    #[case::mplus_5(DifficultyKey::MythicPlusFive, 6)]
514    #[case::mplus_6_7(DifficultyKey::MythicPlusSixSeven, 7)]
515    #[case::mplus_8_9(DifficultyKey::MythicPlusEightNine, 8)]
516    #[case::mplus_10plus(DifficultyKey::MythicPlusTenPlus, 9)]
517    #[case::mplus_vault_br(DifficultyKey::MythicPlusVaultBr, 10)]
518    fn difficulty_key_order(#[case] key: DifficultyKey, #[case] expected: u8) -> Result<()> {
519        verify_that!(key.order(), eq(expected))
520    }
521
522    #[gtest]
523    fn difficulty_key_order_strictly_increasing() -> Result<()> {
524        let ordered = [
525            DifficultyKey::Lfr,
526            DifficultyKey::Normal,
527            DifficultyKey::Heroic,
528            DifficultyKey::Mythic,
529            DifficultyKey::MythicPlusTwoThree,
530            DifficultyKey::MythicPlusFour,
531            DifficultyKey::MythicPlusFive,
532            DifficultyKey::MythicPlusSixSeven,
533            DifficultyKey::MythicPlusEightNine,
534            DifficultyKey::MythicPlusTenPlus,
535            DifficultyKey::MythicPlusVaultBr,
536        ];
537
538        for pair in ordered.windows(2) {
539            verify_that!(pair[0].order() < pair[1].order(), eq(true))?;
540        }
541
542        Ok(())
543    }
544
545    #[gtest]
546    fn difficulty_key_serialize_rename() -> Result<()> {
547        verify_that!(
548            serde_json::to_string(&DifficultyKey::MythicPlusVaultBr).ok(),
549            some(eq("\"MythicPlus_VaultBR\""))
550        )
551    }
552
553    #[gtest]
554    #[rstest]
555    #[case::lfr("\"LFR\"", DifficultyKey::Lfr)]
556    #[case::mplus_2_3("\"MythicPlus_2_3\"", DifficultyKey::MythicPlusTwoThree)]
557    #[case::mplus_vault_br("\"MythicPlus_VaultBR\"", DifficultyKey::MythicPlusVaultBr)]
558    fn difficulty_key_deserialize_rename(
559        #[case] wire: &str,
560        #[case] expected: DifficultyKey,
561    ) -> Result<()> {
562        verify_that!(
563            serde_json::from_str::<DifficultyKey>(wire).ok(),
564            some(eq(expected))
565        )
566    }
567}