mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Merge branch 'holychowders/improve_perception_system' into 'master'
Agent: Use FOV when scanning for hostile targets and refactor `choose_target()`. See merge request veloren/veloren!3307
This commit is contained in:
commit
3abba05fb8
3
.gitignore
vendored
3
.gitignore
vendored
@ -33,6 +33,9 @@ server_settings.ron
|
||||
run.sh
|
||||
maps
|
||||
screenshots
|
||||
notes.md
|
||||
notes.txt
|
||||
todo.md
|
||||
todo.txt
|
||||
userdata
|
||||
temp
|
||||
|
@ -15,9 +15,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Admin command to reload all chunks on the server
|
||||
- Furniture and waypoints in site2 towns
|
||||
- text input for trading
|
||||
- Themed Site CliffTown, hoodoo/arabic inspired stone structures inhabited by mountaineer NPCs.
|
||||
- Themed Site CliffTown, hoodoo/arabic inspired stone structures inhabited by mountaineer NPCs.
|
||||
- NPCs now have rudimentary personalities
|
||||
- Added Belarusian translation
|
||||
- Add FOV check for agents scanning for targets they are hostile to
|
||||
|
||||
### Changed
|
||||
|
||||
@ -37,6 +38,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Fix agents not idling
|
||||
- Fixed an error where '{amount} Exp' floater did not use existing localizations
|
||||
- Fix villagers seeing cultists and familiar enemies through objects.
|
||||
- Menacing agents are now less spammy with their menacing messages
|
||||
|
||||
## [0.12.0] - 2022-02-19
|
||||
|
||||
@ -49,7 +51,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Shrubs, a system for spawning smaller tree-like plants into the world.
|
||||
- Waterfalls
|
||||
- Sailing boat (currently requires spawning in)
|
||||
- Added a filter search function for crafting menu, use "input:_____" to search for recipe inputs
|
||||
- Added a filter search function for crafting menu, use "input:______" to search for recipe inputs
|
||||
- Added catalan (Catalonia) language translation
|
||||
- Sneaking with weapons drawn
|
||||
- Stealth stat values on (some) armors
|
||||
|
@ -282,7 +282,7 @@ impl<'a> From<&'a Body> for Psyche {
|
||||
},
|
||||
},
|
||||
sight_dist: 40.0,
|
||||
listen_dist: 30.0,
|
||||
listen_dist: 5.0,
|
||||
aggro_dist: match body {
|
||||
Body::Humanoid(_) => Some(20.0),
|
||||
_ => None, // Always aggressive if detected
|
||||
|
@ -48,10 +48,10 @@ 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, entities_have_line_of_sight, get_attacker,
|
||||
get_entity_by_id, is_dead, is_dead_or_invulnerable, is_dressed_as_cultist,
|
||||
is_invulnerable, is_village_guard, is_villager, stop_pursuing,
|
||||
},
|
||||
},
|
||||
};
|
||||
use common::{
|
||||
combat,
|
||||
combat::compute_stealth_coefficient,
|
||||
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},
|
||||
@ -311,7 +312,7 @@ impl<'a> System<'a> for Sys {
|
||||
// over the old one (i.e: because it's either close or
|
||||
// because they attacked us).
|
||||
if agent.target.map_or(true, |target| {
|
||||
data.is_entity_more_dangerous_than_target(
|
||||
data.is_more_dangerous_than_target(
|
||||
attacker, target, &read_data,
|
||||
)
|
||||
}) {
|
||||
@ -574,7 +575,6 @@ impl<'a> AgentData<'a> {
|
||||
{
|
||||
let target = *target;
|
||||
let selected_at = *selected_at;
|
||||
|
||||
if let Some(tgt_pos) = read_data.positions.get(target) {
|
||||
let dist_sqrd = self.pos.0.distance_squared(tgt_pos.0);
|
||||
let origin_dist_sqrd = match agent.patrol_origin {
|
||||
@ -605,13 +605,12 @@ impl<'a> AgentData<'a> {
|
||||
let has_opportunity_to_flee = agent.action_state.timer < FLEE_DURATION;
|
||||
let within_flee_distance = dist_sqrd < MAX_FLEE_DIST.powi(2);
|
||||
|
||||
// FIXME: Using the action state timer to see if an agent is allowed to speak is
|
||||
// a hack.
|
||||
// FIXME: Using action state timer to see if allowed to speak is a hack.
|
||||
if agent.action_state.timer == 0.0 {
|
||||
self.cry_out(agent, event_emitter, read_data);
|
||||
agent.action_state.timer = 0.01;
|
||||
} else if within_flee_distance && has_opportunity_to_flee {
|
||||
self.flee(agent, controller, &read_data.terrain, tgt_pos);
|
||||
self.flee(agent, controller, tgt_pos, &read_data.terrain);
|
||||
agent.action_state.timer += read_data.dt.0;
|
||||
} else {
|
||||
agent.action_state.timer = 0.0;
|
||||
@ -710,13 +709,13 @@ impl<'a> AgentData<'a> {
|
||||
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
|
||||
// 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 +727,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
|
||||
@ -757,7 +756,7 @@ impl<'a> AgentData<'a> {
|
||||
controller.inputs.move_dir =
|
||||
bearing.xy().try_normalized().unwrap_or_else(Vec2::zero)
|
||||
* speed.min(agent.rtsim_controller.speed_factor);
|
||||
self.jump_if(controller, bearing.z > 1.5 || self.traversal_config.can_fly);
|
||||
self.jump_if(bearing.z > 1.5 || self.traversal_config.can_fly, controller);
|
||||
controller.inputs.climb = Some(comp::Climb::Up);
|
||||
//.filter(|_| bearing.z > 0.1 || self.physics_state.in_liquid().is_some());
|
||||
|
||||
@ -937,7 +936,7 @@ impl<'a> AgentData<'a> {
|
||||
let dist_sqrd = self.pos.0.distance_squared(tgt_pos.0);
|
||||
controller.inputs.move_dir = bearing.xy().try_normalized().unwrap_or_else(Vec2::zero)
|
||||
* speed.min(0.2 + (dist_sqrd - AVG_FOLLOW_DIST.powi(2)) / 8.0);
|
||||
self.jump_if(controller, bearing.z > 1.5);
|
||||
self.jump_if(bearing.z > 1.5, controller);
|
||||
controller.inputs.move_z = bearing.z;
|
||||
}
|
||||
}
|
||||
@ -1432,11 +1431,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 is_dressed_as_cultist(target, read_data) {
|
||||
chat("npc.speech.villager_cultist_alarm");
|
||||
} else {
|
||||
chat("npc.speech.menacing");
|
||||
@ -1445,20 +1443,29 @@ 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(
|
||||
&self,
|
||||
agent: &mut Agent,
|
||||
controller: &mut Controller,
|
||||
terrain: &TerrainGrid,
|
||||
tgt_pos: &Pos,
|
||||
terrain: &TerrainGrid,
|
||||
) {
|
||||
if let Some(body) = self.body {
|
||||
if body.can_strafe() && !self.is_gliding {
|
||||
controller.push_action(ControlAction::Unwield);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((bearing, speed)) = agent.chaser.chase(
|
||||
&*terrain,
|
||||
self.pos.0,
|
||||
@ -1476,7 +1483,7 @@ impl<'a> AgentData<'a> {
|
||||
) {
|
||||
controller.inputs.move_dir =
|
||||
bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
|
||||
self.jump_if(controller, bearing.z > 1.5);
|
||||
self.jump_if(bearing.z > 1.5, controller);
|
||||
controller.inputs.move_z = bearing.z;
|
||||
}
|
||||
}
|
||||
@ -1543,159 +1550,59 @@ impl<'a> AgentData<'a> {
|
||||
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 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_pos = |entity| read_data.positions.get(entity);
|
||||
let get_enemy = |entity: EcsEntity| {
|
||||
if self.is_enemy(entity, read_data) {
|
||||
Some(entity)
|
||||
} else if self.should_defend(entity, read_data) {
|
||||
if let Some(attacker) = get_attacker(entity, read_data) {
|
||||
if !self.passive_towards(attacker, read_data) {
|
||||
// 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_target = |entity| {
|
||||
read_data.healths.get(entity).map_or(false, |health| {
|
||||
!health.is_dead && !is_invulnerable(entity, read_data)
|
||||
})
|
||||
};
|
||||
|
||||
// Search area
|
||||
// TODO choose target by more than just distance
|
||||
// TODO: This is a temporary hack. Remove this once footsteps are emitted and
|
||||
// agents are capable of detecting when someone is directly behind them.
|
||||
let within_listen_dist = |e_pos: &Pos| {
|
||||
let listen_dist = agent.psyche.listen_dist;
|
||||
|
||||
e_pos.0.distance_squared(self.pos.0) < listen_dist.powi(2)
|
||||
};
|
||||
|
||||
let is_detected = |entity: EcsEntity, e_pos: &Pos| {
|
||||
let chance = thread_rng().gen_bool(0.3);
|
||||
|
||||
(within_listen_dist(e_pos) && chance)
|
||||
|| self.can_see_entity(agent, controller, entity, e_pos, read_data)
|
||||
};
|
||||
|
||||
// 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)))
|
||||
.in_circle_aabr(self.pos.0.xy(), agent.psyche.search_dist())
|
||||
.filter(|entity| is_valid_target(*entity))
|
||||
.filter_map(get_enemy)
|
||||
.filter_map(|entity| get_pos(entity).map(|pos| (entity, pos)))
|
||||
.filter(|(entity, e_pos)| is_detected(*entity, e_pos))
|
||||
.min_by_key(|(_, e_pos)| (e_pos.0.distance_squared(self.pos.0) * 100.0) as i32)
|
||||
.map(|(e, _)| e);
|
||||
.map(|(entity, _)| entity);
|
||||
|
||||
if agent.target.is_none() && target.is_some() {
|
||||
if aggro_on {
|
||||
@ -1704,7 +1611,7 @@ impl<'a> AgentData<'a> {
|
||||
controller.push_utterance(UtteranceKind::Surprised);
|
||||
}
|
||||
}
|
||||
|
||||
// Change target to new target.
|
||||
agent.target = target.map(|target| Target {
|
||||
target,
|
||||
hostile: true,
|
||||
@ -2090,13 +1997,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,
|
||||
@ -2142,13 +2045,7 @@ impl<'a> AgentData<'a> {
|
||||
tgt_data,
|
||||
read_data,
|
||||
),
|
||||
Tactic::RadialTurret => self.handle_radial_turret_attack(
|
||||
agent,
|
||||
controller,
|
||||
&attack_data,
|
||||
tgt_data,
|
||||
read_data,
|
||||
),
|
||||
Tactic::RadialTurret => self.handle_radial_turret_attack(controller),
|
||||
Tactic::Yeti => {
|
||||
self.handle_yeti_attack(agent, controller, &attack_data, tgt_data, read_data)
|
||||
},
|
||||
@ -2208,7 +2105,7 @@ impl<'a> AgentData<'a> {
|
||||
let sound_was_threatening = sound_was_loud
|
||||
|| matches!(sound.kind, SoundKind::Utterance(UtteranceKind::Scream, _));
|
||||
|
||||
let is_enemy = matches!(self.alignment, Some(Alignment::Enemy));
|
||||
let has_enemy_alignment = matches!(self.alignment, Some(Alignment::Enemy));
|
||||
// FIXME: We need to be able to change the name of a guard without breaking this
|
||||
// logic. The `Mark` enum from common::agent could be used to match with
|
||||
// `agent::Mark::Guard`
|
||||
@ -2216,7 +2113,7 @@ impl<'a> AgentData<'a> {
|
||||
.stats
|
||||
.get(*self.entity)
|
||||
.map_or(false, |stats| stats.name == *"Guard".to_string());
|
||||
let follows_threatening_sounds = is_enemy || is_village_guard;
|
||||
let follows_threatening_sounds = has_enemy_alignment || is_village_guard;
|
||||
|
||||
// TODO: Awareness currently doesn't influence anything.
|
||||
//agent.awareness += 0.5 * sound.vol;
|
||||
@ -2225,7 +2122,7 @@ impl<'a> AgentData<'a> {
|
||||
if !self.below_flee_health(agent) && follows_threatening_sounds {
|
||||
self.follow(agent, controller, &read_data.terrain, &sound_pos);
|
||||
} else if self.below_flee_health(agent) || !follows_threatening_sounds {
|
||||
self.flee(agent, controller, &read_data.terrain, &sound_pos);
|
||||
self.flee(agent, controller, &sound_pos, &read_data.terrain);
|
||||
} else {
|
||||
self.idle(agent, controller, read_data, rng);
|
||||
}
|
||||
@ -2351,7 +2248,7 @@ impl<'a> AgentData<'a> {
|
||||
) {
|
||||
controller.inputs.move_dir =
|
||||
bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed * speed_multiplier;
|
||||
self.jump_if(controller, bearing.z > 1.5);
|
||||
self.jump_if(bearing.z > 1.5, controller);
|
||||
controller.inputs.move_z = bearing.z;
|
||||
true
|
||||
} else {
|
||||
@ -2373,7 +2270,7 @@ impl<'a> AgentData<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn jump_if(&self, controller: &mut Controller, condition: bool) {
|
||||
fn jump_if(&self, condition: bool, controller: &mut Controller) {
|
||||
if condition {
|
||||
controller.push_basic_input(InputKind::Jump);
|
||||
} else {
|
||||
@ -2407,9 +2304,11 @@ impl<'a> AgentData<'a> {
|
||||
event_emitter: &mut Emitter<'_, ServerEvent>,
|
||||
read_data: &ReadData,
|
||||
) {
|
||||
let is_enemy = matches!(self.alignment, Some(Alignment::Enemy));
|
||||
let has_enemy_alignment = matches!(self.alignment, Some(Alignment::Enemy));
|
||||
|
||||
if is_enemy {
|
||||
if has_enemy_alignment {
|
||||
// FIXME: If going to use "cultist + low health + fleeing" string, make sure
|
||||
// they are each true.
|
||||
self.chat_npc_if_allowed_to_speak(
|
||||
"npc.speech.cultist_low_health_fleeing",
|
||||
agent,
|
||||
@ -2443,7 +2342,7 @@ impl<'a> AgentData<'a> {
|
||||
self.damage.min(1.0) < agent.psyche.flee_health
|
||||
}
|
||||
|
||||
fn is_entity_more_dangerous_than_target(
|
||||
fn is_more_dangerous_than_target(
|
||||
&self,
|
||||
entity: EcsEntity,
|
||||
target: Target,
|
||||
@ -2475,16 +2374,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 +2383,88 @@ impl<'a> AgentData<'a> {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn is_enemy(&self, entity: EcsEntity, read_data: &ReadData) -> bool {
|
||||
let other_alignment = read_data.alignments.get(entity);
|
||||
|
||||
(entity != *self.entity)
|
||||
&& !self.passive_towards(entity, read_data)
|
||||
&& (are_our_owners_hostile(self.alignment, other_alignment, read_data)
|
||||
|| self.remembers_fight_with(entity, read_data)
|
||||
|| (is_villager(self.alignment) && is_dressed_as_cultist(entity, read_data)))
|
||||
}
|
||||
|
||||
fn should_defend(&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_village_guard(*self.entity, read_data) && is_villager(entity_alignment))
|
||||
|| self_owns_entity
|
||||
}
|
||||
|
||||
fn passive_towards(&self, entity: EcsEntity, read_data: &ReadData) -> bool {
|
||||
if let (Some(self_alignment), Some(other_alignment)) =
|
||||
(self.alignment, read_data.alignments.get(entity))
|
||||
{
|
||||
self_alignment.passive_towards(*other_alignment)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn can_see_entity(
|
||||
&self,
|
||||
agent: &Agent,
|
||||
controller: &Controller,
|
||||
other: EcsEntity,
|
||||
other_pos: &Pos,
|
||||
read_data: &ReadData,
|
||||
) -> bool {
|
||||
let other_stealth_coefficient = {
|
||||
let is_other_stealthy = read_data
|
||||
.char_states
|
||||
.get(other)
|
||||
.map_or(false, CharacterState::is_stealthy);
|
||||
|
||||
if is_other_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
|
||||
}
|
||||
};
|
||||
|
||||
let dist_sqrd = other_pos.0.distance_squared(self.pos.0);
|
||||
|
||||
let within_sight_dist = {
|
||||
let sight_dist = agent.psyche.sight_dist / other_stealth_coefficient;
|
||||
dist_sqrd < sight_dist.powi(2)
|
||||
};
|
||||
|
||||
let within_fov = (other_pos.0 - self.pos.0)
|
||||
.try_normalized()
|
||||
.map_or(false, |v| v.dot(*controller.inputs.look_dir) > 0.15);
|
||||
|
||||
let other_body = read_data.bodies.get(other);
|
||||
|
||||
within_sight_dist
|
||||
&& within_fov
|
||||
&& entities_have_line_of_sight(self.pos, self.body, other_pos, other_body, 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::entities_have_line_of_sight, AgentData, AttackData,
|
||||
ReadData, TargetData,
|
||||
};
|
||||
use common::{
|
||||
comp::{
|
||||
@ -130,16 +130,15 @@ impl<'a> AgentData<'a> {
|
||||
tgt_data: &TargetData,
|
||||
read_data: &ReadData,
|
||||
) {
|
||||
let line_of_sight_with_target = || {
|
||||
entities_have_line_of_sight(self.pos, self.body, tgt_data.pos, tgt_data.body, read_data)
|
||||
};
|
||||
|
||||
let elevation = self.pos.0.z - tgt_data.pos.0.z;
|
||||
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,
|
||||
)
|
||||
&& line_of_sight_with_target()
|
||||
{
|
||||
controller.push_basic_input(InputKind::Primary);
|
||||
} else if attack_data.dist_sqrd < (PREF_DIST / 2.).powi(2) {
|
||||
@ -186,12 +185,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 line_of_sight_with_target() {
|
||||
controller.push_basic_input(InputKind::Primary);
|
||||
}
|
||||
controller.inputs.move_dir =
|
||||
@ -219,12 +213,11 @@ impl<'a> AgentData<'a> {
|
||||
rng: &mut impl Rng,
|
||||
) {
|
||||
let has_leap = || self.skill_set.has_skill(Skill::Axe(AxeSkill::UnlockLeap));
|
||||
|
||||
let has_energy = |need| self.energy.current() > need;
|
||||
|
||||
let use_leap = |controller: &mut Controller| {
|
||||
controller.push_basic_input(InputKind::Ability(0));
|
||||
};
|
||||
|
||||
if attack_data.in_min_range() && attack_data.angle < 45.0 {
|
||||
controller.inputs.move_dir = Vec2::zero();
|
||||
if agent.action_state.timer > 5.0 {
|
||||
@ -253,11 +246,12 @@ 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,
|
||||
&& entities_have_line_of_sight(
|
||||
self.pos,
|
||||
self.body,
|
||||
tgt_data.pos,
|
||||
attack_data.dist_sqrd,
|
||||
tgt_data.body,
|
||||
read_data,
|
||||
)
|
||||
{
|
||||
use_leap(controller);
|
||||
@ -319,11 +313,12 @@ 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,
|
||||
&& entities_have_line_of_sight(
|
||||
self.pos,
|
||||
self.body,
|
||||
tgt_data.pos,
|
||||
attack_data.dist_sqrd,
|
||||
tgt_data.body,
|
||||
read_data,
|
||||
)
|
||||
{
|
||||
use_leap(controller);
|
||||
@ -370,11 +365,12 @@ impl<'a> AgentData<'a> {
|
||||
read_data,
|
||||
Path::Separate,
|
||||
None,
|
||||
) && can_see_tgt(
|
||||
&*read_data.terrain,
|
||||
) && entities_have_line_of_sight(
|
||||
self.pos,
|
||||
self.body,
|
||||
tgt_data.pos,
|
||||
attack_data.dist_sqrd,
|
||||
tgt_data.body,
|
||||
read_data,
|
||||
) {
|
||||
if agent.action_state.timer > 4.0 && attack_data.angle < 45.0 {
|
||||
controller.push_basic_input(InputKind::Secondary);
|
||||
@ -413,6 +409,11 @@ impl<'a> AgentData<'a> {
|
||||
const MIN_CHARGE_FRAC: f32 = 0.5;
|
||||
const OPTIMAL_TARGET_VELOCITY: f32 = 5.0;
|
||||
const DESIRED_ENERGY_LEVEL: f32 = 50.0;
|
||||
|
||||
let line_of_sight_with_target = || {
|
||||
entities_have_line_of_sight(self.pos, self.body, tgt_data.pos, tgt_data.body, read_data)
|
||||
};
|
||||
|
||||
// Logic to use abilities
|
||||
if let CharacterState::ChargedRanged(c) = self.char_state {
|
||||
if !matches!(c.stage_section, StageSection::Recover) {
|
||||
@ -437,12 +438,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,
|
||||
)
|
||||
&& line_of_sight_with_target()
|
||||
{
|
||||
// Only keep firing if not in melee range or if can see target
|
||||
controller.push_basic_input(InputKind::Secondary);
|
||||
@ -476,14 +472,7 @@ impl<'a> AgentData<'a> {
|
||||
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,
|
||||
)
|
||||
{
|
||||
} else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) && line_of_sight_with_target() {
|
||||
// If not really far, and can see target, attempt to shoot bow
|
||||
if self.energy.current() < DESIRED_ENERGY_LEVEL {
|
||||
// If low on energy, use primary to attempt to regen energy
|
||||
@ -522,13 +511,7 @@ 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 line_of_sight_with_target() && attack_data.angle < 45.0 {
|
||||
controller.inputs.move_dir = bearing
|
||||
.xy()
|
||||
.rotated_z(rng.gen_range(0.5..1.57))
|
||||
@ -539,7 +522,7 @@ impl<'a> AgentData<'a> {
|
||||
// Unless cannot see target, then move towards them
|
||||
controller.inputs.move_dir =
|
||||
bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
|
||||
self.jump_if(controller, bearing.z > 1.5);
|
||||
self.jump_if(bearing.z > 1.5, controller);
|
||||
controller.inputs.move_z = bearing.z;
|
||||
}
|
||||
}
|
||||
@ -660,11 +643,12 @@ impl<'a> AgentData<'a> {
|
||||
..self.traversal_config
|
||||
},
|
||||
) {
|
||||
if can_see_tgt(
|
||||
&*read_data.terrain,
|
||||
if entities_have_line_of_sight(
|
||||
self.pos,
|
||||
self.body,
|
||||
tgt_data.pos,
|
||||
attack_data.dist_sqrd,
|
||||
tgt_data.body,
|
||||
read_data,
|
||||
) && attack_data.angle < 45.0
|
||||
{
|
||||
controller.inputs.move_dir = bearing
|
||||
@ -677,7 +661,7 @@ impl<'a> AgentData<'a> {
|
||||
// Unless cannot see target, then move towards them
|
||||
controller.inputs.move_dir =
|
||||
bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
|
||||
self.jump_if(controller, bearing.z > 1.5);
|
||||
self.jump_if(bearing.z > 1.5, controller);
|
||||
controller.inputs.move_z = bearing.z;
|
||||
}
|
||||
}
|
||||
@ -713,14 +697,14 @@ impl<'a> AgentData<'a> {
|
||||
) {
|
||||
const DESIRED_ENERGY_LEVEL: f32 = 50.0;
|
||||
const DESIRED_COMBO_LEVEL: u32 = 8;
|
||||
|
||||
let line_of_sight_with_target = || {
|
||||
entities_have_line_of_sight(self.pos, self.body, tgt_data.pos, tgt_data.body, read_data)
|
||||
};
|
||||
|
||||
// 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,
|
||||
)
|
||||
&& line_of_sight_with_target()
|
||||
{
|
||||
// If far enough away, and can see target, check which skill is appropriate to
|
||||
// use
|
||||
@ -798,13 +782,7 @@ 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 line_of_sight_with_target() && attack_data.angle < 45.0 {
|
||||
controller.inputs.move_dir = bearing
|
||||
.xy()
|
||||
.rotated_z(rng.gen_range(0.5..1.57))
|
||||
@ -815,7 +793,7 @@ impl<'a> AgentData<'a> {
|
||||
// Unless cannot see target, then move towards them
|
||||
controller.inputs.move_dir =
|
||||
bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
|
||||
self.jump_if(controller, bearing.z > 1.5);
|
||||
self.jump_if(bearing.z > 1.5, controller);
|
||||
controller.inputs.move_z = bearing.z;
|
||||
}
|
||||
}
|
||||
@ -863,11 +841,12 @@ impl<'a> AgentData<'a> {
|
||||
read_data,
|
||||
Path::Separate,
|
||||
None,
|
||||
) && can_see_tgt(
|
||||
&*read_data.terrain,
|
||||
) && entities_have_line_of_sight(
|
||||
self.pos,
|
||||
self.body,
|
||||
tgt_data.pos,
|
||||
attack_data.dist_sqrd,
|
||||
tgt_data.body,
|
||||
read_data,
|
||||
) && attack_data.angle < 90.0
|
||||
{
|
||||
if agent.action_state.timer > 5.0 {
|
||||
@ -1006,11 +985,12 @@ impl<'a> AgentData<'a> {
|
||||
},
|
||||
) {
|
||||
if attack_data.angle < 15.0
|
||||
&& can_see_tgt(
|
||||
&*read_data.terrain,
|
||||
&& entities_have_line_of_sight(
|
||||
self.pos,
|
||||
self.body,
|
||||
tgt_data.pos,
|
||||
attack_data.dist_sqrd,
|
||||
tgt_data.body,
|
||||
read_data,
|
||||
)
|
||||
{
|
||||
if agent.action_state.timer > 5.0 {
|
||||
@ -1033,12 +1013,12 @@ impl<'a> AgentData<'a> {
|
||||
agent.action_state.timer += read_data.dt.0;
|
||||
}
|
||||
controller.push_basic_input(InputKind::Secondary);
|
||||
self.jump_if(controller, bearing.z > 1.5);
|
||||
self.jump_if(bearing.z > 1.5, controller);
|
||||
controller.inputs.move_z = bearing.z;
|
||||
} else {
|
||||
controller.inputs.move_dir =
|
||||
bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
|
||||
self.jump_if(controller, bearing.z > 1.5);
|
||||
self.jump_if(bearing.z > 1.5, controller);
|
||||
controller.inputs.move_z = bearing.z;
|
||||
}
|
||||
} else {
|
||||
@ -1204,11 +1184,12 @@ impl<'a> AgentData<'a> {
|
||||
Path::Separate,
|
||||
None,
|
||||
) && attack_data.angle < 15.0
|
||||
&& can_see_tgt(
|
||||
&*read_data.terrain,
|
||||
&& entities_have_line_of_sight(
|
||||
self.pos,
|
||||
self.body,
|
||||
tgt_data.pos,
|
||||
attack_data.dist_sqrd,
|
||||
tgt_data.body,
|
||||
read_data,
|
||||
)
|
||||
{
|
||||
controller.push_basic_input(InputKind::Primary);
|
||||
@ -1364,12 +1345,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 entities_have_line_of_sight(self.pos, self.body, tgt_data.pos, tgt_data.body, read_data)
|
||||
&& attack_data.angle < 15.0
|
||||
{
|
||||
controller.push_basic_input(InputKind::Primary);
|
||||
} else {
|
||||
@ -1386,12 +1363,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 entities_have_line_of_sight(self.pos, self.body, tgt_data.pos, tgt_data.body, read_data)
|
||||
&& attack_data.angle < 15.0
|
||||
{
|
||||
controller.push_basic_input(InputKind::Primary);
|
||||
} else {
|
||||
@ -1403,7 +1376,6 @@ impl<'a> AgentData<'a> {
|
||||
&self,
|
||||
agent: &mut Agent,
|
||||
controller: &mut Controller,
|
||||
attack_data: &AttackData,
|
||||
tgt_data: &TargetData,
|
||||
read_data: &ReadData,
|
||||
) {
|
||||
@ -1414,26 +1386,15 @@ 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 entities_have_line_of_sight(self.pos, self.body, tgt_data.pos, tgt_data.body, read_data)
|
||||
{
|
||||
controller.push_basic_input(InputKind::Primary);
|
||||
} else {
|
||||
agent.target = None;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_radial_turret_attack(
|
||||
&self,
|
||||
_agent: &mut Agent,
|
||||
controller: &mut Controller,
|
||||
_attack_data: &AttackData,
|
||||
_tgt_data: &TargetData,
|
||||
_read_data: &ReadData,
|
||||
) {
|
||||
pub fn handle_radial_turret_attack(&self, controller: &mut Controller) {
|
||||
controller.push_basic_input(InputKind::Primary);
|
||||
}
|
||||
|
||||
@ -1465,11 +1426,12 @@ 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,
|
||||
if entities_have_line_of_sight(
|
||||
self.pos,
|
||||
self.body,
|
||||
tgt_data.pos,
|
||||
attack_data.dist_sqrd,
|
||||
tgt_data.body,
|
||||
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,11 +1522,12 @@ impl<'a> AgentData<'a> {
|
||||
let small_chance = rng.gen_bool(0.05);
|
||||
|
||||
if small_chance
|
||||
&& can_see_tgt(
|
||||
&*read_data.terrain,
|
||||
&& entities_have_line_of_sight(
|
||||
self.pos,
|
||||
self.body,
|
||||
tgt_data.pos,
|
||||
attack_data.dist_sqrd,
|
||||
tgt_data.body,
|
||||
read_data,
|
||||
)
|
||||
&& attack_data.angle < 15.0
|
||||
{
|
||||
@ -1592,7 +1555,7 @@ impl<'a> AgentData<'a> {
|
||||
controller.inputs.move_z = 1.0;
|
||||
} else {
|
||||
// Jump
|
||||
self.jump_if(controller, bearing.z > 1.5);
|
||||
self.jump_if(bearing.z > 1.5, controller);
|
||||
controller.inputs.move_z = bearing.z;
|
||||
}
|
||||
}
|
||||
@ -1686,11 +1649,12 @@ 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,
|
||||
&& entities_have_line_of_sight(
|
||||
self.pos,
|
||||
self.body,
|
||||
tgt_data.pos,
|
||||
attack_data.dist_sqrd,
|
||||
tgt_data.body,
|
||||
read_data,
|
||||
)
|
||||
&& attack_data.angle < 15.0
|
||||
{
|
||||
@ -1712,7 +1676,7 @@ impl<'a> AgentData<'a> {
|
||||
controller.push_basic_input(InputKind::Fly);
|
||||
controller.inputs.move_z = 1.0;
|
||||
} else {
|
||||
self.jump_if(controller, bearing.z > 1.5);
|
||||
self.jump_if(bearing.z > 1.5, controller);
|
||||
controller.inputs.move_z = bearing.z;
|
||||
}
|
||||
}
|
||||
@ -1903,11 +1867,12 @@ impl<'a> AgentData<'a> {
|
||||
},
|
||||
) {
|
||||
if attack_data.angle < 15.0
|
||||
&& can_see_tgt(
|
||||
&*read_data.terrain,
|
||||
&& entities_have_line_of_sight(
|
||||
self.pos,
|
||||
self.body,
|
||||
tgt_data.pos,
|
||||
attack_data.dist_sqrd,
|
||||
tgt_data.body,
|
||||
read_data,
|
||||
)
|
||||
{
|
||||
if agent.action_state.timer > 5.0 {
|
||||
@ -1930,12 +1895,12 @@ impl<'a> AgentData<'a> {
|
||||
agent.action_state.timer += read_data.dt.0;
|
||||
}
|
||||
controller.push_basic_input(InputKind::Ability(0));
|
||||
self.jump_if(controller, bearing.z > 1.5);
|
||||
self.jump_if(bearing.z > 1.5, controller);
|
||||
controller.inputs.move_z = bearing.z;
|
||||
} else {
|
||||
controller.inputs.move_dir =
|
||||
bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
|
||||
self.jump_if(controller, bearing.z > 1.5);
|
||||
self.jump_if(bearing.z > 1.5, controller);
|
||||
controller.inputs.move_z = bearing.z;
|
||||
}
|
||||
} else {
|
||||
@ -2118,6 +2083,10 @@ impl<'a> AgentData<'a> {
|
||||
.map(|t| t.target)
|
||||
.and_then(|e| read_data.velocities.get(e))
|
||||
.map_or(0.0, |v| v.0.cross(self.ori.look_vec()).magnitude_squared());
|
||||
let line_of_sight_with_target = || {
|
||||
entities_have_line_of_sight(self.pos, self.body, tgt_data.pos, tgt_data.body, read_data)
|
||||
};
|
||||
|
||||
if attack_data.dist_sqrd < golem_melee_range.powi(2) {
|
||||
if agent.action_state.counter < 7.5 {
|
||||
// If target is close, whack them
|
||||
@ -2134,12 +2103,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,
|
||||
)
|
||||
&& line_of_sight_with_target()
|
||||
&& attack_data.angle < 45.0
|
||||
{
|
||||
// If target in range threshold and haven't been lasering for more than 5
|
||||
@ -2151,14 +2115,7 @@ impl<'a> AgentData<'a> {
|
||||
controller.push_basic_input(InputKind::Ability(0));
|
||||
}
|
||||
} 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,
|
||||
)
|
||||
{
|
||||
if target_speed_cross_sqd < GOLEM_TARGET_SPEED.powi(2) && line_of_sight_with_target() {
|
||||
// If target is far-ish and moving slow-ish, rocket them
|
||||
controller.push_basic_input(InputKind::Ability(1));
|
||||
} else if health_fraction < 0.7 {
|
||||
@ -2190,6 +2147,10 @@ impl<'a> AgentData<'a> {
|
||||
const BUBBLE_RANGE: f32 = 20.0;
|
||||
const MINION_SUMMON_THRESHOLD: f32 = 0.20;
|
||||
let health_fraction = self.health.map_or(0.5, |h| h.fraction());
|
||||
let line_of_sight_with_target = || {
|
||||
entities_have_line_of_sight(self.pos, self.body, tgt_data.pos, tgt_data.body, read_data)
|
||||
};
|
||||
|
||||
// Sets counter at start of combat, using `condition` to keep track of whether
|
||||
// it was already intitialized
|
||||
if !agent.action_state.condition {
|
||||
@ -2219,26 +2180,12 @@ impl<'a> AgentData<'a> {
|
||||
} else if attack_data.in_min_range() && attack_data.angle < 60.0 {
|
||||
// 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,
|
||||
)
|
||||
{
|
||||
} else if attack_data.angle < 30.0 && line_of_sight_with_target() {
|
||||
// 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,
|
||||
)
|
||||
{
|
||||
} else if attack_data.angle < 90.0 && line_of_sight_with_target() {
|
||||
// Start scuttling if not close enough to do something else and in angle and can
|
||||
// see target
|
||||
controller.push_basic_input(InputKind::Secondary);
|
||||
@ -2321,6 +2268,9 @@ impl<'a> AgentData<'a> {
|
||||
const FIRE_BREATH_RANGE: f32 = 20.0;
|
||||
const MAX_PUMPKIN_RANGE: f32 = 50.0;
|
||||
let health_fraction = self.health.map_or(0.5, |h| h.fraction());
|
||||
let line_of_sight_with_target = || {
|
||||
entities_have_line_of_sight(self.pos, self.body, tgt_data.pos, tgt_data.body, read_data)
|
||||
};
|
||||
|
||||
if health_fraction < VINE_CREATION_THRESHOLD && !agent.action_state.condition {
|
||||
// Summon vines when reach threshold of health
|
||||
@ -2332,12 +2282,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,
|
||||
)
|
||||
&& line_of_sight_with_target()
|
||||
{
|
||||
// Keep breathing fire if close enough, can see target, and have not been
|
||||
// breathing for more than 5 seconds
|
||||
@ -2345,25 +2290,11 @@ impl<'a> AgentData<'a> {
|
||||
} else if attack_data.in_min_range() && attack_data.angle < 60.0 {
|
||||
// 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,
|
||||
)
|
||||
{
|
||||
} else if attack_data.angle < 30.0 && line_of_sight_with_target() {
|
||||
// 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,
|
||||
)
|
||||
{
|
||||
} else if attack_data.dist_sqrd < MAX_PUMPKIN_RANGE.powi(2) && line_of_sight_with_target() {
|
||||
// Throw a pumpkin at them if close enough and can see them
|
||||
controller.push_basic_input(InputKind::Ability(1));
|
||||
}
|
||||
@ -2457,11 +2388,12 @@ 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,
|
||||
&& entities_have_line_of_sight(
|
||||
self.pos,
|
||||
self.body,
|
||||
tgt_data.pos,
|
||||
attack_data.dist_sqrd,
|
||||
tgt_data.body,
|
||||
read_data,
|
||||
)
|
||||
{
|
||||
// If in pathing range and can see target, move towards them
|
||||
@ -2594,11 +2526,12 @@ 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,
|
||||
} else if entities_have_line_of_sight(
|
||||
self.pos,
|
||||
self.body,
|
||||
tgt_data.pos,
|
||||
attack_data.dist_sqrd,
|
||||
tgt_data.body,
|
||||
read_data,
|
||||
) {
|
||||
// Else if in sight, barrage
|
||||
controller.push_basic_input(InputKind::Secondary);
|
||||
|
@ -1,8 +1,11 @@
|
||||
use crate::sys::agent::{AgentData, ReadData};
|
||||
use common::{
|
||||
comp::{agent::Psyche, buff::BuffKind, Alignment, Pos},
|
||||
comp::{
|
||||
agent::Psyche, buff::BuffKind, inventory::item::ItemTag, item::ItemDesc, Alignment, Body,
|
||||
Pos,
|
||||
},
|
||||
consts::GRAVITY,
|
||||
terrain::{Block, TerrainGrid},
|
||||
terrain::Block,
|
||||
util::Dir,
|
||||
vol::ReadVol,
|
||||
};
|
||||
@ -12,16 +15,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,7 +32,8 @@ 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
|
||||
/// Gets alignment of owner if alignment given is `Owned`.
|
||||
/// Returns original alignment if not owned.
|
||||
pub fn try_owner_alignment<'a>(
|
||||
alignment: Option<&'a Alignment>,
|
||||
read_data: &'a ReadData,
|
||||
@ -122,7 +116,88 @@ 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_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 {
|
||||
try_owner_alignment(our_alignment, read_data).map_or(false, |our_owners_alignment| {
|
||||
try_owner_alignment(their_alignment, read_data).map_or(false, |their_owners_alignment| {
|
||||
our_owners_alignment.hostile_towards(*their_owners_alignment)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn entities_have_line_of_sight(
|
||||
pos: &Pos,
|
||||
body: Option<&Body>,
|
||||
other_pos: &Pos,
|
||||
other_body: Option<&Body>,
|
||||
read_data: &ReadData,
|
||||
) -> bool {
|
||||
let get_eye_pos = |pos: &Pos, body: Option<&Body>| {
|
||||
let eye_offset = body.map_or(0.0, |b| b.eye_height());
|
||||
|
||||
Pos(pos.0.with_z(pos.0.z + eye_offset))
|
||||
};
|
||||
let eye_pos = get_eye_pos(pos, body);
|
||||
let other_eye_pos = get_eye_pos(other_pos, other_body);
|
||||
|
||||
positions_have_line_of_sight(&eye_pos, &other_eye_pos, read_data)
|
||||
}
|
||||
|
||||
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, pos_b.0)
|
||||
.until(Block::is_opaque)
|
||||
.cast()
|
||||
.0
|
||||
.powi(2)
|
||||
>= dist_sqrd
|
||||
}
|
||||
|
||||
pub fn is_dressed_as_cultist(entity: EcsEntity, read_data: &ReadData) -> bool {
|
||||
read_data
|
||||
.inventories
|
||||
.get(entity)
|
||||
.map_or(false, |inventory| {
|
||||
inventory
|
||||
.equipped_items()
|
||||
.filter(|item| item.tags().contains(&ItemTag::Cultist))
|
||||
.count()
|
||||
> 2
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_attacker(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