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