veloren/server/src/sys/agent.rs

1759 lines
80 KiB
Rust
Raw Normal View History

use common::{
2020-04-18 18:28:19 +00:00
comp::{
self,
2021-02-07 07:22:06 +00:00
agent::{AgentEvent, Tactic, Target, DEFAULT_INTERACTION_TIME},
group,
inventory::slot::EquipSlot,
2021-02-07 07:22:06 +00:00
invite::InviteResponse,
2020-11-06 17:39:49 +00:00
item::{
tool::{ToolKind, UniqueKind},
ItemKind,
},
skills::{AxeSkill, BowSkill, HammerSkill, Skill, StaffSkill, SwordSkill},
Agent, Alignment, Body, CharacterState, ControlAction, ControlEvent, Controller, Energy,
Health, Inventory, LightEmitter, MountState, Ori, PhysicsState, Pos, Scale, Stats,
UnresolvedChatMsg, Vel,
2020-04-18 18:28:19 +00:00
},
2021-02-07 07:22:06 +00:00
event::{Emitter, EventBus, ServerEvent},
path::TraversalConfig,
resources::{DeltaTime, TimeOfDay},
terrain::{Block, TerrainGrid},
2020-10-07 02:23:20 +00:00
time::DayPeriod,
uid::{Uid, UidAllocator},
util::Dir,
vol::ReadVol,
vsystem::{Origin, Phase, VJob, VSystem},
};
use rand::{thread_rng, Rng};
use rayon::iter::ParallelIterator;
use specs::{
saveload::{Marker, MarkerAllocator},
2021-02-07 07:22:06 +00:00
shred::ResourceId,
Entities, Entity as EcsEntity, Join, ParJoin, Read, ReadExpect, ReadStorage, SystemData, World,
Write, WriteStorage,
};
use std::f32::consts::PI;
use vek::*;
2021-02-07 07:22:06 +00:00
struct AgentData<'a> {
entity: &'a EcsEntity,
uid: &'a Uid,
pos: &'a Pos,
vel: &'a Vel,
ori: &'a Ori,
energy: &'a Energy,
body: Option<&'a Body>,
inventory: &'a Inventory,
stats: &'a Stats,
physics_state: &'a PhysicsState,
alignment: Option<&'a Alignment>,
traversal_config: TraversalConfig,
scale: f32,
flees: bool,
damage: f32,
light_emitter: Option<&'a LightEmitter>,
glider_equipped: bool,
is_gliding: bool,
}
#[derive(SystemData)]
pub struct ReadData<'a> {
entities: Entities<'a>,
uid_allocator: Read<'a, UidAllocator>,
dt: Read<'a, DeltaTime>,
group_manager: Read<'a, group::GroupManager>,
energies: ReadStorage<'a, Energy>,
positions: ReadStorage<'a, Pos>,
velocities: ReadStorage<'a, Vel>,
orientations: ReadStorage<'a, Ori>,
scales: ReadStorage<'a, Scale>,
healths: ReadStorage<'a, Health>,
inventories: ReadStorage<'a, Inventory>,
stats: ReadStorage<'a, Stats>,
physics_states: ReadStorage<'a, PhysicsState>,
char_states: ReadStorage<'a, CharacterState>,
uids: ReadStorage<'a, Uid>,
groups: ReadStorage<'a, group::Group>,
terrain: ReadExpect<'a, TerrainGrid>,
alignments: ReadStorage<'a, Alignment>,
bodies: ReadStorage<'a, Body>,
mount_states: ReadStorage<'a, MountState>,
//ReadStorage<'a, Invite>,
time_of_day: Read<'a, TimeOfDay>,
light_emitter: ReadStorage<'a, LightEmitter>,
}
// This is 3.1 to last longer than the last damage timer (3.0 seconds)
2021-03-03 03:55:28 +00:00
const DAMAGE_MEMORY_DURATION: f64 = 0.1;
const FLEE_DURATION: f32 = 3.0;
2021-02-07 07:22:06 +00:00
const MAX_FOLLOW_DIST: f32 = 12.0;
const MAX_CHASE_DIST: f32 = 250.0;
2021-02-07 07:22:06 +00:00
const MAX_FLEE_DIST: f32 = 20.0;
const LISTEN_DIST: f32 = 16.0;
const SEARCH_DIST: f32 = 48.0;
const SIGHT_DIST: f32 = 80.0;
const SNEAK_COEFFICIENT: f32 = 0.25;
const AVG_FOLLOW_DIST: f32 = 6.0;
2019-06-09 19:33:20 +00:00
/// This system will allow NPCs to modify their controller
#[derive(Default)]
pub struct Sys;
impl<'a> VSystem<'a> for Sys {
#[allow(clippy::type_complexity)]
type SystemData = (
2021-02-07 07:22:06 +00:00
ReadData<'a>,
Write<'a, EventBus<ServerEvent>>,
WriteStorage<'a, Agent>,
2019-06-09 14:20:20 +00:00
WriteStorage<'a, Controller>,
);
const NAME: &'static str = "agent";
const ORIGIN: Origin = Origin::Server;
const PHASE: Phase = Phase::Create;
#[allow(clippy::or_fun_call)] // TODO: Pending review in #587
2019-08-04 08:21:29 +00:00
fn run(
_job: &mut VJob<Self>,
(read_data, event_bus, mut agents, mut controllers): Self::SystemData,
2019-08-04 08:21:29 +00:00
) {
(
2021-02-07 07:22:06 +00:00
&read_data.entities,
(&read_data.energies, &read_data.healths),
&read_data.positions,
&read_data.velocities,
&read_data.orientations,
read_data.bodies.maybe(),
&read_data.inventories,
&read_data.stats,
&read_data.physics_states,
&read_data.uids,
&mut agents,
&mut controllers,
2021-02-07 07:22:06 +00:00
read_data.light_emitter.maybe(),
read_data.groups.maybe(),
read_data.mount_states.maybe(),
)
.par_join()
2021-02-07 07:22:06 +00:00
.filter(|(_, _, _, _, _, _, _, _, _, _, _, _, _, _, mount_state)| {
// Skip mounted entities
2021-02-07 07:22:06 +00:00
mount_state
.map(|ms| *ms == MountState::Unmounted)
.unwrap_or(true)
})
2021-02-07 07:22:06 +00:00
.for_each(
|(
entity,
(energy, health),
pos,
vel,
ori,
body,
inventory,
stats,
physics_state,
uid,
agent,
controller,
light_emitter,
groups,
_,
)| {
//// 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!(
&read_data.alignments.get(entity),
&Some(Alignment::Owned(_))
) {
groups
.and_then(|g| read_data.group_manager.group_info(*g))
.and_then(|info| read_data.uids.get(info.leader))
.copied()
.map(Alignment::Owned)
.or(read_data.alignments.get(entity).copied())
} else {
read_data.alignments.get(entity).copied()
};
2021-02-07 07:22:06 +00:00
controller.reset();
let mut event_emitter = event_bus.emitter();
2021-02-07 07:22:06 +00:00
// Default to looking in orientation direction (can be overridden below)
controller.inputs.look_dir = ori.look_dir();
2021-02-07 07:22:06 +00:00
let scale = read_data.scales.get(entity).map(|s| s.0).unwrap_or(1.0);
2020-01-25 18:49:47 +00:00
2021-02-07 07:22:06 +00:00
let glider_equipped = inventory
.equipped(EquipSlot::Glider)
.as_ref()
.map_or(false, |item| {
matches!(item.kind(), comp::item::ItemKind::Glider(_))
});
let is_gliding = matches!(
read_data.char_states.get(entity),
Some(CharacterState::GlideWield) | Some(CharacterState::Glide)
) && !physics_state.on_ground;
2021-02-07 07:22:06 +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).
let node_tolerance = scale * 1.5;
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),
can_fly: body.map(|b| b.can_fly()).unwrap_or(false),
};
2021-02-07 07:22:06 +00:00
let flees = alignment
.map(|a| !matches!(a, Alignment::Enemy | Alignment::Owned(_)))
.unwrap_or(true);
2021-02-07 07:22:06 +00:00
let damage = health.current() as f32 / health.maximum() as f32;
2021-02-07 07:22:06 +00:00
// Package all this agent's data into a convenient struct
let data = AgentData {
entity: &entity,
uid,
pos,
vel,
ori,
energy,
body,
inventory,
stats,
physics_state,
alignment: alignment.as_ref(),
traversal_config,
scale,
flees,
damage,
light_emitter,
glider_equipped,
is_gliding,
};
2021-01-24 04:00:57 +00:00
2021-02-07 07:22:06 +00:00
///////////////////////////////////////////////////////////
// Behavior tree
///////////////////////////////////////////////////////////
2021-02-07 07:22:06 +00:00
// If falling fast and can glide, save yourself!
if data.fall_glide() {
//toggle glider when vertical velocity is above some threshold (here ~
// glider fall vertical speed)
if vel.0.z < -26.0 {
controller.actions.push(ControlAction::GlideWield);
2021-03-03 03:55:28 +00:00
if let Some(Target { target, hostile: _ }) = agent.target {
if let Some(tgt_pos) = read_data.positions.get(target) {
controller.inputs.move_dir = (pos.0 - tgt_pos.0)
.xy()
.try_normalized()
.unwrap_or_else(Vec2::zero);
}
}
2021-02-07 07:22:06 +00:00
}
2021-03-03 03:55:28 +00:00
} else if let Some(Target { target, hostile }) = agent.target {
if let Some(tgt_health) = read_data.healths.get(target) {
// If the target is hostile (either based on alignment or if
// the target just attacked
if !tgt_health.is_dead {
if hostile {
data.hostile_tree(
agent,
controller,
&read_data,
&mut event_emitter,
);
// Target is something worth following methinks
} else if let Some(Alignment::Owned(_)) = data.alignment {
if let Some(tgt_pos) = read_data.positions.get(target) {
let dist_sqrd = pos.0.distance_squared(tgt_pos.0);
// If really far away drop everything and follow
if dist_sqrd > (2.0 * MAX_FOLLOW_DIST).powi(2) {
agent.bearing = Vec2::zero();
data.follow(
agent,
controller,
&read_data.terrain,
tgt_pos,
);
// Attack target's attacker
} else if tgt_health.last_change.0 < 5.0
&& tgt_health.last_change.1.amount < 0
2021-02-07 07:22:06 +00:00
{
2021-03-03 03:55:28 +00:00
if let comp::HealthSource::Damage {
by: Some(by), ..
} = tgt_health.last_change.1.cause
{
if let Some(attacker) = read_data
.uid_allocator
.retrieve_entity_internal(by.id())
2021-02-07 07:22:06 +00:00
{
agent.target = Some(Target {
2021-03-03 03:55:28 +00:00
target: attacker,
hostile: true,
2021-02-07 07:22:06 +00:00
});
2021-03-03 03:55:28 +00:00
if let (Some(tgt_pos), Some(tgt_health)) = (
read_data.positions.get(attacker),
read_data.healths.get(attacker),
) {
if tgt_health.is_dead
|| dist_sqrd > MAX_CHASE_DIST.powi(2)
{
agent.target = Some(Target {
target,
hostile: false,
});
data.idle(
agent, controller, &read_data,
);
} else {
data.attack(
agent,
controller,
&read_data.terrain,
tgt_pos,
read_data.bodies.get(attacker),
&read_data.dt,
);
}
}
}
}
2021-03-03 03:55:28 +00:00
// Follow owner if too far away and not
// fighting
} else if dist_sqrd > MAX_FOLLOW_DIST.powi(2) {
data.follow(
agent,
controller,
&read_data.terrain,
tgt_pos,
);
// Otherwise just idle
} else {
data.idle_tree(
agent,
controller,
&read_data,
&mut event_emitter,
);
}
}
} else {
2021-02-07 07:22:06 +00:00
data.idle_tree(
agent,
controller,
&read_data,
&mut event_emitter,
);
}
2021-02-07 07:22:06 +00:00
} else {
2021-03-03 03:55:28 +00:00
agent.target = None;
2021-02-07 07:22:06 +00:00
data.idle_tree(agent, controller, &read_data, &mut event_emitter);
}
} else {
2021-03-03 03:55:28 +00:00
agent.target = None;
2021-02-07 07:22:06 +00:00
data.idle_tree(agent, controller, &read_data, &mut event_emitter);
}
} else {
// Target an entity that's attacking us if the attack was recent
if health.last_change.0 < DAMAGE_MEMORY_DURATION {
if let comp::HealthSource::Damage { by: Some(by), .. } =
health.last_change.1.cause
{
if let Some(attacker) =
read_data.uid_allocator.retrieve_entity_internal(by.id())
{
if let Some(tgt_pos) = read_data.positions.get(attacker) {
2021-03-03 03:55:28 +00:00
// If the target is dead, remove the target and idle.
2021-02-07 07:22:06 +00:00
if read_data
.healths
.get(attacker)
2021-03-03 03:55:28 +00:00
.map_or(true, |a| a.is_dead)
{
2021-03-03 03:55:28 +00:00
agent.target = None;
data.idle_tree(
agent,
controller,
&read_data,
&mut event_emitter,
);
} else {
2021-02-07 07:22:06 +00:00
agent.target = Some(Target {
target: attacker,
hostile: true,
});
data.attack(
agent,
controller,
&read_data.terrain,
tgt_pos,
read_data.bodies.get(attacker),
&read_data.dt,
);
}
} else {
2021-02-07 07:22:06 +00:00
agent.target = None;
data.idle_tree(
agent,
controller,
&read_data,
&mut event_emitter,
);
}
}
} else {
2021-02-07 07:22:06 +00:00
agent.target = None;
data.idle_tree(agent, controller, &read_data, &mut event_emitter);
}
} else {
data.idle_tree(agent, controller, &read_data, &mut event_emitter);
}
}
2021-02-07 07:22:06 +00:00
debug_assert!(controller.inputs.move_dir.map(|e| !e.is_nan()).reduce_and());
debug_assert!(controller.inputs.look_dir.map(|e| !e.is_nan()).reduce_and());
},
);
}
}
2021-02-07 07:22:06 +00:00
impl<'a> AgentData<'a> {
fn fall_glide(&self) -> bool { self.glider_equipped && !self.physics_state.on_ground }
2021-02-07 07:22:06 +00:00
fn idle_tree(
&self,
agent: &mut Agent,
controller: &mut Controller,
read_data: &ReadData,
event_emitter: &mut Emitter<'_, ServerEvent>,
) {
2021-03-03 03:55:28 +00:00
// Set owner if no target
if agent.target.is_none() && thread_rng().gen_bool(0.1) {
if let Some(Alignment::Owned(owner)) = self.alignment {
if let Some(owner) = read_data.uid_allocator.retrieve_entity_internal(owner.id()) {
agent.target = Some(Target {
target: owner,
hostile: false,
});
}
}
}
2021-02-07 07:22:06 +00:00
// Interact if incoming messages
if !agent.inbox.is_empty() {
2021-03-03 03:55:28 +00:00
agent.action_timer = 0.1;
2021-02-07 07:22:06 +00:00
}
if agent.action_timer > 0.0 {
2021-03-03 03:55:28 +00:00
if agent.action_timer < DEFAULT_INTERACTION_TIME {
self.interact(agent, controller, &read_data, event_emitter);
2021-02-07 07:22:06 +00:00
} else {
2021-03-03 03:55:28 +00:00
agent.action_timer = 0.0;
2021-02-07 07:22:06 +00:00
agent.target = None;
2021-03-03 03:55:28 +00:00
controller.actions.push(ControlAction::Stand);
2021-02-07 07:22:06 +00:00
self.idle(agent, controller, &read_data);
}
} else if thread_rng().gen::<f32>() < 0.1 {
self.choose_target(agent, controller, &read_data);
} else {
self.idle(agent, controller, &read_data);
}
}
2020-07-29 19:58:14 +00:00
2021-02-07 07:22:06 +00:00
fn hostile_tree(
&self,
agent: &mut Agent,
controller: &mut Controller,
read_data: &ReadData,
event_emitter: &mut Emitter<'_, ServerEvent>,
) {
if let Some(Target { target, .. }) = agent.target {
if let (Some(tgt_pos), Some(tgt_health)) = (
read_data.positions.get(target),
read_data.healths.get(target),
) {
let dist_sqrd = self.pos.0.distance_squared(tgt_pos.0);
// Should the agent flee?
if 1.0 - agent.psyche.aggro > self.damage && self.flees {
if agent.action_timer == 0.0 && agent.can_speak {
let msg = "npc.speech.villager_under_attack".to_string();
event_emitter
.emit(ServerEvent::Chat(UnresolvedChatMsg::npc(*self.uid, msg)));
agent.action_timer = 0.01;
} else if agent.action_timer < FLEE_DURATION || dist_sqrd < MAX_FLEE_DIST {
self.flee(
agent,
controller,
&read_data.terrain,
tgt_pos,
&read_data.dt,
);
} else {
agent.action_timer = 0.0;
agent.target = None;
self.idle(agent, controller, &read_data);
}
2021-02-07 07:22:06 +00:00
// If not fleeing, attack the hostile
// entity!
} else {
// If the hostile entity is dead, return to idle
if tgt_health.is_dead {
agent.target = None;
if agent.can_speak {
let msg = "I have destroyed my enemy!".to_string();
event_emitter
.emit(ServerEvent::Chat(UnresolvedChatMsg::npc(*self.uid, msg)));
}
} else if dist_sqrd < SIGHT_DIST.powi(2) {
self.attack(
agent,
controller,
&read_data.terrain,
tgt_pos,
read_data.bodies.get(target),
&read_data.dt,
);
} else {
agent.target = None;
self.idle(agent, controller, &read_data);
}
}
}
}
}
2021-02-07 07:22:06 +00:00
fn idle(&self, agent: &mut Agent, controller: &mut Controller, read_data: &ReadData) {
// Light lanterns at night
// TODO Add a method to turn on NPC lanterns underground
let lantern_equipped = self
.inventory
.equipped(EquipSlot::Lantern)
.as_ref()
.map_or(false, |item| {
matches!(item.kind(), comp::item::ItemKind::Lantern(_))
});
let lantern_turned_on = self.light_emitter.is_some();
let day_period = DayPeriod::from(read_data.time_of_day.0);
// Only emit event for agents that have a lantern equipped
2021-03-03 03:55:28 +00:00
if lantern_equipped && thread_rng().gen_bool(0.001) {
2021-02-07 07:22:06 +00:00
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
2021-03-03 03:55:28 +00:00
controller.events.push(ControlEvent::EnableLantern)
2021-02-07 07:22:06 +00:00
} 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
2021-03-03 03:55:28 +00:00
controller.events.push(ControlEvent::DisableLantern)
2021-02-07 07:22:06 +00:00
}
};
2021-02-07 07:22:06 +00:00
agent.action_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
controller.inputs.fly.set_state(
self.traversal_config.can_fly
&& !read_data
.terrain
.ray(self.pos.0, self.pos.0 + (Vec3::unit_z() * 3.0))
.until(Block::is_solid)
.cast()
.1
.map_or(true, |b| b.is_some()),
);
if let Some((bearing, speed)) = agent.chaser.chase(
&*read_data.terrain,
self.pos.0,
self.vel.0,
*travel_to,
TraversalConfig {
min_tgt_dist: 1.25,
..self.traversal_config
},
) {
controller.inputs.move_dir =
bearing.xy().try_normalized().unwrap_or_else(Vec2::zero)
* speed.min(agent.rtsim_controller.speed_factor);
controller.inputs.jump.set_state(
bearing.z > 1.5
|| self.traversal_config.can_fly && self.traversal_config.on_ground,
);
controller.inputs.climb = Some(comp::Climb::Up);
//.filter(|_| bearing.z > 0.1 || self.physics_state.in_liquid.is_some());
2021-02-07 07:22:06 +00:00
controller.inputs.move_z = bearing.z
+ if self.traversal_config.can_fly {
if read_data
.terrain
.ray(
self.pos.0 + Vec3::unit_z(),
self.pos.0
+ bearing.try_normalized().unwrap_or_else(Vec3::unit_y) * 60.0
+ Vec3::unit_z(),
)
.until(Block::is_solid)
.cast()
.1
.map_or(true, |b| b.is_some())
{
1.0 //fly up when approaching obstacles
} else {
-0.1
} //flying things should slowly come down from the stratosphere
} else {
0.05 //normal land traveller offset
};
2021-02-07 07:22:06 +00:00
// Put away weapon
2021-03-03 03:55:28 +00:00
if thread_rng().gen_bool(0.1)
2021-02-07 07:22:06 +00:00
&& matches!(
read_data.char_states.get(*self.entity),
Some(CharacterState::Wielding)
)
{
controller.actions.push(ControlAction::Unwield);
}
}
} else {
agent.bearing += Vec2::new(
thread_rng().gen::<f32>() - 0.5,
thread_rng().gen::<f32>() - 0.5,
) * 0.1
- agent.bearing * 0.003
- agent.patrol_origin.map_or(Vec2::zero(), |patrol_origin| {
(self.pos.0 - patrol_origin).xy() * 0.0002
});
2021-02-07 07:22:06 +00:00
// Stop if we're too close to a wall
agent.bearing *= 0.1
+ if read_data
.terrain
.ray(
self.pos.0 + Vec3::unit_z(),
self.pos.0
+ Vec3::from(agent.bearing)
.try_normalized()
.unwrap_or_else(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
};
2021-02-07 07:22:06 +00:00
if agent.bearing.magnitude_squared() > 0.5f32.powi(2) {
controller.inputs.move_dir = agent.bearing * 0.65;
}
2021-02-07 07:22:06 +00:00
// Put away weapon
2021-03-03 03:55:28 +00:00
if thread_rng().gen_bool(0.1)
2021-02-07 07:22:06 +00:00
&& matches!(
read_data.char_states.get(*self.entity),
Some(CharacterState::Wielding)
)
{
controller.actions.push(ControlAction::Unwield);
}
2021-02-07 07:22:06 +00:00
// Sit
if thread_rng().gen::<f32>() < 0.0035 {
controller.actions.push(ControlAction::Sit);
}
}
}
2021-02-07 07:22:06 +00:00
fn interact(
&self,
agent: &mut Agent,
controller: &mut Controller,
read_data: &ReadData,
event_emitter: &mut Emitter<'_, ServerEvent>,
) {
// TODO: Process group invites
// TODO: Add Group AgentEvent
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::InviteResponse(InviteResponse::Accept));
} else {
controller
.events
.push(ControlEvent::InviteResponse(InviteResponse::Decline));
}
agent.action_timer += read_data.dt.0;
2021-03-03 03:55:28 +00:00
if agent.can_speak {
if let Some(AgentEvent::Talk(by)) = agent.inbox.pop_back() {
if let Some(target) = read_data.uid_allocator.retrieve_entity_internal(by.id()) {
agent.target = Some(Target {
target,
hostile: false,
});
if let Some(tgt_pos) = read_data.positions.get(target) {
let eye_offset = self.body.map_or(0.0, |b| b.eye_height());
let tgt_eye_offset =
read_data.bodies.get(target).map_or(0.0, |b| b.eye_height());
if let Some(dir) = Dir::from_unnormalized(
Vec3::new(tgt_pos.0.x, tgt_pos.0.y, tgt_pos.0.z + tgt_eye_offset)
- Vec3::new(self.pos.0.x, self.pos.0.y, self.pos.0.z + eye_offset),
) {
controller.inputs.look_dir = dir;
}
controller.actions.push(ControlAction::Stand);
controller.actions.push(ControlAction::Talk);
if let Some((_travel_to, destination_name)) =
&agent.rtsim_controller.travel_to
{
let msg =
format!("I'm heading to {}! Want to come along?", destination_name);
event_emitter
.emit(ServerEvent::Chat(UnresolvedChatMsg::npc(*self.uid, msg)));
} else {
let msg = "npc.speech.villager".to_string();
event_emitter
.emit(ServerEvent::Chat(UnresolvedChatMsg::npc(*self.uid, msg)));
}
}
}
} else if let Some(Target { target, .. }) = &agent.target {
if let Some(tgt_pos) = read_data.positions.get(*target) {
2021-02-07 07:22:06 +00:00
let eye_offset = self.body.map_or(0.0, |b| b.eye_height());
2021-03-03 03:55:28 +00:00
let tgt_eye_offset = read_data
.bodies
.get(*target)
.map_or(0.0, |b| b.eye_height());
2021-02-07 07:22:06 +00:00
if let Some(dir) = Dir::from_unnormalized(
Vec3::new(tgt_pos.0.x, tgt_pos.0.y, tgt_pos.0.z + tgt_eye_offset)
- Vec3::new(self.pos.0.x, self.pos.0.y, self.pos.0.z + eye_offset),
) {
controller.inputs.look_dir = dir;
}
}
2021-03-03 03:55:28 +00:00
} else {
agent.action_timer = 0.0;
2021-02-07 07:22:06 +00:00
}
} else {
2021-03-03 03:55:28 +00:00
agent.inbox.clear();
2021-02-07 07:22:06 +00:00
}
}
2021-02-07 07:22:06 +00:00
fn flee(
&self,
agent: &mut Agent,
controller: &mut Controller,
terrain: &TerrainGrid,
tgt_pos: &Pos,
dt: &DeltaTime,
) {
if let Some(body) = self.body {
if body.can_strafe() && !self.is_gliding {
controller.actions.push(ControlAction::Unwield);
}
}
if let Some((bearing, speed)) = agent.chaser.chase(
&*terrain,
self.pos.0,
self.vel.0,
// Away from the target (ironically)
self.pos.0
+ (self.pos.0 - tgt_pos.0)
.try_normalized()
.unwrap_or_else(Vec3::unit_y)
* 50.0,
TraversalConfig {
min_tgt_dist: 1.25,
..self.traversal_config
},
) {
controller.inputs.move_dir =
bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
controller.inputs.jump.set_state(bearing.z > 1.5);
controller.inputs.move_z = bearing.z;
}
agent.action_timer += dt.0;
}
fn choose_target(&self, agent: &mut Agent, controller: &mut Controller, read_data: &ReadData) {
agent.action_timer = 0.0;
// 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 target = (&read_data.entities, &read_data.positions, &read_data.healths, read_data.alignments.maybe(), read_data.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;
}
2021-02-07 07:22:06 +00:00
((e_pos.0.distance_squared(self.pos.0) < search_dist.powi(2) &&
// Within our view
(e_pos.0 - self.pos.0).try_normalized().map(|v| v.dot(*controller.inputs.look_dir) > 0.15).unwrap_or(true))
// Within listen distance
|| e_pos.0.distance_squared(self.pos.0) < listen_dist.powi(2)) // TODO implement proper sound system for agents
&& e != self.entity
&& !e_health.is_dead
&& self.alignment.and_then(|a| e_alignment.map(|b| a.hostile_towards(*b))).unwrap_or(false)
})
// Can we even see them?
.filter(|(_, e_pos, _, _, _)| read_data.terrain
.ray(self.pos.0 + Vec3::unit_z(), e_pos.0 + Vec3::unit_z())
.until(Block::is_opaque)
.cast()
.0 >= e_pos.0.distance(self.pos.0))
.min_by_key(|(_, e_pos, _, _, _)| (e_pos.0.distance_squared(self.pos.0) * 100.0) as i32) // TODO choose target by more than just distance
.map(|(e, _, _, _, _)| e);
if let Some(target) = target {
agent.target = Some(Target {
target,
hostile: true,
})
} else {
agent.target = None;
}
}
2021-02-07 07:22:06 +00:00
fn attack(
&self,
agent: &mut Agent,
controller: &mut Controller,
terrain: &TerrainGrid,
tgt_pos: &Pos,
tgt_body: Option<&Body>,
dt: &DeltaTime,
) {
let min_attack_dist = self.body.map_or(3.0, |b| b.radius() * self.scale + 2.0);
let tactic = match self
.inventory
.equipped(EquipSlot::Mainhand)
.as_ref()
.and_then(|item| {
if let ItemKind::Tool(tool) = &item.kind() {
Some(&tool.kind)
} else {
None
}
2021-02-07 07:22:06 +00:00
}) {
2021-03-03 03:55:28 +00:00
Some(ToolKind::Bow) | Some(ToolKind::BowSimple) => Tactic::Bow,
Some(ToolKind::Staff) | Some(ToolKind::StaffSimple) => Tactic::Staff,
2021-02-07 07:22:06 +00:00
Some(ToolKind::Hammer) => Tactic::Hammer,
2021-03-03 03:55:28 +00:00
Some(ToolKind::Sword)
| Some(ToolKind::Spear)
| Some(ToolKind::SwordSimple)
| Some(ToolKind::AxeSimple) => Tactic::Sword,
2021-02-07 07:22:06 +00:00
Some(ToolKind::Axe) => Tactic::Axe,
Some(ToolKind::Unique(UniqueKind::StoneGolemFist)) => Tactic::StoneGolemBoss,
Some(ToolKind::Unique(UniqueKind::QuadMedQuick)) => Tactic::CircleCharge {
radius: 3,
circle_time: 2,
},
Some(ToolKind::Unique(UniqueKind::QuadMedCharge)) => Tactic::CircleCharge {
radius: 15,
circle_time: 1,
},
2021-02-07 07:22:06 +00:00
Some(ToolKind::Unique(UniqueKind::QuadMedJump)) => Tactic::QuadMedJump,
Some(ToolKind::Unique(UniqueKind::QuadMedBasic)) => Tactic::QuadMedBasic,
Some(ToolKind::Unique(UniqueKind::QuadLowRanged)) => Tactic::QuadLowRanged,
Some(ToolKind::Unique(UniqueKind::QuadLowTail)) => Tactic::TailSlap,
Some(ToolKind::Unique(UniqueKind::QuadLowQuick)) => Tactic::QuadLowQuick,
Some(ToolKind::Unique(UniqueKind::QuadLowBasic)) => Tactic::QuadLowBasic,
2021-03-03 03:55:28 +00:00
Some(ToolKind::Unique(UniqueKind::QuadLowBreathe))
| Some(ToolKind::Unique(UniqueKind::QuadLowBeam)) => Tactic::Lavadrake,
2021-02-07 07:22:06 +00:00
Some(ToolKind::Unique(UniqueKind::TheropodBasic)) => Tactic::Theropod,
Some(ToolKind::Unique(UniqueKind::TheropodBird)) => Tactic::Theropod,
Some(ToolKind::Unique(UniqueKind::ObjectTurret)) => Tactic::Turret,
_ => Tactic::Melee,
};
2020-01-25 18:49:47 +00:00
2021-02-07 07:22:06 +00:00
// Wield the weapon as running towards the target
controller.actions.push(ControlAction::Wield);
2020-07-30 19:26:49 +00:00
2021-02-07 07:22:06 +00:00
let eye_offset = self.body.map_or(0.0, |b| b.eye_height());
2021-02-07 07:22:06 +00:00
let tgt_eye_offset = tgt_body.map_or(0.0, |b| b.eye_height()) +
// Special case for jumping attacks to jump at the body
// of the target and not the ground around the target
// For the ranged it is to shoot at the feet and not
// the head to get splash damage
if tactic == Tactic::QuadMedJump {
1.0
} else if matches!(tactic, Tactic::QuadLowRanged) {
-1.0
} else {
0.0
};
2021-02-07 07:22:06 +00:00
// Hacky distance offset for ranged weapons. This is
// intentionally hacky for now before we make ranged
// NPCs lead targets and implement varying aiming
// skill
let distance_offset = match tactic {
Tactic::Bow => {
0.0004 /* Yay magic numbers */ * self.pos.0.distance_squared(tgt_pos.0)
},
Tactic::Staff => {
0.0015 /* Yay magic numbers */ * self.pos.0.distance_squared(tgt_pos.0)
},
Tactic::QuadLowRanged => {
0.03 /* Yay magic numbers */ * self.pos.0.distance_squared(tgt_pos.0)
},
_ => 0.0,
};
// Apply the distance and eye offsets to make the
// look_dir the vector from projectile launch to
// target point
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(self.pos.0.x, self.pos.0.y, self.pos.0.z + eye_offset),
) {
controller.inputs.look_dir = dir;
}
let dist_sqrd = self.pos.0.distance_squared(tgt_pos.0);
// Match on tactic. Each tactic has different controls
// depending on the distance from the agent to the target
match tactic {
Tactic::Melee => {
if dist_sqrd < (min_attack_dist * self.scale).powi(2) {
controller.inputs.primary.set_state(true);
controller.inputs.move_dir = Vec2::zero();
} else if dist_sqrd < MAX_CHASE_DIST.powi(2) {
if let Some((bearing, speed)) = agent.chaser.chase(
&*terrain,
self.pos.0,
self.vel.0,
tgt_pos.0,
TraversalConfig {
min_tgt_dist: 1.25,
..self.traversal_config
},
) {
controller.inputs.move_dir =
bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
controller.inputs.jump.set_state(bearing.z > 1.5);
controller.inputs.move_z = bearing.z;
}
if self.body.map(|b| b.is_humanoid()).unwrap_or(false)
&& dist_sqrd < 16.0f32.powi(2)
&& thread_rng().gen::<f32>() < 0.02
{
controller.inputs.roll.set_state(true);
}
} else {
agent.target = None;
}
},
Tactic::Axe => {
if dist_sqrd < (min_attack_dist * self.scale).powi(2) {
controller.inputs.move_dir = Vec2::zero();
if agent.action_timer > 6.0 {
controller.inputs.secondary.set_state(false);
agent.action_timer = 0.0;
} else if agent.action_timer > 4.0 && self.energy.current() > 10 {
controller.inputs.secondary.set_state(true);
agent.action_timer += dt.0;
} else if self
.stats
.skill_set
.has_skill(Skill::Axe(AxeSkill::UnlockLeap))
&& self.energy.current() > 800
&& thread_rng().gen_bool(0.5)
{
controller.inputs.ability3.set_state(true);
agent.action_timer += dt.0;
} else {
controller.inputs.primary.set_state(true);
agent.action_timer += dt.0;
}
} else if dist_sqrd < MAX_CHASE_DIST.powi(2) {
if let Some((bearing, speed)) = agent.chaser.chase(
&*terrain,
self.pos.0,
self.vel.0,
tgt_pos.0,
TraversalConfig {
min_tgt_dist: 1.25,
..self.traversal_config
},
) {
controller.inputs.move_dir =
bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
controller.inputs.jump.set_state(bearing.z > 1.5);
controller.inputs.move_z = bearing.z;
}
if self.body.map(|b| b.is_humanoid()).unwrap_or(false)
&& dist_sqrd < 16.0f32.powi(2)
&& thread_rng().gen::<f32>() < 0.02
{
controller.inputs.roll.set_state(true);
}
} else {
agent.target = None;
}
},
Tactic::Hammer => {
if dist_sqrd < (min_attack_dist * self.scale).powi(2) {
controller.inputs.move_dir = Vec2::zero();
if agent.action_timer > 4.0 {
controller.inputs.secondary.set_state(false);
agent.action_timer = 0.0;
} else if agent.action_timer > 2.0 {
controller.inputs.secondary.set_state(true);
agent.action_timer += dt.0;
} else if self
.stats
.skill_set
.has_skill(Skill::Hammer(HammerSkill::UnlockLeap))
&& self.energy.current() > 700
&& thread_rng().gen_bool(0.9)
{
controller.inputs.ability3.set_state(true);
agent.action_timer += dt.0;
} else {
controller.inputs.primary.set_state(true);
agent.action_timer += dt.0;
}
} else if dist_sqrd < MAX_CHASE_DIST.powi(2) {
if let Some((bearing, speed)) = agent.chaser.chase(
&*terrain,
self.pos.0,
self.vel.0,
tgt_pos.0,
TraversalConfig {
min_tgt_dist: 1.25,
..self.traversal_config
},
) {
if can_see_tgt(&*terrain, self.pos, tgt_pos, dist_sqrd) {
controller.inputs.move_dir =
bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
if self
.stats
.skill_set
.has_skill(Skill::Hammer(HammerSkill::UnlockLeap))
&& agent.action_timer > 5.0
{
controller.inputs.ability3.set_state(true);
agent.action_timer = 0.0;
} else {
agent.action_timer += dt.0;
2020-01-25 18:49:47 +00:00
}
2021-02-07 07:22:06 +00:00
} else {
controller.inputs.move_dir =
bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
controller.inputs.jump.set_state(bearing.z > 1.5);
controller.inputs.move_z = bearing.z;
2020-01-25 18:49:47 +00:00
}
}
2021-02-07 07:22:06 +00:00
if self.body.map(|b| b.is_humanoid()).unwrap_or(false)
&& dist_sqrd < 16.0f32.powi(2)
&& thread_rng().gen::<f32>() < 0.02
{
controller.inputs.roll.set_state(true);
}
} else {
agent.target = None;
2020-01-25 18:49:47 +00:00
}
2021-02-07 07:22:06 +00:00
},
Tactic::Sword => {
if dist_sqrd < (min_attack_dist * self.scale).powi(2) {
controller.inputs.move_dir = Vec2::zero();
if self
.stats
.skill_set
.has_skill(Skill::Sword(SwordSkill::UnlockSpin))
&& agent.action_timer < 2.0
&& self.energy.current() > 600
{
controller.inputs.ability3.set_state(true);
agent.action_timer += dt.0;
} else if agent.action_timer > 2.0 {
agent.action_timer = 0.0;
} else {
controller.inputs.primary.set_state(true);
agent.action_timer += dt.0;
}
} else if dist_sqrd < MAX_CHASE_DIST.powi(2) {
if let Some((bearing, speed)) = agent.chaser.chase(
&*terrain,
self.pos.0,
self.vel.0,
tgt_pos.0,
TraversalConfig {
min_tgt_dist: 1.25,
..self.traversal_config
},
) {
if can_see_tgt(&*terrain, self.pos, tgt_pos, dist_sqrd) {
controller.inputs.move_dir =
bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
if agent.action_timer > 4.0 {
controller.inputs.secondary.set_state(true);
agent.action_timer = 0.0;
} else {
agent.action_timer += dt.0;
}
} else {
controller.inputs.move_dir =
bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
controller.inputs.jump.set_state(bearing.z > 1.5);
controller.inputs.move_z = bearing.z;
}
2021-02-07 07:22:06 +00:00
}
if self.body.map(|b| b.is_humanoid()).unwrap_or(false)
&& dist_sqrd < 16.0f32.powi(2)
&& thread_rng().gen::<f32>() < 0.02
{
controller.inputs.roll.set_state(true);
}
} else {
agent.target = None;
}
},
Tactic::Bow => {
if self.body.map(|b| b.is_humanoid()).unwrap_or(false)
&& dist_sqrd < (2.0 * min_attack_dist * self.scale).powi(2)
{
controller.inputs.roll.set_state(true);
} else if dist_sqrd < MAX_CHASE_DIST.powi(2) {
if let Some((bearing, speed)) = agent.chaser.chase(
&*terrain,
self.pos.0,
self.vel.0,
tgt_pos.0,
TraversalConfig {
min_tgt_dist: 1.25,
..self.traversal_config
},
) {
if can_see_tgt(&*terrain, self.pos, tgt_pos, dist_sqrd) {
controller.inputs.move_dir = bearing
.xy()
.rotated_z(thread_rng().gen_range(0.5..1.57))
.try_normalized()
.unwrap_or_else(Vec2::zero)
* speed;
if agent.action_timer > 4.0 {
controller.inputs.secondary.set_state(false);
agent.action_timer = 0.0;
} else if agent.action_timer > 2.0 && self.energy.current() > 300 {
controller.inputs.secondary.set_state(true);
agent.action_timer += dt.0;
} else if self
.stats
.skill_set
.has_skill(Skill::Bow(BowSkill::UnlockRepeater))
&& self.energy.current() > 400
&& thread_rng().gen_bool(0.8)
{
2021-02-07 07:22:06 +00:00
controller.inputs.secondary.set_state(false);
controller.inputs.ability3.set_state(true);
agent.action_timer += dt.0;
} else {
controller.inputs.secondary.set_state(false);
controller.inputs.primary.set_state(true);
agent.action_timer += dt.0;
2020-01-25 18:49:47 +00:00
}
2021-02-07 07:22:06 +00:00
} else {
controller.inputs.move_dir =
bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
controller.inputs.jump.set_state(bearing.z > 1.5);
controller.inputs.move_z = bearing.z;
2020-01-25 18:49:47 +00:00
}
2021-02-07 07:22:06 +00:00
}
if self.body.map(|b| b.is_humanoid()).unwrap_or(false)
&& dist_sqrd < 16.0f32.powi(2)
&& thread_rng().gen::<f32>() < 0.02
{
controller.inputs.roll.set_state(true);
}
} else if can_see_tgt(&*terrain, self.pos, tgt_pos, dist_sqrd) {
if let Some((bearing, speed)) = agent.chaser.chase(
&*terrain,
self.pos.0,
self.vel.0,
tgt_pos.0,
TraversalConfig {
min_tgt_dist: 1.25,
..self.traversal_config
},
) {
controller.inputs.move_dir =
bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
controller.inputs.jump.set_state(bearing.z > 1.5);
controller.inputs.move_z = bearing.z;
}
} else {
agent.target = None;
}
2021-02-07 07:22:06 +00:00
},
Tactic::Staff => {
if self.body.map(|b| b.is_humanoid()).unwrap_or(false)
&& dist_sqrd < (min_attack_dist * self.scale).powi(2)
{
controller.inputs.roll.set_state(true);
} else if dist_sqrd < (5.0 * min_attack_dist * self.scale).powi(2) {
if agent.action_timer < 1.5 {
controller.inputs.move_dir = (tgt_pos.0 - self.pos.0)
.xy()
.rotated_z(0.47 * PI)
.try_normalized()
.unwrap_or_else(Vec2::unit_y);
agent.action_timer += dt.0;
} else if agent.action_timer < 3.0 {
controller.inputs.move_dir = (tgt_pos.0 - self.pos.0)
.xy()
.rotated_z(-0.47 * PI)
.try_normalized()
.unwrap_or_else(Vec2::unit_y);
agent.action_timer += dt.0;
} else {
agent.action_timer = 0.0;
}
if self
.stats
.skill_set
.has_skill(Skill::Staff(StaffSkill::UnlockShockwave))
&& self.energy.current() > 800
&& thread_rng().gen::<f32>() > 0.8
{
controller.inputs.ability3.set_state(true);
} else if self.energy.current() > 10 {
controller.inputs.secondary.set_state(true);
} else {
controller.inputs.primary.set_state(true);
}
} else if dist_sqrd < MAX_CHASE_DIST.powi(2) {
if let Some((bearing, speed)) = agent.chaser.chase(
&*terrain,
self.pos.0,
self.vel.0,
tgt_pos.0,
TraversalConfig {
min_tgt_dist: 1.25,
..self.traversal_config
},
) {
if can_see_tgt(&*terrain, self.pos, tgt_pos, dist_sqrd) {
controller.inputs.move_dir = bearing
.xy()
.rotated_z(thread_rng().gen_range(-1.57..-0.5))
.try_normalized()
.unwrap_or_else(Vec2::zero)
* speed;
controller.inputs.primary.set_state(true);
} else {
controller.inputs.move_dir =
bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
controller.inputs.jump.set_state(bearing.z > 1.5);
controller.inputs.move_z = bearing.z;
}
}
if self.body.map(|b| b.is_humanoid()).unwrap_or(false)
&& dist_sqrd < 16.0f32.powi(2)
&& thread_rng().gen::<f32>() < 0.02
{
controller.inputs.roll.set_state(true);
}
} else if can_see_tgt(&*terrain, self.pos, tgt_pos, dist_sqrd) {
if let Some((bearing, speed)) = agent.chaser.chase(
&*terrain,
self.pos.0,
self.vel.0,
tgt_pos.0,
TraversalConfig {
min_tgt_dist: 1.25,
..self.traversal_config
},
) {
controller.inputs.move_dir =
bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
controller.inputs.jump.set_state(bearing.z > 1.5);
controller.inputs.move_z = bearing.z;
}
} else {
agent.target = None;
}
},
Tactic::StoneGolemBoss => {
if dist_sqrd < (min_attack_dist * self.scale).powi(2) {
// 2.0 is temporary correction factor to allow them to melee with their
// large hitbox
controller.inputs.move_dir = Vec2::zero();
controller.inputs.primary.set_state(true);
} else if dist_sqrd < MAX_CHASE_DIST.powi(2) {
if self.vel.0.is_approx_zero() {
controller.inputs.ability3.set_state(true);
}
if let Some((bearing, speed)) = agent.chaser.chase(
&*terrain,
self.pos.0,
self.vel.0,
tgt_pos.0,
TraversalConfig {
min_tgt_dist: 1.25,
..self.traversal_config
},
) {
if can_see_tgt(&*terrain, self.pos, tgt_pos, dist_sqrd) {
controller.inputs.move_dir =
bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
if agent.action_timer > 5.0 {
controller.inputs.secondary.set_state(true);
agent.action_timer = 0.0;
} else {
agent.action_timer += dt.0;
}
} else {
controller.inputs.move_dir =
bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
controller.inputs.jump.set_state(bearing.z > 1.5);
controller.inputs.move_z = bearing.z;
}
}
} else {
agent.target = None;
}
},
Tactic::CircleCharge {
radius,
circle_time,
} => {
if dist_sqrd < (min_attack_dist * self.scale).powi(2) && thread_rng().gen_bool(0.5)
{
controller.inputs.move_dir = Vec2::zero();
controller.inputs.primary.set_state(true);
} else if dist_sqrd < (radius as f32 * min_attack_dist * self.scale).powi(2) {
controller.inputs.move_dir = (self.pos.0 - tgt_pos.0)
.xy()
.try_normalized()
.unwrap_or_else(Vec2::unit_y);
} else if dist_sqrd < ((radius as f32 + 1.0) * min_attack_dist * self.scale).powi(2)
&& dist_sqrd > (radius as f32 * min_attack_dist * self.scale).powi(2)
{
if agent.action_timer < circle_time as f32 {
controller.inputs.move_dir = (tgt_pos.0 - self.pos.0)
.xy()
.rotated_z(0.47 * PI)
.try_normalized()
.unwrap_or_else(Vec2::unit_y);
agent.action_timer += dt.0;
} else if agent.action_timer < circle_time as f32 + 0.5 {
controller.inputs.secondary.set_state(true);
agent.action_timer += dt.0;
} else if agent.action_timer < 2.0 * circle_time as f32 + 0.5 {
controller.inputs.move_dir = (tgt_pos.0 - self.pos.0)
.xy()
.rotated_z(-0.47 * PI)
.try_normalized()
.unwrap_or_else(Vec2::unit_y);
agent.action_timer += dt.0;
} else if agent.action_timer < 2.0 * circle_time as f32 + 1.0 {
controller.inputs.secondary.set_state(true);
agent.action_timer += dt.0;
} else {
agent.action_timer = 0.0;
}
} else if dist_sqrd < MAX_CHASE_DIST.powi(2) {
if let Some((bearing, speed)) = agent.chaser.chase(
&*terrain,
self.pos.0,
self.vel.0,
tgt_pos.0,
TraversalConfig {
min_tgt_dist: 1.25,
..self.traversal_config
},
) {
controller.inputs.move_dir =
bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
controller.inputs.jump.set_state(bearing.z > 1.5);
controller.inputs.move_z = bearing.z;
}
} else {
agent.target = None;
}
},
Tactic::QuadLowRanged => {
if dist_sqrd < (3.0 * min_attack_dist * self.scale).powi(2) {
controller.inputs.move_dir = (tgt_pos.0 - self.pos.0)
.xy()
.try_normalized()
.unwrap_or_else(Vec2::unit_y);
controller.inputs.primary.set_state(true);
} else if dist_sqrd < MAX_CHASE_DIST.powi(2) {
if let Some((bearing, speed)) = agent.chaser.chase(
&*terrain,
self.pos.0,
self.vel.0,
tgt_pos.0,
TraversalConfig {
min_tgt_dist: 1.25,
..self.traversal_config
},
) {
if can_see_tgt(&*terrain, self.pos, tgt_pos, dist_sqrd) {
if agent.action_timer > 5.0 {
agent.action_timer = 0.0;
} else if agent.action_timer > 2.5 {
controller.inputs.move_dir = (tgt_pos.0 - self.pos.0)
.xy()
.rotated_z(1.75 * PI)
.try_normalized()
.unwrap_or_else(Vec2::zero)
* speed;
agent.action_timer += dt.0;
} else {
controller.inputs.move_dir = (tgt_pos.0 - self.pos.0)
.xy()
.rotated_z(0.25 * PI)
.try_normalized()
.unwrap_or_else(Vec2::zero)
* speed;
agent.action_timer += dt.0;
}
controller.inputs.secondary.set_state(true);
controller.inputs.jump.set_state(bearing.z > 1.5);
controller.inputs.move_z = bearing.z;
} else {
controller.inputs.move_dir =
bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
controller.inputs.jump.set_state(bearing.z > 1.5);
controller.inputs.move_z = bearing.z;
}
} else {
agent.target = None;
}
} else {
agent.target = None;
}
},
Tactic::TailSlap => {
if dist_sqrd < (1.5 * min_attack_dist * self.scale).powi(2) {
if agent.action_timer > 4.0 {
controller.inputs.primary.set_state(false);
agent.action_timer = 0.0;
} else if agent.action_timer > 1.0 {
controller.inputs.primary.set_state(true);
agent.action_timer += dt.0;
} else {
controller.inputs.secondary.set_state(true);
agent.action_timer += dt.0;
}
controller.inputs.move_dir = (tgt_pos.0 - self.pos.0)
.xy()
.try_normalized()
.unwrap_or_else(Vec2::unit_y)
* 0.1;
} else if dist_sqrd < MAX_CHASE_DIST.powi(2) {
if let Some((bearing, speed)) = agent.chaser.chase(
&*terrain,
self.pos.0,
self.vel.0,
tgt_pos.0,
TraversalConfig {
min_tgt_dist: 1.25,
..self.traversal_config
},
) {
controller.inputs.move_dir =
bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
controller.inputs.jump.set_state(bearing.z > 1.5);
controller.inputs.move_z = bearing.z;
}
} else {
agent.target = None;
}
},
Tactic::QuadLowQuick => {
if dist_sqrd < (1.5 * min_attack_dist * self.scale).powi(2) {
controller.inputs.move_dir = Vec2::zero();
controller.inputs.secondary.set_state(true);
} else if dist_sqrd < (3.0 * min_attack_dist * self.scale).powi(2)
&& dist_sqrd > (2.0 * min_attack_dist * self.scale).powi(2)
{
controller.inputs.primary.set_state(true);
controller.inputs.move_dir = (tgt_pos.0 - self.pos.0)
.xy()
.rotated_z(-0.47 * PI)
.try_normalized()
.unwrap_or_else(Vec2::unit_y);
} else if dist_sqrd < MAX_CHASE_DIST.powi(2) {
if let Some((bearing, speed)) = agent.chaser.chase(
&*terrain,
self.pos.0,
self.vel.0,
tgt_pos.0,
TraversalConfig {
min_tgt_dist: 1.25,
..self.traversal_config
},
) {
controller.inputs.move_dir =
bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
controller.inputs.jump.set_state(bearing.z > 1.5);
controller.inputs.move_z = bearing.z;
}
} else {
agent.target = None;
}
},
Tactic::QuadLowBasic => {
if dist_sqrd < (1.5 * min_attack_dist * self.scale).powi(2) {
controller.inputs.move_dir = Vec2::zero();
if agent.action_timer > 5.0 {
agent.action_timer = 0.0;
} else if agent.action_timer > 2.0 {
controller.inputs.secondary.set_state(true);
agent.action_timer += dt.0;
} else {
controller.inputs.primary.set_state(true);
agent.action_timer += dt.0;
}
} else if dist_sqrd < MAX_CHASE_DIST.powi(2) {
if let Some((bearing, speed)) = agent.chaser.chase(
&*terrain,
self.pos.0,
self.vel.0,
tgt_pos.0,
TraversalConfig {
min_tgt_dist: 1.25,
..self.traversal_config
},
) {
controller.inputs.move_dir =
bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
controller.inputs.jump.set_state(bearing.z > 1.5);
controller.inputs.move_z = bearing.z;
}
} else {
agent.target = None;
}
},
Tactic::QuadMedJump => {
if dist_sqrd < (1.5 * min_attack_dist * self.scale).powi(2) {
controller.inputs.move_dir = Vec2::zero();
controller.inputs.secondary.set_state(true);
} else if dist_sqrd < (5.0 * min_attack_dist * self.scale).powi(2) {
controller.inputs.ability3.set_state(true);
} else if dist_sqrd < MAX_CHASE_DIST.powi(2) {
if let Some((bearing, speed)) = agent.chaser.chase(
&*terrain,
self.pos.0,
self.vel.0,
tgt_pos.0,
TraversalConfig {
min_tgt_dist: 1.25,
..self.traversal_config
},
) {
if can_see_tgt(&*terrain, self.pos, tgt_pos, dist_sqrd) {
controller.inputs.primary.set_state(true);
controller.inputs.move_dir =
bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
} else {
controller.inputs.move_dir =
bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
controller.inputs.jump.set_state(bearing.z > 1.5);
controller.inputs.move_z = bearing.z;
}
}
} else {
agent.target = None;
}
},
Tactic::QuadMedBasic => {
if dist_sqrd < (min_attack_dist * self.scale).powi(2) {
controller.inputs.move_dir = Vec2::zero();
if agent.action_timer < 2.0 {
controller.inputs.secondary.set_state(true);
agent.action_timer += dt.0;
} else if agent.action_timer < 3.0 {
controller.inputs.primary.set_state(true);
agent.action_timer += dt.0;
} else {
agent.action_timer = 0.0;
}
} else if dist_sqrd < MAX_CHASE_DIST.powi(2) {
if let Some((bearing, speed)) = agent.chaser.chase(
&*terrain,
self.pos.0,
self.vel.0,
tgt_pos.0,
TraversalConfig {
min_tgt_dist: 1.25,
..self.traversal_config
},
) {
controller.inputs.move_dir =
bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
controller.inputs.jump.set_state(bearing.z > 1.5);
controller.inputs.move_z = bearing.z;
}
} else {
agent.target = None;
}
},
2021-03-03 03:55:28 +00:00
Tactic::Lavadrake | Tactic::QuadLowBeam => {
2021-02-07 07:22:06 +00:00
if dist_sqrd < (2.5 * min_attack_dist * self.scale).powi(2) {
controller.inputs.move_dir = Vec2::zero();
controller.inputs.secondary.set_state(true);
} else if dist_sqrd < (7.0 * min_attack_dist * self.scale).powi(2) {
if agent.action_timer < 2.0 {
controller.inputs.move_dir = (tgt_pos.0 - self.pos.0)
.xy()
.rotated_z(0.47 * PI)
.try_normalized()
.unwrap_or_else(Vec2::unit_y);
controller.inputs.primary.set_state(true);
agent.action_timer += dt.0;
} else if agent.action_timer < 4.0 {
controller.inputs.move_dir = (tgt_pos.0 - self.pos.0)
.xy()
.rotated_z(-0.47 * PI)
.try_normalized()
.unwrap_or_else(Vec2::unit_y);
controller.inputs.primary.set_state(true);
agent.action_timer += dt.0;
} else if agent.action_timer < 6.0 {
controller.inputs.ability3.set_state(true);
agent.action_timer += dt.0;
} else {
agent.action_timer = 0.0;
}
} else if dist_sqrd < MAX_CHASE_DIST.powi(2) {
if let Some((bearing, speed)) = agent.chaser.chase(
&*terrain,
self.pos.0,
self.vel.0,
tgt_pos.0,
TraversalConfig {
min_tgt_dist: 1.25,
..self.traversal_config
},
) {
controller.inputs.move_dir =
bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
controller.inputs.jump.set_state(bearing.z > 1.5);
controller.inputs.move_z = bearing.z;
}
} else {
agent.target = None;
}
},
Tactic::Theropod => {
if dist_sqrd < (2.0 * min_attack_dist * self.scale).powi(2) {
controller.inputs.move_dir = Vec2::zero();
controller.inputs.primary.set_state(true);
} else if dist_sqrd < MAX_CHASE_DIST.powi(2) {
if let Some((bearing, speed)) = agent.chaser.chase(
&*terrain,
self.pos.0,
self.vel.0,
tgt_pos.0,
TraversalConfig {
min_tgt_dist: 1.25,
..self.traversal_config
},
) {
controller.inputs.move_dir =
bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
controller.inputs.jump.set_state(bearing.z > 1.5);
controller.inputs.move_z = bearing.z;
}
} else {
agent.target = None;
}
},
Tactic::Turret => {
2021-03-03 03:55:28 +00:00
if can_see_tgt(&*terrain, self.pos, tgt_pos, dist_sqrd) {
2021-02-07 07:22:06 +00:00
controller.inputs.primary.set_state(true);
} else {
agent.target = None;
}
},
Tactic::FixedTurret => {
controller.inputs.look_dir = self.ori.look_dir();
2021-03-03 03:55:28 +00:00
if can_see_tgt(&*terrain, self.pos, tgt_pos, dist_sqrd) {
2021-02-07 07:22:06 +00:00
controller.inputs.primary.set_state(true);
} else {
agent.target = None;
}
},
Tactic::RotatingTurret => {
controller.inputs.look_dir = Dir::new(
Quaternion::from_xyzw(self.ori.look_dir().x, self.ori.look_dir().y, 0.0, 0.0)
2021-03-03 03:55:28 +00:00
.rotated_z(6.0 * dt.0 as f32)
.into_vec3()
.try_normalized()
.unwrap_or_default(),
2021-02-07 07:22:06 +00:00
);
2021-03-03 03:55:28 +00:00
if can_see_tgt(&*terrain, self.pos, tgt_pos, dist_sqrd) {
2021-02-07 07:22:06 +00:00
controller.inputs.primary.set_state(true);
} else {
agent.target = None;
}
},
}
2021-02-07 07:22:06 +00:00
}
2021-02-06 06:15:25 +00:00
2021-02-07 07:22:06 +00:00
fn follow(
&self,
agent: &mut Agent,
controller: &mut Controller,
terrain: &TerrainGrid,
tgt_pos: &Pos,
) {
if let Some((bearing, speed)) = agent.chaser.chase(
&*terrain,
self.pos.0,
self.vel.0,
tgt_pos.0,
TraversalConfig {
min_tgt_dist: AVG_FOLLOW_DIST,
..self.traversal_config
},
) {
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);
controller.inputs.jump.set_state(bearing.z > 1.5);
controller.inputs.move_z = bearing.z;
}
}
}
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
}