1use std::fmt::Debug;
2
3use chrono::{DateTime, Local};
4use enum_map::EnumMap;
5use num_derive::FromPrimitive;
6use num_traits::FromPrimitive;
7
8use super::{Mirror, NormalCost, RelationEntry, SFError, ScrapBook};
9use crate::{PlayerId, command::*, gamestate::items::*, misc::*};
10
11#[derive(Debug, Clone, Default)]
12#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
13pub struct Character {
16 pub player_id: PlayerId,
20 pub name: String,
22 pub level: u16,
24 pub silver: u64,
26 pub mushrooms: u32,
28
29 pub class: Class,
31
32 pub race: Race,
35 pub portrait: Portrait,
37 pub description: String,
39
40 pub experience: u64,
42 pub next_level_xp: u64,
45 pub honor: u32,
47 pub rank: u32,
49
50 pub inventory: Inventory,
53 pub equipment: Equipment,
55
56 pub manequin: Option<Equipment>,
59 pub active_potions: [Option<Potion>; 3],
61
62 pub armor: u64,
64
65 pub min_damage: u32,
67 pub max_damage: u32,
69
70 pub attribute_basis: EnumMap<AttributeType, u32>,
72 pub attribute_additions: EnumMap<AttributeType, u32>,
74 pub attribute_times_bought: EnumMap<AttributeType, u32>,
77
78 pub mount: Option<Mount>,
80 pub mount_end: Option<DateTime<Local>>,
83 pub mount_dragon_refund: u64,
85
86 pub mirror: Mirror,
89 pub scrapbook: Option<ScrapBook>,
91
92 pub relations: Vec<RelationEntry>,
95}
96
97#[derive(Debug, Default, Clone, PartialEq, Eq)]
101#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
102#[allow(missing_docs)]
103pub struct Portrait {
104 pub gender: Gender,
106 pub hair_color: u8,
107 pub hair: u8,
108 pub mouth: u8,
109 pub brows: u8,
110 pub eyes: u8,
111 pub beards: u8,
112 pub nose: u8,
113 pub ears: u8,
114 pub extra: u8,
115 pub horns: u8,
116 pub special_portrait: i64,
118}
119
120impl Portrait {
121 pub(crate) fn parse(data: &[i64]) -> Result<Portrait, SFError> {
122 Ok(Self {
123 mouth: data.csiget(0, "mouth", 1)?,
124 hair_color: data.csimget(1, "hair color", 100, |a| a / 100)?,
125 hair: data.csimget(1, "hair", 1, |a| a % 100)?,
126 brows: data.csimget(2, "brows", 1, |a| a % 100)?,
127 eyes: data.csiget(3, "eyes", 1)?,
128 beards: data.csimget(4, "beards", 1, |a| a % 100)?,
129 nose: data.csiget(5, "nose", 1)?,
130 ears: data.csiget(6, "ears", 1)?,
131 extra: data.csiget(7, "extra", 1)?,
132 horns: data.csimget(8, "horns", 1, |a| a % 100)?,
133 special_portrait: data.cget(9, "special portrait")?,
134 gender: Gender::from_i64(data.csimget(11, "gender", 1, |a| a % 2)?)
135 .unwrap_or_default(),
136 })
137 }
138}
139
140#[derive(Debug, Clone, Default, Copy, PartialEq, Eq, FromPrimitive, Hash)]
141#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
142#[allow(missing_docs)]
143pub enum Gender {
144 #[default]
145 Female = 0,
146 Male,
147}
148
149#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, FromPrimitive, Hash)]
150#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
151#[allow(missing_docs)]
152pub enum Class {
153 #[default]
154 Warrior = 0,
155 Mage,
156 Scout,
157 Assassin,
158 BattleMage,
159 Berserker,
160 DemonHunter,
161 Druid,
162 Bard,
163 Necromancer,
164 Paladin,
165 PlagueDoctor,
166}
167
168#[allow(clippy::enum_glob_use)]
169impl Class {
170 #[must_use]
171 #[allow(clippy::enum_glob_use)]
172 pub fn main_attribute(&self) -> AttributeType {
173 use Class::*;
174 match self {
175 Paladin | BattleMage | Berserker | Warrior => {
176 AttributeType::Strength
177 }
178 Assassin | DemonHunter | Scout | PlagueDoctor => {
179 AttributeType::Dexterity
180 }
181 Druid | Bard | Necromancer | Mage => AttributeType::Intelligence,
182 }
183 }
184
185 #[must_use]
186 pub(crate) fn weapon_multiplier(self) -> f64 {
187 use Class::*;
188 match self {
189 PlagueDoctor | Paladin | Warrior | Assassin | BattleMage
190 | Berserker => 2.0,
191 Scout => 2.5,
192 Mage | DemonHunter | Druid | Bard | Necromancer => 4.5,
193 }
194 }
195
196 #[must_use]
197 pub(crate) fn life_multiplier(self, is_companion: bool) -> f64 {
198 use Class::*;
199
200 match self {
201 Warrior if is_companion => 6.1,
202 Paladin => 6.0,
203 Warrior | BattleMage | Druid => 5.0,
204 PlagueDoctor | Scout | Assassin | Berserker | DemonHunter
205 | Necromancer => 4.0,
206 Mage | Bard => 2.0,
207 }
208 }
209
210 #[must_use]
211 pub(crate) fn armor_factor(self) -> f64 {
212 use Class::*;
213 match self {
214 Berserker => 0.5,
215 Paladin | Warrior | Mage | Scout | DemonHunter | Druid
216 | Assassin => 1.0,
217 Bard | Necromancer => 2.0,
218 PlagueDoctor => 2.5,
219 BattleMage => 5.0,
220 }
221 }
222
223 #[must_use]
224 pub(crate) fn max_damage_reduction(self) -> f64 {
225 use Class::*;
226 match self {
227 Bard | BattleMage | DemonHunter | Warrior => 0.5,
228 Paladin => 0.45,
229 PlagueDoctor | Druid | Assassin | Berserker | Scout => 0.25,
230 Necromancer => 0.2,
231 Mage => 0.1,
232 }
233 }
234
235 #[must_use]
236 pub fn can_wear_shield(self) -> bool {
237 matches!(self, Self::Paladin | Self::Warrior)
238 }
239
240 #[must_use]
241 pub(crate) fn damage_factor(self, against: Class) -> f64 {
242 use Class::*;
243 match self {
244 Druid if against == Class::DemonHunter => 0.33 + 0.15,
245 Druid if against == Class::Mage => 0.33 + 0.33,
246 Druid => 0.33,
247 Necromancer if against == Class::DemonHunter => 0.56 + 0.1,
248 Necromancer => 0.56,
249 Assassin => 0.625,
250 Paladin => 0.83,
251 Warrior | Mage | Scout | BattleMage | DemonHunter => 1.0,
252 Bard => 1.125,
253 Berserker | PlagueDoctor => 1.25,
254 }
255 }
256}
257
258#[derive(Debug, Clone, Copy, PartialEq, Eq, FromPrimitive, Hash)]
259#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
260#[allow(missing_docs)]
261pub enum DruidMask {
262 Cat = 4,
263 Bear = 5,
264}
265
266#[derive(Debug, Clone, Copy, PartialEq, Eq, FromPrimitive, Hash)]
267#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
268#[allow(missing_docs)]
269pub enum BardInstrument {
270 Harp = 1,
271 Lute,
272 Flute,
273}
274
275#[derive(Debug, PartialEq, Eq, Default, Clone, Copy, FromPrimitive, Hash)]
276#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
277#[allow(missing_docs)]
278pub enum Race {
279 #[default]
280 Human = 1,
281 Elf,
282 Dwarf,
283 Gnome,
284 Orc,
285 DarkElf,
286 Goblin,
287 Demon,
288}
289
290impl Race {
291 #[must_use]
292 pub fn stat_modifiers(self) -> EnumMap<AttributeType, i32> {
293 let raw = match self {
294 Race::Human => [0, 0, 0, 0, 0],
295 Race::Elf => [-1, 2, 0, -1, 0],
296 Race::Dwarf => [0, -2, -1, 2, 1],
297 Race::Gnome => [-2, 3, -1, -1, 1],
298 Race::Orc => [1, 0, -1, 0, 0],
299 Race::DarkElf => [-2, 2, 1, -1, 0],
300 Race::Goblin => [-2, 2, 0, -1, 1],
301 Race::Demon => [3, -1, 0, 1, -3],
302 };
303 EnumMap::from_array(raw)
304 }
305}
306
307#[derive(Debug, Copy, Clone, FromPrimitive, PartialEq, Eq, Hash)]
308#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
309#[allow(missing_docs)]
310pub enum Mount {
311 Cow = 1,
312 Horse = 2,
313 Tiger = 3,
314 Dragon = 4,
315}
316
317impl Mount {
318 #[must_use]
320 pub fn cost(&self) -> NormalCost {
321 match self {
322 Mount::Cow => NormalCost {
323 silver: 100,
324 mushrooms: 0,
325 },
326 Mount::Horse => NormalCost {
327 silver: 500,
328 mushrooms: 0,
329 },
330 Mount::Tiger => NormalCost {
331 silver: 1000,
332 mushrooms: 1,
333 },
334 Mount::Dragon => NormalCost {
335 silver: 0,
336 mushrooms: 25,
337 },
338 }
339 }
340}