1use super::*;
2use crate::data::quest::{
3 COURIER_QUEST_VARIANTS, CourierQuest, CourierQuestInstance, Payload, Recipient,
4};
5use common::{
6 comp::{Item, item::ItemBase},
7 rtsim::ActorId,
8 spot::Spot,
9};
10use std::num::NonZeroU32;
11
12pub fn create_deposit<S: State, T: Action<S, bool>>(
17 ctx: &mut NpcCtx,
18 item: ItemResource,
19 amount: f32,
20 then: T,
21) -> Option<impl Action<S, bool> + use<S, T>> {
22 if let Some(npc_entity) = ctx.system_data.id_maps.rtsim_entity(ctx.actor_id)
23 && ctx
24 .system_data
25 .inventories
26 .lock()
27 .unwrap()
28 .get(npc_entity)
29 .is_some_and(|inv| {
30 inv.item_count(&item.to_equivalent_item_def()) >= amount.ceil() as u64
31 })
32 {
33 Some(then.and_then(move |should_proceed: bool| {
34 just(move |ctx, _| {
35 if !should_proceed {
36 false
37 } else if let Some(npc_entity) = ctx.system_data.id_maps.rtsim_entity(ctx.actor_id)
38 && ctx
39 .system_data
40 .inventories
41 .lock()
42 .unwrap()
43 .get_mut(npc_entity)
44 .and_then(|mut inv| {
45 inv.remove_item_amount(
46 &item.to_equivalent_item_def(),
47 amount.ceil() as u32,
48 &ctx.system_data.ability_map,
49 &ctx.system_data.msm,
50 )
51 })
52 .is_some()
53 {
54 true
55 } else {
56 false
57 }
58 })
59 }))
60 } else {
61 None
62 }
63}
64
65#[allow(clippy::result_unit_err)]
66pub fn resolve_take_deposit(
67 ctx: &mut NpcCtx,
68 quest_id: QuestId,
69 success: bool,
70) -> Result<Option<(Arc<ItemDef>, u32)>, ()> {
71 if let Some(outcome) = ctx
72 .data
73 .quests
74 .get(quest_id)
75 .and_then(|q| q.resolve(ctx.actor_id, success))
76 {
77 if let Some((item, amount)) = &outcome.deposit
79 && let Some(npc_entity) = ctx.system_data.id_maps.rtsim_entity(ctx.actor_id)
80 && let Some(mut inv) = ctx
81 .system_data
82 .inventories
83 .lock()
84 .unwrap()
85 .get_mut(npc_entity)
86 {
87 let item_def = item.to_equivalent_item_def();
88 let amount = amount.floor() as u32;
90
91 let mut item = Item::new_from_item_base(
92 ItemBase::Simple(item_def.clone()),
93 Vec::new(),
94 &ctx.system_data.ability_map,
95 &ctx.system_data.msm,
96 );
97 item.set_amount(amount)
98 .expect("Item cannot be stacked that far!");
99 let _ = inv.push(item);
100
101 Ok(Some((item_def, amount)))
102 } else {
103 Ok(None)
104 }
105 } else {
106 Err(())
107 }
108}
109
110pub fn finalize_courier_task(ctx: &mut NpcCtx, quest_id: QuestId, read_only: bool) -> bool {
123 fn required_count(raw: f32) -> u32 {
124 debug_assert!(
125 raw.is_finite() && raw >= 0.0,
126 "courier quest required amount must be finite and non-negative, got {raw}",
127 );
128 raw.round() as u32
129 }
130
131 if let Some(quest) = ctx.data.quests.get(quest_id)
132 && let QuestKind::Courier { instance } = &quest.kind
133 && let Some(entity) = ctx.system_data.id_maps.rtsim_entity(instance.messenger)
134 && let Ok(mut inventories) = ctx.system_data.inventories.lock()
135 && let Some(mut inv) = inventories.get_mut(entity)
136 && let Ok(required_items) = instance.get_required_items()
137 && required_items.iter().all(|(item_def, amount)| {
138 inv.item_count(item_def) >= u64::from(required_count(*amount))
139 })
140 && (read_only
141 || required_items.iter().all(|(item_def, amount)| {
142 inv.remove_item_amount(
143 item_def,
144 required_count(*amount),
145 &ctx.system_data.ability_map,
146 &ctx.system_data.msm,
147 )
148 .is_some()
149 }))
150 {
151 true
152 } else {
153 false
154 }
155}
156
157pub fn create_quest<S: State>(quest: Quest) -> impl Action<S, QuestId> {
162 just(move |ctx, _| {
163 let quest_id = ctx.data.quests.register();
164 ctx.controller
165 .quests_to_create
166 .push((quest_id, quest.clone()));
167 quest_id
168 })
169}
170
171pub fn quest_request<S: State>(session: DialogueSession) -> impl Action<S> {
172 now(move |ctx, _| {
173 let mut quests = Vec::new();
174
175 const ESCORT_REWARD_ITEM: ItemResource = ItemResource::Coin;
177 if ctx.npc.job.is_none()
179 && matches!(ctx.actor.profession(), Some(Profession::Merchant))
181 && let Some((dst_site_id, dst_site, dist)) = ctx.data
183 .sites
184 .iter()
185 .map(|(site_id, site)| (site_id, site, site.wpos.as_().distance(ctx.actor.wpos.xy())))
187 .filter(|(site_id, _, dist)| Some(*site_id) != ctx.actor.current_site && (1000.0..5_000.0).contains(dist))
189 .choose(&mut ChaChaRng::from_seed([(ctx.time.0 / (60.0 * 15.0)) as u8; 32]))
192 && let escort_reward_amount = dist / 5.0
194 && let Some(dst_site_name) = util::site_name(ctx, dst_site_id)
195 && let time_limit = 1.0 + dist as f64 / 80.0
196 && let Some(accept_quest) = create_deposit(ctx, ESCORT_REWARD_ITEM, escort_reward_amount, session
197 .ask_yes_no_question(Content::localized("npc-response-quest-escort-ask")
198 .with_arg("dst", dst_site_name.clone())
199 .with_arg("coins", escort_reward_amount as u64)
200 .with_arg("mins", time_limit as u64)))
201 {
202 let dst_wpos = dst_site.wpos.as_();
203 quests.push(
204 accept_quest
205 .and_then(move |yes| {
206 now(move |ctx, _| {
207 if yes {
208 let quest =
209 Quest::escort(ctx.actor_id, session.target, dst_site_id)
210 .with_deposit(ESCORT_REWARD_ITEM, escort_reward_amount)
211 .with_timeout(ctx.time.add_minutes(time_limit));
212 create_quest(quest.clone())
213 .and_then(move |quest_id| {
214 now(move |ctx, _| {
215 ctx.controller.job = Some(Job::Quest(quest_id));
216 session.give_marker(
217 Marker::at(dst_wpos)
218 .with_id(quest_id)
219 .with_label(
220 Content::localized("hud-map-escort-label")
221 .with_arg(
222 "name",
223 ctx.actor
224 .get_name()
225 .unwrap_or_else(|| {
226 "<unknown>".to_string()
227 }),
228 )
229 .with_arg(
230 "place",
231 dst_site_name.clone(),
232 ),
233 )
234 .with_quest_flag(true),
235 )
236 })
237 })
238 .then(session.say_statement(Content::localized(
239 "npc-response-quest-escort-start",
240 )))
241 .boxed()
242 } else {
243 session
244 .say_statement(Content::localized(
245 "npc-response-quest-rejected",
246 ))
247 .boxed()
248 }
249 })
250 })
251 .boxed(),
252 );
253 }
254
255 const SLAY_REWARD_ITEM: ItemResource = ItemResource::Coin;
257 if let Some((monster_id, monster)) = ctx.data.actors
258 .iter()
259 .filter(|(_, npc)| matches!(&npc.role, Role::Monster))
261 .filter(|(id, _)| ctx.data.quests.related_to(*id).count() == 0)
263 .filter(|(_, npc)| npc.wpos.xy().distance(ctx.actor.wpos.xy()) < 2500.0)
265 .min_by_key(|(_, npc)| npc.wpos.xy().distance_squared(ctx.actor.wpos.xy()) as i64)
267 && let monster_pos = monster.wpos
268 && let monster_body = monster.body
269 && let slay_reward_amount = 1000.0
270 && let Some(accept_quest) = create_deposit(
271 ctx,
272 SLAY_REWARD_ITEM,
273 slay_reward_amount,
274 session.ask_yes_no_question(
275 Content::localized("npc-response-quest-slay-ask")
276 .with_arg("body", monster_body.localize_npc())
277 .with_arg("coins", slay_reward_amount as u64),
278 ),
279 )
280 {
281 quests.push(
282 accept_quest
283 .and_then(move |yes| {
284 now(move |ctx, _| {
285 if yes {
286 let quest = Quest::slay(ctx.actor_id, monster_id, session.target)
287 .with_deposit(ESCORT_REWARD_ITEM, slay_reward_amount)
288 .with_timeout(ctx.time.add_minutes(60.0));
289 create_quest(quest.clone())
290 .then(
291 session.give_marker(
292 Marker::at(monster_pos.xy())
293 .with_id(monster_id)
294 .with_label(
295 Content::localized("hud-map-creature-label")
296 .with_arg(
297 "body",
298 monster_body.localize_npc(),
299 ),
300 )
301 .with_quest_flag(true),
302 ),
303 )
304 .then(session.say_statement(Content::localized(
305 "npc-response-quest-slay-start",
306 )))
307 .then(session.say_statement(Content::localized(
308 "npc-response-quest-slay-start_2",
309 )))
310 .then(session.say_statement(Content::localized(
311 "npc-response-quest-slay-start_3",
312 )))
313 .then(session.say_statement(Content::localized(
314 "npc-response-quest-slay-start_4",
315 )))
316 .boxed()
317 } else {
318 session
319 .say_statement(Content::localized(
320 "npc-response-quest-rejected",
321 ))
322 .boxed()
323 }
324 })
325 })
326 .boxed(),
327 );
328 }
329
330 const COURIER_REWARD_ITEM: ItemResource = ItemResource::Coin;
331 if let Some(courier_quest) = roll_courier_quest(ctx, session.target)
332 && let Some(quest_tgt) = ctx.data.actors.get(courier_quest.target_actor)
333 && let Some(tgt_site_name) = courier_quest
334 .target_site
335 .and_then(|tgt_site_id| ctx.data.sites.get(tgt_site_id))
336 .and_then(|queried_site| queried_site.world_site)
337 .and_then(|queried_site_world_id| ctx.index.sites.get(queried_site_world_id).name())
338 {
339 let quest_tgt_actor = courier_quest.target_actor;
340 let (start_stmt, start_question) = courier_quest.get_start_dialogue(
341 quest_tgt
342 .get_name()
343 .unwrap_or_else(|| "<unknown>".to_string())
344 .as_str(),
345 tgt_site_name,
346 );
347 let proposed_quest = create_deposit(
348 ctx,
349 COURIER_REWARD_ITEM,
350 courier_quest.get_reward(),
351 session
352 .say_statement(start_stmt)
353 .then(session.ask_yes_no_question(start_question)),
354 );
355
356 if let Some(accept_quest) = proposed_quest {
357 let tgt_name = quest_tgt
359 .get_name()
360 .unwrap_or_else(|| "<unknown>".to_string());
361 let tgt_name_marker = tgt_name.clone();
362 let tgt_actor_wpos = quest_tgt.wpos.xy();
363 let tgt_actor = quest_tgt_actor;
364 let quest_exp = ctx.time.add_minutes(180.0);
365
366 let quest_offer = accept_quest
367 .and_then(move |yes| {
368 now(move |_ctx, _| {
369 if yes {
370 let quest = Quest::courier(tgt_actor, courier_quest)
371 .with_deposit(COURIER_REWARD_ITEM, courier_quest.get_reward())
372 .with_timeout(quest_exp);
373 create_quest(quest)
374 .and_then(move |quest_id| {
375 now(move |_ctx, _| {
376 if let Some(chunk_pos) = courier_quest.spot {
377 let chunk_wpos = chunk_pos.cpos_to_wpos();
378 session.give_marker(
381 Marker::at(Vec2::new(
382 chunk_wpos.x as f32,
383 chunk_wpos.y as f32,
384 ))
385 .with_id(quest_id)
386 .with_label(
387 courier_quest
388 .get_spot_map_label(tgt_name.as_str()),
389 )
390 .with_quest_flag(true),
391 )
392 } else {
393 session.give_marker(
396 Marker::at(tgt_actor_wpos)
397 .with_id(tgt_actor)
398 .with_label(
399 Content::localized(
400 "hud-map-character-label",
401 )
402 .with_arg("name", tgt_name.as_str()),
403 )
404 .with_kind(MarkerKind::Character),
405 )
406 }
407 })
408 })
409 .then(
410 session.give_marker(
415 Marker::at(tgt_actor_wpos)
416 .with_id(tgt_actor)
417 .with_label(
418 Content::localized("hud-map-character-label")
419 .with_arg("name", tgt_name_marker),
420 )
421 .with_kind(MarkerKind::Character),
422 ),
423 )
424 .then(session.say_statement(Content::localized(
425 "npc-response-quest-courier-start",
426 )))
427 .then(session.say_statement(Content::localized(
428 "npc-response-quest-courier-start_2",
429 )))
430 .then(session.say_statement(Content::localized(
431 "npc-response-quest-courier-start_3",
432 )))
433 .boxed()
434 } else {
435 session
436 .say_statement(Content::localized(
437 "npc-response-quest-rejected",
438 ))
439 .boxed()
440 }
441 })
442 })
443 .boxed();
444
445 quests.push(quest_offer);
446 }
447 }
448
449 if quests.is_empty() {
450 session
451 .say_statement(Content::localized("npc-response-quest-nothing"))
452 .boxed()
453 } else {
454 quests.remove(ctx.rng.random_range(0..quests.len()))
455 }
456 })
457}
458
459pub fn check_for_timeouts<S: State>(ctx: &mut NpcCtx) -> Option<impl Action<S> + use<S>> {
460 for quest_id in ctx.data.quests.related_to(ctx.actor_id) {
461 let Some(quest) = ctx.data.quests.get(quest_id) else {
462 continue;
463 };
464 if let Some(timeout) = quest.timeout
465 && ctx.time > timeout
467 && let Ok(Some(_)) = resolve_take_deposit(ctx, quest_id, false)
469 {
470 if ctx.npc.job == Some(Job::Quest(quest_id)) {
472 ctx.controller.end_quest();
473 }
474
475 match quest.kind {
477 QuestKind::Escort { escorter, .. } => {
478 return Some(
479 goto_actor(escorter, 2.0)
480 .then(do_dialogue(escorter, move |session| {
481 session
482 .say_statement(Content::localized("npc-response-quest-timeout"))
483 }))
484 .boxed(),
485 );
486 },
487 QuestKind::Slay { .. } => {},
488 QuestKind::Courier { .. } => {},
489 }
490 }
491 }
492 None
493}
494
495pub fn escorted<S: State>(
496 quest_id: QuestId,
497 escorter: ActorId,
498 dst_site: SiteId,
499) -> impl Action<S> {
500 follow_actor(escorter, 5.0)
501 .stop_if(move |ctx: &mut NpcCtx| {
502 if let Some(escorter_pos) = util::locate_actor(ctx, escorter)
504 && ctx.actor.wpos.xy().distance_squared(escorter_pos.xy()) > 20.0f32.powi(2)
505 && ctx.rng.random_bool(ctx.dt as f64 / 30.0)
506 {
507 ctx.controller
508 .say(None, Content::localized("npc-speech-wait_for_me"));
509 }
510 ctx.data
512 .sites
513 .get(dst_site)
514 .is_none_or(|site| site.wpos.as_().distance_squared(ctx.actor.wpos.xy()) < 150.0f32.powi(2))
515 })
516 .then(goto_actor(escorter, 2.0))
517 .then(do_dialogue(escorter, move |session| {
518 session
519 .say_statement(Content::localized("npc-response-quest-escort-complete"))
520 .then(now(move |ctx, _| {
522 ctx.controller.end_quest();
523 match resolve_take_deposit(ctx, quest_id, true) {
524 Ok(deposit) => session.say_statement_with_gift(Content::localized("npc-response-quest-reward"), deposit).boxed(),
525 Err(()) => finish().boxed(),
526 }
527 }))
528 }))
529 .stop_if(move |ctx: &mut NpcCtx| {
530 ctx.data
532 .quests
533 .get(quest_id)
534 .is_none_or(|q| q.resolution().is_some())
535 })
536 .map(|_, _| ())
537}
538
539pub fn get_nearest_spot(
548 ctx: &mut NpcCtx,
549 quest: CourierQuest,
550 target_chunk: Vec2<i32>,
551) -> Option<Vec2<i32>> {
552 match quest.payload() {
553 None | Some(Payload::LegoomLeaf) => None,
555 Some(Payload::GnarlingCarving) => ctx
557 .world
558 .sim()
559 .get_nearest_spot(target_chunk, |spot| matches!(spot, Spot::GnarlingTotem)),
560 }
561}
562
563const MAX_COURIER_QUEST_DISTANCE: f32 = 5_000.0;
564
565impl CourierQuestInstance {
568 pub fn get_required_items(self) -> Result<Vec<(Arc<ItemDef>, f32)>, common::assets::Error> {
571 match self.kind.payload() {
572 None => Ok(vec![]),
573 Some(Payload::GnarlingCarving) => Ok(vec![(
574 Arc::<ItemDef>::load_cloned("common.items.quest.gnarling_carving")?,
575 1.0_f32,
576 )]),
577 Some(Payload::LegoomLeaf) => Ok(vec![(
578 Arc::<ItemDef>::load_cloned("common.items.quest.legoom_leaf")?,
579 1.0_f32,
580 )]),
581 }
582 }
583
584 pub fn get_reward(self) -> f32 {
588 match self.kind {
589 CourierQuest::Message => f32::max(
590 150.0,
591 1000.0 * (self.distance.get() as f32 / MAX_COURIER_QUEST_DISTANCE),
592 ),
593 CourierQuest::Deliver {
594 payload: Payload::GnarlingCarving,
595 recipient: Recipient::Other,
596 } => f32::max(
597 500.0,
598 1400.0 * (self.distance.get() as f32 / MAX_COURIER_QUEST_DISTANCE),
599 ),
600 CourierQuest::Deliver {
601 payload: Payload::GnarlingCarving,
602 recipient: Recipient::Giver,
603 } => 350.0,
604 CourierQuest::Deliver {
605 payload: Payload::LegoomLeaf,
606 recipient: Recipient::Other,
607 } => f32::max(
608 400.0,
609 1200.0 * (self.distance.get() as f32 / MAX_COURIER_QUEST_DISTANCE),
610 ),
611 CourierQuest::Deliver {
612 payload: Payload::LegoomLeaf,
613 recipient: Recipient::Giver,
614 } => 200.0,
615 }
616 }
617
618 pub fn get_spot_map_label(self, npc_name: &str) -> Content {
621 Content::localized(match self.kind.payload() {
622 Some(Payload::GnarlingCarving) => "hud-map-spot-gnarling-carving-label",
623 None | Some(Payload::LegoomLeaf) => "hud-map-spot-unspecified",
625 })
626 .with_arg("name", npc_name)
627 }
628
629 pub fn lacks_items(self) -> Content {
631 Content::localized(match self.kind.payload() {
632 Some(Payload::GnarlingCarving) => {
633 "npc-response-quest-courier-gnarling-carving-insufficient-items"
634 },
635 Some(Payload::LegoomLeaf) => {
636 "npc-response-quest-courier-legoom-leaf-insufficient-items"
637 },
638 None => "npc-response-quest-courier-generic-insufficient-items",
640 })
641 }
642
643 pub fn what_items_needed(self, is_target_npc: bool, npc_name: &str) -> (Content, Content) {
649 (
650 Content::localized(match self.kind {
651 CourierQuest::Deliver {
652 recipient: Recipient::Other,
653 ..
654 } => {
655 if is_target_npc {
656 "dialogue-question-quest-courier-what-target"
657 } else {
658 "dialogue-question-quest-courier-what"
659 }
660 },
661 CourierQuest::Deliver {
662 recipient: Recipient::Giver,
663 ..
664 } => "dialogue-question-quest-fetch-what",
665 CourierQuest::Message => {
666 if is_target_npc {
667 "dialogue-question-quest-messenger-what-target"
668 } else {
669 "dialogue-question-quest-messenger-what"
670 }
671 },
672 })
673 .with_arg("name", npc_name),
674 Content::localized(match self.kind.payload() {
675 Some(Payload::GnarlingCarving) => {
676 "npc-response-quest-courier-gnarling-carving-what-is-needed"
677 },
678 Some(Payload::LegoomLeaf) => {
679 "npc-response-quest-courier-legoom-leaf-what-is-needed"
680 },
681 None => {
682 if is_target_npc {
683 "npc-response-quest-messenger-what-is-needed-target"
684 } else {
685 "npc-response-quest-messenger-what-is-needed"
686 }
687 },
688 })
689 .with_arg("name", npc_name),
690 )
691 }
692
693 pub fn get_spot_name(self) -> Content {
696 Content::localized(match self.kind.payload() {
697 Some(Payload::GnarlingCarving) => "spot-name-gnarling-totem",
698 None | Some(Payload::LegoomLeaf) => "spot-name-unspecified",
699 })
700 }
701
702 pub fn get_start_dialogue(
709 self,
710 tgt_npc_name_str: &str,
711 tgt_site_name: &str,
712 ) -> (Content, Content) {
713 const COURIER_GNARLING_CARVING_START_STMT: &str =
714 "npc-response-quest-courier-gnarling-carving";
715 const COURIER_LEGOOM_LEAF_START_STMT: &str = "npc-response-quest-courier-legoom-leaf";
716 const MESSENGER_SEND_WORD_START_STMT: &str = "npc-response-quest-messenger-send-word";
717
718 match self.kind {
719 CourierQuest::Deliver {
720 payload: Payload::GnarlingCarving,
721 recipient: Recipient::Other,
722 } => (
723 Content::localized(COURIER_GNARLING_CARVING_START_STMT),
724 Content::localized("npc-response-quest-spot-courier-ask")
725 .with_arg("spot", self.get_spot_name())
726 .with_arg("coins", self.get_reward() as u64)
727 .with_arg("name", tgt_npc_name_str)
728 .with_arg("site", tgt_site_name),
729 ),
730 CourierQuest::Deliver {
731 payload: Payload::GnarlingCarving,
732 recipient: Recipient::Giver,
733 } => (
734 Content::localized(COURIER_GNARLING_CARVING_START_STMT),
735 Content::localized("npc-response-quest-spot-fetch-ask")
736 .with_arg("spot", self.get_spot_name())
737 .with_arg("coins", self.get_reward() as u64),
738 ),
739 CourierQuest::Deliver {
740 payload: Payload::LegoomLeaf,
741 recipient: Recipient::Other,
742 } => (
743 Content::localized(COURIER_LEGOOM_LEAF_START_STMT),
744 Content::localized("npc-response-quest-courier-ask")
745 .with_arg("coins", self.get_reward() as u64)
746 .with_arg("name", tgt_npc_name_str)
747 .with_arg("site", tgt_site_name),
748 ),
749 CourierQuest::Deliver {
750 payload: Payload::LegoomLeaf,
751 recipient: Recipient::Giver,
752 } => (
753 Content::localized(COURIER_LEGOOM_LEAF_START_STMT),
754 Content::localized("npc-response-quest-fetch-ask")
755 .with_arg("coins", self.get_reward() as u64),
756 ),
757 CourierQuest::Message => (
758 Content::localized(MESSENGER_SEND_WORD_START_STMT),
759 Content::localized("npc-response-quest-messenger-ask")
760 .with_arg("coins", self.get_reward() as u64)
761 .with_arg("name", tgt_npc_name_str)
762 .with_arg("site", tgt_site_name),
763 ),
764 }
765 }
766
767 pub fn get_dialogue_where_target(
770 self,
771 npc_name: &str,
772 at: Vec2<f32>,
773 target: ActorId,
774 ) -> (Content, Marker, Content) {
775 (
776 Content::localized("dialogue-question-quest-courier-where").with_arg("name", npc_name),
777 Marker::at(at)
778 .with_label(
779 Content::localized("hud-map-character-label").with_arg("name", npc_name),
780 )
781 .with_kind(MarkerKind::Character)
782 .with_id(target)
783 .with_quest_flag(true),
784 Content::localized("npc-response-quest-courier-where").with_arg("name", npc_name),
785 )
786 }
787
788 pub fn get_courier_claim_dialogue(self) -> (Content, Content) {
792 (
793 Content::localized("dialogue-question-quest-courier-claim"),
794 Content::localized("npc-response-quest-courier-thanks"),
795 )
796 }
797
798 pub fn get_quest_spot_start_marker(
801 self,
802 at: Vec2<f32>,
803 tgt_npc_name: &str,
804 quest_id: QuestId,
805 ) -> Marker {
806 Marker::at(at)
807 .with_id(quest_id)
808 .with_kind(MarkerKind::Unknown)
809 .with_quest_flag(true)
810 .with_label(self.get_spot_map_label(tgt_npc_name))
811 }
812
813 pub fn get_quest_actor_target_marker(
816 self,
817 at: Vec2<f32>,
818 tgt_actor_name: &str,
819 target_actor_id: ActorId,
820 ) -> Marker {
821 Marker::at(at)
822 .with_id(target_actor_id)
823 .with_kind(MarkerKind::Character)
824 .with_quest_flag(true)
825 .with_label(
826 Content::localized("hud-map-character-label").with_arg("name", tgt_actor_name),
827 )
828 }
829}
830
831fn roll_courier_quest(ctx: &mut NpcCtx, messenger: ActorId) -> Option<CourierQuestInstance> {
833 let kind = COURIER_QUEST_VARIANTS
834 .choose(&mut ctx.rng)
835 .copied()
836 .unwrap_or(CourierQuest::Message);
837
838 let (target_site, target_actor, distance) = match kind {
839 CourierQuest::Deliver {
841 recipient: Recipient::Giver,
842 ..
843 } => (ctx.actor.current_site, ctx.actor_id, 0.0),
844 CourierQuest::Deliver {
847 recipient: Recipient::Other,
848 ..
849 }
850 | CourierQuest::Message => ctx
851 .data
852 .actors
853 .iter()
854 .filter(|(_, actor)| actor.npc().is_some())
855 .filter_map(|(npc_id, npc)| match &npc.role {
856 Role::Civilised(Some(Profession::Hunter))
857 | Role::Civilised(Some(Profession::Farmer))
858 | Role::Civilised(Some(Profession::Blacksmith))
859 | Role::Civilised(Some(Profession::Alchemist))
860 | Role::Civilised(Some(Profession::Chef))
861 | Role::Civilised(Some(Profession::Herbalist))
862 | Role::Civilised(Some(Profession::Guard)) => {
863 let distance = ctx.actor.wpos.xy().distance(npc.wpos.xy());
864 (distance <= MAX_COURIER_QUEST_DISTANCE).then_some((npc_id, npc, distance))
865 },
866 _ => None,
867 })
868 .choose(&mut ctx.rng)
869 .and_then(|(tgt_npc_id, tgt_npc, distance)| {
870 ctx.data
871 .sites
872 .iter()
873 .filter_map(|(site_id, site)| {
874 (tgt_npc.wpos.xy().distance(site.wpos.as_()) <= 512.0).then_some(site_id)
875 })
876 .choose(&mut ctx.rng)
877 .map(|site_id| (Some(site_id), tgt_npc_id, distance))
878 })?,
879 };
880
881 let spot = get_nearest_spot(ctx, kind, ctx.actor.wpos.xy().wpos_to_cpos().as_());
882
883 if match kind.payload() {
887 Some(Payload::GnarlingCarving) => spot.is_none(),
888 Some(Payload::LegoomLeaf) | None => false,
889 } {
890 return None;
891 }
892
893 const ONE: NonZeroU32 = NonZeroU32::new(1).unwrap();
894 Some(CourierQuestInstance {
895 kind,
896 spot,
897 source_site: ctx.actor.current_site,
898 source_actor: ctx.actor_id,
899 target_actor,
900 target_site,
901 messenger,
902 distance: NonZeroU32::new(distance as u32).unwrap_or(ONE),
903 })
904}