Skip to main content

nil_core/battle/
mod.rs

1// Copyright (C) Call of Nil contributors
2// SPDX-License-Identifier: AGPL-3.0-only
3
4pub 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}