1use super::{
2 character::Character,
3 dice::Dice,
4 item::{DamageResult, ItemProperty},
5};
6use crate::string::align_string;
7use serde::{Serialize, Deserialize};
8use std::cmp::max;
9
10#[derive(Debug, PartialEq, Serialize, Deserialize)]
11#[allow(unused)]
12pub enum HitResult {
13 Hit,
14 CriticalHit,
15 Miss,
16 TargetConcealed,
17 EpicDodged,
18}
19
20#[allow(unused)]
21impl HitResult {
22 pub fn is_missed(&self) -> bool {
23 match *self {
24 Self::Miss | Self::TargetConcealed | Self::EpicDodged => true,
25 _ => false,
26 }
27 }
28 pub fn is_crit(&self) -> bool {
29 if *self == Self::CriticalHit {
30 true
31 } else {
32 false
33 }
34 }
35}
36
37#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize)]
38pub enum AttackType {
39 MainHand,
40 OffHand,
41 Extra,
42}
43
44impl std::fmt::Display for AttackType {
45 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46 write!(
47 f,
48 "AttackType::{}",
49 match self {
50 Self::MainHand => "MainHand",
51 Self::OffHand => "OffHand",
52 Self::Extra => "Extra",
53 }
54 )
55 }
56}
57
58#[derive(Debug, PartialEq, Serialize, Deserialize)]
59pub struct AttackInfo {
60 pub ab: i32,
61 pub type_: AttackType,
62}
63
64impl AttackInfo {
65 pub fn new(ab: i32, type_: AttackType) -> Self {
66 Self { ab, type_ }
67 }
68}
69
70#[derive(Clone, Default, Debug, Serialize, Deserialize)]
71pub struct CombatStatistics {
72 pub total_hits: i64,
73 pub critical_hits: i64,
74 pub total_misses: i64,
75 pub concealed_attacks: i64,
76 pub epic_dodged_attacks: i64,
77 pub dmg_dealt: DamageResult,
78}
79
80impl CombatStatistics {
81 pub fn new() -> Self {
82 Self::default()
83 }
84
85 pub fn total_attacks(&self) -> i64 {
86 self.total_hits + self.total_misses
87 }
88}
89
90impl ToString for CombatStatistics {
91 fn to_string(&self) -> String {
92 let mut string_list: Vec<String> = vec![];
93
94 string_list.push(align_string(
95 "TOTAL ATTACK",
96 self.total_attacks().to_string(),
97 ));
98 string_list.push(align_string("TOTAL HIT", self.total_hits.to_string()));
99 string_list.push(align_string(
100 " * CRITICAL HIT",
101 self.critical_hits.to_string(),
102 ));
103 string_list.push("".into());
104 string_list.push(align_string("TOTAL MISS", self.total_misses.to_string()));
105 string_list.push(align_string(
106 " * CONCEALED",
107 self.concealed_attacks.to_string(),
108 ));
109 string_list.push(align_string(
110 " * EPIC DODGED",
111 self.epic_dodged_attacks.to_string(),
112 ));
113 string_list.push("".into());
114 string_list.push(align_string(
115 "TOTAL DAMAGE",
116 self.dmg_dealt.total_dmg().to_string(),
117 ));
118
119 for type_ in self.dmg_dealt.get_types_sorted() {
120 string_list.push(align_string(
121 format!(" * {}", type_.to_string().to_uppercase()).as_str(),
122 self.dmg_dealt.get(type_).to_string(),
123 ));
124 }
125
126 string_list.join("\n")
127 }
128}
129
130pub struct Combat<'a> {
131 attacker: &'a Character,
132 defender: &'a Character,
133}
134
135impl<'a> Combat<'a> {
136 pub fn new(attacker: &'a Character, defender: &'a Character) -> Self {
137 Self { attacker, defender }
138 }
139
140 fn resolve_concealment(attacker: &Character, defender: &Character) -> f32 {
143 if attacker.has_blind_fight() {
144 (defender.concealment.pow(2) as f32) / 100.0
145 } else {
146 defender.concealment as f32
147 }
148 }
149
150 fn resolve_damage(
151 attacker: &Character,
152 defender: &Character,
153 atk_info: AttackInfo,
154 is_crit: bool,
155 ) -> DamageResult {
156 let dmg_result = DamageResult::new();
157
158 let multiplier = if !is_crit {
159 1
160 } else {
161 attacker.weapon_crit_multiplier()
162 };
163
164 let weapon_base_dmg_type = *attacker.weapon.base.damage_type.first().unwrap();
169
170 let str_mod_bonus = ((attacker.abilities.str.get_mod()
172 + if attacker.is_weapon_twohanded() {
173 let str_mod = attacker.abilities.str.get_mod();
174 max(0, ((str_mod as f32 * 1.5) as i32) - str_mod)
175 } else {
176 0
177 })
178 / if atk_info.type_ == AttackType::OffHand {
179 2
180 } else {
181 1
182 })
183 * multiplier;
184
185 dmg_result.add(weapon_base_dmg_type, str_mod_bonus);
186
187 let weapon_base_dmg = attacker.weapon.base.damage.roll_m(multiplier);
189 dmg_result.add(weapon_base_dmg_type, weapon_base_dmg);
190
191 let _ = attacker
193 .weapon
194 .item_properties
195 .iter()
196 .filter(|x| match x {
197 ItemProperty::EnchantmentBonus(_) => true,
198 ItemProperty::DamageBonus(_) => true,
199 ItemProperty::MassiveCrit(_) => {
200 if is_crit {
201 true
202 } else {
203 false
204 }
205 }
206 _ => false,
207 })
208 .map(|x| match x {
209 ItemProperty::EnchantmentBonus(bonus) => {
210 dmg_result.add(weapon_base_dmg_type, bonus * multiplier);
211 }
212 ItemProperty::DamageBonus(dmg) => {
213 dmg_result.add(dmg.type_, dmg.roll_m(multiplier));
214 }
215 ItemProperty::MassiveCrit(dice) => {
216 dmg_result.add(weapon_base_dmg_type, dice.roll());
217 }
218 _ => (),
219 })
220 .collect::<Vec<_>>();
221
222 if attacker.has_bane_of_enemies() {
224 dmg_result.add(weapon_base_dmg_type, Dice::from("2d6").roll_m(multiplier));
225 }
226
227 if attacker.has_overwhelming_critical() {
229 dmg_result.add(weapon_base_dmg_type, Dice::from("1d6").roll_m(multiplier));
230 }
231
232 if attacker.has_weapon_spec() {
234 dmg_result.add(weapon_base_dmg_type, 2 * multiplier);
235 }
236
237 if attacker.has_epic_weapon_spec() {
239 dmg_result.add(weapon_base_dmg_type, 4 * multiplier);
240 }
241
242 let dmg_types = dmg_result.get_types();
244
245 for dmg_type in dmg_types {
246 let defender_dmg_immunity = defender.damage_immunity(dmg_type);
247 let defender_dmg_reduction = defender.damage_reduction(dmg_type);
248
249 if defender_dmg_immunity > 0 {
250 dmg_result.sub(
251 dmg_type,
252 dmg_result.get(dmg_type) * defender_dmg_immunity / 100,
253 );
254 }
255
256 if defender_dmg_reduction > 0 {
257 dmg_result.sub(dmg_type, defender_dmg_reduction);
258 }
259 }
260
261 dmg_result
262 }
263
264 pub fn resolve_round(&self) -> CombatStatistics {
265 let mut round_statistics = CombatStatistics::default();
266 let mut defender_can_epic_dodge = true;
267
268 for atk_no in 1..=self.attacker.total_apr() {
269 let atk_info = if let Some(atk_info) = self.attacker.atk_ab(atk_no) {
270 atk_info
271 } else {
272 println!("Combat::round() - Attack info is none!");
273 continue;
274 };
275
276 let defender_concealment = Self::resolve_concealment(self.attacker, self.defender);
277
278 if defender_concealment > 0.0
280 && (Dice::from("1d100").roll() as f32) < defender_concealment
281 {
282 round_statistics.concealed_attacks += 1;
283 round_statistics.total_misses += 1;
284
285 continue;
286 }
287
288 let hit_roll = Dice::from("1d20").roll();
289
290 if hit_roll != 1 && (hit_roll == 20 || (atk_info.ab + hit_roll >= self.defender.ac)) {
291 if self.defender.has_epic_dodge() && defender_can_epic_dodge {
292 defender_can_epic_dodge = false;
293
294 round_statistics.epic_dodged_attacks += 1;
295 round_statistics.total_misses += 1;
296
297 continue;
298 }
299
300 let is_crit = if !self.defender.is_crit_immune()
302 && hit_roll >= self.attacker.weapon_threat_range()
303 && atk_info.ab + Dice::from("1d20").roll() >= self.defender.ac
304 {
305 round_statistics.critical_hits += 1;
306 true
307 } else {
308 false
309 };
310
311 round_statistics.total_hits += 1;
312
313 let dmg_result =
315 Self::resolve_damage(self.attacker, self.defender, atk_info, is_crit);
316
317 round_statistics.dmg_dealt.add_from(&dmg_result);
318 } else {
319 round_statistics.total_misses += 1;
320 }
321 }
322
323 round_statistics
324 }
325}
326
327#[cfg(test)]
328mod test {
329 use crate::{
330 character::{AbilityList, Character, CharacterBuilder},
331 combat::{AttackInfo, AttackType, Combat},
332 dice::Dice,
333 feat::feat_db::get_feat,
334 item::{Damage, DamageResult, DamageType, ItemProperty, Weapon, WeaponBase},
335 size::SizeCategory,
336 };
337
338 #[test]
339 fn combat() {
340 let character: Character = Character::builder()
341 .ab(50)
342 .base_apr(4)
343 .extra_apr(1)
344 .feats(vec![])
345 .build();
346
347 assert_eq!(
348 character.atk_ab(1).unwrap(),
349 AttackInfo::new(50, AttackType::MainHand)
350 );
351 assert_eq!(
352 character.atk_ab(2).unwrap(),
353 AttackInfo::new(45, AttackType::MainHand)
354 );
355 assert_eq!(
356 character.atk_ab(3).unwrap(),
357 AttackInfo::new(40, AttackType::MainHand)
358 );
359 assert_eq!(
360 character.atk_ab(4).unwrap(),
361 AttackInfo::new(35, AttackType::MainHand)
362 );
363 assert_eq!(
364 character.atk_ab(5).unwrap(),
365 AttackInfo::new(50, AttackType::Extra)
366 );
367 assert_eq!(character.atk_ab(6), None);
368
369 let character2 = CharacterBuilder::from(character)
370 .ab(48)
371 .feats(vec![get_feat("Dual Wielding")])
372 .build();
373
374 assert_eq!(
375 character2.atk_ab(1).unwrap(),
376 AttackInfo::new(48, AttackType::MainHand)
377 );
378 assert_eq!(
379 character2.atk_ab(2).unwrap(),
380 AttackInfo::new(43, AttackType::MainHand)
381 );
382 assert_eq!(
383 character2.atk_ab(3).unwrap(),
384 AttackInfo::new(38, AttackType::MainHand)
385 );
386 assert_eq!(
387 character2.atk_ab(4).unwrap(),
388 AttackInfo::new(33, AttackType::MainHand)
389 );
390 assert_eq!(
391 character2.atk_ab(5).unwrap(),
392 AttackInfo::new(50, AttackType::Extra)
393 );
394 assert_eq!(
395 character2.atk_ab(6).unwrap(),
396 AttackInfo::new(48, AttackType::OffHand)
397 );
398 assert_eq!(
399 character2.atk_ab(7).unwrap(),
400 AttackInfo::new(43, AttackType::OffHand)
401 );
402 assert_eq!(character2.atk_ab(8), None);
403
404 let monk_character = CharacterBuilder::from(character2)
405 .ab(48)
406 .feats(vec![get_feat("Dual Wielding"), get_feat("Monk")])
407 .build();
408
409 assert_eq!(
410 monk_character.atk_ab(1).unwrap(),
411 AttackInfo::new(48, AttackType::MainHand)
412 );
413 assert_eq!(
414 monk_character.atk_ab(2).unwrap(),
415 AttackInfo::new(45, AttackType::MainHand)
416 );
417 assert_eq!(
418 monk_character.atk_ab(3).unwrap(),
419 AttackInfo::new(42, AttackType::MainHand)
420 );
421 assert_eq!(
422 monk_character.atk_ab(4).unwrap(),
423 AttackInfo::new(39, AttackType::MainHand)
424 );
425 assert_eq!(
426 monk_character.atk_ab(5).unwrap(),
427 AttackInfo::new(50, AttackType::Extra)
428 );
429 assert_eq!(
430 monk_character.atk_ab(6).unwrap(),
431 AttackInfo::new(48, AttackType::OffHand)
432 );
433 assert_eq!(
434 monk_character.atk_ab(7).unwrap(),
435 AttackInfo::new(45, AttackType::OffHand)
436 );
437 assert_eq!(monk_character.atk_ab(8), None);
438
439 let attacker = Character::builder()
440 .ab(50)
441 .feats(vec![get_feat("Blind Fight")])
442 .build();
443
444 let defender = Character::builder().concealment(50).build();
445 assert_eq!(Combat::resolve_concealment(&attacker, &defender), 25.0);
446
447 let defender = Character::builder().concealment(25).build();
448 assert_eq!(Combat::resolve_concealment(&attacker, &defender), 6.25);
449
450 let defender = Character::builder().concealment(0).build();
451 assert_eq!(Combat::resolve_concealment(&attacker, &defender), 0.0);
452 }
453
454 #[test]
455 fn damage() {
456 let attacker = Character::builder()
457 .abilities(AbilityList::builder().str(38).build())
458 .weapon(Weapon::new(
459 "".into(),
460 WeaponBase::new(
461 "".into(),
462 SizeCategory::Medium,
463 Dice::from(6),
464 18,
465 2,
466 vec![DamageType::Slashing],
467 ),
468 vec![
469 ItemProperty::Keen,
470 ItemProperty::EnchantmentBonus(4),
471 ItemProperty::DamageBonus(Damage::new(
472 DamageType::Divine,
473 Dice::from(4),
474 true,
475 true,
476 )),
477 ItemProperty::MassiveCrit(Dice::from(6)),
478 ],
479 ))
480 .feats(vec![get_feat("Increased Multiplier")])
481 .build();
482
483 let defender = Character::builder()
484 .physical_immunity(10)
485 .physical_damage_reduction(5)
486 .build();
487
488 let round_result = Combat::resolve_damage(
489 &attacker,
490 &defender,
491 AttackInfo::new(50, AttackType::MainHand),
492 false,
493 );
494
495 assert_eq!(round_result.get(DamageType::Slashing), 17);
496 assert_eq!(round_result.get(DamageType::Divine), 4);
497 assert_eq!(round_result.total_dmg(), 21);
498
499 let round_result = Combat::resolve_damage(
500 &attacker,
501 &defender,
502 AttackInfo::new(50, AttackType::MainHand),
503 true,
504 );
505
506 assert_eq!(round_result.get(DamageType::Slashing), 66);
507 assert_eq!(round_result.get(DamageType::Divine), 12);
508 assert_eq!(round_result.total_dmg(), 78);
509
510 let attacker = Character::builder()
512 .abilities(AbilityList::builder().str(38).build())
513 .weapon(Weapon::new(
514 "".into(),
515 WeaponBase::new(
516 "".into(),
517 SizeCategory::Large,
518 Dice::from(6),
519 18,
520 2,
521 vec![DamageType::Slashing],
522 ),
523 vec![
524 ItemProperty::Keen,
525 ItemProperty::EnchantmentBonus(4),
526 ItemProperty::DamageBonus(Damage::new(
527 DamageType::Divine,
528 Dice::from(4),
529 true,
530 true,
531 )),
532 ItemProperty::MassiveCrit(Dice::from(6)),
533 ],
534 ))
535 .feats(vec![get_feat("Increased Multiplier")])
536 .build();
537
538 let defender = Character::builder()
539 .physical_immunity(0)
540 .physical_damage_reduction(0)
541 .build();
542
543 let round_result = Combat::resolve_damage(
544 &attacker,
545 &defender,
546 AttackInfo::new(50, AttackType::MainHand),
547 false,
548 );
549
550 assert_eq!(round_result.get(DamageType::Slashing), 31);
551
552 let round_result = Combat::resolve_damage(
554 &attacker,
555 &defender,
556 AttackInfo::new(50, AttackType::OffHand),
557 false,
558 );
559
560 assert_eq!(round_result.get(DamageType::Slashing), 20);
561
562 let mut dmg1 = DamageResult::new();
563 dmg1.add(DamageType::Acid, 4);
564 dmg1.add(DamageType::Bludgeoning, 6);
565
566 assert_eq!(dmg1.total_dmg(), 10);
567
568 let dmg2 = DamageResult::new();
569 dmg2.add(DamageType::Cold, 2);
570 dmg2.add(DamageType::Divine, 1);
571
572 dmg1.add_from(&dmg2);
573
574 assert_eq!(dmg1.get(DamageType::Cold), 2);
575 assert_eq!(dmg1.get(DamageType::Divine), 1);
576 assert_eq!(dmg1.total_dmg(), 13);
577 }
578}