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