Skip to main content

sf_api/simulate/
upgradeable.rs

1use std::sync::Arc;
2
3use enum_map::EnumMap;
4
5use crate::{
6    command::AttributeType,
7    gamestate::{
8        GameState, character::Class, dungeons::CompanionClass, items::*,
9        social::OtherPlayer, underworld::UnderworldBuildingType,
10    },
11    misc::EnumMapGet,
12};
13
14#[derive(Debug, Clone)]
15pub struct UpgradeableFighter {
16    pub name: Arc<str>,
17    pub is_companion: bool,
18    pub level: u16,
19    pub class: Class,
20    /// The base attributes without any equipment, or other boosts
21    pub attribute_basis: EnumMap<AttributeType, u32>,
22    pub pet_attribute_bonus_perc: EnumMap<AttributeType, f64>,
23
24    pub equipment: Equipment,
25    pub active_potions: [Option<Potion>; 3],
26    /// This should be the percentage bonus to skills from pets
27    /// The hp bonus in percent this player has from the personal demon portal
28    pub portal_hp_bonus: u32,
29    /// The damage bonus in percent this player has from the guild demon portal
30    pub portal_dmg_bonus: u32,
31    // The level of the gladiator in the underworld
32    pub gladiator: u32,
33}
34impl UpgradeableFighter {
35    /// Inserts a gem on the item in the specified slot
36    /// If the gem could be inserted the old gem (if any) will be returned
37    /// # Errors
38    ///
39    /// Will return `Err` if the gem could not be inserted. It will contain
40    /// the gem you tried to insert
41    pub fn insert_gem(
42        &mut self,
43        gem: Gem,
44        slot: EquipmentSlot,
45    ) -> Result<Option<Gem>, Gem> {
46        let Some(item) = self.equipment.0.get_mut(slot).as_mut() else {
47            return Err(gem);
48        };
49        let Some(gem_slot) = &mut item.gem_slot else {
50            return Err(gem);
51        };
52
53        let old_gem = match *gem_slot {
54            GemSlot::Filled(gem) => Some(gem),
55            GemSlot::Empty => None,
56        };
57        *gem_slot = GemSlot::Filled(gem);
58        Ok(old_gem)
59    }
60
61    /// Removes the gem at the provided slot and returns the old gem, if
62    /// any
63    pub fn extract_gem(&mut self, slot: EquipmentSlot) -> Option<Gem> {
64        let item = self.equipment.0.get_mut(slot).as_mut()?;
65        let gem_slot = &mut item.gem_slot?;
66
67        let old_gem = match *gem_slot {
68            GemSlot::Filled(gem) => Some(gem),
69            GemSlot::Empty => None,
70        };
71        *gem_slot = GemSlot::Empty;
72        old_gem
73    }
74
75    /// Uses a potion in the provided slot and returns the old potion, if any
76    pub fn use_potion(
77        &mut self,
78        potion: Potion,
79        slot: usize,
80    ) -> Option<Potion> {
81        self.active_potions
82            .get_mut(slot)
83            .and_then(|a| a.replace(potion))
84    }
85
86    /// Removes the potion at the provided slot and returns the old potion, if
87    /// any
88    pub fn remove_potion(&mut self, slot: usize) -> Option<Potion> {
89        self.active_potions.get_mut(slot).and_then(|a| a.take())
90    }
91
92    /// Equip the provided item.
93    /// If the item could be equiped, the previous item will be returned
94    /// # Errors
95    ///
96    /// Will return `Err` if the item could not be equipped. It will contain
97    /// the item you tried to insert
98    pub fn equip(
99        &mut self,
100        item: Item,
101        slot: EquipmentSlot,
102    ) -> Result<Option<Item>, Item> {
103        let Some(item_slot) = item.typ.equipment_slot() else {
104            return Err(item);
105        };
106
107        if (self.is_companion && !item.can_be_equipped_by_companion(self.class))
108            || (!self.is_companion && !item.can_be_equipped_by(self.class))
109        {
110            return Err(item);
111        }
112
113        if item_slot != slot {
114            let is_offhand = slot == EquipmentSlot::Shield
115                && item_slot == EquipmentSlot::Weapon;
116            if !(is_offhand && self.class != Class::Assassin) {
117                return Err(item);
118            }
119        }
120        if slot == EquipmentSlot::Shield
121            && (!self.class.can_wear_shield() || self.is_companion)
122        {
123            return Err(item);
124        }
125
126        let res = self.unequip(slot);
127        *self.equipment.0.get_mut(slot) = Some(item);
128        Ok(res)
129    }
130
131    /// Unequips the item at the provided slot and returns the old item, if any
132    pub fn unequip(&mut self, slot: EquipmentSlot) -> Option<Item> {
133        self.equipment.0.get_mut(slot).take()
134    }
135
136    /// Unequips the item at the provided slot and returns the old item, if any
137    #[must_use]
138    pub fn get_equipment(&self, slot: EquipmentSlot) -> Option<&Item> {
139        self.equipment.0.get(slot).as_ref()
140    }
141
142    #[must_use]
143    pub fn from_other(other: &OtherPlayer) -> Self {
144        UpgradeableFighter {
145            name: other.name.as_str().into(),
146            is_companion: false,
147            level: other.level,
148            class: other.class,
149            attribute_basis: other.attribute_basis,
150            equipment: other.equipment.clone(),
151            active_potions: other.active_potions,
152            pet_attribute_bonus_perc: other
153                .attribute_pet_bonus
154                .map(|_, a| f64::from(a) / 100.0),
155            portal_hp_bonus: other.portal_hp_bonus,
156            portal_dmg_bonus: other.portal_dmg_bonus,
157            // TODO:
158            gladiator: other.gladiator_lvl,
159        }
160    }
161
162    #[must_use]
163    pub fn attributes(&self) -> EnumMap<AttributeType, u32> {
164        let mut total = EnumMap::default();
165
166        for equip in self.equipment.0.iter().flat_map(|a| a.1) {
167            for (k, v) in &equip.attributes {
168                *total.get_mut(k) += v;
169            }
170
171            if let Some(GemSlot::Filled(gem)) = &equip.gem_slot {
172                use AttributeType as AT;
173                let mut value = gem.value;
174                if matches!(equip.typ, ItemType::Weapon { .. })
175                    && !self.is_companion
176                {
177                    value *= 2;
178                }
179
180                let mut add_atr = |at| *total.get_mut(at) += value;
181                match gem.typ {
182                    GemType::Strength => add_atr(AT::Strength),
183                    GemType::Dexterity => add_atr(AT::Dexterity),
184                    GemType::Intelligence => add_atr(AT::Intelligence),
185                    GemType::Constitution => add_atr(AT::Constitution),
186                    GemType::Luck => add_atr(AT::Luck),
187                    GemType::All => {
188                        total.iter_mut().for_each(|a| *a.1 += value);
189                    }
190                    GemType::Legendary => {
191                        add_atr(AT::Constitution);
192                        add_atr(self.class.main_attribute());
193                    }
194                }
195            }
196        }
197
198        let class_bonus: f64 = match self.class {
199            Class::BattleMage => 0.1111,
200            _ => 0.0,
201        };
202
203        let pet_boni = self.pet_attribute_bonus_perc;
204
205        for (k, v) in &mut total {
206            let class_bonus = (f64::from(*v) * class_bonus).trunc() as u32;
207            *v += class_bonus + self.attribute_basis.get(k);
208            if let Some(potion) = self
209                .active_potions
210                .iter()
211                .flatten()
212                .find(|a| a.typ == k.into())
213            {
214                *v += (f64::from(*v) * potion.size.effect()) as u32;
215            }
216
217            let pet_bonus = (f64::from(*v) * (*pet_boni.get(k))).trunc() as u32;
218            *v += pet_bonus;
219        }
220        total
221    }
222
223    #[must_use]
224    #[allow(clippy::enum_glob_use)]
225    pub fn hit_points(&self, attributes: &EnumMap<AttributeType, u32>) -> i64 {
226        let mut total = i64::from(*attributes.get(AttributeType::Constitution));
227        total = (total as f64 * self.class.health_multiplier(self.is_companion))
228            .trunc() as i64;
229
230        total *= i64::from(self.level) + 1;
231
232        if self
233            .active_potions
234            .iter()
235            .flatten()
236            .any(|a| a.typ == PotionType::EternalLife)
237        {
238            total = (total as f64 * 1.25).trunc() as i64;
239        }
240
241        let portal_bonus = (total as f64
242            * (f64::from(self.portal_hp_bonus) / 100.0))
243            .trunc() as i64;
244
245        total += portal_bonus;
246
247        let mut rune_multi = 0;
248        for rune in self
249            .equipment
250            .0
251            .iter()
252            .flat_map(|a| a.1)
253            .filter_map(|a| a.rune)
254        {
255            if rune.typ == RuneType::ExtraHitPoints {
256                rune_multi += u32::from(rune.value);
257            }
258        }
259
260        let rune_bonus =
261            (total as f64 * (f64::from(rune_multi) / 100.0)).trunc() as i64;
262
263        total += rune_bonus;
264        total
265    }
266}
267
268#[derive(Debug)]
269pub struct PlayerFighterSquad {
270    pub character: UpgradeableFighter,
271    pub companions: Option<EnumMap<CompanionClass, UpgradeableFighter>>,
272}
273
274impl PlayerFighterSquad {
275    #[must_use]
276    pub fn new(gs: &GameState) -> PlayerFighterSquad {
277        let mut pet_attribute_bonus_perc = EnumMap::default();
278        if let Some(pets) = &gs.pets {
279            for (typ, info) in &pets.habitats {
280                let mut total_bonus = 0;
281                for pet in &info.pets {
282                    total_bonus += match pet.level {
283                        0 => 0,
284                        1..100 => 100,
285                        100..150 => 150,
286                        150..200 => 175,
287                        200.. => 200,
288                    };
289                }
290                *pet_attribute_bonus_perc.get_mut(typ.into()) =
291                    f64::from(total_bonus / 100) / 100.0;
292            }
293        }
294        let portal_hp_bonus = gs
295            .dungeons
296            .portal
297            .as_ref()
298            .map(|a| a.player_hp_bonus)
299            .unwrap_or_default()
300            .into();
301        let portal_dmg_bonus = gs
302            .guild
303            .as_ref()
304            .map(|a| a.portal.damage_bonus)
305            .unwrap_or_default()
306            .into();
307
308        let gladiator = match &gs.underworld {
309            Some(uw) => uw.buildings[UnderworldBuildingType::GladiatorTrainer]
310                .level
311                .into(),
312            None => 0,
313        };
314
315        let char = &gs.character;
316        let character = UpgradeableFighter {
317            name: char.name.as_str().into(),
318            is_companion: false,
319            level: char.level,
320            class: char.class,
321            attribute_basis: char.attribute_basis,
322            equipment: char.equipment.clone(),
323            active_potions: char.active_potions,
324            pet_attribute_bonus_perc,
325            portal_hp_bonus,
326            portal_dmg_bonus,
327            gladiator,
328        };
329        let mut companions = None;
330        if let Some(comps) = &gs.dungeons.companions {
331            let classes = [
332                CompanionClass::Warrior,
333                CompanionClass::Mage,
334                CompanionClass::Scout,
335            ];
336
337            let res = classes.map(|class| {
338                let comp = comps.get(class);
339                UpgradeableFighter {
340                    name: format!("{}'a {class:?} companion", char.name).into(),
341                    is_companion: true,
342                    level: comp.level.try_into().unwrap_or(1),
343                    class: class.into(),
344                    attribute_basis: comp.attributes,
345                    equipment: comp.equipment.clone(),
346                    active_potions: char.active_potions,
347                    pet_attribute_bonus_perc,
348                    portal_hp_bonus,
349                    portal_dmg_bonus,
350                    gladiator,
351                }
352            });
353            companions = Some(EnumMap::from_array(res));
354        }
355
356        PlayerFighterSquad {
357            character,
358            companions,
359        }
360    }
361}