mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Various RtSim and Agent Interaction Fixes
This commit is contained in:
parent
39ac300bef
commit
6ea43cfd75
@ -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
|
||||
|
||||
|
@ -9,5 +9,5 @@ ItemDef(
|
||||
),
|
||||
)),
|
||||
quality: Epic,
|
||||
tags: [],
|
||||
)
|
||||
tags: [Cultist],
|
||||
)
|
||||
|
@ -9,5 +9,5 @@ ItemDef(
|
||||
),
|
||||
)),
|
||||
quality: Epic,
|
||||
tags: [],
|
||||
)
|
||||
tags: [Cultist],
|
||||
)
|
||||
|
@ -9,5 +9,5 @@ ItemDef(
|
||||
),
|
||||
)),
|
||||
quality: Epic,
|
||||
tags: [],
|
||||
)
|
||||
tags: [Cultist],
|
||||
)
|
||||
|
@ -9,5 +9,5 @@ ItemDef(
|
||||
),
|
||||
)),
|
||||
quality: Epic,
|
||||
tags: [],
|
||||
)
|
||||
tags: [Cultist],
|
||||
)
|
||||
|
@ -9,5 +9,5 @@ ItemDef(
|
||||
),
|
||||
)),
|
||||
quality: Epic,
|
||||
tags: [],
|
||||
)
|
||||
tags: [Cultist],
|
||||
)
|
||||
|
@ -9,5 +9,5 @@ ItemDef(
|
||||
),
|
||||
)),
|
||||
quality: Epic,
|
||||
tags: [],
|
||||
)
|
||||
tags: [Cultist],
|
||||
)
|
||||
|
18
assets/common/items/tag_examples/cultist.ron
Normal file
18
assets/common/items/tag_examples/cultist.ron
Normal 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: [],
|
||||
)
|
||||
|
||||
|
@ -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!",
|
||||
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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>(),
|
||||
|
@ -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,
|
||||
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
Loading…
Reference in New Issue
Block a user