Various RtSim and Agent Interaction Fixes

This commit is contained in:
James Melkonian 2021-03-16 01:30:35 +00:00 committed by Marcel
parent 39ac300bef
commit 6ea43cfd75
20 changed files with 290 additions and 67 deletions

View File

@ -27,7 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Skill trees - Skill trees
- Lactose tolerant golems - Lactose tolerant golems
- 6 different gems. (Topaz, Amethyst, Sapphire, Emerald, Ruby and Diamond) - 6 different gems. (Topaz, Amethyst, Sapphire, Emerald, Ruby and Diamond)
- Poise system (not currently accessible to players for balancing reasons) - Poise system
- Snow particles - Snow particles
- Basic NPC interaction - Basic NPC interaction
- Lights in dungeons - 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 a bug where the stairs to the boss floor in dungeons would sometimes not spawn
- Fixed waypoints being placed underwater - Fixed waypoints being placed underwater
- Objects and golems are not affected by bleed debuff anymore - 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 ## [0.8.0] - 2020-11-28

View File

@ -9,5 +9,5 @@ ItemDef(
), ),
)), )),
quality: Epic, quality: Epic,
tags: [], tags: [Cultist],
) )

View File

@ -9,5 +9,5 @@ ItemDef(
), ),
)), )),
quality: Epic, quality: Epic,
tags: [], tags: [Cultist],
) )

View File

@ -9,5 +9,5 @@ ItemDef(
), ),
)), )),
quality: Epic, quality: Epic,
tags: [], tags: [Cultist],
) )

View File

@ -9,5 +9,5 @@ ItemDef(
), ),
)), )),
quality: Epic, quality: Epic,
tags: [], tags: [Cultist],
) )

View File

@ -9,5 +9,5 @@ ItemDef(
), ),
)), )),
quality: Epic, quality: Epic,
tags: [], tags: [Cultist],
) )

View File

@ -9,5 +9,5 @@ ItemDef(
), ),
)), )),
quality: Epic, quality: Epic,
tags: [], tags: [Cultist],
) )

View File

@ -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: [],
)

View File

@ -88,6 +88,20 @@
"I wish someone would keep the wolves away from the village.", "I wish someone would keep the wolves away from the village.",
"I had a wonderful dream about cheese last night. What does it mean?", "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": [ "npc.speech.villager_under_attack": [
"Help, I'm under attack!", "Help, I'm under attack!",
"Help! I'm under attack!", "Help! I'm under attack!",

View File

@ -2124,6 +2124,7 @@ impl Client {
// NPCs can't talk. Should be filtered by hud/mod.rs for voxygen and should be filtered // 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 // by server (due to not having a Pos) for chat-cli
comp::ChatType::Npc(_uid, _r) => "".to_string(), comp::ChatType::Npc(_uid, _r) => "".to_string(),
comp::ChatType::NpcSay(uid, _r) => message_format(uid, message, None),
comp::ChatType::Meta => message.to_string(), comp::ChatType::Meta => message.to_string(),
} }
} }

View File

@ -106,6 +106,8 @@ pub enum ChatType<G> {
/// ///
/// The u16 field is a random number for selecting localization variants. /// The u16 field is a random number for selecting localization variants.
Npc(Uid, u16), Npc(Uid, u16),
/// From NPCs but in the chat for clients in the near vicinity
NpcSay(Uid, u16),
/// Anything else /// Anything else
Meta, Meta,
// Looted items // Looted items
@ -136,6 +138,7 @@ pub type UnresolvedChatMsg = GenericChatMsg<Group>;
impl<G> GenericChatMsg<G> { impl<G> GenericChatMsg<G> {
pub const NPC_DISTANCE: f32 = 100.0; pub const NPC_DISTANCE: f32 = 100.0;
pub const NPC_SAY_DISTANCE: f32 = 30.0;
pub const REGION_DISTANCE: f32 = 1000.0; pub const REGION_DISTANCE: f32 = 1000.0;
pub const SAY_DISTANCE: f32 = 100.0; pub const SAY_DISTANCE: f32 = 100.0;
@ -144,6 +147,11 @@ impl<G> GenericChatMsg<G> {
Self { chat_type, message } 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<T>(self, mut f: impl FnMut(G) -> T) -> GenericChatMsg<T> { pub fn map_group<T>(self, mut f: impl FnMut(G) -> T) -> GenericChatMsg<T> {
let chat_type = match self.chat_type { let chat_type = match self.chat_type {
ChatType::Online(a) => ChatType::Online(a), ChatType::Online(a) => ChatType::Online(a),
@ -161,6 +169,7 @@ impl<G> GenericChatMsg<G> {
ChatType::Region(a) => ChatType::Region(a), ChatType::Region(a) => ChatType::Region(a),
ChatType::World(a) => ChatType::World(a), ChatType::World(a) => ChatType::World(a),
ChatType::Npc(a, b) => ChatType::Npc(a, b), ChatType::Npc(a, b) => ChatType::Npc(a, b),
ChatType::NpcSay(a, b) => ChatType::NpcSay(a, b),
ChatType::Meta => ChatType::Meta, ChatType::Meta => ChatType::Meta,
}; };
@ -172,7 +181,7 @@ impl<G> GenericChatMsg<G> {
pub fn to_bubble(&self) -> Option<(SpeechBubble, Uid)> { pub fn to_bubble(&self) -> Option<(SpeechBubble, Uid)> {
let icon = self.icon(); 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)) Some((SpeechBubble::npc_new(&self.message, r, icon), from))
} else { } else {
self.uid() self.uid()
@ -197,6 +206,7 @@ impl<G> GenericChatMsg<G> {
ChatType::Region(_u) => SpeechBubbleType::Region, ChatType::Region(_u) => SpeechBubbleType::Region,
ChatType::World(_u) => SpeechBubbleType::World, ChatType::World(_u) => SpeechBubbleType::World,
ChatType::Npc(_u, _r) => SpeechBubbleType::None, ChatType::Npc(_u, _r) => SpeechBubbleType::None,
ChatType::NpcSay(_u, _r) => SpeechBubbleType::Say,
ChatType::Meta => SpeechBubbleType::None, ChatType::Meta => SpeechBubbleType::None,
} }
} }
@ -218,6 +228,7 @@ impl<G> GenericChatMsg<G> {
ChatType::Region(u) => Some(*u), ChatType::Region(u) => Some(*u),
ChatType::World(u) => Some(*u), ChatType::World(u) => Some(*u),
ChatType::Npc(u, _r) => Some(*u), ChatType::Npc(u, _r) => Some(*u),
ChatType::NpcSay(u, _r) => Some(*u),
ChatType::Meta => None, ChatType::Meta => None,
} }
} }

View File

@ -94,6 +94,7 @@ pub enum ItemTag {
ClothItem, ClothItem,
ModularComponent(ModularComponentTag), ModularComponent(ModularComponentTag),
MetalIngot, MetalIngot,
Cultist,
} }
impl TagExampleInfo for ItemTag { impl TagExampleInfo for ItemTag {
@ -102,6 +103,7 @@ impl TagExampleInfo for ItemTag {
ItemTag::ClothItem => "cloth item", ItemTag::ClothItem => "cloth item",
ItemTag::ModularComponent(kind) => kind.name(), ItemTag::ModularComponent(kind) => kind.name(),
ItemTag::MetalIngot => "metal ingot", ItemTag::MetalIngot => "metal ingot",
ItemTag::Cultist => "cultist",
} }
} }
@ -110,6 +112,7 @@ impl TagExampleInfo for ItemTag {
ItemTag::ClothItem => "common.items.tag_examples.cloth_item", ItemTag::ClothItem => "common.items.tag_examples.cloth_item",
ItemTag::ModularComponent(tag) => tag.exemplar_identifier(), ItemTag::ModularComponent(tag) => tag.exemplar_identifier(),
ItemTag::MetalIngot => "common.items.tag_examples.metal_ingot", ItemTag::MetalIngot => "common.items.tag_examples.metal_ingot",
ItemTag::Cultist => "common.items.tag_examples.cultist",
} }
} }
} }

View File

@ -16,6 +16,26 @@ impl Component for RtSimEntity {
type Storage = IdvStorage<Self>; type Storage = IdvStorage<Self>;
} }
#[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) /// 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 /// 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 /// to `comp::Controller` in that it provides a consistent interface for
@ -33,6 +53,8 @@ pub struct RtSimController {
pub travel_to: Option<(Vec3<f32>, String)>, pub travel_to: Option<(Vec3<f32>, String)>,
/// Proportion of full speed to move /// Proportion of full speed to move
pub speed_factor: f32, pub speed_factor: f32,
/// Events
pub events: Vec<RtSimEvent>,
} }
impl Default for RtSimController { impl Default for RtSimController {
@ -40,6 +62,7 @@ impl Default for RtSimController {
Self { Self {
travel_to: None, travel_to: None,
speed_factor: 1.0, speed_factor: 1.0,
events: Vec::new(),
} }
} }
} }
@ -51,6 +74,7 @@ impl RtSimController {
Self { Self {
travel_to: Some((pos, format!("{:0.1?}", pos))), travel_to: Some((pos, format!("{:0.1?}", pos))),
speed_factor: 0.25, speed_factor: 0.25,
events: Vec::new(),
} }
} }
} }

View File

@ -1,5 +1,11 @@
use super::*; 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::{ use world::{
civ::{Site, Track}, civ::{Site, Track},
util::RandomPerm, util::RandomPerm,
@ -127,7 +133,7 @@ impl Entity {
.build() .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(|| { let tgt_site = self.brain.tgt.or_else(|| {
world world
.civs() .civs()
@ -183,6 +189,11 @@ impl Entity {
self.controller.travel_to = Some((travel_to, destination_name)); self.controller.travel_to = Some((travel_to, destination_name));
self.controller.speed_factor = 0.70; 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 { pub struct Brain {
tgt: Option<Id<Site>>, tgt: Option<Id<Site>>,
track: Option<(Track, usize)>, track: Option<(Track, usize)>,
memories: Vec<Memory>,
}
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))
}
} }

View File

@ -6,10 +6,10 @@ mod load_chunks;
mod tick; mod tick;
mod unload_chunks; mod unload_chunks;
use self::{chunks::Chunks, entity::Entity}; use self::chunks::Chunks;
use common::{ use common::{
comp, comp,
rtsim::{RtSimController, RtSimEntity, RtSimId}, rtsim::{Memory, RtSimController, RtSimEntity, RtSimId},
terrain::TerrainChunk, terrain::TerrainChunk,
vol::RectRasterableVol, vol::RectRasterableVol,
}; };
@ -20,6 +20,8 @@ use slab::Slab;
use specs::{DispatcherBuilder, WorldExt}; use specs::{DispatcherBuilder, WorldExt};
use vek::*; use vek::*;
pub use self::entity::Entity;
pub struct RtSim { pub struct RtSim {
tick: u64, tick: u64,
chunks: Chunks, chunks: Chunks,
@ -71,6 +73,14 @@ impl RtSim {
// tracing::info!("Destroyed rtsim entity {}", entity); // tracing::info!("Destroyed rtsim entity {}", entity);
self.entities.remove(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) { pub fn add_server_systems(dispatch_builder: &mut DispatcherBuilder) {

View File

@ -5,7 +5,7 @@ use common::{
comp, comp,
comp::inventory::loadout_builder::LoadoutBuilder, comp::inventory::loadout_builder::LoadoutBuilder,
event::{EventBus, ServerEvent}, event::{EventBus, ServerEvent},
resources::DeltaTime, resources::{DeltaTime, Time},
terrain::TerrainGrid, terrain::TerrainGrid,
}; };
use common_ecs::{Job, Origin, Phase, System}; use common_ecs::{Job, Origin, Phase, System};
@ -19,6 +19,7 @@ pub struct Sys;
impl<'a> System<'a> for Sys { impl<'a> System<'a> for Sys {
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
type SystemData = ( type SystemData = (
Read<'a, Time>,
Read<'a, DeltaTime>, Read<'a, DeltaTime>,
Read<'a, EventBus<ServerEvent>>, Read<'a, EventBus<ServerEvent>>,
WriteExpect<'a, RtSim>, WriteExpect<'a, RtSim>,
@ -37,6 +38,7 @@ impl<'a> System<'a> for Sys {
fn run( fn run(
_job: &mut Job<Self>, _job: &mut Job<Self>,
( (
time,
dt, dt,
server_event_bus, server_event_bus,
mut rtsim, mut rtsim,
@ -56,7 +58,7 @@ impl<'a> System<'a> for Sys {
let mut to_reify = Vec::new(); let mut to_reify = Vec::new();
for (id, entity) in rtsim.entities.iter_mut() { for (id, entity) in rtsim.entities.iter_mut() {
if entity.is_loaded { if entity.is_loaded {
// No load-specific behaviour yet // Nothing here yet
} else if rtsim } else if rtsim
.chunks .chunks
.chunk_at(entity.pos.xy()) .chunk_at(entity.pos.xy())
@ -87,7 +89,7 @@ impl<'a> System<'a> for Sys {
// Tick entity AI // Tick entity AI
if entity.last_tick + ENTITY_TICK_PERIOD <= rtsim.tick { 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; entity.last_tick = rtsim.tick;
} }
} }

View File

@ -523,7 +523,18 @@ impl StateExt for State {
} }
} }
}, },
comp::ChatType::NpcSay(uid, _r) => {
let entity_opt =
(*ecs.read_resource::<UidAllocator>()).retrieve_entity_internal(uid.0);
let positions = ecs.read_storage::<comp::Pos>();
if let Some(speaker_pos) = entity_opt.and_then(|e| positions.get(e)) {
for (client, pos) in (&ecs.read_storage::<Client>(), &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) => { comp::ChatType::FactionMeta(s) | comp::ChatType::Faction(_, s) => {
for (client, faction) in ( for (client, faction) in (
&ecs.read_storage::<Client>(), &ecs.read_storage::<Client>(),

View File

@ -1,13 +1,14 @@
use crate::rtsim::{Entity as RtSimData, RtSim};
use common::{ use common::{
comp::{ comp::{
self, self,
agent::{AgentEvent, Tactic, Target, DEFAULT_INTERACTION_TIME, TRADE_INTERACTION_TIME}, agent::{AgentEvent, Tactic, Target, DEFAULT_INTERACTION_TIME, TRADE_INTERACTION_TIME},
group, group,
inventory::{slot::EquipSlot, trade_pricing::TradePricing}, inventory::{item::ItemTag, slot::EquipSlot, trade_pricing::TradePricing},
invite::InviteResponse, invite::InviteResponse,
item::{ item::{
tool::{ToolKind, UniqueKind}, tool::{ToolKind, UniqueKind},
ItemKind, ItemDesc, ItemKind,
}, },
skills::{AxeSkill, BowSkill, HammerSkill, Skill, StaffSkill, SwordSkill}, skills::{AxeSkill, BowSkill, HammerSkill, Skill, StaffSkill, SwordSkill},
Agent, Alignment, Body, CharacterState, ControlAction, ControlEvent, Controller, Energy, Agent, Alignment, Body, CharacterState, ControlAction, ControlEvent, Controller, Energy,
@ -16,7 +17,8 @@ use common::{
}, },
event::{Emitter, EventBus, ServerEvent}, event::{Emitter, EventBus, ServerEvent},
path::TraversalConfig, path::TraversalConfig,
resources::{DeltaTime, TimeOfDay}, resources::{DeltaTime, Time, TimeOfDay},
rtsim::{Memory, MemoryItem, RtSimEntity, RtSimEvent},
terrain::{Block, TerrainGrid}, terrain::{Block, TerrainGrid},
time::DayPeriod, time::DayPeriod,
trade::{Good, TradeAction, TradePhase, TradeResult}, trade::{Good, TradeAction, TradePhase, TradeResult},
@ -32,13 +34,14 @@ use specs::{
saveload::{Marker, MarkerAllocator}, saveload::{Marker, MarkerAllocator},
shred::ResourceId, shred::ResourceId,
Entities, Entity as EcsEntity, Join, ParJoin, Read, ReadExpect, ReadStorage, SystemData, World, Entities, Entity as EcsEntity, Join, ParJoin, Read, ReadExpect, ReadStorage, SystemData, World,
Write, WriteStorage, Write, WriteExpect, WriteStorage,
}; };
use std::{f32::consts::PI, sync::Arc}; use std::{f32::consts::PI, sync::Arc};
use vek::*; use vek::*;
struct AgentData<'a> { struct AgentData<'a> {
entity: &'a EcsEntity, entity: &'a EcsEntity,
rtsim_entity: Option<&'a RtSimData>,
uid: &'a Uid, uid: &'a Uid,
pos: &'a Pos, pos: &'a Pos,
vel: &'a Vel, vel: &'a Vel,
@ -63,6 +66,7 @@ pub struct ReadData<'a> {
entities: Entities<'a>, entities: Entities<'a>,
uid_allocator: Read<'a, UidAllocator>, uid_allocator: Read<'a, UidAllocator>,
dt: Read<'a, DeltaTime>, dt: Read<'a, DeltaTime>,
time: Read<'a, Time>,
group_manager: Read<'a, group::GroupManager>, group_manager: Read<'a, group::GroupManager>,
energies: ReadStorage<'a, Energy>, energies: ReadStorage<'a, Energy>,
positions: ReadStorage<'a, Pos>, positions: ReadStorage<'a, Pos>,
@ -83,6 +87,7 @@ pub struct ReadData<'a> {
time_of_day: Read<'a, TimeOfDay>, time_of_day: Read<'a, TimeOfDay>,
light_emitter: ReadStorage<'a, LightEmitter>, light_emitter: ReadStorage<'a, LightEmitter>,
world: ReadExpect<'a, Arc<world::World>>, world: ReadExpect<'a, Arc<world::World>>,
rtsim_entities: ReadStorage<'a, RtSimEntity>,
} }
// This is 3.1 to last longer than the last damage timer (3.0 seconds) // 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<ServerEvent>>, Write<'a, EventBus<ServerEvent>>,
WriteStorage<'a, Agent>, WriteStorage<'a, Agent>,
WriteStorage<'a, Controller>, WriteStorage<'a, Controller>,
WriteExpect<'a, RtSim>,
); );
const NAME: &'static str = "agent"; 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 #[allow(clippy::or_fun_call)] // TODO: Pending review in #587
fn run( fn run(
job: &mut Job<Self>, job: &mut Job<Self>,
(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); job.cpu_stats.measure(ParMode::Rayon);
( (
&read_data.entities, &read_data.entities,
@ -220,12 +227,16 @@ impl<'a> System<'a> for Sys {
let flees = alignment let flees = alignment
.map(|a| !matches!(a, Alignment::Enemy | Alignment::Owned(_))) .map(|a| !matches!(a, Alignment::Enemy | Alignment::Owned(_)))
.unwrap_or(true); .unwrap_or(true);
let damage = health.current() as f32 / health.maximum() as f32; 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 // Package all this agent's data into a convenient struct
let data = AgentData { let data = AgentData {
entity: &entity, entity: &entity,
rtsim_entity,
uid, uid,
pos, pos,
vel, vel,
@ -404,6 +415,20 @@ impl<'a> System<'a> for Sys {
read_data.bodies.get(attacker), read_data.bodies.get(attacker),
&read_data.dt, &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 { } else {
agent.target = None; 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()); 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); self.idle(agent, controller, &read_data);
} }
} else if thread_rng().gen::<f32>() < 0.1 { } else if thread_rng().gen::<f32>() < 0.1 {
self.choose_target(agent, controller, &read_data); self.choose_target(agent, controller, &read_data, event_emitter);
} else { } else {
self.idle(agent, controller, &read_data); self.idle(agent, controller, &read_data);
} }
@ -775,16 +809,43 @@ impl<'a> AgentData<'a> {
) { ) {
controller.inputs.look_dir = dir; controller.inputs.look_dir = dir;
} }
controller.actions.push(ControlAction::Stand);
controller.actions.push(ControlAction::Talk); controller.actions.push(ControlAction::Talk);
if let Some((_travel_to, destination_name)) = if let (Some((_travel_to, destination_name)), Some(rtsim_entity)) =
&agent.rtsim_controller.travel_to (&agent.rtsim_controller.travel_to, &self.rtsim_entity)
{ {
let msg = format!( let msg = if let Some(tgt_stats) = read_data.stats.get(target) {
"I'm heading to {}! Want to come along?", agent.rtsim_controller.events.push(RtSimEvent::AddMemory(
destination_name Memory {
); item: MemoryItem::CharacterInteraction {
event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc( 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, *self.uid, msg,
))); )));
} else { } else {
@ -800,7 +861,6 @@ impl<'a> AgentData<'a> {
Some(AgentEvent::TradeInvite(_with)) => { Some(AgentEvent::TradeInvite(_with)) => {
if agent.trade_for_site.is_some() && !agent.trading { if agent.trade_for_site.is_some() && !agent.trading {
// stand still and looking towards the trading player // stand still and looking towards the trading player
controller.actions.push(ControlAction::Stand);
controller.actions.push(ControlAction::Talk); controller.actions.push(ControlAction::Talk);
controller controller
.events .events
@ -817,12 +877,12 @@ impl<'a> AgentData<'a> {
if agent.trading { if agent.trading {
match result { match result {
TradeResult::Completed => { TradeResult::Completed => {
event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc( event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc_say(
*self.uid, *self.uid,
"Thank you for trading with me!".to_string(), "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, *self.uid,
"Maybe another time, have a good day!".to_string(), "Maybe another time, have a good day!".to_string(),
))), ))),
@ -892,8 +952,9 @@ impl<'a> AgentData<'a> {
"That only covers {:.1}% of my costs!", "That only covers {:.1}% of my costs!",
balance0 / balance1 * 100.0 balance0 / balance1 * 100.0
); );
event_emitter event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc_say(
.emit(ServerEvent::Chat(UnresolvedChatMsg::npc(*self.uid, msg))); *self.uid, msg,
)));
} }
if pending.phase != TradePhase::Mutate { if pending.phase != TradePhase::Mutate {
// we got into the review phase but without balanced goods, decline // we got into the review phase but without balanced goods, decline
@ -981,14 +1042,20 @@ impl<'a> AgentData<'a> {
agent.action_timer += dt.0; 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; agent.action_timer = 0.0;
// Search for new targets (this looks expensive, but it's only run occasionally) // 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 // 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() .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 search_dist = SEARCH_DIST;
let mut listen_dist = LISTEN_DIST; let mut listen_dist = LISTEN_DIST;
if char_state.map_or(false, |c_s| c_s.is_stealthy()) { 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_pos.0.distance_squared(self.pos.0) < listen_dist.powi(2)) // TODO implement proper sound system for agents
&& e != self.entity && e != self.entity
&& !e_health.is_dead && !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? // 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()) .ray(self.pos.0 + Vec3::unit_z(), e_pos.0 + Vec3::unit_z())
.until(Block::is_opaque) .until(Block::is_opaque)
.cast() .cast()
.0 >= e_pos.0.distance(self.pos.0)) .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 .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); .map(|(e, _, _, _, _, _, _)| e);
if let Some(target) = target { if let Some(target) = target {
agent.target = Some(Target { agent.target = Some(Target {
target, target,

View File

@ -613,7 +613,8 @@ fn render_chat_line(chat_type: &ChatType<String>, imgs: &Imgs) -> (Color, conrod
ChatType::Faction(_uid, _s) => (FACTION_COLOR, imgs.chat_faction_small), ChatType::Faction(_uid, _s) => (FACTION_COLOR, imgs.chat_faction_small),
ChatType::Region(_uid) => (REGION_COLOR, imgs.chat_region_small), ChatType::Region(_uid) => (REGION_COLOR, imgs.chat_region_small),
ChatType::World(_uid) => (WORLD_COLOR, imgs.chat_world_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), ChatType::Meta => (INFO_COLOR, imgs.chat_command_info_small),
} }
} }

View File

@ -186,16 +186,14 @@ fn armor_desc(armor: &Armor, desc: &str, slots: u16) -> String {
Protection::Normal(a) => a.to_string(), Protection::Normal(a) => a.to_string(),
Protection::Invincible => "Inf".to_string(), Protection::Invincible => "Inf".to_string(),
}; };
//let armor_poise_resilience = match armor.get_poise_resilience() { let armor_poise_resilience = match armor.get_poise_resilience() {
// Protection::Normal(a) => a.to_string(), Protection::Normal(a) => a.to_string(),
// Protection::Invincible => "Inf".to_string(), Protection::Invincible => "Inf".to_string(),
//}; };
let mut description = format!( let mut description = format!(
"{}\n\nArmor: {}", "{}\n\nArmor: {}\n\nPoise Resilience: {}",
//"{}\n\nArmor: {}\n\nPoise Resilience: {}", kind, armor_protection, armor_poise_resilience
kind,
armor_protection, /* armor_poise_resilience // Add back when we are ready for poise */
); );
if !desc.is_empty() { if !desc.is_empty() {
@ -235,7 +233,6 @@ fn tool_desc(tool: &Tool, components: &[Item], msm: &MaterialStatManifest, desc:
// Get tool stats // Get tool stats
let stats = tool.stats.resolve_stats(msm, components).clamp_speed(); let stats = tool.stats.resolve_stats(msm, components).clamp_speed();
//let poise_strength = tool.base_poise_strength();
let hands = match tool.hands { let hands = match tool.hands {
Hands::One => "One", Hands::One => "One",
Hands::Two => "Two", Hands::Two => "Two",
@ -260,13 +257,9 @@ fn tool_desc(tool: &Tool, components: &[Item], msm: &MaterialStatManifest, desc:
fn statblock_desc(stats: &Stats) -> String { fn statblock_desc(stats: &Stats) -> String {
format!( format!(
"DPS: {:0.1}\n\nPower: {:0.1}\n\nSpeed: {:0.1}\n\n", "Power: {:0.1}\n\nPoise Strength: {: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<Right-Click to use>",
stats.speed * stats.power * 10.0, // Damage per second
stats.power * 10.0, stats.power * 10.0,
//stats.poise_strength * 10.0, stats.poise_strength * 10.0,
stats.speed, stats.speed,
) + &format!( ) + &format!(
"Critical chance: {:0.1}%\n\nCritical multiplier: {:0.1}x\n\n", "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) ingredient_desc("mushrooms", "common.items.food.mushroom", &testmsm)
); );
assert_eq!( assert_eq!(
"Crafting Ingredient\n\nA bronze ingot.\n\nStat multipliers:\nDPS: 210.0\n\nPower: \ "Crafting Ingredient\n\nA bronze ingot.\n\nStat multipliers:\nPower: 30.0\n\nPoise \
30.0\n\nSpeed: 7.0\n\nCritical chance: 50.0%\n\nCritical multiplier: 2.0x\n\n", Strength: 50.0\n\nSpeed: 7.0\n\nCritical chance: 50.0%\n\nCritical multiplier: \
2.0x\n\n",
ingredient_desc( ingredient_desc(
"A bronze ingot.", "A bronze ingot.",
"common.items.crafting_ing.bronze_ingot", "common.items.crafting_ing.bronze_ingot",