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 get_attacker(entity: EcsEntity, read_data: &ReadData) -> Option<EcsEntity> {
206 read_data
207 .healths
208 .get(entity)
209 .filter(|health| health.last_change.amount < 0.0)
210 .and_then(|health| health.last_change.damage_by())
211 .and_then(|damage_contributor| get_entity_by_id(damage_contributor.uid(), read_data))
212}
213
214impl AgentData<'_> {
215 pub fn has_buff(&self, read_data: &ReadData, buff: BuffKind) -> bool {
216 read_data
217 .buffs
218 .get(*self.entity)
219 .is_some_and(|b| b.kinds[buff].is_some())
220 }
221
222 pub fn extract_ability(&self, input: AbilityInput) -> Option<AbilityData> {
223 let context = AbilityContext::from(self.stance, Some(self.inventory), self.combo);
224 AbilityData::from_ability(
225 &self
226 .active_abilities
227 .activate_ability(
228 input,
229 Some(self.inventory),
230 self.skill_set,
231 self.body,
232 Some(self.char_state),
233 &context,
234 self.stats,
235 )
236 .map_or(Default::default(), |a| a.0),
237 )
238 }
239}
240
241pub fn handle_attack_aggression(
247 agent_data: &AgentData,
248 agent: &mut Agent,
249 controller: &mut Controller,
250 attack_data: &AttackData,
251 tgt_data: &TargetData,
252 read_data: &ReadData,
253 rng: &mut impl Rng,
254 timer_pos_timeout_index: usize,
255 timer_guarded_cycle_index: usize,
256 fcounter_guarded_timer_index: usize,
257 icounter_action_mode_index: usize,
258 condition_guarded_defend_index: usize,
259 condition_rolling_breakthrough_index: usize,
260 position_guarded_cover_index: usize,
261 position_flee_index: usize,
262) -> bool {
263 if let Some(health) = agent_data.health {
264 agent.combat_state.int_counters[icounter_action_mode_index] = if health.fraction() < 0.1 {
265 agent.combat_state.positions[position_guarded_cover_index] = None;
266 ActionMode::Fleeing as u8
267 } else if health.fraction() < 0.9 {
268 agent.combat_state.positions[position_flee_index] = None;
269 ActionMode::Guarded as u8
270 } else {
271 agent.combat_state.positions[position_guarded_cover_index] = None;
272 agent.combat_state.positions[position_flee_index] = None;
273 ActionMode::Reckless as u8
274 };
275 }
276
277 if agent_data.vel.0.magnitude_squared() < 1_f32.powi(2) {
280 agent.combat_state.timers[timer_pos_timeout_index] += read_data.dt.0;
281 } else {
282 agent.combat_state.timers[timer_pos_timeout_index] = 0.0;
283 }
284
285 if agent.combat_state.timers[timer_pos_timeout_index] > 2.0 {
286 agent.combat_state.positions[position_guarded_cover_index] = None;
287 agent.combat_state.positions[position_flee_index] = None;
288 agent.combat_state.timers[timer_pos_timeout_index] = 0.0;
289 }
290
291 match ActionMode::from_u8(agent.combat_state.int_counters[icounter_action_mode_index]) {
292 ActionMode::Reckless => true,
293 ActionMode::Guarded => {
294 agent.combat_state.timers[timer_guarded_cycle_index] += read_data.dt.0;
295 if agent.combat_state.timers[timer_guarded_cycle_index]
296 > agent.combat_state.counters[fcounter_guarded_timer_index]
297 {
298 agent.combat_state.timers[timer_guarded_cycle_index] = 0.0;
299 agent.combat_state.conditions[condition_guarded_defend_index] ^= true;
300 agent.combat_state.counters[fcounter_guarded_timer_index] =
301 if agent.combat_state.conditions[condition_guarded_defend_index] {
302 rng.gen_range(3.0..6.0)
303 } else {
304 rng.gen_range(6.0..10.0)
305 };
306 }
307 if let Some(pos) = agent.combat_state.positions[position_guarded_cover_index] {
308 if pos.distance_squared(agent_data.pos.0) < 3_f32.powi(2) {
309 agent.combat_state.positions[position_guarded_cover_index] = None;
310 }
311 }
312 if !agent.combat_state.conditions[condition_guarded_defend_index] {
313 agent.combat_state.positions[position_guarded_cover_index] = None;
314 true
315 } else {
316 if attack_data.dist_sqrd > 10_f32.powi(2) {
317 if let Some(pos) = agent.combat_state.positions[position_guarded_cover_index] {
320 if pos.distance_squared(agent_data.pos.0) < 5_f32.powi(2) {
321 agent.combat_state.positions[position_guarded_cover_index] = None;
322 }
323 agent_data.path_toward_target(
324 agent,
325 controller,
326 pos,
327 read_data,
328 Path::Separate,
329 None,
330 );
331 } else {
332 agent.combat_state.positions[position_guarded_cover_index] = {
333 let rand_dir = {
334 let dir = (tgt_data.pos.0 - agent_data.pos.0)
335 .try_normalized()
336 .unwrap_or(Vec3::unit_x())
337 .xy();
338 if rng.gen_bool(0.5) {
339 dir.rotated_z(PI / 2.0 + rng.gen_range(-0.75..0.0))
340 } else {
341 dir.rotated_z(-PI / 2.0 + rng.gen_range(-0.0..0.75))
342 }
343 };
344 let attempted_dist = rng.gen_range(6.0..16.0);
345 let actual_dist = read_data
346 .terrain
347 .ray(
348 agent_data.pos.0 + Vec3::unit_z() * 0.5,
349 agent_data.pos.0
350 + Vec3::unit_z() * 0.5
351 + rand_dir * attempted_dist,
352 )
353 .until(Block::is_solid)
354 .cast()
355 .0
356 - 1.0;
357 Some(agent_data.pos.0 + rand_dir * actual_dist)
358 };
359 }
360 } else if let Some(pos) = agent.combat_state.positions[position_guarded_cover_index]
361 {
362 agent_data.path_toward_target(
363 agent,
364 controller,
365 pos,
366 read_data,
367 Path::Separate,
368 None,
369 );
370 if agent.combat_state.conditions[condition_rolling_breakthrough_index] {
371 controller.push_basic_input(InputKind::Roll);
372 agent.combat_state.conditions[condition_rolling_breakthrough_index] = false;
373 }
374 if tgt_data.char_state.is_some_and(|cs| cs.is_melee_attack()) {
375 controller.push_basic_input(InputKind::Block);
376 }
377 } else {
378 agent.combat_state.positions[position_guarded_cover_index] = {
379 let backwards = (agent_data.pos.0 - tgt_data.pos.0)
380 .try_normalized()
381 .unwrap_or(Vec3::unit_x())
382 .xy();
383 let pos = if read_data
384 .terrain
385 .ray(
386 agent_data.pos.0 + Vec3::unit_z() * 0.5,
387 agent_data.pos.0 + Vec3::unit_z() * 0.5 + backwards * 6.0,
388 )
389 .until(Block::is_solid)
390 .cast()
391 .0
392 > 5.0
393 {
394 agent_data.pos.0 + backwards * 5.0
395 } else {
396 agent.combat_state.conditions[condition_rolling_breakthrough_index] =
397 true;
398 agent_data.pos.0
399 - backwards
400 * read_data
401 .terrain
402 .ray(
403 agent_data.pos.0 + Vec3::unit_z() * 0.5,
404 agent_data.pos.0 + Vec3::unit_z() * 0.5
405 - backwards * 10.0,
406 )
407 .until(Block::is_solid)
408 .cast()
409 .0
410 - 1.0
411 };
412 Some(pos)
413 }
414 }
415 false
416 }
417 },
418 ActionMode::Fleeing => {
419 if agent.combat_state.conditions[condition_rolling_breakthrough_index] {
420 controller.push_basic_input(InputKind::Roll);
421 agent.combat_state.conditions[condition_rolling_breakthrough_index] = false;
422 }
423 if let Some(pos) = agent.combat_state.positions[position_flee_index] {
424 if let Some(dir) = Dir::from_unnormalized(pos - agent_data.pos.0) {
425 controller.inputs.look_dir = dir;
426 }
427 if pos.distance_squared(agent_data.pos.0) < 5_f32.powi(2) {
428 agent.combat_state.positions[position_flee_index] = None;
429 }
430 agent_data.path_toward_target(
431 agent,
432 controller,
433 pos,
434 read_data,
435 Path::Separate,
436 None,
437 );
438 } else {
439 agent.combat_state.positions[position_flee_index] = {
440 let rand_dir = {
441 let dir = (agent_data.pos.0 - tgt_data.pos.0)
442 .try_normalized()
443 .unwrap_or(Vec3::unit_x())
444 .xy();
445 dir.rotated_z(rng.gen_range(-0.75..0.75))
446 };
447 let attempted_dist = rng.gen_range(16.0..26.0);
448 let actual_dist = read_data
449 .terrain
450 .ray(
451 agent_data.pos.0 + Vec3::unit_z() * 0.5,
452 agent_data.pos.0 + Vec3::unit_z() * 0.5 + rand_dir * attempted_dist,
453 )
454 .until(Block::is_solid)
455 .cast()
456 .0
457 - 1.0;
458 if actual_dist < 10.0 {
459 let dist = read_data
460 .terrain
461 .ray(
462 agent_data.pos.0 + Vec3::unit_z() * 0.5,
463 agent_data.pos.0 + Vec3::unit_z() * 0.5 - rand_dir * attempted_dist,
464 )
465 .until(Block::is_solid)
466 .cast()
467 .0
468 - 1.0;
469 agent.combat_state.conditions[condition_rolling_breakthrough_index] = true;
470 Some(agent_data.pos.0 - rand_dir * dist)
471 } else {
472 Some(agent_data.pos.0 + rand_dir * actual_dist)
473 }
474 };
475 }
476 false
477 },
478 }
479}