diff --git a/Cargo.lock b/Cargo.lock index 1c98e40f9b..e158b707e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5394,6 +5394,7 @@ dependencies = [ "approx 0.4.0", "arraygen", "assets_manager", + "bitflags", "criterion", "crossbeam-channel", "crossbeam-utils 0.8.3", diff --git a/common/Cargo.toml b/common/Cargo.toml index bca6817839..7e770dd2f7 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -24,6 +24,7 @@ serde = { version = "1.0.110", features = ["derive", "rc"] } approx = "0.4.0" arraygen = "0.1.13" crossbeam-utils = "0.8.1" +bitflags = "1.2" crossbeam-channel = "0.5" enum-iterator = "0.6" lazy_static = "1.4.0" diff --git a/common/src/comp/agent.rs b/common/src/comp/agent.rs index c29f412a19..a777055891 100644 --- a/common/src/comp/agent.rs +++ b/common/src/comp/agent.rs @@ -98,6 +98,90 @@ impl Component for Alignment { type Storage = IdvStorage; } +bitflags::bitflags! { + #[derive(Default)] + pub struct BehaviorCapability: u8 { + const SPEAK = 0b00000001; + } +} +bitflags::bitflags! { + #[derive(Default)] + pub struct BehaviorState: u8 { + const TRADING = 0b00000001; + const TRADING_ISSUER = 0b00000010; + } +} + +/// # Behavior Component +/// This component allow an Entity to register one or more behavior tags. +/// These tags act as flags of what an Entity can do, or what it is doing. +/// Behaviors Tags can be added and removed as the Entity lives, to update its +/// state when needed +#[derive(Default, Copy, Clone, Debug)] +pub struct Behavior { + capabilities: BehaviorCapability, + state: BehaviorState, + pub trade_site: Option, +} + +impl From for Behavior { + fn from(capabilities: BehaviorCapability) -> Self { + Behavior { + capabilities, + state: BehaviorState::default(), + trade_site: None, + } + } +} + +impl Behavior { + /// Builder function + /// Set capabilities if Option is Some + pub fn maybe_with_capabilities( + mut self, + maybe_capabilities: Option, + ) -> Self { + if let Some(capabilities) = maybe_capabilities { + self.allow(capabilities) + } + self + } + + /// Builder function + /// Set trade_site if Option is Some + pub fn with_trade_site(mut self, trade_site: Option) -> Self { + self.trade_site = trade_site; + self + } + + /// Set capabilities to the Behavior + pub fn allow(&mut self, capabilities: BehaviorCapability) { + self.capabilities.set(capabilities, true) + } + + /// Unset capabilities to the Behavior + pub fn deny(&mut self, capabilities: BehaviorCapability) { + self.capabilities.set(capabilities, false) + } + + /// Check if the Behavior is able to do something + pub fn can(&self, capabilities: BehaviorCapability) -> bool { + self.capabilities.contains(capabilities) + } + + /// Check if the Behavior is able to trade + pub fn can_trade(&self) -> bool { self.trade_site.is_some() } + + /// Set a state to the Behavior + pub fn set(&mut self, state: BehaviorState) { self.state.set(state, true) } + + /// Unset a state to the Behavior + pub fn unset(&mut self, state: BehaviorState) { self.state.set(state, false) } + + /// Check if the Behavior has a specific state + pub fn is(&self, state: BehaviorState) -> bool { self.state.contains(state) } +} + #[derive(Clone, Debug, Default)] pub struct Psyche { pub aggro: f32, // 0.0 = always flees, 1.0 = always attacks, 0.5 = flee at 50% health @@ -210,12 +294,7 @@ pub struct Agent { pub patrol_origin: Option>, pub target: Option, pub chaser: Chaser, - /// Does the agent talk when e.g. hit by the player - // TODO move speech patterns into a Behavior component - pub can_speak: bool, - pub trade_for_site: Option, - pub trading: bool, - pub trading_issuer: bool, + pub behavior: Behavior, pub psyche: Psyche, pub inbox: VecDeque, pub action_timer: f32, @@ -228,31 +307,27 @@ impl Agent { self } - pub fn with_destination(pos: Vec3) -> Self { - Self { - can_speak: true, - psyche: Psyche { aggro: 1.0 }, - rtsim_controller: RtSimController::with_destination(pos), - ..Default::default() - } + pub fn with_destination(mut self, pos: Vec3) -> Self { + self.psyche = Psyche { aggro: 1.0 }; + self.rtsim_controller = RtSimController::with_destination(pos); + self.behavior.allow(BehaviorCapability::SPEAK); + self } pub fn new( patrol_origin: Option>, - can_speak: bool, - trade_for_site: Option, body: &Body, + behavior: Behavior, no_flee: bool, ) -> Self { Agent { patrol_origin, - can_speak, - trade_for_site, psyche: if no_flee { Psyche { aggro: 1.0 } } else { Psyche::from(body) }, + behavior, ..Default::default() } } @@ -261,3 +336,30 @@ impl Agent { impl Component for Agent { type Storage = IdvStorage; } + +#[cfg(test)] +mod tests { + use super::{Behavior, BehaviorCapability, BehaviorState}; + + /// Test to verify that Behavior is working correctly at its most basic + /// usages + #[test] + pub fn behavior_basic() { + let mut b = Behavior::default(); + // test capabilities + assert!(!b.can(BehaviorCapability::SPEAK)); + b.allow(BehaviorCapability::SPEAK); + assert!(b.can(BehaviorCapability::SPEAK)); + b.deny(BehaviorCapability::SPEAK); + assert!(!b.can(BehaviorCapability::SPEAK)); + // test states + assert!(!b.is(BehaviorState::TRADING)); + b.set(BehaviorState::TRADING); + assert!(b.is(BehaviorState::TRADING)); + b.unset(BehaviorState::TRADING); + assert!(!b.is(BehaviorState::TRADING)); + // test `from` + let b = Behavior::from(BehaviorCapability::SPEAK); + assert!(b.can(BehaviorCapability::SPEAK)); + } +} diff --git a/common/src/comp/mod.rs b/common/src/comp/mod.rs index 879a4dcee4..00ea323c21 100644 --- a/common/src/comp/mod.rs +++ b/common/src/comp/mod.rs @@ -45,7 +45,7 @@ pub mod visual; pub use self::{ ability::{CharacterAbility, CharacterAbilityType}, admin::Admin, - agent::{Agent, Alignment}, + agent::{Agent, Alignment, Behavior, BehaviorCapability, BehaviorState}, aura::{Aura, AuraChange, AuraKind, Auras}, beam::{Beam, BeamSegment}, body::{ diff --git a/common/src/states/basic_summon.rs b/common/src/states/basic_summon.rs index 17dd549fcf..86f3f857fd 100644 --- a/common/src/states/basic_summon.rs +++ b/common/src/states/basic_summon.rs @@ -2,7 +2,7 @@ use crate::{ comp::{ self, inventory::loadout_builder::{LoadoutBuilder, LoadoutConfig}, - CharacterState, StateUpdate, + Behavior, BehaviorCapability, CharacterState, StateUpdate, }, event::{LocalEvent, ServerEvent}, outcome::Outcome, @@ -104,7 +104,12 @@ impl CharacterBehavior for Data { poise: comp::Poise::new(body), loadout, body, - agent: Some(comp::Agent::new(None, false, None, &body, true)), + agent: Some(comp::Agent::new( + None, + &body, + Behavior::from(BehaviorCapability::SPEAK), + true, + )), alignment: comp::Alignment::Owned(*data.uid), scale: self .static_data diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 9c3407132b..d9a5a89bf2 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -1074,7 +1074,7 @@ fn handle_spawn_airship( animated: true, }); if let Some(pos) = destination { - builder = builder.with(comp::Agent::with_destination(pos)) + builder = builder.with(comp::Agent::default().with_destination(pos)) } builder.build(); diff --git a/server/src/events/invite.rs b/server/src/events/invite.rs index dd859c910e..7df7dd20af 100644 --- a/server/src/events/invite.rs +++ b/server/src/events/invite.rs @@ -225,8 +225,18 @@ pub fn handle_invite_accept(server: &mut Server, entity: specs::Entity) { } let pricing = agents .get(inviter) - .and_then(|a| index.get_site_prices(a)) - .or_else(|| agents.get(entity).and_then(|a| index.get_site_prices(a))); + .and_then(|a| { + a.behavior + .trade_site + .and_then(|id| index.get_site_prices(id)) + }) + .or_else(|| { + agents.get(entity).and_then(|a| { + a.behavior + .trade_site + .and_then(|id| index.get_site_prices(id)) + }) + }); clients.get(inviter).map(|c| { c.send(ServerGeneral::UpdatePendingTrade( id, diff --git a/server/src/events/trade.rs b/server/src/events/trade.rs index ae39974cc6..657602673b 100644 --- a/server/src/events/trade.rs +++ b/server/src/events/trade.rs @@ -32,15 +32,16 @@ fn notify_agent_prices( entity: EcsEntity, event: AgentEvent, ) { - if let Some(agent) = agents.get_mut(entity) { - let prices = index.get_site_prices(agent); + if let Some((Some(site_id), agent)) = agents.get_mut(entity).map(|a| (a.behavior.trade_site, a)) + { + let prices = index.get_site_prices(site_id); if let AgentEvent::UpdatePendingTrade(boxval) = event { // Box<(tid, pend, _, inventories)>) = event { agent .inbox .push_front(AgentEvent::UpdatePendingTrade(Box::new(( - // Prefer using this Agent's price data, but use the counterparty's price data - // if we don't have price data + // Prefer using this Agent's price data, but use the counterparty's price + // data if we don't have price data boxval.0, boxval.1, prices.unwrap_or(boxval.2), @@ -118,7 +119,10 @@ pub fn handle_process_trade_action( // Get price info from the first Agent in the trade (currently, an // Agent will never initiate a trade with another agent though) prices = prices.or_else(|| { - agents.get(e).and_then(|a| server.index.get_site_prices(a)) + agents + .get(e) + .and_then(|a| a.behavior.trade_site) + .and_then(|id| server.index.get_site_prices(id)) }); } } diff --git a/server/src/rtsim/tick.rs b/server/src/rtsim/tick.rs index c68d7a99b3..fa526e995d 100644 --- a/server/src/rtsim/tick.rs +++ b/server/src/rtsim/tick.rs @@ -2,7 +2,7 @@ use super::*; use common::{ - comp::{self, inventory::loadout_builder::LoadoutBuilder}, + comp::{self, inventory::loadout_builder::LoadoutBuilder, Behavior, BehaviorCapability}, event::{EventBus, ServerEvent}, resources::{DeltaTime, Time}, terrain::TerrainGrid, @@ -104,12 +104,16 @@ impl<'a> System<'a> for Sys { + Vec3::new(0.5, 0.5, body.flying_height()); let pos = comp::Pos(spawn_pos); let agent = Some(comp::Agent::new( - None, - matches!(body, comp::Body::Humanoid(_)), None, &body, + if matches!(body, comp::Body::Humanoid(_)) { + Behavior::from(BehaviorCapability::SPEAK) + } else { + Behavior::default() + }, false, )); + let rtsim_entity = Some(RtSimEntity(id)); let event = match body { comp::Body::Ship(ship) => ServerEvent::CreateShip { diff --git a/server/src/sys/agent.rs b/server/src/sys/agent.rs index 50fa9e5089..8b7e537c0a 100644 --- a/server/src/sys/agent.rs +++ b/server/src/sys/agent.rs @@ -14,9 +14,9 @@ use common::{ ItemDesc, ItemKind, }, skills::{AxeSkill, BowSkill, HammerSkill, Skill, StaffSkill, SwordSkill}, - Agent, Alignment, Body, CharacterState, ControlAction, ControlEvent, Controller, Energy, - Health, InputKind, Inventory, LightEmitter, MountState, Ori, PhysicsState, Pos, Scale, - Stats, UnresolvedChatMsg, Vel, + Agent, Alignment, BehaviorCapability, BehaviorState, Body, CharacterState, ControlAction, + ControlEvent, Controller, Energy, Health, InputKind, Inventory, LightEmitter, MountState, + Ori, PhysicsState, Pos, Scale, Stats, UnresolvedChatMsg, Vel, }, event::{Emitter, EventBus, ServerEvent}, path::TraversalConfig, @@ -137,9 +137,11 @@ impl<'a> System<'a> for Sys { ( &read_data.entities, (&read_data.energies, read_data.healths.maybe()), - &read_data.positions, - &read_data.velocities, - &read_data.orientations, + ( + &read_data.positions, + &read_data.velocities, + &read_data.orientations, + ), read_data.bodies.maybe(), &read_data.inventories, &read_data.stats, @@ -153,14 +155,12 @@ impl<'a> System<'a> for Sys { &read_data.char_states, ) .par_join() - .filter( - |(_, _, _, _, _, _, _, _, _, _, _, _, _, _, mount_state, _)| { - // Skip mounted entities - mount_state - .map(|ms| *ms == MountState::Unmounted) - .unwrap_or(true) - }, - ) + .filter(|(_, _, _, _, _, _, _, _, _, _, _, _, mount_state, _)| { + // Skip mounted entities + mount_state + .map(|ms| *ms == MountState::Unmounted) + .unwrap_or(true) + }) .for_each_init( || { prof_span!(guard, "agent rayon job"); @@ -170,9 +170,7 @@ impl<'a> System<'a> for Sys { ( entity, (energy, health), - pos, - vel, - ori, + (pos, vel, ori), body, inventory, stats, @@ -551,7 +549,7 @@ impl<'a> AgentData<'a> { } if agent.action_timer > 0.0 { if agent.action_timer - < (if agent.trading { + < (if agent.behavior.is(BehaviorState::TRADING) { TRADE_INTERACTION_TIME } else { DEFAULT_INTERACTION_TIME @@ -588,7 +586,7 @@ impl<'a> AgentData<'a> { let dist_sqrd = self.pos.0.distance_squared(tgt_pos.0); // Should the agent flee? if 1.0 - agent.psyche.aggro > self.damage && self.flees { - if agent.action_timer == 0.0 && agent.can_speak { + if agent.action_timer == 0.0 && agent.behavior.can(BehaviorCapability::SPEAK) { let msg = "npc.speech.villager_under_attack".to_string(); event_emitter .emit(ServerEvent::Chat(UnresolvedChatMsg::npc(*self.uid, msg))); @@ -616,7 +614,7 @@ impl<'a> AgentData<'a> { read_data.buffs.get(target), ) { agent.target = None; - if agent.can_speak { + if agent.behavior.can(BehaviorCapability::SPEAK) { let msg = "npc.speech.villager_enemy_killed".to_string(); event_emitter .emit(ServerEvent::Chat(UnresolvedChatMsg::npc(*self.uid, msg))); @@ -879,7 +877,7 @@ impl<'a> AgentData<'a> { let msg = agent.inbox.pop_back(); match msg { Some(AgentEvent::Talk(by, subject)) => { - if agent.can_speak { + if agent.behavior.can(BehaviorCapability::SPEAK) { if let Some(target) = read_data.uid_allocator.retrieve_entity_internal(by.id()) { agent.target = Some(Target { @@ -933,7 +931,7 @@ impl<'a> AgentData<'a> { event_emitter.emit(ServerEvent::Chat( UnresolvedChatMsg::npc(*self.uid, msg), )); - } else if agent.trade_for_site.is_some() { + } else if agent.behavior.can_trade() { let msg = "npc.speech.merchant_advertisement".to_string(); event_emitter.emit(ServerEvent::Chat( UnresolvedChatMsg::npc(*self.uid, msg), @@ -946,8 +944,8 @@ impl<'a> AgentData<'a> { } }, Subject::Trade => { - if agent.trade_for_site.is_some() { - if !agent.trading { + if agent.behavior.can_trade() { + if !agent.behavior.is(BehaviorState::TRADING) { controller.events.push(ControlEvent::InitiateInvite( by, InviteKind::Trade, @@ -1095,8 +1093,8 @@ impl<'a> AgentData<'a> { } }, Some(AgentEvent::TradeInvite(with)) => { - if agent.trade_for_site.is_some() { - if !agent.trading { + if agent.behavior.can_trade() { + if !agent.behavior.is(BehaviorState::TRADING) { // stand still and looking towards the trading player controller.actions.push(ControlAction::Stand); controller.actions.push(ControlAction::Talk); @@ -1112,13 +1110,13 @@ impl<'a> AgentData<'a> { controller .events .push(ControlEvent::InviteResponse(InviteResponse::Accept)); - agent.trading_issuer = false; - agent.trading = true; + agent.behavior.unset(BehaviorState::TRADING_ISSUER); + agent.behavior.set(BehaviorState::TRADING); } else { controller .events .push(ControlEvent::InviteResponse(InviteResponse::Decline)); - if agent.can_speak { + if agent.behavior.can(BehaviorCapability::SPEAK) { event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc( *self.uid, "npc.speech.merchant_busy".to_string(), @@ -1130,7 +1128,7 @@ impl<'a> AgentData<'a> { controller .events .push(ControlEvent::InviteResponse(InviteResponse::Decline)); - if agent.can_speak { + if agent.behavior.can(BehaviorCapability::SPEAK) { event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc( *self.uid, "npc.speech.villager_decline_trade".to_string(), @@ -1139,7 +1137,7 @@ impl<'a> AgentData<'a> { } }, Some(AgentEvent::TradeAccepted(with)) => { - if !agent.trading { + if !agent.behavior.is(BehaviorState::TRADING) { if let Some(target) = read_data.uid_allocator.retrieve_entity_internal(with.id()) { @@ -1149,12 +1147,12 @@ impl<'a> AgentData<'a> { selected_at: read_data.time.0, }); } - agent.trading = true; - agent.trading_issuer = true; + agent.behavior.set(BehaviorState::TRADING); + agent.behavior.set(BehaviorState::TRADING_ISSUER); } }, Some(AgentEvent::FinishedTrade(result)) => { - if agent.trading { + if agent.behavior.is(BehaviorState::TRADING) { match result { TradeResult::Completed => { event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc( @@ -1167,13 +1165,17 @@ impl<'a> AgentData<'a> { "npc.speech.merchant_trade_declined".to_string(), ))), } - agent.trading = false; + agent.behavior.unset(BehaviorState::TRADING); } }, Some(AgentEvent::UpdatePendingTrade(boxval)) => { let (tradeid, pending, prices, inventories) = *boxval; - if agent.trading { - let who: usize = if agent.trading_issuer { 0 } else { 1 }; + if agent.behavior.is(BehaviorState::TRADING) { + let who: usize = if agent.behavior.is(BehaviorState::TRADING_ISSUER) { + 0 + } else { + 1 + }; let balance0: f32 = prices.balance(&pending.offers, &inventories, 1 - who, true); let balance1: f32 = prices.balance(&pending.offers, &inventories, who, false); @@ -1203,7 +1205,7 @@ impl<'a> AgentData<'a> { } if pending.phase != TradePhase::Mutate { // we got into the review phase but without balanced goods, decline - agent.trading = false; + agent.behavior.unset(BehaviorState::TRADING); event_emitter.emit(ServerEvent::ProcessTradeAction( *self.entity, tradeid, @@ -1214,7 +1216,7 @@ impl<'a> AgentData<'a> { } }, None => { - if agent.can_speak { + if agent.behavior.can(BehaviorCapability::SPEAK) { // no new events, continue looking towards the last interacting player for some // time if let Some(Target { target, .. }) = &agent.target { @@ -1337,7 +1339,7 @@ impl<'a> AgentData<'a> { ( 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 agent.behavior.can(BehaviorCapability::SPEAK) { if self.rtsim_entity.is_some() { agent.rtsim_controller.events.push( RtSimEvent::AddMemory(Memory { diff --git a/server/src/sys/terrain.rs b/server/src/sys/terrain.rs index 34dd4284c3..1ed8c054f6 100644 --- a/server/src/sys/terrain.rs +++ b/server/src/sys/terrain.rs @@ -2,7 +2,10 @@ use crate::{ chunk_generator::ChunkGenerator, client::Client, presence::Presence, rtsim::RtSim, Tick, }; use common::{ - comp::{self, bird_medium, inventory::loadout_builder::LoadoutConfig, Alignment, Pos}, + comp::{ + self, bird_medium, inventory::loadout_builder::LoadoutConfig, Alignment, + BehaviorCapability, Pos, + }, event::{EventBus, ServerEvent}, generation::get_npc_name, npc::NPC_NAMES, @@ -12,6 +15,7 @@ use common::{ use common_ecs::{Job, Origin, Phase, System}; use common_net::msg::ServerGeneral; use common_sys::state::TerrainChanges; +use comp::Behavior; use specs::{Join, Read, ReadStorage, Write, WriteExpect}; use std::sync::Arc; use vek::*; @@ -191,9 +195,12 @@ impl<'a> System<'a> for Sys { agent: if entity.has_agency { Some(comp::Agent::new( Some(entity.pos), - can_speak, - trade_for_site, &body, + Behavior::default() + .maybe_with_capabilities( + can_speak.then(|| BehaviorCapability::SPEAK), + ) + .with_trade_site(trade_for_site), matches!( loadout_config, Some(comp::inventory::loadout_builder::LoadoutConfig::Guard) diff --git a/world/src/index.rs b/world/src/index.rs index feaef98cf0..bdc8aecbae 100644 --- a/world/src/index.rs +++ b/world/src/index.rs @@ -4,9 +4,8 @@ use crate::{ }; use common::{ assets::{AssetExt, AssetHandle}, - comp::Agent, store::Store, - trade::SitePrices, + trade::{SiteId, SitePrices}, }; use core::ops::Deref; use noise::{Seedable, SuperSimplex}; @@ -72,11 +71,9 @@ impl Index { pub fn colors(&self) -> AssetHandle> { self.colors } - pub fn get_site_prices(&self, agent: &Agent) -> Option { - agent - .trade_for_site - .map(|i| self.sites.recreate_id(i)) - .flatten() + pub fn get_site_prices(&self, site_id: SiteId) -> Option { + self.sites + .recreate_id(site_id) .map(|i| self.sites.get(i)) .map(|s| s.economy.get_site_prices()) }