mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
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.
This commit is contained in:
parent
0fd20ba00e
commit
d3873e357e
@ -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<i32>) -> impl Iterator<Item = specs::Entity> + 'a {
|
||||
let iter = |max_entity_radius, grid: &'a hashbrown::HashMap<_, _>, lg2_cell_size| {
|
||||
// Add buffer for other entity radius
|
||||
|
@ -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::<f32>() < 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<EcsEntity>| 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)
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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<EcsEntity> {
|
||||
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))
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user