mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Make NPCs Aware of Sound - See Issue #913
This commit is contained in:
parent
9060e1ea71
commit
d5f3ba77d4
@ -60,6 +60,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Added timed bans and ban history.
|
||||
- Added non-admin moderators with limit privileges and updated the security model to reflect this.
|
||||
- Chat tabs
|
||||
- NPC's now hear certain sounds
|
||||
|
||||
### Changed
|
||||
|
||||
|
@ -14,6 +14,7 @@ use super::dialogue::Subject;
|
||||
|
||||
pub const DEFAULT_INTERACTION_TIME: f32 = 3.0;
|
||||
pub const TRADE_INTERACTION_TIME: f32 = 300.0;
|
||||
pub const MAX_LISTEN_DIST: f32 = 100.0;
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
pub enum Alignment {
|
||||
@ -245,7 +246,7 @@ pub enum AgentEvent {
|
||||
TradeAccepted(Uid),
|
||||
FinishedTrade(TradeResult),
|
||||
UpdatePendingTrade(
|
||||
// this data structure is large so box it to keep AgentEvent small
|
||||
// This data structure is large so box it to keep AgentEvent small
|
||||
Box<(
|
||||
TradeId,
|
||||
PendingTrade,
|
||||
@ -253,7 +254,43 @@ pub enum AgentEvent {
|
||||
[Option<ReducedInventory>; 2],
|
||||
)>,
|
||||
),
|
||||
// Add others here
|
||||
ServerSound(Sound),
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct Sound {
|
||||
pub kind: SoundKind,
|
||||
pub pos: Vec3<f32>,
|
||||
pub vol: f32,
|
||||
pub time: f64,
|
||||
}
|
||||
|
||||
impl Sound {
|
||||
pub fn new(kind: SoundKind, pos: Vec3<f32>, vol: f32, time: f64) -> Self {
|
||||
Sound {
|
||||
kind,
|
||||
pos,
|
||||
vol,
|
||||
time,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_new_vol(mut self, new_vol: f32) -> Self {
|
||||
self.vol = new_vol;
|
||||
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum SoundKind {
|
||||
Unknown,
|
||||
Movement,
|
||||
Melee,
|
||||
Projectile,
|
||||
Explosion,
|
||||
Beam,
|
||||
Shockwave,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@ -274,6 +311,8 @@ pub struct Agent {
|
||||
pub inbox: VecDeque<AgentEvent>,
|
||||
pub action_state: ActionState,
|
||||
pub bearing: Vec2<f32>,
|
||||
pub sounds_heard: Vec<Sound>,
|
||||
pub awareness: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
|
@ -18,5 +18,8 @@ pub const IRON_DENSITY: f32 = 7870.0;
|
||||
pub const HUMAN_DENSITY: f32 = 990.0; // value we use to make humanoids gently float
|
||||
// 1 thread might be used for long-running cpu intensive tasks, like chunk
|
||||
// generation. having at least 2 helps not blocking in the main tick here
|
||||
|
||||
pub const MIN_RECOMMENDED_RAYON_THREADS: usize = 2;
|
||||
pub const MIN_RECOMMENDED_TOKIO_THREADS: usize = 2;
|
||||
|
||||
pub const SOUND_TRAVEL_DIST_PER_VOLUME: f32 = 3.0;
|
||||
|
@ -2,6 +2,7 @@ use crate::{
|
||||
character::CharacterId,
|
||||
comp::{
|
||||
self,
|
||||
agent::Sound,
|
||||
invite::{InviteKind, InviteResponse},
|
||||
item::Item,
|
||||
DisconnectReason, Ori, Pos,
|
||||
@ -175,6 +176,9 @@ pub enum ServerEvent {
|
||||
range: Option<f32>,
|
||||
pos: Pos,
|
||||
},
|
||||
Sound {
|
||||
sound: Sound,
|
||||
},
|
||||
}
|
||||
|
||||
pub struct EventBus<E> {
|
||||
|
@ -69,7 +69,6 @@ pub trait CharacterBehavior {
|
||||
ControlAction::CancelInput(input) => self.cancel_input(data, input),
|
||||
}
|
||||
}
|
||||
// fn init(data: &JoinData) -> CharacterState;
|
||||
}
|
||||
|
||||
/// Read-Only Data sent from Character Behavior System to behavior fn's
|
||||
|
@ -1,6 +1,7 @@
|
||||
use common::{
|
||||
combat::{AttackSource, AttackerInfo, TargetInfo},
|
||||
comp::{
|
||||
agent::{Sound, SoundKind},
|
||||
Beam, BeamSegment, Body, CharacterState, Combo, Energy, Group, Health, HealthSource,
|
||||
Inventory, Ori, Pos, Scale, Stats,
|
||||
},
|
||||
@ -13,6 +14,7 @@ use common::{
|
||||
GroupTarget,
|
||||
};
|
||||
use common_ecs::{Job, Origin, ParMode, Phase, System};
|
||||
use rand::{thread_rng, Rng};
|
||||
use rayon::iter::ParallelIterator;
|
||||
use specs::{
|
||||
saveload::MarkerAllocator, shred::ResourceId, Entities, Join, ParJoin, Read, ReadExpect,
|
||||
@ -88,6 +90,14 @@ impl<'a> System<'a> for Sys {
|
||||
None => return (server_events, add_hit_entities, outcomes),
|
||||
};
|
||||
let end_time = creation_time + beam_segment.duration.as_secs_f64();
|
||||
|
||||
let mut rng = thread_rng();
|
||||
if rng.gen_bool(0.005) {
|
||||
server_events.push(ServerEvent::Sound {
|
||||
sound: Sound::new(SoundKind::Beam, pos.0, 7.0, time),
|
||||
});
|
||||
}
|
||||
|
||||
// If beam segment is out of time emit destroy event but still continue since it
|
||||
// may have traveled and produced effects a bit before reaching its
|
||||
// end point
|
||||
@ -151,8 +161,7 @@ impl<'a> System<'a> for Sys {
|
||||
let height_b = body_b.height() * scale_b;
|
||||
|
||||
// Check if it is a hit
|
||||
let hit = entity != target
|
||||
&& !health_b.is_dead
|
||||
let hit = entity != target && !health_b.is_dead
|
||||
// Collision shapes
|
||||
&& sphere_wedge_cylinder_collision(pos.0, frame_start_dist, frame_end_dist, *ori.look_dir(), beam_segment.angle, pos_b.0, rad_b, height_b);
|
||||
|
||||
|
@ -1,11 +1,13 @@
|
||||
use common::{
|
||||
combat::{AttackSource, AttackerInfo, TargetInfo},
|
||||
comp::{
|
||||
agent::{Sound, SoundKind},
|
||||
Body, CharacterState, Combo, Energy, Group, Health, Inventory, Melee, Ori, Pos, Scale,
|
||||
Stats,
|
||||
},
|
||||
event::{EventBus, ServerEvent},
|
||||
outcome::Outcome,
|
||||
resources::Time,
|
||||
uid::Uid,
|
||||
util::Dir,
|
||||
GroupTarget,
|
||||
@ -18,6 +20,7 @@ use vek::*;
|
||||
|
||||
#[derive(SystemData)]
|
||||
pub struct ReadData<'a> {
|
||||
time: Read<'a, Time>,
|
||||
entities: Entities<'a>,
|
||||
uids: ReadStorage<'a, Uid>,
|
||||
positions: ReadStorage<'a, Pos>,
|
||||
@ -66,6 +69,9 @@ impl<'a> System<'a> for Sys {
|
||||
if melee_attack.applied {
|
||||
continue;
|
||||
}
|
||||
server_emitter.emit(ServerEvent::Sound {
|
||||
sound: Sound::new(SoundKind::Melee, pos.0, 3.0, read_data.time.0),
|
||||
});
|
||||
melee_attack.applied = true;
|
||||
|
||||
// Scales
|
||||
|
@ -1,17 +1,19 @@
|
||||
use common::{
|
||||
combat::{AttackSource, AttackerInfo, TargetInfo},
|
||||
comp::{
|
||||
agent::{Sound, SoundKind},
|
||||
projectile, Body, CharacterState, Combo, Energy, Group, Health, HealthSource, Inventory,
|
||||
Ori, PhysicsState, Pos, Projectile, Stats, Vel,
|
||||
},
|
||||
event::{EventBus, ServerEvent},
|
||||
outcome::Outcome,
|
||||
resources::DeltaTime,
|
||||
resources::{DeltaTime, Time},
|
||||
uid::{Uid, UidAllocator},
|
||||
util::Dir,
|
||||
GroupTarget,
|
||||
};
|
||||
use common_ecs::{Job, Origin, Phase, System};
|
||||
use rand::{thread_rng, Rng};
|
||||
use specs::{
|
||||
saveload::MarkerAllocator, shred::ResourceId, Entities, Join, Read, ReadStorage, SystemData,
|
||||
World, Write, WriteStorage,
|
||||
@ -21,6 +23,7 @@ use vek::*;
|
||||
|
||||
#[derive(SystemData)]
|
||||
pub struct ReadData<'a> {
|
||||
time: Read<'a, Time>,
|
||||
entities: Entities<'a>,
|
||||
dt: Read<'a, DeltaTime>,
|
||||
uid_allocator: Read<'a, UidAllocator>,
|
||||
@ -59,7 +62,6 @@ impl<'a> System<'a> for Sys {
|
||||
(read_data, mut orientations, mut projectiles, mut outcomes): Self::SystemData,
|
||||
) {
|
||||
let mut server_emitter = read_data.server_bus.emitter();
|
||||
|
||||
// Attacks
|
||||
'projectile_loop: for (entity, pos, physics, vel, mut projectile) in (
|
||||
&read_data.entities,
|
||||
@ -70,7 +72,15 @@ impl<'a> System<'a> for Sys {
|
||||
)
|
||||
.join()
|
||||
{
|
||||
let mut rng = thread_rng();
|
||||
if physics.on_surface().is_none() && rng.gen_bool(0.05) {
|
||||
server_emitter.emit(ServerEvent::Sound {
|
||||
sound: Sound::new(SoundKind::Projectile, pos.0, 2.0, read_data.time.0),
|
||||
});
|
||||
}
|
||||
|
||||
let mut projectile_vanished: bool = false;
|
||||
|
||||
// Hit entity
|
||||
for other in physics.touch_entities.iter().copied() {
|
||||
let same_group = projectile
|
||||
@ -86,16 +96,14 @@ impl<'a> System<'a> for Sys {
|
||||
.and_then(|e| read_data.groups.get(e))
|
||||
);
|
||||
|
||||
// Skip if in the same group
|
||||
let target_group = if same_group {
|
||||
GroupTarget::InGroup
|
||||
} else {
|
||||
GroupTarget::OutOfGroup
|
||||
};
|
||||
|
||||
if projectile.ignore_group
|
||||
// Skip if in the same group
|
||||
&& same_group
|
||||
{
|
||||
if projectile.ignore_group && same_group {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -173,7 +181,7 @@ impl<'a> System<'a> for Sys {
|
||||
pos: pos.0,
|
||||
explosion: e,
|
||||
owner: projectile.owner,
|
||||
})
|
||||
});
|
||||
},
|
||||
projectile::Effect::Vanish => {
|
||||
server_emitter.emit(ServerEvent::Destroy {
|
||||
@ -198,8 +206,7 @@ impl<'a> System<'a> for Sys {
|
||||
}
|
||||
}
|
||||
|
||||
// Hit something solid
|
||||
if physics.on_wall.is_some() || physics.on_ground || physics.on_ceiling {
|
||||
if physics.on_surface().is_some() {
|
||||
let projectile = &mut *projectile;
|
||||
for effect in projectile.hit_solid.drain(..) {
|
||||
match effect {
|
||||
@ -208,7 +215,7 @@ impl<'a> System<'a> for Sys {
|
||||
pos: pos.0,
|
||||
explosion: e,
|
||||
owner: projectile.owner,
|
||||
})
|
||||
});
|
||||
},
|
||||
projectile::Effect::Vanish => {
|
||||
server_emitter.emit(ServerEvent::Destroy {
|
||||
|
@ -1,6 +1,7 @@
|
||||
use common::{
|
||||
combat::{AttackSource, AttackerInfo, TargetInfo},
|
||||
comp::{
|
||||
agent::{Sound, SoundKind},
|
||||
Body, CharacterState, Combo, Energy, Group, Health, HealthSource, Inventory, Ori,
|
||||
PhysicsState, Pos, Scale, Shockwave, ShockwaveHitEntities, Stats,
|
||||
},
|
||||
@ -12,6 +13,7 @@ use common::{
|
||||
GroupTarget,
|
||||
};
|
||||
use common_ecs::{Job, Origin, Phase, System};
|
||||
use rand::{thread_rng, Rng};
|
||||
use specs::{
|
||||
saveload::MarkerAllocator, shred::ResourceId, Entities, Join, Read, ReadStorage, SystemData,
|
||||
World, Write, WriteStorage,
|
||||
@ -83,9 +85,15 @@ impl<'a> System<'a> for Sys {
|
||||
|
||||
let end_time = creation_time + shockwave.duration.as_secs_f64();
|
||||
|
||||
let mut rng = thread_rng();
|
||||
if rng.gen_bool(0.05) {
|
||||
server_emitter.emit(ServerEvent::Sound {
|
||||
sound: Sound::new(SoundKind::Shockwave, pos.0, 16.0, time),
|
||||
});
|
||||
}
|
||||
|
||||
// If shockwave is out of time emit destroy event but still continue since it
|
||||
// may have traveled and produced effects a bit before reaching it's
|
||||
// end point
|
||||
// may have traveled and produced effects a bit before reaching it's end point
|
||||
if time > end_time {
|
||||
server_emitter.emit(ServerEvent::Destroy {
|
||||
entity,
|
||||
|
@ -1,7 +1,9 @@
|
||||
use crate::{
|
||||
client::Client,
|
||||
comp::{
|
||||
biped_large, quadruped_low, quadruped_medium, quadruped_small, skills::SkillGroupKind,
|
||||
agent::{Sound, SoundKind},
|
||||
biped_large, quadruped_low, quadruped_medium, quadruped_small,
|
||||
skills::SkillGroupKind,
|
||||
theropod, PhysicsState,
|
||||
},
|
||||
rtsim::RtSim,
|
||||
@ -476,19 +478,22 @@ pub fn handle_delete(server: &mut Server, entity: EcsEntity) {
|
||||
}
|
||||
|
||||
pub fn handle_land_on_ground(server: &Server, entity: EcsEntity, vel: Vec3<f32>) {
|
||||
let state = &server.state;
|
||||
let ecs = server.state.ecs();
|
||||
|
||||
if vel.z <= -30.0 {
|
||||
let mass = state
|
||||
.ecs()
|
||||
let mass = ecs
|
||||
.read_storage::<comp::Mass>()
|
||||
.get(entity)
|
||||
.copied()
|
||||
.unwrap_or_default();
|
||||
let inventories = state.ecs().read_storage::<Inventory>();
|
||||
let falldmg = mass.0 * vel.z.powi(2) / 200.0;
|
||||
let stats = state.ecs().read_storage::<Stats>();
|
||||
let impact_energy = mass.0 * vel.z.powi(2) / 2.0;
|
||||
let falldmg = impact_energy / 100.0;
|
||||
|
||||
let inventories = ecs.read_storage::<Inventory>();
|
||||
let stats = ecs.read_storage::<Stats>();
|
||||
|
||||
// Handle health change
|
||||
if let Some(mut health) = state.ecs().write_storage::<comp::Health>().get_mut(entity) {
|
||||
if let Some(mut health) = ecs.write_storage::<comp::Health>().get_mut(entity) {
|
||||
let damage = Damage {
|
||||
source: DamageSource::Falling,
|
||||
kind: DamageKind::Crushing,
|
||||
@ -503,7 +508,7 @@ pub fn handle_land_on_ground(server: &Server, entity: EcsEntity, vel: Vec3<f32>)
|
||||
health.change_by(change);
|
||||
}
|
||||
// Handle poise change
|
||||
if let Some(mut poise) = state.ecs().write_storage::<comp::Poise>().get_mut(entity) {
|
||||
if let Some(mut poise) = ecs.write_storage::<comp::Poise>().get_mut(entity) {
|
||||
let poise_damage = PoiseChange {
|
||||
amount: -(mass.0 * vel.magnitude_squared() / 1500.0) as i32,
|
||||
source: PoiseSource::Falling,
|
||||
@ -557,6 +562,13 @@ pub fn handle_respawn(server: &Server, entity: EcsEntity) {
|
||||
pub fn handle_explosion(server: &Server, pos: Vec3<f32>, explosion: Explosion, owner: Option<Uid>) {
|
||||
// Go through all other entities
|
||||
let ecs = &server.state.ecs();
|
||||
let server_eventbus = ecs.read_resource::<EventBus<ServerEvent>>();
|
||||
let time = ecs.read_resource::<Time>();
|
||||
|
||||
let explosion_volume = 2.5 * explosion.radius;
|
||||
server_eventbus.emit_now(ServerEvent::Sound {
|
||||
sound: Sound::new(SoundKind::Explosion, pos, explosion_volume, time.0),
|
||||
});
|
||||
|
||||
// Add an outcome
|
||||
// Uses radius as outcome power for now
|
||||
@ -751,8 +763,6 @@ pub fn handle_explosion(server: &Server, pos: Vec3<f32>, explosion: Explosion, o
|
||||
char_state: char_state_b_maybe,
|
||||
};
|
||||
|
||||
let server_eventbus = ecs.read_resource::<EventBus<ServerEvent>>();
|
||||
|
||||
attack.apply_attack(
|
||||
target_group,
|
||||
attacker_info,
|
||||
|
@ -1,13 +1,19 @@
|
||||
use specs::{world::WorldExt, Builder, Entity as EcsEntity};
|
||||
use specs::{world::WorldExt, Builder, Entity as EcsEntity, Join};
|
||||
use tracing::error;
|
||||
use vek::*;
|
||||
|
||||
use common::{
|
||||
comp::{
|
||||
self, agent::AgentEvent, dialogue::Subject, inventory::slot::EquipSlot, item, slot::Slot,
|
||||
tool::ToolKind, Inventory, Pos,
|
||||
self,
|
||||
agent::{AgentEvent, Sound, MAX_LISTEN_DIST},
|
||||
dialogue::Subject,
|
||||
inventory::slot::EquipSlot,
|
||||
item,
|
||||
slot::Slot,
|
||||
tool::ToolKind,
|
||||
Inventory, Pos,
|
||||
},
|
||||
consts::MAX_MOUNT_RANGE,
|
||||
consts::{MAX_MOUNT_RANGE, SOUND_TRAVEL_DIST_PER_VOLUME},
|
||||
outcome::Outcome,
|
||||
uid::Uid,
|
||||
vol::ReadVol,
|
||||
@ -72,7 +78,7 @@ pub fn handle_npc_interaction(server: &mut Server, interactor: EcsEntity, npc_en
|
||||
if let Some(interactor_uid) = state.ecs().uid_from_entity(interactor) {
|
||||
agent
|
||||
.inbox
|
||||
.push_front(AgentEvent::Talk(interactor_uid, Subject::Regular));
|
||||
.push_back(AgentEvent::Talk(interactor_uid, Subject::Regular));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -103,8 +109,7 @@ pub fn handle_mount(server: &mut Server, mounter: EcsEntity, mountee: EcsEntity)
|
||||
state.ecs().uid_from_entity(mountee),
|
||||
) {
|
||||
// We know the entities must exist to be able to look up their UIDs, so these
|
||||
// are guaranteed to work; hence we can ignore possible errors
|
||||
// here.
|
||||
// are guaranteed to work; hence we can ignore possible errors here.
|
||||
state.write_component_ignore_entity_dead(
|
||||
mountee,
|
||||
comp::MountState::MountedBy(mounter_uid),
|
||||
@ -301,3 +306,27 @@ pub fn handle_mine_block(server: &mut Server, pos: Vec3<i32>, tool: Option<ToolK
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_sound(server: &mut Server, sound: &Sound) {
|
||||
let ecs = &server.state.ecs();
|
||||
let positions = &ecs.read_storage::<comp::Pos>();
|
||||
let agents = &mut ecs.write_storage::<comp::Agent>();
|
||||
|
||||
for (agent, agent_pos) in (agents, positions).join() {
|
||||
// TODO: Use pathfinding for more dropoff around obstacles
|
||||
let agent_dist_sqrd = agent_pos.0.distance_squared(sound.pos);
|
||||
let sound_travel_dist_sqrd = (sound.vol * SOUND_TRAVEL_DIST_PER_VOLUME).powi(2);
|
||||
|
||||
let vol_dropoff = agent_dist_sqrd / sound_travel_dist_sqrd * sound.vol;
|
||||
let propagated_sound = sound.with_new_vol(sound.vol - vol_dropoff);
|
||||
|
||||
let can_hear_sound = propagated_sound.vol > 0.00;
|
||||
let should_hear_sound = agent_dist_sqrd < MAX_LISTEN_DIST.powi(2);
|
||||
|
||||
if can_hear_sound && should_hear_sound {
|
||||
agent
|
||||
.inbox
|
||||
.push_back(AgentEvent::ServerSound(propagated_sound));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -132,7 +132,7 @@ pub fn handle_invite(
|
||||
} else if let Some(agent) = agents.get_mut(invitee) {
|
||||
if send_invite() {
|
||||
if let Some(inviter) = uids.get(inviter) {
|
||||
agent.inbox.push_front(AgentEvent::TradeInvite(*inviter));
|
||||
agent.inbox.push_back(AgentEvent::TradeInvite(*inviter));
|
||||
invite_sent = true;
|
||||
}
|
||||
}
|
||||
@ -221,7 +221,7 @@ pub fn handle_invite_accept(server: &mut Server, entity: specs::Entity) {
|
||||
if let Some(agent) = agents.get_mut(inviter) {
|
||||
agent
|
||||
.inbox
|
||||
.push_front(AgentEvent::TradeAccepted(invitee_uid));
|
||||
.push_back(AgentEvent::TradeAccepted(invitee_uid));
|
||||
}
|
||||
#[cfg(feature = "worldgen")]
|
||||
let pricing = agents
|
||||
|
@ -14,7 +14,7 @@ use group_manip::handle_group;
|
||||
use information::handle_site_info;
|
||||
use interaction::{
|
||||
handle_lantern, handle_mine_block, handle_mount, handle_npc_interaction, handle_possess,
|
||||
handle_unmount,
|
||||
handle_sound, handle_unmount,
|
||||
};
|
||||
use inventory_manip::handle_inventory;
|
||||
use invite::{handle_invite, handle_invite_response};
|
||||
@ -110,7 +110,6 @@ impl Server {
|
||||
},
|
||||
ServerEvent::InitiateInvite(interactor, target, kind) => {
|
||||
handle_invite(self, interactor, target, kind)
|
||||
//handle_initiate_trade(self, interactor, target)
|
||||
},
|
||||
ServerEvent::InviteResponse(entity, response) => {
|
||||
handle_invite_response(self, entity, response)
|
||||
@ -217,10 +216,11 @@ impl Server {
|
||||
ServerEvent::CreateSafezone { range, pos } => {
|
||||
self.state.create_safezone(range, pos).build();
|
||||
},
|
||||
ServerEvent::Sound { sound } => handle_sound(self, &sound),
|
||||
}
|
||||
}
|
||||
|
||||
// Generate requested chunks.
|
||||
// Generate requested chunks
|
||||
for (entity, key) in requested_chunks {
|
||||
self.generate_chunk(entity, key);
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ fn notify_agent_simple(
|
||||
event: AgentEvent,
|
||||
) {
|
||||
if let Some(agent) = agents.get_mut(entity) {
|
||||
agent.inbox.push_front(event);
|
||||
agent.inbox.push_back(event);
|
||||
}
|
||||
}
|
||||
|
||||
@ -42,7 +42,7 @@ fn notify_agent_prices(
|
||||
// Box<(tid, pend, _, inventories)>) = event {
|
||||
agent
|
||||
.inbox
|
||||
.push_front(AgentEvent::UpdatePendingTrade(Box::new((
|
||||
.push_back(AgentEvent::UpdatePendingTrade(Box::new((
|
||||
// Prefer using this Agent's price data, but use the counterparty's price
|
||||
// data if we don't have price data
|
||||
boxval.0,
|
||||
|
@ -2,7 +2,9 @@ use crate::rtsim::{Entity as RtSimData, RtSim};
|
||||
use common::{
|
||||
comp::{
|
||||
self,
|
||||
agent::{AgentEvent, Target, DEFAULT_INTERACTION_TIME, TRADE_INTERACTION_TIME},
|
||||
agent::{
|
||||
AgentEvent, Target, DEFAULT_INTERACTION_TIME, MAX_LISTEN_DIST, TRADE_INTERACTION_TIME,
|
||||
},
|
||||
buff::{BuffKind, Buffs},
|
||||
compass::{Direction, Distance},
|
||||
dialogue::{MoodContext, MoodState, Subject},
|
||||
@ -150,7 +152,6 @@ const FLEE_DURATION: f32 = 3.0;
|
||||
const MAX_FOLLOW_DIST: f32 = 12.0;
|
||||
const MAX_CHASE_DIST: f32 = 250.0;
|
||||
const MAX_FLEE_DIST: f32 = 20.0;
|
||||
const LISTEN_DIST: f32 = 16.0;
|
||||
const SEARCH_DIST: f32 = 48.0;
|
||||
const SIGHT_DIST: f32 = 80.0;
|
||||
const SNEAK_COEFFICIENT: f32 = 0.25;
|
||||
@ -158,6 +159,9 @@ const AVG_FOLLOW_DIST: f32 = 6.0;
|
||||
const RETARGETING_THRESHOLD_SECONDS: f64 = 10.0;
|
||||
const HEALING_ITEM_THRESHOLD: f32 = 0.5;
|
||||
const DEFAULT_ATTACK_RANGE: f32 = 2.0;
|
||||
const AWARENESS_INVESTIGATE_THRESHOLD: f32 = 1.0;
|
||||
const AWARENESS_DECREMENT_CONSTANT: f32 = 0.07;
|
||||
const SECONDS_BEFORE_FORGET_SOUNDS: f64 = 180.0;
|
||||
|
||||
/// This system will allow NPCs to modify their controller
|
||||
#[derive(Default)]
|
||||
@ -479,8 +483,7 @@ impl<'a> System<'a> for Sys {
|
||||
{
|
||||
if let Some(tgt_pos) = read_data.positions.get(attacker) {
|
||||
// If the target is dead or in a safezone, remove the
|
||||
// target
|
||||
// and idle.
|
||||
// target and idle.
|
||||
if should_stop_attacking(
|
||||
read_data.healths.get(attacker),
|
||||
read_data.buffs.get(attacker),
|
||||
@ -585,6 +588,9 @@ impl<'a> AgentData<'a> {
|
||||
read_data: &ReadData,
|
||||
event_emitter: &mut Emitter<'_, ServerEvent>,
|
||||
) {
|
||||
decrement_awareness(agent);
|
||||
forget_old_sounds(agent, read_data);
|
||||
|
||||
// Set owner if no target
|
||||
if agent.target.is_none() && thread_rng().gen_bool(0.1) {
|
||||
if let Some(Alignment::Owned(owner)) = self.alignment {
|
||||
@ -599,7 +605,12 @@ impl<'a> AgentData<'a> {
|
||||
}
|
||||
// Interact if incoming messages
|
||||
if !agent.inbox.is_empty() {
|
||||
agent.action_state.timer = 0.1;
|
||||
if !matches!(agent.inbox.front(), Some(AgentEvent::ServerSound(_))) {
|
||||
agent.action_state.timer = 0.1;
|
||||
} else if let Some(AgentEvent::ServerSound(sound)) = agent.inbox.pop_front() {
|
||||
agent.sounds_heard.push(sound);
|
||||
agent.awareness += sound.vol;
|
||||
}
|
||||
}
|
||||
if agent.action_state.timer > 0.0 {
|
||||
if agent.action_state.timer
|
||||
@ -618,6 +629,8 @@ impl<'a> AgentData<'a> {
|
||||
}
|
||||
} else if thread_rng().gen::<f32>() < 0.1 {
|
||||
self.choose_target(agent, controller, &read_data, event_emitter);
|
||||
} else if agent.awareness > AWARENESS_INVESTIGATE_THRESHOLD {
|
||||
self.handle_elevated_awareness(agent, controller, read_data);
|
||||
} else {
|
||||
self.idle(agent, controller, &read_data);
|
||||
}
|
||||
@ -654,13 +667,8 @@ impl<'a> AgentData<'a> {
|
||||
agent.action_state.timer = 0.01;
|
||||
} else if agent.action_state.timer < FLEE_DURATION || dist_sqrd < MAX_FLEE_DIST
|
||||
{
|
||||
self.flee(
|
||||
agent,
|
||||
controller,
|
||||
&read_data.terrain,
|
||||
tgt_pos,
|
||||
&read_data.dt,
|
||||
);
|
||||
self.flee(agent, controller, &read_data.terrain, tgt_pos);
|
||||
agent.action_state.timer += read_data.dt.0;
|
||||
} else {
|
||||
agent.action_state.timer = 0.0;
|
||||
agent.target = None;
|
||||
@ -680,12 +688,10 @@ impl<'a> AgentData<'a> {
|
||||
event_emitter
|
||||
.emit(ServerEvent::Chat(UnresolvedChatMsg::npc(*self.uid, msg)));
|
||||
}
|
||||
|
||||
agent.target = None;
|
||||
// Choose a new target every 10 seconds, but only for
|
||||
// enemies
|
||||
// TODO: This should be more
|
||||
// principled. Consider factoring
|
||||
// TODO: This should be more principled. Consider factoring
|
||||
// health, combat rating, wielded weapon, etc, into the
|
||||
// decision to change target.
|
||||
} else if read_data.time.0 - selected_at > RETARGETING_THRESHOLD_SECONDS
|
||||
@ -951,7 +957,8 @@ impl<'a> AgentData<'a> {
|
||||
// .push(ControlEvent::InviteResponse(InviteResponse::Decline));
|
||||
// }
|
||||
agent.action_state.timer += read_data.dt.0;
|
||||
let msg = agent.inbox.pop_back();
|
||||
|
||||
let msg = agent.inbox.pop_front();
|
||||
match msg {
|
||||
Some(AgentEvent::Talk(by, subject)) => {
|
||||
if agent.behavior.can(BehaviorCapability::SPEAK) {
|
||||
@ -1304,9 +1311,9 @@ impl<'a> AgentData<'a> {
|
||||
}
|
||||
}
|
||||
},
|
||||
None => {
|
||||
_ => {
|
||||
if agent.behavior.can(BehaviorCapability::SPEAK) {
|
||||
// no new events, continue looking towards the last interacting player for some
|
||||
// No new events, continue looking towards the last interacting player for some
|
||||
// time
|
||||
if let Some(Target { target, .. }) = &agent.target {
|
||||
self.look_toward(controller, read_data, target);
|
||||
@ -1348,7 +1355,6 @@ impl<'a> AgentData<'a> {
|
||||
controller: &mut Controller,
|
||||
terrain: &TerrainGrid,
|
||||
tgt_pos: &Pos,
|
||||
dt: &DeltaTime,
|
||||
) {
|
||||
if let Some(body) = self.body {
|
||||
if body.can_strafe() && !self.is_gliding {
|
||||
@ -1375,7 +1381,6 @@ impl<'a> AgentData<'a> {
|
||||
self.jump_if(controller, bearing.z > 1.5);
|
||||
controller.inputs.move_z = bearing.z;
|
||||
}
|
||||
agent.action_state.timer += dt.0;
|
||||
}
|
||||
|
||||
/// Attempt to consume a healing item, and return whether any healing items
|
||||
@ -1457,7 +1462,7 @@ impl<'a> AgentData<'a> {
|
||||
})
|
||||
.filter(|(e, e_pos, e_health, e_stats, e_inventory, e_alignment, char_state)| {
|
||||
let mut search_dist = SEARCH_DIST;
|
||||
let mut listen_dist = LISTEN_DIST;
|
||||
let mut listen_dist = MAX_LISTEN_DIST;
|
||||
if char_state.map_or(false, |c_s| c_s.is_stealthy()) {
|
||||
// TODO: make sneak more effective based on a stat like e_stats.fitness
|
||||
search_dist *= SNEAK_COEFFICIENT;
|
||||
@ -3597,6 +3602,49 @@ impl<'a> AgentData<'a> {
|
||||
controller.inputs.move_z = bearing.z;
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_elevated_awareness(
|
||||
&self,
|
||||
agent: &mut Agent,
|
||||
controller: &mut Controller,
|
||||
read_data: &ReadData,
|
||||
) {
|
||||
// Currently this means that we are in a safezone
|
||||
if invulnerability_is_in_buffs(read_data.buffs.get(*self.entity)) {
|
||||
self.idle(agent, controller, &read_data);
|
||||
return;
|
||||
}
|
||||
|
||||
let is_enemy = matches!(self.alignment, Some(Alignment::Enemy));
|
||||
|
||||
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);
|
||||
|
||||
if is_enemy {
|
||||
let far_enough = dist_sqrd > 10.0_f32.powi(2);
|
||||
|
||||
if far_enough {
|
||||
self.follow(agent, controller, &read_data.terrain, &sound_pos);
|
||||
} else {
|
||||
// TODO: Change this to a search action instead of idle
|
||||
self.idle(agent, controller, &read_data);
|
||||
}
|
||||
} else if self.flees {
|
||||
let aggro = agent.psyche.aggro;
|
||||
let close_enough = dist_sqrd < 35.0_f32.powi(2);
|
||||
let loud_sound = sound.vol >= 10.0;
|
||||
|
||||
if close_enough && (aggro <= 0.5 || (aggro <= 0.7 && loud_sound)) {
|
||||
self.flee(agent, controller, &read_data.terrain, &sound_pos);
|
||||
} else {
|
||||
self.idle(agent, controller, &read_data);
|
||||
}
|
||||
} else {
|
||||
self.idle(agent, controller, &read_data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn can_see_tgt(terrain: &TerrainGrid, pos: &Pos, tgt_pos: &Pos, dist_sqrd: f32) -> bool {
|
||||
@ -3614,6 +3662,8 @@ fn should_stop_attacking(health: Option<&Health>, buffs: Option<&Buffs>) -> bool
|
||||
health.map_or(true, |a| a.is_dead) || invulnerability_is_in_buffs(buffs)
|
||||
}
|
||||
|
||||
// FIXME: The logic that is used in this function and throughout the code
|
||||
// shouldn't be used to mean that a character is in a safezone.
|
||||
fn invulnerability_is_in_buffs(buffs: Option<&Buffs>) -> bool {
|
||||
buffs.map_or(false, |b| b.kinds.contains_key(&BuffKind::Invulnerability))
|
||||
}
|
||||
@ -3648,3 +3698,40 @@ fn aim_projectile(speed: f32, pos: Vec3<f32>, tgt: Vec3<f32>) -> Option<Dir> {
|
||||
|
||||
Dir::from_unnormalized(to_tgt)
|
||||
}
|
||||
|
||||
fn forget_old_sounds(agent: &mut Agent, read_data: &ReadData) {
|
||||
if !agent.sounds_heard.is_empty() {
|
||||
// Keep (retain) only newer sounds
|
||||
agent
|
||||
.sounds_heard
|
||||
.retain(|&sound| read_data.time.0 - sound.time <= SECONDS_BEFORE_FORGET_SOUNDS);
|
||||
}
|
||||
}
|
||||
|
||||
fn decrement_awareness(agent: &mut Agent) {
|
||||
let mut decrement = AWARENESS_DECREMENT_CONSTANT;
|
||||
let awareness = agent.awareness;
|
||||
|
||||
let too_high = awareness >= 100.0;
|
||||
let high = awareness >= 50.0;
|
||||
let medium = awareness >= 30.0;
|
||||
let low = awareness > 15.0;
|
||||
let positive = awareness >= 0.0;
|
||||
let negative = awareness < 0.0;
|
||||
|
||||
if too_high {
|
||||
decrement *= 3.0;
|
||||
} else if high {
|
||||
decrement *= 1.0;
|
||||
} else if medium {
|
||||
decrement *= 2.5;
|
||||
} else if low {
|
||||
decrement *= 0.70;
|
||||
} else if positive {
|
||||
decrement *= 0.5;
|
||||
} else if negative {
|
||||
return;
|
||||
}
|
||||
|
||||
agent.awareness -= decrement;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user