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
83    let attack = world.military().attack_of(ruler.clone());
84    let mut defense_cache = HashMap::new();
85
86    for target in targets {
87      let coord = target.coord();
88      let owner = world.continent().owner_of(coord)?;
89
90      let defense = *defense_cache
91        .entry(owner.clone())
92        .or_insert_with(|| {
93          world
94            .military()
95            .defense_of(owner.clone())
96            .mean()
97        });
98
99      if *attack > (defense * 2) {
100        let behavior = PlunderTargetBehavior::builder()
101          .origin(self.origin)
102          .target(coord)
103          .build()
104          .boxed();
105
106        behaviors.push(behavior);
107      }
108    }
109
110    drop(defense_cache);
111
112    BehaviorProcessor::new(world, behaviors)
113      .take(1)
114      .try_each()?;
115
116    Ok(ControlFlow::Break(()))
117  }
118}
119
120#[derive(Builder, Debug)]
121pub struct PlunderTargetBehavior {
122  origin: Coord,
123  target: Coord,
124}
125
126impl PlunderTargetBehavior {
127  fn attacker<'a>(&self, world: &'a World) -> Result<&'a Ruler> {
128    world.continent().owner_of(self.origin)
129  }
130
131  fn attacker_personnel(&self, world: &World) -> Result<ArmyPersonnel> {
132    let attacker = self.attacker(world)?;
133    world
134      .military()
135      .idle_armies_at(self.origin)
136      .filter(|army| army.is_owned_by(attacker))
137      .sum::<ArmyPersonnel>()
138      .pipe(Ok)
139  }
140
141  fn defender<'a>(&self, world: &'a World) -> Result<&'a Ruler> {
142    world.continent().owner_of(self.target)
143  }
144
145  fn defender_personnel(&self, world: &World) -> ArmyPersonnel {
146    world
147      .military()
148      .fold_idle_personnel_at(self.target)
149  }
150}
151
152impl Behavior for PlunderTargetBehavior {
153  fn score(&self, world: &World) -> Result<BehaviorScore> {
154    let attacker_personnel = self.attacker_personnel(world)?;
155    if attacker_personnel.is_empty() {
156      return Ok(BehaviorScore::MIN);
157    }
158
159    let defender_personnel = self.defender_personnel(world);
160    let wall = world
161      .infrastructure(self.target)?
162      .wall()
163      .level();
164
165    let result = world.simulate_battle(
166      &attacker_personnel.to_vec(),
167      &defender_personnel.to_vec(),
168      Luck::new(0),
169      wall,
170    )?;
171
172    if result.winner().is_defender() {
173      return Ok(BehaviorScore::MIN);
174    }
175
176    let attack = result
177      .attacker_surviving_personnel()
178      .attack()
179      .conv::<f64>();
180
181    let original_attack = result
182      .attacker_personnel()
183      .attack()
184      .conv::<f64>();
185
186    let surviving_ratio = attack / original_attack;
187
188    let mut score = if surviving_ratio < 0.5 {
189      BehaviorScore::MIN
190    } else {
191      BehaviorScore::new(surviving_ratio)
192    };
193
194    let attacker = self.attacker(world)?;
195    let Some(attacker_ethics) = world.get_ethics(attacker)? else {
196      return Ok(BehaviorScore::MIN);
197    };
198
199    // Defender may be a player, so no ethics.
200    let defender = self.defender(world)?;
201    if let Some(defender_ethics) = world.get_ethics(defender)? {
202      let attacker_truth_ethics = attacker_ethics.truth();
203      let defender_truth_ethics = defender_ethics.truth();
204
205      // Rulers shouldn't attack those who share their truth ethics,
206      // unless they're fanatic militarists. In that case, there should
207      // be a small chance of them attacking anyway.
208      if attacker_truth_ethics.is_same_variant(defender_truth_ethics) {
209        if attacker_ethics.is_fanatic_militarist() {
210          score *= 0.2;
211        } else {
212          return Ok(BehaviorScore::MIN);
213        }
214      }
215    }
216
217    Ok(score)
218  }
219
220  fn behave(&self, world: &mut World) -> Result<ControlFlow<()>> {
221    let attacker_personnel = self.attacker_personnel(world)?;
222    let request = ManeuverRequest::builder()
223      .kind(ManeuverKind::Attack)
224      .origin(self.origin)
225      .destination(self.target)
226      .personnel(attacker_personnel)
227      .build();
228
229    let _id = world.request_maneuver_with_emit(request, false)?;
230
231    Ok(ControlFlow::Break(()))
232  }
233}