2020-01-23 14:10:49 +00:00
|
|
|
use crate::terrain::TerrainGrid;
|
2020-01-24 21:24:57 +00:00
|
|
|
use crate::{
|
2020-01-25 18:49:47 +00:00
|
|
|
comp::{self, agent::Activity, Agent, Alignment, Controller, MountState, Pos, Stats},
|
|
|
|
path::Chaser,
|
2020-01-25 02:15:15 +00:00
|
|
|
state::Time,
|
2020-01-25 12:27:36 +00:00
|
|
|
sync::UidAllocator,
|
2020-01-24 21:24:57 +00:00
|
|
|
};
|
|
|
|
use rand::{seq::SliceRandom, thread_rng, Rng};
|
|
|
|
use specs::{
|
|
|
|
saveload::{Marker, MarkerAllocator},
|
|
|
|
Entities, Join, Read, ReadExpect, ReadStorage, System, WriteStorage,
|
|
|
|
};
|
2019-04-16 21:06:33 +00:00
|
|
|
use vek::*;
|
|
|
|
|
2019-06-09 19:33:20 +00:00
|
|
|
/// This system will allow NPCs to modify their controller
|
2019-04-16 21:06:33 +00:00
|
|
|
pub struct Sys;
|
|
|
|
impl<'a> System<'a> for Sys {
|
|
|
|
type SystemData = (
|
2020-01-24 21:24:57 +00:00
|
|
|
Read<'a, UidAllocator>,
|
2020-01-25 02:15:15 +00:00
|
|
|
Read<'a, Time>,
|
2019-05-25 21:13:38 +00:00
|
|
|
Entities<'a>,
|
2019-04-16 21:06:33 +00:00
|
|
|
ReadStorage<'a, Pos>,
|
2019-08-02 20:25:33 +00:00
|
|
|
ReadStorage<'a, Stats>,
|
2019-12-11 05:28:45 +00:00
|
|
|
ReadExpect<'a, TerrainGrid>,
|
2020-01-24 21:24:57 +00:00
|
|
|
ReadStorage<'a, Alignment>,
|
2019-08-02 20:25:33 +00:00
|
|
|
WriteStorage<'a, Agent>,
|
2019-06-09 14:20:20 +00:00
|
|
|
WriteStorage<'a, Controller>,
|
2019-09-09 19:11:40 +00:00
|
|
|
ReadStorage<'a, MountState>,
|
2019-04-16 21:06:33 +00:00
|
|
|
);
|
|
|
|
|
2019-08-04 08:21:29 +00:00
|
|
|
fn run(
|
|
|
|
&mut self,
|
2019-12-11 05:28:45 +00:00
|
|
|
(
|
2020-01-24 21:24:57 +00:00
|
|
|
uid_allocator,
|
2020-01-25 02:15:15 +00:00
|
|
|
time,
|
2019-12-11 05:28:45 +00:00
|
|
|
entities,
|
|
|
|
positions,
|
|
|
|
stats,
|
|
|
|
terrain,
|
2020-01-24 21:24:57 +00:00
|
|
|
alignments,
|
2019-12-11 05:28:45 +00:00
|
|
|
mut agents,
|
|
|
|
mut controllers,
|
|
|
|
mount_states,
|
|
|
|
): Self::SystemData,
|
2019-08-04 08:21:29 +00:00
|
|
|
) {
|
2020-01-24 21:24:57 +00:00
|
|
|
for (entity, pos, alignment, agent, controller, mount_state) in (
|
2019-09-09 19:11:40 +00:00
|
|
|
&entities,
|
|
|
|
&positions,
|
2020-01-24 21:24:57 +00:00
|
|
|
alignments.maybe(),
|
2019-09-09 19:11:40 +00:00
|
|
|
&mut agents,
|
|
|
|
&mut controllers,
|
|
|
|
mount_states.maybe(),
|
|
|
|
)
|
|
|
|
.join()
|
2019-05-25 21:13:38 +00:00
|
|
|
{
|
2019-09-09 19:11:40 +00:00
|
|
|
// Skip mounted entities
|
|
|
|
if mount_state
|
|
|
|
.map(|ms| {
|
|
|
|
if let MountState::Unmounted = ms {
|
|
|
|
false
|
|
|
|
} else {
|
|
|
|
true
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.unwrap_or(false)
|
|
|
|
{
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
controller.reset();
|
|
|
|
|
2019-12-01 23:40:05 +00:00
|
|
|
let mut inputs = &mut controller.inputs;
|
2019-10-15 04:06:14 +00:00
|
|
|
|
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-01-25 21:43:02 +00:00
|
|
|
const SIGHT_DIST: f32 = 30.0;
|
2020-01-25 18:49:47 +00:00
|
|
|
const MIN_ATTACK_DIST: f32 = 3.25;
|
|
|
|
|
|
|
|
let mut do_idle = false;
|
2020-01-26 00:06:03 +00:00
|
|
|
let mut choose_target = false;
|
2020-01-25 18:49:47 +00:00
|
|
|
|
2020-01-26 12:47:41 +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.01
|
|
|
|
- if let Some(patrol_origin) = agent.patrol_origin {
|
|
|
|
Vec2::<f32>::from(pos.0 - patrol_origin) * 0.0002
|
|
|
|
} else {
|
|
|
|
Vec2::zero()
|
|
|
|
};
|
|
|
|
|
|
|
|
if bearing.magnitude_squared() > 0.25f32.powf(2.0) {
|
|
|
|
inputs.move_dir = bearing.normalized() * 0.65;
|
|
|
|
}
|
2020-01-25 23:39:38 +00:00
|
|
|
|
2020-01-26 12:47:41 +00:00
|
|
|
// Sometimes try searching for new targets
|
|
|
|
if thread_rng().gen::<f32>() < 0.1 {
|
|
|
|
choose_target = true;
|
|
|
|
}
|
2020-01-25 18:49:47 +00:00
|
|
|
}
|
2020-01-26 12:47:41 +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) {
|
|
|
|
if let Some(bearing) =
|
|
|
|
chaser.chase(&*terrain, pos.0, tgt_pos.0, AVG_FOLLOW_DIST)
|
|
|
|
{
|
|
|
|
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-01-26 12:47:41 +00:00
|
|
|
Activity::Attack(target, chaser, _) => {
|
|
|
|
if let (Some(tgt_pos), _tgt_stats, tgt_alignment) = (
|
|
|
|
positions.get(*target),
|
|
|
|
stats.get(*target),
|
|
|
|
alignments.get(*target),
|
|
|
|
) {
|
|
|
|
// Don't attack aligned entities
|
|
|
|
// TODO: This is a bit of a hack, find a better way to do this
|
|
|
|
if let (Some(alignment), Some(tgt_alignment)) =
|
|
|
|
(alignment, tgt_alignment)
|
|
|
|
{
|
|
|
|
if !tgt_alignment.hostile_towards(*alignment) {
|
|
|
|
do_idle = true;
|
|
|
|
break 'activity;
|
|
|
|
}
|
2020-01-26 00:06:03 +00:00
|
|
|
}
|
|
|
|
|
2020-01-26 12:47:41 +00:00
|
|
|
let dist_sqrd = pos.0.distance_squared(tgt_pos.0);
|
|
|
|
if dist_sqrd < MIN_ATTACK_DIST.powf(2.0) {
|
|
|
|
// Close-range attack
|
|
|
|
inputs.look_dir = tgt_pos.0 - pos.0;
|
|
|
|
inputs.move_dir = Vec2::from(tgt_pos.0 - pos.0)
|
|
|
|
.try_normalized()
|
|
|
|
.unwrap_or(Vec2::unit_y())
|
|
|
|
* 0.01;
|
|
|
|
inputs.primary.set_state(true);
|
|
|
|
} else if dist_sqrd < MAX_CHASE_DIST.powf(2.0) {
|
|
|
|
// Long-range chase
|
|
|
|
if let Some(bearing) =
|
|
|
|
chaser.chase(&*terrain, pos.0, tgt_pos.0, 1.25)
|
|
|
|
{
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if do_idle {
|
2020-01-25 23:39:38 +00:00
|
|
|
agent.activity = Activity::Idle(Vec2::zero());
|
2020-01-25 18:49:47 +00:00
|
|
|
}
|
|
|
|
|
2020-01-26 12:47:41 +00:00
|
|
|
// Choose a new target to attack: only go out of our way to attack targets we are
|
|
|
|
// hostile toward!
|
2020-01-26 00:06:03 +00:00
|
|
|
if choose_target {
|
2020-01-26 12:47:41 +00:00
|
|
|
// 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
|
2020-01-26 00:06:03 +00:00
|
|
|
let entities = (&entities, &positions, &stats, alignments.maybe())
|
|
|
|
.join()
|
|
|
|
.filter(|(e, e_pos, e_stats, e_alignment)| {
|
2020-01-26 12:47:41 +00:00
|
|
|
(e_pos.0 - pos.0).magnitude_squared() < SIGHT_DIST.powf(2.0)
|
2020-01-26 00:06:03 +00:00
|
|
|
&& *e != entity
|
|
|
|
&& !e_stats.is_dead
|
|
|
|
&& alignment
|
|
|
|
.and_then(|a| e_alignment.map(|b| a.hostile_towards(*b)))
|
|
|
|
.unwrap_or(false)
|
|
|
|
})
|
|
|
|
.map(|(e, _, _, _)| e)
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
|
|
|
if let Some(target) = (&entities).choose(&mut thread_rng()).cloned() {
|
|
|
|
agent.activity = Activity::Attack(target, Chaser::default(), time.0);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-01-25 18:49:47 +00:00
|
|
|
// --- Activity overrides (in reverse order of priority: most important goes last!) ---
|
|
|
|
|
|
|
|
// Attack a target that's attacking us
|
|
|
|
if let Some(stats) = stats.get(entity) {
|
|
|
|
// Only if the attack was recent
|
|
|
|
if stats.health.last_change.0 < 5.0 {
|
|
|
|
if let comp::HealthSource::Attack { by } = 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(attacker, Chaser::default(), time.0);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Follow owner if we're too far, or if they're under attack
|
|
|
|
if let Some(owner) = agent.owner {
|
|
|
|
if let Some(owner_pos) = positions.get(owner) {
|
2020-01-26 12:47:41 +00:00
|
|
|
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(attacker, Chaser::default(), time.0);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-12-01 23:40:05 +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());
|
2019-04-16 21:06:33 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|