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 =
110 wall_level * ((remaining_rams / 200.0) - (losses_ratio * 1.2));
111
112 if wall_levels_to_decrease > Wall::MAX_LEVEL {
113 downgraded_wall_level = -Wall::MAX_LEVEL;
114 } else if wall_levels_to_decrease > 0.0 {
115 downgraded_wall_level = BuildingLevelDiff::from(-wall_levels_to_decrease);
116 }
117 }
118 }
119 BattleWinner::Defender => {
120 for squad in defending_squads {
121 let squad_size = f64::from(squad.size());
122 squad_survivors = squad_size - (squad_size * losses_ratio);
123 defender_surviving_personnel += Squad::new(squad.id(), squad_survivors);
124 }
125
126 if wall_level > 0 && attacker_power.rams_amount > 0.0 {
127 let diff = -(wall_level * (losses_ratio * 0.7));
128 downgraded_wall_level = BuildingLevelDiff::from(diff);
129 }
130 }
131 }
132
133 Ok(BattleResult {
134 attacker_personnel,
135 attacker_surviving_personnel,
136 defender_personnel,
137 defender_surviving_personnel,
138 wall_level,
139 downgraded_wall_level,
140 winner,
141 luck,
142 })
143 }
144
145 #[inline]
146 pub fn attacker_personnel(&self) -> &ArmyPersonnel {
147 &self.attacker_personnel
148 }
149
150 #[inline]
151 pub fn attacker_surviving_personnel(&self) -> &ArmyPersonnel {
152 &self.attacker_surviving_personnel
153 }
154
155 #[inline]
156 pub fn defender_personnel(&self) -> &ArmyPersonnel {
157 &self.defender_personnel
158 }
159
160 #[inline]
161 pub fn defender_surviving_personnel(&self) -> &ArmyPersonnel {
162 &self.defender_surviving_personnel
163 }
164
165 pub fn defender_surviving_personnel_ratio(&self) -> f64 {
166 let total = self
167 .defender_personnel
168 .iter()
169 .map(|squad| f64::from(squad.size()))
170 .sum::<f64>();
171
172 let surviving = self
173 .defender_surviving_personnel
174 .iter()
175 .map(|squad| f64::from(squad.size()))
176 .sum::<f64>();
177
178 if total > 0.0 { surviving / total } else { 0.0 }
179 }
180
181 #[inline]
182 pub fn wall_level(&self) -> BuildingLevel {
183 self.wall_level
184 }
185
186 #[inline]
187 pub fn downgraded_wall_level(&self) -> BuildingLevelDiff {
188 self.downgraded_wall_level
189 }
190}
191
192#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
193#[serde(rename_all = "kebab-case")]
194pub enum BattleWinner {
195 Attacker,
196 Defender,
197}
198
199impl BattleWinner {
200 fn determine(attacker: &OffensivePower, defender: &DefensivePower) -> Self {
201 if attacker.total > defender.total { Self::Attacker } else { Self::Defender }
202 }
203}
204
205struct OffensivePower {
206 total: f64,
207 infantry: f64,
208 cavalry: f64,
209 ranged: f64,
210 rams_amount: f64,
211}
212
213impl OffensivePower {
214 fn new(squads: &[Squad], luck: Luck) -> Self {
215 let mut infantry = 0.0;
216 let mut cavalry = 0.0;
217 let mut ranged = 0.0;
218 let mut rams_amount = 0.0;
219 let mut ranged_with_debuff = 0.0;
220
221 let mut army_size = 0.0;
222 let mut ranged_amount = 0.0;
223
224 for squad in squads {
225 army_size += f64::from(squad.size());
226 match squad.kind() {
227 UnitKind::Infantry => {
228 infantry += *squad.attack();
229 if squad.id() == UnitId::Ram {
230 rams_amount = f64::from(squad.size());
231 }
232 }
233 UnitKind::Cavalry => {
234 cavalry += *squad.attack();
235 }
236 UnitKind::Ranged => {
237 ranged += *squad.attack();
238 ranged_with_debuff += *squad.attack() * f64::from(squad.unit().stats().ranged_debuff());
239 ranged_amount += f64::from(squad.size());
240 }
241 }
242 }
243
244 if ranged_amount / army_size > 0.3 {
245 ranged = ranged_with_debuff;
246 }
247 infantry += infantry * luck;
248 cavalry += cavalry * luck;
249 ranged += ranged * luck;
250
251 let total = infantry + cavalry + ranged;
252
253 OffensivePower {
254 total,
255 infantry,
256 cavalry,
257 ranged,
258 rams_amount,
259 }
260 }
261}
262
263struct DefensivePower {
264 total: f64,
265}
266
267impl DefensivePower {
268 fn new(
269 squads: &[Squad],
270 offensive_power: &OffensivePower,
271 defending_wall: Option<&WallStats>,
272 infrastructure_stats: &InfrastructureStats,
273 ) -> Result<Self> {
274 let mut infantry = 0.0;
275 let mut cavalry = 0.0;
276 let mut ranged = 0.0;
277
278 let mut army_size = 0.0;
279
280 for squad in squads {
281 infantry += squad.defense().infantry;
282 cavalry += squad.defense().cavalry;
283 ranged += squad.defense().ranged;
284
285 army_size += f64::from(squad.size());
286 }
287
288 let mut total = 0.0;
289
290 if army_size > 0.0 {
291 let infantry_power_per_unit = infantry / army_size;
292 let cavalry_power_per_unit = cavalry / army_size;
293 let ranged_power_per_unit = ranged / army_size;
294
295 let infantry_necessary_units = offensive_power.infantry / infantry_power_per_unit;
296 let cavalry_necessary_units = offensive_power.cavalry / cavalry_power_per_unit;
297 let ranged_necessary_units = offensive_power.ranged / ranged_power_per_unit;
298
299 let necessary_units =
300 infantry_necessary_units + cavalry_necessary_units + ranged_necessary_units;
301
302 let infantry_proportion = infantry_necessary_units / necessary_units;
303 let cavalry_proportion = cavalry_necessary_units / necessary_units;
304 let ranged_proportion = ranged_necessary_units / necessary_units;
305
306 infantry = infantry_proportion * army_size * infantry_power_per_unit;
307 cavalry = cavalry_proportion * army_size * cavalry_power_per_unit;
308 ranged = ranged_proportion * army_size * ranged_power_per_unit;
309
310 total = infantry + cavalry + ranged;
311 }
312
313 if let Some(wall) = defending_wall {
314 let mut attacking_rams = offensive_power.rams_amount;
315
316 if attacking_rams > 0.0 {
317 let rams_growth_per_wall_level: f64 = growth()
318 .floor(wall.level)
319 .ceil(200)
320 .max_level(Wall::MAX_LEVEL)
321 .call();
322
323 let mut rams_vec = Vec::new();
324 let mut rams_per_wall_level = f64::from(wall.level);
325 let mut total_of_rams = 0.0;
326
327 for _ in 1..=usize::from(wall.level) {
328 rams_vec.push(rams_per_wall_level - total_of_rams);
329 total_of_rams = rams_per_wall_level;
330 rams_per_wall_level += rams_per_wall_level * rams_growth_per_wall_level;
331 }
332
333 let mut wall_levels_to_decrease: u8 = 0;
334 for value in rams_vec.iter().rev() {
335 if attacking_rams >= *value && wall_levels_to_decrease < wall.level {
336 attacking_rams -= value;
337 wall_levels_to_decrease += 1;
338 }
339 }
340
341 if wall.level - wall_levels_to_decrease > 0 {
342 let new_wall = infrastructure_stats
343 .wall()
344 .get(wall.level - wall_levels_to_decrease)?;
345
346 total += new_wall.defense + ((new_wall.defense_percent / 100.0) * total);
347 }
348 } else if wall.level > 0 {
349 total += wall.defense + ((wall.defense_percent / 100.0) * total);
350 }
351 }
352
353 Ok(DefensivePower { total })
354 }
355}