Skip to main content

sf_api/gamestate/
dungeons.rs

1use chrono::{DateTime, Local};
2use enum_map::{Enum, EnumArray, EnumMap};
3use num_derive::FromPrimitive;
4use num_traits::FromPrimitive;
5use strum::{EnumCount, EnumIter};
6
7use super::{
8    AttributeType, CCGet, Class, EnumMapGet, Item, SFError, ServerTime,
9    items::Equipment,
10};
11use crate::misc::soft_into;
12
13/// The personal demon portal
14#[derive(Debug, Default, Clone)]
15#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
16pub struct Portal {
17    /// The amount of enemies you have fought in the portal already
18    pub finished: u16,
19    /// If this is true, you can fight the portal via the `FightPortal`
20    /// command. You will have to wait until the next day (on the server) and
21    /// send an `Update` to make sure this is set correctly
22    pub can_fight: bool,
23    /// The level of the enemy in the portal. For some reason this is always
24    /// wrong by a few levels?
25    pub enemy_level: u32,
26    /// The amount of health the enemy has left
27    pub enemy_hp_percentage: u8,
28    /// Percentage boost to the players hp
29    pub player_hp_bonus: u16,
30}
31
32impl Portal {
33    pub(crate) fn update(
34        &mut self,
35        data: &[i64],
36        server_time: ServerTime,
37    ) -> Result<(), SFError> {
38        self.finished = data.csiget(0, "portal fights", 10_000)?;
39        self.enemy_hp_percentage = data.csiget(1, "portal hp", 0)?;
40
41        let current_day = chrono::Datelike::ordinal(&server_time.current());
42        let last_portal_day: u32 = data.csiget(2, "portal day", 0)?;
43        self.can_fight = last_portal_day != current_day;
44
45        Ok(())
46    }
47}
48
49/// The information about all generic dungeons in the game. Information about
50/// special dungeons like the portal
51#[derive(Debug, Default, Clone)]
52#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
53pub struct Dungeons {
54    /// The next time you can fight in the dungeons for free
55    pub next_free_fight: Option<DateTime<Local>>,
56    /// All the light dungeons. Notably tower information is also in here
57    pub light: EnumMap<LightDungeon, DungeonProgress>,
58    /// All the shadow dungeons. Notably twister & cont. loop of idols is also
59    /// in here
60    pub shadow: EnumMap<ShadowDungeon, DungeonProgress>,
61    pub portal: Option<Portal>,
62    /// The companions unlocked from unlocking the tower. Note that the tower
63    /// info itself is just handled as a normal light dungeon
64    pub companions: Option<EnumMap<CompanionClass, Companion>>,
65}
66
67impl Dungeons {
68    /// Returns the progress for that dungeon
69    pub fn progress(&self, dungeon: impl Into<Dungeon>) -> DungeonProgress {
70        let dungeon: Dungeon = dungeon.into();
71        match dungeon {
72            Dungeon::Light(dungeon) => *self.light.get(dungeon),
73            Dungeon::Shadow(dungeon) => *self.shadow.get(dungeon),
74        }
75    }
76
77    /// Returns the current enemy for that dungeon. Note that the special
78    /// "mirrorimage" enemy will be listed as a warrior with 0 stats/lvl/xp/hp.
79    // If you care about the actual stats, you should map this to the player
80    // stats yourself
81    #[cfg(feature = "simulation")]
82    pub fn current_enemy(
83        &self,
84        dungeon: impl Into<Dungeon> + Copy,
85    ) -> Option<&'static crate::simulate::Monster> {
86        get_dungeon_monster(dungeon, self.progress(dungeon))
87    }
88}
89
90/// The current state of a dungeon
91#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
92#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
93pub enum DungeonProgress {
94    /// The dungeon has not yet been unlocked
95    #[default]
96    Locked,
97    /// The dungeon is open and can be fought in
98    Open {
99        /// The amount of enemies already finished
100        finished: u16,
101    },
102    /// The dungeon has been fully cleared and can not be entered anymore
103    Finished,
104}
105
106/// The category of a dungeon. This is only used internally, so there is no
107/// real point for you to use this
108#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
109#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
110#[allow(missing_docs)]
111pub enum DungeonType {
112    Light,
113    Shadow,
114}
115
116/// The category of a dungeon. This is only used internally, so there is no
117/// real point for you to use this
118#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
119#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
120#[allow(missing_docs)]
121pub enum Dungeon {
122    Light(LightDungeon),
123    Shadow(ShadowDungeon),
124}
125
126impl Dungeon {
127    #[must_use]
128    #[allow(clippy::match_same_arms)]
129    pub fn is_with_companions(self) -> bool {
130        match self {
131            Dungeon::Light(LightDungeon::Tower) => true,
132            Dungeon::Shadow(ShadowDungeon::Twister) => false,
133            Dungeon::Light(_) => false,
134            Dungeon::Shadow(_) => true,
135        }
136    }
137}
138
139/// All possible light dungeons. They are NOT numbered continuously (17 is
140/// missing), so you should use `LightDungeon::iter()`, if you want to iterate
141/// these
142#[derive(
143    Debug,
144    Clone,
145    Copy,
146    PartialEq,
147    Eq,
148    EnumCount,
149    EnumIter,
150    Enum,
151    FromPrimitive,
152    Hash,
153)]
154#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
155#[allow(missing_docs)]
156pub enum LightDungeon {
157    DesecratedCatacombs = 0,
158    MinesOfGloria = 1,
159    RuinsOfGnark = 2,
160    CutthroatGrotto = 3,
161    EmeraldScaleAltar = 4,
162    ToxicTree = 5,
163    MagmaStream = 6,
164    FrostBloodTemple = 7,
165    PyramidsofMadness = 8,
166    BlackSkullFortress = 9,
167    CircusOfHorror = 10,
168    Hell = 11,
169    The13thFloor = 12,
170    Easteros = 13,
171    Tower = 14,
172    TimeHonoredSchoolofMagic = 15,
173    Hemorridor = 16,
174    NordicGods = 18,
175    MountOlympus = 19,
176    TavernoftheDarkDoppelgangers = 20,
177    DragonsHoard = 21,
178    HouseOfHorrors = 22,
179    ThirdLeagueOfSuperheroes = 23,
180    DojoOfChildhoodHeroes = 24,
181    MonsterGrotto = 25,
182    CityOfIntrigues = 26,
183    SchoolOfMagicExpress = 27,
184    AshMountain = 28,
185    PlayaGamesHQ = 29,
186    TrainingCamp = 30,
187    Sandstorm = 31,
188    ArcadeOfTheOldPixelIcons = 32,
189    TheServerRoom = 33,
190    WorkshopOfTheHunters = 34,
191    RetroTVLegends = 35,
192    MeetingRoom = 36,
193}
194
195impl From<LightDungeon> for Dungeon {
196    fn from(val: LightDungeon) -> Self {
197        Dungeon::Light(val)
198    }
199}
200
201/// All possible shadow dungeons. You can use `ShadowDungeon::iter()`, if you
202/// want to iterate these
203#[derive(
204    Debug,
205    Clone,
206    Copy,
207    PartialEq,
208    Eq,
209    EnumCount,
210    EnumIter,
211    Enum,
212    FromPrimitive,
213    Hash,
214)]
215#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
216#[allow(missing_docs)]
217pub enum ShadowDungeon {
218    DesecratedCatacombs = 0,
219    MinesOfGloria = 1,
220    RuinsOfGnark = 2,
221    CutthroatGrotto = 3,
222    EmeraldScaleAltar = 4,
223    ToxicTree = 5,
224    MagmaStream = 6,
225    FrostBloodTemple = 7,
226    PyramidsOfMadness = 8,
227    BlackSkullFortress = 9,
228    CircusOfHorror = 10,
229    Hell = 11,
230    The13thFloor = 12,
231    Easteros = 13,
232    Twister = 14,
233    TimeHonoredSchoolOfMagic = 15,
234    Hemorridor = 16,
235    ContinuousLoopofIdols = 17,
236    NordicGods = 18,
237    MountOlympus = 19,
238    TavernOfTheDarkDoppelgangers = 20,
239    DragonsHoard = 21,
240    HouseOfHorrors = 22,
241    ThirdLeagueofSuperheroes = 23,
242    DojoOfChildhoodHeroes = 24,
243    MonsterGrotto = 25,
244    CityOfIntrigues = 26,
245    SchoolOfMagicExpress = 27,
246    AshMountain = 28,
247    PlayaGamesHQ = 29,
248    // TrainingDungeon & Sandstorm do not have a shadow version
249    ArcadeOfTheOldPixelIcons = 32,
250    TheServerRoom = 33,
251    WorkshopOfTheHunters = 34,
252    RetroTVLegends = 35,
253    MeetingRoom = 36,
254}
255
256impl From<ShadowDungeon> for Dungeon {
257    fn from(val: ShadowDungeon) -> Self {
258        Dungeon::Shadow(val)
259    }
260}
261
262fn update_progress<T: FromPrimitive + EnumArray<DungeonProgress>>(
263    data: &[i64],
264    dungeons: &mut EnumMap<T, DungeonProgress>,
265) {
266    for (dungeon_id, progress) in data.iter().copied().enumerate() {
267        let Some(dungeon_typ) = FromPrimitive::from_usize(dungeon_id) else {
268            continue;
269        };
270        let dungeon = dungeons.get_mut(dungeon_typ);
271        *dungeon = match progress {
272            -1 => DungeonProgress::Locked,
273            x => {
274                let stage = soft_into(x, "dungeon progress", 0);
275                if stage == 10 || stage == 100 && dungeon_id == 14 {
276                    DungeonProgress::Finished
277                } else {
278                    DungeonProgress::Open { finished: stage }
279                }
280            }
281        };
282    }
283}
284
285impl Dungeons {
286    /// Check if a specific companion can equip the given item
287    #[must_use]
288    pub fn can_companion_equip(
289        &self,
290        companion: CompanionClass,
291        item: &Item,
292    ) -> bool {
293        // When we have no companions they can also not equip anything
294        if self.companions.is_none() {
295            return false;
296        }
297        item.can_be_equipped_by_companion(companion)
298    }
299
300    pub(crate) fn update_progress(
301        &mut self,
302        data: &[i64],
303        dungeon_type: DungeonType,
304    ) {
305        match dungeon_type {
306            DungeonType::Light => update_progress(data, &mut self.light),
307            DungeonType::Shadow => {
308                update_progress(data, &mut self.shadow);
309                for (dungeon, limit) in [
310                    (ShadowDungeon::ContinuousLoopofIdols, 21),
311                    (ShadowDungeon::Twister, 1000),
312                ] {
313                    let d = self.shadow.get_mut(dungeon);
314                    if let DungeonProgress::Open { finished, .. } = d
315                        && *finished >= limit
316                    {
317                        *d = DungeonProgress::Finished;
318                    }
319                }
320            }
321        }
322    }
323}
324
325/// The class of a companion. There is only 1 companion per class, so this is
326/// also a ident of the characters
327#[derive(
328    Debug, Clone, Copy, PartialEq, Eq, EnumCount, Enum, EnumIter, Hash,
329)]
330#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
331pub enum CompanionClass {
332    /// Bert
333    Warrior = 0,
334    /// Mark
335    Mage = 1,
336    /// Kunigunde
337    Scout = 2,
338}
339
340impl From<CompanionClass> for Class {
341    fn from(value: CompanionClass) -> Self {
342        match value {
343            CompanionClass::Warrior => Class::Warrior,
344            CompanionClass::Mage => Class::Mage,
345            CompanionClass::Scout => Class::Scout,
346        }
347    }
348}
349
350/// All the information about a single companion. The class is not included
351/// here, as you access this via a map, where the key will be the class
352#[derive(Debug, Default, Clone)]
353#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
354pub struct Companion {
355    /// I can not recall, if I made this signed on purpose, because this should
356    /// always be > 0
357    pub level: i64,
358    /// The equipment this companion is wearing
359    pub equipment: Equipment,
360    /// The total attributes of this companion
361    pub attributes: EnumMap<AttributeType, u32>,
362}
363
364#[cfg(feature = "simulation")]
365pub fn get_dungeon_monster(
366    dungeon: impl Into<Dungeon>,
367    progress: DungeonProgress,
368) -> Option<&'static crate::simulate::Monster> {
369    let stage = match progress {
370        DungeonProgress::Open { finished } => finished,
371        DungeonProgress::Locked | DungeonProgress::Finished => return None,
372    };
373    crate::simulate::constants::get_dungeon_enemies(dungeon.into())
374        .get(stage as usize)
375}