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
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_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
91pub 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 agent.awareness.set_maximally_aware();
126 }
127 }
128 true
129}
130
131pub 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 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 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
190pub 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
220pub 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
258pub 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 !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 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
394pub 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 .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 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 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 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}