1#![allow(
2 clippy::cast_possible_wrap,
3 clippy::cast_sign_loss,
4 clippy::cast_precision_loss,
5 clippy::cast_possible_truncation
6)]
7use enum_map::{Enum, EnumMap};
8use fastrand::Rng;
9use strum::{EnumIter, IntoEnumIterator};
10
11use crate::{
12 command::AttributeType,
13 gamestate::{
14 character::Class, dungeons::CompanionClass, items::*, GameState,
15 },
16 misc::EnumMapGet,
17};
18
19pub mod constants;
20
21use BattleEvent as BE;
22
23#[derive(Debug, Clone)]
24pub struct UpgradeableFighter {
25 is_companion: bool,
26 level: u16,
27 class: Class,
28 pub attribute_basis: EnumMap<AttributeType, u32>,
30 _attributes_bought: EnumMap<AttributeType, u32>,
31 pet_attribute_bonus_perc: EnumMap<AttributeType, f64>,
32
33 equipment: Equipment,
34 active_potions: [Option<Potion>; 3],
35 portal_hp_bonus: u32,
38 portal_dmg_bonus: u32,
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum Minion {
44 Skeleton { revived: u8 },
45 Hound,
46 Golem,
47}
48
49#[derive(Debug, Clone)]
50pub struct BattleFighter {
51 pub is_companion: bool,
52 pub level: u16,
53 pub class: Class,
54 pub attributes: EnumMap<AttributeType, u32>,
55 pub max_hp: i64,
56 pub current_hp: i64,
57 pub equip: EquipmentEffects,
58 pub portal_dmg_bonus: f64,
59 pub rounds_in_battle: u32,
60 pub class_effect: ClassEffect,
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub enum HarpQuality {
65 Bad,
66 Medium,
67 Good,
68}
69
70fn calc_unarmed_base_dmg(
73 slot: EquipmentSlot,
74 level: u16,
75 class: Class,
76) -> (u32, u32) {
77 if level <= 10 {
78 return (1, 2);
79 }
80 let dmg_level = f64::from(level - 9);
81 let multiplier = match class {
82 Class::Assassin if slot == EquipmentSlot::Weapon => 1.25,
83 Class::Assassin => 0.875,
84 _ => 0.7,
85 };
86
87 let base = dmg_level * multiplier * class.weapon_multiplier();
88 let min = ((base * 2.0) / 3.0).trunc().max(1.0);
89 let max = ((base * 4.0) / 3.0).trunc().max(2.0);
90 (min as u32, max as u32)
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum ClassEffect {
95 Druid {
96 bear: bool,
98 swoops: u8,
100 },
101 Bard {
102 quality: HarpQuality,
103 remaining: u8,
104 },
105 Necromancer {
106 typ: Minion,
107 remaining: u8,
108 },
109 DemonHunter {
110 revived: u8,
111 },
112 Normal,
113}
114
115#[derive(Debug, Clone, Copy, PartialEq, Eq)]
116pub enum AttackType {
117 Weapon,
118 Offhand,
119 Swoop,
120 Minion,
121}
122
123impl ClassEffect {
124 fn druid_swoops(self) -> u8 {
125 match self {
126 ClassEffect::Druid { swoops, .. } => swoops,
127 _ => 0,
128 }
129 }
130}
131
132impl BattleFighter {
133 #[must_use]
134 pub fn from_monster(monster: &Monster) -> Self {
135 let weapon = calc_unarmed_base_dmg(
137 EquipmentSlot::Weapon,
138 monster.level,
139 monster.class,
140 );
141
142 Self {
143 is_companion: false,
144 level: monster.level,
145 class: monster.class,
146 attributes: monster.attributes,
147 max_hp: monster.hp as i64,
148 current_hp: monster.hp as i64,
149 equip: EquipmentEffects {
150 element_res: EnumMap::default(),
151 element_dmg: EnumMap::default(),
152 weapon,
153 offhand: (0, 0),
154 reaction_boost: false,
155 extra_crit_dmg: false,
156 armor: 0,
157 },
158 portal_dmg_bonus: 1.0,
159 rounds_in_battle: 0,
160 class_effect: ClassEffect::Normal,
161 }
162 }
163
164 #[must_use]
165 pub fn from_upgradeable(char: &UpgradeableFighter) -> Self {
166 let attributes = char.attributes();
167 let hp = char.hit_points(&attributes);
168
169 let mut equip = EquipmentEffects {
170 element_res: EnumMap::default(),
171 element_dmg: EnumMap::default(),
172 reaction_boost: false,
173 extra_crit_dmg: false,
174 armor: 0,
175 weapon: (0, 0),
176 offhand: (0, 0),
177 };
178
179 for (slot, item) in &char.equipment.0 {
180 let Some(item) = item else {
181 match slot {
182 EquipmentSlot::Weapon => {
183 equip.weapon =
184 calc_unarmed_base_dmg(slot, char.level, char.class);
185 }
186 EquipmentSlot::Shield if char.class == Class::Assassin => {
187 equip.offhand =
188 calc_unarmed_base_dmg(slot, char.level, char.class);
189 }
190 _ => {}
191 }
192 continue;
193 };
194 equip.armor += item.armor();
195 match item.enchantment {
196 Some(Enchantment::SwordOfVengeance) => {
197 equip.extra_crit_dmg = true;
198 }
199 Some(Enchantment::ShadowOfTheCowboy) => {
200 equip.reaction_boost = true;
201 }
202 _ => {}
203 };
204 if let Some(rune) = item.rune {
205 use RuneType as RT;
206
207 let mut apply = |is_res, element| {
208 let target = if is_res {
209 &mut equip.element_res
210 } else {
211 &mut equip.element_dmg
212 };
213 *target.get_mut(element) += f64::from(rune.value) / 100.0;
214 };
215 match rune.typ {
216 RT::FireResistance => apply(true, Element::Fire),
217 RT::ColdResistence => apply(true, Element::Cold),
218 RT::LightningResistance => apply(true, Element::Lightning),
219 RT::TotalResistence => {
220 for (_, val) in &mut equip.element_res {
221 *val += f64::from(rune.value) / 100.0;
222 }
223 }
224 RT::FireDamage => apply(false, Element::Fire),
225 RT::ColdDamage => apply(false, Element::Cold),
226 RT::LightningDamage => apply(false, Element::Lightning),
227 _ => {}
228 }
229 }
230
231 match item.typ {
232 ItemType::Weapon { min_dmg, max_dmg } => match slot {
233 EquipmentSlot::Weapon => equip.weapon = (min_dmg, max_dmg),
234 EquipmentSlot::Shield => equip.offhand = (min_dmg, max_dmg),
235 _ => {}
236 },
237 ItemType::Shield { block_chance } => {
238 equip.offhand = (block_chance, 0);
239 }
240 _ => (),
241 }
242 }
243
244 let portal_dmg_bonus = 1.0 + f64::from(char.portal_dmg_bonus) / 100.0;
245
246 BattleFighter {
247 is_companion: char.is_companion,
248 class: char.class,
249 attributes,
250 max_hp: hp,
251 current_hp: hp,
252 equip,
253 rounds_in_battle: 0,
254 class_effect: ClassEffect::Normal,
255 portal_dmg_bonus,
256 level: char.level,
257 }
258 }
259
260 #[must_use]
261 pub fn from_squad(squad: &PlayerFighterSquad) -> Vec<Self> {
262 let mut res = if let Some(comps) = &squad.companions {
263 let mut res = Vec::with_capacity(4);
264 for comp in comps.as_array() {
265 res.push(Self::from_upgradeable(comp));
266 }
267 res
268 } else {
269 Vec::with_capacity(1)
270 };
271 res.push(BattleFighter::from_upgradeable(&squad.character));
272 res
273 }
274
275 pub fn reset(&mut self) {
276 self.class_effect = ClassEffect::Normal;
277 self.current_hp = self.max_hp;
278 self.rounds_in_battle = 0;
279 }
280}
281
282#[derive(Debug, Clone)]
283pub struct EquipmentEffects {
284 element_res: EnumMap<Element, f64>,
285 element_dmg: EnumMap<Element, f64>,
286
287 weapon: (u32, u32),
288 offhand: (u32, u32),
290
291 reaction_boost: bool,
293 extra_crit_dmg: bool,
295
296 armor: u32,
297}
298
299#[derive(Debug, Clone, Copy, Enum, EnumIter)]
300pub enum Element {
301 Lightning,
302 Cold,
303 Fire,
304}
305
306#[derive(Debug)]
307pub struct BattleTeam<'a> {
308 current_fighter: usize,
309 fighters: &'a mut [BattleFighter],
310}
311
312#[allow(clippy::extra_unused_lifetimes)]
313impl<'a> BattleTeam<'_> {
314 #[must_use]
315 pub fn current(&self) -> Option<&BattleFighter> {
316 self.fighters.get(self.current_fighter)
317 }
318 #[must_use]
319 pub fn current_mut(&mut self) -> Option<&mut BattleFighter> {
320 self.fighters.get_mut(self.current_fighter)
321 }
322
323 fn reset(&mut self) {
324 self.current_fighter = 0;
325 for fighter in self.fighters.iter_mut() {
326 fighter.reset();
327 }
328 }
329}
330
331#[derive(Debug, Clone, Copy, PartialEq, Eq, Enum)]
332pub enum BattleSide {
333 Left,
334 Right,
335}
336
337#[derive(Debug)]
338pub struct Battle<'a> {
339 pub round: u32,
340 pub started: Option<BattleSide>,
341 pub left: BattleTeam<'a>,
342 pub right: BattleTeam<'a>,
343 pub rng: Rng,
344}
345
346impl<'a> Battle<'a> {
347 pub fn new(
348 left: &'a mut [BattleFighter],
349 right: &'a mut [BattleFighter],
350 ) -> Self {
351 Self {
352 round: 0,
353 started: None,
354 left: BattleTeam {
355 current_fighter: 0,
356 fighters: left,
357 },
358 right: BattleTeam {
359 current_fighter: 0,
360 fighters: right,
361 },
362 rng: fastrand::Rng::default(),
363 }
364 }
365
366 pub fn simulate(&mut self, logger: &mut impl BattleLogger) -> BattleSide {
368 self.reset();
369 loop {
370 if let Some(winner) = self.simulate_turn(logger) {
371 return winner;
372 }
373 }
374 }
375
376 pub fn reset(&mut self) {
377 self.round = 0;
378 self.left.reset();
379 self.right.reset();
380 self.started = None;
381 }
382
383 pub fn simulate_turn(
387 &mut self,
388 logger: &mut impl BattleLogger,
389 ) -> Option<BattleSide> {
390 use AttackType::{Offhand, Swoop, Weapon};
391 use BattleSide::{Left, Right};
392 use Class::{
393 Assassin, Bard, BattleMage, Berserker, DemonHunter, Druid, Mage,
394 Necromancer, Paladin, Scout, Warrior,
395 };
396
397 logger.log(BE::TurnUpdate(self));
398
399 let Some(left) = self.left.current_mut() else {
400 logger.log(BE::BattleEnd(self, Right));
401 return Some(Right);
402 };
403 let Some(right) = self.right.current_mut() else {
404 logger.log(BE::BattleEnd(self, Left));
405 return Some(Left);
406 };
407
408 self.round += 1;
409 left.rounds_in_battle += 1;
410 right.rounds_in_battle += 1;
411
412 let attacking_side = if let Some(started) = self.started {
413 let one_vs_one_round =
414 left.rounds_in_battle.min(right.rounds_in_battle);
415
416 match started {
419 _ if one_vs_one_round % 2 == 1 => started,
420 Left => Right,
421 Right => Left,
422 }
423 } else {
424 let attacking_side =
426 match (right.equip.reaction_boost, left.equip.reaction_boost) {
427 (true, true) | (false, false) if self.rng.bool() => Right,
428 (true, false) => Right,
429 _ => Left,
430 };
431 self.started = Some(attacking_side);
432 attacking_side
433 };
434
435 let (attacker, defender) = match attacking_side {
436 Left => (left, right),
437 Right => (right, left),
438 };
439
440 let turn = self.round;
441 let rng = &mut self.rng;
442 match attacker.class {
443 Paladin => {
444 attack(attacker, defender, rng, Weapon, turn, logger);
446 }
447 Warrior | Scout | Mage | DemonHunter => {
448 attack(attacker, defender, rng, Weapon, turn, logger);
449 }
450 Assassin => {
451 attack(attacker, defender, rng, Weapon, turn, logger);
452 attack(attacker, defender, rng, Offhand, turn, logger);
453 }
454 Berserker => {
455 for _ in 0..15 {
456 attack(attacker, defender, rng, Weapon, turn, logger);
457 if defender.current_hp <= 0 || rng.bool() {
458 break;
459 }
460 }
461 }
462 BattleMage => {
463 if attacker.rounds_in_battle == 1 {
464 if defender.class == Mage {
465 logger.log(BE::CometRepelled(attacker, defender));
466 } else {
467 let dmg = match defender.class {
468 Mage => 0,
469 Bard => attacker.max_hp / 10,
470 Scout | Assassin | Berserker | Necromancer
471 | DemonHunter => attacker.max_hp / 5,
472 Warrior | BattleMage | Druid => attacker.max_hp / 4,
473 Paladin => (attacker.max_hp as f64 / (10.0 / 3.0))
474 .trunc()
475 as i64,
476 };
477 let dmg = dmg.min(defender.max_hp / 3);
478 logger.log(BE::CometAttack(attacker, defender));
479 do_damage(attacker, defender, dmg, rng, logger);
481 }
482 }
483 attack(attacker, defender, rng, Weapon, turn, logger);
484 }
485 Druid => {
486 if !matches!(
488 attacker.class_effect,
489 ClassEffect::Druid { bear: true, .. }
490 ) {
491 let swoops = attacker.class_effect.druid_swoops();
492 let swoop_chance =
493 0.15 + ((f32::from(swoops) * 5.0) / 100.0);
494 if defender.class != Class::Mage
495 && rng.f32() <= swoop_chance
496 {
497 attack(attacker, defender, rng, Swoop, turn, logger);
498 attacker.class_effect = ClassEffect::Druid {
499 bear: false,
500 swoops: (swoops + 1).min(7),
502 }
503 }
504 }
505
506 attack(attacker, defender, rng, Weapon, turn, logger);
507 attacker.class_effect = ClassEffect::Druid {
510 bear: false,
511 swoops: attacker.class_effect.druid_swoops(),
512 };
513 }
514 Bard => {
515 if attacker.rounds_in_battle % 4 == 0 {
517 let quality = rng.u8(0..4);
518 let (quality, remaining) = match quality {
519 0 => (HarpQuality::Bad, 3),
520 1 | 2 => (HarpQuality::Medium, 3),
521 _ => (HarpQuality::Good, 4),
522 };
523 attacker.class_effect =
524 ClassEffect::Bard { quality, remaining };
525 logger.log(BE::BardPlay(attacker, defender, quality));
526 }
527 attack(attacker, defender, rng, Weapon, turn, logger);
528 if let ClassEffect::Bard { remaining, .. } =
529 &mut attacker.class_effect
530 {
531 *remaining = remaining.saturating_sub(1);
532 }
533 }
534 Necromancer => {
535 let has_minion = matches!(
536 attacker.class_effect,
537 ClassEffect::Necromancer { remaining: 1.., .. }
538 );
539 if !has_minion && defender.class != Class::Mage && rng.bool() {
540 let (typ, rem) = match rng.u8(0..3) {
541 0 => (Minion::Skeleton { revived: 0 }, 3),
542 1 => (Minion::Hound, 2),
543 _ => (Minion::Golem, 4),
544 };
545 attacker.class_effect = ClassEffect::Necromancer {
546 typ,
547 remaining: rem,
548 };
549 logger.log(BE::MinionSpawned(attacker, defender, typ));
550 attack(
551 attacker,
552 defender,
553 rng,
554 AttackType::Minion,
555 turn,
556 logger,
557 );
558 } else {
559 if has_minion {
560 attack(
561 attacker,
562 defender,
563 rng,
564 AttackType::Minion,
565 turn,
566 logger,
567 );
568 }
569 attack(attacker, defender, rng, Weapon, turn, logger);
570 }
571 if let ClassEffect::Necromancer { remaining, typ } =
572 &mut attacker.class_effect
573 {
574 if *remaining > 0 {
575 let mut has_revived = false;
576 if let Minion::Skeleton { revived } = typ {
577 if *revived < 2 && self.rng.bool() {
578 *revived += 1;
579 has_revived = true;
580 }
581 }
582 if has_revived {
583 *remaining = 1;
585 logger.log(BE::MinionSkeletonRevived(
586 attacker, defender,
587 ));
588 } else {
589 *remaining -= 1;
590 }
591 }
592 }
593 }
594 }
595 if defender.current_hp <= 0 {
596 match attacking_side {
597 Left => {
598 self.right.current_fighter += 1;
599 logger.log(BE::FighterDefeat(self, Right));
600 }
601 Right => {
602 self.left.current_fighter += 1;
603 logger.log(BE::FighterDefeat(self, Left));
604 }
605 }
606 }
607 None
608 }
609}
610
611fn do_damage(
614 from: &mut BattleFighter,
615 to: &mut BattleFighter,
616 damage: i64,
617 rng: &mut Rng,
618 logger: &mut impl BattleLogger,
619) {
620 to.current_hp -= damage;
621 logger.log(BE::DamageReceived(from, to, damage));
622
623 if to.current_hp > 0 {
624 return;
625 }
626 let ClassEffect::DemonHunter { revived } = &mut to.class_effect else {
627 return;
628 };
629 let (chance, hp_restore) = match revived {
630 0 => (0.44, 0.9),
631 1 => (0.33, 0.8),
632 2 => (0.22, 0.7),
633 3 => (0.11, 0.6),
634 _ => return,
635 };
636
637 if rng.f32() >= chance {
638 return;
639 }
640
641 to.current_hp = (hp_restore * to.max_hp as f64) as i64;
643 *revived += 1;
644 logger.log(BE::DemonHunterRevived(from, to));
645}
646
647fn attack(
648 attacker: &mut BattleFighter,
649 defender: &mut BattleFighter,
650 rng: &mut Rng,
651 typ: AttackType,
652 turn: u32,
653 logger: &mut impl BattleLogger,
654) {
655 if defender.current_hp <= 0 {
656 return;
658 }
659
660 logger.log(BE::Attack(attacker, defender, typ));
661 if attacker.class != Class::Mage {
663 if defender.class == Class::Scout && rng.bool() {
665 if defender.class == Class::Druid {
667 defender.class_effect = ClassEffect::Druid {
670 bear: true,
671 swoops: defender.class_effect.druid_swoops(),
672 };
673 }
674 logger.log(BE::Dodged(attacker, defender));
675 return;
676 }
677 if defender.class == Class::Warrior
678 && !defender.is_companion
679 && defender.equip.offhand.0 as f32 / 100.0 > rng.f32()
680 {
681 logger.log(BE::Blocked(attacker, defender));
683 return;
684 }
685 }
686
687 let char_damage_modifier = 1.0
690 + f64::from(*attacker.attributes.get(attacker.class.main_attribute()))
691 / 10.0;
692
693 let mut elemental_bonus = 1.0;
694 for element in Element::iter() {
695 let plus = attacker.equip.element_dmg.get(element);
696 let minus = defender.equip.element_dmg.get(element);
697
698 if plus > minus {
699 elemental_bonus += plus - minus;
700 }
701 }
702
703 let armor = f64::from(defender.equip.armor) * defender.class.armor_factor();
704 let max_dr = defender.class.max_damage_reduction();
705 let armor_damage_effect = if attacker.class == Class::Mage {
707 1.0
708 } else {
709 1.0 - (armor / f64::from(attacker.level)).min(max_dr)
710 };
711
712 let class_effect_dmg_bonus = match attacker.class_effect {
714 ClassEffect::Bard { quality, .. } if defender.class != Class::Mage => {
715 match quality {
716 HarpQuality::Bad => 1.2,
717 HarpQuality::Medium => 1.4,
718 HarpQuality::Good => 1.6,
719 }
720 }
721 ClassEffect::Necromancer {
722 typ: minion_type,
723 remaining: 1..,
724 } if typ == AttackType::Minion => match minion_type {
725 Minion::Skeleton { .. } => 1.25,
726 Minion::Hound => 2.0,
727 Minion::Golem => 1.0,
728 },
729 ClassEffect::Druid { .. } if typ == AttackType::Swoop => 1.8,
730 _ => 1.0,
731 };
732
733 let rage_bonus = 1.0 * (f64::from(turn.saturating_sub(1)) / 6.0);
735
736 let damage_bonus = char_damage_modifier
737 * attacker.portal_dmg_bonus
738 * elemental_bonus
739 * armor_damage_effect
740 * attacker.class.damage_factor(defender.class)
741 * rage_bonus
742 * class_effect_dmg_bonus;
743
744 let weapon = match typ {
746 AttackType::Offhand => attacker.equip.offhand,
747 _ => attacker.equip.weapon,
748 };
749
750 let calc_damage =
751 |weapon_dmg| (f64::from(weapon_dmg) * damage_bonus).trunc() as i64;
752
753 let min_base_damage = calc_damage(weapon.0);
754 let max_base_damage = calc_damage(weapon.1);
755
756 let mut damage = rng.i64(min_base_damage..=max_base_damage);
757
758 let luck_mod = attacker.attributes.get(AttributeType::Luck) * 5;
761 let raw_crit_chance = f64::from(luck_mod) / f64::from(defender.level);
762 let mut crit_chance = raw_crit_chance.min(0.5);
763 let mut crit_dmg_factor = 2.0;
764
765 match attacker.class_effect {
766 ClassEffect::Druid { bear: true, .. } => {
767 crit_chance += 0.1;
768 crit_dmg_factor += 2.0;
769 }
770 ClassEffect::Necromancer {
771 typ: Minion::Hound, ..
772 } => {
773 crit_chance += 0.1;
774 crit_dmg_factor += 0.5;
775 }
776 _ => {}
777 }
778
779 if rng.f64() <= crit_chance {
780 if attacker.equip.extra_crit_dmg {
781 crit_dmg_factor += 0.05;
782 };
783 logger.log(BE::Crit(attacker, defender));
784 damage = (damage as f64 * crit_dmg_factor) as i64;
785 }
786
787 do_damage(attacker, defender, damage, rng, logger);
788}
789
790#[derive(Debug)]
791pub struct PlayerFighterSquad {
792 pub character: UpgradeableFighter,
793 pub companions: Option<EnumMap<CompanionClass, UpgradeableFighter>>,
794}
795
796impl PlayerFighterSquad {
797 #[must_use]
798 pub fn new(gs: &GameState) -> PlayerFighterSquad {
799 let mut pet_attribute_bonus_perc = EnumMap::default();
800 if let Some(pets) = &gs.pets {
801 for (typ, info) in &pets.habitats {
802 let mut total_bonus = 0;
803 for pet in &info.pets {
804 total_bonus += match pet.level {
805 0 => 0,
806 1..100 => 100,
807 100..150 => 150,
808 150..200 => 175,
809 200.. => 200,
810 };
811 }
812 *pet_attribute_bonus_perc.get_mut(typ.into()) =
813 f64::from(total_bonus / 100) / 100.0;
814 }
815 };
816 let portal_hp_bonus = gs
817 .dungeons
818 .portal
819 .as_ref()
820 .map(|a| a.player_hp_bonus)
821 .unwrap_or_default()
822 .into();
823 let portal_dmg_bonus = gs
824 .guild
825 .as_ref()
826 .map(|a| a.portal.damage_bonus)
827 .unwrap_or_default()
828 .into();
829
830 let char = &gs.character;
831 let character = UpgradeableFighter {
832 is_companion: false,
833 level: char.level,
834 class: char.class,
835 attribute_basis: char.attribute_basis,
836 _attributes_bought: char.attribute_times_bought,
837 equipment: char.equipment.clone(),
838 active_potions: char.active_potions,
839 pet_attribute_bonus_perc,
840 portal_hp_bonus,
841 portal_dmg_bonus,
842 };
843 let mut companions = None;
844 if let Some(comps) = &gs.dungeons.companions {
845 let classes = [
846 CompanionClass::Warrior,
847 CompanionClass::Mage,
848 CompanionClass::Scout,
849 ];
850
851 let res = classes.map(|class| {
852 let comp = comps.get(class);
853 UpgradeableFighter {
854 is_companion: true,
855 level: comp.level.try_into().unwrap_or(1),
856 class: class.into(),
857 attribute_basis: comp.attributes,
858 _attributes_bought: EnumMap::default(),
859 equipment: comp.equipment.clone(),
860 active_potions: char.active_potions,
861 pet_attribute_bonus_perc,
862 portal_hp_bonus,
863 portal_dmg_bonus,
864 }
865 });
866 companions = Some(EnumMap::from_array(res));
867 }
868
869 PlayerFighterSquad {
870 character,
871 companions,
872 }
873 }
874}
875
876impl UpgradeableFighter {
877 #[must_use]
878 pub fn attributes(&self) -> EnumMap<AttributeType, u32> {
879 let mut total = EnumMap::default();
880
881 for equip in self.equipment.0.iter().flat_map(|a| a.1) {
882 for (k, v) in &equip.attributes {
883 *total.get_mut(k) += v;
884 }
885
886 if let Some(GemSlot::Filled(gem)) = &equip.gem_slot {
887 use AttributeType as AT;
888 let mut value = gem.value;
889 if matches!(equip.typ, ItemType::Weapon { .. })
890 && !self.is_companion
891 {
892 value *= 2;
893 }
894
895 let mut add_atr = |at| *total.get_mut(at) += value;
896 match gem.typ {
897 GemType::Strength => add_atr(AT::Strength),
898 GemType::Dexterity => add_atr(AT::Dexterity),
899 GemType::Intelligence => add_atr(AT::Intelligence),
900 GemType::Constitution => add_atr(AT::Constitution),
901 GemType::Luck => add_atr(AT::Luck),
902 GemType::All => {
903 total.iter_mut().for_each(|a| *a.1 += value);
904 }
905 GemType::Legendary => {
906 add_atr(AT::Constitution);
907 add_atr(self.class.main_attribute());
908 }
909 }
910 }
911 }
912
913 let class_bonus: f64 = match self.class {
914 Class::BattleMage => 0.1111,
915 _ => 0.0,
916 };
917
918 let pet_boni = self.pet_attribute_bonus_perc;
919
920 for (k, v) in &mut total {
921 let class_bonus = (f64::from(*v) * class_bonus).trunc() as u32;
922 *v += class_bonus + self.attribute_basis.get(k);
923 if let Some(potion) = self
924 .active_potions
925 .iter()
926 .flatten()
927 .find(|a| a.typ == k.into())
928 {
929 *v += (f64::from(*v) * potion.size.effect()) as u32;
930 }
931
932 let pet_bonus = (f64::from(*v) * (*pet_boni.get(k))).trunc() as u32;
933 *v += pet_bonus;
934 }
935 total
936 }
937
938 #[must_use]
939 #[allow(clippy::enum_glob_use)]
940 pub fn hit_points(&self, attributes: &EnumMap<AttributeType, u32>) -> i64 {
941 use Class::*;
942
943 let mut total = i64::from(*attributes.get(AttributeType::Constitution));
944 total = (total as f64
945 * match self.class {
946 Warrior if self.is_companion => 6.1,
947 Paladin => 6.0,
948 Warrior | BattleMage | Druid => 5.0,
949 Scout | Assassin | Berserker | DemonHunter | Necromancer => 4.0,
950 Mage | Bard => 2.0,
951 })
952 .trunc() as i64;
953
954 total *= i64::from(self.level) + 1;
955
956 if self
957 .active_potions
958 .iter()
959 .flatten()
960 .any(|a| a.typ == PotionType::EternalLife)
961 {
962 total = (total as f64 * 1.25).trunc() as i64;
963 }
964
965 let portal_bonus = (total as f64
966 * (f64::from(self.portal_hp_bonus) / 100.0))
967 .trunc() as i64;
968
969 total += portal_bonus;
970
971 let mut rune_multi = 0;
972 for rune in self
973 .equipment
974 .0
975 .iter()
976 .flat_map(|a| a.1)
977 .filter_map(|a| a.rune)
978 {
979 if rune.typ == RuneType::ExtraHitPoints {
980 rune_multi += u32::from(rune.value);
981 }
982 }
983
984 let rune_bonus =
985 (total as f64 * (f64::from(rune_multi) / 100.0)).trunc() as i64;
986
987 total += rune_bonus;
988 total
989 }
990}
991
992#[derive(Debug, Clone, PartialEq, Eq)]
993pub struct Monster {
994 pub level: u16,
995 pub class: Class,
996 pub attributes: EnumMap<AttributeType, u32>,
997 pub hp: u64,
998 pub xp: u32,
999}
1000
1001impl Monster {
1002 #[must_use]
1003 pub const fn new(
1004 level: u16,
1005 class: Class,
1006 attribs: [u32; 5],
1007 hp: u64,
1008 xp: u32,
1009 ) -> Self {
1010 Monster {
1011 level,
1012 class,
1013 attributes: EnumMap::from_array(attribs),
1014 hp,
1015 xp,
1016 }
1017 }
1018}
1019
1020#[derive(Debug)]
1021#[non_exhaustive]
1022pub enum BattleEvent<'a, 'b> {
1023 TurnUpdate(&'a Battle<'b>),
1024 BattleEnd(&'a Battle<'b>, BattleSide),
1025 Attack(&'b BattleFighter, &'b BattleFighter, AttackType),
1026 Dodged(&'b BattleFighter, &'b BattleFighter),
1027 Blocked(&'b BattleFighter, &'b BattleFighter),
1028 Crit(&'b BattleFighter, &'b BattleFighter),
1029 DamageReceived(&'b BattleFighter, &'b BattleFighter, i64),
1030 DemonHunterRevived(&'b BattleFighter, &'b BattleFighter),
1031 CometRepelled(&'b BattleFighter, &'b BattleFighter),
1032 CometAttack(&'b BattleFighter, &'b BattleFighter),
1033 MinionSpawned(&'b BattleFighter, &'b BattleFighter, Minion),
1034 MinionSkeletonRevived(&'b BattleFighter, &'b BattleFighter),
1035 BardPlay(&'b BattleFighter, &'b BattleFighter, HarpQuality),
1036 FighterDefeat(&'a Battle<'b>, BattleSide),
1037}
1038
1039pub trait BattleLogger {
1040 fn log(&mut self, event: BattleEvent<'_, '_>);
1041}
1042
1043impl BattleLogger for () {
1044 fn log(&mut self, _event: BattleEvent<'_, '_>) {
1045 }
1046}