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::infrastructure::building::wall::WallStats;
10use crate::infrastructure::prelude::BuildingLevel;
11use crate::military::army::personnel::ArmyPersonnel;
12use crate::military::squad::Squad;
13use crate::military::squad::size::SquadSize;
14use crate::military::unit::UnitKind;
15use bon::Builder;
16use luck::Luck;
17use serde::{Deserialize, Serialize};
18
19#[derive(Builder)]
20pub struct Battle<'a> {
21  #[builder(default)]
22  attacker: &'a [Squad],
23
24  #[builder(default)]
25  defender: &'a [Squad],
26
27  #[builder(default)]
28  luck: Luck,
29
30  wall: Option<&'a WallStats>,
31}
32
33impl Battle<'_> {
34  #[inline]
35  pub fn result(self) -> BattleResult {
36    BattleResult::new(self.attacker, self.defender, self.luck, self.wall)
37  }
38}
39
40#[derive(Clone, Debug, Deserialize, Serialize)]
41#[serde(rename_all = "camelCase")]
42pub struct BattleResult {
43  attacker_personnel: ArmyPersonnel,
44  attacker_surviving_personnel: ArmyPersonnel,
45  defender_personnel: ArmyPersonnel,
46  defender_surviving_personnel: ArmyPersonnel,
47  wall_level: BuildingLevel,
48  winner: BattleWinner,
49  luck: Luck,
50}
51
52impl BattleResult {
53  #[rustfmt::skip]
54  fn new(
55    attacking_squads: &[Squad],
56    defending_squads: &[Squad],
57    luck: Luck,
58    wall: Option<&WallStats>
59  ) -> Self {
60    let attacker_power = OffensivePower::new(attacking_squads, luck);
61    let defender_power = DefensivePower::new(defending_squads, &attacker_power, wall);
62
63    let winner = BattleWinner::determine(&attacker_power, &defender_power);
64
65    let attacker_personnel: ArmyPersonnel = attacking_squads.iter().cloned().collect();
66    let defender_personnel: ArmyPersonnel = defending_squads.iter().cloned().collect();
67
68    let mut attacker_surviving_personnel = ArmyPersonnel::default();
69    let mut defender_surviving_personnel = ArmyPersonnel::default();
70
71    let losses_ratio = match winner {
72      BattleWinner::Attacker => defender_power.total / attacker_power.total,
73      BattleWinner::Defender => attacker_power.total / defender_power.total,
74    };
75
76    let mut squad_survivors: f64;
77    match winner {
78      BattleWinner::Attacker => {
79        for squad in attacking_squads {
80          let squad_size = f64::from(squad.size());
81          squad_survivors = squad_size - (squad_size * losses_ratio);
82          attacker_surviving_personnel += Squad::new(squad.id(), SquadSize::from(squad_survivors));
83        }
84      }
85      BattleWinner::Defender => {
86        for squad in defending_squads {
87          let squad_size = f64::from(squad.size());
88          squad_survivors = squad_size - (squad_size * losses_ratio);
89          defender_surviving_personnel += Squad::new(squad.id(), SquadSize::from(squad_survivors));
90        }
91      }
92    }
93
94    let wall_level = wall
95      .map(|stats| stats.level)
96      .unwrap_or_default();
97
98    BattleResult {
99      attacker_personnel,
100      attacker_surviving_personnel,
101      defender_personnel,
102      defender_surviving_personnel,
103      wall_level,
104      winner,
105      luck,
106    }
107  }
108
109  #[inline]
110  pub fn attacker_personnel(&self) -> &ArmyPersonnel {
111    &self.attacker_personnel
112  }
113
114  #[inline]
115  pub fn attacker_surviving_personnel(&self) -> &ArmyPersonnel {
116    &self.attacker_surviving_personnel
117  }
118
119  #[inline]
120  pub fn defender_personnel(&self) -> &ArmyPersonnel {
121    &self.defender_personnel
122  }
123
124  #[inline]
125  pub fn defender_surviving_personnel(&self) -> &ArmyPersonnel {
126    &self.defender_surviving_personnel
127  }
128
129  pub fn defender_surviving_personnel_ratio(&self) -> f64 {
130    let total = self
131      .defender_personnel
132      .iter()
133      .map(|squad| f64::from(squad.size()))
134      .sum::<f64>();
135
136    let surviving = self
137      .defender_surviving_personnel
138      .iter()
139      .map(|squad| f64::from(squad.size()))
140      .sum::<f64>();
141
142    if total > 0.0 { surviving / total } else { 0.0 }
143  }
144}
145
146#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
147#[serde(rename_all = "kebab-case")]
148pub enum BattleWinner {
149  Attacker,
150  Defender,
151}
152
153impl BattleWinner {
154  fn determine(attacker: &OffensivePower, defender: &DefensivePower) -> Self {
155    if attacker.total > defender.total { Self::Attacker } else { Self::Defender }
156  }
157}
158
159struct OffensivePower {
160  total: f64,
161  infantry: f64,
162  cavalry: f64,
163  ranged: f64,
164}
165
166impl OffensivePower {
167  fn new(squads: &[Squad], luck: Luck) -> Self {
168    let mut infantry = 0.0;
169    let mut cavalry = 0.0;
170    let mut ranged = 0.0;
171
172    let mut army_size = 0.0;
173    let mut ranged_size = 0.0;
174
175    for squad in squads {
176      army_size += f64::from(squad.size());
177      match squad.kind() {
178        UnitKind::Infantry => {
179          infantry += *squad.attack();
180        }
181        UnitKind::Cavalry => {
182          cavalry += *squad.attack();
183        }
184        UnitKind::Ranged => {
185          ranged += *squad.attack();
186          ranged_size += f64::from(squad.size());
187        }
188      }
189    }
190
191    if ranged_size / army_size > 0.3 {
192      ranged *= sum_ranged_debuff(squads);
193    }
194    infantry += infantry * luck;
195    cavalry += cavalry * luck;
196    ranged += ranged * luck;
197
198    let total = infantry + cavalry + ranged;
199
200    OffensivePower { total, infantry, cavalry, ranged }
201  }
202}
203
204struct DefensivePower {
205  total: f64,
206}
207
208impl DefensivePower {
209  fn new(
210    squads: &[Squad],
211    offensive_power: &OffensivePower,
212    defending_wall: Option<&WallStats>,
213  ) -> Self {
214    let mut infantry_power = 0.0;
215    let mut cavalry_power = 0.0;
216    let mut ranged_power = 0.0;
217
218    let mut army_size = 0.0;
219    let mut ranged_squad_size = 0.0;
220
221    for squad in squads {
222      if squad.kind() == UnitKind::Ranged {
223        ranged_squad_size += f64::from(squad.size());
224      }
225      army_size += f64::from(squad.size());
226    }
227
228    for squad in squads {
229      if squad.kind() == UnitKind::Ranged && ranged_squad_size / army_size > 0.5 {
230        infantry_power += squad.defense().infantry * sum_ranged_debuff(squads);
231        cavalry_power += squad.defense().cavalry * sum_ranged_debuff(squads);
232        ranged_power += squad.defense().ranged * sum_ranged_debuff(squads);
233      } else {
234        infantry_power += squad.defense().infantry;
235        cavalry_power += squad.defense().cavalry;
236        ranged_power += squad.defense().ranged;
237      }
238    }
239
240    let mut total = 0.0;
241
242    if army_size > 0.0 {
243      let infantry_power_per_unit = infantry_power / army_size;
244      let cavalry_power_per_unit = cavalry_power / army_size;
245      let ranged_power_per_unit = ranged_power / army_size;
246
247      let mut infantry_necessary_units = offensive_power.infantry / infantry_power_per_unit;
248      let mut cavalry_necessary_units = offensive_power.cavalry / cavalry_power_per_unit;
249      let mut ranged_necessary_units = offensive_power.ranged / ranged_power_per_unit;
250
251      let necessary_units =
252        infantry_necessary_units + cavalry_necessary_units + ranged_necessary_units;
253
254      infantry_necessary_units /= necessary_units;
255      cavalry_necessary_units /= necessary_units;
256      ranged_necessary_units /= necessary_units;
257
258      infantry_power = infantry_necessary_units * army_size * infantry_power_per_unit;
259      cavalry_power = cavalry_necessary_units * army_size * cavalry_power_per_unit;
260      ranged_power = ranged_necessary_units * army_size * ranged_power_per_unit;
261
262      total = infantry_power + cavalry_power + ranged_power;
263    }
264
265    if let Some(wall_power) = defending_wall {
266      total = add_wall_power(wall_power, total);
267    }
268
269    DefensivePower { total }
270  }
271}
272
273fn sum_ranged_debuff(squads: &[Squad]) -> f64 {
274  let mut ranged_debuff = 0.0;
275  let mut ranged_amount = 0;
276  for squad in squads {
277    if squad.kind() == UnitKind::Ranged {
278      let size = *squad.size();
279      let stats = squad.unit().stats();
280      ranged_debuff += stats.ranged_debuff() * size;
281      ranged_amount += size;
282    }
283  }
284  ranged_debuff / f64::from(ranged_amount)
285}
286
287fn add_wall_power(wall: &WallStats, current_power: f64) -> f64 {
288  current_power
289    + f64::from(wall.defense)
290    + ((f64::from(wall.defense_percent) / 100.0) * current_power)
291}