veloren_server/sys/agent/behavior_tree/
interaction.rs

1use common::{
2    comp::{
3        BehaviorState, Content, ControlAction, Item, TradingBehavior, UnresolvedChatMsg,
4        UtteranceKind,
5        agent::{AgentEvent, Target, TimerAction},
6        inventory::item::{ItemTag, MaterialStatManifest},
7        invite::InviteResponse,
8        tool::AbilityMap,
9    },
10    event::{ChatEvent, EmitExt, ProcessTradeActionEvent},
11    rtsim::{Actor, NpcInput},
12    trade::{TradeAction, TradePhase, TradeResult},
13};
14use rand::Rng;
15
16use crate::sys::agent::util::get_entity_by_id;
17
18use super::{BehaviorData, BehaviorTree};
19
20enum ActionStateInteractionTimers {
21    TimerInteraction = 0,
22}
23
24/// Interact if incoming messages
25pub fn process_inbox_sound_and_hurt(bdata: &mut BehaviorData) -> bool {
26    if !bdata.agent.inbox.is_empty() {
27        if matches!(
28            bdata.agent.inbox.front(),
29            Some(AgentEvent::ServerSound(_)) | Some(AgentEvent::Hurt)
30        ) {
31            let sound = bdata.agent.inbox.pop_front();
32            match sound {
33                Some(AgentEvent::ServerSound(sound)) => {
34                    bdata.agent.sounds_heard.push(sound);
35                },
36                Some(AgentEvent::Hurt) => {
37                    // Hurt utterances at random upon receiving damage
38                    if bdata.rng.random::<f32>() < 0.4 {
39                        bdata.controller.push_utterance(UtteranceKind::Hurt);
40                    }
41                },
42                //Note: this should be unreachable
43                Some(_) | None => {},
44            }
45        } else {
46            bdata.agent.behavior_state.timers
47                [ActionStateInteractionTimers::TimerInteraction as usize] = 0.1;
48        }
49    }
50    false
51}
52
53/// If we receive a new interaction, start the interaction timer
54pub fn process_inbox_interaction(bdata: &mut BehaviorData) -> bool {
55    if BehaviorTree::interaction(bdata.agent).run(bdata) {
56        bdata
57            .agent
58            .timer
59            .start(bdata.read_data.time.0, TimerAction::Interact);
60    }
61    false
62}
63
64/// Increment agent's behavior_state timer
65pub fn increment_timer_deltatime(bdata: &mut BehaviorData) -> bool {
66    bdata.agent.behavior_state.timers[ActionStateInteractionTimers::TimerInteraction as usize] +=
67        bdata.read_data.dt.0;
68    false
69}
70
71pub fn handle_inbox_dialogue(bdata: &mut BehaviorData) -> bool {
72    let BehaviorData {
73        agent, read_data, ..
74    } = bdata;
75
76    if !matches!(agent.inbox.front(), Some(AgentEvent::Dialogue(_, _))) {
77        return false;
78    }
79
80    if let Some(AgentEvent::Dialogue(sender, dialogue)) = agent.inbox.pop_front()
81        && let Some(rtsim_outbox) = &mut agent.rtsim_outbox
82        && let Some(sender_entity) = read_data.id_maps.uid_entity(sender)
83        && let Some(sender_actor) = read_data
84            .presences
85            .get(sender_entity)
86            .and_then(|p| p.kind.character_id().map(Actor::Character))
87            .or_else(|| Some(Actor::Npc(*read_data.rtsim_entities.get(sender_entity)?)))
88    {
89        rtsim_outbox.push_back(NpcInput::Dialogue(sender_actor, dialogue));
90        return false;
91    }
92    true
93}
94
95/// Handles Talk event if the front of the agent's inbox contains one
96pub fn handle_inbox_talk(bdata: &mut BehaviorData) -> bool {
97    let BehaviorData {
98        agent, read_data, ..
99    } = bdata;
100
101    if !matches!(agent.inbox.front(), Some(AgentEvent::Talk(_))) {
102        return false;
103    }
104
105    if let Some(AgentEvent::Talk(by)) = agent.inbox.pop_front() {
106        let by_entity = get_entity_by_id(by, read_data);
107
108        if let Some(rtsim_outbox) = &mut agent.rtsim_outbox
109            && let Some(by_entity) = by_entity
110            && let Some(actor) = read_data
111                .presences
112                .get(by_entity)
113                .and_then(|p| p.kind.character_id().map(Actor::Character))
114                .or_else(|| Some(Actor::Npc(*read_data.rtsim_entities.get(by_entity)?)))
115        {
116            rtsim_outbox.push_back(NpcInput::Interaction(actor));
117            return false;
118        }
119
120        if agent.allowed_to_speak()
121            && let Some(target) = by_entity
122        {
123            let target_pos = read_data.positions.get(target).map(|pos| pos.0);
124
125            agent.target = Some(Target::new(
126                target,
127                false,
128                read_data.time.0,
129                false,
130                target_pos,
131            ));
132            // We're always aware of someone we're talking to
133            agent.awareness.set_maximally_aware();
134        }
135    }
136    true
137}
138
139/// Handles TradeInvite event if the front of the agent's inbox contains one
140pub fn handle_inbox_trade_invite(bdata: &mut BehaviorData) -> bool {
141    let BehaviorData {
142        agent,
143        agent_data,
144        read_data,
145        emitters,
146        controller,
147        ..
148    } = bdata;
149
150    if !matches!(agent.inbox.front(), Some(AgentEvent::TradeInvite(_))) {
151        return false;
152    }
153
154    if let Some(AgentEvent::TradeInvite(with)) = agent.inbox.pop_front() {
155        if agent
156            .behavior
157            .can_trade(agent_data.alignment.copied(), with)
158        {
159            if !agent.behavior.is(BehaviorState::TRADING) {
160                // stand still and looking towards the trading player
161                controller.push_action(ControlAction::Stand);
162                controller.push_action(ControlAction::Talk(Some(with)));
163                if let Some(target) = get_entity_by_id(with, read_data) {
164                    let target_pos = read_data.positions.get(target).map(|pos| pos.0);
165
166                    agent.target = Some(Target::new(
167                        target,
168                        false,
169                        read_data.time.0,
170                        false,
171                        target_pos,
172                    ));
173                }
174                controller.push_invite_response(InviteResponse::Accept);
175                agent.behavior.unset(BehaviorState::TRADING_ISSUER);
176                agent.behavior.set(BehaviorState::TRADING);
177            } else {
178                controller.push_invite_response(InviteResponse::Decline);
179                agent_data.chat_npc_if_allowed_to_speak(
180                    Content::localized("npc-speech-merchant_busy_trading"),
181                    agent,
182                    emitters,
183                );
184            }
185        } else {
186            // TODO: Provide a hint where to find the closest merchant?
187            controller.push_invite_response(InviteResponse::Decline);
188            agent_data.chat_npc_if_allowed_to_speak(
189                Content::localized("npc-speech-villager_decline_trade"),
190                agent,
191                emitters,
192            );
193        }
194    }
195    true
196}
197
198/// Handles TradeAccepted event if the front of the agent's inbox contains one
199pub fn handle_inbox_trade_accepted(bdata: &mut BehaviorData) -> bool {
200    let BehaviorData {
201        agent, read_data, ..
202    } = bdata;
203
204    if !matches!(agent.inbox.front(), Some(AgentEvent::TradeAccepted(_))) {
205        return false;
206    }
207
208    if let Some(AgentEvent::TradeAccepted(with)) = agent.inbox.pop_front()
209        && !agent.behavior.is(BehaviorState::TRADING)
210    {
211        if let Some(target) = get_entity_by_id(with, read_data) {
212            let target_pos = read_data.positions.get(target).map(|pos| pos.0);
213
214            agent.target = Some(Target::new(
215                target,
216                false,
217                read_data.time.0,
218                false,
219                target_pos,
220            ));
221        }
222        agent.behavior.set(BehaviorState::TRADING);
223        agent.behavior.set(BehaviorState::TRADING_ISSUER);
224    }
225    true
226}
227
228/// Handles TradeFinished event if the front of the agent's inbox contains one
229pub fn handle_inbox_finished_trade(bdata: &mut BehaviorData) -> bool {
230    let BehaviorData {
231        agent,
232        agent_data,
233        emitters,
234        ..
235    } = bdata;
236
237    if !matches!(agent.inbox.front(), Some(AgentEvent::FinishedTrade(_))) {
238        return false;
239    }
240
241    if let Some(AgentEvent::FinishedTrade(result)) = agent.inbox.pop_front()
242        && agent.behavior.is(BehaviorState::TRADING)
243    {
244        match result {
245            TradeResult::Completed => {
246                agent_data.chat_npc_if_allowed_to_speak(
247                    Content::localized("npc-speech-merchant_trade_successful"),
248                    agent,
249                    emitters,
250                );
251            },
252            _ => {
253                agent_data.chat_npc_if_allowed_to_speak(
254                    Content::localized("npc-speech-merchant_trade_declined"),
255                    agent,
256                    emitters,
257                );
258            },
259        }
260        agent.behavior.unset(BehaviorState::TRADING);
261        agent.target = None;
262    }
263    true
264}
265
266/// Handles UpdatePendingTrade event if the front of the agent's inbox contains
267/// one
268pub fn handle_inbox_update_pending_trade(bdata: &mut BehaviorData) -> bool {
269    let BehaviorData {
270        agent,
271        agent_data,
272        read_data,
273        emitters,
274        ..
275    } = bdata;
276
277    if !matches!(agent.inbox.front(), Some(AgentEvent::UpdatePendingTrade(_))) {
278        return false;
279    }
280
281    if let Some(AgentEvent::UpdatePendingTrade(boxval)) = agent.inbox.pop_front() {
282        let (tradeid, pending, prices, inventories) = *boxval;
283        if agent.behavior.is(BehaviorState::TRADING) {
284            let who = usize::from(!agent.behavior.is(BehaviorState::TRADING_ISSUER));
285            let mut message = |content: Content| {
286                if let Some(with) = agent
287                    .target
288                    .as_ref()
289                    .and_then(|tgt_data| read_data.uids.get(tgt_data.target))
290                {
291                    emitters.emit(ChatEvent {
292                        msg: UnresolvedChatMsg::npc_tell(*agent_data.uid, *with, content),
293                        from_client: false,
294                    });
295                } else {
296                    emitters.emit(ChatEvent {
297                        msg: UnresolvedChatMsg::npc_say(*agent_data.uid, content),
298                        from_client: false,
299                    });
300                }
301            };
302            match agent.behavior.trading_behavior {
303                TradingBehavior::RequireBalanced { .. } => {
304                    let balance0 = prices.balance(&pending.offers, &inventories, 1 - who, true);
305                    let balance1 = prices.balance(&pending.offers, &inventories, who, false);
306                    match (balance0, balance1) {
307                        (_, None) => {
308                            message(Content::localized("npc-speech-merchant_reject_sell_item"))
309                        },
310                        (None, _) => {
311                            message(Content::localized("npc-speech-merchant_reject_buy_item"))
312                        },
313                        (Some(balance0), Some(balance1)) => {
314                            if balance0 >= balance1 {
315                                // If the trade is favourable to us, only send an accept message if
316                                // we're not already accepting
317                                // (since otherwise, spam-clicking the accept button
318                                // results in lagging and moving to the review phase of an
319                                // unfavorable trade (although since
320                                // the phase is included in the message, this shouldn't
321                                // result in fully accepting an unfavourable trade))
322                                if !pending.accept_flags[who] && !pending.is_empty_trade() {
323                                    emitters.emit(ProcessTradeActionEvent(
324                                        *agent_data.entity,
325                                        tradeid,
326                                        TradeAction::Accept(pending.phase),
327                                    ));
328                                    tracing::trace!(
329                                        ?tradeid,
330                                        ?balance0,
331                                        ?balance1,
332                                        "Accept Pending Trade"
333                                    );
334                                }
335                            } else {
336                                if balance1 > 0.0 {
337                                    message(Content::localized_with_args(
338                                        "npc-speech-merchant_trade_balance",
339                                        [(
340                                            "percentage",
341                                            format!("{:.0}", (balance0 / balance1 * 100.0).floor()),
342                                        )],
343                                    ));
344                                }
345                                if pending.phase != TradePhase::Mutate {
346                                    // we got into the review phase but without balanced goods,
347                                    // decline
348                                    agent.behavior.unset(BehaviorState::TRADING);
349                                    agent.target = None;
350                                    emitters.emit(ProcessTradeActionEvent(
351                                        *agent_data.entity,
352                                        tradeid,
353                                        TradeAction::Decline,
354                                    ));
355                                }
356                            }
357                        },
358                    }
359                },
360                TradingBehavior::AcceptFood => {
361                    let mut only_food = true;
362                    let ability_map = AbilityMap::load().read();
363                    let msm = MaterialStatManifest::load().read();
364                    if let Some(ri) = &inventories[1 - who] {
365                        for (slot, _) in pending.offers[1 - who].iter() {
366                            if let Some(item) = ri.inventory.get(slot)
367                                && let Ok(item) = Item::new_from_item_definition_id(
368                                    item.name.as_ref(),
369                                    &ability_map,
370                                    &msm,
371                                )
372                                && !item.tags().contains(&ItemTag::Food)
373                            {
374                                only_food = false;
375                                break;
376                            }
377                        }
378                    }
379                    if !pending.accept_flags[who]
380                        && pending.offers[who].is_empty()
381                        && !pending.offers[1 - who].is_empty()
382                        && only_food
383                    {
384                        emitters.emit(ProcessTradeActionEvent(
385                            *agent_data.entity,
386                            tradeid,
387                            TradeAction::Accept(pending.phase),
388                        ));
389                    }
390                },
391                TradingBehavior::None => {
392                    agent.behavior.unset(BehaviorState::TRADING);
393                    agent.target = None;
394                    emitters.emit(ProcessTradeActionEvent(
395                        *agent_data.entity,
396                        tradeid,
397                        TradeAction::Decline,
398                    ));
399                },
400            }
401        }
402    }
403    true
404}
405
406/// Deny any received interaction:
407/// - `AgentEvent::Talk` and `AgentEvent::TradeAccepted` are cut short by an
408///   "I'm busy" message
409/// - `AgentEvent::TradeInvite` are denied
410/// - `AgentEvent::FinishedTrade` are still handled
411/// - `AgentEvent::UpdatePendingTrade` will immediately close the trade
412pub fn handle_inbox_cancel_interactions(bdata: &mut BehaviorData) -> bool {
413    let BehaviorData {
414        agent,
415        agent_data,
416        emitters,
417        controller,
418        ..
419    } = bdata;
420
421    if let Some(msg) = agent.inbox.front() {
422        match msg {
423            AgentEvent::Talk(by) | AgentEvent::TradeAccepted(by) | AgentEvent::Dialogue(by, _) => {
424                if agent
425                    .target
426                    .zip(get_entity_by_id(*by, bdata.read_data))
427                    // In combat, speak to players that aren't the current target.
428                    .is_some_and(|(target, speaker)| !target.hostile || target.target != speaker)
429                {
430                    agent_data.chat_npc_if_allowed_to_speak(
431                        Content::localized("npc-speech-villager_busy"),
432                        agent,
433                        emitters,
434                    );
435                }
436            },
437            AgentEvent::TradeInvite(by) => {
438                controller.push_invite_response(InviteResponse::Decline);
439                if let (Some(target), Some(speaker)) =
440                    (agent.target, get_entity_by_id(*by, bdata.read_data))
441                {
442                    // in combat, speak to players that aren't the current target
443                    if !target.hostile || target.target != speaker {
444                        if agent.behavior.can_trade(agent_data.alignment.copied(), *by) {
445                            agent_data.chat_npc_if_allowed_to_speak(
446                                Content::localized("npc-speech-merchant_busy_combat"),
447                                agent,
448                                emitters,
449                            );
450                        } else {
451                            agent_data.chat_npc_if_allowed_to_speak(
452                                Content::localized("npc-speech-villager_busy"),
453                                agent,
454                                emitters,
455                            );
456                        }
457                    }
458                }
459            },
460            AgentEvent::FinishedTrade(result) => {
461                // copy pasted from recv_interaction
462                // because the trade is not cancellable in this state
463                if agent.behavior.is(BehaviorState::TRADING) {
464                    match result {
465                        TradeResult::Completed => {
466                            agent_data.chat_npc_if_allowed_to_speak(
467                                Content::localized("npc-speech-merchant_trade_successful"),
468                                agent,
469                                emitters,
470                            );
471                        },
472                        _ => {
473                            agent_data.chat_npc_if_allowed_to_speak(
474                                Content::localized("npc-speech-merchant_trade_declined"),
475                                agent,
476                                emitters,
477                            );
478                        },
479                    }
480                    agent.behavior.unset(BehaviorState::TRADING);
481                    agent.target = None;
482                }
483            },
484            AgentEvent::UpdatePendingTrade(boxval) => {
485                // immediately cancel the trade
486                let (tradeid, _pending, _prices, _inventories) = &**boxval;
487                agent.behavior.unset(BehaviorState::TRADING);
488                agent.target = None;
489                emitters.emit(ProcessTradeActionEvent(
490                    *agent_data.entity,
491                    *tradeid,
492                    TradeAction::Decline,
493                ));
494                agent_data.chat_npc_if_allowed_to_speak(
495                    Content::localized("npc-speech-merchant_trade_cancelled_hostile"),
496                    agent,
497                    emitters,
498                );
499            },
500            AgentEvent::ServerSound(_) | AgentEvent::Hurt => return false,
501        };
502
503        agent.inbox.pop_front();
504    }
505    false
506}