arelith/
simulator.rs

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}