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 / 250.0) + 1.0 - losses_ratio);
111
112 downgraded_wall_level = if wall_levels_to_decrease > Wall::MAX_LEVEL {
113 -Wall::MAX_LEVEL
114 } else {
115 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.9));
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
182#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
183#[serde(rename_all = "kebab-case")]
184pub enum BattleWinner {
185 Attacker,
186 Defender,
187}
188
189impl BattleWinner {
190 fn determine(attacker: &OffensivePower, defender: &DefensivePower) -> Self {
191 if attacker.total > defender.total { Self::Attacker } else { Self::Defender }
192 }
193}
194
195struct OffensivePower {
196 total: f64,
197 infantry: f64,
198 cavalry: f64,
199 ranged: f64,
200 rams_amount: f64,
201}
202
203impl OffensivePower {
204 fn new(squads: &[Squad], luck: Luck) -> Self {
205 let mut infantry = 0.0;
206 let mut cavalry = 0.0;
207 let mut ranged = 0.0;
208 let mut rams_amount = 0.0;
209 let mut ranged_with_debuff = 0.0;
210
211 let mut army_size = 0.0;
212 let mut ranged_amount = 0.0;
213
214 for squad in squads {
215 army_size += f64::from(squad.size());
216 match squad.kind() {
217 UnitKind::Infantry => {
218 infantry += *squad.attack();
219 if squad.id() == UnitId::Ram {
220 rams_amount = f64::from(squad.size());
221 }
222 }
223 UnitKind::Cavalry => {
224 cavalry += *squad.attack();
225 }
226 UnitKind::Ranged => {
227 ranged += *squad.attack();
228 ranged_with_debuff += *squad.attack() * f64::from(squad.unit().stats().ranged_debuff());
229 ranged_amount += f64::from(squad.size());
230 }
231 }
232 }
233
234 if ranged_amount / army_size > 0.3 {
235 ranged = ranged_with_debuff;
236 }
237 infantry += infantry * luck;
238 cavalry += cavalry * luck;
239 ranged += ranged * luck;
240
241 let total = infantry + cavalry + ranged;
242
243 OffensivePower {
244 total,
245 infantry,
246 cavalry,
247 ranged,
248 rams_amount,
249 }
250 }
251}
252
253struct DefensivePower {
254 total: f64,
255}
256
257impl DefensivePower {
258 fn new(
259 squads: &[Squad],
260 offensive_power: &OffensivePower,
261 defending_wall: Option<&WallStats>,
262 infrastructure_stats: &InfrastructureStats,
263 ) -> Result<Self> {
264 let mut infantry = 0.0;
265 let mut cavalry = 0.0;
266 let mut ranged = 0.0;
267
268 let mut army_size = 0.0;
269
270 for squad in squads {
271 infantry += squad.defense().infantry;
272 cavalry += squad.defense().cavalry;
273 ranged += squad.defense().ranged;
274
275 army_size += f64::from(squad.size());
276 }
277
278 let mut total = 0.0;
279
280 if army_size > 0.0 {
281 let infantry_power_per_unit = infantry / army_size;
282 let cavalry_power_per_unit = cavalry / army_size;
283 let ranged_power_per_unit = ranged / army_size;
284
285 let infantry_necessary_units = offensive_power.infantry / infantry_power_per_unit;
286 let cavalry_necessary_units = offensive_power.cavalry / cavalry_power_per_unit;
287 let ranged_necessary_units = offensive_power.ranged / ranged_power_per_unit;
288
289 let necessary_units =
290 infantry_necessary_units + cavalry_necessary_units + ranged_necessary_units;
291
292 let infantry_proportion = infantry_necessary_units / necessary_units;
293 let cavalry_proportion = cavalry_necessary_units / necessary_units;
294 let ranged_proportion = ranged_necessary_units / necessary_units;
295
296 infantry = infantry_proportion * army_size * infantry_power_per_unit;
297 cavalry = cavalry_proportion * army_size * cavalry_power_per_unit;
298 ranged = ranged_proportion * army_size * ranged_power_per_unit;
299
300 total = infantry + cavalry + ranged;
301 }
302
303 if let Some(wall) = defending_wall {
304 let mut attacking_rams = offensive_power.rams_amount;
305
306 if attacking_rams > 0.0 {
307 let rams_growth_per_wall_level: f64 = growth()
308 .floor(wall.level)
309 .ceil(200)
310 .max_level(Wall::MAX_LEVEL)
311 .call();
312
313 let mut rams_vec = Vec::new();
314 let mut rams_per_wall_level = f64::from(wall.level);
315 let mut total_of_rams = 0.0;
316
317 for _ in 1..=usize::from(wall.level) {
318 rams_vec.push(rams_per_wall_level - total_of_rams);
319 total_of_rams = rams_per_wall_level;
320 rams_per_wall_level += rams_per_wall_level * rams_growth_per_wall_level;
321 }
322
323 let mut wall_levels_to_decrease: u8 = 0;
324 for value in rams_vec.iter().rev() {
325 if attacking_rams >= *value && wall_levels_to_decrease < wall.level {
326 attacking_rams -= value;
327 wall_levels_to_decrease += 1;
328 }
329 }
330
331 if wall.level - wall_levels_to_decrease > 0 {
332 let new_wall = infrastructure_stats
333 .wall()
334 .get(wall.level - wall_levels_to_decrease)?;
335
336 total += new_wall.defense + ((new_wall.defense_percent / 100.0) * total);
337 }
338 } else if wall.level > 0 {
339 total += wall.defense + ((wall.defense_percent / 100.0) * total);
340 }
341 }
342
343 Ok(DefensivePower { total })
344 }
345}