Skip to main content

sf_api/gamestate/
character.rs

1use std::fmt::Debug;
2
3use chrono::{DateTime, Local};
4use enum_map::EnumMap;
5use num_derive::FromPrimitive;
6use num_traits::FromPrimitive;
7
8use super::{NormalCost, RelationEntry, SFError, ScrapBook};
9use crate::{PlayerId, command::*, gamestate::items::*, misc::*};
10
11/// Everything, that can be considered part of the character and not the rest
12/// of the world
13#[derive(Debug, Clone, Default)]
14#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
15pub struct Character {
16    /// This is the unique identifier of this character. Can be used to compare
17    /// against places, that also have `player_ids` to make sure a Hall of
18    /// Fame entry or similar is not the player
19    pub player_id: PlayerId,
20    /// The name of this character
21    pub name: String,
22    /// The current level of this character
23    pub level: u16,
24    /// The amount of silver a player has. 100 silver = 1 gold
25    pub silver: u64,
26    /// The amount of moshrooms a player has
27    pub mushrooms: u32,
28
29    /// The class of this character
30    pub class: Class,
31
32    /// The race of this character. Has some effects on attributes, which is
33    /// why this is not in portrait
34    pub race: Race,
35    /// Everything that determines the players looks except for the race
36    pub portrait: Portrait,
37    /// The description of this character
38    pub description: String,
39
40    /// The amount of experience already earned in the current level
41    pub experience: u64,
42    /// The amount of experience required to level up.
43    /// `next_level_xp - experience` is the amount of xp missing to level up
44    pub next_level_xp: u64,
45    /// The amount of honor earned through the arena
46    pub honor: u32,
47    /// The rank in the hall of fame
48    pub rank: u32,
49
50    /// All the items this character has stored. These are all the slots right
51    /// next to the portrait in the web ui
52    pub inventory: Inventory,
53    /// All items the character has currently equipped (on the body)
54    pub equipment: Equipment,
55
56    /// If the character has a mannequin, this will contain all the equipment
57    /// stored in it
58    pub mannequin: Option<Equipment>,
59    /// The potions currently active
60    pub active_potions: [Option<Potion>; 3],
61
62    /// The total armor of our character. Basically all equipped armor combined
63    pub armor: u64,
64
65    /// The min amount of damage the weapon claims it can do without any bonus
66    pub min_damage: u32,
67    /// The max amount of damage the weapon claims it can do without any bonus
68    pub max_damage: u32,
69
70    /// The base attributes without any equipment, or other boosts
71    pub attribute_basis: EnumMap<AttributeType, u32>,
72    /// All bonus attributes from equipment/pets/potions
73    pub attribute_additions: EnumMap<AttributeType, u32>,
74    /// The amount of times an attribute has been bought already.
75    /// Important to calculate the price of the next attribute to buy
76    pub attribute_times_bought: EnumMap<AttributeType, u32>,
77
78    /// The mount this character has rented
79    pub mount: Option<Mount>,
80    /// The point at which the mount will end. Note that this might be None,
81    /// whilst mount is Some
82    pub mount_end: Option<DateTime<Local>>,
83    /// The silver you get for buying a dragon
84    pub mount_dragon_refund: u64,
85
86    /// If the scrapbook has been unlocked, it can be found here
87    pub scrapbook: Option<ScrapBook>,
88
89    /// A list of other characters, that the set some sort of special relation
90    /// to. Either good, or bad
91    pub relations: Vec<RelationEntry>,
92}
93
94/// All the exclusively cosmetic info necessary to build a player image, that is
95/// otherwise useless. As these values might change their based on each other,
96/// some of them are not fully parsed (to a more descriptive enum)
97#[derive(Debug, Default, Clone, PartialEq, Eq)]
98#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
99#[allow(missing_docs)]
100pub struct Portrait {
101    /// The gender (m/w)
102    pub gender: Gender,
103    pub hair_color: u8,
104    pub hair: u8,
105    pub mouth: u8,
106    pub brows: u8,
107    pub eyes: u8,
108    pub beards: u8,
109    pub nose: u8,
110    pub ears: u8,
111    pub extra: u8,
112    pub horns: u8,
113    /// Influencers get a special portrait. Otherwise this should be 0
114    pub special_portrait: i64,
115}
116
117impl Portrait {
118    pub(crate) fn parse(data: &[i64]) -> Result<Portrait, SFError> {
119        Ok(Self {
120            mouth: data.csiget(0, "mouth", 1)?,
121            hair_color: data.csimget(1, "hair color", 100, |a| a / 100)?,
122            hair: data.csimget(1, "hair", 1, |a| a % 100)?,
123            brows: data.csimget(2, "brows", 1, |a| a % 100)?,
124            eyes: data.csiget(3, "eyes", 1)?,
125            beards: data.csimget(4, "beards", 1, |a| a % 100)?,
126            nose: data.csiget(5, "nose", 1)?,
127            ears: data.csiget(6, "ears", 1)?,
128            extra: data.csiget(7, "extra", 1)?,
129            horns: data.csimget(8, "horns", 1, |a| a % 100)?,
130            special_portrait: data.cget(9, "special portrait")?,
131            gender: Gender::from_i64(data.csimget(11, "gender", 1, |a| a % 2)?)
132                .unwrap_or_default(),
133        })
134    }
135}
136
137#[derive(Debug, Clone, Default, Copy, PartialEq, Eq, FromPrimitive, Hash)]
138#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
139#[allow(missing_docs)]
140pub enum Gender {
141    #[default]
142    Female = 0,
143    Male,
144}
145
146#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, FromPrimitive, Hash)]
147#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
148#[allow(missing_docs)]
149pub enum Class {
150    #[default]
151    Warrior = 0,
152    Mage,
153    Scout,
154    Assassin,
155    BattleMage,
156    Berserker,
157    DemonHunter,
158    Druid,
159    Bard,
160    Necromancer,
161    Paladin,
162    PlagueDoctor,
163}
164
165#[allow(clippy::enum_glob_use)]
166impl Class {
167    #[must_use]
168    #[allow(clippy::enum_glob_use)]
169    pub fn main_attribute(&self) -> AttributeType {
170        use Class::*;
171        match self {
172            Paladin | BattleMage | Berserker | Warrior => {
173                AttributeType::Strength
174            }
175            Assassin | DemonHunter | Scout | PlagueDoctor => {
176                AttributeType::Dexterity
177            }
178            Druid | Bard | Necromancer | Mage => AttributeType::Intelligence,
179        }
180    }
181
182    #[must_use]
183    pub fn weapon_multiplier(self) -> f64 {
184        use Class::*;
185        match self {
186            PlagueDoctor | Paladin | Warrior | Assassin | BattleMage
187            | Berserker => 2.0,
188            // TODO: Recheck these
189            Scout | DemonHunter => 2.5,
190            Mage | Druid | Bard | Necromancer => 4.5,
191        }
192    }
193
194    #[must_use]
195    pub fn weapon_gem_multiplier(&self) -> i32 {
196        match self {
197            Class::Warrior | Class::Assassin | Class::Berserker => 1,
198            _ => 2,
199        }
200    }
201
202    #[must_use]
203    pub fn weapon_attribute_multiplier(&self) -> i32 {
204        match self {
205            Class::Warrior
206            | Class::BattleMage
207            | Class::Berserker
208            | Class::Paladin
209            | Class::PlagueDoctor
210            | Class::Assassin => 1,
211            _ => 2,
212        }
213    }
214
215    #[cfg(feature = "simulation")]
216    #[must_use]
217    pub(crate) fn health_multiplier(self, is_companion: bool) -> f64 {
218        use Class::*;
219
220        match self {
221            Warrior if is_companion => 6.1,
222            Warrior | BattleMage | Druid => 5.0,
223            Paladin => 6.0,
224            PlagueDoctor | Scout | Assassin | Berserker | DemonHunter
225            | Necromancer => 4.0,
226            Mage | Bard => 2.0,
227        }
228    }
229
230    #[must_use]
231    pub fn item_armor_multiplier(&self) -> f64 {
232        match self {
233            Class::Warrior
234            | Class::Berserker
235            | Class::DemonHunter
236            | Class::Paladin => 15.0,
237            Class::Scout | Class::Assassin | Class::Druid | Class::Bard => 7.5,
238            Class::Mage
239            | Class::BattleMage
240            | Class::Necromancer
241            | Class::PlagueDoctor => 3.0,
242        }
243    }
244
245    #[must_use]
246    pub fn item_bonus_multiplier(&self) -> f64 {
247        match self {
248            Class::BattleMage | Class::PlagueDoctor => 1.11,
249            Class::Berserker => 1.1,
250            _ => 1.0,
251        }
252    }
253
254    #[must_use]
255    pub fn armor_multiplier(&self) -> f64 {
256        match self {
257            Class::BattleMage => 5.0,
258            Class::Bard | Class::Necromancer | Class::PlagueDoctor => 2.0,
259            Class::Berserker => 0.5,
260            _ => 1.0,
261        }
262    }
263
264    #[must_use]
265    pub fn max_armor_reduction(&self) -> u32 {
266        match self {
267            Class::Mage => 10,
268            Class::Warrior
269            | Class::BattleMage
270            | Class::DemonHunter
271            | Class::Bard => 50,
272            Class::Paladin => 45,
273            Class::Scout
274            | Class::Assassin
275            | Class::Berserker
276            | Class::Druid => 25,
277            Class::Necromancer | Class::PlagueDoctor => 20,
278        }
279    }
280
281    #[must_use]
282    pub fn damage_multiplier(&self) -> f64 {
283        match self {
284            Class::Assassin => 0.625,
285            Class::Berserker | Class::PlagueDoctor => 1.25,
286            Class::Druid => 1.0 / 3.0,
287            Class::Bard => 1.125,
288            Class::Necromancer => 5.0 / 9.0,
289            Class::Paladin => 0.833,
290            _ => 1.0,
291        }
292    }
293
294    #[must_use]
295    pub fn can_wear_shield(self) -> bool {
296        matches!(self, Self::Paladin | Self::Warrior)
297    }
298}
299
300#[derive(Debug, PartialEq, Eq, Default, Clone, Copy, FromPrimitive, Hash)]
301#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
302#[allow(missing_docs)]
303pub enum Race {
304    #[default]
305    Human = 1,
306    Elf,
307    Dwarf,
308    Gnome,
309    Orc,
310    DarkElf,
311    Goblin,
312    Demon,
313}
314
315impl Race {
316    /// These are the boni the game claims to give to certain races. As far as I
317    /// can tell though, these are actually irrellevant. Changing the race mid
318    /// game does nothing and the calcs without it are linig up perfectly. That
319    /// means these values here have no reason to exist
320    #[must_use]
321    pub fn stat_modifiers(self) -> EnumMap<AttributeType, i32> {
322        let raw = match self {
323            Race::Human => [0, 0, 0, 0, 0],
324            Race::Elf => [-1, 2, 0, -1, 0],
325            Race::Dwarf => [0, -2, -1, 2, 1],
326            Race::Gnome => [-2, 3, -1, -1, 1],
327            Race::Orc => [1, 0, -1, 0, 0],
328            Race::DarkElf => [-2, 2, 1, -1, 0],
329            Race::Goblin => [-2, 2, 0, -1, 1],
330            Race::Demon => [3, -1, 0, 1, -3],
331        };
332        EnumMap::from_array(raw)
333    }
334}
335
336#[derive(Debug, Copy, Clone, FromPrimitive, PartialEq, Eq, Hash)]
337#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
338#[allow(missing_docs)]
339pub enum Mount {
340    Cow = 1,
341    Horse = 2,
342    Tiger = 3,
343    Dragon = 4,
344}
345
346impl Mount {
347    /// Returns the cost of this mount
348    #[must_use]
349    pub fn cost(&self) -> NormalCost {
350        match self {
351            Mount::Cow => NormalCost {
352                silver: 100,
353                mushrooms: 0,
354            },
355            Mount::Horse => NormalCost {
356                silver: 500,
357                mushrooms: 0,
358            },
359            Mount::Tiger => NormalCost {
360                silver: 1000,
361                mushrooms: 1,
362            },
363            Mount::Dragon => NormalCost {
364                silver: 0,
365                mushrooms: 25,
366            },
367        }
368    }
369}