Skip to main content

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