1pub mod luck;
5
6#[cfg(test)]
7mod tests;
8
9use crate::infrastructure::building::wall::WallStats;
10use crate::infrastructure::prelude::BuildingLevel;
11use crate::military::army::personnel::ArmyPersonnel;
12use crate::military::squad::Squad;
13use crate::military::squad::size::SquadSize;
14use crate::military::unit::UnitKind;
15use bon::Builder;
16use luck::Luck;
17use serde::{Deserialize, Serialize};
18
19#[derive(Builder)]
20pub struct Battle<'a> {
21 #[builder(default)]
22 attacker: &'a [Squad],
23
24 #[builder(default)]
25 defender: &'a [Squad],
26
27 #[builder(default)]
28 luck: Luck,
29
30 wall: Option<&'a WallStats>,
31}
32
33impl Battle<'_> {
34 #[inline]
35 pub fn result(self) -> BattleResult {
36 BattleResult::new(self.attacker, self.defender, self.luck, self.wall)
37 }
38}
39
40#[derive(Clone, Debug, Deserialize, Serialize)]
41#[serde(rename_all = "camelCase")]
42pub struct BattleResult {
43 attacker_personnel: ArmyPersonnel,
44 attacker_surviving_personnel: ArmyPersonnel,
45 defender_personnel: ArmyPersonnel,
46 defender_surviving_personnel: ArmyPersonnel,
47 wall_level: BuildingLevel,
48 winner: BattleWinner,
49 luck: Luck,
50}
51
52impl BattleResult {
53 #[rustfmt::skip]
54 fn new(
55 attacking_squads: &[Squad],
56 defending_squads: &[Squad],
57 luck: Luck,
58 wall: Option<&WallStats>
59 ) -> Self {
60 let attacker_power = OffensivePower::new(attacking_squads, luck);
61 let defender_power = DefensivePower::new(defending_squads, &attacker_power, wall);
62
63 let winner = BattleWinner::determine(&attacker_power, &defender_power);
64
65 let attacker_personnel: ArmyPersonnel = attacking_squads.iter().cloned().collect();
66 let defender_personnel: ArmyPersonnel = defending_squads.iter().cloned().collect();
67
68 let mut attacker_surviving_personnel = ArmyPersonnel::default();
69 let mut defender_surviving_personnel = ArmyPersonnel::default();
70
71 let losses_ratio = match winner {
72 BattleWinner::Attacker => defender_power.total / attacker_power.total,
73 BattleWinner::Defender => attacker_power.total / defender_power.total,
74 };
75
76 let mut squad_survivors: f64;
77 match winner {
78 BattleWinner::Attacker => {
79 for squad in attacking_squads {
80 let squad_size = f64::from(squad.size());
81 squad_survivors = squad_size - (squad_size * losses_ratio);
82 attacker_surviving_personnel += Squad::new(squad.id(), SquadSize::from(squad_survivors));
83 }
84 }
85 BattleWinner::Defender => {
86 for squad in defending_squads {
87 let squad_size = f64::from(squad.size());
88 squad_survivors = squad_size - (squad_size * losses_ratio);
89 defender_surviving_personnel += Squad::new(squad.id(), SquadSize::from(squad_survivors));
90 }
91 }
92 }
93
94 let wall_level = wall
95 .map(|stats| stats.level)
96 .unwrap_or_default();
97
98 BattleResult {
99 attacker_personnel,
100 attacker_surviving_personnel,
101 defender_personnel,
102 defender_surviving_personnel,
103 wall_level,
104 winner,
105 luck,
106 }
107 }
108
109 #[inline]
110 pub fn attacker_personnel(&self) -> &ArmyPersonnel {
111 &self.attacker_personnel
112 }
113
114 #[inline]
115 pub fn attacker_surviving_personnel(&self) -> &ArmyPersonnel {
116 &self.attacker_surviving_personnel
117 }
118
119 #[inline]
120 pub fn defender_personnel(&self) -> &ArmyPersonnel {
121 &self.defender_personnel
122 }
123
124 #[inline]
125 pub fn defender_surviving_personnel(&self) -> &ArmyPersonnel {
126 &self.defender_surviving_personnel
127 }
128
129 pub fn defender_surviving_personnel_ratio(&self) -> f64 {
130 let total = self
131 .defender_personnel
132 .iter()
133 .map(|squad| f64::from(squad.size()))
134 .sum::<f64>();
135
136 let surviving = self
137 .defender_surviving_personnel
138 .iter()
139 .map(|squad| f64::from(squad.size()))
140 .sum::<f64>();
141
142 if total > 0.0 { surviving / total } else { 0.0 }
143 }
144}
145
146#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
147#[serde(rename_all = "kebab-case")]
148pub enum BattleWinner {
149 Attacker,
150 Defender,
151}
152
153impl BattleWinner {
154 fn determine(attacker: &OffensivePower, defender: &DefensivePower) -> Self {
155 if attacker.total > defender.total { Self::Attacker } else { Self::Defender }
156 }
157}
158
159struct OffensivePower {
160 total: f64,
161 infantry: f64,
162 cavalry: f64,
163 ranged: f64,
164}
165
166impl OffensivePower {
167 fn new(squads: &[Squad], luck: Luck) -> Self {
168 let mut infantry = 0.0;
169 let mut cavalry = 0.0;
170 let mut ranged = 0.0;
171
172 let mut army_size = 0.0;
173 let mut ranged_size = 0.0;
174
175 for squad in squads {
176 army_size += f64::from(squad.size());
177 match squad.kind() {
178 UnitKind::Infantry => {
179 infantry += *squad.attack();
180 }
181 UnitKind::Cavalry => {
182 cavalry += *squad.attack();
183 }
184 UnitKind::Ranged => {
185 ranged += *squad.attack();
186 ranged_size += f64::from(squad.size());
187 }
188 }
189 }
190
191 if ranged_size / army_size > 0.3 {
192 ranged *= sum_ranged_debuff(squads);
193 }
194 infantry += infantry * luck;
195 cavalry += cavalry * luck;
196 ranged += ranged * luck;
197
198 let total = infantry + cavalry + ranged;
199
200 OffensivePower { total, infantry, cavalry, ranged }
201 }
202}
203
204struct DefensivePower {
205 total: f64,
206}
207
208impl DefensivePower {
209 fn new(
210 squads: &[Squad],
211 offensive_power: &OffensivePower,
212 defending_wall: Option<&WallStats>,
213 ) -> Self {
214 let mut infantry_power = 0.0;
215 let mut cavalry_power = 0.0;
216 let mut ranged_power = 0.0;
217
218 let mut army_size = 0.0;
219 let mut ranged_squad_size = 0.0;
220
221 for squad in squads {
222 if squad.kind() == UnitKind::Ranged {
223 ranged_squad_size += f64::from(squad.size());
224 }
225 army_size += f64::from(squad.size());
226 }
227
228 for squad in squads {
229 if squad.kind() == UnitKind::Ranged && ranged_squad_size / army_size > 0.5 {
230 infantry_power += squad.defense().infantry * sum_ranged_debuff(squads);
231 cavalry_power += squad.defense().cavalry * sum_ranged_debuff(squads);
232 ranged_power += squad.defense().ranged * sum_ranged_debuff(squads);
233 } else {
234 infantry_power += squad.defense().infantry;
235 cavalry_power += squad.defense().cavalry;
236 ranged_power += squad.defense().ranged;
237 }
238 }
239
240 let mut total = 0.0;
241
242 if army_size > 0.0 {
243 let infantry_power_per_unit = infantry_power / army_size;
244 let cavalry_power_per_unit = cavalry_power / army_size;
245 let ranged_power_per_unit = ranged_power / army_size;
246
247 let mut infantry_necessary_units = offensive_power.infantry / infantry_power_per_unit;
248 let mut cavalry_necessary_units = offensive_power.cavalry / cavalry_power_per_unit;
249 let mut ranged_necessary_units = offensive_power.ranged / ranged_power_per_unit;
250
251 let necessary_units =
252 infantry_necessary_units + cavalry_necessary_units + ranged_necessary_units;
253
254 infantry_necessary_units /= necessary_units;
255 cavalry_necessary_units /= necessary_units;
256 ranged_necessary_units /= necessary_units;
257
258 infantry_power = infantry_necessary_units * army_size * infantry_power_per_unit;
259 cavalry_power = cavalry_necessary_units * army_size * cavalry_power_per_unit;
260 ranged_power = ranged_necessary_units * army_size * ranged_power_per_unit;
261
262 total = infantry_power + cavalry_power + ranged_power;
263 }
264
265 if let Some(wall_power) = defending_wall {
266 total = add_wall_power(wall_power, total);
267 }
268
269 DefensivePower { total }
270 }
271}
272
273fn sum_ranged_debuff(squads: &[Squad]) -> f64 {
274 let mut ranged_debuff = 0.0;
275 let mut ranged_amount = 0;
276 for squad in squads {
277 if squad.kind() == UnitKind::Ranged {
278 let size = *squad.size();
279 let stats = squad.unit().stats();
280 ranged_debuff += stats.ranged_debuff() * size;
281 ranged_amount += size;
282 }
283 }
284 ranged_debuff / f64::from(ranged_amount)
285}
286
287fn add_wall_power(wall: &WallStats, current_power: f64) -> f64 {
288 current_power
289 + f64::from(wall.defense)
290 + ((f64::from(wall.defense_percent) / 100.0) * current_power)
291}