1use crate::data::{AbilityData, ActionMode, AgentData, AttackData, Path, ReadData, TargetData};
2use common::{
3 comp::{
4 Agent, Alignment, Body, Controller, InputKind, Pos, Scale,
5 ability::AbilityInput,
6 agent::Psyche,
7 buff::BuffKind,
8 item::{ItemDesc, ItemTag},
9 },
10 terrain::Block,
11 uid::Uid,
12 util::Dir,
13 vol::ReadVol,
14};
15use core::f32::consts::PI;
16use rand::RngExt;
17use specs::Entity as EcsEntity;
18use vek::*;
19
20pub fn is_dead_or_invulnerable(entity: EcsEntity, read_data: &ReadData) -> bool {
21 is_dead(entity, read_data) || is_invulnerable(entity, read_data)
22}
23
24pub fn is_dead(entity: EcsEntity, read_data: &ReadData) -> bool {
25 let health = read_data.healths.get(entity);
26 health.is_some_and(|a| a.is_dead)
27}
28
29pub fn is_invulnerable(entity: EcsEntity, read_data: &ReadData) -> bool {
32 let buffs = read_data.buffs.get(entity);
33
34 buffs.is_some_and(|b| b.kinds[BuffKind::Invulnerability].is_some())
35}
36
37pub fn is_steering(entity: EcsEntity, read_data: &ReadData) -> bool {
38 read_data
39 .is_volume_riders
40 .get(entity)
41 .is_some_and(|r| r.is_steering_entity())
42}
43
44fn try_owner_alignment<'a>(
47 alignment: Option<&'a Alignment>,
48 read_data: &'a ReadData,
49) -> Option<&'a Alignment> {
50 if let Some(&Alignment::Owned(owner_uid)) = alignment
51 && let Some(owner) = get_entity_by_id(owner_uid, read_data)
52 {
53 return read_data.alignments.get(owner);
54 }
55 alignment
56}
57
58pub fn get_entity_by_id(uid: Uid, read_data: &ReadData) -> Option<EcsEntity> {
59 read_data.id_maps.uid_entity(uid)
60}
61
62pub fn stop_pursuing(
67 dist_to_target_sqrd: f32,
68 dist_to_home_sqrd: f32,
69 own_health_fraction: f32,
70 target_health_fraction: f32,
71 dur_since_last_attacked: f64,
72 psyche: &Psyche,
73) -> bool {
74 psyche.should_stop_pursuing
75 && should_let_target_escape(
76 dist_to_home_sqrd,
77 dur_since_last_attacked,
78 own_health_fraction,
79 ) > should_continue_to_pursue(dist_to_target_sqrd, psyche, target_health_fraction)
80}
81
82fn should_continue_to_pursue(
84 dist_to_target_sqrd: f32,
85 psyche: &Psyche,
86 target_health_fraction: f32,
87) -> f32 {
88 let aggression_score = (1.0 / psyche.flee_health.max(0.25))
89 * psyche.aggro_dist.unwrap_or(psyche.sight_dist)
90 * psyche.sight_dist;
91
92 (100.0 * aggression_score) / (dist_to_target_sqrd * target_health_fraction)
93}
94
95fn should_let_target_escape(
98 dist_to_home_sqrd: f32,
99 dur_since_last_attacked: f64,
100 own_health_fraction: f32,
101) -> f32 {
102 (dist_to_home_sqrd / own_health_fraction) * dur_since_last_attacked as f32 * 0.005
103}
104
105pub(crate) fn is_villager(alignment: Option<&Alignment>) -> bool {
107 alignment.is_some_and(|alignment| matches!(alignment, Alignment::Npc))
108}
109
110pub(crate) fn is_village_guard(agent: &Agent) -> bool {
111 agent.mark == Some(common::comp::agent::Mark::Guard)
112}
113
114pub(crate) fn are_our_owners_hostile(
115 our_alignment: Option<&Alignment>,
116 their_alignment: Option<&Alignment>,
117 read_data: &ReadData,
118) -> bool {
119 try_owner_alignment(our_alignment, read_data).is_some_and(|our_owners_alignment| {
120 try_owner_alignment(their_alignment, read_data).is_some_and(|their_owners_alignment| {
121 our_owners_alignment.hostile_towards(*their_owners_alignment)
122 })
123 })
124}
125
126pub(crate) fn entities_have_line_of_sight(
127 pos: &Pos,
128 body: Option<&Body>,
129 scale: f32,
130 other_pos: &Pos,
131 other_body: Option<&Body>,
132 other_scale: Option<&Scale>,
133 read_data: &ReadData,
134) -> bool {
135 let get_eye_pos = |pos: &Pos, body: Option<&Body>, scale: f32| {
136 let eye_offset = body.map_or(0.0, |b| b.eye_height(scale));
137
138 Pos(pos.0.with_z(pos.0.z + eye_offset))
139 };
140 let eye_pos = get_eye_pos(pos, body, scale);
141 let other_eye_pos = get_eye_pos(other_pos, other_body, other_scale.map_or(1.0, |s| s.0));
142
143 positions_have_line_of_sight(&eye_pos, &other_eye_pos, read_data)
144}
145
146fn positions_have_line_of_sight(pos_a: &Pos, pos_b: &Pos, read_data: &ReadData) -> bool {
147 let dist_sqrd = pos_b.0.distance_squared(pos_a.0);
148
149 read_data
150 .terrain
151 .ray(pos_a.0, pos_b.0)
152 .until(Block::is_opaque)
153 .cast()
154 .0
155 .powi(2)
156 >= (dist_sqrd - 0.01)
157}
158
159pub(crate) fn is_dressed_as_cultist(entity: EcsEntity, read_data: &ReadData) -> bool {
160 read_data.inventories.get(entity).is_some_and(|inventory| {
161 inventory
162 .equipped_items()
163 .filter(|item| item.tags().contains(&ItemTag::Cultist))
164 .count()
165 > 2
166 })
167}
168
169pub(crate) fn is_dressed_as_witch(entity: EcsEntity, read_data: &ReadData) -> bool {
170 read_data.inventories.get(entity).is_some_and(|inventory| {
171 inventory
172 .equipped_items()
173 .filter(|item| item.tags().contains(&ItemTag::Witch))
174 .count()
175 > 3
176 })
177}
178
179pub(crate) fn is_dressed_as_pirate(entity: EcsEntity, read_data: &ReadData) -> bool {
180 read_data.inventories.get(entity).is_some_and(|inventory| {
181 inventory
182 .equipped_items()
183 .filter(|item| item.tags().contains(&ItemTag::Pirate))
184 .count()
185 > 4
186 })
187}
188
189pub(crate) fn get_attacker(entity: EcsEntity, read_data: &ReadData) -> Option<EcsEntity> {
190 read_data
191 .healths
192 .get(entity)
193 .filter(|health| health.last_change.amount < 0.0)
194 .and_then(|health| health.last_change.damage_by())
195 .and_then(|damage_contributor| get_entity_by_id(damage_contributor.uid(), read_data))
196}
197
198impl AgentData<'_> {
199 pub fn has_buff(&self, read_data: &ReadData, buff: BuffKind) -> bool {
200 read_data
201 .buffs
202 .get(*self.entity)
203 .is_some_and(|b| b.kinds[buff].is_some())
204 }
205
206 pub fn extract_ability(&self, input: AbilityInput) -> Option<AbilityData> {
207 AbilityData::from_ability(
208 &self
209 .active_abilities
210 .activate_ability(
211 input,
212 Some(self.inventory),
213 self.skill_set,
214 self.body,
215 Some(self.char_state),
216 self.stance,
217 self.combo,
218 self.stats,
219 self.buffs,
220 )
221 .map_or(Default::default(), |a| a.0),
222 )
223 }
224}
225
226pub(crate) fn handle_attack_aggression(
232 agent_data: &AgentData,
233 agent: &mut Agent,
234 controller: &mut Controller,
235 attack_data: &AttackData,
236 tgt_data: &TargetData,
237 read_data: &ReadData,
238 rng: &mut impl RngExt,
239 timer_pos_timeout_index: usize,
240 timer_guarded_cycle_index: usize,
241 fcounter_guarded_timer_index: usize,
242 icounter_action_mode_index: usize,
243 condition_guarded_defend_index: usize,
244 condition_rolling_breakthrough_index: usize,
245 position_guarded_cover_index: usize,
246 position_flee_index: usize,
247) -> bool {
248 if let Some(health) = agent_data.health {
249 agent.combat_state.int_counters[icounter_action_mode_index] = if health.fraction() < 0.1 {
250 agent.combat_state.positions[position_guarded_cover_index] = None;
251 ActionMode::Fleeing as u8
252 } else if health.fraction() < 0.9 {
253 agent.combat_state.positions[position_flee_index] = None;
254 ActionMode::Guarded as u8
255 } else {
256 agent.combat_state.positions[position_guarded_cover_index] = None;
257 agent.combat_state.positions[position_flee_index] = None;
258 ActionMode::Reckless as u8
259 };
260 }
261
262 if agent_data.vel.0.magnitude_squared() < 1_f32.powi(2) {
265 agent.combat_state.timers[timer_pos_timeout_index] += read_data.dt.0;
266 } else {
267 agent.combat_state.timers[timer_pos_timeout_index] = 0.0;
268 }
269
270 if agent.combat_state.timers[timer_pos_timeout_index] > 2.0 {
271 agent.combat_state.positions[position_guarded_cover_index] = None;
272 agent.combat_state.positions[position_flee_index] = None;
273 agent.combat_state.timers[timer_pos_timeout_index] = 0.0;
274 }
275
276 match ActionMode::from_u8(agent.combat_state.int_counters[icounter_action_mode_index]) {
277 ActionMode::Reckless => true,
278 ActionMode::Guarded => {
279 agent.combat_state.timers[timer_guarded_cycle_index] += read_data.dt.0;
280 if agent.combat_state.timers[timer_guarded_cycle_index]
281 > agent.combat_state.counters[fcounter_guarded_timer_index]
282 {
283 agent.combat_state.timers[timer_guarded_cycle_index] = 0.0;
284 agent.combat_state.conditions[condition_guarded_defend_index] ^= true;
285 agent.combat_state.counters[fcounter_guarded_timer_index] =
286 if agent.combat_state.conditions[condition_guarded_defend_index] {
287 rng.random_range(3.0..6.0)
288 } else {
289 rng.random_range(6.0..10.0)
290 };
291 }
292 if let Some(pos) = agent.combat_state.positions[position_guarded_cover_index]
293 && pos.distance_squared(agent_data.pos.0) < 3_f32.powi(2)
294 {
295 agent.combat_state.positions[position_guarded_cover_index] = None;
296 }
297 if !agent.combat_state.conditions[condition_guarded_defend_index] {
298 agent.combat_state.positions[position_guarded_cover_index] = None;
299 true
300 } else {
301 if attack_data.dist_sqrd > 10_f32.powi(2) {
302 if let Some(pos) = agent.combat_state.positions[position_guarded_cover_index] {
305 if pos.distance_squared(agent_data.pos.0) < 5_f32.powi(2) {
306 agent.combat_state.positions[position_guarded_cover_index] = None;
307 }
308 agent_data.path_toward_target(
309 agent,
310 controller,
311 pos,
312 read_data,
313 Path::Separate,
314 None,
315 );
316 } else {
317 agent.combat_state.positions[position_guarded_cover_index] = {
318 let rand_dir = {
319 let dir = (tgt_data.pos.0 - agent_data.pos.0)
320 .try_normalized()
321 .unwrap_or(Vec3::unit_x())
322 .xy();
323 if rng.random_bool(0.5) {
324 dir.rotated_z(PI / 2.0 + rng.random_range(-0.75..0.0))
325 } else {
326 dir.rotated_z(-PI / 2.0 + rng.random_range(-0.0..0.75))
327 }
328 };
329 let attempted_dist = rng.random_range(6.0..16.0);
330 let actual_dist = read_data
331 .terrain
332 .ray(
333 agent_data.pos.0 + Vec3::unit_z() * 0.5,
334 agent_data.pos.0
335 + Vec3::unit_z() * 0.5
336 + rand_dir * attempted_dist,
337 )
338 .until(Block::is_solid)
339 .cast()
340 .0
341 - 1.0;
342 Some(agent_data.pos.0 + rand_dir * actual_dist)
343 };
344 }
345 } else if let Some(pos) = agent.combat_state.positions[position_guarded_cover_index]
346 {
347 agent_data.path_toward_target(
348 agent,
349 controller,
350 pos,
351 read_data,
352 Path::Separate,
353 None,
354 );
355 if agent.combat_state.conditions[condition_rolling_breakthrough_index] {
356 controller.push_basic_input(InputKind::Roll);
357 agent.combat_state.conditions[condition_rolling_breakthrough_index] = false;
358 }
359 if tgt_data.char_state.is_some_and(|cs| {
360 cs.is_melee_attack() && cs.is_blockable().unwrap_or(false)
361 }) {
362 controller.push_basic_input(InputKind::Block);
363 }
364 } else {
365 agent.combat_state.positions[position_guarded_cover_index] = {
366 let backwards = (agent_data.pos.0 - tgt_data.pos.0)
367 .try_normalized()
368 .unwrap_or(Vec3::unit_x())
369 .xy();
370 let pos = if read_data
371 .terrain
372 .ray(
373 agent_data.pos.0 + Vec3::unit_z() * 0.5,
374 agent_data.pos.0 + Vec3::unit_z() * 0.5 + backwards * 6.0,
375 )
376 .until(Block::is_solid)
377 .cast()
378 .0
379 > 5.0
380 {
381 agent_data.pos.0 + backwards * 5.0
382 } else {
383 agent.combat_state.conditions[condition_rolling_breakthrough_index] =
384 true;
385 agent_data.pos.0
386 - backwards
387 * read_data
388 .terrain
389 .ray(
390 agent_data.pos.0 + Vec3::unit_z() * 0.5,
391 agent_data.pos.0 + Vec3::unit_z() * 0.5
392 - backwards * 10.0,
393 )
394 .until(Block::is_solid)
395 .cast()
396 .0
397 - 1.0
398 };
399 Some(pos)
400 }
401 }
402 false
403 }
404 },
405 ActionMode::Fleeing => {
406 if agent.combat_state.conditions[condition_rolling_breakthrough_index] {
407 controller.push_basic_input(InputKind::Roll);
408 agent.combat_state.conditions[condition_rolling_breakthrough_index] = false;
409 }
410 if let Some(pos) = agent.combat_state.positions[position_flee_index] {
411 if let Some(dir) = Dir::from_unnormalized(pos - agent_data.pos.0) {
412 controller.inputs.look_dir = dir;
413 }
414 if pos.distance_squared(agent_data.pos.0) < 5_f32.powi(2) {
415 agent.combat_state.positions[position_flee_index] = None;
416 }
417 agent_data.path_toward_target(
418 agent,
419 controller,
420 pos,
421 read_data,
422 Path::Separate,
423 None,
424 );
425 } else {
426 agent.combat_state.positions[position_flee_index] = {
427 let rand_dir = {
428 let dir = (agent_data.pos.0 - tgt_data.pos.0)
429 .try_normalized()
430 .unwrap_or(Vec3::unit_x())
431 .xy();
432 dir.rotated_z(rng.random_range(-0.75..0.75))
433 };
434 let attempted_dist = rng.random_range(16.0..26.0);
435 let actual_dist = read_data
436 .terrain
437 .ray(
438 agent_data.pos.0 + Vec3::unit_z() * 0.5,
439 agent_data.pos.0 + Vec3::unit_z() * 0.5 + rand_dir * attempted_dist,
440 )
441 .until(Block::is_solid)
442 .cast()
443 .0
444 - 1.0;
445 if actual_dist < 10.0 {
446 let dist = read_data
447 .terrain
448 .ray(
449 agent_data.pos.0 + Vec3::unit_z() * 0.5,
450 agent_data.pos.0 + Vec3::unit_z() * 0.5 - rand_dir * attempted_dist,
451 )
452 .until(Block::is_solid)
453 .cast()
454 .0
455 - 1.0;
456 agent.combat_state.conditions[condition_rolling_breakthrough_index] = true;
457 Some(agent_data.pos.0 - rand_dir * dist)
458 } else {
459 Some(agent_data.pos.0 + rand_dir * actual_dist)
460 }
461 };
462 }
463 false
464 },
465 }
466}