Skip to main content

nil_core/behavior/impl/
plunder.rs

1// Copyright (C) Call of Nil contributors
2// SPDX-License-Identifier: AGPL-3.0-only
3
4use crate::battle::luck::Luck;
5use crate::behavior::r#impl::idle::IdleBehavior;
6use crate::behavior::score::BehaviorScore;
7use crate::behavior::{Behavior, BehaviorProcessor};
8use crate::continent::coord::Coord;
9use crate::continent::distance::Distance;
10use crate::error::Result;
11use crate::ethic::EthicPowerAxis;
12use crate::infrastructure::building::Building;
13use crate::military::army::personnel::ArmyPersonnel;
14use crate::military::maneuver::{ManeuverKind, ManeuverRequest};
15use crate::military::unit::r#impl::axeman::Axeman;
16use crate::military::unit::stats::power::AttackPower;
17use crate::ruler::Ruler;
18use crate::world::World;
19use bon::Builder;
20use itertools::Itertools;
21use nil_util::iter::IterExt;
22use std::collections::HashMap;
23use std::ops::ControlFlow;
24use tap::{Conv, Pipe};
25
26#[derive(Builder, Debug)]
27pub struct PlunderBehavior {
28  origin: Coord,
29}
30
31impl PlunderBehavior {
32  const MAX_DISTANCE: Distance = Distance::new(20);
33  const MIN_IDLE_POWER: AttackPower = Axeman::STATS.attack() * 100;
34}
35
36impl Behavior for PlunderBehavior {
37  fn score(&self, world: &World) -> Result<BehaviorScore> {
38    let ruler = world.continent().owner_of(self.origin)?;
39    if !ruler.is_bot() {
40      return Ok(BehaviorScore::MIN);
41    }
42
43    if world
44      .military()
45      .idle_armies_at(self.origin)
46      .filter(|army| army.is_owned_by(ruler))
47      .sum::<AttackPower>()
48      .le(&Self::MIN_IDLE_POWER)
49    {
50      return Ok(BehaviorScore::MIN);
51    }
52
53    let Some(ethics) = world.get_ethics(ruler)? else {
54      return Ok(BehaviorScore::MIN);
55    };
56
57    let score = match ethics.power() {
58      EthicPowerAxis::FanaticMilitarist => BehaviorScore::MAX,
59      EthicPowerAxis::Militarist => BehaviorScore::new(0.75),
60      EthicPowerAxis::Pacifist => BehaviorScore::new(0.25),
61      EthicPowerAxis::FanaticPacifist => BehaviorScore::MIN,
62    };
63
64    Ok(score)
65  }
66
67  fn behave(&self, world: &mut World) -> Result<ControlFlow<()>> {
68    let ruler = world.continent().owner_of(self.origin)?;
69    let targets = world
70      .continent()
71      .cities_within(self.origin, Self::MAX_DISTANCE)
72      .filter(|city| {
73        let owner = city.owner();
74        owner != ruler && !owner.is_precursor()
75      })
76      .collect_vec();
77
78    if targets.is_empty() {
79      return Ok(ControlFlow::Break(()));
80    }
81
82    let mut behaviors = vec![IdleBehavior.boxed()];
83
84    let attack = world.military().attack_of(ruler.clone());
85    let mut defense_cache = HashMap::new();
86
87    for target in targets {
88      let coord = target.coord();
89      let owner = world.continent().owner_of(coord)?;
90
91      let defense = *defense_cache
92        .entry(owner.clone())
93        .or_insert_with(|| {
94          world
95            .military()
96            .defense_of(owner.clone())
97            .mean()
98        });
99
100      if *attack > (defense * 2) {
101        let behavior = PlunderTargetBehavior::builder()
102          .origin(self.origin)
103          .target(coord)
104          .build()
105          .boxed();
106
107        behaviors.push(behavior);
108      }
109    }
110
111    drop(defense_cache);
112
113    BehaviorProcessor::new(world, behaviors)
114      .take(1)
115      .try_each()?;
116
117    Ok(ControlFlow::Break(()))
118  }
119}
120
121#[derive(Builder, Debug)]
122pub struct PlunderTargetBehavior {
123  origin: Coord,
124  target: Coord,
125}
126
127impl PlunderTargetBehavior {
128  fn attacker<'a>(&self, world: &'a World) -> Result<&'a Ruler> {
129    world.continent().owner_of(self.origin)
130  }
131
132  fn attacker_personnel(&self, world: &World) -> Result<ArmyPersonnel> {
133    let attacker = self.attacker(world)?;
134    world
135      .military()
136      .idle_armies_at(self.origin)
137      .filter(|army| army.is_owned_by(attacker))
138      .sum::<ArmyPersonnel>()
139      .pipe(Ok)
140  }
141
142  fn defender<'a>(&self, world: &'a World) -> Result<&'a Ruler> {
143    world.continent().owner_of(self.target)
144  }
145
146  fn defender_personnel(&self, world: &World) -> ArmyPersonnel {
147    world
148      .military()
149      .fold_idle_personnel_at(self.target)
150  }
151}
152
153impl Behavior for PlunderTargetBehavior {
154  fn score(&self, world: &World) -> Result<BehaviorScore> {
155    let attacker_personnel = self.attacker_personnel(world)?;
156    if attacker_personnel.is_empty() {
157      return Ok(BehaviorScore::MIN);
158    }
159
160    let defender_personnel = self.defender_personnel(world);
161    let wall = world
162      .infrastructure(self.target)?
163      .wall()
164      .level();
165
166    let result = world.simulate_battle(
167      &attacker_personnel.to_vec(),
168      &defender_personnel.to_vec(),
169      Luck::new(0),
170      wall,
171    )?;
172
173    if result.winner().is_defender() {
174      return Ok(BehaviorScore::MIN);
175    }
176
177    let attack = result
178      .attacker_surviving_personnel()
179      .attack()
180      .conv::<f64>();
181
182    let original_attack = result
183      .attacker_personnel()
184      .attack()
185      .conv::<f64>();
186
187    let surviving_ratio = attack / original_attack;
188
189    let mut score = if surviving_ratio < 0.5 {
190      BehaviorScore::MIN
191    } else {
192      BehaviorScore::new(surviving_ratio)
193    };
194
195    let attacker = self.attacker(world)?;
196    let Some(attacker_ethics) = world.get_ethics(attacker)? else {
197      return Ok(BehaviorScore::MIN);
198    };
199
200    // Defender may be a player, so no ethics.
201    let defender = self.defender(world)?;
202    if let Some(defender_ethics) = world.get_ethics(defender)? {
203      let attacker_truth_ethics = attacker_ethics.truth();
204      let defender_truth_ethics = defender_ethics.truth();
205
206      // Rulers shouldn't attack those who share their truth ethics,
207      // unless they're fanatic militarists. In that case, there should
208      // be a small chance of them attacking anyway.
209      if attacker_truth_ethics.is_same_variant(defender_truth_ethics) {
210        if attacker_ethics.is_fanatic_militarist() {
211          score *= 0.2;
212        } else {
213          return Ok(BehaviorScore::MIN);
214        }
215      }
216    }
217
218    Ok(score)
219  }
220
221  fn behave(&self, world: &mut World) -> Result<ControlFlow<()>> {
222    let attacker_personnel = self.attacker_personnel(world)?;
223    let request = ManeuverRequest::builder()
224      .kind(ManeuverKind::Attack)
225      .origin(self.origin)
226      .destination(self.target)
227      .personnel(attacker_personnel)
228      .build();
229
230    let _id = world.request_maneuver_with_emit(request, false)?;
231
232    Ok(ControlFlow::Break(()))
233  }
234}