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#[derive(Debug, Clone, Default)]
14#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
15pub 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 mannequin: 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 scrapbook: Option<ScrapBook>,
88
89 pub relations: Vec<RelationEntry>,
92}
93
94#[derive(Debug, Default, Clone, PartialEq, Eq)]
98#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
99#[allow(missing_docs)]
100pub struct Portrait {
101 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 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 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 #[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 #[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}