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
24pub 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 if bdata.rng.random::<f32>() < 0.4 {
39 bdata.controller.push_utterance(UtteranceKind::Hurt);
40 }
41 },
42 Some(_) | None => {},
44 }
45 } else {
46 bdata.agent.behavior_state.timers
47 [ActionStateInteractionTimers::TimerInteraction as usize] = 0.1;
48 }
49 }
50 false
51}
52
53pub 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
64pub 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
95pub 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 agent.awareness.set_maximally_aware();
134 }
135 }
136 true
137}
138
139pub 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 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 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
198pub 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
228pub 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
266pub 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 !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 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
406pub 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 .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 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 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 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}