From d3873e357e0924f690b82563cbcb13a9829e1971 Mon Sep 17 00:00:00 2001 From: holychowders Date: Tue, 29 Mar 2022 19:04:20 -0500 Subject: [PATCH] Agent: Use FOV when scanning for hostile targets and refactor `choose_target()`. - Refactors `choose_target()`, renaming it and extracting functions with more meaningful names and more correct behavior. - Adds FOV for agents scanning for hostile targets. --- common/src/util/spatial_grid.rs | 7 +- server/src/sys/agent.rs | 293 ++++++++++++-------------------- server/src/sys/agent/attack.rs | 222 +++++------------------- server/src/sys/agent/util.rs | 119 +++++++++++-- 4 files changed, 260 insertions(+), 381 deletions(-) diff --git a/common/src/util/spatial_grid.rs b/common/src/util/spatial_grid.rs index aab6b67381..094f19f76a 100644 --- a/common/src/util/spatial_grid.rs +++ b/common/src/util/spatial_grid.rs @@ -48,10 +48,9 @@ impl SpatialGrid { } } - /// Get an iterator over the entities overlapping the - /// provided axis aligned bounding region - /// NOTE: for best optimization of the iterator use `for_each` rather than a - /// for loop + /// Get an iterator over the entities overlapping the provided axis aligned + /// bounding region. NOTE: for best optimization of the iterator use + /// `for_each` rather than a for loop. pub fn in_aabr<'a>(&'a self, aabr: Aabr) -> impl Iterator + 'a { let iter = |max_entity_radius, grid: &'a hashbrown::HashMap<_, _>, lg2_cell_size| { // Add buffer for other entity radius diff --git a/server/src/sys/agent.rs b/server/src/sys/agent.rs index 5fcfb5ffe2..3e5891e98d 100644 --- a/server/src/sys/agent.rs +++ b/server/src/sys/agent.rs @@ -13,13 +13,14 @@ use crate::{ }, data::{AgentData, AttackData, Path, ReadData, Tactic, TargetData}, util::{ - aim_projectile, can_see_tgt, get_entity_by_id, is_dead, is_dead_or_invulnerable, - is_invulnerable, is_villager, stop_pursuing, try_owner_alignment, + aim_projectile, are_our_owners_hostile, does_entity_see_other, + entity_looks_like_cultist, get_attacker_of_entity, get_entity_by_id, is_dead, + is_dead_or_invulnerable, is_entity_a_village_guard, is_invulnerable, is_villager, + stop_pursuing, }, }, }; use common::{ - combat, comp::{ self, agent::{ @@ -29,7 +30,7 @@ use common::{ buff::BuffKind, compass::{Direction, Distance}, dialogue::{MoodContext, MoodState, Subject}, - inventory::{item::ItemTag, slot::EquipSlot}, + inventory::slot::EquipSlot, invite::{InviteKind, InviteResponse}, item::{ tool::{AbilitySpec, ToolKind}, @@ -37,7 +38,7 @@ use common::{ }, projectile::ProjectileConstructor, Agent, Alignment, BehaviorState, Body, CharacterState, ControlAction, ControlEvent, - Controller, Health, HealthChange, InputKind, Inventory, InventoryAction, Pos, Scale, + Controller, Health, HealthChange, InputKind, InventoryAction, Pos, Scale, UnresolvedChatMsg, UtteranceKind, }, effect::{BuffEffect, Effect}, @@ -537,7 +538,7 @@ impl<'a> AgentData<'a> { } if rng.gen::() < 0.1 { - self.choose_target(agent, controller, read_data); + self.scan_for_new_hostile_target(agent, controller, read_data); } else { self.handle_sounds_heard(agent, controller, read_data, rng); } @@ -639,7 +640,7 @@ impl<'a> AgentData<'a> { read_data.time.0 - selected_at > RETARGETING_THRESHOLD_SECONDS; if !in_aggro_range && is_time_to_retarget { - self.choose_target(agent, controller, read_data); + self.scan_for_new_hostile_target(agent, controller, read_data); } if aggro_on { @@ -709,14 +710,14 @@ impl<'a> AgentData<'a> { // Only emit event for agents that have a lantern equipped if lantern_equipped && rng.gen_bool(0.001) { if day_period.is_dark() && !lantern_turned_on { - // Agents with turned off lanterns turn them on randomly once it's - // nighttime and keep them on + // Agents with turned off lanterns turn them on randomly once it's nighttime and + // keep them on. // Only emit event for agents that sill need to - // turn on their lantern + // turn on their lantern. controller.push_event(ControlEvent::EnableLantern) } else if lantern_turned_on && day_period.is_light() { // agents with turned on lanterns turn them off randomly once it's - // daytime and keep them off + // daytime and keep them off. controller.push_event(ControlEvent::DisableLantern) } }; @@ -728,8 +729,8 @@ impl<'a> AgentData<'a> { agent.action_state.timer = 0.0; if let Some((travel_to, _destination)) = &agent.rtsim_controller.travel_to { - // if it has an rtsim destination and can fly then it should - // if it is flying and bumps something above it then it should move down + // If it has an rtsim destination and can fly, then it should. + // If it is flying and bumps something above it, then it should move down. if self.traversal_config.can_fly && !read_data .terrain @@ -1432,11 +1433,10 @@ impl<'a> AgentData<'a> { if small_chance { controller.push_utterance(UtteranceKind::Angry); - if is_villager(self.alignment) { if self.remembers_fight_with(target, read_data) { chat_villager_remembers_fighting(); - } else if self.entity_looks_like_cultist(target, read_data) { + } else if entity_looks_like_cultist(target, read_data) { chat("npc.speech.villager_cultist_alarm"); } else { chat("npc.speech.menacing"); @@ -1445,6 +1445,14 @@ impl<'a> AgentData<'a> { chat("npc.speech.menacing"); } } + + // Remember target. + self.rtsim_entity.is_some().then(|| { + read_data + .stats + .get(target) + .map(|stats| agent.add_fight_to_memory(&stats.name, read_data.time.0)) + }); } fn flee( @@ -1539,172 +1547,76 @@ impl<'a> AgentData<'a> { } } - fn choose_target(&self, agent: &mut Agent, controller: &mut Controller, read_data: &ReadData) { + fn scan_for_new_hostile_target( + &self, + agent: &mut Agent, + controller: &mut Controller, + read_data: &ReadData, + ) { agent.action_state.timer = 0.0; let mut aggro_on = false; - let worth_choosing = |entity| { - read_data.positions.get(entity).and_then(|pos| { - Some(( - entity, - pos, - read_data.healths.get(entity)?, - read_data.inventories.get(entity)?, - read_data.alignments.get(entity), - read_data.char_states.get(entity), - read_data.bodies.get(entity), - )) + let get_pos = |entity| read_data.positions.get(entity); + let is_alignment_passive_towards_entity = |entity| { + self.alignment.map_or(false, |alignment| { + read_data + .alignments + .get(entity) + .map_or(false, |entity_alignment| { + alignment.passive_towards(*entity_alignment) + }) }) }; - - let max_search_dist = agent.psyche.search_dist(); - let max_sight_dist = agent.psyche.sight_dist; - let max_listen_dist = agent.psyche.listen_dist; - let in_sight_dist = - |e_pos: &Pos, e_char_state: Option<&CharacterState>, inventory: &Inventory| { - let search_dist = max_sight_dist - / if e_char_state.map_or(false, CharacterState::is_stealthy) { - combat::compute_stealth_coefficient(Some(inventory)) + let get_enemy = |entity: EcsEntity| { + if self.is_entity_an_enemy(entity, read_data) { + Some(entity) + } else if self.defends_entity(entity, read_data) { + if let Some(attacker) = get_attacker_of_entity(entity, read_data) { + if !is_alignment_passive_towards_entity(attacker) { + // aggro_on: attack immediately, do not warn/menace. + aggro_on = true; + Some(attacker) } else { - 1.0 - }; - e_pos.0.distance_squared(self.pos.0) < search_dist.powi(2) - }; - - let within_fov = |e_pos: &Pos| { - (e_pos.0 - self.pos.0) - .try_normalized() - .map_or(true, |v| v.dot(*controller.inputs.look_dir) > 0.15) - }; - - let in_listen_dist = - |e_pos: &Pos, e_char_state: Option<&CharacterState>, inventory: &Inventory| { - let listen_dist = max_listen_dist - / if e_char_state.map_or(false, CharacterState::is_stealthy) { - combat::compute_stealth_coefficient(Some(inventory)) - } else { - 1.0 - }; - // TODO implement proper sound system for agents - e_pos.0.distance_squared(self.pos.0) < listen_dist.powi(2) - }; - - let within_reach = - |e_pos: &Pos, e_char_state: Option<&CharacterState>, e_inventory: &Inventory| { - (in_sight_dist(e_pos, e_char_state, e_inventory) && within_fov(e_pos)) - || in_listen_dist(e_pos, e_char_state, e_inventory) - }; - - let is_owner_hostile = |e_alignment: Option<&Alignment>| { - try_owner_alignment(self.alignment, read_data).map_or(false, |owner_alignment| { - try_owner_alignment(e_alignment, read_data).map_or(false, |e_owner_alignment| { - owner_alignment.hostile_towards(*e_owner_alignment) - }) - }) - }; - - let guard_other = - |e_health: &Health, e_body: Option<&Body>, e_alignment: Option<&Alignment>| { - let i_am_a_guard = read_data - .stats - .get(*self.entity) - .map_or(false, |stats| stats.name == "Guard"); - let we_are_friendly: bool = self.alignment.map_or(false, |ma| { - e_alignment.map_or(false, |ea| !ea.hostile_towards(*ma)) - }); - let we_share_species: bool = self.body.map_or(false, |mb| { - e_body.map_or(false, |eb| { - eb.is_same_species_as(mb) || (eb.is_humanoid() && mb.is_humanoid()) - }) - }); - let i_own_other = - matches!(e_alignment, Some(Alignment::Owned(ouid)) if self.uid == ouid); - let other_has_taken_damage = read_data.time.0 - e_health.last_change.time.0 < 5.0; - let attacker_of = |health: &Health| health.last_change.damage_by(); - - let i_should_defend = other_has_taken_damage - && ((we_are_friendly && we_share_species) - || (i_am_a_guard && is_villager(e_alignment)) - || i_own_other); - - i_should_defend - .then(|| { - attacker_of(e_health) - .and_then(|damage_contributor| { - get_entity_by_id(damage_contributor.uid().0, read_data) - }) - .and_then(|attacker| { - read_data.alignments.get(attacker).and_then(|aa| { - self.alignment.and_then({ - |ma| { - if !ma.passive_towards(*aa) { - read_data - .positions - .get(attacker) - .map(|a_pos| (attacker, *a_pos)) - } else { - None - } - } - }) - }) - }) - }) - .flatten() - }; - - let possible_target = - |(entity, e_pos, e_health, e_inventory, e_alignment, e_char_state, e_body): ( - EcsEntity, - &Pos, - &Health, - &Inventory, - Option<&Alignment>, - Option<&CharacterState>, - Option<&Body>, - )| { - let can_target = within_reach(e_pos, e_char_state, e_inventory) - && entity != *self.entity - && !e_health.is_dead - && !is_invulnerable(entity, read_data); - - if !can_target { - None - } else if is_owner_hostile(e_alignment) { - Some((entity, *e_pos)) - } else if let Some(villain_info) = guard_other(e_health, e_body, e_alignment) { - aggro_on = true; - Some(villain_info) - } else if self.remembers_fight_with(entity, read_data) - || is_villager(self.alignment) - && self.entity_looks_like_cultist(entity, read_data) - { - Some((entity, *e_pos)) + None + } } else { None } - }; + } else { + None + } + }; + let is_valid = |entity| { + read_data.healths.get(entity).map_or(false, |health| { + !health.is_dead && !is_invulnerable(entity, read_data) + }) + }; + let lost_target = |target: Option| agent.target.is_none() && target.is_some(); - // Search area - // TODO choose target by more than just distance + // Search the area. + // TODO: choose target by more than just distance let common::CachedSpatialGrid(grid) = self.cached_spatial_grid; - let target = grid - .in_circle_aabr(self.pos.0.xy(), max_search_dist) - .filter_map(worth_choosing) - .filter_map(possible_target) - // TODO: This seems expensive. Cache this to avoid recomputing each tick - .filter(|(_, e_pos)| can_see_tgt(&read_data.terrain, self.pos, e_pos, e_pos.0.distance_squared(self.pos.0))) - .min_by_key(|(_, e_pos)| (e_pos.0.distance_squared(self.pos.0) * 100.0) as i32) - .map(|(e, _)| e); - if agent.target.is_none() && target.is_some() { + let target = grid + .in_circle_aabr(self.pos.0.xy(), agent.psyche.search_dist()) + .filter(|entity| { + does_entity_see_other(agent, *self.entity, *entity, controller, read_data) + && is_valid(*entity) + }) + .filter_map(get_enemy) + .min_by_key(|entity| { + // TODO: This seems expensive. Cache this to avoid recomputing each tick + get_pos(*entity).map(|pos| (pos.0.distance_squared(self.pos.0) * 100.0) as i32) + }); + + if lost_target(target) { if aggro_on { controller.push_utterance(UtteranceKind::Angry); } else { controller.push_utterance(UtteranceKind::Surprised); } } - + // Change target to new target. agent.target = target.map(|target| Target { target, hostile: true, @@ -2090,13 +2002,9 @@ impl<'a> AgentData<'a> { tgt_data, read_data, ), - Tactic::RotatingTurret => self.handle_rotating_turret_attack( - agent, - controller, - &attack_data, - tgt_data, - read_data, - ), + Tactic::RotatingTurret => { + self.handle_rotating_turret_attack(agent, controller, tgt_data, read_data) + }, Tactic::Mindflayer => self.handle_mindflayer_attack( agent, controller, @@ -2475,16 +2383,6 @@ impl<'a> AgentData<'a> { }) } - fn entity_looks_like_cultist(&self, entity: EcsEntity, read_data: &ReadData) -> bool { - let number_of_cultist_items_equipped = read_data.inventories.get(entity).map_or(0, |inv| { - inv.equipped_items() - .filter(|item| item.tags().contains(&ItemTag::Cultist)) - .count() - }); - - number_of_cultist_items_equipped > 2 - } - fn remembers_fight_with(&self, other: EcsEntity, read_data: &ReadData) -> bool { let name = || read_data.stats.get(other).map(|stats| stats.name.clone()); @@ -2494,4 +2392,39 @@ impl<'a> AgentData<'a> { }) }) } + + fn is_entity_an_enemy(&self, entity: EcsEntity, read_data: &ReadData) -> bool { + let alignment = read_data.alignments.get(entity); + + (entity != *self.entity) + && (are_our_owners_hostile(self.alignment, alignment, read_data) + || self.remembers_fight_with(entity, read_data) + || self.is_villager_and_entity_is_cultist(entity, read_data)) + } + + fn defends_entity(&self, entity: EcsEntity, read_data: &ReadData) -> bool { + let entity_alignment = read_data.alignments.get(entity); + + let we_are_friendly = entity_alignment.map_or(false, |entity_alignment| { + self.alignment.map_or(false, |alignment| { + !alignment.hostile_towards(*entity_alignment) + }) + }); + let we_share_species = read_data.bodies.get(entity).map_or(false, |entity_body| { + self.body.map_or(false, |body| { + entity_body.is_same_species_as(body) + || (entity_body.is_humanoid() && body.is_humanoid()) + }) + }); + let self_owns_entity = + matches!(entity_alignment, Some(Alignment::Owned(ouid)) if *self.uid == *ouid); + + (we_are_friendly && we_share_species) + || (is_entity_a_village_guard(*self.entity, read_data) && is_villager(entity_alignment)) + || self_owns_entity + } + + fn is_villager_and_entity_is_cultist(&self, entity: EcsEntity, read_data: &ReadData) -> bool { + is_villager(self.alignment) && entity_looks_like_cultist(entity, read_data) + } } diff --git a/server/src/sys/agent/attack.rs b/server/src/sys/agent/attack.rs index 7e6ab6b142..bc8549926b 100644 --- a/server/src/sys/agent/attack.rs +++ b/server/src/sys/agent/attack.rs @@ -1,6 +1,6 @@ use crate::sys::agent::{ - consts::MAX_PATH_DIST, data::Path, util::can_see_tgt, AgentData, AttackData, ReadData, - TargetData, + consts::MAX_PATH_DIST, data::Path, util::positions_have_line_of_sight, AgentData, AttackData, + ReadData, TargetData, }; use common::{ comp::{ @@ -134,12 +134,7 @@ impl<'a> AgentData<'a> { const PREF_DIST: f32 = 30_f32; if attack_data.angle_xy < 30.0 && (elevation > 10.0 || attack_data.dist_sqrd > PREF_DIST.powi(2)) - && can_see_tgt( - &read_data.terrain, - self.pos, - tgt_data.pos, - attack_data.dist_sqrd, - ) + && positions_have_line_of_sight(self.pos, tgt_data.pos, read_data) { controller.push_basic_input(InputKind::Primary); } else if attack_data.dist_sqrd < (PREF_DIST / 2.).powi(2) { @@ -186,12 +181,7 @@ impl<'a> AgentData<'a> { ..self.traversal_config }, ) { - if can_see_tgt( - &read_data.terrain, - self.pos, - tgt_data.pos, - attack_data.dist_sqrd, - ) { + if positions_have_line_of_sight(self.pos, tgt_data.pos, read_data) { controller.push_basic_input(InputKind::Primary); } controller.inputs.move_dir = @@ -253,12 +243,7 @@ impl<'a> AgentData<'a> { if attack_data.dist_sqrd < 32.0f32.powi(2) && has_leap() && has_energy(50.0) - && can_see_tgt( - &read_data.terrain, - self.pos, - tgt_data.pos, - attack_data.dist_sqrd, - ) + && positions_have_line_of_sight(self.pos, tgt_data.pos, read_data) { use_leap(controller); } @@ -319,12 +304,7 @@ impl<'a> AgentData<'a> { if attack_data.dist_sqrd < 32.0f32.powi(2) && has_leap() && has_energy(50.0) - && can_see_tgt( - &read_data.terrain, - self.pos, - tgt_data.pos, - attack_data.dist_sqrd, - ) + && positions_have_line_of_sight(self.pos, tgt_data.pos, read_data) { use_leap(controller); } @@ -370,12 +350,8 @@ impl<'a> AgentData<'a> { read_data, Path::Separate, None, - ) && can_see_tgt( - &*read_data.terrain, - self.pos, - tgt_data.pos, - attack_data.dist_sqrd, - ) { + ) && positions_have_line_of_sight(self.pos, tgt_data.pos, read_data) + { if agent.action_state.timer > 4.0 && attack_data.angle < 45.0 { controller.push_basic_input(InputKind::Secondary); agent.action_state.timer = 0.0; @@ -437,12 +413,7 @@ impl<'a> AgentData<'a> { // If in repeater ranged, have enough energy, and aren't in recovery, try to // keep firing if attack_data.dist_sqrd > attack_data.min_attack_dist.powi(2) - && can_see_tgt( - &*read_data.terrain, - self.pos, - tgt_data.pos, - attack_data.dist_sqrd, - ) + && positions_have_line_of_sight(self.pos, tgt_data.pos, read_data) { // Only keep firing if not in melee range or if can see target controller.push_basic_input(InputKind::Secondary); @@ -477,12 +448,7 @@ impl<'a> AgentData<'a> { } } } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) - && can_see_tgt( - &*read_data.terrain, - self.pos, - tgt_data.pos, - attack_data.dist_sqrd, - ) + && positions_have_line_of_sight(self.pos, tgt_data.pos, read_data) { // If not really far, and can see target, attempt to shoot bow if self.energy.current() < DESIRED_ENERGY_LEVEL { @@ -522,12 +488,8 @@ impl<'a> AgentData<'a> { ..self.traversal_config }, ) { - if can_see_tgt( - &*read_data.terrain, - self.pos, - tgt_data.pos, - attack_data.dist_sqrd, - ) && attack_data.angle < 45.0 + if positions_have_line_of_sight(self.pos, tgt_data.pos, read_data) + && attack_data.angle < 45.0 { controller.inputs.move_dir = bearing .xy() @@ -660,12 +622,8 @@ impl<'a> AgentData<'a> { ..self.traversal_config }, ) { - if can_see_tgt( - &*read_data.terrain, - self.pos, - tgt_data.pos, - attack_data.dist_sqrd, - ) && attack_data.angle < 45.0 + if positions_have_line_of_sight(self.pos, tgt_data.pos, read_data) + && attack_data.angle < 45.0 { controller.inputs.move_dir = bearing .xy() @@ -715,12 +673,7 @@ impl<'a> AgentData<'a> { const DESIRED_COMBO_LEVEL: u32 = 8; // Logic to use abilities if attack_data.dist_sqrd > attack_data.min_attack_dist.powi(2) - && can_see_tgt( - &*read_data.terrain, - self.pos, - tgt_data.pos, - attack_data.dist_sqrd, - ) + && positions_have_line_of_sight(self.pos, tgt_data.pos, read_data) { // If far enough away, and can see target, check which skill is appropriate to // use @@ -798,12 +751,8 @@ impl<'a> AgentData<'a> { ..self.traversal_config }, ) { - if can_see_tgt( - &*read_data.terrain, - self.pos, - tgt_data.pos, - attack_data.dist_sqrd, - ) && attack_data.angle < 45.0 + if positions_have_line_of_sight(self.pos, tgt_data.pos, read_data) + && attack_data.angle < 45.0 { controller.inputs.move_dir = bearing .xy() @@ -863,12 +812,8 @@ impl<'a> AgentData<'a> { read_data, Path::Separate, None, - ) && can_see_tgt( - &*read_data.terrain, - self.pos, - tgt_data.pos, - attack_data.dist_sqrd, - ) && attack_data.angle < 90.0 + ) && positions_have_line_of_sight(self.pos, tgt_data.pos, read_data) + && attack_data.angle < 90.0 { if agent.action_state.timer > 5.0 { controller.push_basic_input(InputKind::Secondary); @@ -1006,12 +951,7 @@ impl<'a> AgentData<'a> { }, ) { if attack_data.angle < 15.0 - && can_see_tgt( - &*read_data.terrain, - self.pos, - tgt_data.pos, - attack_data.dist_sqrd, - ) + && positions_have_line_of_sight(self.pos, tgt_data.pos, read_data) { if agent.action_state.timer > 5.0 { agent.action_state.timer = 0.0; @@ -1204,12 +1144,7 @@ impl<'a> AgentData<'a> { Path::Separate, None, ) && attack_data.angle < 15.0 - && can_see_tgt( - &*read_data.terrain, - self.pos, - tgt_data.pos, - attack_data.dist_sqrd, - ) + && positions_have_line_of_sight(self.pos, tgt_data.pos, read_data) { controller.push_basic_input(InputKind::Primary); } @@ -1364,12 +1299,8 @@ impl<'a> AgentData<'a> { tgt_data: &TargetData, read_data: &ReadData, ) { - if can_see_tgt( - &*read_data.terrain, - self.pos, - tgt_data.pos, - attack_data.dist_sqrd, - ) && attack_data.angle < 15.0 + if positions_have_line_of_sight(self.pos, tgt_data.pos, read_data) + && attack_data.angle < 15.0 { controller.push_basic_input(InputKind::Primary); } else { @@ -1386,12 +1317,8 @@ impl<'a> AgentData<'a> { read_data: &ReadData, ) { controller.inputs.look_dir = self.ori.look_dir(); - if can_see_tgt( - &*read_data.terrain, - self.pos, - tgt_data.pos, - attack_data.dist_sqrd, - ) && attack_data.angle < 15.0 + if positions_have_line_of_sight(self.pos, tgt_data.pos, read_data) + && attack_data.angle < 15.0 { controller.push_basic_input(InputKind::Primary); } else { @@ -1403,7 +1330,6 @@ impl<'a> AgentData<'a> { &self, agent: &mut Agent, controller: &mut Controller, - attack_data: &AttackData, tgt_data: &TargetData, read_data: &ReadData, ) { @@ -1414,12 +1340,7 @@ impl<'a> AgentData<'a> { .try_normalized() .unwrap_or_default(), ); - if can_see_tgt( - &*read_data.terrain, - self.pos, - tgt_data.pos, - attack_data.dist_sqrd, - ) { + if positions_have_line_of_sight(self.pos, tgt_data.pos, read_data) { controller.push_basic_input(InputKind::Primary); } else { agent.target = None; @@ -1465,12 +1386,7 @@ impl<'a> AgentData<'a> { agent.action_state.counter -= MINION_SUMMON_THRESHOLD; } } else if attack_data.dist_sqrd < MINDFLAYER_ATTACK_DIST.powi(2) { - if can_see_tgt( - &*read_data.terrain, - self.pos, - tgt_data.pos, - attack_data.dist_sqrd, - ) { + if positions_have_line_of_sight(self.pos, tgt_data.pos, read_data) { // If close to target, use either primary or secondary ability if matches!(self.char_state, CharacterState::BasicBeam(c) if c.timer < Duration::from_secs(10) && !matches!(c.stage_section, StageSection::Recover)) { @@ -1560,12 +1476,7 @@ impl<'a> AgentData<'a> { let small_chance = rng.gen_bool(0.05); if small_chance - && can_see_tgt( - &*read_data.terrain, - self.pos, - tgt_data.pos, - attack_data.dist_sqrd, - ) + && positions_have_line_of_sight(self.pos, tgt_data.pos, read_data) && attack_data.angle < 15.0 { // Fireball @@ -1686,12 +1597,7 @@ impl<'a> AgentData<'a> { controller.push_cancel_input(InputKind::Fly); if attack_data.dist_sqrd > 30.0_f32.powi(2) { if rng.gen_bool(0.05) - && can_see_tgt( - &*read_data.terrain, - self.pos, - tgt_data.pos, - attack_data.dist_sqrd, - ) + && positions_have_line_of_sight(self.pos, tgt_data.pos, read_data) && attack_data.angle < 15.0 { controller.push_basic_input(InputKind::Primary); @@ -1903,12 +1809,7 @@ impl<'a> AgentData<'a> { }, ) { if attack_data.angle < 15.0 - && can_see_tgt( - &*read_data.terrain, - self.pos, - tgt_data.pos, - attack_data.dist_sqrd, - ) + && positions_have_line_of_sight(self.pos, tgt_data.pos, read_data) { if agent.action_state.timer > 5.0 { agent.action_state.timer = 0.0; @@ -2134,12 +2035,7 @@ impl<'a> AgentData<'a> { } else if attack_data.dist_sqrd < GOLEM_LASER_RANGE.powi(2) { if matches!(self.char_state, CharacterState::BasicBeam(c) if c.timer < Duration::from_secs(5)) || target_speed_cross_sqd < GOLEM_TARGET_SPEED.powi(2) - && can_see_tgt( - &*read_data.terrain, - self.pos, - tgt_data.pos, - attack_data.dist_sqrd, - ) + && positions_have_line_of_sight(self.pos, tgt_data.pos, read_data) && attack_data.angle < 45.0 { // If target in range threshold and haven't been lasering for more than 5 @@ -2152,12 +2048,7 @@ impl<'a> AgentData<'a> { } } else if attack_data.dist_sqrd < GOLEM_LONG_RANGE.powi(2) { if target_speed_cross_sqd < GOLEM_TARGET_SPEED.powi(2) - && can_see_tgt( - &*read_data.terrain, - self.pos, - tgt_data.pos, - attack_data.dist_sqrd, - ) + && positions_have_line_of_sight(self.pos, tgt_data.pos, read_data) { // If target is far-ish and moving slow-ish, rocket them controller.push_basic_input(InputKind::Ability(1)); @@ -2220,24 +2111,14 @@ impl<'a> AgentData<'a> { // Pincer them if they're in range and angle controller.push_basic_input(InputKind::Primary); } else if attack_data.angle < 30.0 - && can_see_tgt( - &*read_data.terrain, - self.pos, - tgt_data.pos, - attack_data.dist_sqrd, - ) + && positions_have_line_of_sight(self.pos, tgt_data.pos, read_data) { // Start bubbling them if not close enough to do something else and in angle and // can see target controller.push_basic_input(InputKind::Ability(0)); } } else if attack_data.angle < 90.0 - && can_see_tgt( - &*read_data.terrain, - self.pos, - tgt_data.pos, - attack_data.dist_sqrd, - ) + && positions_have_line_of_sight(self.pos, tgt_data.pos, read_data) { // Start scuttling if not close enough to do something else and in angle and can // see target @@ -2332,12 +2213,7 @@ impl<'a> AgentData<'a> { } } else if attack_data.dist_sqrd < FIRE_BREATH_RANGE.powi(2) { if matches!(self.char_state, CharacterState::BasicBeam(c) if c.timer < Duration::from_secs(5)) - && can_see_tgt( - &*read_data.terrain, - self.pos, - tgt_data.pos, - attack_data.dist_sqrd, - ) + && positions_have_line_of_sight(self.pos, tgt_data.pos, read_data) { // Keep breathing fire if close enough, can see target, and have not been // breathing for more than 5 seconds @@ -2346,23 +2222,13 @@ impl<'a> AgentData<'a> { // Scythe them if they're in range and angle controller.push_basic_input(InputKind::Primary); } else if attack_data.angle < 30.0 - && can_see_tgt( - &*read_data.terrain, - self.pos, - tgt_data.pos, - attack_data.dist_sqrd, - ) + && positions_have_line_of_sight(self.pos, tgt_data.pos, read_data) { // Start breathing fire at them if close enough, in angle, and can see target controller.push_basic_input(InputKind::Secondary); } } else if attack_data.dist_sqrd < MAX_PUMPKIN_RANGE.powi(2) - && can_see_tgt( - &*read_data.terrain, - self.pos, - tgt_data.pos, - attack_data.dist_sqrd, - ) + && positions_have_line_of_sight(self.pos, tgt_data.pos, read_data) { // Throw a pumpkin at them if close enough and can see them controller.push_basic_input(InputKind::Ability(1)); @@ -2457,12 +2323,7 @@ impl<'a> AgentData<'a> { if attack_data.in_min_range() { controller.push_basic_input(InputKind::Primary); } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) - && can_see_tgt( - &read_data.terrain, - self.pos, - tgt_data.pos, - attack_data.dist_sqrd, - ) + && positions_have_line_of_sight(self.pos, tgt_data.pos, read_data) { // If in pathing range and can see target, move towards them self.path_toward_target( @@ -2594,12 +2455,7 @@ impl<'a> AgentData<'a> { if attack_data.in_min_range() { // If in range, shockwave controller.push_basic_input(InputKind::Ability(0)); - } else if can_see_tgt( - &read_data.terrain, - self.pos, - tgt_data.pos, - attack_data.dist_sqrd, - ) { + } else if positions_have_line_of_sight(self.pos, tgt_data.pos, read_data) { // Else if in sight, barrage controller.push_basic_input(InputKind::Secondary); } diff --git a/server/src/sys/agent/util.rs b/server/src/sys/agent/util.rs index 54ad778e71..70c2178a41 100644 --- a/server/src/sys/agent/util.rs +++ b/server/src/sys/agent/util.rs @@ -1,8 +1,12 @@ use crate::sys::agent::{AgentData, ReadData}; use common::{ - comp::{agent::Psyche, buff::BuffKind, Alignment, Pos}, + combat::compute_stealth_coefficient, + comp::{ + agent::Psyche, buff::BuffKind, inventory::item::ItemTag, item::ItemDesc, Agent, Alignment, + CharacterState, Controller, Pos, + }, consts::GRAVITY, - terrain::{Block, TerrainGrid}, + terrain::Block, util::Dir, vol::ReadVol, }; @@ -12,16 +16,6 @@ use specs::{ }; use vek::*; -pub fn can_see_tgt(terrain: &TerrainGrid, pos: &Pos, tgt_pos: &Pos, dist_sqrd: f32) -> bool { - terrain - .ray(pos.0 + Vec3::unit_z(), tgt_pos.0 + Vec3::unit_z()) - .until(Block::is_opaque) - .cast() - .0 - .powi(2) - >= dist_sqrd -} - pub fn is_dead_or_invulnerable(entity: EcsEntity, read_data: &ReadData) -> bool { is_dead(entity, read_data) || is_invulnerable(entity, read_data) } @@ -39,8 +33,9 @@ pub fn is_invulnerable(entity: EcsEntity, read_data: &ReadData) -> bool { buffs.map_or(false, |b| b.kinds.contains_key(&BuffKind::Invulnerability)) } -/// Attempts to get alignment of owner if entity has Owned alignment -pub fn try_owner_alignment<'a>( +/// Gets alignment of owner if alignment given is `Owned`. +/// Returns original alignment if not owned. +pub fn owner_alignment<'a>( alignment: Option<&'a Alignment>, read_data: &'a ReadData, ) -> Option<&'a Alignment> { @@ -122,7 +117,103 @@ fn should_let_target_escape( (dist_to_home_sqrd / own_health_fraction) * dur_since_last_attacked as f32 * 0.005 } +pub fn entity_looks_like_cultist(entity: EcsEntity, read_data: &ReadData) -> bool { + let number_of_cultist_items_equipped = read_data.inventories.get(entity).map_or(0, |inv| { + inv.equipped_items() + .filter(|item| item.tags().contains(&ItemTag::Cultist)) + .count() + }); + + number_of_cultist_items_equipped > 2 +} + // FIXME: `Alignment::Npc` doesn't necessarily mean villager. pub fn is_villager(alignment: Option<&Alignment>) -> bool { alignment.map_or(false, |alignment| matches!(alignment, Alignment::Npc)) } + +pub fn is_entity_a_village_guard(entity: EcsEntity, read_data: &ReadData) -> bool { + read_data + .stats + .get(entity) + .map_or(false, |stats| stats.name == "Guard") +} + +pub fn are_our_owners_hostile( + our_alignment: Option<&Alignment>, + their_alignment: Option<&Alignment>, + read_data: &ReadData, +) -> bool { + owner_alignment(our_alignment, read_data).map_or(false, |our_owners_alignment| { + owner_alignment(their_alignment, read_data).map_or(false, |their_owners_alignment| { + our_owners_alignment.hostile_towards(*their_owners_alignment) + }) + }) +} + +pub fn positions_have_line_of_sight(pos_a: &Pos, pos_b: &Pos, read_data: &ReadData) -> bool { + let dist_sqrd = pos_b.0.distance_squared(pos_a.0); + + read_data + .terrain + .ray(pos_a.0 + Vec3::unit_z(), pos_b.0 + Vec3::unit_z()) + .until(Block::is_opaque) + .cast() + .0 + .powi(2) + >= dist_sqrd +} + +pub fn does_entity_see_other( + agent: &Agent, + entity: EcsEntity, + other: EcsEntity, + controller: &Controller, + read_data: &ReadData, +) -> bool { + let stealth_coefficient = { + let is_other_being_stealthy = read_data + .char_states + .get(other) + .map_or(false, CharacterState::is_stealthy); + + if is_other_being_stealthy { + // TODO: We shouldn't have to check CharacterState. This should be factored in + // by the function (such as the one we're calling below) that supposedly + // computes a coefficient given stealthy-ness. + compute_stealth_coefficient(read_data.inventories.get(other)) + } else { + 1.0 + } + }; + + if let (Some(pos), Some(other_pos)) = ( + read_data.positions.get(entity), + read_data.positions.get(other), + ) { + let dist = other_pos.0 - pos.0; + let dist_sqrd = other_pos.0.distance_squared(pos.0); + + let within_sight_dist = { + let sight_dist = agent.psyche.sight_dist / stealth_coefficient; + dist_sqrd < sight_dist.powi(2) + }; + + let within_fov = dist + .try_normalized() + // FIXME: Should this be map_or(false)? + .map_or(true, |v| v.dot(*controller.inputs.look_dir) > 0.15); + + within_sight_dist && positions_have_line_of_sight(pos, other_pos, read_data) && within_fov + } else { + false + } +} + +pub fn get_attacker_of_entity(entity: EcsEntity, read_data: &ReadData) -> Option { + read_data + .healths + .get(entity) + .and_then(|health| health.last_change.damage_by()) + .and_then(|damage_contributor| get_entity_by_id(damage_contributor.uid().0, read_data)) +}