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