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.gen::<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 if 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)?.0)))
88 {
89 rtsim_outbox.push_back(NpcInput::Dialogue(sender_actor, dialogue));
90 return false;
91 }
92 }
93 true
94}
95
96pub fn handle_inbox_talk(bdata: &mut BehaviorData) -> bool {
98 let BehaviorData {
99 agent, read_data, ..
100 } = bdata;
101
102 if !matches!(agent.inbox.front(), Some(AgentEvent::Talk(_))) {
103 return false;
104 }
105
106 if let Some(AgentEvent::Talk(by)) = agent.inbox.pop_front() {
107 let by_entity = get_entity_by_id(by, read_data);
108
109 if let Some(rtsim_outbox) = &mut agent.rtsim_outbox {
110 if let Some(by_entity) = by_entity
111 && let Some(actor) = read_data
112 .presences
113 .get(by_entity)
114 .and_then(|p| p.kind.character_id().map(Actor::Character))
115 .or_else(|| Some(Actor::Npc(read_data.rtsim_entities.get(by_entity)?.0)))
116 {
117 rtsim_outbox.push_back(NpcInput::Interaction(actor));
118 return false;
119 }
120 }
121
122 if agent.allowed_to_speak() {
123 if let Some(target) = by_entity {
124 let target_pos = read_data.positions.get(target).map(|pos| pos.0);
125
126 agent.target = Some(Target::new(
127 target,
128 false,
129 read_data.time.0,
130 false,
131 target_pos,
132 ));
133 agent.awareness.set_maximally_aware();
135 }
136 }
137 }
138 true
139}
140
141pub fn handle_inbox_trade_invite(bdata: &mut BehaviorData) -> bool {
143 let BehaviorData {
144 agent,
145 agent_data,
146 read_data,
147 emitters,
148 controller,
149 ..
150 } = bdata;
151
152 if !matches!(agent.inbox.front(), Some(AgentEvent::TradeInvite(_))) {
153 return false;
154 }
155
156 if let Some(AgentEvent::TradeInvite(with)) = agent.inbox.pop_front() {
157 if agent
158 .behavior
159 .can_trade(agent_data.alignment.copied(), with)
160 {
161 if !agent.behavior.is(BehaviorState::TRADING) {
162 controller.push_action(ControlAction::Stand);
164 controller.push_action(ControlAction::Talk);
165 if let Some(target) = get_entity_by_id(with, read_data) {
166 let target_pos = read_data.positions.get(target).map(|pos| pos.0);
167
168 agent.target = Some(Target::new(
169 target,
170 false,
171 read_data.time.0,
172 false,
173 target_pos,
174 ));
175 }
176 controller.push_invite_response(InviteResponse::Accept);
177 agent.behavior.unset(BehaviorState::TRADING_ISSUER);
178 agent.behavior.set(BehaviorState::TRADING);
179 } else {
180 controller.push_invite_response(InviteResponse::Decline);
181 agent_data.chat_npc_if_allowed_to_speak(
182 Content::localized("npc-speech-merchant_busy_trading"),
183 agent,
184 emitters,
185 );
186 }
187 } else {
188 controller.push_invite_response(InviteResponse::Decline);
190 agent_data.chat_npc_if_allowed_to_speak(
191 Content::localized("npc-speech-villager_decline_trade"),
192 agent,
193 emitters,
194 );
195 }
196 }
197 true
198}
199
200pub fn handle_inbox_trade_accepted(bdata: &mut BehaviorData) -> bool {
202 let BehaviorData {
203 agent, read_data, ..
204 } = bdata;
205
206 if !matches!(agent.inbox.front(), Some(AgentEvent::TradeAccepted(_))) {
207 return false;
208 }
209
210 if let Some(AgentEvent::TradeAccepted(with)) = agent.inbox.pop_front() {
211 if !agent.behavior.is(BehaviorState::TRADING) {
212 if let Some(target) = get_entity_by_id(with, read_data) {
213 let target_pos = read_data.positions.get(target).map(|pos| pos.0);
214
215 agent.target = Some(Target::new(
216 target,
217 false,
218 read_data.time.0,
219 false,
220 target_pos,
221 ));
222 }
223 agent.behavior.set(BehaviorState::TRADING);
224 agent.behavior.set(BehaviorState::TRADING_ISSUER);
225 }
226 }
227 true
228}
229
230pub fn handle_inbox_finished_trade(bdata: &mut BehaviorData) -> bool {
232 let BehaviorData {
233 agent,
234 agent_data,
235 emitters,
236 ..
237 } = bdata;
238
239 if !matches!(agent.inbox.front(), Some(AgentEvent::FinishedTrade(_))) {
240 return false;
241 }
242
243 if let Some(AgentEvent::FinishedTrade(result)) = agent.inbox.pop_front() {
244 if agent.behavior.is(BehaviorState::TRADING) {
245 match result {
246 TradeResult::Completed => {
247 agent_data.chat_npc_if_allowed_to_speak(
248 Content::localized("npc-speech-merchant_trade_successful"),
249 agent,
250 emitters,
251 );
252 },
253 _ => {
254 agent_data.chat_npc_if_allowed_to_speak(
255 Content::localized("npc-speech-merchant_trade_declined"),
256 agent,
257 emitters,
258 );
259 },
260 }
261 agent.behavior.unset(BehaviorState::TRADING);
262 agent.target = None;
263 }
264 }
265 true
266}
267
268pub fn handle_inbox_update_pending_trade(bdata: &mut BehaviorData) -> bool {
271 let BehaviorData {
272 agent,
273 agent_data,
274 read_data,
275 emitters,
276 ..
277 } = bdata;
278
279 if !matches!(agent.inbox.front(), Some(AgentEvent::UpdatePendingTrade(_))) {
280 return false;
281 }
282
283 if let Some(AgentEvent::UpdatePendingTrade(boxval)) = agent.inbox.pop_front() {
284 let (tradeid, pending, prices, inventories) = *boxval;
285 if agent.behavior.is(BehaviorState::TRADING) {
286 let who = usize::from(!agent.behavior.is(BehaviorState::TRADING_ISSUER));
287 let mut message = |content: Content| {
288 if let Some(with) = agent
289 .target
290 .as_ref()
291 .and_then(|tgt_data| read_data.uids.get(tgt_data.target))
292 {
293 emitters.emit(ChatEvent {
294 msg: UnresolvedChatMsg::npc_tell(*agent_data.uid, *with, content),
295 from_client: false,
296 });
297 } else {
298 emitters.emit(ChatEvent {
299 msg: UnresolvedChatMsg::npc_say(*agent_data.uid, content),
300 from_client: false,
301 });
302 }
303 };
304 match agent.behavior.trading_behavior {
305 TradingBehavior::RequireBalanced { .. } => {
306 let balance0 = prices.balance(&pending.offers, &inventories, 1 - who, true);
307 let balance1 = prices.balance(&pending.offers, &inventories, who, false);
308 match (balance0, balance1) {
309 (_, None) => message(Content::Plain(
311 "I'm not willing to sell that item".to_string(),
312 )),
313 (None, _) => message(Content::Plain(
315 "I'm not willing to buy that item".to_string(),
316 )),
317 (Some(balance0), Some(balance1)) => {
318 if balance0 >= balance1 {
319 if !pending.accept_flags[who] && !pending.is_empty_trade() {
327 emitters.emit(ProcessTradeActionEvent(
328 *agent_data.entity,
329 tradeid,
330 TradeAction::Accept(pending.phase),
331 ));
332 tracing::trace!(
333 ?tradeid,
334 ?balance0,
335 ?balance1,
336 "Accept Pending Trade"
337 );
338 }
339 } else {
340 if balance1 > 0.0 {
341 message(Content::Plain(format!(
343 "That only covers {:.0}% of my costs!",
344 (balance0 / balance1 * 100.0).floor()
345 )));
346 }
347 if pending.phase != TradePhase::Mutate {
348 agent.behavior.unset(BehaviorState::TRADING);
351 agent.target = None;
352 emitters.emit(ProcessTradeActionEvent(
353 *agent_data.entity,
354 tradeid,
355 TradeAction::Decline,
356 ));
357 }
358 }
359 },
360 }
361 },
362 TradingBehavior::AcceptFood => {
363 let mut only_food = true;
364 let ability_map = AbilityMap::load().read();
365 let msm = MaterialStatManifest::load().read();
366 if let Some(ri) = &inventories[1 - who] {
367 for (slot, _) in pending.offers[1 - who].iter() {
368 if let Some(item) = ri.inventory.get(slot) {
369 if let Ok(item) = Item::new_from_item_definition_id(
370 item.name.as_ref(),
371 &ability_map,
372 &msm,
373 ) {
374 if !item.tags().contains(&ItemTag::Food) {
375 only_food = false;
376 break;
377 }
378 }
379 }
380 }
381 }
382 if !pending.accept_flags[who]
383 && pending.offers[who].is_empty()
384 && !pending.offers[1 - who].is_empty()
385 && only_food
386 {
387 emitters.emit(ProcessTradeActionEvent(
388 *agent_data.entity,
389 tradeid,
390 TradeAction::Accept(pending.phase),
391 ));
392 }
393 },
394 TradingBehavior::None => {
395 agent.behavior.unset(BehaviorState::TRADING);
396 agent.target = None;
397 emitters.emit(ProcessTradeActionEvent(
398 *agent_data.entity,
399 tradeid,
400 TradeAction::Decline,
401 ));
402 },
403 }
404 }
405 }
406 true
407}
408
409pub fn handle_inbox_cancel_interactions(bdata: &mut BehaviorData) -> bool {
416 let BehaviorData {
417 agent,
418 agent_data,
419 emitters,
420 controller,
421 ..
422 } = bdata;
423
424 if let Some(msg) = agent.inbox.front() {
425 match msg {
426 AgentEvent::Talk(by) | AgentEvent::TradeAccepted(by) | AgentEvent::Dialogue(by, _) => {
427 if agent
428 .target
429 .zip(get_entity_by_id(*by, bdata.read_data))
430 .is_some_and(|(target, speaker)| !target.hostile || target.target != speaker)
432 {
433 agent_data.chat_npc_if_allowed_to_speak(
434 Content::localized("npc-speech-villager_busy"),
435 agent,
436 emitters,
437 );
438 }
439 },
440 AgentEvent::TradeInvite(by) => {
441 controller.push_invite_response(InviteResponse::Decline);
442 if let (Some(target), Some(speaker)) =
443 (agent.target, get_entity_by_id(*by, bdata.read_data))
444 {
445 if !target.hostile || target.target != speaker {
447 if agent.behavior.can_trade(agent_data.alignment.copied(), *by) {
448 agent_data.chat_npc_if_allowed_to_speak(
449 Content::localized("npc-speech-merchant_busy_combat"),
450 agent,
451 emitters,
452 );
453 } else {
454 agent_data.chat_npc_if_allowed_to_speak(
455 Content::localized("npc-speech-villager_busy"),
456 agent,
457 emitters,
458 );
459 }
460 }
461 }
462 },
463 AgentEvent::FinishedTrade(result) => {
464 if agent.behavior.is(BehaviorState::TRADING) {
467 match result {
468 TradeResult::Completed => {
469 agent_data.chat_npc_if_allowed_to_speak(
470 Content::localized("npc-speech-merchant_trade_successful"),
471 agent,
472 emitters,
473 );
474 },
475 _ => {
476 agent_data.chat_npc_if_allowed_to_speak(
477 Content::localized("npc-speech-merchant_trade_declined"),
478 agent,
479 emitters,
480 );
481 },
482 }
483 agent.behavior.unset(BehaviorState::TRADING);
484 agent.target = None;
485 }
486 },
487 AgentEvent::UpdatePendingTrade(boxval) => {
488 let (tradeid, _pending, _prices, _inventories) = &**boxval;
490 agent.behavior.unset(BehaviorState::TRADING);
491 agent.target = None;
492 emitters.emit(ProcessTradeActionEvent(
493 *agent_data.entity,
494 *tradeid,
495 TradeAction::Decline,
496 ));
497 agent_data.chat_npc_if_allowed_to_speak(
498 Content::localized("npc-speech-merchant_trade_cancelled_hostile"),
499 agent,
500 emitters,
501 );
502 },
503 AgentEvent::ServerSound(_) | AgentEvent::Hurt => return false,
504 };
505
506 agent.inbox.pop_front();
507 }
508 false
509}