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