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