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::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 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 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}