1use super::{
2 character::Character,
3 combat::{Combat, CombatStatistics},
4 feat::feat_db::get_feat,
5 string::align_string,
6};
7use serde::{Deserialize, Serialize};
8use std::{cell::Cell, collections::HashMap};
9
10type CombatCallbackFn = dyn Fn(&Character, &i32, &CombatStatistics) -> ();
11
12#[derive(Clone, Default, Debug, Serialize, Deserialize)]
13pub struct DamageTestResult {
14 total_rounds: i32,
15 statistics: HashMap<i32, CombatStatistics>,
16}
17
18impl DamageTestResult {
19 pub fn new() -> Self {
20 Self::default()
21 }
22
23 fn target_ac_result_string(&self, target_ac: i32) -> String {
24 let target = self.statistics.get(&target_ac);
25
26 if target.is_none() {
27 return "".into();
28 }
29
30 let target = target.unwrap();
31 let mut string_list: Vec<String> = vec![];
32
33 string_list.push(align_string("TARGET AC", target_ac.to_string()));
34 string_list.push("".into());
35 string_list.push(target.to_string());
36 string_list.push("".into());
37 string_list.push(align_string(
38 "AVERAGE DAMAGE PER ROUND",
39 format!("{:.2}", target.dmg_dealt.total_dmg() / self.total_rounds),
40 ));
41
42 string_list.join("\n")
43 }
44}
45
46impl ToString for DamageTestResult {
47 fn to_string(&self) -> String {
48 let mut string_list: Vec<String> = vec![];
49
50 let mut ac_list = self.statistics.keys().collect::<Vec<&i32>>();
51 ac_list.sort();
52
53 for (i, &&ac) in ac_list.iter().enumerate() {
54 string_list.push(self.target_ac_result_string(ac));
55 string_list.push("".into());
56
57 if i != ac_list.len() - 1 {
58 string_list.push("=".repeat(50));
59 string_list.push("".into());
60 }
61 }
62
63 string_list.join("\n")
64 }
65}
66
67#[derive(Default)]
68pub struct CombatSimulator<'a> {
69 total_rounds: i32,
70 damage_test_notifier: Cell<Option<&'a CombatCallbackFn>>,
71}
72
73impl<'a> CombatSimulator<'a> {
74 pub fn new(total_rounds: i32) -> Self {
75 Self {
76 total_rounds,
77 damage_test_notifier: Cell::new(None),
78 }
79 }
80
81 pub fn begin(&self, attacker: &Character, defender: &Character) -> CombatStatistics {
82 let mut statistics = CombatStatistics::new();
83 let combat = Combat::new(attacker, defender);
84
85 for _ in 1..=self.total_rounds {
86 let round_statistics = combat.resolve_round();
87
88 statistics.total_hits += round_statistics.total_hits;
89 statistics.total_misses += round_statistics.total_misses;
90 statistics.concealed_attacks += round_statistics.concealed_attacks;
91 statistics.epic_dodged_attacks += round_statistics.epic_dodged_attacks;
92 statistics.critical_hits += round_statistics.critical_hits;
93 statistics.dmg_dealt.add_from(&round_statistics.dmg_dealt);
94 }
95
96 statistics
97 }
98
99 pub fn damage_test(
100 &self,
101 attacker: &Character,
102 target_ac_list: Vec<i32>,
103 target_concealment: i32,
104 target_physical_immunity: i32,
105 target_defensive_essence: i32,
106 target_has_epic_dodge: bool,
107 ) -> DamageTestResult {
108 let mut result = DamageTestResult::new();
109
110 for target_ac in target_ac_list {
111 let mut dummy = Character::builder()
112 .name("Combat Dummy".into())
113 .ac(target_ac)
114 .concealment(target_concealment)
115 .physical_immunity(target_physical_immunity)
116 .defensive_essence(target_defensive_essence);
117
118 if target_has_epic_dodge {
119 dummy = dummy.add_feat(get_feat("Epic Dodge"));
120 }
121
122 let dummy = dummy.build();
123 let combat_statistics = self.begin(attacker, &dummy);
124
125 if let Some(f) = self.damage_test_notifier.get() {
126 f(&attacker, &target_ac, &combat_statistics);
127 }
128
129 result.statistics.insert(target_ac, combat_statistics);
130 }
131
132 result.total_rounds = self.total_rounds;
133 result
134 }
135
136 pub fn set_damage_test_notifier(&self, f: &'a CombatCallbackFn) {
137 self.damage_test_notifier.set(Some(f));
138 }
139}