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::WallStats;
11use crate::infrastructure::prelude::{BuildingLevel, Wall};
12use crate::infrastructure::stats::InfrastructureStats;
13use crate::military::army::personnel::ArmyPersonnel;
14use crate::military::squad::Squad;
15use crate::military::squad::size::SquadSize;
16use crate::military::unit::{UnitId, UnitKind};
17use bon::Builder;
18use luck::Luck;
19use nil_num::growth::growth;
20use serde::{Deserialize, Serialize};
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)]
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")]
53pub struct BattleResult {
54  attacker_personnel: ArmyPersonnel,
55  attacker_surviving_personnel: ArmyPersonnel,
56  defender_personnel: ArmyPersonnel,
57  defender_surviving_personnel: ArmyPersonnel,
58  wall_level: BuildingLevel,
59  winner: BattleWinner,
60  luck: Luck,
61}
62
63impl BattleResult {
64  #[rustfmt::skip]
65  fn new(
66    attacking_squads: &[Squad],
67    defending_squads: &[Squad],
68    luck: Luck,
69    wall: Option<&WallStats>,
70    infrastructure_stats: &InfrastructureStats,
71  ) -> Result<Self> {
72    let attacker_power = OffensivePower::new(attacking_squads, luck);
73    let defender_power = DefensivePower::new(defending_squads, &attacker_power, wall, infrastructure_stats)?;
74
75    let winner = BattleWinner::determine(&attacker_power, &defender_power);
76
77    let attacker_personnel: ArmyPersonnel = attacking_squads.iter().cloned().collect();
78    let defender_personnel: ArmyPersonnel = defending_squads.iter().cloned().collect();
79
80    let mut attacker_surviving_personnel = ArmyPersonnel::default();
81    let mut defender_surviving_personnel = ArmyPersonnel::default();
82
83    let losses_ratio = match winner {
84      BattleWinner::Attacker => (defender_power.total / attacker_power.total).powf(1.5),
85      BattleWinner::Defender => (attacker_power.total / defender_power.total).powf(1.5),
86    };
87
88    let mut squad_survivors: f64;
89    match winner {
90      BattleWinner::Attacker => {
91        for squad in attacking_squads {
92          let squad_size = f64::from(squad.size());
93          squad_survivors = squad_size - (squad_size * losses_ratio);
94          attacker_surviving_personnel += Squad::new(squad.id(), SquadSize::from(squad_survivors));
95        }
96      }
97      BattleWinner::Defender => {
98        for squad in defending_squads {
99          let squad_size = f64::from(squad.size());
100          squad_survivors = squad_size - (squad_size * losses_ratio);
101          defender_surviving_personnel += Squad::new(squad.id(), SquadSize::from(squad_survivors));
102        }
103      }
104    }
105
106    let wall_level = wall
107      .map(|stats| stats.level)
108      .unwrap_or_default();
109
110    Ok(BattleResult {
111      attacker_personnel,
112      attacker_surviving_personnel,
113      defender_personnel,
114      defender_surviving_personnel,
115      wall_level,
116      winner,
117      luck,
118    })
119  }
120
121  #[inline]
122  pub fn attacker_personnel(&self) -> &ArmyPersonnel {
123    &self.attacker_personnel
124  }
125
126  #[inline]
127  pub fn attacker_surviving_personnel(&self) -> &ArmyPersonnel {
128    &self.attacker_surviving_personnel
129  }
130
131  #[inline]
132  pub fn defender_personnel(&self) -> &ArmyPersonnel {
133    &self.defender_personnel
134  }
135
136  #[inline]
137  pub fn defender_surviving_personnel(&self) -> &ArmyPersonnel {
138    &self.defender_surviving_personnel
139  }
140
141  pub fn defender_surviving_personnel_ratio(&self) -> f64 {
142    let total = self
143      .defender_personnel
144      .iter()
145      .map(|squad| f64::from(squad.size()))
146      .sum::<f64>();
147
148    let surviving = self
149      .defender_surviving_personnel
150      .iter()
151      .map(|squad| f64::from(squad.size()))
152      .sum::<f64>();
153
154    if total > 0.0 { surviving / total } else { 0.0 }
155  }
156}
157
158#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
159#[serde(rename_all = "kebab-case")]
160pub enum BattleWinner {
161  Attacker,
162  Defender,
163}
164
165impl BattleWinner {
166  fn determine(attacker: &OffensivePower, defender: &DefensivePower) -> Self {
167    if attacker.total > defender.total { Self::Attacker } else { Self::Defender }
168  }
169}
170
171struct OffensivePower {
172  total: f64,
173  infantry: f64,
174  cavalry: f64,
175  ranged: f64,
176  rams_amount: f64,
177}
178
179impl OffensivePower {
180  fn new(squads: &[Squad], luck: Luck) -> Self {
181    let mut infantry = 0.0;
182    let mut cavalry = 0.0;
183    let mut ranged = 0.0;
184    let mut rams_amount = 0.0;
185    let mut ranged_with_debuff = 0.0;
186
187    let mut army_size = 0.0;
188    let mut ranged_amount = 0.0;
189
190    for squad in squads {
191      army_size += f64::from(squad.size());
192      match squad.kind() {
193        UnitKind::Infantry => {
194          infantry += *squad.attack();
195          if squad.id() == UnitId::Ram {
196            rams_amount = f64::from(squad.size());
197          }
198        }
199        UnitKind::Cavalry => {
200          cavalry += *squad.attack();
201        }
202        UnitKind::Ranged => {
203          ranged += *squad.attack();
204          ranged_with_debuff += *squad.attack() * f64::from(squad.unit().stats().ranged_debuff());
205          ranged_amount += f64::from(squad.size());
206        }
207      }
208    }
209
210    if ranged_amount / army_size > 0.3 {
211      ranged = ranged_with_debuff;
212    }
213    infantry += infantry * luck;
214    cavalry += cavalry * luck;
215    ranged += ranged * luck;
216
217    let total = infantry + cavalry + ranged;
218
219    OffensivePower {
220      total,
221      infantry,
222      cavalry,
223      ranged,
224      rams_amount,
225    }
226  }
227}
228
229struct DefensivePower {
230  total: f64,
231}
232
233impl DefensivePower {
234  fn new(
235    squads: &[Squad],
236    offensive_power: &OffensivePower,
237    defending_wall: Option<&WallStats>,
238    infrastructure_stats: &InfrastructureStats,
239  ) -> Result<Self> {
240    let mut infantry = 0.0;
241    let mut cavalry = 0.0;
242    let mut ranged = 0.0;
243
244    let mut army_size = 0.0;
245
246    for squad in squads {
247      infantry += squad.defense().infantry;
248      cavalry += squad.defense().cavalry;
249      ranged += squad.defense().ranged;
250
251      army_size += f64::from(squad.size());
252    }
253
254    let mut total = 0.0;
255
256    if army_size > 0.0 {
257      let infantry_power_per_unit = infantry / army_size;
258      let cavalry_power_per_unit = cavalry / army_size;
259      let ranged_power_per_unit = ranged / army_size;
260
261      let infantry_necessary_units = offensive_power.infantry / infantry_power_per_unit;
262      let cavalry_necessary_units = offensive_power.cavalry / cavalry_power_per_unit;
263      let ranged_necessary_units = offensive_power.ranged / ranged_power_per_unit;
264
265      let necessary_units =
266        infantry_necessary_units + cavalry_necessary_units + ranged_necessary_units;
267
268      let infantry_proportion = infantry_necessary_units / necessary_units;
269      let cavalry_proportion = cavalry_necessary_units / necessary_units;
270      let ranged_proportion = ranged_necessary_units / necessary_units;
271
272      infantry = infantry_proportion * army_size * infantry_power_per_unit;
273      cavalry = cavalry_proportion * army_size * cavalry_power_per_unit;
274      ranged = ranged_proportion * army_size * ranged_power_per_unit;
275
276      total = infantry + cavalry + ranged;
277    }
278
279    if let Some(wall) = defending_wall {
280      let mut attacking_rams = offensive_power.rams_amount;
281
282      if attacking_rams > 0.0 {
283        let rams_growth_per_wall_level: f64 = growth()
284          .floor(wall.level)
285          .ceil(200)
286          .max_level(Wall::MAX_LEVEL)
287          .call();
288
289        let mut rams_vec: Vec<f64> = Vec::new();
290        let mut rams_per_wall_level: f64 = f64::from(wall.level);
291
292        for _ in 1..=usize::from(wall.level) {
293          rams_per_wall_level += rams_per_wall_level * rams_growth_per_wall_level;
294          rams_vec.push(rams_per_wall_level * rams_growth_per_wall_level);
295        }
296
297        let mut wall_levels_to_decrease = 0;
298        for value in rams_vec.iter().rev() {
299          if attacking_rams >= *value && wall_levels_to_decrease < u8::from(wall.level) {
300            attacking_rams -= value;
301            wall_levels_to_decrease += 1;
302          }
303        }
304
305        let new_wall = infrastructure_stats
306          .wall()
307          .get(BuildingLevel::new(
308            u8::from(wall.level) - wall_levels_to_decrease,
309          ))?;
310
311        total +=
312          f64::from(new_wall.defense) + ((f64::from(new_wall.defense_percent) / 100.0) * total);
313      } else {
314        total += f64::from(wall.defense) + ((f64::from(wall.defense_percent) / 100.0) * total);
315      }
316    }
317
318    Ok(DefensivePower { total })
319  }
320}