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