nil_core/behavior/impl/
plunder.rs1use 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 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 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}