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
- 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,5 +9,5 @@ ItemDef(
),
)),
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 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!",

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
// 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(),
}
}

View File

@ -106,6 +106,8 @@ pub enum ChatType<G> {
///
/// 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<Group>;
impl<G> GenericChatMsg<G> {
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<G> GenericChatMsg<G> {
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> {
let chat_type = match self.chat_type {
ChatType::Online(a) => ChatType::Online(a),
@ -161,6 +169,7 @@ impl<G> GenericChatMsg<G> {
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<G> GenericChatMsg<G> {
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<G> GenericChatMsg<G> {
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<G> GenericChatMsg<G> {
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,
}
}

View File

@ -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",
}
}
}

View File

@ -16,6 +16,26 @@ impl Component for RtSimEntity {
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)
/// 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<f32>, String)>,
/// Proportion of full speed to move
pub speed_factor: f32,
/// Events
pub events: Vec<RtSimEvent>,
}
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(),
}
}
}

View File

@ -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<Id<Site>>,
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 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) {

View File

@ -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<ServerEvent>>,
WriteExpect<'a, RtSim>,
@ -37,6 +38,7 @@ impl<'a> System<'a> for Sys {
fn run(
_job: &mut Job<Self>,
(
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;
}
}

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) => {
for (client, faction) in (
&ecs.read_storage::<Client>(),

View File

@ -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<world::World>>,
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<ServerEvent>>,
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<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);
(
&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::<f32>() < 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,

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::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),
}
}

View File

@ -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<Right-Click to use>",
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",