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        compass::{Direction, Distance},
7        dialogue::Subject,
8        inventory::item::{ItemTag, MaterialStatManifest},
9        invite::{InviteKind, InviteResponse},
10        tool::AbilityMap,
11    },
12    event::{ChatEvent, EmitExt, ProcessTradeActionEvent},
13    rtsim::{Actor, NpcInput, PersonalityTrait},
14    trade::{TradeAction, TradePhase, TradeResult},
15};
16use rand::{Rng, thread_rng};
17
18use crate::sys::agent::util::get_entity_by_id;
19
20use super::{BehaviorData, BehaviorTree};
21
22enum ActionStateInteractionTimers {
23    TimerInteraction = 0,
24}
25
26/// Interact if incoming messages
27pub fn process_inbox_sound_and_hurt(bdata: &mut BehaviorData) -> bool {
28    if !bdata.agent.inbox.is_empty() {
29        if matches!(
30            bdata.agent.inbox.front(),
31            Some(AgentEvent::ServerSound(_)) | Some(AgentEvent::Hurt)
32        ) {
33            let sound = bdata.agent.inbox.pop_front();
34            match sound {
35                Some(AgentEvent::ServerSound(sound)) => {
36                    bdata.agent.sounds_heard.push(sound);
37                },
38                Some(AgentEvent::Hurt) => {
39                    // Hurt utterances at random upon receiving damage
40                    if bdata.rng.gen::<f32>() < 0.4 {
41                        bdata.controller.push_utterance(UtteranceKind::Hurt);
42                    }
43                },
44                //Note: this should be unreachable
45                Some(_) | None => {},
46            }
47        } else {
48            bdata.agent.behavior_state.timers
49                [ActionStateInteractionTimers::TimerInteraction as usize] = 0.1;
50        }
51    }
52    false
53}
54
55/// If we receive a new interaction, start the interaction timer
56pub fn process_inbox_interaction(bdata: &mut BehaviorData) -> bool {
57    if BehaviorTree::interaction(bdata.agent).run(bdata) {
58        bdata
59            .agent
60            .timer
61            .start(bdata.read_data.time.0, TimerAction::Interact);
62    }
63    false
64}
65
66/// Increment agent's behavior_state timer
67pub fn increment_timer_deltatime(bdata: &mut BehaviorData) -> bool {
68    bdata.agent.behavior_state.timers[ActionStateInteractionTimers::TimerInteraction as usize] +=
69        bdata.read_data.dt.0;
70    false
71}
72
73pub fn handle_inbox_dialogue(bdata: &mut BehaviorData) -> bool {
74    let BehaviorData {
75        agent, read_data, ..
76    } = bdata;
77
78    if !matches!(agent.inbox.front(), Some(AgentEvent::Dialogue(_, _))) {
79        return false;
80    }
81
82    if let Some(AgentEvent::Dialogue(sender, dialogue)) = agent.inbox.pop_front() {
83        if let Some(rtsim_outbox) = &mut agent.rtsim_outbox
84            && let Some(sender_entity) = read_data.id_maps.uid_entity(sender)
85            && let Some(sender_actor) = read_data
86                .presences
87                .get(sender_entity)
88                .and_then(|p| p.kind.character_id().map(Actor::Character))
89                .or_else(|| Some(Actor::Npc(read_data.rtsim_entities.get(sender_entity)?.0)))
90        {
91            rtsim_outbox.push_back(NpcInput::Dialogue(sender_actor, dialogue));
92            return false;
93        }
94    }
95    true
96}
97
98/// Handles Talk event if the front of the agent's inbox contains one
99pub fn handle_inbox_talk(bdata: &mut BehaviorData) -> bool {
100    let BehaviorData {
101        agent,
102        agent_data,
103        read_data,
104        emitters,
105        controller,
106        ..
107    } = bdata;
108
109    if !matches!(agent.inbox.front(), Some(AgentEvent::Talk(_, _))) {
110        return false;
111    }
112
113    if let Some(AgentEvent::Talk(by, subject)) = agent.inbox.pop_front() {
114        let by_entity = get_entity_by_id(by, read_data);
115
116        if let Some(rtsim_outbox) = &mut agent.rtsim_outbox {
117            if let Subject::Regular | Subject::Mood | Subject::Work = subject
118                && let Some(by_entity) = by_entity
119                && let Some(actor) = read_data
120                    .presences
121                    .get(by_entity)
122                    .and_then(|p| p.kind.character_id().map(Actor::Character))
123                    .or_else(|| Some(Actor::Npc(read_data.rtsim_entities.get(by_entity)?.0)))
124            {
125                rtsim_outbox.push_back(NpcInput::Interaction(actor, subject));
126                return false;
127            }
128        }
129
130        if agent.allowed_to_speak() {
131            if let Some(target) = by_entity {
132                let target_pos = read_data.positions.get(target).map(|pos| pos.0);
133
134                agent.target = Some(Target::new(
135                    target,
136                    false,
137                    read_data.time.0,
138                    false,
139                    target_pos,
140                ));
141                // We're always aware of someone we're talking to
142                agent.awareness.set_maximally_aware();
143
144                controller.push_action(ControlAction::Stand);
145                controller.push_action(ControlAction::Talk);
146                controller.push_utterance(UtteranceKind::Greeting);
147
148                match subject {
149                    Subject::Regular => {
150                        if let Some(tgt_stats) = read_data.stats.get(target) {
151                            if let Some(destination_name) = &agent.rtsim_controller.heading_to {
152                                let personality = &agent.rtsim_controller.personality;
153                                let standard_response_msg = || -> String {
154                                    if personality.will_ambush() {
155                                        format!(
156                                            "I'm heading to {}! Want to come along? We'll make \
157                                             great travel buddies, hehe.",
158                                            destination_name
159                                        )
160                                    } else if personality.is(PersonalityTrait::Extroverted) {
161                                        format!(
162                                            "I'm heading to {}! Want to come along?",
163                                            destination_name
164                                        )
165                                    } else if personality.is(PersonalityTrait::Disagreeable) {
166                                        "Hrm.".to_string()
167                                    } else {
168                                        "Hello!".to_string()
169                                    }
170                                };
171                                let msg = if false
172                                /* TODO: Remembers character */
173                                {
174                                    if personality.will_ambush() {
175                                        "Just follow me a bit more, hehe.".to_string()
176                                    } else if personality.is(PersonalityTrait::Extroverted) {
177                                        if personality.is(PersonalityTrait::Extroverted) {
178                                            format!(
179                                                "Greetings fair {}! It has been far too long \
180                                                 since last I saw you. I'm going to {} right now.",
181                                                &tgt_stats.name, destination_name
182                                            )
183                                        } else if personality.is(PersonalityTrait::Disagreeable) {
184                                            "Oh. It's you again.".to_string()
185                                        } else {
186                                            format!(
187                                                "Hi again {}! Unfortunately I'm in a hurry right \
188                                                 now. See you!",
189                                                &tgt_stats.name
190                                            )
191                                        }
192                                    } else {
193                                        standard_response_msg()
194                                    }
195                                } else {
196                                    standard_response_msg()
197                                };
198                                // TODO: Localise
199                                agent_data.chat_npc(Content::Plain(msg), emitters);
200                            } else {
201                                let mut rng = thread_rng();
202                                agent_data.chat_npc(
203                                    agent
204                                        .rtsim_controller
205                                        .personality
206                                        .get_generic_comment(&mut rng),
207                                    emitters,
208                                );
209                            }
210                        }
211                    },
212                    Subject::Trade => {
213                        if agent.behavior.can_trade(agent_data.alignment.copied(), by) {
214                            if !agent.behavior.is(BehaviorState::TRADING) {
215                                controller.push_initiate_invite(by, InviteKind::Trade);
216                                agent_data.chat_npc_if_allowed_to_speak(
217                                    Content::localized("npc-speech-merchant_advertisement"),
218                                    agent,
219                                    emitters,
220                                );
221                            } else {
222                                agent_data.chat_npc_if_allowed_to_speak(
223                                    Content::localized("npc-speech-merchant_busy"),
224                                    agent,
225                                    emitters,
226                                );
227                            }
228                        } else {
229                            // TODO: maybe make some travellers willing to trade with
230                            // simpler goods like potions
231                            agent_data.chat_npc_if_allowed_to_speak(
232                                Content::localized("npc-speech-villager_decline_trade"),
233                                agent,
234                                emitters,
235                            );
236                        }
237                    },
238                    Subject::Mood => {
239                        // TODO: Reimplement in rtsim2
240                    },
241                    Subject::Location(location) => {
242                        if let Some(tgt_pos) = read_data.positions.get(target) {
243                            let raw_dir = location.origin.as_::<f32>() - tgt_pos.0.xy();
244                            let dist = Distance::from_dir(raw_dir).name();
245                            let dir = Direction::from_dir(raw_dir).name();
246
247                            // TODO: Localise
248                            let msg = format!(
249                                "{} ? I think it's {} {} from here!",
250                                location.name, dist, dir
251                            );
252                            agent_data.chat_npc(Content::Plain(msg), emitters);
253                        }
254                    },
255                    Subject::Person(person) => {
256                        if let Some(src_pos) = read_data.positions.get(target) {
257                            // TODO: Localise
258                            let msg = if let Some(person_pos) = person.origin {
259                                let distance =
260                                    Distance::from_dir(person_pos.xy().as_() - src_pos.0.xy());
261                                match distance {
262                                    Distance::NextTo | Distance::Near => {
263                                        format!(
264                                            "{} ? I think he's {} {} from here!",
265                                            person.name(),
266                                            distance.name(),
267                                            Direction::from_dir(
268                                                person_pos.xy().as_() - src_pos.0.xy()
269                                            )
270                                            .name()
271                                        )
272                                    },
273                                    _ => {
274                                        format!(
275                                            "{} ? I think he's gone visiting another town. Come \
276                                             back later!",
277                                            person.name()
278                                        )
279                                    },
280                                }
281                            } else {
282                                format!(
283                                    "{} ? Sorry, I don't know where you can find him.",
284                                    person.name()
285                                )
286                            };
287                            agent_data.chat_npc(Content::Plain(msg), emitters);
288                        }
289                    },
290                    Subject::Work => {},
291                }
292            }
293        }
294    }
295    true
296}
297
298/// Handles TradeInvite event if the front of the agent's inbox contains one
299pub fn handle_inbox_trade_invite(bdata: &mut BehaviorData) -> bool {
300    let BehaviorData {
301        agent,
302        agent_data,
303        read_data,
304        emitters,
305        controller,
306        ..
307    } = bdata;
308
309    if !matches!(agent.inbox.front(), Some(AgentEvent::TradeInvite(_))) {
310        return false;
311    }
312
313    if let Some(AgentEvent::TradeInvite(with)) = agent.inbox.pop_front() {
314        if agent
315            .behavior
316            .can_trade(agent_data.alignment.copied(), with)
317        {
318            if !agent.behavior.is(BehaviorState::TRADING) {
319                // stand still and looking towards the trading player
320                controller.push_action(ControlAction::Stand);
321                controller.push_action(ControlAction::Talk);
322                if let Some(target) = get_entity_by_id(with, read_data) {
323                    let target_pos = read_data.positions.get(target).map(|pos| pos.0);
324
325                    agent.target = Some(Target::new(
326                        target,
327                        false,
328                        read_data.time.0,
329                        false,
330                        target_pos,
331                    ));
332                }
333                controller.push_invite_response(InviteResponse::Accept);
334                agent.behavior.unset(BehaviorState::TRADING_ISSUER);
335                agent.behavior.set(BehaviorState::TRADING);
336            } else {
337                controller.push_invite_response(InviteResponse::Decline);
338                agent_data.chat_npc_if_allowed_to_speak(
339                    Content::localized("npc-speech-merchant_busy"),
340                    agent,
341                    emitters,
342                );
343            }
344        } else {
345            // TODO: Provide a hint where to find the closest merchant?
346            controller.push_invite_response(InviteResponse::Decline);
347            agent_data.chat_npc_if_allowed_to_speak(
348                Content::localized("npc-speech-villager_decline_trade"),
349                agent,
350                emitters,
351            );
352        }
353    }
354    true
355}
356
357/// Handles TradeAccepted event if the front of the agent's inbox contains one
358pub fn handle_inbox_trade_accepted(bdata: &mut BehaviorData) -> bool {
359    let BehaviorData {
360        agent, read_data, ..
361    } = bdata;
362
363    if !matches!(agent.inbox.front(), Some(AgentEvent::TradeAccepted(_))) {
364        return false;
365    }
366
367    if let Some(AgentEvent::TradeAccepted(with)) = agent.inbox.pop_front() {
368        if !agent.behavior.is(BehaviorState::TRADING) {
369            if let Some(target) = get_entity_by_id(with, read_data) {
370                let target_pos = read_data.positions.get(target).map(|pos| pos.0);
371
372                agent.target = Some(Target::new(
373                    target,
374                    false,
375                    read_data.time.0,
376                    false,
377                    target_pos,
378                ));
379            }
380            agent.behavior.set(BehaviorState::TRADING);
381            agent.behavior.set(BehaviorState::TRADING_ISSUER);
382        }
383    }
384    true
385}
386
387/// Handles TradeFinished event if the front of the agent's inbox contains one
388pub fn handle_inbox_finished_trade(bdata: &mut BehaviorData) -> bool {
389    let BehaviorData {
390        agent,
391        agent_data,
392        emitters,
393        ..
394    } = bdata;
395
396    if !matches!(agent.inbox.front(), Some(AgentEvent::FinishedTrade(_))) {
397        return false;
398    }
399
400    if let Some(AgentEvent::FinishedTrade(result)) = agent.inbox.pop_front() {
401        if agent.behavior.is(BehaviorState::TRADING) {
402            match result {
403                TradeResult::Completed => {
404                    agent_data.chat_npc_if_allowed_to_speak(
405                        Content::localized("npc-speech-merchant_trade_successful"),
406                        agent,
407                        emitters,
408                    );
409                },
410                _ => {
411                    agent_data.chat_npc_if_allowed_to_speak(
412                        Content::localized("npc-speech-merchant_trade_declined"),
413                        agent,
414                        emitters,
415                    );
416                },
417            }
418            agent.behavior.unset(BehaviorState::TRADING);
419            agent.target = None;
420        }
421    }
422    true
423}
424
425/// Handles UpdatePendingTrade event if the front of the agent's inbox contains
426/// one
427pub fn handle_inbox_update_pending_trade(bdata: &mut BehaviorData) -> bool {
428    let BehaviorData {
429        agent,
430        agent_data,
431        read_data,
432        emitters,
433        ..
434    } = bdata;
435
436    if !matches!(agent.inbox.front(), Some(AgentEvent::UpdatePendingTrade(_))) {
437        return false;
438    }
439
440    if let Some(AgentEvent::UpdatePendingTrade(boxval)) = agent.inbox.pop_front() {
441        let (tradeid, pending, prices, inventories) = *boxval;
442        if agent.behavior.is(BehaviorState::TRADING) {
443            let who = usize::from(!agent.behavior.is(BehaviorState::TRADING_ISSUER));
444            let mut message = |content: Content| {
445                if let Some(with) = agent
446                    .target
447                    .as_ref()
448                    .and_then(|tgt_data| read_data.uids.get(tgt_data.target))
449                {
450                    emitters.emit(ChatEvent(UnresolvedChatMsg::npc_tell(
451                        *agent_data.uid,
452                        *with,
453                        content,
454                    )));
455                } else {
456                    emitters.emit(ChatEvent(UnresolvedChatMsg::npc_say(
457                        *agent_data.uid,
458                        content,
459                    )));
460                }
461            };
462            match agent.behavior.trading_behavior {
463                TradingBehavior::RequireBalanced { .. } => {
464                    let balance0 = prices.balance(&pending.offers, &inventories, 1 - who, true);
465                    let balance1 = prices.balance(&pending.offers, &inventories, who, false);
466                    match (balance0, balance1) {
467                        // TODO: Localise
468                        (_, None) => message(Content::Plain(
469                            "I'm not willing to sell that item".to_string(),
470                        )),
471                        // TODO: Localise
472                        (None, _) => message(Content::Plain(
473                            "I'm not willing to buy that item".to_string(),
474                        )),
475                        (Some(balance0), Some(balance1)) => {
476                            if balance0 >= balance1 {
477                                // If the trade is favourable to us, only send an accept message if
478                                // we're not already accepting
479                                // (since otherwise, spam-clicking the accept button
480                                // results in lagging and moving to the review phase of an
481                                // unfavorable trade (although since
482                                // the phase is included in the message, this shouldn't
483                                // result in fully accepting an unfavourable trade))
484                                if !pending.accept_flags[who] && !pending.is_empty_trade() {
485                                    emitters.emit(ProcessTradeActionEvent(
486                                        *agent_data.entity,
487                                        tradeid,
488                                        TradeAction::Accept(pending.phase),
489                                    ));
490                                    tracing::trace!(
491                                        ?tradeid,
492                                        ?balance0,
493                                        ?balance1,
494                                        "Accept Pending Trade"
495                                    );
496                                }
497                            } else {
498                                if balance1 > 0.0 {
499                                    // TODO: Localise
500                                    message(Content::Plain(format!(
501                                        "That only covers {:.0}% of my costs!",
502                                        (balance0 / balance1 * 100.0).floor()
503                                    )));
504                                }
505                                if pending.phase != TradePhase::Mutate {
506                                    // we got into the review phase but without balanced goods,
507                                    // decline
508                                    agent.behavior.unset(BehaviorState::TRADING);
509                                    agent.target = None;
510                                    emitters.emit(ProcessTradeActionEvent(
511                                        *agent_data.entity,
512                                        tradeid,
513                                        TradeAction::Decline,
514                                    ));
515                                }
516                            }
517                        },
518                    }
519                },
520                TradingBehavior::AcceptFood => {
521                    let mut only_food = true;
522                    let ability_map = AbilityMap::load().read();
523                    let msm = MaterialStatManifest::load().read();
524                    if let Some(ri) = &inventories[1 - who] {
525                        for (slot, _) in pending.offers[1 - who].iter() {
526                            if let Some(item) = ri.inventory.get(slot) {
527                                if let Ok(item) = Item::new_from_item_definition_id(
528                                    item.name.as_ref(),
529                                    &ability_map,
530                                    &msm,
531                                ) {
532                                    if !item.tags().contains(&ItemTag::Food) {
533                                        only_food = false;
534                                        break;
535                                    }
536                                }
537                            }
538                        }
539                    }
540                    if !pending.accept_flags[who]
541                        && pending.offers[who].is_empty()
542                        && !pending.offers[1 - who].is_empty()
543                        && only_food
544                    {
545                        emitters.emit(ProcessTradeActionEvent(
546                            *agent_data.entity,
547                            tradeid,
548                            TradeAction::Accept(pending.phase),
549                        ));
550                    }
551                },
552                TradingBehavior::None => {
553                    agent.behavior.unset(BehaviorState::TRADING);
554                    agent.target = None;
555                    emitters.emit(ProcessTradeActionEvent(
556                        *agent_data.entity,
557                        tradeid,
558                        TradeAction::Decline,
559                    ));
560                },
561            }
562        }
563    }
564    true
565}
566
567/// Deny any received interaction:
568/// - `AgentEvent::Talk` and `AgentEvent::TradeAccepted` are cut short by an
569///   "I'm busy" message
570/// - `AgentEvent::TradeInvite` are denied
571/// - `AgentEvent::FinishedTrade` are still handled
572/// - `AgentEvent::UpdatePendingTrade` will immediately close the trade
573pub fn handle_inbox_cancel_interactions(bdata: &mut BehaviorData) -> bool {
574    let BehaviorData {
575        agent,
576        agent_data,
577        emitters,
578        controller,
579        ..
580    } = bdata;
581
582    if let Some(msg) = agent.inbox.front() {
583        match msg {
584            AgentEvent::Talk(by, _)
585            | AgentEvent::TradeAccepted(by)
586            | AgentEvent::Dialogue(by, _) => {
587                if agent
588                    .target
589                    .zip(get_entity_by_id(*by, bdata.read_data))
590                    // In combat, speak to players that aren't the current target.
591                    .is_some_and(|(target, speaker)| !target.hostile || target.target != speaker)
592                {
593                    agent_data.chat_npc_if_allowed_to_speak(
594                        Content::localized("npc-speech-villager_busy"),
595                        agent,
596                        emitters,
597                    );
598                }
599            },
600            AgentEvent::TradeInvite(by) => {
601                controller.push_invite_response(InviteResponse::Decline);
602                if let (Some(target), Some(speaker)) =
603                    (agent.target, get_entity_by_id(*by, bdata.read_data))
604                {
605                    // in combat, speak to players that aren't the current target
606                    if !target.hostile || target.target != speaker {
607                        if agent.behavior.can_trade(agent_data.alignment.copied(), *by) {
608                            agent_data.chat_npc_if_allowed_to_speak(
609                                Content::localized("npc-speech-merchant_busy"),
610                                agent,
611                                emitters,
612                            );
613                        } else {
614                            agent_data.chat_npc_if_allowed_to_speak(
615                                Content::localized("npc-speech-villager_busy"),
616                                agent,
617                                emitters,
618                            );
619                        }
620                    }
621                }
622            },
623            AgentEvent::FinishedTrade(result) => {
624                // copy pasted from recv_interaction
625                // because the trade is not cancellable in this state
626                if agent.behavior.is(BehaviorState::TRADING) {
627                    match result {
628                        TradeResult::Completed => {
629                            agent_data.chat_npc_if_allowed_to_speak(
630                                Content::localized("npc-speech-merchant_trade_successful"),
631                                agent,
632                                emitters,
633                            );
634                        },
635                        _ => {
636                            agent_data.chat_npc_if_allowed_to_speak(
637                                Content::localized("npc-speech-merchant_trade_declined"),
638                                agent,
639                                emitters,
640                            );
641                        },
642                    }
643                    agent.behavior.unset(BehaviorState::TRADING);
644                    agent.target = None;
645                }
646            },
647            AgentEvent::UpdatePendingTrade(boxval) => {
648                // immediately cancel the trade
649                let (tradeid, _pending, _prices, _inventories) = &**boxval;
650                agent.behavior.unset(BehaviorState::TRADING);
651                agent.target = None;
652                emitters.emit(ProcessTradeActionEvent(
653                    *agent_data.entity,
654                    *tradeid,
655                    TradeAction::Decline,
656                ));
657                agent_data.chat_npc_if_allowed_to_speak(
658                    Content::localized("npc-speech-merchant_trade_cancelled_hostile"),
659                    agent,
660                    emitters,
661                );
662            },
663            AgentEvent::ServerSound(_) | AgentEvent::Hurt => return false,
664        };
665
666        agent.inbox.pop_front();
667    }
668    false
669}