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