2022-01-18 03:02:43 +00:00
|
|
|
pub mod attack;
|
2022-07-29 10:13:00 +00:00
|
|
|
pub mod behavior_tree;
|
2022-01-18 03:02:43 +00:00
|
|
|
pub mod consts;
|
|
|
|
pub mod data;
|
|
|
|
pub mod util;
|
|
|
|
|
|
|
|
use crate::{
|
2022-07-29 16:33:34 +00:00
|
|
|
rtsim::RtSim,
|
2022-01-18 03:02:43 +00:00
|
|
|
sys::agent::{
|
2022-07-29 10:13:00 +00:00
|
|
|
behavior_tree::BehaviorTree,
|
2022-01-18 03:02:43 +00:00
|
|
|
consts::{
|
2022-07-29 16:33:34 +00:00
|
|
|
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
|
|
|
},
|
2022-02-25 20:35:35 +00:00
|
|
|
data::{AgentData, AttackData, Path, ReadData, Tactic, TargetData},
|
2022-01-18 03:02:43 +00:00
|
|
|
util::{
|
2022-04-28 00:29:10 +00:00
|
|
|
aim_projectile, are_our_owners_hostile, entities_have_line_of_sight, get_attacker,
|
2022-07-29 16:33:34 +00:00
|
|
|
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
|
|
|
},
|
|
|
|
},
|
|
|
|
};
|
2020-12-01 00:28:00 +00:00
|
|
|
use common::{
|
2022-05-04 13:24:19 +00:00
|
|
|
combat::perception_dist_multiplier_from_stealth,
|
2020-04-18 18:28:19 +00:00
|
|
|
comp::{
|
|
|
|
self,
|
2022-07-29 16:33:34 +00:00
|
|
|
agent::{Sound, SoundKind, Target},
|
2022-01-18 03:02:43 +00:00
|
|
|
buff::BuffKind,
|
2022-03-30 00:04:20 +00:00
|
|
|
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
|
|
|
},
|
2022-05-27 16:57:53 +00:00
|
|
|
item_drop,
|
2021-10-27 18:01:21 +00:00
|
|
|
projectile::ProjectileConstructor,
|
2022-07-29 16:33:34 +00:00
|
|
|
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,
|
2022-07-29 16:33:34 +00:00
|
|
|
rtsim::RtSimEvent,
|
2022-01-18 03:02:43 +00:00
|
|
|
states::basic_beam,
|
2020-09-26 13:55:01 +00:00
|
|
|
terrain::{Block, TerrainGrid},
|
2020-10-07 02:23:20 +00:00
|
|
|
time::DayPeriod,
|
2020-03-28 01:31:22 +00:00
|
|
|
util::Dir,
|
2020-01-27 10:02:36 +00:00
|
|
|
vol::ReadVol,
|
2020-01-24 21:24:57 +00:00
|
|
|
};
|
2021-03-13 06:48:30 +00:00
|
|
|
use common_base::prof_span;
|
2021-03-09 10:52:57 +00:00
|
|
|
use common_ecs::{Job, Origin, ParMode, Phase, System};
|
2020-01-27 15:51:07 +00:00
|
|
|
use rand::{thread_rng, Rng};
|
2020-11-25 22:47:16 +00:00
|
|
|
use rayon::iter::ParallelIterator;
|
2022-07-29 17:06:08 +00:00
|
|
|
use specs::{Entity as EcsEntity, Join, ParJoin, Read, WriteExpect, WriteStorage};
|
2019-04-16 21:06:33 +00:00
|
|
|
use vek::*;
|
|
|
|
|
2019-06-09 19:33:20 +00:00
|
|
|
/// This system will allow NPCs to modify their controller
|
2021-03-04 14:00:16 +00:00
|
|
|
#[derive(Default)]
|
2019-04-16 21:06:33 +00:00
|
|
|
pub struct Sys;
|
2021-03-08 11:13:59 +00:00
|
|
|
impl<'a> System<'a> for Sys {
|
2019-04-16 21:06:33 +00:00
|
|
|
type SystemData = (
|
2021-02-07 07:22:06 +00:00
|
|
|
ReadData<'a>,
|
2022-05-09 19:58:13 +00:00
|
|
|
Read<'a, EventBus<ServerEvent>>,
|
2019-08-02 20:25:33 +00:00
|
|
|
WriteStorage<'a, Agent>,
|
2019-06-09 14:20:20 +00:00
|
|
|
WriteStorage<'a, Controller>,
|
2021-03-16 01:30:35 +00:00
|
|
|
WriteExpect<'a, RtSim>,
|
2019-04-16 21:06:33 +00:00
|
|
|
);
|
|
|
|
|
2021-03-04 14:00:16 +00:00
|
|
|
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(
|
2021-03-09 10:52:57 +00:00
|
|
|
job: &mut Job<Self>,
|
2021-03-16 01:30:35 +00:00
|
|
|
(read_data, event_bus, mut agents, mut controllers, mut rtsim): Self::SystemData,
|
2019-08-04 08:21:29 +00:00
|
|
|
) {
|
2021-03-16 01:30:35 +00:00
|
|
|
let rtsim = &mut *rtsim;
|
2021-03-09 10:52:57 +00:00
|
|
|
job.cpu_stats.measure(ParMode::Rayon);
|
2021-07-12 15:40:29 +00:00
|
|
|
|
2020-11-25 22:47:16 +00:00
|
|
|
(
|
2021-02-07 07:22:06 +00:00
|
|
|
&read_data.entities,
|
2021-03-22 17:37:15 +00:00
|
|
|
(&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,
|
2019-09-09 19:11:40 +00:00
|
|
|
&mut agents,
|
|
|
|
&mut controllers,
|
2021-02-07 07:22:06 +00:00
|
|
|
read_data.light_emitter.maybe(),
|
|
|
|
read_data.groups.maybe(),
|
2022-01-15 18:22:28 +00:00
|
|
|
!&read_data.is_mounts,
|
2019-09-09 19:11:40 +00:00
|
|
|
)
|
2020-11-25 22:47:16 +00:00
|
|
|
.par_join()
|
2021-03-13 06:48:30 +00:00
|
|
|
.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,
|
2021-07-12 15:40:29 +00:00
|
|
|
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-07-29 16:33:34 +00:00
|
|
|
// let mut rng = thread_rng();
|
2022-01-18 03:02:43 +00:00
|
|
|
|
2021-07-12 15:40:29 +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(_))
|
|
|
|
) {
|
2021-07-12 15:40:29 +00:00
|
|
|
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()
|
2021-07-12 15:40:29 +00:00
|
|
|
.map_or_else(
|
|
|
|
|| read_data.alignments.get(entity).copied(),
|
|
|
|
|uid| Some(Alignment::Owned(uid)),
|
|
|
|
)
|
2021-02-07 07:22:06 +00:00
|
|
|
};
|
2019-09-09 19:11:40 +00:00
|
|
|
|
2021-07-14 01:40:56 +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
|
2021-07-14 19:04:42 +00:00
|
|
|
// probably not only that, do we really need this at all?
|
2021-07-14 01:40:56 +00:00
|
|
|
controller.reset();
|
|
|
|
controller.inputs.look_dir = ori.look_dir();
|
|
|
|
}
|
2021-02-22 00:57:25 +00:00
|
|
|
|
2021-07-12 15:40:29 +00:00
|
|
|
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-07-12 15:40:29 +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();
|
2020-11-12 21:31:28 +00:00
|
|
|
|
2021-05-30 15:38:47 +00:00
|
|
|
if let Some(pid) = agent.position_pid_controller.as_mut() {
|
2021-05-29 18:45:46 +00:00
|
|
|
pid.add_measurement(read_data.time.0, pos.0);
|
|
|
|
}
|
|
|
|
|
2021-07-12 15:40:29 +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
|
2021-02-07 07:22:06 +00:00
|
|
|
// obstacles that smaller entities would not).
|
|
|
|
let node_tolerance = scale * 1.5;
|
2021-07-12 15:40:29 +00:00
|
|
|
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(),
|
2021-03-23 09:51:53 +00:00
|
|
|
in_liquid: physics_state.in_liquid().is_some(),
|
2021-02-07 07:22:06 +00:00
|
|
|
min_tgt_dist: 1.0,
|
2021-07-12 15:40:29 +00:00
|
|
|
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
|
|
|
};
|
2021-07-12 15:40:29 +00:00
|
|
|
let health_fraction = health.map_or(1.0, Health::fraction);
|
2021-03-16 01:30:35 +00:00
|
|
|
let rtsim_entity = read_data
|
|
|
|
.rtsim_entities
|
|
|
|
.get(entity)
|
|
|
|
.and_then(|rtsim_ent| rtsim.get_entity(rtsim_ent.0));
|
2020-11-23 19:27:18 +00:00
|
|
|
|
2021-07-14 19:04:42 +00:00
|
|
|
if traversal_config.can_fly && matches!(body, Some(Body::Ship(_))) {
|
|
|
|
// hack (kinda): Never turn off flight airships
|
2021-07-12 15:40:29 +00:00
|
|
|
// since it results in stuttering and falling back to the ground.
|
2021-07-14 19:04:42 +00:00
|
|
|
//
|
|
|
|
// 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-04-28 02:31:51 +00:00
|
|
|
}
|
|
|
|
|
2021-02-07 07:22:06 +00:00
|
|
|
// Package all this agent's data into a convenient struct
|
|
|
|
let data = AgentData {
|
|
|
|
entity: &entity,
|
2021-03-16 01:30:35 +00:00
|
|
|
rtsim_entity,
|
2021-02-07 07:22:06 +00:00
|
|
|
uid,
|
|
|
|
pos,
|
|
|
|
vel,
|
|
|
|
ori,
|
|
|
|
energy,
|
|
|
|
body,
|
|
|
|
inventory,
|
2021-04-14 15:35:34 +00:00
|
|
|
skill_set,
|
2021-02-07 07:22:06 +00:00
|
|
|
physics_state,
|
|
|
|
alignment: alignment.as_ref(),
|
|
|
|
traversal_config,
|
|
|
|
scale,
|
2021-07-12 15:40:29 +00:00
|
|
|
damage: health_fraction,
|
2021-02-07 07:22:06 +00:00
|
|
|
light_emitter,
|
|
|
|
glider_equipped,
|
|
|
|
is_gliding,
|
2021-03-23 15:02:15 +00:00
|
|
|
health: read_data.healths.get(entity),
|
2021-03-21 18:22:14 +00:00
|
|
|
char_state,
|
2021-11-12 03:37:37 +00:00
|
|
|
active_abilities,
|
2021-04-21 17:10:13 +00:00
|
|
|
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.
|
2020-01-25 23:39:38 +00:00
|
|
|
|
2022-07-28 21:31:44 +00:00
|
|
|
BehaviorTree::root().run(
|
|
|
|
agent,
|
|
|
|
data,
|
|
|
|
&read_data,
|
|
|
|
&mut event_emitter,
|
|
|
|
controller,
|
|
|
|
);
|
|
|
|
|
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-03-16 01:30:35 +00:00
|
|
|
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) => {
|
2021-07-12 15:40:29 +00:00
|
|
|
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) => {
|
2021-07-12 15:40:29 +00:00
|
|
|
rtsim.set_entity_mood(rtsim_entity.0, memory.clone());
|
2021-03-29 14:47:42 +00:00
|
|
|
},
|
2021-07-12 15:40:29 +00:00
|
|
|
RtSimEvent::PrintMemories => {},
|
2021-03-16 01:30:35 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-02-07 07:22:06 +00:00
|
|
|
}
|
|
|
|
}
|
2020-03-17 17:27:52 +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
|
|
|
|
2021-07-22 14:02:43 +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
|
|
|
|
2021-07-22 14:02:43 +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
|
|
|
|
2021-07-22 14:02:43 +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);
|
2021-07-22 14:02:43 +00:00
|
|
|
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| {
|
2021-11-18 15:41:08 +00:00
|
|
|
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 {
|
2022-04-26 18:22:55 +00:00
|
|
|
// 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
|
2022-03-30 00:04:20 +00:00
|
|
|
// turn on their lantern.
|
2022-01-26 18:40:18 +00:00
|
|
|
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
|
2022-03-30 00:04:20 +00:00
|
|
|
// daytime and keep them off.
|
2022-01-26 18:40:18 +00:00
|
|
|
controller.push_event(ControlEvent::DisableLantern)
|
2021-02-07 07:22:06 +00:00
|
|
|
}
|
|
|
|
};
|
2020-11-25 22:47:16 +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 {
|
2022-03-30 00:04:20 +00:00
|
|
|
// 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-04-21 17:10:13 +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);
|
2022-03-30 21:54:03 +00:00
|
|
|
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);
|
2021-03-23 09:51:53 +00:00
|
|
|
//.filter(|_| bearing.z > 0.1 || self.physics_state.in_liquid().is_some());
|
2020-11-25 22:47:16 +00:00
|
|
|
|
2021-05-29 18:45:46 +00:00
|
|
|
let height_offset = bearing.z
|
2021-02-07 07:22:06 +00:00
|
|
|
+ if self.traversal_config.can_fly {
|
2021-04-21 17:10:13 +00:00
|
|
|
// NOTE: costs 4 us (imbris)
|
2021-03-12 22:14:08 +00:00
|
|
|
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
|
2021-03-12 22:14:08 +00:00
|
|
|
.map_or(true, |b| b.is_some());
|
2021-04-21 17:10:13 +00:00
|
|
|
|
2021-03-24 19:42:37 +00:00
|
|
|
let mut ground_too_close = self
|
2021-03-12 22:14:08 +00:00
|
|
|
.body
|
|
|
|
.map(|body| {
|
2021-04-15 18:07:46 +00:00
|
|
|
#[cfg(feature = "worldgen")]
|
2021-05-29 18:45:46 +00:00
|
|
|
let height_approx = self.pos.0.z
|
2021-03-12 22:14:08 +00:00
|
|
|
- read_data
|
|
|
|
.world
|
|
|
|
.sim()
|
|
|
|
.get_alt_approx(self.pos.0.xy().map(|x: f32| x as i32))
|
|
|
|
.unwrap_or(0.0);
|
2021-04-15 18:07:46 +00:00
|
|
|
#[cfg(not(feature = "worldgen"))]
|
2021-05-29 18:45:46 +00:00
|
|
|
let height_approx = self.pos.0.z;
|
2021-03-12 18:53:06 +00:00
|
|
|
|
2021-03-12 22:14:08 +00:00
|
|
|
height_approx < body.flying_height()
|
|
|
|
})
|
|
|
|
.unwrap_or(false);
|
|
|
|
|
2021-03-24 19:42:37 +00:00
|
|
|
const NUM_RAYS: usize = 5;
|
2021-04-21 17:10:13 +00:00
|
|
|
|
|
|
|
// NOTE: costs 15-20 us (imbris)
|
2021-03-24 19:42:37 +00:00
|
|
|
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())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-12 22:14:08 +00:00
|
|
|
if obstacle_ahead || ground_too_close {
|
2021-05-29 18:45:46 +00:00
|
|
|
5.0 //fly up when approaching obstacles
|
2021-02-07 07:22:06 +00:00
|
|
|
} else {
|
2021-05-29 18:45:46 +00:00
|
|
|
-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() {
|
2021-05-29 18:45:46 +00:00
|
|
|
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
|
|
|
|
});
|
2020-11-25 22:47:16 +00:00
|
|
|
|
2021-02-07 07:22:06 +00:00
|
|
|
// Stop if we're too close to a wall
|
2022-01-25 16:25:40 +00:00
|
|
|
// 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())
|
2022-01-25 16:25:40 +00:00
|
|
|
&& 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
|
|
|
|
};
|
2020-11-25 22:47:16 +00:00
|
|
|
|
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;
|
|
|
|
}
|
2020-11-25 22:47:16 +00:00
|
|
|
|
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
|
|
|
}
|
2020-11-25 22:47:16 +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-06-16 12:49:43 +00:00
|
|
|
}
|
|
|
|
|
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
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-11-25 22:47:16 +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);
|
2022-03-30 21:54:03 +00:00
|
|
|
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,
|
2021-07-31 19:33:28 +00:00
|
|
|
target: EcsEntity,
|
2021-03-29 14:47:42 +00:00
|
|
|
) -> bool {
|
2021-07-31 19:33:28 +00:00
|
|
|
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());
|
2021-07-31 19:33:28 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-07-31 19:33:28 +00:00
|
|
|
fn menacing(
|
|
|
|
&self,
|
2022-05-01 15:06:43 +00:00
|
|
|
agent: &mut Agent,
|
2021-07-31 19:33:28 +00:00
|
|
|
controller: &mut Controller,
|
2022-05-01 15:06:43 +00:00
|
|
|
target: EcsEntity,
|
2021-07-31 19:33:28 +00:00
|
|
|
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-07-31 19:33:28 +00:00
|
|
|
) {
|
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;
|
2022-05-01 15:06:43 +00:00
|
|
|
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
|
|
|
|
2021-07-31 19:33:28 +00:00
|
|
|
self.look_toward(controller, read_data, target);
|
2022-01-26 18:52:19 +00:00
|
|
|
controller.push_action(ControlAction::Wield);
|
2021-07-31 19:33:28 +00:00
|
|
|
|
|
|
|
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);
|
2022-05-01 15:06:43 +00:00
|
|
|
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");
|
2022-05-01 15:06:43 +00:00
|
|
|
} else {
|
2022-08-06 17:47:45 +00:00
|
|
|
chat("npc-speech-menacing");
|
2022-05-01 15:06:43 +00:00
|
|
|
}
|
|
|
|
} else {
|
2022-08-06 17:47:45 +00:00
|
|
|
chat("npc-speech-menacing");
|
2022-05-01 15:06:43 +00:00
|
|
|
}
|
2021-07-31 19:33:28 +00:00
|
|
|
}
|
2022-03-30 00:04:20 +00:00
|
|
|
|
|
|
|
// 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))
|
|
|
|
});
|
2021-07-31 19:33:28 +00:00
|
|
|
}
|
|
|
|
|
2022-04-26 18:22:55 +00:00
|
|
|
fn flee(
|
2021-02-07 07:22:06 +00:00
|
|
|
&self,
|
|
|
|
agent: &mut Agent,
|
|
|
|
controller: &mut Controller,
|
2022-04-29 20:23:07 +00:00
|
|
|
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
|
|
|
}
|
|
|
|
}
|
2022-03-30 21:54:03 +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
|
2022-04-26 18:22:55 +00:00
|
|
|
+ (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;
|
2022-03-30 21:54:03 +00:00
|
|
|
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
|
2022-01-16 17:06:35 +00:00
|
|
|
/// 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| {
|
2021-09-09 04:07:17 +00:00
|
|
|
let mut value = 0.0;
|
2021-07-14 12:23:51 +00:00
|
|
|
|
2021-11-18 15:41:08 +00:00
|
|
|
if let ItemKind::Consumable { kind, effects, .. } = &*item.kind() {
|
2022-01-16 17:06:35 +00:00
|
|
|
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) =>
|
|
|
|
{
|
2022-01-16 17:06:35 +00:00
|
|
|
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
|
|
|
}
|
2021-09-09 04:07:17 +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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-26 18:22:55 +00:00
|
|
|
fn choose_target(&self, agent: &mut Agent, controller: &mut Controller, read_data: &ReadData) {
|
2021-04-28 22:41:04 +00:00
|
|
|
agent.action_state.timer = 0.0;
|
2022-01-30 21:09:39 +00:00
|
|
|
let mut aggro_on = false;
|
2021-02-07 07:22:06 +00:00
|
|
|
|
2022-03-30 00:04:20 +00:00
|
|
|
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 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 {
|
2022-03-30 00:04:20 +00:00
|
|
|
None
|
|
|
|
}
|
2022-05-01 15:06:43 +00:00
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
2022-03-30 00:04:20 +00:00
|
|
|
} else {
|
2022-05-19 22:06:44 +00:00
|
|
|
Some((entity, false))
|
2022-03-30 00:04:20 +00:00
|
|
|
}
|
|
|
|
};
|
2022-05-19 22:06:44 +00:00
|
|
|
let is_valid_target = |entity: EcsEntity| match read_data.bodies.get(entity) {
|
2022-05-27 16:57:53 +00:00
|
|
|
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.
|
2022-05-31 14:57:58 +00:00
|
|
|
let wants_pickup = matches!(self.body, Some(Body::Humanoid(_)))
|
2022-06-07 07:25:27 +00:00
|
|
|
|| matches!(item, item_drop::Body::Consumable);
|
2022-05-31 14:57:58 +00:00
|
|
|
|
|
|
|
// 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,
|
|
|
|
)
|
2022-05-31 14:57:58 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
if attempt_pickup {
|
|
|
|
Some((entity, false))
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
2022-05-27 16:57:53 +00:00
|
|
|
},
|
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
|
|
|
|
}
|
|
|
|
},
|
2022-03-30 00:04:20 +00:00
|
|
|
};
|
2021-07-12 15:40:29 +00:00
|
|
|
|
2022-05-05 02:22:19 +00:00
|
|
|
let can_sense_directly_near =
|
|
|
|
{ |e_pos: &Pos| e_pos.0.distance_squared(self.pos.0) < 5_f32.powi(2) };
|
2022-04-28 22:23:48 +00:00
|
|
|
|
|
|
|
let is_detected = |entity: EcsEntity, e_pos: &Pos| {
|
|
|
|
let chance = thread_rng().gen_bool(0.3);
|
|
|
|
|
2022-05-05 02:22:19 +00:00
|
|
|
(can_sense_directly_near(e_pos) && chance)
|
2022-04-29 21:00:14 +00:00
|
|
|
|| self.can_see_entity(agent, controller, entity, e_pos, read_data)
|
2022-04-28 22:23:48 +00:00
|
|
|
};
|
|
|
|
|
2022-03-30 00:04:20 +00:00
|
|
|
// Search the area.
|
|
|
|
// TODO: choose target by more than just distance
|
2021-07-12 15:40:29 +00:00
|
|
|
let common::CachedSpatialGrid(grid) = self.cached_spatial_grid;
|
2022-03-30 00:04:20 +00:00
|
|
|
|
2021-07-12 15:40:29 +00:00
|
|
|
let target = grid
|
2022-03-30 00:04:20 +00:00
|
|
|
.in_circle_aabr(self.pos.0.xy(), agent.psyche.search_dist())
|
2022-05-19 22:06:44 +00:00
|
|
|
.filter_map(is_valid_target)
|
2022-03-30 00:04:20 +00:00
|
|
|
.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-03-30 00:04:20 +00:00
|
|
|
|
2022-04-24 20:15:30 +00:00
|
|
|
if agent.target.is_none() && target.is_some() {
|
2022-01-30 21:09:39 +00:00
|
|
|
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,
|
2021-04-21 17:10:13 +00:00
|
|
|
selected_at: read_data.time.0,
|
2022-01-30 21:09:39 +00:00
|
|
|
aggro_on,
|
2022-05-19 22:06:44 +00:00
|
|
|
})
|
2021-02-07 07:22:06 +00:00
|
|
|
}
|
2021-02-22 00:57:25 +00:00
|
|
|
|
2021-02-07 07:22:06 +00:00
|
|
|
fn attack(
|
|
|
|
&self,
|
|
|
|
agent: &mut Agent,
|
|
|
|
controller: &mut Controller,
|
2021-05-06 21:17:05 +00:00
|
|
|
tgt_data: &TargetData,
|
2021-03-21 18:22:14 +00:00
|
|
|
read_data: &ReadData,
|
2022-01-18 03:02:43 +00:00
|
|
|
rng: &mut impl Rng,
|
2021-02-07 07:22:06 +00:00
|
|
|
) {
|
2021-04-30 19:25:08 +00:00
|
|
|
let tool_tactic = |tool_kind| match tool_kind {
|
|
|
|
ToolKind::Bow => Tactic::Bow,
|
|
|
|
ToolKind::Staff => Tactic::Staff,
|
2021-07-06 08:29:11 +00:00
|
|
|
ToolKind::Sceptre => Tactic::Sceptre,
|
2021-04-30 19:25:08 +00:00
|
|
|
ToolKind::Hammer => Tactic::Hammer,
|
2022-02-17 05:58:25 +00:00
|
|
|
ToolKind::Sword | ToolKind::Blowgun => Tactic::Sword,
|
2021-04-30 19:25:08 +00:00
|
|
|
ToolKind::Axe => Tactic::Axe,
|
2022-01-21 23:28:15 +00:00
|
|
|
_ => Tactic::SimpleMelee,
|
2021-04-30 19:25:08 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
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()
|
2021-04-30 19:25:08 +00:00
|
|
|
.map(|item| {
|
2021-05-01 13:25:38 +00:00
|
|
|
if let Some(ability_spec) = item.ability_spec() {
|
2021-11-18 15:41:08 +00:00
|
|
|
match &*ability_spec {
|
2021-04-30 19:25:08 +00:00
|
|
|
AbilitySpec::Custom(spec) => match spec.as_str() {
|
2022-01-20 01:15:50 +00:00
|
|
|
"Oni" | "Sword Simple" => Tactic::Sword,
|
2021-04-30 19:25:08 +00:00
|
|
|
"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,
|
2021-04-30 19:25:08 +00:00
|
|
|
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
|
|
|
|
},
|
2021-06-23 20:15:05 +00:00
|
|
|
"Quad Low Tail" | "Husk Brute" => Tactic::TailSlap,
|
2021-04-30 19:25:08 +00:00
|
|
|
"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,
|
2021-04-30 19:25:08 +00:00
|
|
|
"Theropod Charge" => Tactic::CircleCharge {
|
|
|
|
radius: 6,
|
|
|
|
circle_time: 1,
|
|
|
|
},
|
|
|
|
"Turret" => Tactic::Turret,
|
2021-05-06 19:41:21 +00:00
|
|
|
"Haniwa Sentry" => Tactic::RotatingTurret,
|
2021-04-30 19:25:08 +00:00
|
|
|
"Bird Large Breathe" => Tactic::BirdLargeBreathe,
|
|
|
|
"Bird Large Fire" => Tactic::BirdLargeFire,
|
2021-05-26 00:37:31 +00:00
|
|
|
"Bird Large Basic" => Tactic::BirdLargeBasic,
|
2021-04-30 19:25:08 +00:00
|
|
|
"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-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,
|
2021-04-30 19:25:08 +00:00
|
|
|
},
|
|
|
|
AbilitySpec::Tool(tool_kind) => tool_tactic(*tool_kind),
|
|
|
|
}
|
2021-11-18 15:41:08 +00:00
|
|
|
} else if let ItemKind::Tool(tool) = &*item.kind() {
|
2021-04-30 19:25:08 +00:00
|
|
|
tool_tactic(tool.kind)
|
2021-02-07 07:22:06 +00:00
|
|
|
} else {
|
2022-01-21 23:28:15 +00:00
|
|
|
Tactic::SimpleMelee
|
2020-01-26 00:06:03 +00:00
|
|
|
}
|
2021-04-30 19:25:08 +00:00
|
|
|
})
|
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
|
|
|
|
2021-09-16 10:42:07 +00:00
|
|
|
let min_attack_dist = (self.body.map_or(0.5, |b| b.max_radius()) + DEFAULT_ATTACK_RANGE)
|
2021-05-06 21:17:05 +00:00
|
|
|
* self.scale
|
2021-09-16 10:42:07 +00:00
|
|
|
+ tgt_data.body.map_or(0.5, |b| b.max_radius()) * tgt_data.scale.map_or(1.0, |s| s.0);
|
2021-05-06 21:17:05 +00:00
|
|
|
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-05-06 21:17:05 +00:00
|
|
|
|
2021-02-07 07:22:06 +00:00
|
|
|
let eye_offset = self.body.map_or(0.0, |b| b.eye_height());
|
2020-11-25 22:47:16 +00:00
|
|
|
|
2021-10-27 18:01:21 +00:00
|
|
|
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-01-31 20:29:50 +00:00
|
|
|
|
2021-08-04 23:13:31 +00:00
|
|
|
// FIXME:
|
|
|
|
// 1) Retrieve actual projectile speed!
|
2021-03-23 09:51:53 +00:00
|
|
|
// 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 {
|
2021-09-05 23:03:54 +00:00
|
|
|
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,
|
2021-09-18 14:16:20 +00:00
|
|
|
self.pos.0
|
|
|
|
+ self.body.map_or(Vec3::zero(), |body| {
|
2021-09-21 23:06:59 +00:00
|
|
|
body.projectile_offsets(self.ori.look_vec())
|
2021-09-18 14:16:20 +00:00
|
|
|
}),
|
2021-09-05 23:03:54 +00:00
|
|
|
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) => {
|
2021-10-27 18:01:21 +00:00
|
|
|
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: _,
|
2021-10-27 18:01:21 +00:00
|
|
|
} => 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,
|
2021-09-18 14:16:20 +00:00
|
|
|
self.pos.0
|
|
|
|
+ self.body.map_or(Vec3::zero(), |body| {
|
2021-09-21 23:06:59 +00:00
|
|
|
body.projectile_offsets(self.ori.look_vec())
|
2021-09-18 14:16:20 +00:00
|
|
|
}),
|
2021-06-20 17:04:06 +00:00
|
|
|
Vec3::new(
|
|
|
|
tgt_data.pos.0.x,
|
|
|
|
tgt_data.pos.0.y,
|
2021-10-27 18:01:21 +00:00
|
|
|
tgt_data.pos.0.z + offset_z,
|
2021-06-20 17:04:06 +00:00
|
|
|
),
|
|
|
|
)
|
|
|
|
},
|
2021-09-05 23:03:54 +00:00
|
|
|
CharacterState::RepeaterRanged(c) => {
|
|
|
|
let projectile_speed = c.static_data.projectile_speed;
|
|
|
|
aim_projectile(
|
|
|
|
projectile_speed,
|
2021-09-18 14:16:20 +00:00
|
|
|
self.pos.0
|
|
|
|
+ self.body.map_or(Vec3::zero(), |body| {
|
2021-09-21 23:06:59 +00:00
|
|
|
body.projectile_offsets(self.ori.look_vec())
|
2021-09-18 14:16:20 +00:00
|
|
|
}),
|
2021-09-05 23:03:54 +00:00
|
|
|
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
|
|
|
};
|
2021-07-14 01:40:56 +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(
|
2021-05-06 21:17:05 +00:00
|
|
|
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-09-05 23:03:54 +00:00
|
|
|
},
|
2021-03-23 09:51:53 +00:00
|
|
|
} {
|
2021-02-07 07:22:06 +00:00
|
|
|
controller.inputs.look_dir = dir;
|
|
|
|
}
|
|
|
|
|
2021-05-06 21:17:05 +00:00
|
|
|
let attack_data = AttackData {
|
|
|
|
min_attack_dist,
|
|
|
|
dist_sqrd,
|
|
|
|
angle,
|
2022-01-21 23:28:15 +00:00
|
|
|
angle_xy,
|
2021-05-06 21:17:05 +00:00
|
|
|
};
|
|
|
|
|
2022-03-12 22:50:55 +00:00
|
|
|
// 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)
|
2021-05-06 21:17:05 +00:00
|
|
|
},
|
|
|
|
Tactic::Bow => {
|
2022-01-18 03:02:43 +00:00
|
|
|
self.handle_bow_attack(agent, controller, &attack_data, tgt_data, read_data, rng)
|
2021-05-06 21:17:05 +00:00
|
|
|
},
|
|
|
|
Tactic::Staff => {
|
2022-01-18 03:02:43 +00:00
|
|
|
self.handle_staff_attack(agent, controller, &attack_data, tgt_data, read_data, rng)
|
2021-07-06 08:29:11 +00:00
|
|
|
},
|
2022-01-18 03:02:43 +00:00
|
|
|
Tactic::Sceptre => self.handle_sceptre_attack(
|
|
|
|
agent,
|
|
|
|
controller,
|
|
|
|
&attack_data,
|
|
|
|
tgt_data,
|
|
|
|
read_data,
|
|
|
|
rng,
|
|
|
|
),
|
2021-07-12 07:09:57 +00:00
|
|
|
Tactic::StoneGolem => {
|
|
|
|
self.handle_stone_golem_attack(agent, controller, &attack_data, tgt_data, read_data)
|
|
|
|
},
|
2021-05-06 21:17:05 +00:00
|
|
|
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,
|
2021-05-06 21:17:05 +00:00
|
|
|
radius,
|
|
|
|
circle_time,
|
2022-01-18 03:02:43 +00:00
|
|
|
rng,
|
2021-05-06 21:17:05 +00:00
|
|
|
),
|
|
|
|
Tactic::QuadLowRanged => self.handle_quadlow_ranged_attack(
|
|
|
|
agent,
|
|
|
|
controller,
|
|
|
|
&attack_data,
|
2021-07-11 18:41:52 +00:00
|
|
|
tgt_data,
|
|
|
|
read_data,
|
2021-05-06 21:17:05 +00:00
|
|
|
),
|
|
|
|
Tactic::TailSlap => {
|
2021-07-11 18:41:52 +00:00
|
|
|
self.handle_tail_slap_attack(agent, controller, &attack_data, tgt_data, read_data)
|
2021-05-06 21:17:05 +00:00
|
|
|
},
|
|
|
|
Tactic::QuadLowQuick => self.handle_quadlow_quick_attack(
|
|
|
|
agent,
|
|
|
|
controller,
|
|
|
|
&attack_data,
|
2021-07-11 18:41:52 +00:00
|
|
|
tgt_data,
|
|
|
|
read_data,
|
2021-05-06 21:17:05 +00:00
|
|
|
),
|
|
|
|
Tactic::QuadLowBasic => self.handle_quadlow_basic_attack(
|
|
|
|
agent,
|
|
|
|
controller,
|
|
|
|
&attack_data,
|
2021-07-11 18:41:52 +00:00
|
|
|
tgt_data,
|
|
|
|
read_data,
|
2021-05-06 21:17:05 +00:00
|
|
|
),
|
|
|
|
Tactic::QuadMedJump => self.handle_quadmed_jump_attack(
|
|
|
|
agent,
|
|
|
|
controller,
|
|
|
|
&attack_data,
|
2021-07-11 18:41:52 +00:00
|
|
|
tgt_data,
|
|
|
|
read_data,
|
2021-05-06 21:17:05 +00:00
|
|
|
),
|
|
|
|
Tactic::QuadMedBasic => self.handle_quadmed_basic_attack(
|
|
|
|
agent,
|
|
|
|
controller,
|
|
|
|
&attack_data,
|
2021-07-11 18:41:52 +00:00
|
|
|
tgt_data,
|
|
|
|
read_data,
|
2021-05-06 21:17:05 +00:00
|
|
|
),
|
|
|
|
Tactic::QuadLowBeam => self.handle_quadlow_beam_attack(
|
|
|
|
agent,
|
|
|
|
controller,
|
|
|
|
&attack_data,
|
2021-07-11 18:41:52 +00:00
|
|
|
tgt_data,
|
|
|
|
read_data,
|
2021-05-06 21:17:05 +00:00
|
|
|
),
|
|
|
|
Tactic::Theropod => {
|
2021-07-11 18:41:52 +00:00
|
|
|
self.handle_theropod_attack(agent, controller, &attack_data, tgt_data, read_data)
|
2021-05-06 21:17:05 +00:00
|
|
|
},
|
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(
|
2021-12-23 14:13:57 +00:00
|
|
|
agent,
|
|
|
|
controller,
|
|
|
|
&attack_data,
|
|
|
|
tgt_data,
|
|
|
|
read_data,
|
|
|
|
rng,
|
|
|
|
),
|
|
|
|
Tactic::ArthropodRanged => self.handle_arthropod_ranged_attack(
|
|
|
|
agent,
|
|
|
|
controller,
|
|
|
|
&attack_data,
|
|
|
|
tgt_data,
|
|
|
|
read_data,
|
|
|
|
),
|
2022-04-26 23:34:29 +00:00
|
|
|
Tactic::Turret => {
|
|
|
|
self.handle_turret_attack(agent, controller, &attack_data, tgt_data, read_data)
|
2021-05-06 21:17:05 +00:00
|
|
|
},
|
2022-04-26 23:34:29 +00:00
|
|
|
Tactic::FixedTurret => self.handle_fixed_turret_attack(
|
|
|
|
agent,
|
|
|
|
controller,
|
|
|
|
&attack_data,
|
|
|
|
tgt_data,
|
|
|
|
read_data,
|
|
|
|
),
|
2022-03-30 00:04:20 +00:00
|
|
|
Tactic::RotatingTurret => {
|
2022-04-26 23:34:29 +00:00
|
|
|
self.handle_rotating_turret_attack(agent, controller, tgt_data, read_data)
|
2022-03-30 00:04:20 +00:00
|
|
|
},
|
2022-01-18 03:02:43 +00:00
|
|
|
Tactic::Mindflayer => self.handle_mindflayer_attack(
|
|
|
|
agent,
|
|
|
|
controller,
|
|
|
|
&attack_data,
|
|
|
|
tgt_data,
|
|
|
|
read_data,
|
|
|
|
rng,
|
|
|
|
),
|
2021-05-06 21:17:05 +00:00
|
|
|
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,
|
2021-05-06 21:17:05 +00:00
|
|
|
),
|
|
|
|
// 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-06 21:17:05 +00:00
|
|
|
),
|
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
|
|
|
),
|
2021-05-06 21:17:05 +00:00
|
|
|
Tactic::Minotaur => {
|
2021-07-11 18:41:52 +00:00
|
|
|
self.handle_minotaur_attack(agent, controller, &attack_data, tgt_data, read_data)
|
2021-05-06 21:17:05 +00:00
|
|
|
},
|
2021-07-12 07:09:57 +00:00
|
|
|
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
|
|
|
),
|
2022-04-22 01:32:56 +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)
|
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,
|
|
|
|
),
|
2021-05-06 21:17:05 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-03-09 17:31:35 +00:00
|
|
|
fn handle_sounds_heard(
|
2021-05-06 21:17:05 +00:00
|
|
|
&self,
|
|
|
|
agent: &mut Agent,
|
|
|
|
controller: &mut Controller,
|
|
|
|
read_data: &ReadData,
|
2022-01-18 03:02:43 +00:00
|
|
|
rng: &mut impl Rng,
|
2021-05-06 21:17:05 +00:00
|
|
|
) {
|
2022-03-09 17:31:35 +00:00
|
|
|
agent.forget_old_sounds(read_data.time.0);
|
2021-05-06 21:17:05 +00:00
|
|
|
|
2022-01-18 03:02:43 +00:00
|
|
|
if is_invulnerable(*self.entity, read_data) {
|
|
|
|
self.idle(agent, controller, read_data, rng);
|
|
|
|
return;
|
2021-05-06 21:17:05 +00:00
|
|
|
}
|
|
|
|
|
2022-01-18 03:02:43 +00:00
|
|
|
if let Some(sound) = agent.sounds_heard.last() {
|
2022-03-09 17:31:35 +00:00
|
|
|
let sound_pos = Pos(sound.pos);
|
|
|
|
let dist_sqrd = self.pos.0.distance_squared(sound_pos.0);
|
2022-03-13 00:09:22 +00:00
|
|
|
// 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
|
2022-03-13 00:09:22 +00:00
|
|
|
// 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);
|
2022-03-09 17:31:35 +00:00
|
|
|
|
|
|
|
let sound_was_loud = sound.vol >= 10.0;
|
|
|
|
let sound_was_threatening = sound_was_loud
|
|
|
|
|| matches!(sound.kind, SoundKind::Utterance(UtteranceKind::Scream, _));
|
|
|
|
|
2022-04-26 18:38:08 +00:00
|
|
|
let has_enemy_alignment = matches!(self.alignment, Some(Alignment::Enemy));
|
2022-03-09 17:31:35 +00:00
|
|
|
// 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());
|
2022-04-26 18:38:08 +00:00
|
|
|
let follows_threatening_sounds = has_enemy_alignment || is_village_guard;
|
2022-03-09 17:31:35 +00:00
|
|
|
|
|
|
|
// TODO: Awareness currently doesn't influence anything.
|
2022-03-19 17:02:29 +00:00
|
|
|
//agent.awareness += 0.5 * sound.vol;
|
2022-03-09 17:31:35 +00:00
|
|
|
|
2022-03-13 00:09:22 +00:00
|
|
|
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);
|
2022-03-13 00:09:22 +00:00
|
|
|
} else if self.below_flee_health(agent) || !follows_threatening_sounds {
|
2022-04-29 20:23:07 +00:00
|
|
|
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);
|
|
|
|
}
|
2022-03-09 17:31:35 +00:00
|
|
|
} else {
|
|
|
|
self.idle(agent, controller, read_data, rng);
|
2021-05-06 21:17:05 +00:00
|
|
|
}
|
2022-04-05 03:40:26 +00:00
|
|
|
} else {
|
|
|
|
self.idle(agent, controller, read_data, rng);
|
2021-05-06 21:17:05 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-18 03:02:43 +00:00
|
|
|
fn attack_target_attacker(
|
2021-05-06 21:17:05 +00:00
|
|
|
&self,
|
|
|
|
agent: &mut Agent,
|
|
|
|
read_data: &ReadData,
|
2022-01-18 03:02:43 +00:00
|
|
|
controller: &mut Controller,
|
|
|
|
rng: &mut impl Rng,
|
2021-05-06 21:17:05 +00:00
|
|
|
) {
|
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
|
|
|
}
|
2021-07-14 01:40:56 +00:00
|
|
|
|
2022-01-18 03:02:43 +00:00
|
|
|
agent.target = Some(Target::new(attacker, true, read_data.time.0, true));
|
2021-07-14 01:40:56 +00:00
|
|
|
|
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) {
|
2022-05-01 15:06:43 +00:00
|
|
|
// 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));
|
2021-07-14 01:40:56 +00:00
|
|
|
|
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),
|
|
|
|
);
|
2022-05-01 15:06:43 +00:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-05-06 21:17:05 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-18 03:02:43 +00:00
|
|
|
/// Directs the entity to path and move toward the target
|
2022-02-25 20:37:01 +00:00
|
|
|
/// 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(
|
2021-05-06 21:17:05 +00:00
|
|
|
&self,
|
|
|
|
agent: &mut Agent,
|
|
|
|
controller: &mut Controller,
|
2022-02-27 06:56:36 +00:00
|
|
|
tgt_pos: Vec3<f32>,
|
2021-05-06 21:17:05 +00:00
|
|
|
read_data: &ReadData,
|
2022-02-25 20:35:35 +00:00
|
|
|
path: Path,
|
2022-01-18 03:02:43 +00:00
|
|
|
speed_multiplier: Option<f32>,
|
|
|
|
) -> bool {
|
2022-03-01 21:47:01 +00:00
|
|
|
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;
|
2022-02-25 20:35:35 +00:00
|
|
|
let pathing_pos = match path {
|
2022-03-01 21:40:43 +00:00
|
|
|
Path::Separate => {
|
2022-02-25 20:35:35 +00:00
|
|
|
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)
|
2021-06-15 13:52:31 +00:00
|
|
|
{
|
2022-02-25 20:35:35 +00:00
|
|
|
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),
|
|
|
|
) {
|
2022-03-01 21:42:03 +00:00
|
|
|
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();
|
2022-03-01 21:42:03 +00:00
|
|
|
if dist_xy < spacing {
|
2022-03-01 21:43:33 +00:00
|
|
|
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);
|
2022-02-25 20:35:35 +00:00
|
|
|
}
|
2021-06-16 10:57:52 +00:00
|
|
|
}
|
2021-06-15 13:52:31 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-03-01 21:33:25 +00:00
|
|
|
partial_path_tgt_pos(
|
2022-03-01 21:38:35 +00:00
|
|
|
sep_vec * SEPARATION_BIAS + pos_difference * (1.0 - SEPARATION_BIAS),
|
2022-02-25 20:44:43 +00:00
|
|
|
)
|
2022-02-25 20:35:35 +00:00
|
|
|
},
|
2022-03-01 21:40:43 +00:00
|
|
|
Path::Full => tgt_pos,
|
2022-03-01 21:38:35 +00:00
|
|
|
Path::Partial => partial_path_tgt_pos(pos_difference),
|
2021-05-21 03:14:45 +00:00
|
|
|
};
|
|
|
|
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: 1.25,
|
|
|
|
..self.traversal_config
|
|
|
|
},
|
|
|
|
) {
|
|
|
|
controller.inputs.move_dir =
|
|
|
|
bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed * speed_multiplier;
|
2022-03-30 21:54:03 +00:00
|
|
|
self.jump_if(bearing.z > 1.5, controller);
|
2021-05-21 03:14:45 +00:00
|
|
|
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,
|
2021-07-31 19:33:28 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-03-30 21:54:03 +00:00
|
|
|
fn jump_if(&self, condition: bool, controller: &mut Controller) {
|
2021-05-21 03:14:45 +00:00
|
|
|
if condition {
|
2022-01-26 19:09:59 +00:00
|
|
|
controller.push_basic_input(InputKind::Jump);
|
2021-05-21 03:14:45 +00:00
|
|
|
} else {
|
2022-01-26 19:15:40 +00:00
|
|
|
controller.push_cancel_input(InputKind::Jump)
|
2021-05-21 03:14:45 +00:00
|
|
|
}
|
|
|
|
}
|
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>) {
|
2021-07-31 19:33:28 +00:00
|
|
|
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,
|
2022-03-08 00:58:44 +00:00
|
|
|
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
|
|
|
|
2022-05-01 15:06:43 +00:00
|
|
|
fn cry_out(
|
|
|
|
&self,
|
|
|
|
agent: &Agent,
|
|
|
|
event_emitter: &mut Emitter<'_, ServerEvent>,
|
|
|
|
read_data: &ReadData,
|
|
|
|
) {
|
2022-04-26 18:38:08 +00:00
|
|
|
let has_enemy_alignment = matches!(self.alignment, Some(Alignment::Enemy));
|
2021-10-22 22:11:09 +00:00
|
|
|
|
2022-04-26 18:38:08 +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,
|
|
|
|
);
|
2022-05-01 15:06:43 +00:00
|
|
|
} 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,
|
|
|
|
);
|
2022-05-01 15:06:43 +00:00
|
|
|
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>,
|
|
|
|
) {
|
2022-05-01 15:06:43 +00:00
|
|
|
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,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
2022-03-12 22:50:55 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
2022-04-26 18:22:55 +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)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
2022-05-01 15:06:43 +00:00
|
|
|
|
|
|
|
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)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
2022-03-30 00:04:20 +00:00
|
|
|
|
2022-04-26 18:22:55 +00:00
|
|
|
fn is_enemy(&self, entity: EcsEntity, read_data: &ReadData) -> bool {
|
2022-04-26 18:38:08 +00:00
|
|
|
let other_alignment = read_data.alignments.get(entity);
|
2022-03-30 00:04:20 +00:00
|
|
|
|
|
|
|
(entity != *self.entity)
|
2022-04-26 18:38:08 +00:00
|
|
|
&& !self.passive_towards(entity, read_data)
|
|
|
|
&& (are_our_owners_hostile(self.alignment, other_alignment, read_data)
|
2022-03-30 00:04:20 +00:00
|
|
|
|| 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)))
|
2022-03-30 00:04:20 +00:00
|
|
|
}
|
|
|
|
|
2022-04-17 18:50:50 +00:00
|
|
|
fn should_defend(&self, entity: EcsEntity, read_data: &ReadData) -> bool {
|
2022-03-30 00:04:20 +00:00
|
|
|
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))
|
2022-03-30 00:04:20 +00:00
|
|
|
|| self_owns_entity
|
|
|
|
}
|
2022-04-26 18:38:08 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
2022-04-28 00:29:10 +00:00
|
|
|
|
|
|
|
fn can_see_entity(
|
|
|
|
&self,
|
|
|
|
agent: &Agent,
|
|
|
|
controller: &Controller,
|
2022-04-29 21:00:14 +00:00
|
|
|
other: EcsEntity,
|
|
|
|
other_pos: &Pos,
|
2022-04-28 00:29:10 +00:00
|
|
|
read_data: &ReadData,
|
|
|
|
) -> bool {
|
2022-05-04 13:24:19 +00:00
|
|
|
let other_stealth_multiplier = {
|
|
|
|
let other_inventory = read_data.inventories.get(other);
|
|
|
|
let other_char_state = read_data.char_states.get(other);
|
2022-04-28 00:29:10 +00:00
|
|
|
|
2022-05-28 23:41:31 +00:00
|
|
|
perception_dist_multiplier_from_stealth(other_inventory, other_char_state, self.msm)
|
2022-05-04 13:24:19 +00:00
|
|
|
};
|
2022-04-28 00:29:10 +00:00
|
|
|
|
|
|
|
let within_sight_dist = {
|
2022-05-04 13:24:19 +00:00
|
|
|
let sight_dist = agent.psyche.sight_dist * other_stealth_multiplier;
|
|
|
|
let dist_sqrd = other_pos.0.distance_squared(self.pos.0);
|
|
|
|
|
2022-04-28 00:29:10 +00:00
|
|
|
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);
|
|
|
|
|
2022-05-04 13:24:19 +00:00
|
|
|
(within_sight_dist)
|
2022-04-28 00:29:10 +00:00
|
|
|
&& within_fov
|
|
|
|
&& entities_have_line_of_sight(self.pos, self.body, other_pos, other_body, read_data)
|
|
|
|
}
|
2021-07-12 15:40:29 +00:00
|
|
|
}
|