veloren/common/src/sys/agent.rs

799 lines
37 KiB
Rust
Raw Normal View History

use crate::{
2020-04-18 18:28:19 +00:00
comp::{
self,
agent::Activity,
group,
group::Invite,
2020-11-06 17:39:49 +00:00
item::{
tool::{ToolKind, UniqueKind},
ItemKind,
},
Agent, Alignment, Body, CharacterState, ControlAction, ControlEvent, Controller, Energy,
GroupManip, Health, LightEmitter, Loadout, MountState, Ori, PhysicsState, Pos, Scale,
UnresolvedChatMsg, Vel,
2020-04-18 18:28:19 +00:00
},
event::{EventBus, ServerEvent},
metrics::SysMetrics,
2020-07-13 22:23:44 +00:00
path::{Chaser, TraversalConfig},
span,
2020-10-07 02:23:20 +00:00
state::{DeltaTime, Time, TimeOfDay},
sync::{Uid, UidAllocator},
terrain::{Block, TerrainGrid},
2020-10-07 02:23:20 +00:00
time::DayPeriod,
util::Dir,
vol::ReadVol,
};
use rand::{thread_rng, Rng};
use specs::{
saveload::{Marker, MarkerAllocator},
Entities, Join, Read, ReadExpect, ReadStorage, System, Write, WriteStorage,
};
use vek::*;
2019-06-09 19:33:20 +00:00
/// This system will allow NPCs to modify their controller
pub struct Sys;
impl<'a> System<'a> for Sys {
#[allow(clippy::type_complexity)]
type SystemData = (
2020-10-08 22:55:30 +00:00
(
Read<'a, UidAllocator>,
Read<'a, Time>,
Read<'a, DeltaTime>,
Read<'a, group::GroupManager>,
),
ReadExpect<'a, SysMetrics>,
Write<'a, EventBus<ServerEvent>>,
Entities<'a>,
ReadStorage<'a, Energy>,
ReadStorage<'a, Pos>,
2020-07-03 19:30:21 +00:00
ReadStorage<'a, Vel>,
ReadStorage<'a, Ori>,
ReadStorage<'a, Scale>,
ReadStorage<'a, Health>,
ReadStorage<'a, Loadout>,
2020-07-09 23:43:11 +00:00
ReadStorage<'a, PhysicsState>,
ReadStorage<'a, Uid>,
ReadStorage<'a, group::Group>,
ReadExpect<'a, TerrainGrid>,
ReadStorage<'a, Alignment>,
2020-07-10 14:00:20 +00:00
ReadStorage<'a, Body>,
WriteStorage<'a, Agent>,
2019-06-09 14:20:20 +00:00
WriteStorage<'a, Controller>,
ReadStorage<'a, MountState>,
ReadStorage<'a, Invite>,
2020-10-07 02:23:20 +00:00
Read<'a, TimeOfDay>,
ReadStorage<'a, LightEmitter>,
ReadStorage<'a, CharacterState>,
);
#[allow(clippy::or_fun_call)] // TODO: Pending review in #587
2019-08-04 08:21:29 +00:00
fn run(
&mut self,
(
2020-10-08 22:55:30 +00:00
(uid_allocator, time, dt, group_manager),
sys_metrics,
event_bus,
entities,
energies,
positions,
2020-07-03 19:30:21 +00:00
velocities,
orientations,
scales,
healths,
loadouts,
2020-07-09 23:43:11 +00:00
physics_states,
uids,
groups,
terrain,
alignments,
2020-07-10 14:00:20 +00:00
bodies,
mut agents,
mut controllers,
mount_states,
invites,
2020-10-07 02:23:20 +00:00
time_of_day,
light_emitter,
char_states,
): Self::SystemData,
2019-08-04 08:21:29 +00:00
) {
let start_time = std::time::Instant::now();
span!(_guard, "run", "agent::Sys::run");
2020-04-18 18:28:19 +00:00
for (
entity,
energy,
2020-04-18 18:28:19 +00:00
pos,
2020-07-03 19:30:21 +00:00
vel,
2020-04-18 18:28:19 +00:00
ori,
alignment,
loadout,
2020-07-09 23:43:11 +00:00
physics_state,
2020-07-10 14:00:20 +00:00
body,
uid,
2020-04-18 18:28:19 +00:00
agent,
controller,
mount_state,
group,
2020-10-07 02:23:20 +00:00
light_emitter,
2020-04-18 18:28:19 +00:00
) in (
&entities,
&energies,
&positions,
2020-07-03 19:30:21 +00:00
&velocities,
&orientations,
alignments.maybe(),
&loadouts,
2020-07-09 23:43:11 +00:00
&physics_states,
2020-07-10 14:00:20 +00:00
bodies.maybe(),
&uids,
&mut agents,
&mut controllers,
mount_states.maybe(),
groups.maybe(),
2020-10-07 02:23:20 +00:00
light_emitter.maybe(),
)
.join()
{
// Hack, replace with better system when groups are more sophisticated
// Override alignment if in a group unless entity is owned already
let alignment = if !matches!(alignment, Some(Alignment::Owned(_))) {
group
.and_then(|g| group_manager.group_info(*g))
.and_then(|info| uids.get(info.leader))
.copied()
.map(Alignment::Owned)
.or(alignment.copied())
} else {
alignment.copied()
};
// Skip mounted entities
if mount_state
.map(|ms| *ms != MountState::Unmounted)
.unwrap_or(false)
{
continue;
}
controller.reset();
2020-10-07 02:23:20 +00:00
let mut event_emitter = event_bus.emitter();
// Light lanterns at night
// TODO Add a method to turn on NPC lanterns underground
let lantern_equipped = loadout.lantern.as_ref().map_or(false, |item| {
matches!(item.kind(), comp::item::ItemKind::Lantern(_))
});
let lantern_turned_on = light_emitter.is_some();
let day_period = DayPeriod::from(time_of_day.0);
// Only emit event for agents that have a lantern equipped
if lantern_equipped {
let mut rng = thread_rng();
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
// Only emit event for agents that sill need to
// turn on their lantern
if let 0 = rng.gen_range(0, 1000) {
controller.events.push(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
if let 0 = rng.gen_range(0, 2000) {
controller.events.push(ControlEvent::DisableLantern)
}
}
};
let mut inputs = &mut controller.inputs;
2020-08-25 12:21:25 +00:00
// Default to looking in orientation direction (can be overridden below)
inputs.look_dir = ori.0;
2020-01-25 18:49:47 +00:00
const AVG_FOLLOW_DIST: f32 = 6.0;
const MAX_FOLLOW_DIST: f32 = 12.0;
const MAX_CHASE_DIST: f32 = 18.0;
2020-04-19 11:50:25 +00:00
const LISTEN_DIST: f32 = 16.0;
const SEARCH_DIST: f32 = 48.0;
const SIGHT_DIST: f32 = 80.0;
const MIN_ATTACK_DIST: f32 = 2.0;
const MAX_FLEE_DIST: f32 = 20.0;
const SNEAK_COEFFICIENT: f32 = 0.25;
2020-01-25 18:49:47 +00:00
2020-04-23 12:29:22 +00:00
let scale = scales.get(entity).map(|s| s.0).unwrap_or(1.0);
2020-04-23 14:01:37 +00:00
2020-04-23 16:00:48 +00:00
// This controls how picky NPCs are about their pathfinding. Giants are larger
// and so can afford to be less precise when trying to move around
// the world (especially since they would otherwise get stuck on
// obstacles that smaller entities would not).
2020-07-30 19:26:49 +00:00
let node_tolerance = scale * 1.5;
2020-07-10 14:00:20 +00:00
let slow_factor = body.map(|b| b.base_accel() / 250.0).unwrap_or(0.0).min(1.0);
let traversal_config = TraversalConfig {
node_tolerance,
slow_factor,
on_ground: physics_state.on_ground,
in_liquid: physics_state.in_liquid.is_some(),
min_tgt_dist: 1.0,
can_climb: body.map(|b| b.can_climb()).unwrap_or(false),
};
2020-01-25 18:49:47 +00:00
let mut do_idle = false;
let mut choose_target = false;
2020-01-25 18:49:47 +00:00
'activity: {
match &mut agent.activity {
Activity::Idle { bearing, chaser } => {
if let Some(travel_to) = agent.rtsim_controller.travel_to {
2020-11-23 15:39:03 +00:00
if let Some((bearing, speed)) =
chaser.chase(&*terrain, pos.0, vel.0, travel_to, TraversalConfig {
min_tgt_dist: 1.25,
..traversal_config
2020-11-23 15:39:03 +00:00
})
{
inputs.move_dir =
bearing.xy().try_normalized().unwrap_or(Vec2::zero())
* speed.min(agent.rtsim_controller.speed_factor);
inputs.jump.set_state(bearing.z > 1.5);
2020-11-14 23:45:27 +00:00
inputs.climb = Some(comp::Climb::Up);
2020-11-23 15:39:03 +00:00
//.filter(|_| bearing.z > 0.1 || physics_state.in_liquid.is_some());
2020-11-14 23:45:27 +00:00
inputs.move_z = bearing.z + 0.05;
}
} else {
*bearing += Vec2::new(
thread_rng().gen::<f32>() - 0.5,
thread_rng().gen::<f32>() - 0.5,
) * 0.1
- *bearing * 0.003
- agent.patrol_origin.map_or(Vec2::zero(), |patrol_origin| {
(pos.0 - patrol_origin).xy() * 0.0002
});
// Stop if we're too close to a wall
*bearing *= 0.1
+ if terrain
.ray(
pos.0 + Vec3::unit_z(),
pos.0
+ Vec3::from(*bearing)
.try_normalized()
.unwrap_or(Vec3::unit_y())
* 5.0
+ Vec3::unit_z(),
)
.until(Block::is_solid)
.cast()
.1
.map_or(true, |b| b.is_none())
{
0.9
} else {
0.0
};
if bearing.magnitude_squared() > 0.5f32.powf(2.0) {
inputs.move_dir = *bearing * 0.65;
}
// Sit
if thread_rng().gen::<f32>() < 0.0035 {
controller.actions.push(ControlAction::Sit);
}
}
controller.actions.push(ControlAction::Unwield);
2020-04-19 11:50:25 +00:00
// Sometimes try searching for new targets
2020-04-18 18:28:19 +00:00
if thread_rng().gen::<f32>() < 0.1 {
choose_target = true;
}
2020-02-26 03:59:08 +00:00
},
Activity::Follow { target, chaser } => {
if let (Some(tgt_pos), _tgt_health) =
(positions.get(*target), healths.get(*target))
{
2020-07-06 19:51:23 +00:00
let dist = pos.0.distance(tgt_pos.0);
// Follow, or return to idle
2020-07-06 19:51:23 +00:00
if dist > AVG_FOLLOW_DIST {
if let Some((bearing, speed)) = chaser.chase(
2020-04-18 18:28:19 +00:00
&*terrain,
pos.0,
vel.0,
2020-04-18 18:28:19 +00:00
tgt_pos.0,
2020-07-13 22:23:44 +00:00
TraversalConfig {
min_tgt_dist: AVG_FOLLOW_DIST,
..traversal_config
2020-07-13 22:23:44 +00:00
},
2020-04-18 18:28:19 +00:00
) {
inputs.move_dir =
bearing.xy().try_normalized().unwrap_or(Vec2::zero())
2020-07-06 19:51:23 +00:00
* speed.min(0.2 + (dist - AVG_FOLLOW_DIST) / 8.0);
2020-07-04 00:17:51 +00:00
inputs.jump.set_state(bearing.z > 1.5);
2020-11-03 22:46:07 +00:00
inputs.move_z = bearing.z;
}
} else {
do_idle = true;
2020-01-25 18:49:47 +00:00
}
} else {
do_idle = true;
}
2020-02-26 03:59:08 +00:00
},
Activity::Attack {
target,
chaser,
been_close,
powerup,
..
} => {
#[derive(Eq, PartialEq)]
enum Tactic {
2020-04-19 11:50:25 +00:00
Melee,
2020-10-19 03:48:28 +00:00
Axe,
Hammer,
Sword,
Bow,
2020-04-19 11:50:25 +00:00
Staff,
StoneGolemBoss,
}
2020-04-18 18:28:19 +00:00
let tactic = match loadout.active_item.as_ref().and_then(|ic| {
if let ItemKind::Tool(tool) = &ic.item.kind() {
2020-04-18 18:28:19 +00:00
Some(&tool.kind)
} else {
None
}
}) {
Some(ToolKind::Bow) => Tactic::Bow,
2020-11-06 17:39:49 +00:00
Some(ToolKind::Staff) => Tactic::Staff,
Some(ToolKind::Hammer) => Tactic::Hammer,
Some(ToolKind::Sword) => Tactic::Sword,
Some(ToolKind::Axe) => Tactic::Axe,
Some(ToolKind::Unique(UniqueKind::StoneGolemFist)) => {
Tactic::StoneGolemBoss
},
_ => Tactic::Melee,
};
if let (Some(tgt_pos), Some(tgt_health), tgt_alignment) = (
positions.get(*target),
healths.get(*target),
2020-07-07 00:11:37 +00:00
alignments.get(*target).copied().unwrap_or(
uids.get(*target)
2020-07-07 00:01:39 +00:00
.copied()
.map(Alignment::Owned)
2020-07-07 00:11:37 +00:00
.unwrap_or(Alignment::Wild),
),
) {
controller.actions.push(ControlAction::Wield);
2020-11-15 01:17:41 +00:00
let eye_offset = body.map_or(0.0, |b| b.eye_height());
let tgt_eye_offset =
2020-11-15 01:17:41 +00:00
bodies.get(*target).map_or(0.0, |b| b.eye_height());
let distance_offset = match tactic {
Tactic::Bow => 0.0004 * pos.0.distance_squared(tgt_pos.0),
Tactic::Staff => 0.0015 * pos.0.distance_squared(tgt_pos.0),
_ => 0.0,
};
if let Some(dir) = Dir::from_unnormalized(
Vec3::new(
tgt_pos.0.x,
tgt_pos.0.y,
tgt_pos.0.z + tgt_eye_offset + distance_offset,
) - Vec3::new(pos.0.x, pos.0.y, pos.0.z + eye_offset),
) {
inputs.look_dir = dir;
}
//if let Some(dir) = Dir::from_unnormalized(tgt_pos.0 - pos.0) {
// inputs.look_dir = dir;
//}
// Don't attack entities we are passive towards
// TODO: This is here, it's a bit of a hack
if let Some(alignment) = alignment {
if alignment.passive_towards(tgt_alignment) || tgt_health.is_dead {
do_idle = true;
break 'activity;
}
}
let dist_sqrd = pos.0.distance_squared(tgt_pos.0);
2020-07-29 19:58:14 +00:00
let damage = healths
2020-07-29 19:58:14 +00:00
.get(entity)
.map(|h| h.current() as f32 / h.maximum() as f32)
2020-07-29 19:58:14 +00:00
.unwrap_or(0.5);
// Flee
2020-07-30 19:26:49 +00:00
let flees = alignment
.map(|a| !matches!(a, Alignment::Enemy | Alignment::Owned(_)))
.unwrap_or(true);
if 1.0 - agent.psyche.aggro > damage && flees {
2020-11-07 18:00:07 +00:00
controller.actions.push(ControlAction::Unwield);
if dist_sqrd < MAX_FLEE_DIST.powf(2.0) {
if let Some((bearing, speed)) = chaser.chase(
&*terrain,
pos.0,
vel.0,
// Away from the target (ironically)
2020-08-12 14:10:12 +00:00
pos.0
+ (pos.0 - tgt_pos.0)
.try_normalized()
.unwrap_or_else(Vec3::unit_y)
* 8.0,
TraversalConfig {
min_tgt_dist: 1.25,
..traversal_config
},
) {
2020-10-17 01:27:57 +00:00
inputs.move_dir =
bearing.xy().try_normalized().unwrap_or(Vec2::zero())
* speed
* 0.4; //Let small/slow animals flee slower than the player
inputs.jump.set_state(bearing.z > 1.5);
2020-11-03 22:46:07 +00:00
inputs.move_z = bearing.z;
}
} else {
do_idle = true;
2020-07-29 19:58:14 +00:00
}
} else if (tactic == Tactic::Staff
&& dist_sqrd < (5.0 * MIN_ATTACK_DIST * scale).powf(2.0))
|| dist_sqrd < (MIN_ATTACK_DIST * scale).powf(2.0)
{
// Close-range attack
2020-10-17 01:27:57 +00:00
inputs.move_dir = (tgt_pos.0 - pos.0)
.xy()
2020-07-07 01:21:14 +00:00
.try_normalized()
.unwrap_or(Vec2::unit_y())
* 0.1;
2020-04-23 12:29:22 +00:00
match tactic {
Tactic::Melee | Tactic::StoneGolemBoss => {
inputs.primary.set_state(true)
},
Tactic::Hammer => {
if *powerup > 4.0 {
inputs.secondary.set_state(false);
*powerup = 0.0;
} else if *powerup > 2.0 {
inputs.secondary.set_state(true);
*powerup += dt.0;
} else if energy.current() > 700 {
inputs.ability3.set_state(true);
*powerup += dt.0;
} else {
inputs.primary.set_state(true);
*powerup += dt.0;
}
},
Tactic::Staff => {
2020-10-18 23:34:50 +00:00
// Kind of arbitrary values, but feel right in game
if energy.current() > 800 && thread_rng().gen_bool(0.2) {
2020-10-17 01:27:57 +00:00
inputs.ability3.set_state(true)
} else if energy.current() > 10 {
inputs.secondary.set_state(true)
} else {
inputs.primary.set_state(true)
}
},
Tactic::Sword => {
if *powerup < 2.0 && energy.current() > 500 {
inputs.ability3.set_state(true);
*powerup += dt.0;
} else if *powerup > 2.0 {
*powerup = 0.0;
} else {
inputs.primary.set_state(true);
*powerup += dt.0;
}
},
2020-10-19 03:48:28 +00:00
Tactic::Axe => {
if *powerup > 6.0 {
inputs.secondary.set_state(false);
*powerup = 0.0;
} else if *powerup > 4.0 && energy.current() > 100 {
2020-10-19 03:48:28 +00:00
inputs.secondary.set_state(true);
*powerup += dt.0;
} else if energy.current() > 800
&& thread_rng().gen_bool(0.5)
{
inputs.ability3.set_state(true);
*powerup += dt.0;
2020-10-19 03:48:28 +00:00
} else {
inputs.primary.set_state(true);
*powerup += dt.0;
}
},
Tactic::Bow => inputs.roll.set_state(true),
}
} else if dist_sqrd < MAX_CHASE_DIST.powf(2.0)
2020-04-18 18:28:19 +00:00
|| (dist_sqrd < SIGHT_DIST.powf(2.0)
&& (!*been_close || !matches!(tactic, Tactic::Melee)))
{
2020-04-19 11:50:25 +00:00
let can_see_tgt = terrain
.ray(pos.0 + Vec3::unit_z(), tgt_pos.0 + Vec3::unit_z())
.until(Block::is_opaque)
2020-04-19 11:50:25 +00:00
.cast()
2020-04-18 18:28:19 +00:00
.0
.powf(2.0)
>= dist_sqrd;
2020-04-19 11:50:25 +00:00
if can_see_tgt {
match tactic {
Tactic::Bow => {
if *powerup > 4.0 {
inputs.secondary.set_state(false);
*powerup = 0.0;
} else if *powerup > 2.0 && energy.current() > 300 {
inputs.secondary.set_state(true);
*powerup += dt.0;
} else if energy.current() > 400
&& thread_rng().gen_bool(0.8)
{
inputs.secondary.set_state(false);
inputs.ability3.set_state(true);
*powerup += dt.0;
} else {
inputs.secondary.set_state(false);
inputs.primary.set_state(true);
*powerup += dt.0;
}
},
Tactic::Sword => {
if *powerup > 4.0 {
inputs.secondary.set_state(true);
*powerup = 0.0;
} else {
*powerup += dt.0;
}
},
Tactic::Staff => {
2020-04-19 11:50:25 +00:00
inputs.primary.set_state(true);
},
Tactic::Hammer => {
if *powerup > 5.0 {
inputs.ability3.set_state(true);
*powerup = 0.0;
} else {
*powerup += dt.0;
}
},
Tactic::StoneGolemBoss => {
if *powerup > 5.0 {
inputs.secondary.set_state(true);
*powerup = 0.0;
} else {
*powerup += dt.0;
}
},
_ => {},
2020-04-19 11:50:25 +00:00
}
}
2020-04-19 11:50:25 +00:00
if dist_sqrd < MAX_CHASE_DIST.powf(2.0) {
*been_close = true;
}
// Long-range chase
if let Some((bearing, speed)) = chaser.chase(
2020-04-18 18:28:19 +00:00
&*terrain,
pos.0,
vel.0,
2020-04-18 18:28:19 +00:00
tgt_pos.0,
2020-07-13 22:23:44 +00:00
TraversalConfig {
min_tgt_dist: 1.25,
..traversal_config
2020-07-13 22:23:44 +00:00
},
2020-04-18 18:28:19 +00:00
) {
2020-10-17 01:27:57 +00:00
if can_see_tgt {
match tactic {
Tactic::Bow => {
2020-10-17 01:27:57 +00:00
inputs.move_dir = bearing
.xy()
.rotated_z(thread_rng().gen_range(0.5, 1.57))
.try_normalized()
.unwrap_or(Vec2::zero())
* speed;
},
Tactic::Staff => {
inputs.move_dir = bearing
.xy()
.rotated_z(thread_rng().gen_range(-1.57, -0.5))
.try_normalized()
.unwrap_or(Vec2::zero())
* speed;
},
_ => {
inputs.move_dir = bearing
.xy()
.try_normalized()
.unwrap_or(Vec2::zero())
* speed;
},
}
} else {
inputs.move_dir =
bearing.xy().try_normalized().unwrap_or(Vec2::zero())
* speed;
inputs.jump.set_state(bearing.z > 1.5);
2020-11-03 22:46:07 +00:00
inputs.move_z = bearing.z;
2020-10-17 01:27:57 +00:00
}
}
if dist_sqrd < 16.0f32.powf(2.0)
&& matches!(tactic, Tactic::Melee)
&& thread_rng().gen::<f32>() < 0.02
{
inputs.roll.set_state(true);
}
} else {
do_idle = true;
2020-01-25 18:49:47 +00:00
}
} else {
do_idle = true;
}
2020-02-26 04:01:51 +00:00
},
2020-01-25 18:49:47 +00:00
}
}
if do_idle {
agent.activity = Activity::Idle {
bearing: Vec2::zero(),
chaser: Chaser::default(),
};
2020-01-25 18:49:47 +00:00
}
// Choose a new target to attack: only go out of our way to attack targets we
// are hostile toward!
if choose_target {
// Search for new targets (this looks expensive, but it's only run occasionally)
// TODO: Replace this with a better system that doesn't consider *all* entities
let closest_entity = (&entities, &positions, &healths, alignments.maybe(), char_states.maybe())
.join()
.filter(|(e, e_pos, e_health, e_alignment, char_state)| {
let mut search_dist = SEARCH_DIST;
let mut listen_dist = LISTEN_DIST;
if char_state.map_or(false, |c_s| c_s.is_stealthy()) {
// TODO: make sneak more effective based on a stat like e_stats.fitness
search_dist *= SNEAK_COEFFICIENT;
listen_dist *= SNEAK_COEFFICIENT;
}
((e_pos.0.distance_squared(pos.0) < search_dist.powf(2.0) &&
// Within our view
(e_pos.0 - pos.0).try_normalized().map(|v| v.dot(*inputs.look_dir) > 0.15).unwrap_or(true))
// Within listen distance
|| e_pos.0.distance_squared(pos.0) < listen_dist.powf(2.0))
&& *e != entity
&& !e_health.is_dead
&& alignment
.and_then(|a| e_alignment.map(|b| a.hostile_towards(*b)))
.unwrap_or(false)
})
2020-04-19 11:50:25 +00:00
// Can we even see them?
.filter(|(_, e_pos, _, _, _)| terrain
2020-04-19 11:50:25 +00:00
.ray(pos.0 + Vec3::unit_z(), e_pos.0 + Vec3::unit_z())
.until(Block::is_opaque)
2020-04-19 11:50:25 +00:00
.cast()
.0 >= e_pos.0.distance(pos.0))
.min_by_key(|(_, e_pos, _, _, _)| (e_pos.0.distance_squared(pos.0) * 100.0) as i32)
.map(|(e, _, _, _, _)| e);
if let Some(target) = closest_entity {
agent.activity = Activity::Attack {
target,
chaser: Chaser::default(),
time: time.0,
been_close: false,
powerup: 0.0,
};
}
}
// --- Activity overrides (in reverse order of priority: most important goes
// last!) ---
2020-01-25 18:49:47 +00:00
// Attack a target that's attacking us
if let Some(my_health) = healths.get(entity) {
2020-01-25 18:49:47 +00:00
// Only if the attack was recent
if my_health.last_change.0 < 3.0 {
2020-11-05 01:21:42 +00:00
if let comp::HealthSource::Damage { by: Some(by), .. } =
my_health.last_change.1.cause
{
2020-01-25 18:49:47 +00:00
if !agent.activity.is_attack() {
if let Some(attacker) = uid_allocator.retrieve_entity_internal(by.id())
{
if healths.get(attacker).map_or(false, |a| !a.is_dead) {
2020-07-30 19:26:49 +00:00
match agent.activity {
Activity::Attack { target, .. } if target == attacker => {},
_ => {
if agent.can_speak {
2020-08-12 14:10:12 +00:00
let msg =
"npc.speech.villager_under_attack".to_string();
2020-10-07 02:23:20 +00:00
event_emitter.emit(ServerEvent::Chat(
2020-08-12 14:10:12 +00:00
UnresolvedChatMsg::npc(*uid, msg),
));
2020-07-30 19:26:49 +00:00
}
agent.activity = Activity::Attack {
target: attacker,
chaser: Chaser::default(),
time: time.0,
been_close: false,
powerup: 0.0,
};
},
}
}
2020-01-25 18:49:47 +00:00
}
}
}
}
}
// Follow owner if we're too far, or if they're under attack
if let Some(Alignment::Owned(owner)) = alignment {
2020-07-07 00:01:39 +00:00
(|| {
let owner = uid_allocator.retrieve_entity_internal(owner.id())?;
let owner_pos = positions.get(owner)?;
let dist_sqrd = pos.0.distance_squared(owner_pos.0);
if dist_sqrd > MAX_FOLLOW_DIST.powf(2.0) && !agent.activity.is_follow() {
2020-07-04 00:17:51 +00:00
agent.activity = Activity::Follow {
target: owner,
chaser: Chaser::default(),
};
2020-01-25 18:49:47 +00:00
}
// Attack owner's attacker
let owner_health = healths.get(owner)?;
if owner_health.last_change.0 < 5.0 && owner_health.last_change.1.amount < 0 {
2020-11-05 01:21:42 +00:00
if let comp::HealthSource::Damage { by: Some(by), .. } =
owner_health.last_change.1.cause
2020-07-07 00:01:39 +00:00
{
if !agent.activity.is_attack() {
let attacker = uid_allocator.retrieve_entity_internal(by.id())?;
agent.activity = Activity::Attack {
target: attacker,
chaser: Chaser::default(),
time: time.0,
been_close: false,
powerup: 0.0,
};
2020-01-25 18:49:47 +00:00
}
}
}
2020-07-07 00:01:39 +00:00
Some(())
})();
2020-01-25 18:49:47 +00:00
}
debug_assert!(inputs.move_dir.map(|e| !e.is_nan()).reduce_and());
debug_assert!(inputs.look_dir.map(|e| !e.is_nan()).reduce_and());
}
2020-08-25 12:21:25 +00:00
// Process group invites
for (_invite, /*alignment,*/ agent, controller) in
(&invites, /*&alignments,*/ &mut agents, &mut controllers).join()
{
let accept = false; // set back to "matches!(alignment, Alignment::Npc)" when we got better NPC recruitment mechanics
if accept {
// Clear agent comp
*agent = Agent::default();
controller
.events
.push(ControlEvent::GroupManip(GroupManip::Accept));
} else {
controller
.events
.push(ControlEvent::GroupManip(GroupManip::Decline));
}
}
sys_metrics.agent_ns.store(
start_time.elapsed().as_nanos() as i64,
std::sync::atomic::Ordering::Relaxed,
);
}
}