Merge branch 'vfoulon80/behavior-component' into 'master'

New Component: Behavior

See merge request veloren/veloren!2033
This commit is contained in:
Imbris 2021-04-08 19:16:28 +00:00
commit 0eecc61ddf
12 changed files with 214 additions and 81 deletions

1
Cargo.lock generated
View File

@ -5394,6 +5394,7 @@ dependencies = [
"approx 0.4.0", "approx 0.4.0",
"arraygen", "arraygen",
"assets_manager", "assets_manager",
"bitflags",
"criterion", "criterion",
"crossbeam-channel", "crossbeam-channel",
"crossbeam-utils 0.8.3", "crossbeam-utils 0.8.3",

View File

@ -24,6 +24,7 @@ serde = { version = "1.0.110", features = ["derive", "rc"] }
approx = "0.4.0" approx = "0.4.0"
arraygen = "0.1.13" arraygen = "0.1.13"
crossbeam-utils = "0.8.1" crossbeam-utils = "0.8.1"
bitflags = "1.2"
crossbeam-channel = "0.5" crossbeam-channel = "0.5"
enum-iterator = "0.6" enum-iterator = "0.6"
lazy_static = "1.4.0" lazy_static = "1.4.0"

View File

@ -98,6 +98,90 @@ impl Component for Alignment {
type Storage = IdvStorage<Self>; type Storage = IdvStorage<Self>;
} }
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<SiteId>,
}
impl From<BehaviorCapability> 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<BehaviorCapability>,
) -> 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<SiteId>) -> 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)] #[derive(Clone, Debug, Default)]
pub struct Psyche { pub struct Psyche {
pub aggro: f32, // 0.0 = always flees, 1.0 = always attacks, 0.5 = flee at 50% health 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<Vec3<f32>>, pub patrol_origin: Option<Vec3<f32>>,
pub target: Option<Target>, pub target: Option<Target>,
pub chaser: Chaser, pub chaser: Chaser,
/// Does the agent talk when e.g. hit by the player pub behavior: Behavior,
// TODO move speech patterns into a Behavior component
pub can_speak: bool,
pub trade_for_site: Option<SiteId>,
pub trading: bool,
pub trading_issuer: bool,
pub psyche: Psyche, pub psyche: Psyche,
pub inbox: VecDeque<AgentEvent>, pub inbox: VecDeque<AgentEvent>,
pub action_timer: f32, pub action_timer: f32,
@ -228,31 +307,27 @@ impl Agent {
self self
} }
pub fn with_destination(pos: Vec3<f32>) -> Self { pub fn with_destination(mut self, pos: Vec3<f32>) -> Self {
Self { self.psyche = Psyche { aggro: 1.0 };
can_speak: true, self.rtsim_controller = RtSimController::with_destination(pos);
psyche: Psyche { aggro: 1.0 }, self.behavior.allow(BehaviorCapability::SPEAK);
rtsim_controller: RtSimController::with_destination(pos), self
..Default::default()
}
} }
pub fn new( pub fn new(
patrol_origin: Option<Vec3<f32>>, patrol_origin: Option<Vec3<f32>>,
can_speak: bool,
trade_for_site: Option<SiteId>,
body: &Body, body: &Body,
behavior: Behavior,
no_flee: bool, no_flee: bool,
) -> Self { ) -> Self {
Agent { Agent {
patrol_origin, patrol_origin,
can_speak,
trade_for_site,
psyche: if no_flee { psyche: if no_flee {
Psyche { aggro: 1.0 } Psyche { aggro: 1.0 }
} else { } else {
Psyche::from(body) Psyche::from(body)
}, },
behavior,
..Default::default() ..Default::default()
} }
} }
@ -261,3 +336,30 @@ impl Agent {
impl Component for Agent { impl Component for Agent {
type Storage = IdvStorage<Self>; type Storage = IdvStorage<Self>;
} }
#[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));
}
}

View File

@ -45,7 +45,7 @@ pub mod visual;
pub use self::{ pub use self::{
ability::{CharacterAbility, CharacterAbilityType}, ability::{CharacterAbility, CharacterAbilityType},
admin::Admin, admin::Admin,
agent::{Agent, Alignment}, agent::{Agent, Alignment, Behavior, BehaviorCapability, BehaviorState},
aura::{Aura, AuraChange, AuraKind, Auras}, aura::{Aura, AuraChange, AuraKind, Auras},
beam::{Beam, BeamSegment}, beam::{Beam, BeamSegment},
body::{ body::{

View File

@ -2,7 +2,7 @@ use crate::{
comp::{ comp::{
self, self,
inventory::loadout_builder::{LoadoutBuilder, LoadoutConfig}, inventory::loadout_builder::{LoadoutBuilder, LoadoutConfig},
CharacterState, StateUpdate, Behavior, BehaviorCapability, CharacterState, StateUpdate,
}, },
event::{LocalEvent, ServerEvent}, event::{LocalEvent, ServerEvent},
outcome::Outcome, outcome::Outcome,
@ -104,7 +104,12 @@ impl CharacterBehavior for Data {
poise: comp::Poise::new(body), poise: comp::Poise::new(body),
loadout, loadout,
body, 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), alignment: comp::Alignment::Owned(*data.uid),
scale: self scale: self
.static_data .static_data

View File

@ -1074,7 +1074,7 @@ fn handle_spawn_airship(
animated: true, animated: true,
}); });
if let Some(pos) = destination { if let Some(pos) = destination {
builder = builder.with(comp::Agent::with_destination(pos)) builder = builder.with(comp::Agent::default().with_destination(pos))
} }
builder.build(); builder.build();

View File

@ -225,8 +225,18 @@ pub fn handle_invite_accept(server: &mut Server, entity: specs::Entity) {
} }
let pricing = agents let pricing = agents
.get(inviter) .get(inviter)
.and_then(|a| index.get_site_prices(a)) .and_then(|a| {
.or_else(|| agents.get(entity).and_then(|a| index.get_site_prices(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| { clients.get(inviter).map(|c| {
c.send(ServerGeneral::UpdatePendingTrade( c.send(ServerGeneral::UpdatePendingTrade(
id, id,

View File

@ -32,15 +32,16 @@ fn notify_agent_prices(
entity: EcsEntity, entity: EcsEntity,
event: AgentEvent, event: AgentEvent,
) { ) {
if let Some(agent) = agents.get_mut(entity) { if let Some((Some(site_id), agent)) = agents.get_mut(entity).map(|a| (a.behavior.trade_site, a))
let prices = index.get_site_prices(agent); {
let prices = index.get_site_prices(site_id);
if let AgentEvent::UpdatePendingTrade(boxval) = event { if let AgentEvent::UpdatePendingTrade(boxval) = event {
// Box<(tid, pend, _, inventories)>) = event { // Box<(tid, pend, _, inventories)>) = event {
agent agent
.inbox .inbox
.push_front(AgentEvent::UpdatePendingTrade(Box::new(( .push_front(AgentEvent::UpdatePendingTrade(Box::new((
// Prefer using this Agent's price data, but use the counterparty's price data // Prefer using this Agent's price data, but use the counterparty's price
// if we don't have price data // data if we don't have price data
boxval.0, boxval.0,
boxval.1, boxval.1,
prices.unwrap_or(boxval.2), 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 // Get price info from the first Agent in the trade (currently, an
// Agent will never initiate a trade with another agent though) // Agent will never initiate a trade with another agent though)
prices = prices.or_else(|| { 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))
}); });
} }
} }

View File

@ -2,7 +2,7 @@
use super::*; use super::*;
use common::{ use common::{
comp::{self, inventory::loadout_builder::LoadoutBuilder}, comp::{self, inventory::loadout_builder::LoadoutBuilder, Behavior, BehaviorCapability},
event::{EventBus, ServerEvent}, event::{EventBus, ServerEvent},
resources::{DeltaTime, Time}, resources::{DeltaTime, Time},
terrain::TerrainGrid, terrain::TerrainGrid,
@ -104,12 +104,16 @@ impl<'a> System<'a> for Sys {
+ Vec3::new(0.5, 0.5, body.flying_height()); + Vec3::new(0.5, 0.5, body.flying_height());
let pos = comp::Pos(spawn_pos); let pos = comp::Pos(spawn_pos);
let agent = Some(comp::Agent::new( let agent = Some(comp::Agent::new(
None,
matches!(body, comp::Body::Humanoid(_)),
None, None,
&body, &body,
if matches!(body, comp::Body::Humanoid(_)) {
Behavior::from(BehaviorCapability::SPEAK)
} else {
Behavior::default()
},
false, false,
)); ));
let rtsim_entity = Some(RtSimEntity(id)); let rtsim_entity = Some(RtSimEntity(id));
let event = match body { let event = match body {
comp::Body::Ship(ship) => ServerEvent::CreateShip { comp::Body::Ship(ship) => ServerEvent::CreateShip {

View File

@ -14,9 +14,9 @@ use common::{
ItemDesc, 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, BehaviorCapability, BehaviorState, Body, CharacterState, ControlAction,
Health, InputKind, Inventory, LightEmitter, MountState, Ori, PhysicsState, Pos, Scale, ControlEvent, Controller, Energy, Health, InputKind, Inventory, LightEmitter, MountState,
Stats, UnresolvedChatMsg, Vel, Ori, PhysicsState, Pos, Scale, Stats, UnresolvedChatMsg, Vel,
}, },
event::{Emitter, EventBus, ServerEvent}, event::{Emitter, EventBus, ServerEvent},
path::TraversalConfig, path::TraversalConfig,
@ -137,9 +137,11 @@ impl<'a> System<'a> for Sys {
( (
&read_data.entities, &read_data.entities,
(&read_data.energies, read_data.healths.maybe()), (&read_data.energies, read_data.healths.maybe()),
&read_data.positions, (
&read_data.velocities, &read_data.positions,
&read_data.orientations, &read_data.velocities,
&read_data.orientations,
),
read_data.bodies.maybe(), read_data.bodies.maybe(),
&read_data.inventories, &read_data.inventories,
&read_data.stats, &read_data.stats,
@ -153,14 +155,12 @@ impl<'a> System<'a> for Sys {
&read_data.char_states, &read_data.char_states,
) )
.par_join() .par_join()
.filter( .filter(|(_, _, _, _, _, _, _, _, _, _, _, _, mount_state, _)| {
|(_, _, _, _, _, _, _, _, _, _, _, _, _, _, mount_state, _)| { // Skip mounted entities
// Skip mounted entities mount_state
mount_state .map(|ms| *ms == MountState::Unmounted)
.map(|ms| *ms == MountState::Unmounted) .unwrap_or(true)
.unwrap_or(true) })
},
)
.for_each_init( .for_each_init(
|| { || {
prof_span!(guard, "agent rayon job"); prof_span!(guard, "agent rayon job");
@ -170,9 +170,7 @@ impl<'a> System<'a> for Sys {
( (
entity, entity,
(energy, health), (energy, health),
pos, (pos, vel, ori),
vel,
ori,
body, body,
inventory, inventory,
stats, stats,
@ -551,7 +549,7 @@ impl<'a> AgentData<'a> {
} }
if agent.action_timer > 0.0 { if agent.action_timer > 0.0 {
if agent.action_timer if agent.action_timer
< (if agent.trading { < (if agent.behavior.is(BehaviorState::TRADING) {
TRADE_INTERACTION_TIME TRADE_INTERACTION_TIME
} else { } else {
DEFAULT_INTERACTION_TIME DEFAULT_INTERACTION_TIME
@ -588,7 +586,7 @@ impl<'a> AgentData<'a> {
let dist_sqrd = self.pos.0.distance_squared(tgt_pos.0); let dist_sqrd = self.pos.0.distance_squared(tgt_pos.0);
// Should the agent flee? // Should the agent flee?
if 1.0 - agent.psyche.aggro > self.damage && self.flees { 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(); let msg = "npc.speech.villager_under_attack".to_string();
event_emitter event_emitter
.emit(ServerEvent::Chat(UnresolvedChatMsg::npc(*self.uid, msg))); .emit(ServerEvent::Chat(UnresolvedChatMsg::npc(*self.uid, msg)));
@ -616,7 +614,7 @@ impl<'a> AgentData<'a> {
read_data.buffs.get(target), read_data.buffs.get(target),
) { ) {
agent.target = None; agent.target = None;
if agent.can_speak { if agent.behavior.can(BehaviorCapability::SPEAK) {
let msg = "npc.speech.villager_enemy_killed".to_string(); let msg = "npc.speech.villager_enemy_killed".to_string();
event_emitter event_emitter
.emit(ServerEvent::Chat(UnresolvedChatMsg::npc(*self.uid, msg))); .emit(ServerEvent::Chat(UnresolvedChatMsg::npc(*self.uid, msg)));
@ -879,7 +877,7 @@ impl<'a> AgentData<'a> {
let msg = agent.inbox.pop_back(); let msg = agent.inbox.pop_back();
match msg { match msg {
Some(AgentEvent::Talk(by, subject)) => { 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()) if let Some(target) = read_data.uid_allocator.retrieve_entity_internal(by.id())
{ {
agent.target = Some(Target { agent.target = Some(Target {
@ -933,7 +931,7 @@ impl<'a> AgentData<'a> {
event_emitter.emit(ServerEvent::Chat( event_emitter.emit(ServerEvent::Chat(
UnresolvedChatMsg::npc(*self.uid, msg), 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(); let msg = "npc.speech.merchant_advertisement".to_string();
event_emitter.emit(ServerEvent::Chat( event_emitter.emit(ServerEvent::Chat(
UnresolvedChatMsg::npc(*self.uid, msg), UnresolvedChatMsg::npc(*self.uid, msg),
@ -946,8 +944,8 @@ impl<'a> AgentData<'a> {
} }
}, },
Subject::Trade => { Subject::Trade => {
if agent.trade_for_site.is_some() { if agent.behavior.can_trade() {
if !agent.trading { if !agent.behavior.is(BehaviorState::TRADING) {
controller.events.push(ControlEvent::InitiateInvite( controller.events.push(ControlEvent::InitiateInvite(
by, by,
InviteKind::Trade, InviteKind::Trade,
@ -1095,8 +1093,8 @@ impl<'a> AgentData<'a> {
} }
}, },
Some(AgentEvent::TradeInvite(with)) => { Some(AgentEvent::TradeInvite(with)) => {
if agent.trade_for_site.is_some() { if agent.behavior.can_trade() {
if !agent.trading { if !agent.behavior.is(BehaviorState::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::Stand);
controller.actions.push(ControlAction::Talk); controller.actions.push(ControlAction::Talk);
@ -1112,13 +1110,13 @@ impl<'a> AgentData<'a> {
controller controller
.events .events
.push(ControlEvent::InviteResponse(InviteResponse::Accept)); .push(ControlEvent::InviteResponse(InviteResponse::Accept));
agent.trading_issuer = false; agent.behavior.unset(BehaviorState::TRADING_ISSUER);
agent.trading = true; agent.behavior.set(BehaviorState::TRADING);
} else { } else {
controller controller
.events .events
.push(ControlEvent::InviteResponse(InviteResponse::Decline)); .push(ControlEvent::InviteResponse(InviteResponse::Decline));
if agent.can_speak { if agent.behavior.can(BehaviorCapability::SPEAK) {
event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc( event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc(
*self.uid, *self.uid,
"npc.speech.merchant_busy".to_string(), "npc.speech.merchant_busy".to_string(),
@ -1130,7 +1128,7 @@ impl<'a> AgentData<'a> {
controller controller
.events .events
.push(ControlEvent::InviteResponse(InviteResponse::Decline)); .push(ControlEvent::InviteResponse(InviteResponse::Decline));
if agent.can_speak { if agent.behavior.can(BehaviorCapability::SPEAK) {
event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc( event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc(
*self.uid, *self.uid,
"npc.speech.villager_decline_trade".to_string(), "npc.speech.villager_decline_trade".to_string(),
@ -1139,7 +1137,7 @@ impl<'a> AgentData<'a> {
} }
}, },
Some(AgentEvent::TradeAccepted(with)) => { Some(AgentEvent::TradeAccepted(with)) => {
if !agent.trading { if !agent.behavior.is(BehaviorState::TRADING) {
if let Some(target) = if let Some(target) =
read_data.uid_allocator.retrieve_entity_internal(with.id()) read_data.uid_allocator.retrieve_entity_internal(with.id())
{ {
@ -1149,12 +1147,12 @@ impl<'a> AgentData<'a> {
selected_at: read_data.time.0, selected_at: read_data.time.0,
}); });
} }
agent.trading = true; agent.behavior.set(BehaviorState::TRADING);
agent.trading_issuer = true; agent.behavior.set(BehaviorState::TRADING_ISSUER);
} }
}, },
Some(AgentEvent::FinishedTrade(result)) => { Some(AgentEvent::FinishedTrade(result)) => {
if agent.trading { if agent.behavior.is(BehaviorState::TRADING) {
match result { match result {
TradeResult::Completed => { TradeResult::Completed => {
event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc( event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc(
@ -1167,13 +1165,17 @@ impl<'a> AgentData<'a> {
"npc.speech.merchant_trade_declined".to_string(), "npc.speech.merchant_trade_declined".to_string(),
))), ))),
} }
agent.trading = false; agent.behavior.unset(BehaviorState::TRADING);
} }
}, },
Some(AgentEvent::UpdatePendingTrade(boxval)) => { Some(AgentEvent::UpdatePendingTrade(boxval)) => {
let (tradeid, pending, prices, inventories) = *boxval; let (tradeid, pending, prices, inventories) = *boxval;
if agent.trading { if agent.behavior.is(BehaviorState::TRADING) {
let who: usize = if agent.trading_issuer { 0 } else { 1 }; let who: usize = if agent.behavior.is(BehaviorState::TRADING_ISSUER) {
0
} else {
1
};
let balance0: f32 = let balance0: f32 =
prices.balance(&pending.offers, &inventories, 1 - who, true); prices.balance(&pending.offers, &inventories, 1 - who, true);
let balance1: f32 = prices.balance(&pending.offers, &inventories, who, false); let balance1: f32 = prices.balance(&pending.offers, &inventories, who, false);
@ -1203,7 +1205,7 @@ impl<'a> AgentData<'a> {
} }
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
agent.trading = false; agent.behavior.unset(BehaviorState::TRADING);
event_emitter.emit(ServerEvent::ProcessTradeAction( event_emitter.emit(ServerEvent::ProcessTradeAction(
*self.entity, *self.entity,
tradeid, tradeid,
@ -1214,7 +1216,7 @@ impl<'a> AgentData<'a> {
} }
}, },
None => { None => {
if agent.can_speak { if agent.behavior.can(BehaviorCapability::SPEAK) {
// no new events, continue looking towards the last interacting player for some // no new events, continue looking towards the last interacting player for some
// time // time
if let Some(Target { target, .. }) = &agent.target { if let Some(Target { target, .. }) = &agent.target {
@ -1337,7 +1339,7 @@ impl<'a> AgentData<'a> {
( (
self.alignment.map_or(false, |alignment| { 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 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() { if self.rtsim_entity.is_some() {
agent.rtsim_controller.events.push( agent.rtsim_controller.events.push(
RtSimEvent::AddMemory(Memory { RtSimEvent::AddMemory(Memory {

View File

@ -2,7 +2,10 @@ use crate::{
chunk_generator::ChunkGenerator, client::Client, presence::Presence, rtsim::RtSim, Tick, chunk_generator::ChunkGenerator, client::Client, presence::Presence, rtsim::RtSim, Tick,
}; };
use common::{ 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}, event::{EventBus, ServerEvent},
generation::get_npc_name, generation::get_npc_name,
npc::NPC_NAMES, npc::NPC_NAMES,
@ -12,6 +15,7 @@ use common::{
use common_ecs::{Job, Origin, Phase, System}; use common_ecs::{Job, Origin, Phase, System};
use common_net::msg::ServerGeneral; use common_net::msg::ServerGeneral;
use common_sys::state::TerrainChanges; use common_sys::state::TerrainChanges;
use comp::Behavior;
use specs::{Join, Read, ReadStorage, Write, WriteExpect}; use specs::{Join, Read, ReadStorage, Write, WriteExpect};
use std::sync::Arc; use std::sync::Arc;
use vek::*; use vek::*;
@ -191,9 +195,12 @@ impl<'a> System<'a> for Sys {
agent: if entity.has_agency { agent: if entity.has_agency {
Some(comp::Agent::new( Some(comp::Agent::new(
Some(entity.pos), Some(entity.pos),
can_speak,
trade_for_site,
&body, &body,
Behavior::default()
.maybe_with_capabilities(
can_speak.then(|| BehaviorCapability::SPEAK),
)
.with_trade_site(trade_for_site),
matches!( matches!(
loadout_config, loadout_config,
Some(comp::inventory::loadout_builder::LoadoutConfig::Guard) Some(comp::inventory::loadout_builder::LoadoutConfig::Guard)

View File

@ -4,9 +4,8 @@ use crate::{
}; };
use common::{ use common::{
assets::{AssetExt, AssetHandle}, assets::{AssetExt, AssetHandle},
comp::Agent,
store::Store, store::Store,
trade::SitePrices, trade::{SiteId, SitePrices},
}; };
use core::ops::Deref; use core::ops::Deref;
use noise::{Seedable, SuperSimplex}; use noise::{Seedable, SuperSimplex};
@ -72,11 +71,9 @@ impl Index {
pub fn colors(&self) -> AssetHandle<Arc<Colors>> { self.colors } pub fn colors(&self) -> AssetHandle<Arc<Colors>> { self.colors }
pub fn get_site_prices(&self, agent: &Agent) -> Option<SitePrices> { pub fn get_site_prices(&self, site_id: SiteId) -> Option<SitePrices> {
agent self.sites
.trade_for_site .recreate_id(site_id)
.map(|i| self.sites.recreate_id(i))
.flatten()
.map(|i| self.sites.get(i)) .map(|i| self.sites.get(i))
.map(|s| s.economy.get_site_prices()) .map(|s| s.economy.get_site_prices())
} }