veloren/common/src/sys/agent.rs

451 lines
19 KiB
Rust
Raw Normal View History

use crate::{
2020-04-18 18:28:19 +00:00
comp::{
self,
agent::Activity,
item::{tool::ToolKind, ItemKind},
Agent, Alignment, CharacterState, ChatMsg, ControlAction, Controller, Loadout, MountState,
Ori, Pos, Scale, Stats,
2020-04-18 18:28:19 +00:00
},
event::{EventBus, ServerEvent},
2020-01-25 18:49:47 +00:00
path::Chaser,
2020-04-18 18:28:19 +00:00
state::{DeltaTime, Time},
sync::{Uid, UidAllocator},
terrain::TerrainGrid,
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 = (
Read<'a, UidAllocator>,
Read<'a, Time>,
Read<'a, DeltaTime>,
Write<'a, EventBus<ServerEvent>>,
Entities<'a>,
ReadStorage<'a, Pos>,
ReadStorage<'a, Ori>,
ReadStorage<'a, Scale>,
ReadStorage<'a, Stats>,
ReadStorage<'a, Loadout>,
ReadStorage<'a, CharacterState>,
ReadStorage<'a, Uid>,
ReadExpect<'a, TerrainGrid>,
ReadStorage<'a, Alignment>,
WriteStorage<'a, Agent>,
2019-06-09 14:20:20 +00:00
WriteStorage<'a, Controller>,
ReadStorage<'a, MountState>,
);
#[allow(clippy::or_fun_call)] // TODO: Pending review in #587
2019-08-04 08:21:29 +00:00
fn run(
&mut self,
(
uid_allocator,
time,
dt,
event_bus,
entities,
positions,
orientations,
scales,
stats,
loadouts,
character_states,
uids,
terrain,
alignments,
mut agents,
mut controllers,
mount_states,
): Self::SystemData,
2019-08-04 08:21:29 +00:00
) {
2020-04-18 18:28:19 +00:00
for (
entity,
pos,
ori,
alignment,
loadout,
character_state,
uid,
2020-04-18 18:28:19 +00:00
agent,
controller,
mount_state,
) in (
&entities,
&positions,
&orientations,
alignments.maybe(),
&loadouts,
&character_states,
&uids,
&mut agents,
&mut controllers,
mount_states.maybe(),
)
.join()
{
// Skip mounted entities
if mount_state
.map(|ms| *ms != MountState::Unmounted)
.unwrap_or(false)
{
continue;
}
controller.reset();
let mut inputs = &mut controller.inputs;
// Default to looking in orientation direction
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 = 24.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 = 128.0;
2020-01-25 18:49:47 +00:00
const MIN_ATTACK_DIST: f32 = 3.25;
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-04-23 12:29:22 +00:00
let traversal_tolerance = scale;
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) => {
*bearing += Vec2::new(
thread_rng().gen::<f32>() - 0.5,
thread_rng().gen::<f32>() - 0.5,
) * 0.1
- *bearing * 0.003
- if let Some(patrol_origin) = agent.patrol_origin {
Vec2::<f32>::from(pos.0 - patrol_origin) * 0.0002
} else {
Vec2::zero()
};
// 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())
2020-04-19 11:50:25 +00:00
* 5.0
+ Vec3::unit_z(),
)
.until(|block| block.is_solid())
.cast()
.1
.map(|b| b.is_none())
.unwrap_or(true)
{
0.9
} else {
0.0
};
if bearing.magnitude_squared() > 0.5f32.powf(2.0) {
inputs.move_dir = *bearing * 0.65;
}
2020-04-19 11:50:25 +00:00
// Put away weapon
2020-04-18 18:28:19 +00:00
if thread_rng().gen::<f32>() < 0.005 {
2020-04-19 11:50:25 +00:00
controller.actions.push(ControlAction::Unwield);
}
// Sit
2020-04-18 18:28:19 +00:00
if thread_rng().gen::<f32>() < 0.0035 {
2020-04-19 11:50:25 +00:00
controller.actions.push(ControlAction::Sit);
}
// 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_stats) =
(positions.get(*target), stats.get(*target))
{
let dist_sqrd = pos.0.distance_squared(tgt_pos.0);
// Follow, or return to idle
if dist_sqrd > AVG_FOLLOW_DIST.powf(2.0) {
2020-04-18 18:28:19 +00:00
if let Some(bearing) = chaser.chase(
&*terrain,
pos.0,
tgt_pos.0,
AVG_FOLLOW_DIST,
traversal_tolerance,
) {
inputs.move_dir = Vec2::from(bearing)
.try_normalized()
.unwrap_or(Vec2::zero());
inputs.jump.set_state(bearing.z > 1.0);
}
} 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,
..
} => {
enum Tactic {
2020-04-19 11:50:25 +00:00
Melee,
RangedPowerup,
2020-04-19 11:50:25 +00:00
Staff,
}
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 {
Some(&tool.kind)
} else {
None
}
}) {
Some(ToolKind::Bow(_)) => Tactic::RangedPowerup,
2020-04-19 11:50:25 +00:00
Some(ToolKind::Staff(_)) => Tactic::Staff,
_ => Tactic::Melee,
};
if let (Some(tgt_pos), Some(tgt_stats), tgt_alignment) = (
positions.get(*target),
stats.get(*target),
alignments
.get(*target)
.copied()
.unwrap_or(Alignment::Owned(*target)),
) {
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_stats.is_dead
{
do_idle = true;
break 'activity;
}
}
let dist_sqrd = pos.0.distance_squared(tgt_pos.0);
2020-04-23 12:29:22 +00:00
if dist_sqrd < (MIN_ATTACK_DIST * scale).powf(2.0) {
// Close-range attack
2020-05-04 16:59:32 +00:00
/*inputs.move_dir = Vec2::from(tgt_pos.0 - pos.0)
.try_normalized()
.unwrap_or(Vec2::unit_y())
* 0.7;*/
2020-04-23 12:29:22 +00:00
match tactic {
Tactic::Melee | Tactic::Staff => inputs.primary.set_state(true),
Tactic::RangedPowerup => 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| !block.is_air())
.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 {
if let Tactic::RangedPowerup = tactic {
if *powerup > 2.0 {
inputs.primary.set_state(false);
*powerup = 0.0;
} else {
inputs.primary.set_state(true);
*powerup += dt.0;
}
} else if let Tactic::Staff = tactic {
if !character_state.is_wield() {
inputs.primary.set_state(true);
}
2020-04-19 11:50:25 +00:00
inputs.secondary.set_state(true);
}
}
2020-04-19 11:50:25 +00:00
if dist_sqrd < MAX_CHASE_DIST.powf(2.0) {
*been_close = true;
}
// Long-range chase
2020-04-18 18:28:19 +00:00
if let Some(bearing) = chaser.chase(
&*terrain,
pos.0,
tgt_pos.0,
1.25,
traversal_tolerance,
) {
inputs.move_dir = Vec2::from(bearing)
.try_normalized()
.unwrap_or(Vec2::zero());
inputs.jump.set_state(bearing.z > 1.0);
}
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(Vec2::zero());
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, &stats, alignments.maybe())
.join()
.filter(|(e, e_pos, e_stats, e_alignment)| {
((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_stats.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
.ray(pos.0 + Vec3::unit_z(), e_pos.0 + Vec3::unit_z())
.until(|block| !block.is_air())
.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_stats) = stats.get(entity) {
2020-01-25 18:49:47 +00:00
// Only if the attack was recent
if my_stats.health.last_change.0 < 5.0 {
if let comp::HealthSource::Attack { by }
| comp::HealthSource::Projectile { owner: Some(by) } =
my_stats.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 stats.get(attacker).map_or(false, |a| !a.is_dead) {
if agent.can_speak {
let msg = "npc.speech.villager_under_attack".to_string();
event_bus
.emit_now(ServerEvent::Chat(ChatMsg::npc(*uid, msg)));
}
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.copied() {
2020-01-25 18:49:47 +00:00
if let Some(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-01-25 18:49:47 +00:00
agent.activity = Activity::Follow(owner, Chaser::default());
}
// Attack owner's attacker
if let Some(owner_stats) = stats.get(owner) {
if owner_stats.health.last_change.0 < 5.0 {
if let comp::HealthSource::Attack { by } =
owner_stats.health.last_change.1.cause
{
if !agent.activity.is_attack() {
if let Some(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
}
}
}
}
}
}
}
debug_assert!(inputs.move_dir.map(|e| !e.is_nan()).reduce_and());
debug_assert!(inputs.look_dir.map(|e| !e.is_nan()).reduce_and());
}
}
}