From c62bc0dd0d4cbbfa11ea5da65e772349d06f6193 Mon Sep 17 00:00:00 2001 From: Avi Weinstock Date: Wed, 21 Sep 2022 19:21:46 -0400 Subject: [PATCH 1/4] Allow pets to be traded with. --- common/src/comp/agent.rs | 5 +- server/src/events/entity_creation.rs | 20 +++++-- server/src/events/trade.rs | 16 +++--- server/src/pet.rs | 12 ++-- server/src/sys/agent/behavior_tree.rs | 24 ++++---- .../sys/agent/behavior_tree/interaction.rs | 56 ++++++++++++++----- server/src/sys/terrain.rs | 1 + voxygen/src/hud/mod.rs | 14 +++-- 8 files changed, 99 insertions(+), 49 deletions(-) diff --git a/common/src/comp/agent.rs b/common/src/comp/agent.rs index 385025ce75..9129a5931a 100644 --- a/common/src/comp/agent.rs +++ b/common/src/comp/agent.rs @@ -86,6 +86,7 @@ bitflags::bitflags! { #[derive(Default)] pub struct BehaviorCapability: u8 { const SPEAK = 0b00000001; + const TRADE = 0b00000010; } } bitflags::bitflags! { @@ -156,7 +157,9 @@ impl Behavior { } /// Check if the Behavior is able to trade - pub fn can_trade(&self) -> bool { self.trade_site.is_some() } + pub fn can_trade(&self, alignment: Option<&Alignment>, counterparty: Uid) -> bool { + self.trade_site.is_some() || alignment == Some(&Alignment::Owned(counterparty)) + } /// Set a state to the Behavior pub fn set(&mut self, state: BehaviorState) { self.state.set(state, true) } diff --git a/server/src/events/entity_creation.rs b/server/src/events/entity_creation.rs index a9c7ada3d1..b73d1de201 100644 --- a/server/src/events/entity_creation.rs +++ b/server/src/events/entity_creation.rs @@ -7,9 +7,9 @@ use common::{ aura::{Aura, AuraKind, AuraTarget}, beam, buff::{BuffCategory, BuffData, BuffKind, BuffSource}, - shockwave, Agent, Alignment, Anchor, Body, Health, Inventory, ItemDrop, LightEmitter, - Object, Ori, PidController, Poise, Pos, Projectile, Scale, SkillSet, Stats, Vel, - WaypointArea, + shockwave, Agent, Alignment, Anchor, BehaviorCapability, Body, Health, Inventory, ItemDrop, + LightEmitter, Object, Ori, PidController, Poise, Pos, Projectile, Scale, SkillSet, Stats, + Vel, WaypointArea, }, event::{EventBus, UpdateCharacterMetadata}, lottery::LootSpec, @@ -98,10 +98,18 @@ pub fn handle_create_npc( let entity = server .state .create_npc(pos, stats, skill_set, health, poise, inventory, body) - .with(scale) - .with(alignment); + .with(scale); - let entity = if let Some(agent) = agent.into() { + let mut agent = agent.into(); + if let Some(agent) = &mut agent { + if let Alignment::Owned(_) = &alignment { + agent.behavior.allow(BehaviorCapability::TRADE); + } + } + + let entity = entity.with(alignment); + + let entity = if let Some(agent) = agent { entity.with(agent) } else { entity diff --git a/server/src/events/trade.rs b/server/src/events/trade.rs index 7b10a65b7a..326e78c4f7 100644 --- a/server/src/events/trade.rs +++ b/server/src/events/trade.rs @@ -35,20 +35,18 @@ fn notify_agent_prices( entity: EcsEntity, event: AgentEvent, ) { - 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 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 + let prices = site_id + .and_then(|site_id| index.get_site_prices(site_id)) + .unwrap_or(boxval.2); // Box<(tid, pend, _, inventories)>) = event { agent .inbox .push_back(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 - boxval.0, - boxval.1, - prices.unwrap_or(boxval.2), - boxval.3, + boxval.0, boxval.1, prices, boxval.3, )))); } } diff --git a/server/src/pet.rs b/server/src/pet.rs index 73b612386b..3fce94ec43 100644 --- a/server/src/pet.rs +++ b/server/src/pet.rs @@ -1,6 +1,9 @@ use crate::{client::Client, events::update_map_markers}; use common::{ - comp::{self, anchor::Anchor, group::GroupManager, Agent, Alignment, Pet}, + comp::{ + self, anchor::Anchor, group::GroupManager, Agent, Alignment, Behavior, BehaviorCapability, + Pet, + }, uid::Uid, }; use common_net::msg::ServerGeneral; @@ -51,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 _ = ecs - .write_storage() - .insert(pet_entity, Agent::from_body(body)); + let agent = Agent::from_body(body).with_behavior( + Behavior::default().maybe_with_capabilities(Some(BehaviorCapability::TRADE)), + ); + let _ = ecs.write_storage().insert(pet_entity, agent); } // Add to group system diff --git a/server/src/sys/agent/behavior_tree.rs b/server/src/sys/agent/behavior_tree.rs index 02967748d0..4b18b4a599 100644 --- a/server/src/sys/agent/behavior_tree.rs +++ b/server/src/sys/agent/behavior_tree.rs @@ -108,17 +108,21 @@ impl BehaviorTree { /// events. pub fn interaction(agent: &Agent) -> Self { let is_in_combat = agent.target.map_or(false, |t| t.hostile); - if !is_in_combat && agent.behavior.can(BehaviorCapability::SPEAK) { - Self { - tree: vec![ - increment_timer_deltatime, - handle_inbox_talk, - handle_inbox_trade_invite, - handle_inbox_trade_accepted, - handle_inbox_finished_trade, - handle_inbox_update_pending_trade, - ], + if !is_in_combat + && (agent.behavior.can(BehaviorCapability::SPEAK) + || agent.behavior.can(BehaviorCapability::TRADE)) + { + let mut tree: Vec = vec![increment_timer_deltatime]; + if agent.behavior.can(BehaviorCapability::SPEAK) { + tree.push(handle_inbox_talk); } + tree.extend_from_slice(&[ + handle_inbox_trade_invite, + handle_inbox_trade_accepted, + handle_inbox_finished_trade, + handle_inbox_update_pending_trade, + ]); + Self { tree } } else { Self { tree: vec![handle_inbox_cancel_interactions], diff --git a/server/src/sys/agent/behavior_tree/interaction.rs b/server/src/sys/agent/behavior_tree/interaction.rs index 706a31fa79..89c36b1444 100644 --- a/server/src/sys/agent/behavior_tree/interaction.rs +++ b/server/src/sys/agent/behavior_tree/interaction.rs @@ -167,7 +167,7 @@ pub fn handle_inbox_talk(bdata: &mut BehaviorData) -> bool { standard_response_msg() }; agent_data.chat_npc(msg, event_emitter); - } else if agent.behavior.can_trade() { + } else if agent.behavior.can_trade(agent_data.alignment, by) { if !agent.behavior.is(BehaviorState::TRADING) { controller.push_initiate_invite(by, InviteKind::Trade); agent_data.chat_npc( @@ -250,21 +250,29 @@ pub fn handle_inbox_talk(bdata: &mut BehaviorData) -> bool { } }, Subject::Trade => { - if agent.behavior.can_trade() { + if agent.behavior.can_trade(agent_data.alignment, by) { if !agent.behavior.is(BehaviorState::TRADING) { controller.push_initiate_invite(by, InviteKind::Trade); - agent_data.chat_npc( + agent_data.chat_npc_if_allowed_to_speak( "npc-speech-merchant_advertisement", + agent, event_emitter, ); } else { - agent_data.chat_npc("npc-speech-merchant_busy", event_emitter); + agent_data.chat_npc_if_allowed_to_speak( + "npc-speech-merchant_busy", + agent, + event_emitter, + ); } } else { // TODO: maybe make some travellers willing to trade with // simpler goods like potions - agent_data - .chat_npc("npc-speech-villager_decline_trade", event_emitter); + agent_data.chat_npc_if_allowed_to_speak( + "npc-speech-villager_decline_trade", + agent, + event_emitter, + ); } }, Subject::Mood => { @@ -387,7 +395,7 @@ pub fn handle_inbox_trade_invite(bdata: &mut BehaviorData) -> bool { } if let Some(AgentEvent::TradeInvite(with)) = agent.inbox.pop_front() { - if agent.behavior.can_trade() { + if agent.behavior.can_trade(agent_data.alignment, with) { if !agent.behavior.is(BehaviorState::TRADING) { // stand still and looking towards the trading player controller.push_action(ControlAction::Stand); @@ -458,10 +466,18 @@ pub fn handle_inbox_finished_trade(bdata: &mut BehaviorData) -> bool { if agent.behavior.is(BehaviorState::TRADING) { match result { TradeResult::Completed => { - agent_data.chat_npc("npc-speech-merchant_trade_successful", event_emitter); + agent_data.chat_npc_if_allowed_to_speak( + "npc-speech-merchant_trade_successful", + agent, + event_emitter, + ); }, _ => { - agent_data.chat_npc("npc-speech-merchant_trade_declined", event_emitter); + agent_data.chat_npc_if_allowed_to_speak( + "npc-speech-merchant_trade_declined", + agent, + event_emitter, + ); }, } agent.behavior.unset(BehaviorState::TRADING); @@ -587,7 +603,7 @@ pub fn handle_inbox_cancel_interactions(bdata: &mut BehaviorData) -> bool { { // in combat, speak to players that aren't the current target if !target.hostile || target.target != speaker { - if agent.behavior.can_trade() { + if agent.behavior.can_trade(agent_data.alignment, *by) { agent_data.chat_npc_if_allowed_to_speak( "npc-speech-merchant_busy", agent, @@ -610,12 +626,18 @@ pub fn handle_inbox_cancel_interactions(bdata: &mut BehaviorData) -> bool { if agent.behavior.is(BehaviorState::TRADING) { match result { TradeResult::Completed => { - agent_data - .chat_npc("npc-speech-merchant_trade_successful", event_emitter); + agent_data.chat_npc_if_allowed_to_speak( + "npc-speech-merchant_trade_successful", + agent, + event_emitter, + ); }, _ => { - agent_data - .chat_npc("npc-speech-merchant_trade_declined", event_emitter); + agent_data.chat_npc_if_allowed_to_speak( + "npc-speech-merchant_trade_declined", + agent, + event_emitter, + ); }, } agent.behavior.unset(BehaviorState::TRADING); @@ -631,7 +653,11 @@ pub fn handle_inbox_cancel_interactions(bdata: &mut BehaviorData) -> bool { *tradeid, TradeAction::Decline, )); - agent_data.chat_npc("npc-speech-merchant_trade_cancelled_hostile", event_emitter); + agent_data.chat_npc_if_allowed_to_speak( + "npc-speech-merchant_trade_cancelled_hostile", + agent, + event_emitter, + ); true }, AgentEvent::ServerSound(_) | AgentEvent::Hurt => false, diff --git a/server/src/sys/terrain.rs b/server/src/sys/terrain.rs index 7a815a0168..6c6be2558d 100644 --- a/server/src/sys/terrain.rs +++ b/server/src/sys/terrain.rs @@ -517,6 +517,7 @@ impl NpcData { .with_behavior( Behavior::default() .maybe_with_capabilities(can_speak.then_some(BehaviorCapability::SPEAK)) + .maybe_with_capabilities(trade_for_site.map(|_| BehaviorCapability::TRADE)) .with_trade_site(trade_for_site), ) .with_patrol_origin(pos) diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index 737539801c..de90b4a5bb 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -2103,12 +2103,18 @@ impl Hud { }, Some(comp::Alignment::Owned(owner)) if Some(*owner) == client.uid() - && !client.is_riding() - && is_mount.is_none() - && is_mountable(body, bodies.get(client.entity())) && dist_sqr < common::consts::MAX_MOUNT_RANGE.powi(2) => { - vec![(GameInput::Mount, i18n.get_msg("hud-mount").to_string())] + let mut options = + vec![(GameInput::Trade, i18n.get_msg("hud-trade").to_string())]; + if !client.is_riding() + && is_mount.is_none() + && is_mountable(body, bodies.get(client.entity())) + { + options + .push((GameInput::Mount, i18n.get_msg("hud-mount").to_string())) + } + options }, _ => Vec::new(), }, From 60f5383f8b34c5673835648861a5a4104dcf95b9 Mon Sep 17 00:00:00 2001 From: Avi Weinstock Date: Thu, 22 Sep 2022 12:53:35 -0400 Subject: [PATCH 2/4] Address comments for MR 3633. --- common/src/comp/agent.rs | 4 ++-- server/src/sys/agent/behavior_tree/interaction.rs | 14 ++++++++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/common/src/comp/agent.rs b/common/src/comp/agent.rs index 9129a5931a..0367ea5be3 100644 --- a/common/src/comp/agent.rs +++ b/common/src/comp/agent.rs @@ -157,8 +157,8 @@ impl Behavior { } /// Check if the Behavior is able to trade - pub fn can_trade(&self, alignment: Option<&Alignment>, counterparty: Uid) -> bool { - self.trade_site.is_some() || alignment == Some(&Alignment::Owned(counterparty)) + pub fn can_trade(&self, alignment: Option, counterparty: Uid) -> bool { + self.trade_site.is_some() || alignment == Some(Alignment::Owned(counterparty)) } /// Set a state to the Behavior diff --git a/server/src/sys/agent/behavior_tree/interaction.rs b/server/src/sys/agent/behavior_tree/interaction.rs index 89c36b1444..32c0980029 100644 --- a/server/src/sys/agent/behavior_tree/interaction.rs +++ b/server/src/sys/agent/behavior_tree/interaction.rs @@ -167,7 +167,10 @@ pub fn handle_inbox_talk(bdata: &mut BehaviorData) -> bool { standard_response_msg() }; agent_data.chat_npc(msg, event_emitter); - } else if agent.behavior.can_trade(agent_data.alignment, by) { + } else if agent + .behavior + .can_trade(agent_data.alignment.copied(), by) + { if !agent.behavior.is(BehaviorState::TRADING) { controller.push_initiate_invite(by, InviteKind::Trade); agent_data.chat_npc( @@ -250,7 +253,7 @@ pub fn handle_inbox_talk(bdata: &mut BehaviorData) -> bool { } }, Subject::Trade => { - if agent.behavior.can_trade(agent_data.alignment, by) { + if agent.behavior.can_trade(agent_data.alignment.copied(), by) { if !agent.behavior.is(BehaviorState::TRADING) { controller.push_initiate_invite(by, InviteKind::Trade); agent_data.chat_npc_if_allowed_to_speak( @@ -395,7 +398,10 @@ pub fn handle_inbox_trade_invite(bdata: &mut BehaviorData) -> bool { } if let Some(AgentEvent::TradeInvite(with)) = agent.inbox.pop_front() { - if agent.behavior.can_trade(agent_data.alignment, with) { + if agent + .behavior + .can_trade(agent_data.alignment.copied(), with) + { if !agent.behavior.is(BehaviorState::TRADING) { // stand still and looking towards the trading player controller.push_action(ControlAction::Stand); @@ -603,7 +609,7 @@ pub fn handle_inbox_cancel_interactions(bdata: &mut BehaviorData) -> bool { { // in combat, speak to players that aren't the current target if !target.hostile || target.target != speaker { - if agent.behavior.can_trade(agent_data.alignment, *by) { + if agent.behavior.can_trade(agent_data.alignment.copied(), *by) { agent_data.chat_npc_if_allowed_to_speak( "npc-speech-merchant_busy", agent, From ff781198d3bcea51ae516377f1bac6a0591935f0 Mon Sep 17 00:00:00 2001 From: Avi Weinstock Date: Thu, 22 Sep 2022 16:08:39 -0400 Subject: [PATCH 3/4] Fix 18-slot pets (they now properly get inventories based on their body type, which is usually 1-slot). Also fixes a related issue where pets lose their natural weapons/armour. --- CHANGELOG.md | 2 ++ server/src/persistence/character.rs | 2 +- server/src/state_ext.rs | 7 +++++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 430d70224d..a52ec830c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 extra ghost slider cursor when set above the limit (instead of snapping back to the limit). Limits on the view distance by the server no longer affect the settings saved on the client. - HQX upscaling shader for people playing on low internal resolutions +- Pets can now be traded with. ### Changed - Use fluent for translations @@ -54,6 +55,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Moderators and admins are no longer blocked from logging in when there are too many players. - FXAA now behaves correctly at non-1.0x internal resolutions - Pets no longer aggro on pet owners after being healed +- Pets no longer lose their intrinsic weapons/armour when loaded on login. ## [0.13.0] - 2022-07-23 diff --git a/server/src/persistence/character.rs b/server/src/persistence/character.rs index 508f8bed60..bde05d00c7 100644 --- a/server/src/persistence/character.rs +++ b/server/src/persistence/character.rs @@ -349,7 +349,7 @@ pub fn load_character_list(player_uuid_: &str, connection: &Connection) -> Chara Ok(CharacterItem { character: char, body: char_body, - inventory: Inventory::with_loadout_humanoid(loadout), + inventory: Inventory::with_loadout(loadout, char_body), }) }) .collect() diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index b4d5891139..7444cc3d86 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -26,7 +26,7 @@ use common::{ resources::{Time, TimeOfDay}, slowjob::SlowJobPool, uid::{Uid, UidAllocator}, - ViewDistances, + LoadoutBuilder, ViewDistances, }; use common_net::{ msg::{CharacterInfo, PlayerListUpdate, PresenceKind, ServerGeneral}, @@ -651,7 +651,10 @@ impl StateExt for State { comp::SkillSet::default(), Some(comp::Health::new(body, DEFAULT_PET_HEALTH_LEVEL)), Poise::new(body), - Inventory::with_empty(), + Inventory::with_loadout( + LoadoutBuilder::from_default(&body).build(), + body, + ), body, ) .with(comp::Scale(1.0)) From e6576f0cf3da0b4909d4831f1f118876dbc27395 Mon Sep 17 00:00:00 2001 From: Avi Weinstock Date: Mon, 26 Sep 2022 17:25:36 -0400 Subject: [PATCH 4/4] Make the trading AI for pets only accept food. --- common/src/comp/agent.rs | 39 +++++- common/src/comp/mod.rs | 5 +- server/src/events/entity_creation.rs | 3 +- server/src/events/invite.rs | 4 +- server/src/events/trade.rs | 4 +- server/src/pet.rs | 5 +- .../sys/agent/behavior_tree/interaction.rs | 132 ++++++++++++------ 7 files changed, 136 insertions(+), 56 deletions(-) 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, )); - } + }, } } }