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 =
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}