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
26pub 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 if bdata.rng.gen::<f32>() < 0.4 {
41 bdata.controller.push_utterance(UtteranceKind::Hurt);
42 }
43 },
44 Some(_) | None => {},
46 }
47 } else {
48 bdata.agent.behavior_state.timers
49 [ActionStateInteractionTimers::TimerInteraction as usize] = 0.1;
50 }
51 }
52 false
53}
54
55pub 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
66pub 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
98pub 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 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 {
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 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 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 },
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 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 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
298pub 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 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 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
357pub 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
387pub 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
425pub 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 (_, None) => message(Content::Plain(
469 "I'm not willing to sell that item".to_string(),
470 )),
471 (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 !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 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 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
567pub 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 .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 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 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 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}