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