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