Make the trading AI for pets only accept food.

This commit is contained in:
Avi Weinstock 2022-09-26 17:25:36 -04:00
parent ff781198d3
commit e6576f0cf3
7 changed files with 136 additions and 56 deletions

View File

@ -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<Alignment>, 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<SiteId>,
pub trading_behavior: TradingBehavior,
}
impl From<BehaviorCapability> for Behavior {
@ -114,7 +134,7 @@ impl From<BehaviorCapability> 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<SiteId>) -> 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<Alignment>, 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<SiteId> {
if let TradingBehavior::RequireBalanced { trade_site } = self.trading_behavior {
Some(trade_site)
} else {
None
}
}
}
#[derive(Clone, Debug, Default)]

View File

@ -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},

View File

@ -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;
}
}

View File

@ -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))
})
});

View File

@ -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))
});
}

View File

@ -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);
}

View File

@ -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,
));
}
},
}
}
}