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::r#impl::wall::{Wall, WallStats};
11use crate::infrastructure::building::level::{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};
20use strum::EnumIs;
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 = Luck::random())]
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")]
53#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
54pub struct BattleResult {
55  attacker_personnel: ArmyPersonnel,
56  attacker_surviving_personnel: ArmyPersonnel,
57  defender_personnel: ArmyPersonnel,
58  defender_surviving_personnel: ArmyPersonnel,
59  wall_level: BuildingLevel,
60  downgraded_wall_level: BuildingLevelDiff,
61  winner: BattleWinner,
62  luck: Luck,
63}
64
65impl BattleResult {
66  fn new(
67    attacking_squads: &[Squad],
68    defending_squads: &[Squad],
69    luck: Luck,
70    wall: Option<&WallStats>,
71    infrastructure_stats: &InfrastructureStats,
72  ) -> Result<Self> {
73    let attacker_power = OffensivePower::new(attacking_squads, luck);
74    let defender_power = DefensivePower::new(
75      defending_squads,
76      &attacker_power,
77      wall,
78      infrastructure_stats,
79    )?;
80
81    let winner = BattleWinner::determine(&attacker_power, &defender_power);
82
83    let attacker_personnel: ArmyPersonnel = attacking_squads.iter().cloned().collect();
84    let defender_personnel: ArmyPersonnel = defending_squads.iter().cloned().collect();
85
86    let mut attacker_surviving_personnel = ArmyPersonnel::default();
87    let mut defender_surviving_personnel = ArmyPersonnel::default();
88
89    let wall_level = wall
90      .map(|stats| stats.level)
91      .unwrap_or_default();
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
98    let mut downgraded_wall_level = BuildingLevelDiff::new(0);
99    let mut diff = 0.0;
100    let mut squad_survivors: f64;
101
102    match winner {
103      BattleWinner::Attacker => {
104        for squad in attacking_squads {
105          let squad_size = f64::from(squad.size());
106          squad_survivors = squad_size - (squad_size * losses_ratio);
107          attacker_surviving_personnel += Squad::new(squad.id(), squad_survivors);
108        }
109
110        if wall_level > 0 && attacker_power.rams_amount > 0.0 {
111          let remaining_rams =
112            attacker_power.rams_amount - (attacker_power.rams_amount * losses_ratio);
113
114          diff = wall_level
115            * ((attacker_power.rams_amount / 700.0)
116              + ((remaining_rams + 100.0) / 400.0)
117              + ((1.0 - losses_ratio) * 0.3))
118            * 0.4;
119        }
120      }
121      BattleWinner::Defender => {
122        for squad in defending_squads {
123          let squad_size = f64::from(squad.size());
124          squad_survivors = squad_size - (squad_size * losses_ratio);
125          defender_surviving_personnel += Squad::new(squad.id(), squad_survivors);
126        }
127
128        if wall_level > 0 && attacker_power.rams_amount > 0.0 {
129          diff = wall_level * (((attacker_power.rams_amount - 150.0) / 320.0) + losses_ratio) * 0.3;
130        }
131      }
132    }
133
134    if diff > Wall::MAX_LEVEL {
135      downgraded_wall_level = -Wall::MAX_LEVEL;
136    } else if diff > 0.0 {
137      downgraded_wall_level = BuildingLevelDiff::from(-diff);
138    }
139
140    Ok(BattleResult {
141      attacker_personnel,
142      attacker_surviving_personnel,
143      defender_personnel,
144      defender_surviving_personnel,
145      wall_level,
146      downgraded_wall_level,
147      winner,
148      luck,
149    })
150  }
151
152  #[inline]
153  pub fn attacker_personnel(&self) -> &ArmyPersonnel {
154    &self.attacker_personnel
155  }
156
157  #[inline]
158  pub fn attacker_surviving_personnel(&self) -> &ArmyPersonnel {
159    &self.attacker_surviving_personnel
160  }
161
162  #[inline]
163  pub fn defender_personnel(&self) -> &ArmyPersonnel {
164    &self.defender_personnel
165  }
166
167  #[inline]
168  pub fn defender_surviving_personnel(&self) -> &ArmyPersonnel {
169    &self.defender_surviving_personnel
170  }
171
172  pub fn defender_surviving_personnel_ratio(&self) -> f64 {
173    let total = self
174      .defender_personnel
175      .iter()
176      .map(|squad| f64::from(squad.size()))
177      .sum::<f64>();
178
179    let surviving = self
180      .defender_surviving_personnel
181      .iter()
182      .map(|squad| f64::from(squad.size()))
183      .sum::<f64>();
184
185    if total > 0.0 { surviving / total } else { 0.0 }
186  }
187
188  #[inline]
189  pub fn wall_level(&self) -> BuildingLevel {
190    self.wall_level
191  }
192
193  #[inline]
194  pub fn downgraded_wall_level(&self) -> BuildingLevelDiff {
195    self.downgraded_wall_level
196  }
197
198  #[inline]
199  pub fn winner(&self) -> BattleWinner {
200    self.winner
201  }
202
203  #[inline]
204  pub fn luck(&self) -> Luck {
205    self.luck
206  }
207}
208
209#[derive(Copy, Debug, Deserialize, Serialize, EnumIs)]
210#[derive_const(Clone, PartialEq, Eq)]
211#[serde(rename_all = "kebab-case")]
212#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
213pub enum BattleWinner {
214  Attacker,
215  Defender,
216}
217
218impl BattleWinner {
219  fn determine(attacker: &OffensivePower, defender: &DefensivePower) -> Self {
220    if attacker.total > defender.total { Self::Attacker } else { Self::Defender }
221  }
222}
223
224struct OffensivePower {
225  total: f64,
226  infantry: f64,
227  cavalry: f64,
228  ranged: f64,
229  rams_amount: f64,
230}
231
232impl OffensivePower {
233  fn new(squads: &[Squad], luck: Luck) -> Self {
234    let mut infantry = 0.0;
235    let mut cavalry = 0.0;
236    let mut ranged = 0.0;
237    let mut rams_amount = 0.0;
238    let mut ranged_with_debuff = 0.0;
239
240    let mut army_size = 0.0;
241    let mut ranged_amount = 0.0;
242
243    for squad in squads {
244      army_size += f64::from(squad.size());
245
246      match squad.kind() {
247        UnitKind::Infantry => {
248          infantry += squad.attack();
249          if squad.id() == UnitId::Ram {
250            rams_amount = f64::from(squad.size());
251          }
252        }
253        UnitKind::Cavalry => {
254          cavalry += squad.attack();
255        }
256        UnitKind::Ranged => {
257          ranged += squad.attack();
258          ranged_with_debuff += squad.attack() * squad.unit().stats().ranged_debuff();
259          ranged_amount += f64::from(squad.size());
260        }
261      }
262    }
263
264    if ranged_amount / army_size > 0.3 {
265      ranged = ranged_with_debuff;
266    }
267
268    infantry += infantry * luck;
269    cavalry += cavalry * luck;
270    ranged += ranged * luck;
271
272    let total = infantry + cavalry + ranged;
273
274    OffensivePower {
275      total,
276      infantry,
277      cavalry,
278      ranged,
279      rams_amount,
280    }
281  }
282}
283
284struct DefensivePower {
285  total: f64,
286}
287
288impl DefensivePower {
289  fn new(
290    squads: &[Squad],
291    offensive_power: &OffensivePower,
292    defending_wall: Option<&WallStats>,
293    infrastructure_stats: &InfrastructureStats,
294  ) -> Result<Self> {
295    let mut infantry = 0.0;
296    let mut cavalry = 0.0;
297    let mut ranged = 0.0;
298
299    let mut army_size = 0.0;
300
301    for squad in squads {
302      infantry += squad.defense().infantry;
303      cavalry += squad.defense().cavalry;
304      ranged += squad.defense().ranged;
305
306      army_size += f64::from(squad.size());
307    }
308
309    let mut total = 0.0;
310
311    if army_size > 0.0 {
312      let infantry_power_per_unit = infantry / army_size;
313      let cavalry_power_per_unit = cavalry / army_size;
314      let ranged_power_per_unit = ranged / army_size;
315
316      let infantry_necessary_units = offensive_power.infantry / infantry_power_per_unit;
317      let cavalry_necessary_units = offensive_power.cavalry / cavalry_power_per_unit;
318      let ranged_necessary_units = offensive_power.ranged / ranged_power_per_unit;
319
320      let necessary_units =
321        infantry_necessary_units + cavalry_necessary_units + ranged_necessary_units;
322
323      let infantry_proportion = infantry_necessary_units / necessary_units;
324      let cavalry_proportion = cavalry_necessary_units / necessary_units;
325      let ranged_proportion = ranged_necessary_units / necessary_units;
326
327      infantry = infantry_proportion * army_size * infantry_power_per_unit;
328      cavalry = cavalry_proportion * army_size * cavalry_power_per_unit;
329      ranged = ranged_proportion * army_size * ranged_power_per_unit;
330
331      total = infantry + cavalry + ranged;
332    }
333
334    if let Some(wall) = defending_wall {
335      let mut attacking_rams = offensive_power.rams_amount;
336
337      let surviving_rams_no_wall = offensive_power.rams_amount
338        - (offensive_power.rams_amount * (total / offensive_power.total));
339
340      attacking_rams = (attacking_rams * 0.87) + (surviving_rams_no_wall * 1.5);
341
342      if attacking_rams > 0.0 {
343        let rams_growth_per_wall_level: f64 = growth()
344          .floor(wall.level)
345          .ceil(650)
346          .max_level(Wall::MAX_LEVEL)
347          .call();
348
349        let mut rams_vec = Vec::new();
350        let mut rams_per_wall_level = f64::from(wall.level);
351        let mut total_of_rams = 0.0;
352
353        for _ in 1..=usize::from(wall.level) {
354          rams_vec.push(rams_per_wall_level - total_of_rams);
355          total_of_rams = rams_per_wall_level;
356          rams_per_wall_level += rams_per_wall_level * rams_growth_per_wall_level;
357        }
358
359        let mut wall_levels_to_decrease: u8 = 0;
360
361        for value in rams_vec.iter().rev() {
362          if attacking_rams >= *value && wall_levels_to_decrease < wall.level {
363            attacking_rams -= value;
364            wall_levels_to_decrease += 1;
365          } else {
366            break;
367          }
368        }
369
370        if wall.level - wall_levels_to_decrease > 0 {
371          let new_wall = infrastructure_stats
372            .wall()
373            .get(wall.level - (wall_levels_to_decrease))?;
374
375          total += new_wall.defense + ((new_wall.defense_percent / 100.0) * total);
376        }
377      } else if wall.level > 0 {
378        total += wall.defense + ((wall.defense_percent / 100.0) * total);
379      }
380    }
381
382    Ok(DefensivePower { total })
383  }
384}