Merge branch 'trade-pets-food' into 'master'

Allow pets to be traded with.

See merge request veloren/veloren!3633
This commit is contained in:
Marcel 2022-10-01 16:01:21 +00:00
commit 95edd377f1
13 changed files with 244 additions and 103 deletions

View File

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

View File

@ -86,6 +86,7 @@ bitflags::bitflags! {
#[derive(Default)]
pub struct BehaviorCapability: u8 {
const SPEAK = 0b00000001;
const TRADE = 0b00000010;
}
}
bitflags::bitflags! {
@ -96,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.
@ -105,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 {
@ -113,7 +134,7 @@ impl From<BehaviorCapability> for Behavior {
Behavior {
capabilities,
state: BehaviorState::default(),
trade_site: None,
trading_behavior: TradingBehavior::None,
}
}
}
@ -136,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
}
@ -156,7 +179,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.trading_behavior.can_trade(alignment, counterparty)
}
/// Set a state to the Behavior
pub fn set(&mut self, state: BehaviorState) { self.state.set(state, true) }
@ -166,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

@ -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,
TradingBehavior, Vel, WaypointArea,
},
event::{EventBus, UpdateCharacterMetadata},
lottery::LootSpec,
@ -98,10 +98,19 @@ 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);
agent.behavior.trading_behavior = TradingBehavior::AcceptFood;
}
}
let entity = entity.with(alignment);
let entity = if let Some(agent) = agent {
entity.with(agent)
} else {
entity

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

@ -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()

View File

@ -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, TradingBehavior,
},
uid::Uid,
};
use common_net::msg::ServerGeneral;
@ -51,9 +54,11 @@ 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 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);
}
// Add to group system

View File

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

View File

@ -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<BehaviorFn> = 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],

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},
@ -167,7 +169,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() {
} 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,21 +255,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.copied(), 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 +400,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() {
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);
@ -458,10 +474,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);
@ -489,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,
));
}
},
}
}
}
@ -587,7 +653,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.copied(), *by) {
agent_data.chat_npc_if_allowed_to_speak(
"npc-speech-merchant_busy",
agent,
@ -610,12 +676,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 +703,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,

View File

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

View File

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