sf_api/gamestate/
rewards.rs

1use std::collections::HashSet;
2
3use chrono::{DateTime, Local};
4use log::warn;
5use num_derive::FromPrimitive;
6use num_traits::FromPrimitive;
7use strum::EnumIter;
8
9use super::{
10    ArrSkip, CCGet, CGet, IdleBuildingType, LightDungeon, Mount, ShopType,
11    character::Class, items::*, tavern::Location, unlockables::HabitatType,
12};
13use crate::{command::AttributeType, error::SFError};
14
15/// The type of a reward you can win by spinning the wheel. The wheel can be
16/// upgraded, so some rewards may not always be available
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
19#[non_exhaustive]
20#[allow(missing_docs)]
21pub enum WheelRewardType {
22    Mushrooms,
23    Stone,
24    StoneXL,
25    Wood,
26    WoodXL,
27    Experience,
28    ExperienceXL,
29    Silver,
30    SilverXL,
31    Arcane,
32    Souls,
33    Item,
34    PetItem(PetItem),
35    Unknown,
36}
37
38/// The thing you won from spinning the wheel
39#[derive(Debug, Clone, Copy)]
40#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
41pub struct WheelReward {
42    /// The type of item you have won
43    pub typ: WheelRewardType,
44    /// The amount of the type you have won
45    pub amount: i64,
46}
47
48impl WheelReward {
49    pub(crate) fn parse(
50        data: &[i64],
51        upgraded: bool,
52    ) -> Result<WheelReward, SFError> {
53        let raw_typ = data.cget(0, "wheel reward typ")?;
54        let mut amount = data.cget(1, "wheel reward amount")?;
55        // NOTE: I have only tested upgraded and inferred not upgraded from that
56        let typ = match raw_typ {
57            0 => WheelRewardType::Mushrooms,
58            1 => {
59                if upgraded {
60                    WheelRewardType::Arcane
61                } else {
62                    WheelRewardType::Wood
63                }
64            }
65            2 => WheelRewardType::ExperienceXL,
66            3 => {
67                if upgraded {
68                    let res = WheelRewardType::PetItem(
69                        PetItem::parse(amount).ok_or_else(|| {
70                            SFError::ParsingError(
71                                "pet wheel reward type",
72                                amount.to_string(),
73                            )
74                        })?,
75                    );
76                    amount = 1;
77                    res
78                } else {
79                    WheelRewardType::Stone
80                }
81            }
82            4 => WheelRewardType::SilverXL,
83            5 => {
84                // The amount does not seem to do anything.
85                // 1 => equipment
86                // 2 => potion
87                amount = 1;
88                WheelRewardType::Item
89            }
90            6 => WheelRewardType::WoodXL,
91            7 => WheelRewardType::Experience,
92            8 => WheelRewardType::StoneXL,
93            9 => {
94                if upgraded {
95                    WheelRewardType::Souls
96                } else {
97                    WheelRewardType::Silver
98                }
99            }
100            x => {
101                warn!("unknown wheel reward type: {x}");
102                WheelRewardType::Unknown
103            }
104        };
105        Ok(WheelReward { typ, amount })
106    }
107}
108
109/// A possible reward on the calendar
110#[derive(Debug, Clone)]
111#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
112pub struct CalendarReward {
113    /// Note that this is technically correct, but at low levels, these are
114    /// often overwritten to silver
115    // FIXME: figure out how exactly
116    pub typ: CalendarRewardType,
117    /// The mount of the type this reward yielded
118    pub amount: i64,
119}
120
121/// The type of reward gainable by collecting the calendar
122#[derive(Debug, Clone, Copy, PartialEq, Eq)]
123#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
124#[allow(missing_docs)]
125pub enum CalendarRewardType {
126    Silver,
127    Mushrooms,
128    Experience,
129    Wood,
130    Stone,
131    Souls,
132    Arcane,
133    Runes,
134    Item,
135    Attribute(AttributeType),
136    Fruit(HabitatType),
137    Level,
138    Potion(PotionType),
139    TenQuicksandGlasses,
140    LevelUp,
141    Unknown,
142}
143
144impl CalendarReward {
145    pub(crate) fn parse(data: &[i64]) -> Result<CalendarReward, SFError> {
146        let amount = data.cget(1, "c reward amount")?;
147        let typ = data.cget(0, "c reward typ")?;
148        let typ = match typ {
149            1 => CalendarRewardType::Silver,
150            2 => CalendarRewardType::Mushrooms,
151            3 => CalendarRewardType::Experience,
152            4 => CalendarRewardType::Wood,
153            5 => CalendarRewardType::Stone,
154            6 => CalendarRewardType::Souls,
155            7 => CalendarRewardType::Arcane,
156            8 => CalendarRewardType::Runes,
157            10 => CalendarRewardType::Item,
158            11 => CalendarRewardType::Attribute(AttributeType::Strength),
159            12 => CalendarRewardType::Attribute(AttributeType::Dexterity),
160            13 => CalendarRewardType::Attribute(AttributeType::Intelligence),
161            14 => CalendarRewardType::Attribute(AttributeType::Constitution),
162            15 => CalendarRewardType::Attribute(AttributeType::Luck),
163            x @ 16..=20 => {
164                if let Some(typ) = HabitatType::from_typ_id(x - 15) {
165                    CalendarRewardType::Fruit(typ)
166                } else {
167                    warn!("unknown pet class in c rewards");
168                    CalendarRewardType::Unknown
169                }
170            }
171            21 => CalendarRewardType::LevelUp,
172            22 => CalendarRewardType::Potion(PotionType::EternalLife),
173            23 => CalendarRewardType::TenQuicksandGlasses,
174            24 => CalendarRewardType::Potion(PotionType::Strength),
175            25 => CalendarRewardType::Potion(PotionType::Dexterity),
176            26 => CalendarRewardType::Potion(PotionType::Intelligence),
177            27 => CalendarRewardType::Potion(PotionType::Constitution),
178            28 => CalendarRewardType::Potion(PotionType::Luck),
179            x => {
180                warn!("Unknown calendar reward: {x}");
181                CalendarRewardType::Unknown
182            }
183        };
184
185        Ok(CalendarReward { typ, amount })
186    }
187}
188
189/// Everything, that changes over time
190#[derive(Debug, Clone, Default)]
191#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
192pub struct TimedSpecials {
193    /// All of the events active in the tavern
194    pub events: Events,
195    /// The stuff you can do for bonus rewards
196    pub tasks: Tasks,
197    /// Grants rewards once a day
198    pub calendar: Calendar,
199    /// Dr. Abawuwu's wheel
200    pub wheel: Wheel,
201    /// The daily reward you can collect before christmas
202    pub advent_calendar: Option<Reward>,
203}
204
205/// Information about the events active in the tavern
206#[derive(Debug, Clone, Default)]
207#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
208pub struct Events {
209    /// All of the events active in the tavern
210    pub active: HashSet<Event>,
211    /// The time at which all of the events end. Mostly just Sunday 23:59.
212    pub ends: Option<DateTime<Local>>,
213}
214
215/// Grants rewards once a day
216#[derive(Debug, Clone, Default)]
217#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
218#[doc(alias = "DailyLoginBonus")]
219pub struct Calendar {
220    /// The amount of times the calendar has been collected already.
221    /// `rewards[collected]` will give you the position in the rewards you will
222    /// get for collecting today (if you can)
223    pub collected: usize,
224    /// The things you can get from the calendar
225    pub rewards: Vec<CalendarReward>,
226    /// The time at which the calendar door will be unlocked. If this is in the
227    /// past, that means it is available to open
228    pub next_possible: Option<DateTime<Local>>,
229}
230
231/// The tasks you get from the goblin gleeman
232#[derive(Debug, Clone, Default)]
233#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
234pub struct Tasks {
235    /// The tasks, that update daily
236    pub daily: DailyTasks,
237    /// The tasks, that follow some server wide theme
238    pub event: EventTasks,
239}
240
241/// Information about the tasks, that reset every day
242#[derive(Debug, Clone, Default)]
243#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
244pub struct DailyTasks {
245    /// The tasks you have to do
246    pub tasks: Vec<Task>,
247    /// The rewards available for completing tasks.
248    pub rewards: [RewardChest; 3],
249}
250
251/// Information about the tasks, that are based on some event theme
252#[derive(Debug, Clone, Default)]
253#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
254pub struct EventTasks {
255    /// The "theme" the event task has. This is mainly irrelevant
256    pub theme: EventTaskTheme,
257    /// The time at which the event tasks have been set
258    pub start: Option<DateTime<Local>>,
259    /// The time at which the event tasks will reset
260    pub end: Option<DateTime<Local>>,
261    /// The actual tasks you have to complete
262    pub tasks: Vec<Task>,
263    /// The rewards available for completing tasks.
264    pub rewards: [RewardChest; 3],
265}
266
267macro_rules! impl_tasks {
268    ($t:ty) => {
269        impl $t {
270            /// The amount of tasks you have completed
271            #[must_use]
272            pub fn completed(&self) -> usize {
273                self.tasks.iter().filter(|a| a.is_completed()).count()
274            }
275
276            /// The amount of points you have collected from completing tasks
277            #[must_use]
278            pub fn earned_points(&self) -> u32 {
279                self.tasks
280                    .iter()
281                    .filter(|a| a.is_completed())
282                    .map(|a| a.point_reward)
283                    .sum()
284            }
285
286            /// The amount of points, that are available in total
287            #[must_use]
288            pub fn total_points(&self) -> u32 {
289                self.tasks.iter().map(|a| a.point_reward).sum()
290            }
291
292            /// Checks if a task of the given type is available and not
293            /// completed
294            #[must_use]
295            pub fn get_available(&self, task_type: TaskType) -> Option<&Task> {
296                self.tasks
297                    .iter()
298                    .find(|task| task.typ == task_type && !task.is_completed())
299            }
300
301            /// Returns all uncompleted tasks
302            #[must_use]
303            pub fn get_uncompleted(&self) -> Vec<&Task> {
304                self.tasks
305                    .iter()
306                    .filter(|task| !task.is_completed())
307                    .collect()
308            }
309
310            /// Checks if the chest at the given index can be opened
311            #[must_use]
312            pub fn can_open_chest(&self, index: usize) -> bool {
313                // Get the chest at the given index
314                let Some(chest) = self.rewards.get(index) else {
315                    return false;
316                };
317
318                // We can't open the chest twice
319                if chest.opened {
320                    return false;
321                }
322
323                // Check if we have enough points to open the given chest
324                self.earned_points() >= chest.required_points
325            }
326        }
327    };
328}
329
330impl_tasks!(DailyTasks);
331impl_tasks!(EventTasks);
332
333impl Task {
334    /// The amount of tasks you have collected
335    #[must_use]
336    pub fn is_completed(&self) -> bool {
337        self.current >= self.target
338    }
339}
340
341/// Dr. Abawuwu's wheel
342#[derive(Debug, Clone, Default)]
343#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
344pub struct Wheel {
345    /// The amount of lucky coins you have to spin the weel
346    pub lucky_coins: u32,
347    /// The amount of times you have spun the wheel today already (0 -> 20)
348    pub spins_today: u8,
349    /// The next time you can spin the wheel for free
350    pub next_free_spin: Option<DateTime<Local>>,
351    /// The result of spinning the wheel
352    pub result: Option<WheelReward>,
353}
354
355/// The theme the event tasks have
356#[derive(Debug, Clone, Copy, PartialEq, Eq, FromPrimitive, Default, Hash)]
357#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
358#[non_exhaustive]
359#[allow(missing_docs)]
360pub enum EventTaskTheme {
361    // 1 is not set
362    Gambler = 2,
363    RankClimber = 3,
364    ShoppingSpree = 4,
365    TimeSkipper = 5,
366    RuffianReset = 6,
367    PartTimeNudist = 7,
368    Scrimper = 8,
369    Scholar = 9,
370    Maximizer = 10,
371    UnderworldFigure = 11,
372    EggHunt = 12,
373    SummerCollectifun = 13,
374    Walpurgis = 14,
375    PetTrainer = 15,
376    FortressMaster = 16,
377    LegendaryDungeon = 17,
378    Hellevator = 18,
379    #[default]
380    Unknown = 245,
381}
382
383/// The type of task you have to do
384#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
385#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
386#[non_exhaustive]
387#[allow(missing_docs)]
388pub enum TaskType {
389    AddSocketToItem,
390    BlacksmithDismantle,
391    BuyHourGlasses,
392    BuyOfferFromArenaManager,
393    ClaimSoulsFromExtractor,
394    CollectGoldFromPit,
395    ConsumeThirstForAdventure,
396    ConsumeThirstFromUnderworld,
397    DefeatGambler,
398    DrinkBeer,
399    EarnMoneyCityGuard,
400    EarnMoneyFromHoFFights,
401    EarnMoneySellingItems,
402    EnterDemonPortal,
403    FeedPets,
404
405    FightGuildHydra,
406    FightGuildPortal,
407    FightInDungeons,
408    FightInPetHabitat,
409    FightMonsterInLegendaryDungeon,
410    FightOtherPets,
411
412    FillMushroomsInAdventuromatic,
413    FindGemInFortress,
414    GainArcaneFromDismantle,
415    GainEpic,
416    GainHonorExpeditions,
417    GainHonorFortress,
418    GainHonorInArena,
419    GainHonorInHoF,
420    GainLegendaryFromLegendaryDungeon,
421    GainMetalFromDismantle,
422    GainSilver,
423    GainSilverFromFightsInHoF,
424    GainXP,
425    GainXpFromAcademy,
426    GainXpFromAdventuromatic,
427    GainXpFromArenaFights,
428    GainXpFromQuests,
429    GetLuckyCoinsFromFlyingTube,
430    GuildReadyFight,
431    LureHeroesIntoUnderworld,
432    PlayGameOfDice,
433    RequestNewGoods,
434    SacrificeRunes,
435    SkipGameOfDiceWait,
436    SkipQuest,
437    SpendGoldInShop,
438    SpendGoldOnUpgrades,
439    SpinWheelOfFortune,
440    ThrowEpicInToilet,
441    ThrowItemInCauldron,
442    ThrowItemInToilet,
443    TravelTo(Location),
444
445    Upgrade(AttributeType),
446    UpgradeAnyAttribute,
447    UpgradeArenaManager,
448    UpgradeItemAttributes,
449
450    WinFightsPlayerPet,
451    WinFightsAgainst(Class),
452    WinFightsBackToBack,
453    WinFightsBareHands,
454    WinFightsInArena,
455    WinFightsInHoF,
456    WinFightsNoChestplate,
457    WinFightsNoEpicsLegendaries,
458    WinFightsNoGear,
459
460    LeaseMount,
461    DefeatMonstersLightDungeon(LightDungeon),
462    BuyWeaponInWeaponsShop,
463    FightHigherRankedPlayer,
464    AddFriend,
465    ClaimNewCustomerPack,
466    JoinOrCreateGuild,
467    UpgradeAnyGuildSkill,
468    CityGuardHours,
469    DrinkPotion(PotionType),
470
471    BuyFromShop(ShopType),
472    FindFruitsOnExpedition,
473    BrewPotions,
474    CollectWood,
475    CollectStone,
476    CommandFortressBattle,
477    FightHellevator,
478    BuyHellevatorTreats,
479    DefeatHellevatorFloors,
480    EnterLegendaryDungeon,
481    OpenLegendaryDungeonCrateChests,
482    FeedPetType(HabitatType),
483    SpendCardsHellevator,
484    OpenAdventCalendar,
485    UpgradeArenaManagerBuilding(IdleBuildingType),
486    EarnMoneyFromExpeditions,
487
488    Unknown,
489}
490
491impl TaskType {
492    pub(crate) fn parse(num: i64) -> TaskType {
493        match num {
494            1 => TaskType::DrinkBeer,
495            2 => TaskType::ConsumeThirstForAdventure,
496            3 => TaskType::WinFightsInArena,
497            4 => TaskType::SpinWheelOfFortune,
498            5 => TaskType::FightGuildHydra,
499            6 => TaskType::FightGuildPortal,
500            7 => TaskType::FeedPets,
501            8 => TaskType::FightOtherPets,
502            9 => TaskType::BlacksmithDismantle,
503            10 => TaskType::ThrowItemInToilet,
504            11 => TaskType::PlayGameOfDice,
505            12 => TaskType::LureHeroesIntoUnderworld,
506            13 => TaskType::EnterDemonPortal,
507            14 => TaskType::DefeatGambler,
508            15 => TaskType::Upgrade(AttributeType::Strength),
509            16 => TaskType::Upgrade(AttributeType::Dexterity),
510            17 => TaskType::Upgrade(AttributeType::Intelligence),
511            18 => TaskType::ConsumeThirstFromUnderworld,
512            19 => TaskType::GuildReadyFight,
513            20 => TaskType::FindGemInFortress,
514            21 => TaskType::ThrowItemInCauldron,
515            22 => TaskType::FightInPetHabitat,
516            23 => TaskType::UpgradeArenaManager,
517            24 => TaskType::SacrificeRunes,
518            25..=45 => {
519                let Some(location) = FromPrimitive::from_i64(num - 24) else {
520                    return TaskType::Unknown;
521                };
522                TaskType::TravelTo(location)
523            }
524            46 => TaskType::ThrowEpicInToilet,
525            47 => TaskType::BuyOfferFromArenaManager,
526            48 => TaskType::WinFightsAgainst(Class::Warrior),
527            49 => TaskType::WinFightsAgainst(Class::Mage),
528            50 => TaskType::WinFightsAgainst(Class::Scout),
529            51 => TaskType::WinFightsAgainst(Class::Assassin),
530            52 => TaskType::WinFightsAgainst(Class::Druid),
531            53 => TaskType::WinFightsAgainst(Class::Bard),
532            54 => TaskType::WinFightsAgainst(Class::BattleMage),
533            55 => TaskType::WinFightsAgainst(Class::Berserker),
534            56 => TaskType::WinFightsAgainst(Class::DemonHunter),
535            57 => TaskType::WinFightsBareHands,
536            58 => TaskType::WinFightsPlayerPet,
537            59 => TaskType::GetLuckyCoinsFromFlyingTube,
538            60 => TaskType::Upgrade(AttributeType::Luck),
539            61 => TaskType::GainHonorInArena,
540            62 => TaskType::GainHonorInHoF,
541            63 => TaskType::GainHonorFortress,
542            64 => TaskType::GainHonorExpeditions,
543            65 => TaskType::SpendGoldInShop,
544            66 => TaskType::SpendGoldOnUpgrades,
545            67 => TaskType::RequestNewGoods,
546            68 => TaskType::BuyHourGlasses,
547            69 => TaskType::SkipQuest,
548            70 => TaskType::SkipGameOfDiceWait,
549            71 => TaskType::WinFightsInHoF,
550            72 => TaskType::WinFightsBackToBack,
551            73 => TaskType::SpendCardsHellevator,
552            74 => TaskType::GainSilverFromFightsInHoF,
553            75 => TaskType::WinFightsNoChestplate,
554            76 => TaskType::WinFightsNoGear,
555            77 => TaskType::WinFightsNoEpicsLegendaries,
556            78 => TaskType::EarnMoneyCityGuard,
557            79 => TaskType::EarnMoneyFromHoFFights,
558            80 => TaskType::EarnMoneySellingItems,
559            81 => TaskType::CollectGoldFromPit,
560            82 => TaskType::GainXpFromQuests,
561            83 => TaskType::GainXpFromAcademy,
562            84 => TaskType::GainXpFromArenaFights,
563            85 => TaskType::GainXpFromAdventuromatic,
564            86 => TaskType::GainArcaneFromDismantle,
565            87 => TaskType::GainMetalFromDismantle,
566            88 => TaskType::UpgradeItemAttributes,
567            89 => TaskType::AddSocketToItem,
568            90 => TaskType::ClaimSoulsFromExtractor,
569            91 => TaskType::FillMushroomsInAdventuromatic,
570            92 => TaskType::WinFightsAgainst(Class::Necromancer),
571            93 => TaskType::GainLegendaryFromLegendaryDungeon,
572            94 => TaskType::FightMonsterInLegendaryDungeon,
573            95 => TaskType::FightInDungeons,
574            96 => TaskType::UpgradeAnyAttribute,
575            97 => TaskType::GainSilver,
576            98 => TaskType::GainXP,
577            99 => TaskType::GainEpic,
578            100 => TaskType::BuyFromShop(ShopType::Magic),
579            101 => TaskType::BuyFromShop(ShopType::Weapon),
580            102 => TaskType::FindFruitsOnExpedition,
581            103 => TaskType::BrewPotions,
582            104 => TaskType::CollectWood,
583            105 => TaskType::CollectStone,
584            106 => TaskType::CommandFortressBattle,
585            107 => TaskType::FightHellevator,
586            108 => TaskType::BuyHellevatorTreats,
587            109 => TaskType::DefeatHellevatorFloors,
588            110 => TaskType::EnterLegendaryDungeon,
589            111 => TaskType::OpenLegendaryDungeonCrateChests,
590            112 => TaskType::FeedPetType(HabitatType::Shadow),
591            113 => TaskType::FeedPetType(HabitatType::Light),
592            114 => TaskType::FeedPetType(HabitatType::Earth),
593            115 => TaskType::FeedPetType(HabitatType::Fire),
594            116 => TaskType::FeedPetType(HabitatType::Water),
595            117 => {
596                TaskType::DefeatMonstersLightDungeon(LightDungeon::TrainingCamp)
597            }
598            118 => TaskType::ClaimNewCustomerPack,
599            119 => TaskType::JoinOrCreateGuild,
600            120 => TaskType::UpgradeAnyGuildSkill,
601            121 => TaskType::AddFriend,
602            122 => TaskType::DrinkPotion(PotionType::Constitution),
603            123 => TaskType::DrinkPotion(PotionType::Strength),
604            124 => TaskType::DrinkPotion(PotionType::Dexterity),
605            125 => TaskType::DrinkPotion(PotionType::Intelligence),
606            126 => TaskType::DrinkPotion(PotionType::EternalLife),
607            127 => TaskType::LeaseMount,
608            128 => TaskType::FightHigherRankedPlayer,
609            129 => TaskType::CityGuardHours,
610            130 => TaskType::BuyWeaponInWeaponsShop,
611            131 => TaskType::Upgrade(AttributeType::Constitution),
612            132 => TaskType::WinFightsAgainst(Class::Paladin),
613            133 => {
614                TaskType::UpgradeArenaManagerBuilding(IdleBuildingType::Seat)
615            }
616            134 => TaskType::OpenAdventCalendar,
617            135 => TaskType::EarnMoneyFromExpeditions,
618            136 => TaskType::WinFightsAgainst(Class::PlagueDoctor),
619
620            ..=0 | 137.. => TaskType::Unknown,
621        }
622    }
623}
624
625/// Something to do to get a point reward
626#[derive(Debug, Clone, Copy, PartialEq, Eq)]
627#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
628pub struct Task {
629    /// The thing you are tasked with doing or getting for this task
630    pub typ: TaskType,
631    /// The amount of times, or the amount of `typ` you have currently
632    pub current: u64,
633    /// The amount current has to be at to complete this task
634    pub target: u64,
635    /// The amount of points you get for completing this task
636    pub point_reward: u32,
637}
638
639impl Task {
640    pub(crate) fn parse(data: &[i64]) -> Result<Task, SFError> {
641        let raw_typ = data.cget(0, "task typ")?;
642        let typ = TaskType::parse(raw_typ);
643
644        if typ == TaskType::Unknown {
645            warn!("Unknown task: {data:?} {raw_typ}");
646        }
647        Ok(Task {
648            typ,
649            current: data.csiget(1, "current ti", 0)?,
650            target: data.csiget(2, "target ti", u64::MAX)?,
651            point_reward: data.csiget(3, "reward ti", 0)?,
652        })
653    }
654}
655
656/// Something you can unlock for completing tasks
657#[derive(Debug, Clone, Default)]
658#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
659pub struct RewardChest {
660    /// Whether or not this chest has been unlocked
661    pub opened: bool,
662    /// The amount of points required to open this chest
663    pub required_points: u32,
664    /// The things you will get for opening this chest
665    pub rewards: Vec<Reward>,
666}
667
668/// The reward for opening a chest
669#[derive(Debug, Clone)]
670#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
671pub struct Reward {
672    /// The type of the thing you are getting
673    pub typ: RewardType,
674    /// The amount of `typ` you get
675    pub amount: u64,
676}
677
678#[derive(Debug, Clone, PartialEq, Eq, Copy, Hash)]
679#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
680pub enum RewardType {
681    HellevatorPoints,
682    HellevatorCards,
683    Mushrooms,
684    Silver,
685    LuckyCoins,
686    Wood,
687    Stone,
688    Arcane,
689    Metal,
690    Souls,
691    Fruit(HabitatType),
692    LegendaryGem,
693    GoldFidget,
694    SilverFidget,
695    BronzeFidget,
696    Gem,
697    FruitBasket,
698    XP,
699    Egg,
700    QuicksandGlass,
701    Honor,
702    Beer,
703    Frame,
704    Mount(Mount),
705    Unknown,
706}
707
708impl RewardType {
709    #[must_use]
710    pub(crate) fn parse(val: i64) -> RewardType {
711        match val {
712            1 => RewardType::HellevatorPoints,
713            2 => RewardType::HellevatorCards,
714            3 => RewardType::Mushrooms,
715            4 => RewardType::Silver,
716            5 => RewardType::LuckyCoins,
717            6 => RewardType::Wood,
718            7 => RewardType::Stone,
719            8 => RewardType::Arcane,
720            9 => RewardType::Metal,
721            10 => RewardType::Souls,
722            11 => RewardType::Fruit(HabitatType::Shadow),
723            12 => RewardType::Fruit(HabitatType::Light),
724            13 => RewardType::Fruit(HabitatType::Earth),
725            14 => RewardType::Fruit(HabitatType::Fire),
726            15 => RewardType::Fruit(HabitatType::Water),
727            16 => RewardType::LegendaryGem,
728            17 => RewardType::GoldFidget,
729            18 => RewardType::SilverFidget,
730            19 => RewardType::BronzeFidget,
731            20..=22 => RewardType::Gem,
732            23 => RewardType::FruitBasket,
733            24 => RewardType::XP,
734            25 => RewardType::Egg,
735            26 => RewardType::QuicksandGlass,
736            27 => RewardType::Honor,
737            28 => RewardType::Beer,
738            29 => RewardType::Frame,
739            30 => RewardType::Mount(Mount::Cow),
740            31 => RewardType::Mount(Mount::Horse),
741            32 => RewardType::Mount(Mount::Tiger),
742            33 => RewardType::Mount(Mount::Dragon),
743            x => {
744                warn!("Unknown reward type: {x}");
745                RewardType::Unknown
746            }
747        }
748    }
749}
750
751impl Reward {
752    pub(crate) fn parse(data: &[i64]) -> Result<Reward, SFError> {
753        Ok(Reward {
754            typ: RewardType::parse(data.cget(0, "reward typ")?),
755            amount: data.csiget(1, "reward amount", 0)?,
756        })
757    }
758}
759
760impl RewardChest {
761    pub(crate) fn parse(data: &[i64]) -> Result<RewardChest, SFError> {
762        let opened = data.cget(0, "rchest opened")? != 0;
763        let required_points = data.ciget(1, "reward chest required points")?;
764        let reward_count: usize = data.ciget(2, "reward chest count")?;
765        let mut rewards = Vec::new();
766        for pos in 0..reward_count {
767            let data = data.skip(3 + pos * 2, "rchest rewards")?;
768            rewards.push(Reward::parse(data)?);
769        }
770        Ok(RewardChest {
771            opened,
772            required_points,
773            rewards,
774        })
775    }
776}
777
778/// The type of event, that is currently happening on the server
779#[derive(Debug, Clone, Copy, FromPrimitive, PartialEq, Eq, Hash, EnumIter)]
780#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
781#[allow(missing_docs)]
782pub enum Event {
783    ExceptionalXPEvent = 0,
784    GloriousGoldGalore,
785    TidyToiletTime,
786    AssemblyOfAwesomeAnimals,
787    FantasticFortressFestivity,
788    DaysOfDoomedSouls,
789    WitchesDance,
790    SandsOfTimeSpecial,
791    ForgeFrenzyFestival,
792    EpicShoppingSpreeExtravaganza,
793    EpicQuestExtravaganza,
794    EpicGoodLuckExtravaganza,
795    OneBeerTwoBeerFreeBeer,
796    PieceworkParty,
797    LuckyDay,
798    CrazyMushroomHarvest,
799    HolidaySale,
800    ValentinesBlessing,
801    BlackGemRush,
802    RumbleForRiches,
803}
804
805pub(crate) fn parse_rewards(vals: &[i64]) -> [RewardChest; 3] {
806    let mut start = 0;
807    core::array::from_fn(|_| -> Result<RewardChest, SFError> {
808        let vals = vals.skip(start, "multi reward chest")?;
809        let chest = RewardChest::parse(vals)?;
810        let consumed = 3 + chest.rewards.len() * 2;
811        start += consumed;
812        Ok(chest)
813    })
814    .map(|res| match res {
815        Ok(res) => res,
816        Err(err) => {
817            warn!("Bad task rewards: {err}");
818            RewardChest::default()
819        }
820    })
821}