From a1db073af5eec9260d3cbc735154616309689d79 Mon Sep 17 00:00:00 2001 From: holychowders <theholychowders@gmail.com> Date: Wed, 14 Jul 2021 07:40:43 +0000 Subject: [PATCH] Make Guards Defend Villagers --- common/src/comp/agent.rs | 2 + common/src/comp/health.rs | 10 + common/systems/src/beam.rs | 8 +- common/systems/src/projectile.rs | 8 +- common/systems/src/shockwave.rs | 8 +- server/src/events/entity_manipulation.rs | 8 +- server/src/sys/agent.rs | 572 +++++++++++------------ 7 files changed, 308 insertions(+), 308 deletions(-) diff --git a/common/src/comp/agent.rs b/common/src/comp/agent.rs index 07f23ab54a..b46e144636 100644 --- a/common/src/comp/agent.rs +++ b/common/src/comp/agent.rs @@ -301,6 +301,8 @@ pub enum SoundKind { Beam, Shockwave, Utterance(UtteranceKind, Body), + // TODO: unify VillagerAlarm with Utterance + VillagerAlarm, } #[derive(Clone, Debug)] diff --git a/common/src/comp/health.rs b/common/src/comp/health.rs index 3015a01f44..7f39648b34 100644 --- a/common/src/comp/health.rs +++ b/common/src/comp/health.rs @@ -34,6 +34,16 @@ pub enum HealthSource { Unknown, } +impl HealthSource { + pub fn damage_by(&self) -> Option<Uid> { + if let HealthSource::Damage { by, .. } = self { + *by + } else { + None + } + } +} + #[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] pub struct Health { current: u32, diff --git a/common/systems/src/beam.rs b/common/systems/src/beam.rs index c3373a3533..dae48525d2 100644 --- a/common/systems/src/beam.rs +++ b/common/systems/src/beam.rs @@ -91,6 +91,10 @@ impl<'a> System<'a> for Sys { }; let end_time = creation_time + beam_segment.duration.as_secs_f64(); + let beam_owner = beam_segment + .owner + .and_then(|uid| read_data.uid_allocator.retrieve_entity_internal(uid.into())); + let mut rng = thread_rng(); if rng.gen_bool(0.005) { server_events.push(ServerEvent::Sound { @@ -119,10 +123,6 @@ impl<'a> System<'a> for Sys { (beam_segment.speed * (time_since_creation - frame_time)).max(0.0); let frame_end_dist = (beam_segment.speed * time_since_creation).max(frame_start_dist); - let beam_owner = beam_segment - .owner - .and_then(|uid| read_data.uid_allocator.retrieve_entity_internal(uid.into())); - // Group to ignore collisions with // Might make this more nuanced if beams are used for non damage effects let group = beam_owner.and_then(|e| read_data.groups.get(e)); diff --git a/common/systems/src/projectile.rs b/common/systems/src/projectile.rs index a1bab8cd96..e9d7a5a3e8 100644 --- a/common/systems/src/projectile.rs +++ b/common/systems/src/projectile.rs @@ -72,6 +72,10 @@ impl<'a> System<'a> for Sys { ) .join() { + let projectile_owner = projectile + .owner + .and_then(|uid| read_data.uid_allocator.retrieve_entity_internal(uid.into())); + let mut rng = thread_rng(); if physics.on_surface().is_none() && rng.gen_bool(0.05) { server_emitter.emit(ServerEvent::Sound { @@ -83,12 +87,10 @@ impl<'a> System<'a> for Sys { // Hit entity for other in physics.touch_entities.iter().copied() { - let same_group = projectile - .owner + let same_group = projectile_owner // Note: somewhat inefficient since we do the lookup for every touching // entity, but if we pull this out of the loop we would want to do it only // if there is at least one touching entity - .and_then(|uid| read_data.uid_allocator.retrieve_entity_internal(uid.into())) .and_then(|e| read_data.groups.get(e)) .map_or(false, |owner_group| Some(owner_group) == read_data.uid_allocator diff --git a/common/systems/src/shockwave.rs b/common/systems/src/shockwave.rs index 88c1aaaba0..2997821811 100644 --- a/common/systems/src/shockwave.rs +++ b/common/systems/src/shockwave.rs @@ -85,6 +85,10 @@ impl<'a> System<'a> for Sys { let end_time = creation_time + shockwave.duration.as_secs_f64(); + let shockwave_owner = shockwave + .owner + .and_then(|uid| read_data.uid_allocator.retrieve_entity_internal(uid.into())); + let mut rng = thread_rng(); if rng.gen_bool(0.05) { server_emitter.emit(ServerEvent::Sound { @@ -120,10 +124,6 @@ impl<'a> System<'a> for Sys { end: frame_end_dist, }; - let shockwave_owner = shockwave - .owner - .and_then(|uid| read_data.uid_allocator.retrieve_entity_internal(uid.into())); - // Group to ignore collisions with // Might make this more nuanced if shockwaves are used for non damage effects let group = shockwave_owner.and_then(|e| read_data.groups.get(e)); diff --git a/server/src/events/entity_manipulation.rs b/server/src/events/entity_manipulation.rs index 2cb6fe3fe0..345984e2a2 100644 --- a/server/src/events/entity_manipulation.rs +++ b/server/src/events/entity_manipulation.rs @@ -692,6 +692,10 @@ pub fn handle_explosion(server: &Server, pos: Vec3<f32>, explosion: Explosion, o let ecs = &server.state.ecs(); let server_eventbus = ecs.read_resource::<EventBus<ServerEvent>>(); let time = ecs.read_resource::<Time>(); + let owner_entity = owner.and_then(|uid| { + ecs.read_resource::<UidAllocator>() + .retrieve_entity_internal(uid.into()) + }); let explosion_volume = 2.5 * explosion.radius; server_eventbus.emit_now(ServerEvent::Sound { @@ -712,10 +716,6 @@ pub fn handle_explosion(server: &Server, pos: Vec3<f32>, explosion: Explosion, o .any(|e| matches!(e, RadiusEffect::Attack(_))), reagent: explosion.reagent, }); - let owner_entity = owner.and_then(|uid| { - ecs.read_resource::<UidAllocator>() - .retrieve_entity_internal(uid.into()) - }); let groups = ecs.read_storage::<comp::Group>(); // Used to get strength of explosion effects as they falloff over distance diff --git a/server/src/sys/agent.rs b/server/src/sys/agent.rs index f59609ee96..4c9e4c7688 100644 --- a/server/src/sys/agent.rs +++ b/server/src/sys/agent.rs @@ -5,7 +5,8 @@ use common::{ comp::{ self, agent::{ - AgentEvent, Target, DEFAULT_INTERACTION_TIME, MAX_LISTEN_DIST, TRADE_INTERACTION_TIME, + AgentEvent, Sound, SoundKind, Target, DEFAULT_INTERACTION_TIME, MAX_LISTEN_DIST, + TRADE_INTERACTION_TIME, }, buff::{BuffKind, Buffs}, compass::{Direction, Distance}, @@ -399,66 +400,10 @@ impl<'a> System<'a> for Sys { &read_data.terrain, tgt_pos, ); - // Attack target's attacker - } else if tgt_health.last_change.0 < 5.0 - && tgt_health.last_change.1.amount < 0 - { - if let comp::HealthSource::Damage { - by: Some(by), .. - } = tgt_health.last_change.1.cause - { - if let Some(attacker) = read_data - .uid_allocator - .retrieve_entity_internal(by.id()) - { - if agent.target.is_none() { - controller.push_event( - ControlEvent::Utterance( - UtteranceKind::Angry, - ), - ); - } - - agent.target = Some(Target { - target: attacker, - hostile: true, - selected_at: read_data.time.0, - }); - if let Some(tgt_pos) = - read_data.positions.get(attacker) - { - if should_stop_attacking( - read_data.healths.get(attacker), - read_data.buffs.get(attacker), - ) { - agent.target = Some(Target { - target, - hostile: false, - selected_at: read_data.time.0, - }); - data.idle( - agent, controller, &read_data, - ); - } else { - let target_data = TargetData { - pos: tgt_pos, - body: read_data - .bodies - .get(attacker), - scale: read_data - .scales - .get(attacker), - }; - data.attack( - agent, - controller, - &target_data, - &read_data, - ); - } - } - } - } + } else if entity_was_attacked(target, &read_data) { + data.attack_agents_attacker( + agent, &read_data, controller, + ); // Follow owner if too far away and not // fighting } else if dist_sqrd > MAX_FOLLOW_DIST.powi(2) { @@ -500,19 +445,12 @@ impl<'a> System<'a> for Sys { // have a health component match health { Some(health) if health.last_change.0 < DAMAGE_MEMORY_DURATION => { - if let comp::HealthSource::Damage { by: Some(by), .. } = - health.last_change.1.cause - { - if let Some(attacker) = - read_data.uid_allocator.retrieve_entity_internal(by.id()) - { + // TODO: Can most of this match be replaced by the function + // `attack_agents_attacker`? + if let Some(by) = health.last_change.1.cause.damage_by() { + if let Some(attacker) = get_entity_by_id(by.id(), &read_data) { if let Some(tgt_pos) = read_data.positions.get(attacker) { - // If the target is dead or in a safezone, remove the - // target and idle. - if should_stop_attacking( - read_data.healths.get(attacker), - read_data.buffs.get(attacker), - ) { + if should_stop_attacking(attacker, &read_data) { agent.target = None; data.idle_tree( agent, @@ -527,16 +465,11 @@ impl<'a> System<'a> for Sys { )); } - agent.target = Some(Target { - target: attacker, - hostile: true, - selected_at: read_data.time.0, - }); - let target_data = TargetData { - pos: tgt_pos, - body: read_data.bodies.get(attacker), - scale: read_data.scales.get(attacker), - }; + agent.target = + build_target(attacker, true, read_data.time.0); + let target_data = build_target_data( + attacker, tgt_pos, &read_data, + ); data.attack( agent, controller, @@ -548,15 +481,13 @@ impl<'a> System<'a> for Sys { .rtsim_entity .and_then(|_| read_data.stats.get(attacker)) { - agent.rtsim_controller.events.push( - RtSimEvent::AddMemory(Memory { - item: MemoryItem::CharacterFight { - name: tgt_stats.name.clone(), - }, - time_to_forget: read_data.time.0 - + 300.0, - }), - ); + if data.rtsim_entity.is_some() { + remember_fight( + agent, + tgt_stats.name.clone(), + read_data.time.0, + ); + } } } } else { @@ -624,12 +555,8 @@ impl<'a> AgentData<'a> { // Set owner if no target if agent.target.is_none() && thread_rng().gen_bool(0.1) { if let Some(Alignment::Owned(owner)) = self.alignment { - if let Some(owner) = read_data.uid_allocator.retrieve_entity_internal(owner.id()) { - agent.target = Some(Target { - target: owner, - hostile: false, - selected_at: read_data.time.0, - }); + if let Some(owner) = get_entity_by_id(owner.id(), read_data) { + agent.target = build_target(owner, false, read_data.time.0); } } } @@ -711,12 +638,10 @@ impl<'a> AgentData<'a> { let dist_sqrd = self.pos.0.distance_squared(tgt_pos.0); // Should the agent flee? if 1.0 - agent.psyche.aggro > self.damage && self.flees { - if agent.action_state.timer == 0.0 - && agent.behavior.can(BehaviorCapability::SPEAK) - { + if agent.action_state.timer == 0.0 && can_speak(agent) { let msg = "npc.speech.villager_under_attack".to_string(); - event_emitter - .emit(ServerEvent::Chat(UnresolvedChatMsg::npc(*self.uid, msg))); + self.chat_general(msg, event_emitter); + self.emit_villager_alarm(read_data.time.0, event_emitter); agent.action_state.timer = 0.01; } else if agent.action_state.timer < FLEE_DURATION || dist_sqrd < MAX_FLEE_DIST { @@ -732,16 +657,14 @@ impl<'a> AgentData<'a> { } else { // If the hostile entity is dead or has an invulnerability buff (eg, those // applied in safezones), return to idle - if should_stop_attacking( - read_data.healths.get(target), - read_data.buffs.get(target), - ) { + if should_stop_attacking(target, read_data) { if agent.behavior.can(BehaviorCapability::SPEAK) { let msg = "npc.speech.villager_enemy_killed".to_string(); event_emitter .emit(ServerEvent::Chat(UnresolvedChatMsg::npc(*self.uid, msg))); } agent.target = None; + self.idle(agent, controller, read_data); // Choose a new target every 10 seconds, but only for // enemies // TODO: This should be more principled. Consider factoring @@ -753,11 +676,7 @@ impl<'a> AgentData<'a> { self.choose_target(agent, controller, read_data, event_emitter); } else { // TODO Add utility for attacking vs leaving target alone - let target_data = TargetData { - pos: tgt_pos, - body: read_data.bodies.get(target), - scale: read_data.scales.get(target), - }; + let target_data = build_target_data(target, tgt_pos, read_data); self.attack(agent, controller, &target_data, read_data); } } @@ -1021,14 +940,9 @@ impl<'a> AgentData<'a> { let msg = agent.inbox.pop_front(); match msg { Some(AgentEvent::Talk(by, subject)) => { - if agent.behavior.can(BehaviorCapability::SPEAK) { - if let Some(target) = read_data.uid_allocator.retrieve_entity_internal(by.id()) - { - agent.target = Some(Target { - target, - hostile: false, - selected_at: read_data.time.0, - }); + if can_speak(agent) { + if let Some(target) = get_entity_by_id(by.id(), read_data) { + agent.target = build_target(target, false, read_data.time.0); if self.look_toward(controller, read_data, &target) { controller.actions.push(ControlAction::Stand); @@ -1074,19 +988,13 @@ impl<'a> AgentData<'a> { destination_name ) }; - event_emitter.emit(ServerEvent::Chat( - UnresolvedChatMsg::npc(*self.uid, msg), - )); + self.chat_general(msg, event_emitter); } else if agent.behavior.can_trade() { let msg = "npc.speech.merchant_advertisement".to_string(); - event_emitter.emit(ServerEvent::Chat( - UnresolvedChatMsg::npc(*self.uid, msg), - )); + self.chat_general(msg, event_emitter); } else { let msg = "npc.speech.villager".to_string(); - event_emitter.emit(ServerEvent::Chat( - UnresolvedChatMsg::npc(*self.uid, msg), - )); + self.chat_general(msg, event_emitter); } }, Subject::Trade => { @@ -1098,26 +1006,16 @@ impl<'a> AgentData<'a> { )); let msg = "npc.speech.merchant_advertisement".to_string(); - event_emitter.emit(ServerEvent::Chat( - UnresolvedChatMsg::npc(*self.uid, msg), - )); + self.chat_general(msg, event_emitter); } else { - event_emitter.emit(ServerEvent::Chat( - UnresolvedChatMsg::npc( - *self.uid, - "npc.speech.merchant_busy".to_string(), - ), - )); + let msg = "npc.speech.merchant_busy".to_string(); + self.chat_general(msg, event_emitter); } } else { // TODO: maybe make some travellers willing to trade with // simpler goods like potions - event_emitter.emit(ServerEvent::Chat( - UnresolvedChatMsg::npc( - *self.uid, - "npc.speech.villager_decline_trade".to_string(), - ), - )); + let msg = "npc.speech.villager_decline_trade".to_string(); + self.chat_general(msg, event_emitter); } }, Subject::Mood => { @@ -1166,33 +1064,21 @@ impl<'a> AgentData<'a> { MemoryItem::Mood { state } => state.describe(), _ => "".to_string(), }; - event_emitter.emit(ServerEvent::Chat( - UnresolvedChatMsg::npc(*self.uid, msg), - )); + self.chat_general(msg, event_emitter); } } }, Subject::Location(location) => { if let Some(tgt_pos) = read_data.positions.get(target) { - event_emitter.emit(ServerEvent::Chat( - UnresolvedChatMsg::npc( - *self.uid, - format!( - "{} ? I think it's {} {} from here!", - location.name, - Distance::from_dir( - location.origin.as_::<f32>() - - tgt_pos.0.xy() - ) - .name(), - Direction::from_dir( - location.origin.as_::<f32>() - - tgt_pos.0.xy() - ) - .name() - ), - ), - )); + let raw_dir = location.origin.as_::<f32>() - tgt_pos.0.xy(); + let dist = Distance::from_dir(raw_dir).name(); + let dir = Direction::from_dir(raw_dir).name(); + + let msg = format!( + "{} ? I think it's {} {} from here!", + location.name, dist, dir + ); + self.chat_general(msg, event_emitter); } }, Subject::Person(person) => { @@ -1227,9 +1113,7 @@ impl<'a> AgentData<'a> { person.name() ) }; - event_emitter.emit(ServerEvent::Chat( - UnresolvedChatMsg::npc(*self.uid, msg), - )); + self.chat_general(msg, event_emitter); } }, Subject::Work => {}, @@ -1244,14 +1128,8 @@ impl<'a> AgentData<'a> { // stand still and looking towards the trading player controller.actions.push(ControlAction::Stand); controller.actions.push(ControlAction::Talk); - if let Some(target) = - read_data.uid_allocator.retrieve_entity_internal(with.id()) - { - agent.target = Some(Target { - target, - hostile: false, - selected_at: read_data.time.0, - }); + if let Some(target) = get_entity_by_id(with.id(), read_data) { + agent.target = build_target(target, false, read_data.time.0); } controller .events @@ -1262,36 +1140,22 @@ impl<'a> AgentData<'a> { controller .events .push(ControlEvent::InviteResponse(InviteResponse::Decline)); - if agent.behavior.can(BehaviorCapability::SPEAK) { - event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc( - *self.uid, - "npc.speech.merchant_busy".to_string(), - ))); - } + let msg = "npc.speech.merchant_busy".to_string(); + self.chat_general_if_can_speak(agent, msg, event_emitter); } } else { // TODO: Provide a hint where to find the closest merchant? controller .events .push(ControlEvent::InviteResponse(InviteResponse::Decline)); - if agent.behavior.can(BehaviorCapability::SPEAK) { - event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc( - *self.uid, - "npc.speech.villager_decline_trade".to_string(), - ))); - } + let msg = "npc.speech.villager_decline_trade".to_string(); + self.chat_general_if_can_speak(agent, msg, event_emitter); } }, Some(AgentEvent::TradeAccepted(with)) => { if !agent.behavior.is(BehaviorState::TRADING) { - if let Some(target) = - read_data.uid_allocator.retrieve_entity_internal(with.id()) - { - agent.target = Some(Target { - target, - hostile: false, - selected_at: read_data.time.0, - }); + if let Some(target) = get_entity_by_id(with.id(), read_data) { + agent.target = build_target(target, false, read_data.time.0); } agent.behavior.set(BehaviorState::TRADING); agent.behavior.set(BehaviorState::TRADING_ISSUER); @@ -1301,15 +1165,13 @@ impl<'a> AgentData<'a> { if agent.behavior.is(BehaviorState::TRADING) { match result { TradeResult::Completed => { - event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc( - *self.uid, - "npc.speech.merchant_trade_successful".to_string(), - ))) + let msg = "npc.speech.merchant_trade_successful".to_string(); + self.chat_general(msg, event_emitter); + }, + _ => { + let msg = "npc.speech.merchant_trade_declined".to_string(); + self.chat_general(msg, event_emitter); }, - _ => event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc( - *self.uid, - "npc.speech.merchant_trade_declined".to_string(), - ))), } agent.behavior.unset(BehaviorState::TRADING); } @@ -1346,6 +1208,7 @@ impl<'a> AgentData<'a> { balance0 / balance1 * 100.0 ); if let Some(tgt_data) = &agent.target { + // If talking with someone in particular, "tell" it only to them if let Some(with) = read_data.uids.get(tgt_data.target) { event_emitter.emit(ServerEvent::Chat( UnresolvedChatMsg::npc_tell(*self.uid, *with, msg), @@ -1374,7 +1237,7 @@ impl<'a> AgentData<'a> { } }, _ => { - if agent.behavior.can(BehaviorCapability::SPEAK) { + if can_speak(agent) { // No new events, continue looking towards the last interacting player for some // time if let Some(Target { target, .. }) = &agent.target { @@ -1509,7 +1372,6 @@ impl<'a> AgentData<'a> { ) { agent.action_state.timer = 0.0; - // Search area let target = self.cached_spatial_grid.0 .in_circle_aabr(self.pos.0.xy(), SEARCH_DIST) .filter_map(|entity| { @@ -1522,76 +1384,80 @@ impl<'a> AgentData<'a> { (entity, pos, health, stats, inventory, read_data.alignments.get(entity), read_data.char_states.get(entity)) }) }) - .filter(|(e, e_pos, e_health, e_stats, e_inventory, e_alignment, char_state)| { - let mut search_dist = SEARCH_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; - listen_dist *= SNEAK_COEFFICIENT; - } - ((e_pos.0.distance_squared(self.pos.0) < search_dist.powi(2) && - // Within our view - (e_pos.0 - self.pos.0).try_normalized().map(|v| v.dot(*controller.inputs.look_dir) > 0.15).unwrap_or(true)) - // Within listen distance - || e_pos.0.distance_squared(self.pos.0) < listen_dist.powi(2)) // TODO implement proper sound system for agents - && e != self.entity - && !e_health.is_dead - && !invulnerability_is_in_buffs(read_data.buffs.get(*e)) - && (try_owner_alignment(self.alignment, read_data).and_then(|a| try_owner_alignment(*e_alignment, read_data).map(|b| a.hostile_towards(*b))).unwrap_or(false) || ( - if let Some(rtsim_entity) = &self.rtsim_entity { - if agent.behavior.can(BehaviorCapability::SPEAK) { - if rtsim_entity.brain.remembers_fight_with_character(&e_stats.name) { - agent.rtsim_controller.events.push( - RtSimEvent::AddMemory(Memory { - item: MemoryItem::CharacterFight { name: e_stats.name.clone() }, - time_to_forget: read_data.time.0 + 300.0, - }) - ); - let msg = format!("{}! How dare you cross me again!", e_stats.name.clone()); - event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc(*self.uid, msg))); - true - } else { - false - } - } else { - false - } - } else { - false - } - ) || - ( - self.alignment.map_or(false, |alignment| { - if matches!(alignment, Alignment::Npc) && e_inventory.equipped_items().filter(|item| item.tags().contains(&ItemTag::Cultist)).count() > 2 { - if agent.behavior.can(BehaviorCapability::SPEAK) { - if self.rtsim_entity.is_some() { - agent.rtsim_controller.events.push( - RtSimEvent::AddMemory(Memory { - item: MemoryItem::CharacterFight { name: e_stats.name.clone() }, - time_to_forget: read_data.time.0 + 300.0, - }) - ); - } - let msg = "npc.speech.villager_cultist_alarm".to_string(); - event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc(*self.uid, msg))); - } - true - } else { - false - } + .filter(|(e, e_pos, e_health, _e_stats, _e_inventory, _e_alignment, char_state)| { + // Filter based on sight and hearing + let mut search_dist = SEARCH_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; + listen_dist *= SNEAK_COEFFICIENT; + } + ((self.within_range_of(search_dist, e_pos.0) && self.within_view_angle(e_pos.0, controller)) || self.within_range_of(listen_dist, e_pos.0)) // TODO implement proper sound system for agents + && e != self.entity + && !e_health.is_dead + && !invulnerability_is_in_buffs(read_data.buffs.get(*e)) + }) + .filter_map(|(e, e_pos, e_health, e_stats, e_inventory, e_alignment, _char_state)| { + // Hostile entities + try_owner_alignment(self.alignment, read_data).and_then(|a| try_owner_alignment(e_alignment, read_data).map(|b| a.hostile_towards(*b))).unwrap_or(false).then(|| (e, e_pos)) + .or({ + // I'm a guard and a villager is in distress + let other_is_npc = matches!(e_alignment, Some(Alignment::Npc)); + let remembers_damage = e_health.last_change.0 < 5.0;//DAMAGE_MEMORY_DURATION; + read_data.stats.get(*self.entity).map_or(false, |stats| stats.name == "Guard" && other_is_npc && remembers_damage) + .then(|| { + e_health.last_change.1.cause.damage_by() }) - )) - + .flatten() + .and_then(|attacker_uid| { + get_entity_by_id(attacker_uid.id(), read_data) + }) + .and_then(|attacker| { + read_data + .positions + .get(attacker) + .map(|a_pos| (attacker, a_pos)) + }) + }).or({ + // I remember fighting this entity + self.rtsim_entity.and_then(|rtsim_entity| { + rtsim_entity.brain.remembers_fight_with_character(&e_stats.name).then(|| { + if can_speak(agent) { + remember_fight(agent, e_stats.name.clone(), read_data.time.0); + let msg = format!("{}! How dare you cross me again!", e_stats.name.clone()); + self.chat_general(msg, event_emitter); + } + (e, e_pos) + }) + }) + }).or({ + // Cultists! + self.alignment.and_then(|alignment| { + (matches!(alignment, Alignment::Npc) && e_inventory.equipped_items().filter(|item| item.tags().contains(&ItemTag::Cultist)).count() > 2).then(|| { + if can_speak(agent) { + if self.rtsim_entity.is_some() { + remember_fight(agent, e_stats.name.clone(), read_data.time.0); + } + let msg = "npc.speech.villager_cultist_alarm".to_string(); + self.chat_general(msg, event_emitter); + self.emit_villager_alarm(read_data.time.0, event_emitter); + } + (e, e_pos) + }) + }) + }) }) // Can we even see them? - .filter(|(_, e_pos, _, _, _, _, _)| read_data.terrain + .filter(|(_e, e_pos)| { + read_data.terrain .ray(self.pos.0 + Vec3::unit_z(), e_pos.0 + Vec3::unit_z()) .until(Block::is_opaque) .cast() - .0 >= e_pos.0.distance(self.pos.0)) - .min_by_key(|(_, e_pos, _, _, _, _, _)| (e_pos.0.distance_squared(self.pos.0) * 100.0) as i32) // TODO choose target by more than just distance - .map(|(e, _, _, _, _, _, _)| e); + .0 >= e_pos.0.distance(self.pos.0) + }) + .min_by_key(|(_e, e_pos)| (e_pos.0.distance_squared(self.pos.0) * 100.0) as i32) // TODO choose target by more than just distance + .map(|(e, _e_pos)| e); if agent.target.is_none() && target.is_some() { controller.push_event(ControlEvent::Utterance(UtteranceKind::Angry)); @@ -3937,33 +3803,74 @@ impl<'a> AgentData<'a> { 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); + // FIXME: Perhaps name should be a field of the agent, not stats + if let Some(agent_stats) = read_data.stats.get(*self.entity) { + 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); + let is_village_guard = agent_stats.name == *"Guard".to_string(); + let is_neutral = self.flees && !is_village_guard; + let is_enemy = matches!(self.alignment, Some(Alignment::Enemy)); - if far_enough { + 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 is_village_guard { self.follow(agent, controller, &read_data.terrain, &sound_pos); + } else if is_neutral { + let aggro = agent.psyche.aggro; + let close_enough = dist_sqrd < 35.0_f32.powi(2); + let sound_was_loud = sound.vol >= 10.0; + + if close_enough && (aggro <= 0.5 || (aggro <= 0.7 && sound_was_loud)) { + self.flee(agent, controller, &read_data.terrain, &sound_pos); + } else { + self.idle(agent, controller, read_data); + } } 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); + fn attack_agents_attacker( + &self, + agent: &mut Agent, + read_data: &ReadData, + controller: &mut Controller, + ) { + 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.1.cause.damage_by() { + if let Some(attacker) = get_entity_by_id(by.id(), read_data) { + if agent.target.is_none() { + controller.push_event(ControlEvent::Utterance(UtteranceKind::Angry)); + } + + agent.target = build_target(attacker, true, read_data.time.0); + + if let Some(tgt_pos) = read_data.positions.get(attacker) { + if should_stop_attacking(attacker, read_data) { + agent.target = build_target(target, false, read_data.time.0); + + self.idle(agent, controller, read_data); + } else { + let target_data = build_target_data(target, tgt_pos, read_data); + + self.attack(agent, controller, &target_data, read_data); + } + } + } } - } else { - self.idle(agent, controller, read_data); } } } @@ -4013,6 +3920,20 @@ impl<'a> AgentData<'a> { } } + fn chat_general_if_can_speak( + &self, + agent: &Agent, + msg: String, + event_emitter: &mut Emitter<'_, ServerEvent>, + ) -> bool { + if can_speak(agent) { + event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc(*self.uid, msg))); + true + } else { + false + } + } + fn jump_if(&self, controller: &mut Controller, condition: bool) { if condition { controller @@ -4024,6 +3945,27 @@ impl<'a> AgentData<'a> { .push(ControlAction::CancelInput(InputKind::Jump)) } } + + fn chat_general(&self, msg: String, event_emitter: &mut Emitter<'_, ServerEvent>) { + event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc(*self.uid, msg))); + } + + fn emit_villager_alarm(&self, time: f64, event_emitter: &mut Emitter<'_, ServerEvent>) { + event_emitter.emit(ServerEvent::Sound { + sound: Sound::new(SoundKind::VillagerAlarm, self.pos.0, 100.0, time), + }); + } + + fn within_view_angle(&self, tgt_pos: Vec3<f32>, controller: &Controller) -> bool { + (tgt_pos - self.pos.0) + .try_normalized() + .map(|v| v.dot(*controller.inputs.look_dir) > 0.15) + .unwrap_or(true) + } + + fn within_range_of(&self, range: f32, their_pos: Vec3<f32>) -> bool { + their_pos.distance_squared(self.pos.0) < range.powi(2) + } } fn can_see_tgt(terrain: &TerrainGrid, pos: &Pos, tgt_pos: &Pos, dist_sqrd: f32) -> bool { @@ -4037,7 +3979,10 @@ fn can_see_tgt(terrain: &TerrainGrid, pos: &Pos, tgt_pos: &Pos, dist_sqrd: f32) } // If target is dead or has invulnerability buff, returns true -fn should_stop_attacking(health: Option<&Health>, buffs: Option<&Buffs>) -> bool { +fn should_stop_attacking(target: EcsEntity, read_data: &ReadData) -> bool { + let health = read_data.healths.get(target); + let buffs = read_data.buffs.get(target); + health.map_or(true, |a| a.is_dead) || invulnerability_is_in_buffs(buffs) } @@ -4053,10 +3998,7 @@ fn try_owner_alignment<'a>( read_data: &'a ReadData, ) -> Option<&'a Alignment> { if let Some(Alignment::Owned(owner_uid)) = alignment { - if let Some(owner) = read_data - .uid_allocator - .retrieve_entity_internal(owner_uid.id()) - { + if let Some(owner) = get_entity_by_id(owner_uid.id(), read_data) { return read_data.alignments.get(owner); } } @@ -4114,3 +4056,47 @@ fn decrement_awareness(agent: &mut Agent) { agent.awareness -= decrement; } + +fn entity_was_attacked(entity: EcsEntity, read_data: &ReadData) -> bool { + if let Some(entity_health) = read_data.healths.get(entity) { + entity_health.last_change.0 < 5.0 && entity_health.last_change.1.amount < 0 + } else { + false + } +} + +fn build_target(target: EcsEntity, is_hostile: bool, time: f64) -> Option<Target> { + Some(Target { + target, + hostile: is_hostile, + selected_at: time, + }) +} + +fn build_target_data<'a>( + target: EcsEntity, + tgt_pos: &'a Pos, + read_data: &'a ReadData, +) -> TargetData<'a> { + TargetData { + pos: tgt_pos, + body: read_data.bodies.get(target), + scale: read_data.scales.get(target), + } +} + +fn can_speak(agent: &Agent) -> bool { agent.behavior.can(BehaviorCapability::SPEAK) } + +fn remember_fight(agent: &mut Agent, name: String, time: f64) { + agent + .rtsim_controller + .events + .push(RtSimEvent::AddMemory(Memory { + item: MemoryItem::CharacterFight { name }, + time_to_forget: time + 300.0, + })); +} + +fn get_entity_by_id(id: u64, read_data: &ReadData) -> Option<EcsEntity> { + read_data.uid_allocator.retrieve_entity_internal(id) +}