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 /// # Behavior Component
/// This component allow an Entity to register one or more behavior tags. /// 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. /// 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 { pub struct Behavior {
capabilities: BehaviorCapability, capabilities: BehaviorCapability,
state: BehaviorState, state: BehaviorState,
pub trade_site: Option<SiteId>, pub trading_behavior: TradingBehavior,
} }
impl From<BehaviorCapability> for Behavior { impl From<BehaviorCapability> for Behavior {
@ -114,7 +134,7 @@ impl From<BehaviorCapability> for Behavior {
Behavior { Behavior {
capabilities, capabilities,
state: BehaviorState::default(), state: BehaviorState::default(),
trade_site: None, trading_behavior: TradingBehavior::None,
} }
} }
} }
@ -137,7 +157,9 @@ impl Behavior {
/// Set trade_site if Option is Some /// Set trade_site if Option is Some
#[must_use] #[must_use]
pub fn with_trade_site(mut self, trade_site: Option<SiteId>) -> Self { 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 self
} }
@ -158,7 +180,7 @@ impl Behavior {
/// Check if the Behavior is able to trade /// Check if the Behavior is able to trade
pub fn can_trade(&self, alignment: Option<Alignment>, counterparty: Uid) -> bool { 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 /// Set a state to the Behavior
@ -169,6 +191,15 @@ impl Behavior {
/// Check if the Behavior has a specific state /// Check if the Behavior has a specific state
pub fn is(&self, state: BehaviorState) -> bool { self.state.contains(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)] #[derive(Clone, Debug, Default)]

View File

@ -55,7 +55,10 @@ pub use self::{
MAX_ABILITIES, MAX_ABILITIES,
}, },
admin::{Admin, AdminRole}, admin::{Admin, AdminRole},
agent::{Agent, Alignment, Behavior, BehaviorCapability, BehaviorState, PidController}, agent::{
Agent, Alignment, Behavior, BehaviorCapability, BehaviorState, PidController,
TradingBehavior,
},
anchor::Anchor, anchor::Anchor,
aura::{Aura, AuraChange, AuraKind, Auras}, aura::{Aura, AuraChange, AuraKind, Auras},
beam::{Beam, BeamSegment}, beam::{Beam, BeamSegment},

View File

@ -9,7 +9,7 @@ use common::{
buff::{BuffCategory, BuffData, BuffKind, BuffSource}, buff::{BuffCategory, BuffData, BuffKind, BuffSource},
shockwave, Agent, Alignment, Anchor, BehaviorCapability, Body, Health, Inventory, ItemDrop, shockwave, Agent, Alignment, Anchor, BehaviorCapability, Body, Health, Inventory, ItemDrop,
LightEmitter, Object, Ori, PidController, Poise, Pos, Projectile, Scale, SkillSet, Stats, LightEmitter, Object, Ori, PidController, Poise, Pos, Projectile, Scale, SkillSet, Stats,
Vel, WaypointArea, TradingBehavior, Vel, WaypointArea,
}, },
event::{EventBus, UpdateCharacterMetadata}, event::{EventBus, UpdateCharacterMetadata},
lottery::LootSpec, lottery::LootSpec,
@ -104,6 +104,7 @@ pub fn handle_create_npc(
if let Some(agent) = &mut agent { if let Some(agent) = &mut agent {
if let Alignment::Owned(_) = &alignment { if let Alignment::Owned(_) = &alignment {
agent.behavior.allow(BehaviorCapability::TRADE); 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) .get(inviter)
.and_then(|a| { .and_then(|a| {
a.behavior a.behavior
.trade_site .trade_site()
.and_then(|id| index.get_site_prices(id)) .and_then(|id| index.get_site_prices(id))
}) })
.or_else(|| { .or_else(|| {
agents.get(entity).and_then(|a| { agents.get(entity).and_then(|a| {
a.behavior a.behavior
.trade_site .trade_site()
.and_then(|id| index.get_site_prices(id)) .and_then(|id| index.get_site_prices(id))
}) })
}); });

View File

@ -35,7 +35,7 @@ fn notify_agent_prices(
entity: EcsEntity, entity: EcsEntity,
event: AgentEvent, 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 { if let AgentEvent::UpdatePendingTrade(boxval) = event {
// Prefer using this Agent's price data, but use the counterparty's price // Prefer using this Agent's price data, but use the counterparty's price
// data if we don't have price data // data if we don't have price data
@ -125,7 +125,7 @@ pub(super) fn handle_process_trade_action(
prices = prices.or_else(|| { prices = prices.or_else(|| {
agents agents
.get(e) .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)) .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::{ use common::{
comp::{ comp::{
self, anchor::Anchor, group::GroupManager, Agent, Alignment, Behavior, BehaviorCapability, self, anchor::Anchor, group::GroupManager, Agent, Alignment, Behavior, BehaviorCapability,
Pet, Pet, TradingBehavior,
}, },
uid::Uid, 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 // Create an agent for this entity using its body
if let Some(body) = ecs.read_storage().get(pet_entity) { 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)), Behavior::default().maybe_with_capabilities(Some(BehaviorCapability::TRADE)),
); );
agent.behavior.trading_behavior = TradingBehavior::AcceptFood;
let _ = ecs.write_storage().insert(pet_entity, agent); let _ = ecs.write_storage().insert(pet_entity, agent);
} }

View File

@ -3,8 +3,10 @@ use common::{
agent::{AgentEvent, Target, TimerAction}, agent::{AgentEvent, Target, TimerAction},
compass::{Direction, Distance}, compass::{Direction, Distance},
dialogue::{MoodContext, MoodState, Subject}, dialogue::{MoodContext, MoodState, Subject},
inventory::item::{ItemTag, MaterialStatManifest},
invite::{InviteKind, InviteResponse}, invite::{InviteKind, InviteResponse},
BehaviorState, ControlAction, UnresolvedChatMsg, UtteranceKind, tool::AbilityMap,
BehaviorState, ControlAction, Item, TradingBehavior, UnresolvedChatMsg, UtteranceKind,
}, },
event::ServerEvent, event::ServerEvent,
rtsim::{Memory, MemoryItem, RtSimEvent}, 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; let (tradeid, pending, prices, inventories) = *boxval;
if agent.behavior.is(BehaviorState::TRADING) { if agent.behavior.is(BehaviorState::TRADING) {
let who = usize::from(!agent.behavior.is(BehaviorState::TRADING_ISSUER)); let who = usize::from(!agent.behavior.is(BehaviorState::TRADING_ISSUER));
let balance0: f32 = prices.balance(&pending.offers, &inventories, 1 - who, true); match agent.behavior.trading_behavior {
let balance1: f32 = prices.balance(&pending.offers, &inventories, who, false); TradingBehavior::RequireBalanced { .. } => {
if balance0 >= balance1 { let balance0: f32 =
// If the trade is favourable to us, only send an accept message if we're prices.balance(&pending.offers, &inventories, 1 - who, true);
// not already accepting (since otherwise, spam-clicking the accept button let balance1: f32 = prices.balance(&pending.offers, &inventories, who, false);
// results in lagging and moving to the review phase of an unfavorable trade if balance0 >= balance1 {
// (although since the phase is included in the message, this shouldn't // If the trade is favourable to us, only send an accept message if we're
// result in fully accepting an unfavourable trade)) // not already accepting (since otherwise, spam-clicking the accept button
if !pending.accept_flags[who] && !pending.is_empty_trade() { // results in lagging and moving to the review phase of an unfavorable trade
event_emitter.emit(ServerEvent::ProcessTradeAction( // (although since the phase is included in the message, this shouldn't
*agent_data.entity, // result in fully accepting an unfavourable trade))
tradeid, if !pending.accept_flags[who] && !pending.is_empty_trade() {
TradeAction::Accept(pending.phase), event_emitter.emit(ServerEvent::ProcessTradeAction(
)); *agent_data.entity,
tracing::trace!(?tradeid, ?balance0, ?balance1, "Accept Pending Trade"); tradeid,
} TradeAction::Accept(pending.phase),
} else { ));
if balance1 > 0.0 { tracing::trace!(?tradeid, ?balance0, ?balance1, "Accept Pending Trade");
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 { } else {
event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc_say( if balance1 > 0.0 {
*agent_data.uid, let msg = format!(
msg, "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 { TradingBehavior::AcceptFood => {
// we got into the review phase but without balanced goods, decline 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); agent.behavior.unset(BehaviorState::TRADING);
event_emitter.emit(ServerEvent::ProcessTradeAction( event_emitter.emit(ServerEvent::ProcessTradeAction(
*agent_data.entity, *agent_data.entity,
tradeid, tradeid,
TradeAction::Decline, TradeAction::Decline,
)); ));
} },
} }
} }
} }