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::{
12    misc::soft_into,
13    simulate::{
14        Monster,
15        constants::{LIGHT_ENEMIES, SHADOW_ENEMIES},
16    },
17};
18
19/// The personal demon portal
20#[derive(Debug, Default, Clone)]
21#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
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/// The information about all generic dungeons in the game. Information about
56/// special dungeons like the portal
57#[derive(Debug, Default, Clone)]
58#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
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/// The current state of a dungeon
96#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
97#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
98pub enum DungeonProgress {
99    /// The dungeon has not yet been unlocked
100    #[default]
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/// The category of a dungeon. This is only used internally, so there is no
112/// real point for you to use this
113#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
114#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
115#[allow(missing_docs)]
116pub enum DungeonType {
117    Light,
118    Shadow,
119}
120
121/// The category of a dungeon. This is only used internally, so there is no
122/// real point for you to use this
123#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
124#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
125#[allow(missing_docs)]
126pub enum Dungeon {
127    Light(LightDungeon),
128    Shadow(ShadowDungeon),
129}
130
131/// All possible light dungeons. They are NOT numbered continuously (17 is
132/// missing), so you should use `LightDungeon::iter()`, if you want to iterate
133/// these
134#[derive(
135    Debug,
136    Clone,
137    Copy,
138    PartialEq,
139    Eq,
140    EnumCount,
141    EnumIter,
142    Enum,
143    FromPrimitive,
144    Hash,
145)]
146#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
147#[allow(missing_docs)]
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    ArcadeOfTheOldPixelIcons = 32,
181    TheServerRoom = 33,
182    WorkshopOfTheHunters = 34,
183    RetroTVLegends = 35,
184    MeetingRoom = 36,
185}
186
187impl From<LightDungeon> for Dungeon {
188    fn from(val: LightDungeon) -> Self {
189        Dungeon::Light(val)
190    }
191}
192
193/// All possible shadow dungeons. You can use `ShadowDungeon::iter()`, if you
194/// want to iterate these
195#[derive(
196    Debug,
197    Clone,
198    Copy,
199    PartialEq,
200    Eq,
201    EnumCount,
202    EnumIter,
203    Enum,
204    FromPrimitive,
205    Hash,
206)]
207#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
208#[allow(missing_docs)]
209pub enum ShadowDungeon {
210    DesecratedCatacombs = 0,
211    MinesOfGloria = 1,
212    RuinsOfGnark = 2,
213    CutthroatGrotto = 3,
214    EmeraldScaleAltar = 4,
215    ToxicTree = 5,
216    MagmaStream = 6,
217    FrostBloodTemple = 7,
218    PyramidsOfMadness = 8,
219    BlackSkullFortress = 9,
220    CircusOfHorror = 10,
221    Hell = 11,
222    The13thFloor = 12,
223    Easteros = 13,
224    Twister = 14,
225    TimeHonoredSchoolOfMagic = 15,
226    Hemorridor = 16,
227    ContinuousLoopofIdols = 17,
228    NordicGods = 18,
229    MountOlympus = 19,
230    TavernOfTheDarkDoppelgangers = 20,
231    DragonsHoard = 21,
232    HouseOfHorrors = 22,
233    ThirdLeagueofSuperheroes = 23,
234    DojoOfChildhoodHeroes = 24,
235    MonsterGrotto = 25,
236    CityOfIntrigues = 26,
237    SchoolOfMagicExpress = 27,
238    AshMountain = 28,
239    PlayaGamesHQ = 29,
240    // TrainingDungeon & Sandstorm do not have a shadow version
241    ArcadeOfTheOldPixelIcons = 32,
242    TheServerRoom = 33,
243    WorkshopOfTheHunters = 34,
244    RetroTVLegends = 35,
245    MeetingRoom = 36,
246}
247
248impl From<ShadowDungeon> for Dungeon {
249    fn from(val: ShadowDungeon) -> Self {
250        Dungeon::Shadow(val)
251    }
252}
253
254fn update_progress<T: FromPrimitive + EnumArray<DungeonProgress>>(
255    data: &[i64],
256    dungeons: &mut EnumMap<T, DungeonProgress>,
257) {
258    for (dungeon_id, progress) in data.iter().copied().enumerate() {
259        let Some(dungeon_typ) = FromPrimitive::from_usize(dungeon_id) else {
260            continue;
261        };
262        let dungeon = dungeons.get_mut(dungeon_typ);
263        *dungeon = match progress {
264            -1 => DungeonProgress::Locked,
265            x => {
266                let stage = soft_into(x, "dungeon progress", 0);
267                if stage == 10 || stage == 100 && dungeon_id == 14 {
268                    DungeonProgress::Finished
269                } else {
270                    DungeonProgress::Open { finished: stage }
271                }
272            }
273        };
274    }
275}
276
277impl Dungeons {
278    /// Check if a specific companion can equip the given item
279    #[must_use]
280    pub fn can_companion_equip(
281        &self,
282        companion: CompanionClass,
283        item: &Item,
284    ) -> bool {
285        // When we have no companions they can also not equip anything
286        if self.companions.is_none() {
287            return false;
288        }
289        item.can_be_equipped_by_companion(companion)
290    }
291
292    pub(crate) fn update_progress(
293        &mut self,
294        data: &[i64],
295        dungeon_type: DungeonType,
296    ) {
297        match dungeon_type {
298            DungeonType::Light => update_progress(data, &mut self.light),
299            DungeonType::Shadow => {
300                update_progress(data, &mut self.shadow);
301                for (dungeon, limit) in [
302                    (ShadowDungeon::ContinuousLoopofIdols, 21),
303                    (ShadowDungeon::Twister, 1000),
304                ] {
305                    let d = self.shadow.get_mut(dungeon);
306                    if let DungeonProgress::Open { finished, .. } = d
307                        && *finished >= limit
308                    {
309                        *d = DungeonProgress::Finished;
310                    }
311                }
312            }
313        }
314    }
315}
316
317/// The class of a companion. There is only 1 companion per class, so this is
318/// also a ident of the characters
319#[derive(
320    Debug, Clone, Copy, PartialEq, Eq, EnumCount, Enum, EnumIter, Hash,
321)]
322#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
323pub enum CompanionClass {
324    /// Bert
325    Warrior = 0,
326    /// Mark
327    Mage = 1,
328    /// Kunigunde
329    Scout = 2,
330}
331
332impl From<CompanionClass> for Class {
333    fn from(value: CompanionClass) -> Self {
334        match value {
335            CompanionClass::Warrior => Class::Warrior,
336            CompanionClass::Mage => Class::Mage,
337            CompanionClass::Scout => Class::Scout,
338        }
339    }
340}
341
342/// All the information about a single companion. The class is not included
343/// here, as you access this via a map, where the key will be the class
344#[derive(Debug, Default, Clone)]
345#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
346pub struct Companion {
347    /// I can not recall, if I made this signed on purpose, because this should
348    /// always be > 0
349    pub level: i64,
350    /// The equipment this companion is wearing
351    pub equipment: Equipment,
352    /// The total attributes of this companion
353    pub attributes: EnumMap<AttributeType, u32>,
354}
355
356pub fn dungeon_enemy(
357    dungeon: impl Into<Dungeon>,
358    progress: DungeonProgress,
359) -> Option<&'static Monster> {
360    let stage = match progress {
361        DungeonProgress::Open { finished } => finished,
362        DungeonProgress::Locked | DungeonProgress::Finished => return None,
363    };
364
365    let dungeon: Dungeon = dungeon.into();
366    match dungeon {
367        Dungeon::Light(dungeon) => {
368            LIGHT_ENEMIES.get(dungeon).get(stage as usize)
369        }
370        Dungeon::Shadow(dungeon) => {
371            SHADOW_ENEMIES.get(dungeon).get(stage as usize)
372        }
373    }
374}