diff --git a/CHANGELOG.md b/CHANGELOG.md index 74995d905c..fbee0afeb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,7 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Skill trees - Lactose tolerant golems - 6 different gems. (Topaz, Amethyst, Sapphire, Emerald, Ruby and Diamond) -- Poise system (not currently accessible to players for balancing reasons) +- Poise system - Snow particles - Basic NPC interaction - Lights in dungeons @@ -97,6 +97,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed a bug where the stairs to the boss floor in dungeons would sometimes not spawn - Fixed waypoints being placed underwater - Objects and golems are not affected by bleed debuff anymore +- Fixed RtSim entity memory loss +- Mandated that merchants not wander away during a trade +- Fixed the villager conception of evil by encouraging them to react violently to characters wearing cultist gear ## [0.8.0] - 2020-11-28 diff --git a/assets/common/items/armor/cultist/belt.ron b/assets/common/items/armor/cultist/belt.ron index c5fdaef53f..43421d8095 100644 --- a/assets/common/items/armor/cultist/belt.ron +++ b/assets/common/items/armor/cultist/belt.ron @@ -9,5 +9,5 @@ ItemDef( ), )), quality: Epic, - tags: [], -) \ No newline at end of file + tags: [Cultist], +) diff --git a/assets/common/items/armor/cultist/chest.ron b/assets/common/items/armor/cultist/chest.ron index 5cf89b6108..be4e9f0040 100644 --- a/assets/common/items/armor/cultist/chest.ron +++ b/assets/common/items/armor/cultist/chest.ron @@ -9,5 +9,5 @@ ItemDef( ), )), quality: Epic, - tags: [], -) \ No newline at end of file + tags: [Cultist], +) diff --git a/assets/common/items/armor/cultist/foot.ron b/assets/common/items/armor/cultist/foot.ron index c95f4ed6cc..ba5caedadc 100644 --- a/assets/common/items/armor/cultist/foot.ron +++ b/assets/common/items/armor/cultist/foot.ron @@ -9,5 +9,5 @@ ItemDef( ), )), quality: Epic, - tags: [], -) \ No newline at end of file + tags: [Cultist], +) diff --git a/assets/common/items/armor/cultist/hand.ron b/assets/common/items/armor/cultist/hand.ron index cf48b137fa..213445d3b4 100644 --- a/assets/common/items/armor/cultist/hand.ron +++ b/assets/common/items/armor/cultist/hand.ron @@ -9,5 +9,5 @@ ItemDef( ), )), quality: Epic, - tags: [], -) \ No newline at end of file + tags: [Cultist], +) diff --git a/assets/common/items/armor/cultist/pants.ron b/assets/common/items/armor/cultist/pants.ron index df7ecd9303..2a48813088 100644 --- a/assets/common/items/armor/cultist/pants.ron +++ b/assets/common/items/armor/cultist/pants.ron @@ -9,5 +9,5 @@ ItemDef( ), )), quality: Epic, - tags: [], -) \ No newline at end of file + tags: [Cultist], +) diff --git a/assets/common/items/armor/cultist/shoulder.ron b/assets/common/items/armor/cultist/shoulder.ron index ebbf339023..58a03db82d 100644 --- a/assets/common/items/armor/cultist/shoulder.ron +++ b/assets/common/items/armor/cultist/shoulder.ron @@ -9,5 +9,5 @@ ItemDef( ), )), quality: Epic, - tags: [], -) \ No newline at end of file + tags: [Cultist], +) diff --git a/assets/common/items/tag_examples/cultist.ron b/assets/common/items/tag_examples/cultist.ron new file mode 100644 index 0000000000..a2d33d9fa4 --- /dev/null +++ b/assets/common/items/tag_examples/cultist.ron @@ -0,0 +1,18 @@ +ItemDef( + name: "Anything related to cultists", + description: "These items are a little creepy.", + kind: TagExamples( + item_ids: [ + "common.items.armor.cultist.belt", + "common.items.armor.cultist.chest", + "common.items.armor.cultist.pants", + "common.items.armor.cultist.foot", + "common.items.armor.cultist.hand", + "common.items.armor.cultist.shoulder", + ], + ), + quality: Common, + tags: [], +) + + diff --git a/assets/voxygen/i18n/en/_manifest.ron b/assets/voxygen/i18n/en/_manifest.ron index 18e353c7d8..655124a619 100644 --- a/assets/voxygen/i18n/en/_manifest.ron +++ b/assets/voxygen/i18n/en/_manifest.ron @@ -88,6 +88,20 @@ "I wish someone would keep the wolves away from the village.", "I had a wonderful dream about cheese last night. What does it mean?", ], + "npc.speech.villager_cultist_alarm": [ + "Lookout! There is a cultist on the loose!", + "To arms! The cultists are attacking!", + "How dare the cultists attack our village!", + "Death to the cultists!", + "Cultists will not be tolerated here!", + "Murderous cultist!", + "Taste the edge of my sword, you dirty cultist!", + "Nothing can clean the blood from your hands, cultist!", + "Billions of blistering blue barnacles! A cultist among us!", + "The evils of this cultist are about to be over!", + "This cultist is mine!", + "Prepare to meet your maker, foul cultist!", + ], "npc.speech.villager_under_attack": [ "Help, I'm under attack!", "Help! I'm under attack!", diff --git a/client/src/lib.rs b/client/src/lib.rs index 27bf2f7c90..1edebd7015 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -2124,6 +2124,7 @@ impl Client { // NPCs can't talk. Should be filtered by hud/mod.rs for voxygen and should be filtered // by server (due to not having a Pos) for chat-cli comp::ChatType::Npc(_uid, _r) => "".to_string(), + comp::ChatType::NpcSay(uid, _r) => message_format(uid, message, None), comp::ChatType::Meta => message.to_string(), } } diff --git a/common/src/comp/chat.rs b/common/src/comp/chat.rs index b0c0470760..9ad3ff27a2 100644 --- a/common/src/comp/chat.rs +++ b/common/src/comp/chat.rs @@ -106,6 +106,8 @@ pub enum ChatType { /// /// The u16 field is a random number for selecting localization variants. Npc(Uid, u16), + /// From NPCs but in the chat for clients in the near vicinity + NpcSay(Uid, u16), /// Anything else Meta, // Looted items @@ -136,6 +138,7 @@ pub type UnresolvedChatMsg = GenericChatMsg; impl GenericChatMsg { pub const NPC_DISTANCE: f32 = 100.0; + pub const NPC_SAY_DISTANCE: f32 = 30.0; pub const REGION_DISTANCE: f32 = 1000.0; pub const SAY_DISTANCE: f32 = 100.0; @@ -144,6 +147,11 @@ impl GenericChatMsg { Self { chat_type, message } } + pub fn npc_say(uid: Uid, message: String) -> Self { + let chat_type = ChatType::NpcSay(uid, rand::random()); + Self { chat_type, message } + } + pub fn map_group(self, mut f: impl FnMut(G) -> T) -> GenericChatMsg { let chat_type = match self.chat_type { ChatType::Online(a) => ChatType::Online(a), @@ -161,6 +169,7 @@ impl GenericChatMsg { ChatType::Region(a) => ChatType::Region(a), ChatType::World(a) => ChatType::World(a), ChatType::Npc(a, b) => ChatType::Npc(a, b), + ChatType::NpcSay(a, b) => ChatType::NpcSay(a, b), ChatType::Meta => ChatType::Meta, }; @@ -172,7 +181,7 @@ impl GenericChatMsg { pub fn to_bubble(&self) -> Option<(SpeechBubble, Uid)> { let icon = self.icon(); - if let ChatType::Npc(from, r) = self.chat_type { + if let ChatType::Npc(from, r) | ChatType::NpcSay(from, r) = self.chat_type { Some((SpeechBubble::npc_new(&self.message, r, icon), from)) } else { self.uid() @@ -197,6 +206,7 @@ impl GenericChatMsg { ChatType::Region(_u) => SpeechBubbleType::Region, ChatType::World(_u) => SpeechBubbleType::World, ChatType::Npc(_u, _r) => SpeechBubbleType::None, + ChatType::NpcSay(_u, _r) => SpeechBubbleType::Say, ChatType::Meta => SpeechBubbleType::None, } } @@ -218,6 +228,7 @@ impl GenericChatMsg { ChatType::Region(u) => Some(*u), ChatType::World(u) => Some(*u), ChatType::Npc(u, _r) => Some(*u), + ChatType::NpcSay(u, _r) => Some(*u), ChatType::Meta => None, } } diff --git a/common/src/comp/inventory/item/mod.rs b/common/src/comp/inventory/item/mod.rs index f05ae30667..8cb1089f28 100644 --- a/common/src/comp/inventory/item/mod.rs +++ b/common/src/comp/inventory/item/mod.rs @@ -94,6 +94,7 @@ pub enum ItemTag { ClothItem, ModularComponent(ModularComponentTag), MetalIngot, + Cultist, } impl TagExampleInfo for ItemTag { @@ -102,6 +103,7 @@ impl TagExampleInfo for ItemTag { ItemTag::ClothItem => "cloth item", ItemTag::ModularComponent(kind) => kind.name(), ItemTag::MetalIngot => "metal ingot", + ItemTag::Cultist => "cultist", } } @@ -110,6 +112,7 @@ impl TagExampleInfo for ItemTag { ItemTag::ClothItem => "common.items.tag_examples.cloth_item", ItemTag::ModularComponent(tag) => tag.exemplar_identifier(), ItemTag::MetalIngot => "common.items.tag_examples.metal_ingot", + ItemTag::Cultist => "common.items.tag_examples.cultist", } } } diff --git a/common/src/rtsim.rs b/common/src/rtsim.rs index 49a831e12d..6d9e564e0a 100644 --- a/common/src/rtsim.rs +++ b/common/src/rtsim.rs @@ -16,6 +16,26 @@ impl Component for RtSimEntity { type Storage = IdvStorage; } +#[derive(Clone, Debug)] +pub enum RtSimEvent { + AddMemory(Memory), + PrintMemories, +} + +#[derive(Clone, Debug)] +pub struct Memory { + pub item: MemoryItem, + pub time_to_forget: f64, +} + +#[derive(Clone, Debug)] +pub enum MemoryItem { + // These are structs to allow more data beyond name to be stored + // such as clothing worn, weapon used, etc. + CharacterInteraction { name: String }, + CharacterFight { name: String }, +} + /// This type is the map route through which the rtsim (real-time simulation) /// aspect of the game communicates with the rest of the game. It is analagous /// to `comp::Controller` in that it provides a consistent interface for @@ -33,6 +53,8 @@ pub struct RtSimController { pub travel_to: Option<(Vec3, String)>, /// Proportion of full speed to move pub speed_factor: f32, + /// Events + pub events: Vec, } impl Default for RtSimController { @@ -40,6 +62,7 @@ impl Default for RtSimController { Self { travel_to: None, speed_factor: 1.0, + events: Vec::new(), } } } @@ -51,6 +74,7 @@ impl RtSimController { Self { travel_to: Some((pos, format!("{:0.1?}", pos))), speed_factor: 0.25, + events: Vec::new(), } } } diff --git a/server/src/rtsim/entity.rs b/server/src/rtsim/entity.rs index 05b96fcbf2..7a038bed20 100644 --- a/server/src/rtsim/entity.rs +++ b/server/src/rtsim/entity.rs @@ -1,5 +1,11 @@ use super::*; -use common::{comp::inventory::loadout_builder::LoadoutBuilder, store::Id, terrain::TerrainGrid}; +use common::{ + comp::inventory::loadout_builder::LoadoutBuilder, + resources::Time, + rtsim::{Memory, MemoryItem}, + store::Id, + terrain::TerrainGrid, +}; use world::{ civ::{Site, Track}, util::RandomPerm, @@ -127,7 +133,7 @@ impl Entity { .build() } - pub fn tick(&mut self, terrain: &TerrainGrid, world: &World, index: &IndexRef) { + pub fn tick(&mut self, time: &Time, terrain: &TerrainGrid, world: &World, index: &IndexRef) { let tgt_site = self.brain.tgt.or_else(|| { world .civs() @@ -183,6 +189,11 @@ impl Entity { self.controller.travel_to = Some((travel_to, destination_name)); self.controller.speed_factor = 0.70; }); + + // Forget old memories + self.brain + .memories + .retain(|memory| memory.time_to_forget > time.0); } } @@ -190,4 +201,17 @@ impl Entity { pub struct Brain { tgt: Option>, track: Option<(Track, usize)>, + memories: Vec, +} + +impl Brain { + pub fn add_memory(&mut self, memory: Memory) { self.memories.push(memory); } + + pub fn remembers_character(&self, name_to_remember: &str) -> bool { + self.memories.iter().any(|memory| matches!(&memory.item, MemoryItem::CharacterInteraction { name, .. } if name == name_to_remember)) + } + + pub fn remembers_fight_with_character(&self, name_to_remember: &str) -> bool { + self.memories.iter().any(|memory| matches!(&memory.item, MemoryItem::CharacterFight { name, .. } if name == name_to_remember)) + } } diff --git a/server/src/rtsim/mod.rs b/server/src/rtsim/mod.rs index 2b193cc645..84ce0cbc5f 100644 --- a/server/src/rtsim/mod.rs +++ b/server/src/rtsim/mod.rs @@ -6,10 +6,10 @@ mod load_chunks; mod tick; mod unload_chunks; -use self::{chunks::Chunks, entity::Entity}; +use self::chunks::Chunks; use common::{ comp, - rtsim::{RtSimController, RtSimEntity, RtSimId}, + rtsim::{Memory, RtSimController, RtSimEntity, RtSimId}, terrain::TerrainChunk, vol::RectRasterableVol, }; @@ -20,6 +20,8 @@ use slab::Slab; use specs::{DispatcherBuilder, WorldExt}; use vek::*; +pub use self::entity::Entity; + pub struct RtSim { tick: u64, chunks: Chunks, @@ -71,6 +73,14 @@ impl RtSim { // tracing::info!("Destroyed rtsim entity {}", entity); self.entities.remove(entity); } + + pub fn get_entity(&self, entity: RtSimId) -> Option<&Entity> { self.entities.get(entity) } + + pub fn insert_entity_memory(&mut self, entity: RtSimId, memory: Memory) { + self.entities + .get_mut(entity) + .map(|entity| entity.brain.add_memory(memory)); + } } pub fn add_server_systems(dispatch_builder: &mut DispatcherBuilder) { diff --git a/server/src/rtsim/tick.rs b/server/src/rtsim/tick.rs index 13ab05faaf..90d527ebf8 100644 --- a/server/src/rtsim/tick.rs +++ b/server/src/rtsim/tick.rs @@ -5,7 +5,7 @@ use common::{ comp, comp::inventory::loadout_builder::LoadoutBuilder, event::{EventBus, ServerEvent}, - resources::DeltaTime, + resources::{DeltaTime, Time}, terrain::TerrainGrid, }; use common_ecs::{Job, Origin, Phase, System}; @@ -19,6 +19,7 @@ pub struct Sys; impl<'a> System<'a> for Sys { #[allow(clippy::type_complexity)] type SystemData = ( + Read<'a, Time>, Read<'a, DeltaTime>, Read<'a, EventBus>, WriteExpect<'a, RtSim>, @@ -37,6 +38,7 @@ impl<'a> System<'a> for Sys { fn run( _job: &mut Job, ( + time, dt, server_event_bus, mut rtsim, @@ -56,7 +58,7 @@ impl<'a> System<'a> for Sys { let mut to_reify = Vec::new(); for (id, entity) in rtsim.entities.iter_mut() { if entity.is_loaded { - // No load-specific behaviour yet + // Nothing here yet } else if rtsim .chunks .chunk_at(entity.pos.xy()) @@ -87,7 +89,7 @@ impl<'a> System<'a> for Sys { // Tick entity AI if entity.last_tick + ENTITY_TICK_PERIOD <= rtsim.tick { - entity.tick(&terrain, &world, &index.as_index_ref()); + entity.tick(&time, &terrain, &world, &index.as_index_ref()); entity.last_tick = rtsim.tick; } } diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index 4e9369a939..e0069e0dda 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -523,7 +523,18 @@ impl StateExt for State { } } }, - + comp::ChatType::NpcSay(uid, _r) => { + let entity_opt = + (*ecs.read_resource::()).retrieve_entity_internal(uid.0); + let positions = ecs.read_storage::(); + if let Some(speaker_pos) = entity_opt.and_then(|e| positions.get(e)) { + for (client, pos) in (&ecs.read_storage::(), &positions).join() { + if is_within(comp::ChatMsg::NPC_SAY_DISTANCE, pos, speaker_pos) { + client.send_fallible(ServerGeneral::ChatMsg(resolved_msg.clone())); + } + } + } + }, comp::ChatType::FactionMeta(s) | comp::ChatType::Faction(_, s) => { for (client, faction) in ( &ecs.read_storage::(), diff --git a/server/src/sys/agent.rs b/server/src/sys/agent.rs index 4abf971136..54e390b6eb 100644 --- a/server/src/sys/agent.rs +++ b/server/src/sys/agent.rs @@ -1,13 +1,14 @@ +use crate::rtsim::{Entity as RtSimData, RtSim}; use common::{ comp::{ self, agent::{AgentEvent, Tactic, Target, DEFAULT_INTERACTION_TIME, TRADE_INTERACTION_TIME}, group, - inventory::{slot::EquipSlot, trade_pricing::TradePricing}, + inventory::{item::ItemTag, slot::EquipSlot, trade_pricing::TradePricing}, invite::InviteResponse, item::{ tool::{ToolKind, UniqueKind}, - ItemKind, + ItemDesc, ItemKind, }, skills::{AxeSkill, BowSkill, HammerSkill, Skill, StaffSkill, SwordSkill}, Agent, Alignment, Body, CharacterState, ControlAction, ControlEvent, Controller, Energy, @@ -16,7 +17,8 @@ use common::{ }, event::{Emitter, EventBus, ServerEvent}, path::TraversalConfig, - resources::{DeltaTime, TimeOfDay}, + resources::{DeltaTime, Time, TimeOfDay}, + rtsim::{Memory, MemoryItem, RtSimEntity, RtSimEvent}, terrain::{Block, TerrainGrid}, time::DayPeriod, trade::{Good, TradeAction, TradePhase, TradeResult}, @@ -32,13 +34,14 @@ use specs::{ saveload::{Marker, MarkerAllocator}, shred::ResourceId, Entities, Entity as EcsEntity, Join, ParJoin, Read, ReadExpect, ReadStorage, SystemData, World, - Write, WriteStorage, + Write, WriteExpect, WriteStorage, }; use std::{f32::consts::PI, sync::Arc}; use vek::*; struct AgentData<'a> { entity: &'a EcsEntity, + rtsim_entity: Option<&'a RtSimData>, uid: &'a Uid, pos: &'a Pos, vel: &'a Vel, @@ -63,6 +66,7 @@ pub struct ReadData<'a> { entities: Entities<'a>, uid_allocator: Read<'a, UidAllocator>, dt: Read<'a, DeltaTime>, + time: Read<'a, Time>, group_manager: Read<'a, group::GroupManager>, energies: ReadStorage<'a, Energy>, positions: ReadStorage<'a, Pos>, @@ -83,6 +87,7 @@ pub struct ReadData<'a> { time_of_day: Read<'a, TimeOfDay>, light_emitter: ReadStorage<'a, LightEmitter>, world: ReadExpect<'a, Arc>, + rtsim_entities: ReadStorage<'a, RtSimEntity>, } // This is 3.1 to last longer than the last damage timer (3.0 seconds) @@ -107,6 +112,7 @@ impl<'a> System<'a> for Sys { Write<'a, EventBus>, WriteStorage<'a, Agent>, WriteStorage<'a, Controller>, + WriteExpect<'a, RtSim>, ); const NAME: &'static str = "agent"; @@ -116,8 +122,9 @@ impl<'a> System<'a> for Sys { #[allow(clippy::or_fun_call)] // TODO: Pending review in #587 fn run( job: &mut Job, - (read_data, event_bus, mut agents, mut controllers): Self::SystemData, + (read_data, event_bus, mut agents, mut controllers, mut rtsim): Self::SystemData, ) { + let rtsim = &mut *rtsim; job.cpu_stats.measure(ParMode::Rayon); ( &read_data.entities, @@ -220,12 +227,16 @@ impl<'a> System<'a> for Sys { let flees = alignment .map(|a| !matches!(a, Alignment::Enemy | Alignment::Owned(_))) .unwrap_or(true); - let damage = health.current() as f32 / health.maximum() as f32; + let rtsim_entity = read_data + .rtsim_entities + .get(entity) + .and_then(|rtsim_ent| rtsim.get_entity(rtsim_ent.0)); // Package all this agent's data into a convenient struct let data = AgentData { entity: &entity, + rtsim_entity, uid, pos, vel, @@ -404,6 +415,20 @@ impl<'a> System<'a> for Sys { read_data.bodies.get(attacker), &read_data.dt, ); + // Remember this encounter if an RtSim entity + if let Some(tgt_stats) = read_data.stats.get(attacker) { + if data.rtsim_entity.is_some() { + 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, + }), + ); + } + } } } else { agent.target = None; @@ -428,6 +453,15 @@ impl<'a> System<'a> for Sys { debug_assert!(controller.inputs.look_dir.map(|e| !e.is_nan()).reduce_and()); }, ); + for (agent, rtsim_entity) in (&mut agents, &read_data.rtsim_entities).join() { + // Entity must be loaded in as it has an agent component :) + // React to all events in the controller + for event in core::mem::take(&mut agent.rtsim_controller.events) { + if let RtSimEvent::AddMemory(memory) = event { + rtsim.insert_entity_memory(rtsim_entity.0, memory.clone()); + } + } + } } } @@ -472,7 +506,7 @@ impl<'a> AgentData<'a> { self.idle(agent, controller, &read_data); } } else if thread_rng().gen::() < 0.1 { - self.choose_target(agent, controller, &read_data); + self.choose_target(agent, controller, &read_data, event_emitter); } else { self.idle(agent, controller, &read_data); } @@ -775,16 +809,43 @@ impl<'a> AgentData<'a> { ) { controller.inputs.look_dir = dir; } - controller.actions.push(ControlAction::Stand); controller.actions.push(ControlAction::Talk); - if let Some((_travel_to, destination_name)) = - &agent.rtsim_controller.travel_to + if let (Some((_travel_to, destination_name)), Some(rtsim_entity)) = + (&agent.rtsim_controller.travel_to, &self.rtsim_entity) { - let msg = format!( - "I'm heading to {}! Want to come along?", - destination_name - ); - event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc( + let msg = if let Some(tgt_stats) = read_data.stats.get(target) { + agent.rtsim_controller.events.push(RtSimEvent::AddMemory( + Memory { + item: MemoryItem::CharacterInteraction { + name: tgt_stats.name.clone(), + }, + time_to_forget: read_data.time.0 + 600.0, + }, + )); + if rtsim_entity.brain.remembers_character(&tgt_stats.name) { + format!( + "Greetings fair {}! It has been far too long since \ + last I saw you.", + &tgt_stats.name + ) + } else { + format!( + "I'm heading to {}! Want to come along?", + destination_name + ) + } + } else { + format!( + "I'm heading to {}! Want to come along?", + destination_name + ) + }; + event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc_say( + *self.uid, msg, + ))); + } else if agent.trade_for_site.is_some() { + let msg = "Can I interest you in a trade?".to_string(); + event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc_say( *self.uid, msg, ))); } else { @@ -800,7 +861,6 @@ impl<'a> AgentData<'a> { Some(AgentEvent::TradeInvite(_with)) => { if agent.trade_for_site.is_some() && !agent.trading { // stand still and looking towards the trading player - controller.actions.push(ControlAction::Stand); controller.actions.push(ControlAction::Talk); controller .events @@ -817,12 +877,12 @@ impl<'a> AgentData<'a> { if agent.trading { match result { TradeResult::Completed => { - event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc( + event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc_say( *self.uid, "Thank you for trading with me!".to_string(), ))) }, - _ => event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc( + _ => event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc_say( *self.uid, "Maybe another time, have a good day!".to_string(), ))), @@ -892,8 +952,9 @@ impl<'a> AgentData<'a> { "That only covers {:.1}% of my costs!", balance0 / balance1 * 100.0 ); - event_emitter - .emit(ServerEvent::Chat(UnresolvedChatMsg::npc(*self.uid, msg))); + event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc_say( + *self.uid, msg, + ))); } if pending.phase != TradePhase::Mutate { // we got into the review phase but without balanced goods, decline @@ -981,14 +1042,20 @@ impl<'a> AgentData<'a> { agent.action_timer += dt.0; } - fn choose_target(&self, agent: &mut Agent, controller: &mut Controller, read_data: &ReadData) { + fn choose_target( + &self, + agent: &mut Agent, + controller: &mut Controller, + read_data: &ReadData, + event_emitter: &mut Emitter<'_, ServerEvent>, + ) { agent.action_timer = 0.0; // Search for new targets (this looks expensive, but it's only run occasionally) // TODO: Replace this with a better system that doesn't consider *all* entities - let target = (&read_data.entities, &read_data.positions, &read_data.healths, read_data.alignments.maybe(), read_data.char_states.maybe()) + let target = (&read_data.entities, &read_data.positions, &read_data.healths, &read_data.stats, &read_data.inventories, read_data.alignments.maybe(), read_data.char_states.maybe()) .join() - .filter(|(e, e_pos, e_health, e_alignment, char_state)| { + .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; if char_state.map_or(false, |c_s| c_s.is_stealthy()) { @@ -1003,16 +1070,56 @@ impl<'a> AgentData<'a> { || e_pos.0.distance_squared(self.pos.0) < listen_dist.powi(2)) // TODO implement proper sound system for agents && e != self.entity && !e_health.is_dead - && self.alignment.and_then(|a| e_alignment.map(|b| a.hostile_towards(*b))).unwrap_or(false) + && (self.alignment.and_then(|a| e_alignment.map(|b| a.hostile_towards(*b))).unwrap_or(false) || ( + if let Some(rtsim_entity) = &self.rtsim_entity { + 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_say(*self.uid, msg))); + true + } 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.can_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 + } + }) + )) + }) // Can we even see them? - .filter(|(_, e_pos, _, _, _)| read_data.terrain + .filter(|(_, e_pos, _, _, _, _, _)| read_data.terrain .ray(self.pos.0 + Vec3::unit_z(), e_pos.0 + Vec3::unit_z()) .until(Block::is_opaque) .cast() .0 >= e_pos.0.distance(self.pos.0)) - .min_by_key(|(_, e_pos, _, _, _)| (e_pos.0.distance_squared(self.pos.0) * 100.0) as i32) // TODO choose target by more than just distance - .map(|(e, _, _, _, _)| e); + .min_by_key(|(_, e_pos, _, _, _, _, _)| (e_pos.0.distance_squared(self.pos.0) * 100.0) as i32) // TODO choose target by more than just distance + .map(|(e, _, _, _, _, _, _)| e); if let Some(target) = target { agent.target = Some(Target { target, diff --git a/voxygen/src/hud/chat.rs b/voxygen/src/hud/chat.rs index e38d3958cf..af6b331632 100644 --- a/voxygen/src/hud/chat.rs +++ b/voxygen/src/hud/chat.rs @@ -613,7 +613,8 @@ fn render_chat_line(chat_type: &ChatType, imgs: &Imgs) -> (Color, conrod ChatType::Faction(_uid, _s) => (FACTION_COLOR, imgs.chat_faction_small), ChatType::Region(_uid) => (REGION_COLOR, imgs.chat_region_small), ChatType::World(_uid) => (WORLD_COLOR, imgs.chat_world_small), - ChatType::Npc(_uid, _r) => panic!("NPCs can't talk"), // Should be filtered by hud/mod.rs + ChatType::Npc(_uid, _r) => panic!("NPCs can't talk!"), // Should be filtered by hud/mod.rs + ChatType::NpcSay(_uid, _r) => (SAY_COLOR, imgs.chat_say_small), ChatType::Meta => (INFO_COLOR, imgs.chat_command_info_small), } } diff --git a/voxygen/src/hud/util.rs b/voxygen/src/hud/util.rs index a5bca89126..2ae0578539 100644 --- a/voxygen/src/hud/util.rs +++ b/voxygen/src/hud/util.rs @@ -186,16 +186,14 @@ fn armor_desc(armor: &Armor, desc: &str, slots: u16) -> String { Protection::Normal(a) => a.to_string(), Protection::Invincible => "Inf".to_string(), }; - //let armor_poise_resilience = match armor.get_poise_resilience() { - // Protection::Normal(a) => a.to_string(), - // Protection::Invincible => "Inf".to_string(), - //}; + let armor_poise_resilience = match armor.get_poise_resilience() { + Protection::Normal(a) => a.to_string(), + Protection::Invincible => "Inf".to_string(), + }; let mut description = format!( - "{}\n\nArmor: {}", - //"{}\n\nArmor: {}\n\nPoise Resilience: {}", - kind, - armor_protection, /* armor_poise_resilience // Add back when we are ready for poise */ + "{}\n\nArmor: {}\n\nPoise Resilience: {}", + kind, armor_protection, armor_poise_resilience ); if !desc.is_empty() { @@ -235,7 +233,6 @@ fn tool_desc(tool: &Tool, components: &[Item], msm: &MaterialStatManifest, desc: // Get tool stats let stats = tool.stats.resolve_stats(msm, components).clamp_speed(); - //let poise_strength = tool.base_poise_strength(); let hands = match tool.hands { Hands::One => "One", Hands::Two => "Two", @@ -260,13 +257,9 @@ fn tool_desc(tool: &Tool, components: &[Item], msm: &MaterialStatManifest, desc: fn statblock_desc(stats: &Stats) -> String { format!( - "DPS: {:0.1}\n\nPower: {:0.1}\n\nSpeed: {:0.1}\n\n", - // add back when ready for poise - //"{}\n\nDPS: {:0.1}\n\nPower: {:0.1}\n\nPoise Strength: {:0.1}\n\nSpeed: \ - // {:0.1}\n\n{}\n\n", - stats.speed * stats.power * 10.0, // Damage per second + "Power: {:0.1}\n\nPoise Strength: {:0.1}\n\nSpeed: {:0.1}\n\n", stats.power * 10.0, - //stats.poise_strength * 10.0, + stats.poise_strength * 10.0, stats.speed, ) + &format!( "Critical chance: {:0.1}%\n\nCritical multiplier: {:0.1}x\n\n", @@ -339,8 +332,9 @@ mod tests { ingredient_desc("mushrooms", "common.items.food.mushroom", &testmsm) ); assert_eq!( - "Crafting Ingredient\n\nA bronze ingot.\n\nStat multipliers:\nDPS: 210.0\n\nPower: \ - 30.0\n\nSpeed: 7.0\n\nCritical chance: 50.0%\n\nCritical multiplier: 2.0x\n\n", + "Crafting Ingredient\n\nA bronze ingot.\n\nStat multipliers:\nPower: 30.0\n\nPoise \ + Strength: 50.0\n\nSpeed: 7.0\n\nCritical chance: 50.0%\n\nCritical multiplier: \ + 2.0x\n\n", ingredient_desc( "A bronze ingot.", "common.items.crafting_ing.bronze_ingot",