use crate::terrain::TerrainGrid; use crate::{ comp::{self, agent::Activity, Agent, Alignment, Controller, MountState, Pos, Stats}, path::Chaser, state::Time, sync::UidAllocator, }; use rand::{seq::SliceRandom, thread_rng, Rng}; use specs::{ saveload::{Marker, MarkerAllocator}, Entities, Join, Read, ReadExpect, ReadStorage, System, WriteStorage, }; use vek::*; /// This system will allow NPCs to modify their controller pub struct Sys; impl<'a> System<'a> for Sys { type SystemData = ( Read<'a, UidAllocator>, Read<'a, Time>, Entities<'a>, ReadStorage<'a, Pos>, ReadStorage<'a, Stats>, ReadExpect<'a, TerrainGrid>, ReadStorage<'a, Alignment>, WriteStorage<'a, Agent>, WriteStorage<'a, Controller>, ReadStorage<'a, MountState>, ); fn run( &mut self, ( uid_allocator, time, entities, positions, stats, terrain, alignments, mut agents, mut controllers, mount_states, ): Self::SystemData, ) { for (entity, pos, alignment, agent, controller, mount_state) in ( &entities, &positions, alignments.maybe(), &mut agents, &mut controllers, mount_states.maybe(), ) .join() { // Skip mounted entities if mount_state .map(|ms| { if let MountState::Unmounted = ms { false } else { true } }) .unwrap_or(false) { continue; } controller.reset(); let mut inputs = &mut controller.inputs; const AVG_FOLLOW_DIST: f32 = 6.0; const MAX_FOLLOW_DIST: f32 = 12.0; const MAX_CHASE_DIST: f32 = 24.0; const SIGHT_DIST: f32 = 20.0; const MIN_ATTACK_DIST: f32 = 3.25; const PATROL_DIST: f32 = 32.0; let mut do_idle = false; match &mut agent.activity { Activity::Idle(wander_pos, chaser) => { if let Some(patrol_origin) = agent.patrol_origin { if thread_rng().gen::() < 0.002 { *wander_pos = if thread_rng().gen::() < 0.5 { Some(patrol_origin.map(|e| { e + thread_rng().gen_range(-1.0, 1.0) * PATROL_DIST })) } else { None }; } if let Some(wander_pos) = wander_pos { if let Some(bearing) = chaser.chase(&*terrain, pos.0, *wander_pos, 2.0) { inputs.move_dir = Vec2::from(bearing).try_normalized().unwrap_or(Vec2::zero()); inputs.jump.set_state(bearing.z > 1.0); } else { *wander_pos = None; } } } // Sometimes try searching for new targets if thread_rng().gen::() < 0.025 { // Search for new targets let entities = (&entities, &positions, &stats, alignments.maybe()) .join() .filter(|(e, e_pos, e_stats, e_alignment)| { (e_pos.0 - pos.0).magnitude() < SIGHT_DIST && *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::>(); if let Some(target) = (&entities).choose(&mut thread_rng()).cloned() { agent.activity = Activity::Attack(target, Chaser::default(), time.0); } } } Activity::Follow(target, chaser) => { if let (Some(tgt_pos), _tgt_stats) = (positions.get(*target), stats.get(*target)) { let dist = pos.0.distance(tgt_pos.0); // Follow, or return to idle if dist > AVG_FOLLOW_DIST { 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; } } else { do_idle = true; } } Activity::Attack(target, chaser, _) => { if let (Some(tgt_pos), _tgt_stats) = (positions.get(*target), stats.get(*target)) { let dist = pos.0.distance(tgt_pos.0); if dist < MIN_ATTACK_DIST { // 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::zero()) * 0.01; inputs.primary.set_state(true); } else if dist < MAX_CHASE_DIST { // 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; } } else { do_idle = true; } } } if do_idle { agent.activity = Activity::Idle(None, Chaser::default()); } // --- 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) { let dist = pos.0.distance(owner_pos.0); if dist > MAX_FOLLOW_DIST && !agent.activity.is_follow() { 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); } } } } } } } /* const PET_DIST: f32 = 6.0; const MAX_PET_DIST: f32 = 16.0; const PATROL_DIST: f32 = 32.0; const SIGHT_DIST: f32 = 24.0; const MIN_ATTACK_DIST: f32 = 3.25; const CHASE_TIME_MIN: f64 = 4.0; let mut chase_tgt = None; let mut choose_target = false; let mut new_target = None; if let Some((target, aggro_time)) = agent.target { // Chase / attack target if let (Some(tgt_pos), stats) = (positions.get(target), stats.get(target)) { if stats.map(|s| s.is_dead).unwrap_or(false) { // Don't target dead entities choose_target = true; } else if pos.0.distance(tgt_pos.0) < SIGHT_DIST || (time.0 - aggro_time) < CHASE_TIME_MIN { chase_tgt = Some((tgt_pos.0, 1.5, true)) } else { // Lose sight of enemies choose_target = true; } } else { choose_target = true; } } else { choose_target = thread_rng().gen::() < 0.05; } // Return to owner if let Some(owner) = agent.owner { if let Some(tgt_pos) = positions.get(owner) { let dist = pos.0.distance(tgt_pos.0); if dist > MAX_PET_DIST || (dist > PET_DIST && agent.target.is_none()) { // Follow owner chase_tgt = Some((tgt_pos.0, 6.0, false)); } else if agent.target.is_none() { choose_target = thread_rng().gen::() < 0.02; } } else { agent.owner = None; } } else if let Some(patrol_origin) = agent.patrol_origin { if pos.0.distance(patrol_origin) > PATROL_DIST { // Return to patrol origin chase_tgt = Some((patrol_origin, 64.0, false)); } } // Attack a target that's attacking us if let Some(stats) = stats.get(entity) { match stats.health.last_change.1.cause { comp::HealthSource::Attack { by } => { if agent.target.is_none() { new_target = uid_allocator.retrieve_entity_internal(by.id()); } else if thread_rng().gen::() < 0.005 { new_target = uid_allocator.retrieve_entity_internal(by.id()); } } _ => {} } } // Choose a new target if choose_target { // Search for new targets let entities = (&entities, &positions, &stats, alignments.maybe()) .join() .filter(|(e, e_pos, e_stats, e_alignment)| { (e_pos.0 - pos.0).magnitude() < SIGHT_DIST && *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::>(); new_target = (&entities).choose(&mut thread_rng()).cloned(); } // Update target when attack begins match agent.target { Some((tgt, time)) if Some(tgt) == new_target => {}, _ => agent.target = new_target.map(|tgt| (tgt, time.0)) } // Chase target if let Some((tgt_pos, min_dist, aggressive)) = chase_tgt { if let Some(bearing) = agent.chaser.chase(&*terrain, pos.0, tgt_pos, min_dist) { inputs.move_dir = Vec2::from(bearing).try_normalized().unwrap_or(Vec2::zero()); inputs.jump.set_state(bearing.z > 1.0); } if aggressive && pos.0.distance(tgt_pos) < MIN_ATTACK_DIST { inputs.look_dir = tgt_pos - pos.0; inputs.move_dir = Vec2::from(tgt_pos - pos.0) .try_normalized() .unwrap_or(Vec2::zero()) * 0.01; inputs.primary.set_state(true); } // We're not wandering agent.wander_pos = None; } else { if let Some(wander_pos) = agent.wander_pos { if pos.0.distance(wander_pos) < 4.0 { agent.wander_pos = None; } else { if let Some(bearing) = agent.chaser.chase(&*terrain, pos.0, wander_pos, 3.0) { inputs.move_dir = Vec2::from(bearing).try_normalized().unwrap_or(Vec2::zero()) * 0.5; inputs.jump.set_state(bearing.z > 1.0); } } } // Choose new wander position /* if agent.wander_pos.is_none() || thread_rng().gen::() < 0.005 { agent.wander_pos = if thread_rng().gen::() < 0.5 { let max_dist = if agent.owner.is_some() { PET_DIST } else { PATROL_DIST }; Some( agent .patrol_origin .unwrap_or(pos.0) .map(|e| e + (thread_rng().gen::() - 0.5) * max_dist), ) } else { None }; } */ } */ debug_assert!(inputs.move_dir.map(|e| !e.is_nan()).reduce_and()); debug_assert!(inputs.look_dir.map(|e| !e.is_nan()).reduce_and()); } } }