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