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 terrain::Block,
11 uid::Uid,
12 util::Dir,
13 vol::ReadVol,
14};
15use core::f32::consts::PI;
16use rand::Rng;
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
44pub fn 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 fn entity_looks_like_cultist(entity: EcsEntity, read_data: &ReadData) -> bool {
106 let number_of_cultist_items_equipped = read_data.inventories.get(entity).map_or(0, |inv| {
107 inv.equipped_items()
108 .filter(|item| item.tags().contains(&ItemTag::Cultist))
109 .count()
110 });
111
112 number_of_cultist_items_equipped > 2
113}
114
115pub fn is_villager(alignment: Option<&Alignment>) -> bool {
117 alignment.is_some_and(|alignment| matches!(alignment, Alignment::Npc))
118}
119
120pub fn is_village_guard(_entity: EcsEntity, _read_data: &ReadData) -> bool {
121 false
126 }
133
134pub fn are_our_owners_hostile(
135 our_alignment: Option<&Alignment>,
136 their_alignment: Option<&Alignment>,
137 read_data: &ReadData,
138) -> bool {
139 try_owner_alignment(our_alignment, read_data).is_some_and(|our_owners_alignment| {
140 try_owner_alignment(their_alignment, read_data).is_some_and(|their_owners_alignment| {
141 our_owners_alignment.hostile_towards(*their_owners_alignment)
142 })
143 })
144}
145
146pub fn entities_have_line_of_sight(
147 pos: &Pos,
148 body: Option<&Body>,
149 scale: f32,
150 other_pos: &Pos,
151 other_body: Option<&Body>,
152 other_scale: Option<&Scale>,
153 read_data: &ReadData,
154) -> bool {
155 let get_eye_pos = |pos: &Pos, body: Option<&Body>, scale: f32| {
156 let eye_offset = body.map_or(0.0, |b| b.eye_height(scale));
157
158 Pos(pos.0.with_z(pos.0.z + eye_offset))
159 };
160 let eye_pos = get_eye_pos(pos, body, scale);
161 let other_eye_pos = get_eye_pos(other_pos, other_body, other_scale.map_or(1.0, |s| s.0));
162
163 positions_have_line_of_sight(&eye_pos, &other_eye_pos, read_data)
164}
165
166pub fn positions_have_line_of_sight(pos_a: &Pos, pos_b: &Pos, read_data: &ReadData) -> bool {
167 let dist_sqrd = pos_b.0.distance_squared(pos_a.0);
168
169 read_data
170 .terrain
171 .ray(pos_a.0, pos_b.0)
172 .until(Block::is_opaque)
173 .cast()
174 .0
175 .powi(2)
176 >= (dist_sqrd - 0.01)
177}
178
179pub fn is_dressed_as_cultist(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::Cultist))
184 .count()
185 > 2
186 })
187}
188
189pub fn is_dressed_as_witch(entity: EcsEntity, read_data: &ReadData) -> bool {
190 read_data.inventories.get(entity).is_some_and(|inventory| {
191 inventory
192 .equipped_items()
193 .filter(|item| item.tags().contains(&ItemTag::Witch))
194 .count()
195 > 3
196 })
197}
198
199pub fn is_dressed_as_pirate(entity: EcsEntity, read_data: &ReadData) -> bool {
200 read_data.inventories.get(entity).is_some_and(|inventory| {
201 inventory
202 .equipped_items()
203 .filter(|item| item.tags().contains(&ItemTag::Pirate))
204 .count()
205 > 4
206 })
207}
208
209pub fn get_attacker(entity: EcsEntity, read_data: &ReadData) -> Option<EcsEntity> {
210 read_data
211 .healths
212 .get(entity)
213 .filter(|health| health.last_change.amount < 0.0)
214 .and_then(|health| health.last_change.damage_by())
215 .and_then(|damage_contributor| get_entity_by_id(damage_contributor.uid(), read_data))
216}
217
218impl AgentData<'_> {
219 pub fn has_buff(&self, read_data: &ReadData, buff: BuffKind) -> bool {
220 read_data
221 .buffs
222 .get(*self.entity)
223 .is_some_and(|b| b.kinds[buff].is_some())
224 }
225
226 pub fn extract_ability(&self, input: AbilityInput) -> Option<AbilityData> {
227 let context = AbilityContext::from(self.stance, Some(self.inventory), self.combo);
228 AbilityData::from_ability(
229 &self
230 .active_abilities
231 .activate_ability(
232 input,
233 Some(self.inventory),
234 self.skill_set,
235 self.body,
236 Some(self.char_state),
237 &context,
238 self.stats,
239 )
240 .map_or(Default::default(), |a| a.0),
241 )
242 }
243}
244
245pub fn handle_attack_aggression(
251 agent_data: &AgentData,
252 agent: &mut Agent,
253 controller: &mut Controller,
254 attack_data: &AttackData,
255 tgt_data: &TargetData,
256 read_data: &ReadData,
257 rng: &mut impl Rng,
258 timer_pos_timeout_index: usize,
259 timer_guarded_cycle_index: usize,
260 fcounter_guarded_timer_index: usize,
261 icounter_action_mode_index: usize,
262 condition_guarded_defend_index: usize,
263 condition_rolling_breakthrough_index: usize,
264 position_guarded_cover_index: usize,
265 position_flee_index: usize,
266) -> bool {
267 if let Some(health) = agent_data.health {
268 agent.combat_state.int_counters[icounter_action_mode_index] = if health.fraction() < 0.1 {
269 agent.combat_state.positions[position_guarded_cover_index] = None;
270 ActionMode::Fleeing as u8
271 } else if health.fraction() < 0.9 {
272 agent.combat_state.positions[position_flee_index] = None;
273 ActionMode::Guarded as u8
274 } else {
275 agent.combat_state.positions[position_guarded_cover_index] = None;
276 agent.combat_state.positions[position_flee_index] = None;
277 ActionMode::Reckless as u8
278 };
279 }
280
281 if agent_data.vel.0.magnitude_squared() < 1_f32.powi(2) {
284 agent.combat_state.timers[timer_pos_timeout_index] += read_data.dt.0;
285 } else {
286 agent.combat_state.timers[timer_pos_timeout_index] = 0.0;
287 }
288
289 if agent.combat_state.timers[timer_pos_timeout_index] > 2.0 {
290 agent.combat_state.positions[position_guarded_cover_index] = None;
291 agent.combat_state.positions[position_flee_index] = None;
292 agent.combat_state.timers[timer_pos_timeout_index] = 0.0;
293 }
294
295 match ActionMode::from_u8(agent.combat_state.int_counters[icounter_action_mode_index]) {
296 ActionMode::Reckless => true,
297 ActionMode::Guarded => {
298 agent.combat_state.timers[timer_guarded_cycle_index] += read_data.dt.0;
299 if agent.combat_state.timers[timer_guarded_cycle_index]
300 > agent.combat_state.counters[fcounter_guarded_timer_index]
301 {
302 agent.combat_state.timers[timer_guarded_cycle_index] = 0.0;
303 agent.combat_state.conditions[condition_guarded_defend_index] ^= true;
304 agent.combat_state.counters[fcounter_guarded_timer_index] =
305 if agent.combat_state.conditions[condition_guarded_defend_index] {
306 rng.random_range(3.0..6.0)
307 } else {
308 rng.random_range(6.0..10.0)
309 };
310 }
311 if let Some(pos) = agent.combat_state.positions[position_guarded_cover_index]
312 && pos.distance_squared(agent_data.pos.0) < 3_f32.powi(2)
313 {
314 agent.combat_state.positions[position_guarded_cover_index] = None;
315 }
316 if !agent.combat_state.conditions[condition_guarded_defend_index] {
317 agent.combat_state.positions[position_guarded_cover_index] = None;
318 true
319 } else {
320 if attack_data.dist_sqrd > 10_f32.powi(2) {
321 if let Some(pos) = agent.combat_state.positions[position_guarded_cover_index] {
324 if pos.distance_squared(agent_data.pos.0) < 5_f32.powi(2) {
325 agent.combat_state.positions[position_guarded_cover_index] = None;
326 }
327 agent_data.path_toward_target(
328 agent,
329 controller,
330 pos,
331 read_data,
332 Path::Separate,
333 None,
334 );
335 } else {
336 agent.combat_state.positions[position_guarded_cover_index] = {
337 let rand_dir = {
338 let dir = (tgt_data.pos.0 - agent_data.pos.0)
339 .try_normalized()
340 .unwrap_or(Vec3::unit_x())
341 .xy();
342 if rng.random_bool(0.5) {
343 dir.rotated_z(PI / 2.0 + rng.random_range(-0.75..0.0))
344 } else {
345 dir.rotated_z(-PI / 2.0 + rng.random_range(-0.0..0.75))
346 }
347 };
348 let attempted_dist = rng.random_range(6.0..16.0);
349 let actual_dist = read_data
350 .terrain
351 .ray(
352 agent_data.pos.0 + Vec3::unit_z() * 0.5,
353 agent_data.pos.0
354 + Vec3::unit_z() * 0.5
355 + rand_dir * attempted_dist,
356 )
357 .until(Block::is_solid)
358 .cast()
359 .0
360 - 1.0;
361 Some(agent_data.pos.0 + rand_dir * actual_dist)
362 };
363 }
364 } else if let Some(pos) = agent.combat_state.positions[position_guarded_cover_index]
365 {
366 agent_data.path_toward_target(
367 agent,
368 controller,
369 pos,
370 read_data,
371 Path::Separate,
372 None,
373 );
374 if agent.combat_state.conditions[condition_rolling_breakthrough_index] {
375 controller.push_basic_input(InputKind::Roll);
376 agent.combat_state.conditions[condition_rolling_breakthrough_index] = false;
377 }
378 if tgt_data.char_state.is_some_and(|cs| {
379 cs.is_melee_attack() && cs.is_blockable().unwrap_or(false)
380 }) {
381 controller.push_basic_input(InputKind::Block);
382 }
383 } else {
384 agent.combat_state.positions[position_guarded_cover_index] = {
385 let backwards = (agent_data.pos.0 - tgt_data.pos.0)
386 .try_normalized()
387 .unwrap_or(Vec3::unit_x())
388 .xy();
389 let pos = if read_data
390 .terrain
391 .ray(
392 agent_data.pos.0 + Vec3::unit_z() * 0.5,
393 agent_data.pos.0 + Vec3::unit_z() * 0.5 + backwards * 6.0,
394 )
395 .until(Block::is_solid)
396 .cast()
397 .0
398 > 5.0
399 {
400 agent_data.pos.0 + backwards * 5.0
401 } else {
402 agent.combat_state.conditions[condition_rolling_breakthrough_index] =
403 true;
404 agent_data.pos.0
405 - backwards
406 * read_data
407 .terrain
408 .ray(
409 agent_data.pos.0 + Vec3::unit_z() * 0.5,
410 agent_data.pos.0 + Vec3::unit_z() * 0.5
411 - backwards * 10.0,
412 )
413 .until(Block::is_solid)
414 .cast()
415 .0
416 - 1.0
417 };
418 Some(pos)
419 }
420 }
421 false
422 }
423 },
424 ActionMode::Fleeing => {
425 if agent.combat_state.conditions[condition_rolling_breakthrough_index] {
426 controller.push_basic_input(InputKind::Roll);
427 agent.combat_state.conditions[condition_rolling_breakthrough_index] = false;
428 }
429 if let Some(pos) = agent.combat_state.positions[position_flee_index] {
430 if let Some(dir) = Dir::from_unnormalized(pos - agent_data.pos.0) {
431 controller.inputs.look_dir = dir;
432 }
433 if pos.distance_squared(agent_data.pos.0) < 5_f32.powi(2) {
434 agent.combat_state.positions[position_flee_index] = None;
435 }
436 agent_data.path_toward_target(
437 agent,
438 controller,
439 pos,
440 read_data,
441 Path::Separate,
442 None,
443 );
444 } else {
445 agent.combat_state.positions[position_flee_index] = {
446 let rand_dir = {
447 let dir = (agent_data.pos.0 - tgt_data.pos.0)
448 .try_normalized()
449 .unwrap_or(Vec3::unit_x())
450 .xy();
451 dir.rotated_z(rng.random_range(-0.75..0.75))
452 };
453 let attempted_dist = rng.random_range(16.0..26.0);
454 let actual_dist = read_data
455 .terrain
456 .ray(
457 agent_data.pos.0 + Vec3::unit_z() * 0.5,
458 agent_data.pos.0 + Vec3::unit_z() * 0.5 + rand_dir * attempted_dist,
459 )
460 .until(Block::is_solid)
461 .cast()
462 .0
463 - 1.0;
464 if actual_dist < 10.0 {
465 let dist = read_data
466 .terrain
467 .ray(
468 agent_data.pos.0 + Vec3::unit_z() * 0.5,
469 agent_data.pos.0 + Vec3::unit_z() * 0.5 - rand_dir * attempted_dist,
470 )
471 .until(Block::is_solid)
472 .cast()
473 .0
474 - 1.0;
475 agent.combat_state.conditions[condition_rolling_breakthrough_index] = true;
476 Some(agent_data.pos.0 - rand_dir * dist)
477 } else {
478 Some(agent_data.pos.0 + rand_dir * actual_dist)
479 }
480 };
481 }
482 false
483 },
484 }
485}