veloren/server/src/sys/agent.rs

1767 lines
69 KiB
Rust
Raw Normal View History

2022-01-18 03:02:43 +00:00
pub mod attack;
pub mod behavior_tree;
2022-01-18 03:02:43 +00:00
pub mod consts;
pub mod data;
pub mod util;
use crate::{
rtsim::RtSim,
2022-01-18 03:02:43 +00:00
sys::agent::{
2022-08-11 19:15:46 +00:00
behavior_tree::{BehaviorData, BehaviorTree},
2022-01-18 03:02:43 +00:00
consts::{
AVG_FOLLOW_DIST, DEFAULT_ATTACK_RANGE, IDLE_HEALING_ITEM_THRESHOLD, PARTIAL_PATH_DIST,
SEPARATION_BIAS, SEPARATION_DIST,
2022-01-18 03:02:43 +00:00
},
data::{AgentData, AttackData, Path, ReadData, Tactic, TargetData},
2022-01-18 03:02:43 +00:00
util::{
aim_projectile, are_our_owners_hostile, entities_have_line_of_sight, get_attacker,
get_entity_by_id, is_dead_or_invulnerable, is_dressed_as_cultist, is_invulnerable,
is_village_guard, is_villager,
2022-01-18 03:02:43 +00:00
},
},
};
use common::{
combat::perception_dist_multiplier_from_stealth,
2020-04-18 18:28:19 +00:00
comp::{
self,
agent::{Sound, SoundKind, Target},
2022-01-18 03:02:43 +00:00
buff::BuffKind,
inventory::slot::EquipSlot,
2020-11-06 17:39:49 +00:00
item::{
2021-08-02 23:22:22 +00:00
tool::{AbilitySpec, ToolKind},
2021-07-14 15:26:29 +00:00
ConsumableKind, Item, ItemDesc, ItemKind,
2020-11-06 17:39:49 +00:00
},
item_drop,
projectile::ProjectileConstructor,
Agent, Alignment, Body, CharacterState, ControlAction, ControlEvent, Controller, Health,
HealthChange, InputKind, InventoryAction, Pos, Scale, UnresolvedChatMsg, UtteranceKind,
2020-04-18 18:28:19 +00:00
},
2021-04-11 23:47:29 +00:00
effect::{BuffEffect, Effect},
2021-02-07 07:22:06 +00:00
event::{Emitter, EventBus, ServerEvent},
path::TraversalConfig,
rtsim::RtSimEvent,
2022-01-18 03:02:43 +00:00
states::basic_beam,
terrain::{Block, TerrainGrid},
2020-10-07 02:23:20 +00:00
time::DayPeriod,
util::Dir,
vol::ReadVol,
};
use common_base::prof_span;
use common_ecs::{Job, Origin, ParMode, Phase, System};
use itertools::Itertools;
use rand::{thread_rng, Rng};
use rayon::iter::ParallelIterator;
2022-07-29 17:06:08 +00:00
use specs::{Entity as EcsEntity, Join, ParJoin, Read, WriteExpect, WriteStorage};
use vek::*;
2019-06-09 19:33:20 +00:00
/// This system will allow NPCs to modify their controller
#[derive(Default)]
pub struct Sys;
2021-03-08 11:13:59 +00:00
impl<'a> System<'a> for Sys {
type SystemData = (
2021-02-07 07:22:06 +00:00
ReadData<'a>,
2022-05-09 19:58:13 +00:00
Read<'a, EventBus<ServerEvent>>,
WriteStorage<'a, Agent>,
2019-06-09 14:20:20 +00:00
WriteStorage<'a, Controller>,
WriteExpect<'a, RtSim>,
);
const NAME: &'static str = "agent";
const ORIGIN: Origin = Origin::Server;
const PHASE: Phase = Phase::Create;
2019-08-04 08:21:29 +00:00
fn run(
job: &mut Job<Self>,
(read_data, event_bus, mut agents, mut controllers, mut rtsim): Self::SystemData,
2019-08-04 08:21:29 +00:00
) {
let rtsim = &mut *rtsim;
job.cpu_stats.measure(ParMode::Rayon);
(
2021-02-07 07:22:06 +00:00
&read_data.entities,
(&read_data.energies, read_data.healths.maybe()),
2021-03-29 20:30:09 +00:00
(
&read_data.positions,
&read_data.velocities,
&read_data.orientations,
),
2021-02-07 07:22:06 +00:00
read_data.bodies.maybe(),
&read_data.inventories,
2021-11-12 03:37:37 +00:00
(
&read_data.char_states,
&read_data.skill_set,
&read_data.active_abilities,
),
2021-02-07 07:22:06 +00:00
&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.is_mounts,
)
.par_join()
.for_each_init(
|| {
prof_span!(guard, "agent rayon job");
guard
},
|_guard,
(
2021-02-07 07:22:06 +00:00
entity,
(energy, health),
2021-03-29 20:30:09 +00:00
(pos, vel, ori),
2021-02-07 07:22:06 +00:00
body,
inventory,
2021-11-12 03:37:37 +00:00
(char_state, skill_set, active_abilities),
2021-02-07 07:22:06 +00:00
physics_state,
uid,
agent,
controller,
light_emitter,
group,
2021-02-07 07:22:06 +00:00
_,
)| {
2022-01-18 03:02:43 +00:00
let mut event_emitter = event_bus.emitter();
2022-08-11 19:15:46 +00:00
let mut rng = thread_rng();
2022-01-18 03:02:43 +00:00
// 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!(
2021-02-07 07:22:06 +00:00
&read_data.alignments.get(entity),
&Some(Alignment::Owned(_))
) {
read_data.alignments.get(entity).copied()
} else {
group
2021-02-07 07:22:06 +00:00
.and_then(|g| read_data.group_manager.group_info(*g))
.and_then(|info| read_data.uids.get(info.leader))
.copied()
.map_or_else(
|| read_data.alignments.get(entity).copied(),
|uid| Some(Alignment::Owned(uid)),
)
2021-02-07 07:22:06 +00:00
};
if !matches!(char_state, CharacterState::LeapMelee(_)) {
2021-07-14 10:22:47 +00:00
// Default to looking in orientation direction
// (can be overridden below)
//
2022-07-15 16:59:37 +00:00
// This definitely breaks LeapMelee and
// probably not only that, do we really need this at all?
controller.reset();
controller.inputs.look_dir = ori.look_dir();
}
let scale = read_data.scales.get(entity).map_or(1.0, |Scale(s)| *s);
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| {
2022-05-18 20:28:06 +00:00
matches!(&*item.kind(), comp::item::ItemKind::Glider)
2021-02-07 07:22:06 +00:00
});
2021-02-07 07:22:06 +00:00
let is_gliding = matches!(
read_data.char_states.get(entity),
2021-08-01 11:20:46 +00:00
Some(CharacterState::GlideWield(_) | CharacterState::Glide(_))
2021-06-20 03:51:04 +00:00
) && physics_state.on_ground.is_none();
2021-05-30 15:38:47 +00:00
if let Some(pid) = agent.position_pid_controller.as_mut() {
pid.add_measurement(read_data.time.0, pos.0);
}
// 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
2021-02-07 07:22:06 +00:00
// obstacles that smaller entities would not).
let node_tolerance = scale * 1.5;
let slow_factor = body.map_or(0.0, |b| b.base_accel() / 250.0).min(1.0);
2021-02-07 07:22:06 +00:00
let traversal_config = TraversalConfig {
node_tolerance,
slow_factor,
2021-06-20 03:51:04 +00:00
on_ground: physics_state.on_ground.is_some(),
in_liquid: physics_state.in_liquid().is_some(),
2021-02-07 07:22:06 +00:00
min_tgt_dist: 1.0,
can_climb: body.map_or(false, Body::can_climb),
can_fly: body.map_or(false, |b| b.fly_thrust().is_some()),
2021-02-07 07:22:06 +00:00
};
let health_fraction = health.map_or(1.0, Health::fraction);
let rtsim_entity = read_data
.rtsim_entities
.get(entity)
.and_then(|rtsim_ent| rtsim.get_entity(rtsim_ent.0));
if traversal_config.can_fly && matches!(body, Some(Body::Ship(_))) {
// hack (kinda): Never turn off flight airships
// since it results in stuttering and falling back to the ground.
//
// TODO: look into `controller.reset()` line above
// and see if it fixes it
2022-01-26 19:09:59 +00:00
controller.push_basic_input(InputKind::Fly);
}
2021-02-07 07:22:06 +00:00
// Package all this agent's data into a convenient struct
let data = AgentData {
entity: &entity,
rtsim_entity,
2021-02-07 07:22:06 +00:00
uid,
pos,
vel,
ori,
energy,
body,
inventory,
skill_set,
2021-02-07 07:22:06 +00:00
physics_state,
alignment: alignment.as_ref(),
traversal_config,
scale,
damage: health_fraction,
2021-02-07 07:22:06 +00:00
light_emitter,
glider_equipped,
is_gliding,
health: read_data.healths.get(entity),
char_state,
2021-11-12 03:37:37 +00:00
active_abilities,
cached_spatial_grid: &read_data.cached_spatial_grid,
2022-05-28 23:41:31 +00:00
msm: &read_data.msm,
2021-02-07 07:22:06 +00:00
};
///////////////////////////////////////////////////////////
// Behavior tree
///////////////////////////////////////////////////////////
2021-03-30 00:25:59 +00:00
// The behavior tree is meant to make decisions for agents
// *but should not* mutate any data (only action nodes
// should do that). Each path should lead to one (and only
// one) action node. This makes bugfinding much easier and
// debugging way easier. If you don't think so, try
// debugging the agent code before this MR
// (https://gitlab.com/veloren/veloren/-/merge_requests/1801).
// Each tick should arrive at one (1) action node which
// then determines what the agent does. If this makes you
// uncomfortable, consider dt the response time of the
// NPC. To make the tree easier to read, subtrees can be
// created as methods on `AgentData`. Action nodes are
// also methods on the `AgentData` struct. Action nodes
// are the only parts of this tree that should provide
// inputs.
2022-08-11 19:15:46 +00:00
let mut behavior_data = BehaviorData {
2022-07-28 21:31:44 +00:00
agent,
2022-08-11 19:15:46 +00:00
agent_data: data,
read_data: &read_data,
event_emitter: &mut event_emitter,
2022-07-28 21:31:44 +00:00
controller,
2022-08-11 19:15:46 +00:00
rng: &mut rng,
};
BehaviorTree::root().run(&mut behavior_data);
2022-07-28 21:31:44 +00:00
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());
},
);
for (agent, rtsim_entity) in (&mut agents, &read_data.rtsim_entities).join() {
// Entity must be loaded in as it has an agent component :)
// React to all events in the controller
for event in core::mem::take(&mut agent.rtsim_controller.events) {
2021-03-29 14:47:42 +00:00
match event {
RtSimEvent::AddMemory(memory) => {
rtsim.insert_entity_memory(rtsim_entity.0, memory.clone());
2021-03-29 14:47:42 +00:00
},
2021-07-14 15:26:29 +00:00
RtSimEvent::ForgetEnemy(name) => {
rtsim.forget_entity_enemy(rtsim_entity.0, &name);
},
2021-03-29 14:47:42 +00:00
RtSimEvent::SetMood(memory) => {
rtsim.set_entity_mood(rtsim_entity.0, memory.clone());
2021-03-29 14:47:42 +00:00
},
RtSimEvent::PrintMemories => {},
}
}
}
2021-02-07 07:22:06 +00:00
}
}
2021-02-07 07:22:06 +00:00
impl<'a> AgentData<'a> {
2021-03-30 00:25:59 +00:00
////////////////////////////////////////
// Action Nodes
////////////////////////////////////////
2021-07-13 20:54:16 +00:00
fn glider_fall(&self, controller: &mut Controller) {
2022-01-26 18:52:19 +00:00
controller.push_action(ControlAction::GlideWield);
2021-07-13 20:54:16 +00:00
let flight_direction =
Vec3::from(self.vel.0.xy().try_normalized().unwrap_or_else(Vec2::zero));
let flight_ori = Quaternion::from_scalar_and_vec3((1.0, flight_direction));
2021-07-13 20:54:16 +00:00
let ori = self.ori.look_vec();
let look_dir = if ori.z > 0.0 {
flight_ori.rotated_x(-0.1)
} else {
flight_ori.rotated_x(0.1)
};
2021-07-13 20:54:16 +00:00
let (_, look_dir) = look_dir.into_scalar_and_vec3();
controller.inputs.look_dir = Dir::from_unnormalized(look_dir).unwrap_or_else(Dir::forward);
}
fn fly_upward(&self, controller: &mut Controller) {
2022-01-26 19:09:59 +00:00
controller.push_basic_input(InputKind::Fly);
controller.inputs.move_z = 1.0;
2021-03-30 00:25:59 +00:00
}
2022-01-18 03:02:43 +00:00
fn idle(
&self,
agent: &mut Agent,
controller: &mut Controller,
read_data: &ReadData,
rng: &mut impl Rng,
) {
2021-02-07 07:22:06 +00:00
// 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(_))
2021-02-07 07:22:06 +00:00
});
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
2022-01-18 03:02:43 +00:00
if lantern_equipped && 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.
2021-02-07 07:22:06 +00:00
// Only emit event for agents that sill need to
// turn on their lantern.
controller.push_event(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.
controller.push_event(ControlEvent::DisableLantern)
2021-02-07 07:22:06 +00:00
}
};
2022-06-07 07:25:27 +00:00
if let Some(body) = self.body {
let attempt_heal = if matches!(body, Body::Humanoid(_)) {
self.damage < IDLE_HEALING_ITEM_THRESHOLD
} else {
true
};
if attempt_heal && self.heal_self(agent, controller, true) {
agent.action_state.timer = 0.01;
return;
}
} else {
2021-04-28 22:41:04 +00:00
agent.action_state.timer = 0.01;
2021-04-11 23:47:29 +00:00
return;
}
2021-04-28 22:41:04 +00:00
agent.action_state.timer = 0.0;
2021-02-07 07:22:06 +00:00
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.
2021-03-14 18:42:39 +00:00
if 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())
{
2022-01-26 19:09:59 +00:00
controller.push_basic_input(InputKind::Fly);
2021-03-14 18:42:39 +00:00
} else {
2022-01-26 19:15:40 +00:00
controller.push_cancel_input(InputKind::Fly)
2021-03-14 18:42:39 +00:00
}
2021-02-07 07:22:06 +00:00
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);
self.jump_if(bearing.z > 1.5 || self.traversal_config.can_fly, controller);
2021-02-07 07:22:06 +00:00
controller.inputs.climb = Some(comp::Climb::Up);
//.filter(|_| bearing.z > 0.1 || self.physics_state.in_liquid().is_some());
let height_offset = bearing.z
2021-02-07 07:22:06 +00:00
+ if self.traversal_config.can_fly {
// NOTE: costs 4 us (imbris)
let obstacle_ahead = read_data
2021-02-07 07:22:06 +00:00
.terrain
.ray(
self.pos.0 + Vec3::unit_z(),
self.pos.0
2021-03-12 02:58:57 +00:00
+ bearing.try_normalized().unwrap_or_else(Vec3::unit_y) * 80.0
2021-02-07 07:22:06 +00:00
+ Vec3::unit_z(),
)
.until(Block::is_solid)
.cast()
.1
.map_or(true, |b| b.is_some());
let mut ground_too_close = self
.body
.map(|body| {
#[cfg(feature = "worldgen")]
let height_approx = self.pos.0.z
- read_data
.world
.sim()
.get_alt_approx(self.pos.0.xy().map(|x: f32| x as i32))
.unwrap_or(0.0);
#[cfg(not(feature = "worldgen"))]
let height_approx = self.pos.0.z;
height_approx < body.flying_height()
})
.unwrap_or(false);
const NUM_RAYS: usize = 5;
// NOTE: costs 15-20 us (imbris)
for i in 0..=NUM_RAYS {
let magnitude = self.body.map_or(20.0, |b| b.flying_height());
// Lerp between a line straight ahead and straight down to detect a
// wedge of obstacles we might fly into (inclusive so that both vectors
// are sampled)
if let Some(dir) = Lerp::lerp(
-Vec3::unit_z(),
Vec3::new(bearing.x, bearing.y, 0.0),
i as f32 / NUM_RAYS as f32,
)
.try_normalized()
{
ground_too_close |= read_data
.terrain
.ray(self.pos.0, self.pos.0 + magnitude * dir)
.until(|b: &Block| b.is_solid() || b.is_liquid())
.cast()
.1
.map_or(false, |b| b.is_some())
}
}
if obstacle_ahead || ground_too_close {
5.0 //fly up when approaching obstacles
2021-02-07 07:22:06 +00:00
} else {
-2.0
2021-02-07 07:22:06 +00:00
} //flying things should slowly come down from the stratosphere
} else {
0.05 //normal land traveller offset
};
2021-05-30 15:38:47 +00:00
if let Some(pid) = agent.position_pid_controller.as_mut() {
pid.sp = self.pos.0.z + height_offset * Vec3::unit_z();
controller.inputs.move_z = pid.calc_err();
} else {
controller.inputs.move_z = height_offset;
}
2021-02-07 07:22:06 +00:00
// Put away weapon
2022-01-18 03:02:43 +00:00
if rng.gen_bool(0.1)
2021-02-07 07:22:06 +00:00
&& matches!(
read_data.char_states.get(*self.entity),
2021-10-05 00:15:58 +00:00
Some(CharacterState::Wielding(_))
2021-02-07 07:22:06 +00:00
)
{
2022-01-26 18:52:19 +00:00
controller.push_action(ControlAction::Unwield);
2021-02-07 07:22:06 +00:00
}
}
} else {
2022-01-18 03:02:43 +00:00
agent.bearing += Vec2::new(rng.gen::<f32>() - 0.5, rng.gen::<f32>() - 0.5) * 0.1
2021-02-07 07:22:06 +00:00
- 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
// or about to walk off a cliff
// NOTE: costs 1 us (imbris) <- before cliff raycast added
2021-02-07 07:22:06 +00:00
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())
&& read_data
.terrain
.ray(
self.pos.0
+ Vec3::from(agent.bearing)
.try_normalized()
.unwrap_or_else(Vec3::unit_y),
self.pos.0
+ Vec3::from(agent.bearing)
.try_normalized()
.unwrap_or_else(Vec3::unit_y)
- Vec3::unit_z() * 4.0,
)
.until(Block::is_solid)
.cast()
.0
< 3.0
2021-02-07 07:22:06 +00:00
{
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
2022-01-18 03:02:43 +00:00
if rng.gen_bool(0.1)
2021-02-07 07:22:06 +00:00
&& matches!(
read_data.char_states.get(*self.entity),
2021-10-05 00:15:58 +00:00
Some(CharacterState::Wielding(_))
2021-02-07 07:22:06 +00:00
)
{
2022-01-26 18:52:19 +00:00
controller.push_action(ControlAction::Unwield);
2021-02-07 07:22:06 +00:00
}
2022-01-18 03:02:43 +00:00
if rng.gen::<f32>() < 0.0015 {
2022-01-26 20:12:19 +00:00
controller.push_utterance(UtteranceKind::Calm);
}
2021-02-07 07:22:06 +00:00
// Sit
2022-01-18 03:02:43 +00:00
if rng.gen::<f32>() < 0.0035 {
2022-01-26 18:52:19 +00:00
controller.push_action(ControlAction::Sit);
2021-02-07 07:22:06 +00:00
}
}
}
2022-01-18 03:02:43 +00:00
pub 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);
self.jump_if(bearing.z > 1.5, controller);
2022-01-18 03:02:43 +00:00
controller.inputs.move_z = bearing.z;
}
}
2021-03-29 14:47:42 +00:00
fn look_toward(
&self,
controller: &mut Controller,
read_data: &ReadData,
target: EcsEntity,
2021-03-29 14:47:42 +00:00
) -> bool {
if let Some(tgt_pos) = read_data.positions.get(target) {
2021-03-29 14:47:42 +00:00
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());
2021-03-29 14:47:42 +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;
}
true
} else {
false
}
}
fn menacing(
&self,
agent: &mut Agent,
controller: &mut Controller,
target: EcsEntity,
read_data: &ReadData,
2021-10-22 22:11:09 +00:00
event_emitter: &mut Emitter<ServerEvent>,
2022-01-18 03:02:43 +00:00
rng: &mut impl Rng,
) {
2021-10-22 22:11:09 +00:00
let max_move = 0.5;
let move_dir = controller.inputs.move_dir;
let move_dir_mag = move_dir.magnitude();
2022-01-18 03:02:43 +00:00
let small_chance = rng.gen::<f32>() < read_data.dt.0 * 0.25;
let mut chat = |msg: &str| {
self.chat_npc_if_allowed_to_speak(msg.to_string(), agent, event_emitter);
};
let mut chat_villager_remembers_fighting = || {
let tgt_name = read_data.stats.get(target).map(|stats| stats.name.clone());
if let Some(tgt_name) = tgt_name {
chat(format!("{}! How dare you cross me again!", &tgt_name).as_str());
} else {
chat("You! How dare you cross me again!");
}
};
2021-10-22 22:11:09 +00:00
self.look_toward(controller, read_data, target);
2022-01-26 18:52:19 +00:00
controller.push_action(ControlAction::Wield);
if move_dir_mag > max_move {
2021-10-22 22:11:09 +00:00
controller.inputs.move_dir = max_move * move_dir / move_dir_mag;
}
if small_chance {
2022-01-26 20:12:19 +00:00
controller.push_utterance(UtteranceKind::Angry);
if is_villager(self.alignment) {
if self.remembers_fight_with(target, read_data) {
chat_villager_remembers_fighting();
2022-04-17 18:50:50 +00:00
} else if is_dressed_as_cultist(target, read_data) {
2022-08-06 17:47:45 +00:00
chat("npc-speech-villager_cultist_alarm");
} else {
2022-08-06 17:47:45 +00:00
chat("npc-speech-menacing");
}
} else {
2022-08-06 17:47:45 +00:00
chat("npc-speech-menacing");
}
}
// Remember target.
self.rtsim_entity.is_some().then(|| {
read_data
.stats
.get(target)
.map(|stats| agent.add_fight_to_memory(&stats.name, read_data.time.0))
});
}
fn flee(
2021-02-07 07:22:06 +00:00
&self,
agent: &mut Agent,
controller: &mut Controller,
tgt_pos: &Pos,
2021-02-07 07:22:06 +00:00
terrain: &TerrainGrid,
) {
if let Some(body) = self.body {
if body.can_strafe() && !self.is_gliding {
2022-01-26 18:52:19 +00:00
controller.push_action(ControlAction::Unwield);
2021-02-07 07:22:06 +00:00
}
}
2021-02-07 07:22:06 +00:00
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)
2021-02-07 07:22:06 +00:00
.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;
self.jump_if(bearing.z > 1.5, controller);
2021-02-07 07:22:06 +00:00
controller.inputs.move_z = bearing.z;
}
}
2021-04-11 23:47:29 +00:00
/// Attempt to consume a healing item, and return whether any healing items
/// were queued. Callers should use this to implement a delay so that
/// the healing isn't interrupted. If `relaxed` is `true`, we allow eating
/// food and prioritise healing.
2022-01-15 22:44:16 +00:00
fn heal_self(&self, _agent: &mut Agent, controller: &mut Controller, relaxed: bool) -> bool {
2021-04-11 23:47:29 +00:00
let healing_value = |item: &Item| {
let mut value = 0.0;
2021-07-14 12:23:51 +00:00
if let ItemKind::Consumable { kind, effects, .. } = &*item.kind() {
if matches!(kind, ConsumableKind::Drink)
|| (relaxed && matches!(kind, ConsumableKind::Food))
{
2022-01-15 22:44:16 +00:00
for effect in effects.iter() {
use BuffKind::*;
match effect {
Effect::Health(HealthChange { amount, .. }) => {
value += *amount;
},
Effect::Buff(BuffEffect { kind, data, .. })
if matches!(kind, Regeneration | Saturation | Potion) =>
{
value += data.strength
* data.duration.map_or(0.0, |d| d.as_secs() as f32);
2022-01-15 22:44:16 +00:00
},
_ => {},
}
2021-04-11 23:47:29 +00:00
}
2021-07-14 12:23:51 +00:00
}
2021-04-11 23:47:29 +00:00
}
value as i32
2021-04-11 23:47:29 +00:00
};
2022-01-16 21:13:13 +00:00
let item = self
2021-04-11 23:47:29 +00:00
.inventory
.slots_with_id()
.filter_map(|(id, slot)| match slot {
Some(item) if healing_value(item) > 0 => Some((id, item)),
_ => None,
})
2022-01-16 21:13:13 +00:00
.max_by_key(|(_, item)| {
if relaxed {
-healing_value(item)
} else {
healing_value(item)
}
});
2021-04-11 23:47:29 +00:00
2022-01-16 21:13:13 +00:00
if let Some((id, _)) = item {
2021-04-11 23:47:29 +00:00
use comp::inventory::slot::Slot;
2022-01-26 18:52:19 +00:00
controller.push_action(ControlAction::InventoryAction(InventoryAction::Use(
Slot::Inventory(id),
)));
2021-04-11 23:47:29 +00:00
true
} else {
false
}
}
fn choose_target(
&self,
agent: &mut Agent,
controller: &mut Controller,
read_data: &ReadData,
event_emitter: &mut Emitter<ServerEvent>,
) {
2021-04-28 22:41:04 +00:00
agent.action_state.timer = 0.0;
let mut aggro_on = false;
2021-02-07 07:22:06 +00:00
// Search the area.
// TODO: choose target by more than just distance
let common::CachedSpatialGrid(grid) = self.cached_spatial_grid;
let entities_nearby = grid
.in_circle_aabr(self.pos.0.xy(), agent.psyche.search_dist())
.collect_vec();
let can_ambush = |entity: EcsEntity, read_data: &ReadData| {
let self_different_from_entity = || {
read_data
.uids
.get(entity)
.map_or(false, |eu| eu != self.uid)
};
if self.will_ambush()
&& self_different_from_entity()
&& !self.passive_towards(entity, read_data)
{
let surrounding_humanoids = entities_nearby
.iter()
.filter(|e| read_data.bodies.get(**e).map_or(false, |b| b.is_humanoid()))
.collect_vec();
surrounding_humanoids.len() == 2
&& surrounding_humanoids.iter().any(|e| **e == entity)
} else {
false
}
};
let get_pos = |entity| read_data.positions.get(entity);
2022-05-19 22:06:44 +00:00
let get_enemy = |(entity, attack_target): (EcsEntity, bool)| {
if attack_target {
if self.is_enemy(entity, read_data) {
Some((entity, true))
} else if can_ambush(entity, read_data) {
controller.clone().push_utterance(UtteranceKind::Ambush);
self.chat_npc_if_allowed_to_speak(
"npc-speech-ambush".to_string(),
agent,
event_emitter,
);
aggro_on = true;
Some((entity, true))
2022-05-19 22:06:44 +00:00
} else if self.should_defend(entity, read_data) {
if let Some(attacker) = get_attacker(entity, read_data) {
if !self.passive_towards(attacker, read_data) {
// aggro_on: attack immediately, do not warn/menace.
aggro_on = true;
Some((attacker, true))
} else {
None
}
2021-10-24 05:31:49 +00:00
} else {
None
}
} else {
None
}
} else {
2022-05-19 22:06:44 +00:00
Some((entity, false))
}
};
2022-05-19 22:06:44 +00:00
let is_valid_target = |entity: EcsEntity| match read_data.bodies.get(entity) {
Some(Body::ItemDrop(item)) => {
2022-07-15 16:59:37 +00:00
//If the agent is humanoid, it will pick up all kinds of item drops. If the
// agent isn't humanoid, it will pick up only consumable item drops.
let wants_pickup = matches!(self.body, Some(Body::Humanoid(_)))
2022-06-07 07:25:27 +00:00
|| matches!(item, item_drop::Body::Consumable);
// The agent will attempt to pickup the item if it wants to pick it up and is
// allowed to
let attempt_pickup = wants_pickup
&& read_data
.loot_owners
.get(entity)
.map_or(true, |loot_owner| {
2022-06-04 17:16:12 +00:00
loot_owner.can_pickup(
*self.uid,
read_data.groups.get(entity),
self.alignment,
self.body,
None,
)
});
if attempt_pickup {
Some((entity, false))
} else {
None
}
},
2022-05-19 22:06:44 +00:00
_ => {
if read_data.healths.get(entity).map_or(false, |health| {
!health.is_dead && !is_invulnerable(entity, read_data)
}) {
Some((entity, true))
} else {
None
}
},
};
let can_sense_directly_near =
{ |e_pos: &Pos| e_pos.0.distance_squared(self.pos.0) < 5_f32.powi(2) };
let is_detected = |entity: EcsEntity, e_pos: &Pos| {
let chance = thread_rng().gen_bool(0.3);
(can_sense_directly_near(e_pos) && chance)
|| self.can_see_entity(agent, controller, entity, e_pos, read_data)
};
let target = entities_nearby
.iter()
.filter_map(|e| is_valid_target(*e))
.filter_map(get_enemy)
2022-05-19 22:06:44 +00:00
.filter_map(|(entity, attack_target)| {
get_pos(entity).map(|pos| (entity, pos, attack_target))
})
.filter(|(entity, e_pos, _)| is_detected(*entity, e_pos))
.min_by_key(|(_, e_pos, attack_target)| {
(
*attack_target,
(e_pos.0.distance_squared(self.pos.0) * 100.0) as i32,
)
})
.map(|(entity, _, attack_target)| (entity, attack_target));
2022-04-24 20:15:30 +00:00
if agent.target.is_none() && target.is_some() {
if aggro_on {
controller.push_utterance(UtteranceKind::Angry);
} else {
controller.push_utterance(UtteranceKind::Surprised);
}
2021-06-15 16:15:58 +00:00
}
2022-05-19 22:06:44 +00:00
agent.target = target.map(|(entity, attack_target)| Target {
target: entity,
hostile: attack_target,
selected_at: read_data.time.0,
aggro_on,
2022-05-19 22:06:44 +00:00
})
2021-02-07 07:22:06 +00:00
}
2021-02-07 07:22:06 +00:00
fn attack(
&self,
agent: &mut Agent,
controller: &mut Controller,
tgt_data: &TargetData,
read_data: &ReadData,
2022-01-18 03:02:43 +00:00
rng: &mut impl Rng,
2021-02-07 07:22:06 +00:00
) {
let tool_tactic = |tool_kind| match tool_kind {
ToolKind::Bow => Tactic::Bow,
ToolKind::Staff => Tactic::Staff,
ToolKind::Sceptre => Tactic::Sceptre,
ToolKind::Hammer => Tactic::Hammer,
2022-02-17 05:58:25 +00:00
ToolKind::Sword | ToolKind::Blowgun => Tactic::Sword,
ToolKind::Axe => Tactic::Axe,
2022-01-21 23:28:15 +00:00
_ => Tactic::SimpleMelee,
};
let tactic = self
2021-02-07 07:22:06 +00:00
.inventory
2021-05-09 21:18:36 +00:00
.equipped(EquipSlot::ActiveMainhand)
2021-02-07 07:22:06 +00:00
.as_ref()
.map(|item| {
2021-05-01 13:25:38 +00:00
if let Some(ability_spec) = item.ability_spec() {
match &*ability_spec {
AbilitySpec::Custom(spec) => match spec.as_str() {
2022-01-20 01:15:50 +00:00
"Oni" | "Sword Simple" => Tactic::Sword,
"Staff Simple" => Tactic::Staff,
"Bow Simple" => Tactic::Bow,
"Stone Golem" => Tactic::StoneGolem,
"Quad Med Quick" => Tactic::CircleCharge {
radius: 3,
circle_time: 2,
},
"Quad Med Jump" => Tactic::QuadMedJump,
"Quad Med Charge" => Tactic::CircleCharge {
2021-06-23 20:15:05 +00:00
radius: 6,
circle_time: 1,
},
"Quad Med Basic" => Tactic::QuadMedBasic,
2021-06-15 05:43:49 +00:00
"Asp" | "Maneater" => Tactic::QuadLowRanged,
"Quad Low Breathe" | "Quad Low Beam" | "Basilisk" => {
Tactic::QuadLowBeam
},
2022-08-24 21:50:53 +00:00
"Organ" => Tactic::OrganAura,
2021-06-23 20:15:05 +00:00
"Quad Low Tail" | "Husk Brute" => Tactic::TailSlap,
"Quad Low Quick" => Tactic::QuadLowQuick,
"Quad Low Basic" => Tactic::QuadLowBasic,
"Theropod Basic" | "Theropod Bird" => Tactic::Theropod,
2022-04-23 14:54:01 +00:00
// Arthropods
"Antlion" => Tactic::ArthropodMelee,
"Tarantula" | "Horn Beetle" => Tactic::ArthropodAmbush,
"Weevil" | "Black Widow" => Tactic::ArthropodRanged,
"Theropod Charge" => Tactic::CircleCharge {
radius: 6,
circle_time: 1,
},
"Turret" => Tactic::Turret,
2021-05-06 19:41:21 +00:00
"Haniwa Sentry" => Tactic::RotatingTurret,
"Bird Large Breathe" => Tactic::BirdLargeBreathe,
"Bird Large Fire" => Tactic::BirdLargeFire,
2021-05-26 00:37:31 +00:00
"Bird Large Basic" => Tactic::BirdLargeBasic,
"Mindflayer" => Tactic::Mindflayer,
2021-04-28 02:33:24 +00:00
"Minotaur" => Tactic::Minotaur,
2021-05-06 16:18:10 +00:00
"Clay Golem" => Tactic::ClayGolem,
2021-05-26 02:20:27 +00:00
"Tidal Warrior" => Tactic::TidalWarrior,
2022-02-09 01:23:23 +00:00
"Tidal Totem"
| "Tornado"
| "Gnarling Totem Red"
| "Gnarling Totem Green"
| "Gnarling Totem White" => Tactic::RadialTurret,
2021-06-05 18:25:47 +00:00
"Yeti" => Tactic::Yeti,
2021-06-20 17:04:06 +00:00
"Harvester" => Tactic::Harvester,
2022-08-24 21:50:53 +00:00
"Cardinal" => Tactic::Cardinal,
"Dagon" => Tactic::Dagon,
2022-01-21 23:28:15 +00:00
"Gnarling Dagger" => Tactic::SimpleBackstab,
"Gnarling Blowgun" => Tactic::ElevatedRanged,
2022-01-30 23:04:09 +00:00
"Deadwood" => Tactic::Deadwood,
2022-02-02 05:17:06 +00:00
"Mandragora" => Tactic::Mandragora,
2022-02-03 01:45:43 +00:00
"Wood Golem" => Tactic::WoodGolem,
2022-02-09 01:23:23 +00:00
"Gnarling Chieftain" => Tactic::GnarlingChieftain,
2022-01-21 23:28:15 +00:00
_ => Tactic::SimpleMelee,
},
AbilitySpec::Tool(tool_kind) => tool_tactic(*tool_kind),
}
} else if let ItemKind::Tool(tool) = &*item.kind() {
tool_tactic(tool.kind)
2021-02-07 07:22:06 +00:00
} else {
2022-01-21 23:28:15 +00:00
Tactic::SimpleMelee
}
})
2022-01-21 23:28:15 +00:00
.unwrap_or(Tactic::SimpleMelee);
2020-01-25 18:49:47 +00:00
2021-02-07 07:22:06 +00:00
// Wield the weapon as running towards the target
2022-01-26 18:52:19 +00:00
controller.push_action(ControlAction::Wield);
2020-07-30 19:26:49 +00:00
let min_attack_dist = (self.body.map_or(0.5, |b| b.max_radius()) + DEFAULT_ATTACK_RANGE)
* self.scale
+ tgt_data.body.map_or(0.5, |b| b.max_radius()) * tgt_data.scale.map_or(1.0, |s| s.0);
let dist_sqrd = self.pos.0.distance_squared(tgt_data.pos.0);
let angle = self
.ori
.look_vec()
.angle_between(tgt_data.pos.0 - self.pos.0)
.to_degrees();
2022-01-21 23:28:15 +00:00
let angle_xy = self
.ori
.look_vec()
.xy()
.angle_between((tgt_data.pos.0 - self.pos.0).xy())
.to_degrees();
2021-02-07 07:22:06 +00:00
let eye_offset = self.body.map_or(0.0, |b| b.eye_height());
let tgt_eye_height = tgt_data.body.map_or(0.0, |b| b.eye_height());
let tgt_eye_offset = tgt_eye_height +
2021-02-07 07:22:06 +00:00
// 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-08-04 23:13:31 +00:00
// FIXME:
// 1) Retrieve actual projectile speed!
// We have to assume projectiles are faster than base speed because there are
// skills that increase it, and in most cases this will cause agents to
// overshoot
2021-08-04 23:13:31 +00:00
//
// 2) We use eye_offset-s which isn't actually ideal.
// Some attacks (beam for example) may use different offsets,
// we should probably use offsets from corresponding states.
//
// 3) Should we even have this big switch?
// Not all attacks may want their direction overwritten.
// And this is quite hard to debug when you don't see it in actual
// attack handler.
2021-09-05 22:07:09 +00:00
if let Some(dir) = match self.char_state {
CharacterState::ChargedRanged(c) if dist_sqrd > 0.0 => {
let charge_factor =
c.timer.as_secs_f32() / c.static_data.charge_duration.as_secs_f32();
let projectile_speed = c.static_data.initial_projectile_speed
+ charge_factor * c.static_data.scaled_projectile_speed;
aim_projectile(
projectile_speed,
self.pos.0
+ self.body.map_or(Vec3::zero(), |body| {
body.projectile_offsets(self.ori.look_vec())
}),
Vec3::new(
tgt_data.pos.0.x,
tgt_data.pos.0.y,
tgt_data.pos.0.z + tgt_eye_offset,
),
)
},
2021-09-05 22:07:09 +00:00
CharacterState::BasicRanged(c) => {
let offset_z = match c.static_data.projectile {
// Aim fireballs at feet instead of eyes for splash damage
ProjectileConstructor::Fireball {
damage: _,
radius: _,
energy_regen: _,
2021-10-29 21:12:57 +00:00
min_falloff: _,
} => 0.0,
_ => tgt_eye_offset,
};
2021-09-05 22:07:09 +00:00
let projectile_speed = c.static_data.projectile_speed;
2021-06-20 17:04:06 +00:00
aim_projectile(
2021-09-05 22:07:09 +00:00
projectile_speed,
self.pos.0
+ self.body.map_or(Vec3::zero(), |body| {
body.projectile_offsets(self.ori.look_vec())
}),
2021-06-20 17:04:06 +00:00
Vec3::new(
tgt_data.pos.0.x,
tgt_data.pos.0.y,
tgt_data.pos.0.z + offset_z,
2021-06-20 17:04:06 +00:00
),
)
},
CharacterState::RepeaterRanged(c) => {
let projectile_speed = c.static_data.projectile_speed;
aim_projectile(
projectile_speed,
self.pos.0
+ self.body.map_or(Vec3::zero(), |body| {
body.projectile_offsets(self.ori.look_vec())
}),
Vec3::new(
tgt_data.pos.0.x,
tgt_data.pos.0.y,
tgt_data.pos.0.z + tgt_eye_offset,
),
)
},
CharacterState::LeapMelee(_) if matches!(tactic, Tactic::Hammer | Tactic::Axe) => {
2021-09-05 22:07:09 +00:00
let direction_weight = match tactic {
Tactic::Hammer => 0.1,
Tactic::Axe => 0.3,
2021-09-05 23:13:20 +00:00
_ => unreachable!("Direction weight called on incorrect tactic."),
2021-09-05 22:07:09 +00:00
};
let tgt_pos = tgt_data.pos.0;
let self_pos = self.pos.0;
let delta_x = (tgt_pos.x - self_pos.x) * direction_weight;
let delta_y = (tgt_pos.y - self_pos.y) * direction_weight;
Dir::from_unnormalized(Vec3::new(delta_x, delta_y, -1.0))
},
2021-09-05 22:07:09 +00:00
CharacterState::BasicBeam(_) => {
let aim_from = self.body.map_or(self.pos.0, |body| {
self.pos.0
+ basic_beam::beam_offsets(
body,
controller.inputs.look_dir,
self.ori.look_vec(),
2021-09-15 23:25:00 +00:00
// Try to match animation by getting some context
self.vel.0 - self.physics_state.ground_vel,
self.physics_state.on_ground,
2021-09-05 22:07:09 +00:00
)
});
2021-08-07 21:18:49 +00:00
let aim_to = Vec3::new(
tgt_data.pos.0.x,
tgt_data.pos.0.y,
tgt_data.pos.0.z + tgt_eye_offset,
2021-08-07 21:18:49 +00:00
);
Dir::from_unnormalized(aim_to - aim_from)
},
2021-09-05 22:07:09 +00:00
_ => {
let aim_from = Vec3::new(self.pos.0.x, self.pos.0.y, self.pos.0.z + eye_offset);
let aim_to = Vec3::new(
tgt_data.pos.0.x,
tgt_data.pos.0.y,
tgt_data.pos.0.z + tgt_eye_offset,
);
Dir::from_unnormalized(aim_to - aim_from)
},
} {
2021-02-07 07:22:06 +00:00
controller.inputs.look_dir = dir;
}
let attack_data = AttackData {
min_attack_dist,
dist_sqrd,
angle,
2022-01-21 23:28:15 +00:00
angle_xy,
};
// Match on tactic. Each tactic has different controls depending on the distance
// from the agent to the target.
2021-02-07 07:22:06 +00:00
match tactic {
2022-01-21 23:28:15 +00:00
Tactic::SimpleMelee => {
self.handle_simple_melee(agent, controller, &attack_data, tgt_data, read_data, rng)
2021-02-07 07:22:06 +00:00
},
Tactic::Axe => {
2022-01-18 03:02:43 +00:00
self.handle_axe_attack(agent, controller, &attack_data, tgt_data, read_data, rng)
2021-02-07 07:22:06 +00:00
},
Tactic::Hammer => {
2022-01-18 03:02:43 +00:00
self.handle_hammer_attack(agent, controller, &attack_data, tgt_data, read_data, rng)
2021-02-07 07:22:06 +00:00
},
Tactic::Sword => {
2022-01-18 03:02:43 +00:00
self.handle_sword_attack(agent, controller, &attack_data, tgt_data, read_data, rng)
},
Tactic::Bow => {
2022-01-18 03:02:43 +00:00
self.handle_bow_attack(agent, controller, &attack_data, tgt_data, read_data, rng)
},
Tactic::Staff => {
2022-01-18 03:02:43 +00:00
self.handle_staff_attack(agent, controller, &attack_data, tgt_data, read_data, rng)
},
2022-01-18 03:02:43 +00:00
Tactic::Sceptre => self.handle_sceptre_attack(
agent,
controller,
&attack_data,
tgt_data,
read_data,
rng,
),
Tactic::StoneGolem => {
self.handle_stone_golem_attack(agent, controller, &attack_data, tgt_data, read_data)
},
Tactic::CircleCharge {
radius,
circle_time,
} => self.handle_circle_charge_attack(
agent,
controller,
&attack_data,
2021-07-11 18:41:52 +00:00
tgt_data,
read_data,
radius,
circle_time,
2022-01-18 03:02:43 +00:00
rng,
),
Tactic::QuadLowRanged => self.handle_quadlow_ranged_attack(
agent,
controller,
&attack_data,
2021-07-11 18:41:52 +00:00
tgt_data,
read_data,
),
Tactic::TailSlap => {
2021-07-11 18:41:52 +00:00
self.handle_tail_slap_attack(agent, controller, &attack_data, tgt_data, read_data)
},
Tactic::QuadLowQuick => self.handle_quadlow_quick_attack(
agent,
controller,
&attack_data,
2021-07-11 18:41:52 +00:00
tgt_data,
read_data,
),
Tactic::QuadLowBasic => self.handle_quadlow_basic_attack(
agent,
controller,
&attack_data,
2021-07-11 18:41:52 +00:00
tgt_data,
read_data,
),
Tactic::QuadMedJump => self.handle_quadmed_jump_attack(
agent,
controller,
&attack_data,
2021-07-11 18:41:52 +00:00
tgt_data,
read_data,
),
Tactic::QuadMedBasic => self.handle_quadmed_basic_attack(
agent,
controller,
&attack_data,
2021-07-11 18:41:52 +00:00
tgt_data,
read_data,
),
Tactic::QuadLowBeam => self.handle_quadlow_beam_attack(
agent,
controller,
&attack_data,
2021-07-11 18:41:52 +00:00
tgt_data,
read_data,
),
2022-08-24 21:50:53 +00:00
Tactic::OrganAura => {
self.handle_organ_aura_attack(agent, controller, &attack_data, tgt_data, read_data)
},
Tactic::Theropod => {
2021-07-11 18:41:52 +00:00
self.handle_theropod_attack(agent, controller, &attack_data, tgt_data, read_data)
},
2022-04-23 14:54:01 +00:00
Tactic::ArthropodMelee => self.handle_arthropod_melee_attack(
2021-09-06 23:43:10 +00:00
agent,
controller,
&attack_data,
tgt_data,
read_data,
),
2022-04-23 14:54:01 +00:00
Tactic::ArthropodAmbush => self.handle_arthropod_ambush_attack(
agent,
controller,
&attack_data,
tgt_data,
read_data,
rng,
),
Tactic::ArthropodRanged => self.handle_arthropod_ranged_attack(
agent,
controller,
&attack_data,
tgt_data,
read_data,
),
Tactic::Turret => {
self.handle_turret_attack(agent, controller, &attack_data, tgt_data, read_data)
},
Tactic::FixedTurret => self.handle_fixed_turret_attack(
agent,
controller,
&attack_data,
tgt_data,
read_data,
),
Tactic::RotatingTurret => {
self.handle_rotating_turret_attack(agent, controller, tgt_data, read_data)
},
2022-01-18 03:02:43 +00:00
Tactic::Mindflayer => self.handle_mindflayer_attack(
agent,
controller,
&attack_data,
tgt_data,
read_data,
rng,
),
Tactic::BirdLargeFire => self.handle_birdlarge_fire_attack(
agent,
controller,
&attack_data,
2021-07-11 18:41:52 +00:00
tgt_data,
read_data,
2022-01-18 03:02:43 +00:00
rng,
),
// Mostly identical to BirdLargeFire but tweaked for flamethrower instead of shockwave
Tactic::BirdLargeBreathe => self.handle_birdlarge_breathe_attack(
agent,
controller,
&attack_data,
2021-07-11 18:41:52 +00:00
tgt_data,
read_data,
2022-01-18 03:02:43 +00:00
rng,
),
2021-05-26 00:37:31 +00:00
Tactic::BirdLargeBasic => self.handle_birdlarge_basic_attack(
agent,
controller,
&attack_data,
2021-07-11 18:41:52 +00:00
tgt_data,
read_data,
2021-05-26 00:37:31 +00:00
),
Tactic::Minotaur => {
2021-07-11 18:41:52 +00:00
self.handle_minotaur_attack(agent, controller, &attack_data, tgt_data, read_data)
},
Tactic::ClayGolem => {
self.handle_clay_golem_attack(agent, controller, &attack_data, tgt_data, read_data)
},
2021-05-26 02:20:27 +00:00
Tactic::TidalWarrior => self.handle_tidal_warrior_attack(
agent,
controller,
&attack_data,
2021-07-11 18:41:52 +00:00
tgt_data,
read_data,
2021-05-26 02:20:27 +00:00
),
Tactic::RadialTurret => self.handle_radial_turret_attack(controller),
2021-06-05 18:25:47 +00:00
Tactic::Yeti => {
2021-07-11 18:41:52 +00:00
self.handle_yeti_attack(agent, controller, &attack_data, tgt_data, read_data)
2021-06-05 18:25:47 +00:00
},
2021-06-20 17:04:06 +00:00
Tactic::Harvester => {
2021-07-11 18:41:52 +00:00
self.handle_harvester_attack(agent, controller, &attack_data, tgt_data, read_data)
2022-08-24 21:50:53 +00:00
},
Tactic::Cardinal => self.handle_cardinal_attack(
agent,
controller,
&attack_data,
tgt_data,
read_data,
rng,
),
Tactic::Dagon => {
self.handle_dagon_attack(agent, controller, &attack_data, tgt_data, read_data)
2021-06-20 17:04:06 +00:00
},
2022-01-21 23:28:15 +00:00
Tactic::SimpleBackstab => {
self.handle_simple_backstab(agent, controller, &attack_data, tgt_data, read_data)
},
Tactic::ElevatedRanged => {
self.handle_elevated_ranged(agent, controller, &attack_data, tgt_data, read_data)
},
2022-01-30 23:04:09 +00:00
Tactic::Deadwood => {
self.handle_deadwood(agent, controller, &attack_data, tgt_data, read_data)
},
2022-02-02 05:17:06 +00:00
Tactic::Mandragora => {
self.handle_mandragora(agent, controller, &attack_data, tgt_data, read_data)
},
2022-02-03 01:45:43 +00:00
Tactic::WoodGolem => {
self.handle_wood_golem(agent, controller, &attack_data, tgt_data, read_data)
},
2022-02-09 01:23:23 +00:00
Tactic::GnarlingChieftain => self.handle_gnarling_chieftain(
agent,
controller,
&attack_data,
tgt_data,
read_data,
rng,
),
}
}
fn handle_sounds_heard(
&self,
agent: &mut Agent,
controller: &mut Controller,
read_data: &ReadData,
2022-01-18 03:02:43 +00:00
rng: &mut impl Rng,
) {
agent.forget_old_sounds(read_data.time.0);
2022-01-18 03:02:43 +00:00
if is_invulnerable(*self.entity, read_data) {
self.idle(agent, controller, read_data, rng);
return;
}
2022-01-18 03:02:43 +00:00
if let Some(sound) = agent.sounds_heard.last() {
let sound_pos = Pos(sound.pos);
let dist_sqrd = self.pos.0.distance_squared(sound_pos.0);
// NOTE: There is an implicit distance requirement given that sound volume
2022-07-15 16:59:37 +00:00
// dissipates as it travels, but we will not want to flee if a sound is super
// loud but heard from a great distance, regardless of how loud it was.
// `is_close` is this limiter.
let is_close = dist_sqrd < 35.0_f32.powi(2);
let sound_was_loud = sound.vol >= 10.0;
let sound_was_threatening = sound_was_loud
|| matches!(sound.kind, SoundKind::Utterance(UtteranceKind::Scream, _));
let has_enemy_alignment = matches!(self.alignment, Some(Alignment::Enemy));
// FIXME: We need to be able to change the name of a guard without breaking this
// logic. The `Mark` enum from common::agent could be used to match with
// `agent::Mark::Guard`
let is_village_guard = read_data
.stats
.get(*self.entity)
.map_or(false, |stats| stats.name == *"Guard".to_string());
let follows_threatening_sounds = has_enemy_alignment || is_village_guard;
// TODO: Awareness currently doesn't influence anything.
//agent.awareness += 0.5 * sound.vol;
if sound_was_threatening && is_close {
if !self.below_flee_health(agent) && follows_threatening_sounds {
2022-01-18 03:02:43 +00:00
self.follow(agent, controller, &read_data.terrain, &sound_pos);
} else if self.below_flee_health(agent) || !follows_threatening_sounds {
self.flee(agent, controller, &sound_pos, &read_data.terrain);
2022-01-18 03:02:43 +00:00
} else {
self.idle(agent, controller, read_data, rng);
}
} else {
self.idle(agent, controller, read_data, rng);
}
} else {
self.idle(agent, controller, read_data, rng);
}
}
2022-01-18 03:02:43 +00:00
fn attack_target_attacker(
&self,
agent: &mut Agent,
read_data: &ReadData,
2022-01-18 03:02:43 +00:00
controller: &mut Controller,
rng: &mut impl Rng,
) {
2022-01-18 03:02:43 +00:00
if let Some(Target { target, .. }) = agent.target {
if let Some(tgt_health) = read_data.healths.get(target) {
if let Some(by) = tgt_health.last_change.damage_by() {
if let Some(attacker) = get_entity_by_id(by.uid().0, read_data) {
if agent.target.is_none() {
2022-01-26 20:12:19 +00:00
controller.push_utterance(UtteranceKind::Angry);
2022-01-18 03:02:43 +00:00
}
2022-01-18 03:02:43 +00:00
agent.target = Some(Target::new(attacker, true, read_data.time.0, true));
2022-01-18 03:02:43 +00:00
if let Some(tgt_pos) = read_data.positions.get(attacker) {
if is_dead_or_invulnerable(attacker, read_data) {
// FIXME?: Shouldn't target be set to `None`?
// If is dead, then probably. If invulnerable, maybe not.
2022-01-18 03:02:43 +00:00
agent.target =
Some(Target::new(target, false, read_data.time.0, false));
2022-01-18 03:02:43 +00:00
self.idle(agent, controller, read_data, rng);
} else {
let target_data = TargetData::new(
tgt_pos,
read_data.bodies.get(target),
read_data.scales.get(target),
);
if let Some(tgt_name) =
read_data.stats.get(target).map(|stats| stats.name.clone())
{
agent.add_fight_to_memory(&tgt_name, read_data.time.0)
}
2022-01-18 03:02:43 +00:00
self.attack(agent, controller, &target_data, read_data, rng);
}
}
}
}
}
}
}
2022-01-18 03:02:43 +00:00
/// Directs the entity to path and move toward the target
/// If path is not Full, the entity will path to a location 50 units along
2022-01-18 03:02:43 +00:00
/// the vector between the entity and the target. The speed multiplier
/// multiplies the movement speed by a value less than 1.0.
/// A `None` value implies a multiplier of 1.0.
/// Returns `false` if the pathfinding algorithm fails to return a path
fn path_toward_target(
&self,
agent: &mut Agent,
controller: &mut Controller,
tgt_pos: Vec3<f32>,
read_data: &ReadData,
path: Path,
2022-01-18 03:02:43 +00:00
speed_multiplier: Option<f32>,
) -> bool {
let partial_path_tgt_pos = |pos_difference: Vec3<f32>| {
2022-03-05 08:54:01 +00:00
self.pos.0
+ PARTIAL_PATH_DIST * pos_difference.try_normalized().unwrap_or_else(Vec3::zero)
2022-02-25 20:44:43 +00:00
};
2022-03-05 08:54:01 +00:00
let pos_difference = tgt_pos - self.pos.0;
let pathing_pos = match path {
2022-03-01 21:40:43 +00:00
Path::Separate => {
let mut sep_vec: Vec3<f32> = Vec3::<f32>::zero();
for entity in read_data
.cached_spatial_grid
.0
2022-03-05 08:54:01 +00:00
.in_circle_aabr(self.pos.0.xy(), SEPARATION_DIST)
{
if let (Some(alignment), Some(other_alignment)) =
(self.alignment, read_data.alignments.get(entity))
{
if Alignment::passive_towards(*alignment, *other_alignment) {
if let (Some(pos), Some(body), Some(other_body)) = (
read_data.positions.get(entity),
self.body,
read_data.bodies.get(entity),
) {
let dist_xy = self.pos.0.xy().distance(pos.0.xy());
2022-02-25 20:48:29 +00:00
let spacing = body.spacing_radius() + other_body.spacing_radius();
if dist_xy < spacing {
let pos_diff = self.pos.0.xy() - pos.0.xy();
sep_vec += pos_diff.try_normalized().unwrap_or_else(Vec2::zero)
* ((spacing - dist_xy) / spacing);
}
}
}
}
}
partial_path_tgt_pos(
sep_vec * SEPARATION_BIAS + pos_difference * (1.0 - SEPARATION_BIAS),
2022-02-25 20:44:43 +00:00
)
},
2022-03-01 21:40:43 +00:00
Path::Full => tgt_pos,
Path::Partial => partial_path_tgt_pos(pos_difference),
};
let speed_multiplier = speed_multiplier.unwrap_or(1.0).min(1.0);
if let Some((bearing, speed)) = agent.chaser.chase(
&*read_data.terrain,
self.pos.0,
self.vel.0,
pathing_pos,
TraversalConfig {
min_tgt_dist: 0.25,
..self.traversal_config
},
) {
controller.inputs.move_dir =
bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed * speed_multiplier;
self.jump_if(bearing.z > 1.5, controller);
controller.inputs.move_z = bearing.z;
true
} else {
false
}
}
2021-10-22 22:11:09 +00:00
fn chat_npc_if_allowed_to_speak(
2021-07-14 07:40:43 +00:00
&self,
msg: impl ToString,
2021-10-22 22:11:09 +00:00
agent: &Agent,
2021-07-14 07:40:43 +00:00
event_emitter: &mut Emitter<'_, ServerEvent>,
) -> bool {
2022-01-18 03:02:43 +00:00
if agent.allowed_to_speak() {
2021-10-22 22:11:09 +00:00
self.chat_npc(msg, event_emitter);
2021-07-14 07:40:43 +00:00
true
} else {
false
}
}
fn jump_if(&self, condition: bool, controller: &mut Controller) {
if condition {
2022-01-26 19:09:59 +00:00
controller.push_basic_input(InputKind::Jump);
} else {
2022-01-26 19:15:40 +00:00
controller.push_cancel_input(InputKind::Jump)
}
}
2021-07-14 07:40:43 +00:00
2021-10-22 22:11:09 +00:00
fn chat_npc(&self, msg: impl ToString, event_emitter: &mut Emitter<'_, ServerEvent>) {
event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc(
*self.uid,
msg.to_string(),
)));
2021-07-14 07:40:43 +00:00
}
2021-10-16 16:11:57 +00:00
fn emit_scream(&self, time: f64, event_emitter: &mut Emitter<'_, ServerEvent>) {
2021-10-15 19:49:25 +00:00
if let Some(body) = self.body {
event_emitter.emit(ServerEvent::Sound {
2021-10-15 20:23:45 +00:00
sound: Sound::new(
SoundKind::Utterance(UtteranceKind::Scream, *body),
self.pos.0,
13.0,
2021-10-15 20:23:45 +00:00
time,
),
2021-10-15 19:49:25 +00:00
});
}
2021-07-14 07:40:43 +00:00
}
2021-10-22 22:11:09 +00:00
fn cry_out(
&self,
agent: &Agent,
event_emitter: &mut Emitter<'_, ServerEvent>,
read_data: &ReadData,
) {
let has_enemy_alignment = matches!(self.alignment, Some(Alignment::Enemy));
2021-10-22 22:11:09 +00:00
if has_enemy_alignment {
// FIXME: If going to use "cultist + low health + fleeing" string, make sure
// they are each true.
2021-10-22 22:11:09 +00:00
self.chat_npc_if_allowed_to_speak(
2022-08-06 17:47:45 +00:00
"npc-speech-cultist_low_health_fleeing",
2021-10-22 22:11:09 +00:00
agent,
event_emitter,
);
} else if is_villager(self.alignment) {
2021-10-22 22:11:09 +00:00
self.chat_npc_if_allowed_to_speak(
2022-08-06 17:47:45 +00:00
"npc-speech-villager_under_attack",
2021-10-22 22:11:09 +00:00
agent,
event_emitter,
);
self.emit_scream(read_data.time.0, event_emitter);
2021-10-22 22:11:09 +00:00
}
}
fn exclaim_relief_about_enemy_dead(
&self,
agent: &Agent,
event_emitter: &mut Emitter<'_, ServerEvent>,
) {
if is_villager(self.alignment) {
2021-10-22 22:11:09 +00:00
self.chat_npc_if_allowed_to_speak(
2022-08-06 17:47:45 +00:00
"npc-speech-villager_enemy_killed",
2021-10-22 22:11:09 +00:00
agent,
event_emitter,
);
}
}
fn below_flee_health(&self, agent: &Agent) -> bool {
self.damage.min(1.0) < agent.psyche.flee_health
}
2022-04-05 02:07:23 +00:00
fn is_more_dangerous_than_target(
2022-04-05 02:07:23 +00:00
&self,
entity: EcsEntity,
target: Target,
read_data: &ReadData,
) -> bool {
let entity_pos = read_data.positions.get(entity);
let target_pos = read_data.positions.get(target.target);
entity_pos.map_or(false, |entity_pos| {
target_pos.map_or(true, |target_pos| {
// Fuzzy factor that makes it harder for players to cheese enemies by making
// them quickly flip aggro between two players.
// It does this by only switching aggro if the entity is closer to the enemy by
// a specific proportional threshold.
const FUZZY_DIST_COMPARISON: f32 = 0.8;
let is_target_further = target_pos.0.distance(entity_pos.0)
< target_pos.0.distance(entity_pos.0) * FUZZY_DIST_COMPARISON;
let is_entity_hostile = read_data
.alignments
.get(entity)
.zip(self.alignment)
.map_or(false, |(entity, me)| me.hostile_towards(*entity));
// Consider entity more dangerous than target if entity is closer or if target
// had not triggered aggro.
!target.aggro_on || (is_target_further && is_entity_hostile)
})
})
}
fn remembers_fight_with(&self, other: EcsEntity, read_data: &ReadData) -> bool {
let name = || read_data.stats.get(other).map(|stats| stats.name.clone());
self.rtsim_entity.map_or(false, |rtsim_entity| {
name().map_or(false, |name| {
rtsim_entity.brain.remembers_fight_with_character(&name)
})
})
}
fn is_enemy(&self, entity: EcsEntity, read_data: &ReadData) -> bool {
let other_alignment = read_data.alignments.get(entity);
(entity != *self.entity)
&& !self.passive_towards(entity, read_data)
&& (are_our_owners_hostile(self.alignment, other_alignment, read_data)
|| self.remembers_fight_with(entity, read_data)
2022-04-24 20:15:30 +00:00
|| (is_villager(self.alignment) && is_dressed_as_cultist(entity, read_data)))
}
fn will_ambush(&self) -> bool {
self.health
.map_or(false, |h| h.current() / h.maximum() > 0.7)
&& self
.rtsim_entity
.map_or(false, |re| re.brain.personality.will_ambush)
}
2022-04-17 18:50:50 +00:00
fn should_defend(&self, entity: EcsEntity, read_data: &ReadData) -> bool {
let entity_alignment = read_data.alignments.get(entity);
let we_are_friendly = entity_alignment.map_or(false, |entity_alignment| {
self.alignment.map_or(false, |alignment| {
!alignment.hostile_towards(*entity_alignment)
})
});
let we_share_species = read_data.bodies.get(entity).map_or(false, |entity_body| {
self.body.map_or(false, |body| {
entity_body.is_same_species_as(body)
|| (entity_body.is_humanoid() && body.is_humanoid())
})
});
let self_owns_entity =
matches!(entity_alignment, Some(Alignment::Owned(ouid)) if *self.uid == *ouid);
(we_are_friendly && we_share_species)
2022-04-17 18:50:50 +00:00
|| (is_village_guard(*self.entity, read_data) && is_villager(entity_alignment))
|| self_owns_entity
}
fn passive_towards(&self, entity: EcsEntity, read_data: &ReadData) -> bool {
if let (Some(self_alignment), Some(other_alignment)) =
(self.alignment, read_data.alignments.get(entity))
{
self_alignment.passive_towards(*other_alignment)
} else {
false
}
}
fn can_see_entity(
&self,
agent: &Agent,
controller: &Controller,
other: EcsEntity,
other_pos: &Pos,
read_data: &ReadData,
) -> bool {
let other_stealth_multiplier = {
let other_inventory = read_data.inventories.get(other);
let other_char_state = read_data.char_states.get(other);
2022-05-28 23:41:31 +00:00
perception_dist_multiplier_from_stealth(other_inventory, other_char_state, self.msm)
};
let within_sight_dist = {
let sight_dist = agent.psyche.sight_dist * other_stealth_multiplier;
let dist_sqrd = other_pos.0.distance_squared(self.pos.0);
dist_sqrd < sight_dist.powi(2)
};
let within_fov = (other_pos.0 - self.pos.0)
.try_normalized()
.map_or(false, |v| v.dot(*controller.inputs.look_dir) > 0.15);
let other_body = read_data.bodies.get(other);
(within_sight_dist)
&& within_fov
&& entities_have_line_of_sight(self.pos, self.body, other_pos, other_body, read_data)
}
}