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