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