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 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 pub portal_hp_bonus: u32,
29 pub portal_dmg_bonus: u32,
31 pub gladiator: u32,
33}
34impl UpgradeableFighter {
35 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 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 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 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 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 pub fn unequip(&mut self, slot: EquipmentSlot) -> Option<Item> {
133 self.equipment.0.get_mut(slot).take()
134 }
135
136 #[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 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}