diff --git a/common/src/comp/agent.rs b/common/src/comp/agent.rs index 0367ea5be3..65d000d25a 100644 --- a/common/src/comp/agent.rs +++ b/common/src/comp/agent.rs @@ -97,6 +97,26 @@ bitflags::bitflags! { } } +#[derive(Default, Copy, Clone, Debug)] +pub enum TradingBehavior { + #[default] + None, + RequireBalanced { + trade_site: SiteId, + }, + AcceptFood, +} + +impl TradingBehavior { + fn can_trade(&self, alignment: Option, counterparty: Uid) -> bool { + match self { + TradingBehavior::RequireBalanced { .. } => true, + TradingBehavior::AcceptFood => alignment == Some(Alignment::Owned(counterparty)), + TradingBehavior::None => false, + } + } +} + /// # 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. @@ -106,7 +126,7 @@ bitflags::bitflags! { pub struct Behavior { capabilities: BehaviorCapability, state: BehaviorState, - pub trade_site: Option, + pub trading_behavior: TradingBehavior, } impl From for Behavior { @@ -114,7 +134,7 @@ impl From for Behavior { Behavior { capabilities, state: BehaviorState::default(), - trade_site: None, + trading_behavior: TradingBehavior::None, } } } @@ -137,7 +157,9 @@ impl Behavior { /// Set trade_site if Option is Some #[must_use] pub fn with_trade_site(mut self, trade_site: Option) -> Self { - self.trade_site = trade_site; + if let Some(trade_site) = trade_site { + self.trading_behavior = TradingBehavior::RequireBalanced { trade_site }; + } self } @@ -158,7 +180,7 @@ impl Behavior { /// Check if the Behavior is able to trade pub fn can_trade(&self, alignment: Option, counterparty: Uid) -> bool { - self.trade_site.is_some() || alignment == Some(Alignment::Owned(counterparty)) + self.trading_behavior.can_trade(alignment, counterparty) } /// Set a state to the Behavior @@ -169,6 +191,15 @@ impl Behavior { /// Check if the Behavior has a specific state pub fn is(&self, state: BehaviorState) -> bool { self.state.contains(state) } + + /// Get the trade site at which this behavior evaluates prices, if it does + pub fn trade_site(&self) -> Option { + if let TradingBehavior::RequireBalanced { trade_site } = self.trading_behavior { + Some(trade_site) + } else { + None + } + } } #[derive(Clone, Debug, Default)] diff --git a/common/src/comp/mod.rs b/common/src/comp/mod.rs index 4da189e746..e7afd6c5da 100644 --- a/common/src/comp/mod.rs +++ b/common/src/comp/mod.rs @@ -55,7 +55,10 @@ pub use self::{ MAX_ABILITIES, }, admin::{Admin, AdminRole}, - agent::{Agent, Alignment, Behavior, BehaviorCapability, BehaviorState, PidController}, + agent::{ + Agent, Alignment, Behavior, BehaviorCapability, BehaviorState, PidController, + TradingBehavior, + }, anchor::Anchor, aura::{Aura, AuraChange, AuraKind, Auras}, beam::{Beam, BeamSegment}, diff --git a/server/src/events/entity_creation.rs b/server/src/events/entity_creation.rs index b73d1de201..7a6d3a493f 100644 --- a/server/src/events/entity_creation.rs +++ b/server/src/events/entity_creation.rs @@ -9,7 +9,7 @@ use common::{ buff::{BuffCategory, BuffData, BuffKind, BuffSource}, shockwave, Agent, Alignment, Anchor, BehaviorCapability, Body, Health, Inventory, ItemDrop, LightEmitter, Object, Ori, PidController, Poise, Pos, Projectile, Scale, SkillSet, Stats, - Vel, WaypointArea, + TradingBehavior, Vel, WaypointArea, }, event::{EventBus, UpdateCharacterMetadata}, lottery::LootSpec, @@ -104,6 +104,7 @@ pub fn handle_create_npc( if let Some(agent) = &mut agent { if let Alignment::Owned(_) = &alignment { agent.behavior.allow(BehaviorCapability::TRADE); + agent.behavior.trading_behavior = TradingBehavior::AcceptFood; } } diff --git a/server/src/events/invite.rs b/server/src/events/invite.rs index 227f38a925..f3285c1fe3 100644 --- a/server/src/events/invite.rs +++ b/server/src/events/invite.rs @@ -246,13 +246,13 @@ pub fn handle_invite_accept(server: &mut Server, entity: Entity) { .get(inviter) .and_then(|a| { a.behavior - .trade_site + .trade_site() .and_then(|id| index.get_site_prices(id)) }) .or_else(|| { agents.get(entity).and_then(|a| { a.behavior - .trade_site + .trade_site() .and_then(|id| index.get_site_prices(id)) }) }); diff --git a/server/src/events/trade.rs b/server/src/events/trade.rs index 326e78c4f7..2b85ece0cf 100644 --- a/server/src/events/trade.rs +++ b/server/src/events/trade.rs @@ -35,7 +35,7 @@ fn notify_agent_prices( entity: EcsEntity, event: AgentEvent, ) { - if let Some((site_id, agent)) = agents.get_mut(entity).map(|a| (a.behavior.trade_site, a)) { + if let Some((site_id, agent)) = agents.get_mut(entity).map(|a| (a.behavior.trade_site(), a)) { if let AgentEvent::UpdatePendingTrade(boxval) = event { // Prefer using this Agent's price data, but use the counterparty's price // data if we don't have price data @@ -125,7 +125,7 @@ pub(super) fn handle_process_trade_action( prices = prices.or_else(|| { agents .get(e) - .and_then(|a| a.behavior.trade_site) + .and_then(|a| a.behavior.trade_site()) .and_then(|id| server.index.get_site_prices(id)) }); } diff --git a/server/src/pet.rs b/server/src/pet.rs index 3fce94ec43..5d4ea39df6 100644 --- a/server/src/pet.rs +++ b/server/src/pet.rs @@ -2,7 +2,7 @@ use crate::{client::Client, events::update_map_markers}; use common::{ comp::{ self, anchor::Anchor, group::GroupManager, Agent, Alignment, Behavior, BehaviorCapability, - Pet, + Pet, TradingBehavior, }, uid::Uid, }; @@ -54,9 +54,10 @@ fn tame_pet_internal(ecs: &specs::World, pet_entity: Entity, owner: Entity, pet: // Create an agent for this entity using its body if let Some(body) = ecs.read_storage().get(pet_entity) { - let agent = Agent::from_body(body).with_behavior( + let mut agent = Agent::from_body(body).with_behavior( Behavior::default().maybe_with_capabilities(Some(BehaviorCapability::TRADE)), ); + agent.behavior.trading_behavior = TradingBehavior::AcceptFood; let _ = ecs.write_storage().insert(pet_entity, agent); } diff --git a/server/src/sys/agent/behavior_tree/interaction.rs b/server/src/sys/agent/behavior_tree/interaction.rs index 32c0980029..8d0a812a30 100644 --- a/server/src/sys/agent/behavior_tree/interaction.rs +++ b/server/src/sys/agent/behavior_tree/interaction.rs @@ -3,8 +3,10 @@ use common::{ agent::{AgentEvent, Target, TimerAction}, compass::{Direction, Distance}, dialogue::{MoodContext, MoodState, Subject}, + inventory::item::{ItemTag, MaterialStatManifest}, invite::{InviteKind, InviteResponse}, - BehaviorState, ControlAction, UnresolvedChatMsg, UtteranceKind, + tool::AbilityMap, + BehaviorState, ControlAction, Item, TradingBehavior, UnresolvedChatMsg, UtteranceKind, }, event::ServerEvent, rtsim::{Memory, MemoryItem, RtSimEvent}, @@ -511,58 +513,100 @@ pub fn handle_inbox_update_pending_trade(bdata: &mut BehaviorData) -> bool { let (tradeid, pending, prices, inventories) = *boxval; if agent.behavior.is(BehaviorState::TRADING) { let who = usize::from(!agent.behavior.is(BehaviorState::TRADING_ISSUER)); - let balance0: f32 = prices.balance(&pending.offers, &inventories, 1 - who, true); - let balance1: f32 = prices.balance(&pending.offers, &inventories, who, false); - if balance0 >= balance1 { - // If the trade is favourable to us, only send an accept message if we're - // not already accepting (since otherwise, spam-clicking the accept button - // results in lagging and moving to the review phase of an unfavorable trade - // (although since the phase is included in the message, this shouldn't - // result in fully accepting an unfavourable trade)) - if !pending.accept_flags[who] && !pending.is_empty_trade() { - event_emitter.emit(ServerEvent::ProcessTradeAction( - *agent_data.entity, - tradeid, - TradeAction::Accept(pending.phase), - )); - tracing::trace!(?tradeid, ?balance0, ?balance1, "Accept Pending Trade"); - } - } else { - if balance1 > 0.0 { - let msg = format!( - "That only covers {:.0}% of my costs!", - (balance0 / balance1 * 100.0).floor() - ); - if let Some(tgt_data) = &agent.target { - // If talking with someone in particular, "tell" it only to them - if let Some(with) = read_data.uids.get(tgt_data.target) { - event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc_tell( - *agent_data.uid, - *with, - msg, - ))); - } else { - event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc_say( - *agent_data.uid, - msg, - ))); + match agent.behavior.trading_behavior { + TradingBehavior::RequireBalanced { .. } => { + let balance0: f32 = + prices.balance(&pending.offers, &inventories, 1 - who, true); + let balance1: f32 = prices.balance(&pending.offers, &inventories, who, false); + if balance0 >= balance1 { + // If the trade is favourable to us, only send an accept message if we're + // not already accepting (since otherwise, spam-clicking the accept button + // results in lagging and moving to the review phase of an unfavorable trade + // (although since the phase is included in the message, this shouldn't + // result in fully accepting an unfavourable trade)) + if !pending.accept_flags[who] && !pending.is_empty_trade() { + event_emitter.emit(ServerEvent::ProcessTradeAction( + *agent_data.entity, + tradeid, + TradeAction::Accept(pending.phase), + )); + tracing::trace!(?tradeid, ?balance0, ?balance1, "Accept Pending Trade"); } } else { - event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc_say( - *agent_data.uid, - msg, - ))); + if balance1 > 0.0 { + let msg = format!( + "That only covers {:.0}% of my costs!", + (balance0 / balance1 * 100.0).floor() + ); + if let Some(tgt_data) = &agent.target { + // If talking with someone in particular, "tell" it only to them + if let Some(with) = read_data.uids.get(tgt_data.target) { + event_emitter.emit(ServerEvent::Chat( + UnresolvedChatMsg::npc_tell(*agent_data.uid, *with, msg), + )); + } else { + event_emitter.emit(ServerEvent::Chat( + UnresolvedChatMsg::npc_say(*agent_data.uid, msg), + )); + } + } else { + event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc_say( + *agent_data.uid, + msg, + ))); + } + } + if pending.phase != TradePhase::Mutate { + // we got into the review phase but without balanced goods, decline + agent.behavior.unset(BehaviorState::TRADING); + event_emitter.emit(ServerEvent::ProcessTradeAction( + *agent_data.entity, + tradeid, + TradeAction::Decline, + )); + } } - } - if pending.phase != TradePhase::Mutate { - // we got into the review phase but without balanced goods, decline + }, + TradingBehavior::AcceptFood => { + let mut only_food = true; + let ability_map = AbilityMap::load().read(); + let msm = MaterialStatManifest::load().read(); + if let Some(ri) = &inventories[1 - who] { + for (slot, _) in pending.offers[1 - who].iter() { + if let Some(item) = ri.inventory.get(slot) { + if let Ok(item) = Item::new_from_item_definition_id( + item.name.as_ref(), + &ability_map, + &msm, + ) { + if !item.tags().contains(&ItemTag::Food) { + only_food = false; + break; + } + } + } + } + } + if !pending.accept_flags[who] + && pending.offers[who].is_empty() + && !pending.offers[1 - who].is_empty() + && only_food + { + event_emitter.emit(ServerEvent::ProcessTradeAction( + *agent_data.entity, + tradeid, + TradeAction::Accept(pending.phase), + )); + } + }, + TradingBehavior::None => { agent.behavior.unset(BehaviorState::TRADING); event_emitter.emit(ServerEvent::ProcessTradeAction( *agent_data.entity, tradeid, TradeAction::Decline, )); - } + }, } } }