1use super::*;
2use crate::data::quest::{
3 COURIER_QUEST_VARIANTS, CourierQuest, CourierQuestInstance, Payload, Recipient,
4};
5use common::{
6 comp::{Item, item::ItemBase},
7 rtsim::NpcId,
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.npc_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.npc_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.npc_id, success))
76 {
77 if let Some((item, amount)) = &outcome.deposit
79 && let Some(npc_entity) = ctx.system_data.id_maps.rtsim_entity(ctx.npc_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) = match instance.messenger {
134 Actor::Character(character_id) => {
135 ctx.system_data.id_maps.character_entity(character_id)
136 },
137 Actor::Npc(npc_id) => ctx.system_data.id_maps.rtsim_entity(npc_id),
138 }
139 && let Ok(mut inventories) = ctx.system_data.inventories.lock()
140 && let Some(mut inv) = inventories.get_mut(entity)
141 && let Ok(required_items) = instance.get_required_items()
142 && required_items.iter().all(|(item_def, amount)| {
143 inv.item_count(item_def) >= u64::from(required_count(*amount))
144 })
145 && (read_only
146 || required_items.iter().all(|(item_def, amount)| {
147 inv.remove_item_amount(
148 item_def,
149 required_count(*amount),
150 &ctx.system_data.ability_map,
151 &ctx.system_data.msm,
152 )
153 .is_some()
154 }))
155 {
156 true
157 } else {
158 false
159 }
160}
161
162pub fn create_quest<S: State>(quest: Quest) -> impl Action<S, QuestId> {
167 just(move |ctx, _| {
168 let quest_id = ctx.data.quests.register();
169 ctx.controller
170 .quests_to_create
171 .push((quest_id, quest.clone()));
172 quest_id
173 })
174}
175
176pub fn quest_request<S: State>(session: DialogueSession) -> impl Action<S> {
177 now(move |ctx, _| {
178 let mut quests = Vec::new();
179
180 const ESCORT_REWARD_ITEM: ItemResource = ItemResource::Coin;
182 if ctx.npc.job.is_none()
184 && matches!(ctx.npc.profession(), Some(Profession::Merchant))
186 && let Some((dst_site_id, dst_site, dist)) = ctx.data
188 .sites
189 .iter()
190 .map(|(site_id, site)| (site_id, site, site.wpos.as_().distance(ctx.npc.wpos.xy())))
192 .filter(|(site_id, _, dist)| Some(*site_id) != ctx.npc.current_site && (1000.0..5_000.0).contains(dist))
194 .choose(&mut ChaChaRng::from_seed([(ctx.time.0 / (60.0 * 15.0)) as u8; 32]))
197 && let escort_reward_amount = dist / 5.0
199 && let Some(dst_site_name) = util::site_name(ctx, dst_site_id)
200 && let time_limit = 1.0 + dist as f64 / 80.0
201 && let Some(accept_quest) = create_deposit(ctx, ESCORT_REWARD_ITEM, escort_reward_amount, session
202 .ask_yes_no_question(Content::localized("npc-response-quest-escort-ask")
203 .with_arg("dst", dst_site_name.clone())
204 .with_arg("coins", escort_reward_amount as u64)
205 .with_arg("mins", time_limit as u64)))
206 {
207 let dst_wpos = dst_site.wpos.as_();
208 quests.push(
209 accept_quest
210 .and_then(move |yes| {
211 now(move |ctx, _| {
212 if yes {
213 let quest =
214 Quest::escort(ctx.npc_id.into(), session.target, dst_site_id)
215 .with_deposit(ESCORT_REWARD_ITEM, escort_reward_amount)
216 .with_timeout(ctx.time.add_minutes(time_limit));
217 create_quest(quest.clone())
218 .and_then(move |quest_id| {
219 now(move |ctx, _| {
220 ctx.controller.job = Some(Job::Quest(quest_id));
221 session.give_marker(
222 Marker::at(dst_wpos)
223 .with_id(quest_id)
224 .with_label(
225 Content::localized("hud-map-escort-label")
226 .with_arg(
227 "name",
228 ctx.npc.get_name().unwrap_or_else(
229 || "<unknown>".to_string(),
230 ),
231 )
232 .with_arg(
233 "place",
234 dst_site_name.clone(),
235 ),
236 )
237 .with_quest_flag(true),
238 )
239 })
240 })
241 .then(session.say_statement(Content::localized(
242 "npc-response-quest-escort-start",
243 )))
244 .boxed()
245 } else {
246 session
247 .say_statement(Content::localized(
248 "npc-response-quest-rejected",
249 ))
250 .boxed()
251 }
252 })
253 })
254 .boxed(),
255 );
256 }
257
258 const SLAY_REWARD_ITEM: ItemResource = ItemResource::Coin;
260 if let Some((monster_id, monster)) = ctx.data
261 .npcs
262 .iter()
263 .filter(|(_, npc)| matches!(&npc.role, Role::Monster))
265 .filter(|(id, _)| ctx.data.quests.related_to(*id).count() == 0)
267 .filter(|(_, npc)| npc.wpos.xy().distance(ctx.npc.wpos.xy()) < 2500.0)
269 .min_by_key(|(_, npc)| npc.wpos.xy().distance_squared(ctx.npc.wpos.xy()) as i64)
271 && let monster_pos = monster.wpos
272 && let monster_body = monster.body
273 && let slay_reward_amount = 1000.0
274 && let Some(accept_quest) = create_deposit(
275 ctx,
276 SLAY_REWARD_ITEM,
277 slay_reward_amount,
278 session.ask_yes_no_question(
279 Content::localized("npc-response-quest-slay-ask")
280 .with_arg("body", monster_body.localize_npc())
281 .with_arg("coins", slay_reward_amount as u64),
282 ),
283 )
284 {
285 quests.push(
286 accept_quest
287 .and_then(move |yes| {
288 now(move |ctx, _| {
289 if yes {
290 let quest = Quest::slay(
291 ctx.npc_id.into(),
292 monster_id.into(),
293 session.target,
294 )
295 .with_deposit(ESCORT_REWARD_ITEM, slay_reward_amount)
296 .with_timeout(ctx.time.add_minutes(60.0));
297 create_quest(quest.clone())
298 .then(
299 session.give_marker(
300 Marker::at(monster_pos.xy())
301 .with_id(Actor::from(monster_id))
302 .with_label(
303 Content::localized("hud-map-creature-label")
304 .with_arg(
305 "body",
306 monster_body.localize_npc(),
307 ),
308 )
309 .with_quest_flag(true),
310 ),
311 )
312 .then(session.say_statement(Content::localized(
313 "npc-response-quest-slay-start",
314 )))
315 .then(session.say_statement(Content::localized(
316 "npc-response-quest-slay-start_2",
317 )))
318 .then(session.say_statement(Content::localized(
319 "npc-response-quest-slay-start_3",
320 )))
321 .then(session.say_statement(Content::localized(
322 "npc-response-quest-slay-start_4",
323 )))
324 .boxed()
325 } else {
326 session
327 .say_statement(Content::localized(
328 "npc-response-quest-rejected",
329 ))
330 .boxed()
331 }
332 })
333 })
334 .boxed(),
335 );
336 }
337
338 const COURIER_REWARD_ITEM: ItemResource = ItemResource::Coin;
339 if let Some(courier_quest) = roll_courier_quest(ctx, session.target)
340 && let Some(quest_tgt) = match courier_quest.target_actor {
341 Actor::Npc(tgt_npc_id) => ctx.data.npcs.npcs.get(tgt_npc_id),
342 Actor::Character(_) => None,
345 }
346 && let Some(tgt_site_name) = courier_quest
347 .target_site
348 .and_then(|tgt_site_id| ctx.data.sites.get(tgt_site_id))
349 .and_then(|queried_site| queried_site.world_site)
350 .and_then(|queried_site_world_id| ctx.index.sites.get(queried_site_world_id).name())
351 {
352 let quest_tgt_actor = courier_quest.target_actor;
353 let (start_stmt, start_question) = courier_quest.get_start_dialogue(
354 quest_tgt
355 .get_name()
356 .unwrap_or_else(|| "<unknown>".to_string())
357 .as_str(),
358 tgt_site_name,
359 );
360 let proposed_quest = create_deposit(
361 ctx,
362 COURIER_REWARD_ITEM,
363 courier_quest.get_reward(),
364 session
365 .say_statement(start_stmt)
366 .then(session.ask_yes_no_question(start_question)),
367 );
368
369 if let Some(accept_quest) = proposed_quest {
370 let tgt_name = quest_tgt
372 .get_name()
373 .unwrap_or_else(|| "<unknown>".to_string());
374 let tgt_name_marker = tgt_name.clone();
375 let tgt_actor_wpos = quest_tgt.wpos.xy();
376 let tgt_actor = quest_tgt_actor;
377 let quest_exp = ctx.time.add_minutes(180.0);
378
379 let quest_offer = accept_quest
380 .and_then(move |yes| {
381 now(move |_ctx, _| {
382 if yes {
383 let quest = Quest::courier(tgt_actor, courier_quest)
384 .with_deposit(COURIER_REWARD_ITEM, courier_quest.get_reward())
385 .with_timeout(quest_exp);
386 create_quest(quest)
387 .and_then(move |quest_id| {
388 now(move |_ctx, _| {
389 if let Some(chunk_pos) = courier_quest.spot {
390 let chunk_wpos = chunk_pos.cpos_to_wpos();
391 session.give_marker(
394 Marker::at(Vec2::new(
395 chunk_wpos.x as f32,
396 chunk_wpos.y as f32,
397 ))
398 .with_id(quest_id)
399 .with_label(
400 courier_quest
401 .get_spot_map_label(tgt_name.as_str()),
402 )
403 .with_quest_flag(true),
404 )
405 } else {
406 session.give_marker(
409 Marker::at(tgt_actor_wpos)
410 .with_id(tgt_actor)
411 .with_label(
412 Content::localized(
413 "hud-map-character-label",
414 )
415 .with_arg("name", tgt_name.as_str()),
416 )
417 .with_kind(MarkerKind::Character),
418 )
419 }
420 })
421 })
422 .then(
423 session.give_marker(
428 Marker::at(tgt_actor_wpos)
429 .with_id(tgt_actor)
430 .with_label(
431 Content::localized("hud-map-character-label")
432 .with_arg("name", tgt_name_marker),
433 )
434 .with_kind(MarkerKind::Character),
435 ),
436 )
437 .then(session.say_statement(Content::localized(
438 "npc-response-quest-courier-start",
439 )))
440 .then(session.say_statement(Content::localized(
441 "npc-response-quest-courier-start_2",
442 )))
443 .then(session.say_statement(Content::localized(
444 "npc-response-quest-courier-start_3",
445 )))
446 .boxed()
447 } else {
448 session
449 .say_statement(Content::localized(
450 "npc-response-quest-rejected",
451 ))
452 .boxed()
453 }
454 })
455 })
456 .boxed();
457
458 quests.push(quest_offer);
459 }
460 }
461
462 if quests.is_empty() {
463 session
464 .say_statement(Content::localized("npc-response-quest-nothing"))
465 .boxed()
466 } else {
467 quests.remove(ctx.rng.random_range(0..quests.len()))
468 }
469 })
470}
471
472pub fn check_for_timeouts<S: State>(ctx: &mut NpcCtx) -> Option<impl Action<S> + use<S>> {
473 for quest_id in ctx.data.quests.related_to(ctx.npc_id) {
474 let Some(quest) = ctx.data.quests.get(quest_id) else {
475 continue;
476 };
477 if let Some(timeout) = quest.timeout
478 && ctx.time > timeout
480 && let Ok(Some(_)) = resolve_take_deposit(ctx, quest_id, false)
482 {
483 if ctx.npc.job == Some(Job::Quest(quest_id)) {
485 ctx.controller.end_quest();
486 }
487
488 match quest.kind {
490 QuestKind::Escort { escorter, .. } => {
491 return Some(
492 goto_actor(escorter, 2.0)
493 .then(do_dialogue(escorter, move |session| {
494 session
495 .say_statement(Content::localized("npc-response-quest-timeout"))
496 }))
497 .boxed(),
498 );
499 },
500 QuestKind::Slay { .. } => {},
501 QuestKind::Courier { .. } => {},
502 }
503 }
504 }
505 None
506}
507
508pub fn escorted<S: State>(quest_id: QuestId, escorter: Actor, dst_site: SiteId) -> impl Action<S> {
509 follow_actor(escorter, 5.0)
510 .stop_if(move |ctx: &mut NpcCtx| {
511 if let Some(escorter_pos) = util::locate_actor(ctx, escorter)
513 && ctx.npc.wpos.xy().distance_squared(escorter_pos.xy()) > 20.0f32.powi(2)
514 && ctx.rng.random_bool(ctx.dt as f64 / 30.0)
515 {
516 ctx.controller
517 .say(None, Content::localized("npc-speech-wait_for_me"));
518 }
519 ctx.data
521 .sites
522 .get(dst_site)
523 .is_none_or(|site| site.wpos.as_().distance_squared(ctx.npc.wpos.xy()) < 150.0f32.powi(2))
524 })
525 .then(goto_actor(escorter, 2.0))
526 .then(do_dialogue(escorter, move |session| {
527 session
528 .say_statement(Content::localized("npc-response-quest-escort-complete"))
529 .then(now(move |ctx, _| {
531 ctx.controller.end_quest();
532 match resolve_take_deposit(ctx, quest_id, true) {
533 Ok(deposit) => session.say_statement_with_gift(Content::localized("npc-response-quest-reward"), deposit).boxed(),
534 Err(()) => finish().boxed(),
535 }
536 }))
537 }))
538 .stop_if(move |ctx: &mut NpcCtx| {
539 ctx.data
541 .quests
542 .get(quest_id)
543 .is_none_or(|q| q.resolution().is_some())
544 })
545 .map(|_, _| ())
546}
547
548pub fn get_nearest_spot(
557 ctx: &mut NpcCtx,
558 quest: CourierQuest,
559 target_chunk: Vec2<i32>,
560) -> Option<Vec2<i32>> {
561 match quest.payload() {
562 None | Some(Payload::LegoomLeaf) => None,
564 Some(Payload::GnarlingCarving) => ctx
566 .world
567 .sim()
568 .get_nearest_spot(target_chunk, |spot| matches!(spot, Spot::GnarlingTotem)),
569 }
570}
571
572const MAX_COURIER_QUEST_DISTANCE: f32 = 5_000.0;
573
574impl CourierQuestInstance {
577 pub fn get_required_items(self) -> Result<Vec<(Arc<ItemDef>, f32)>, common::assets::Error> {
580 match self.kind.payload() {
581 None => Ok(vec![]),
582 Some(Payload::GnarlingCarving) => Ok(vec![(
583 Arc::<ItemDef>::load_cloned("common.items.quest.gnarling_carving")?,
584 1.0_f32,
585 )]),
586 Some(Payload::LegoomLeaf) => Ok(vec![(
587 Arc::<ItemDef>::load_cloned("common.items.quest.legoom_leaf")?,
588 1.0_f32,
589 )]),
590 }
591 }
592
593 pub fn get_reward(self) -> f32 {
597 match self.kind {
598 CourierQuest::Message => f32::max(
599 150.0,
600 1000.0 * (self.distance.get() as f32 / MAX_COURIER_QUEST_DISTANCE),
601 ),
602 CourierQuest::Deliver {
603 payload: Payload::GnarlingCarving,
604 recipient: Recipient::Other,
605 } => f32::max(
606 500.0,
607 1400.0 * (self.distance.get() as f32 / MAX_COURIER_QUEST_DISTANCE),
608 ),
609 CourierQuest::Deliver {
610 payload: Payload::GnarlingCarving,
611 recipient: Recipient::Giver,
612 } => 350.0,
613 CourierQuest::Deliver {
614 payload: Payload::LegoomLeaf,
615 recipient: Recipient::Other,
616 } => f32::max(
617 400.0,
618 1200.0 * (self.distance.get() as f32 / MAX_COURIER_QUEST_DISTANCE),
619 ),
620 CourierQuest::Deliver {
621 payload: Payload::LegoomLeaf,
622 recipient: Recipient::Giver,
623 } => 200.0,
624 }
625 }
626
627 pub fn get_spot_map_label(self, npc_name: &str) -> Content {
630 Content::localized(match self.kind.payload() {
631 Some(Payload::GnarlingCarving) => "hud-map-spot-gnarling-carving-label",
632 None | Some(Payload::LegoomLeaf) => "hud-map-spot-unspecified",
634 })
635 .with_arg("name", npc_name)
636 }
637
638 pub fn lacks_items(self) -> Content {
640 Content::localized(match self.kind.payload() {
641 Some(Payload::GnarlingCarving) => {
642 "npc-response-quest-courier-gnarling-carving-insufficient-items"
643 },
644 Some(Payload::LegoomLeaf) => {
645 "npc-response-quest-courier-legoom-leaf-insufficient-items"
646 },
647 None => "npc-response-quest-courier-generic-insufficient-items",
649 })
650 }
651
652 pub fn what_items_needed(self, is_target_npc: bool, npc_name: &str) -> (Content, Content) {
658 (
659 Content::localized(match self.kind {
660 CourierQuest::Deliver {
661 recipient: Recipient::Other,
662 ..
663 } => {
664 if is_target_npc {
665 "dialogue-question-quest-courier-what-target"
666 } else {
667 "dialogue-question-quest-courier-what"
668 }
669 },
670 CourierQuest::Deliver {
671 recipient: Recipient::Giver,
672 ..
673 } => "dialogue-question-quest-fetch-what",
674 CourierQuest::Message => {
675 if is_target_npc {
676 "dialogue-question-quest-messenger-what-target"
677 } else {
678 "dialogue-question-quest-messenger-what"
679 }
680 },
681 })
682 .with_arg("name", npc_name),
683 Content::localized(match self.kind.payload() {
684 Some(Payload::GnarlingCarving) => {
685 "npc-response-quest-courier-gnarling-carving-what-is-needed"
686 },
687 Some(Payload::LegoomLeaf) => {
688 "npc-response-quest-courier-legoom-leaf-what-is-needed"
689 },
690 None => {
691 if is_target_npc {
692 "npc-response-quest-messenger-what-is-needed-target"
693 } else {
694 "npc-response-quest-messenger-what-is-needed"
695 }
696 },
697 })
698 .with_arg("name", npc_name),
699 )
700 }
701
702 pub fn get_spot_name(self) -> Content {
705 Content::localized(match self.kind.payload() {
706 Some(Payload::GnarlingCarving) => "spot-name-gnarling-totem",
707 None | Some(Payload::LegoomLeaf) => "spot-name-unspecified",
708 })
709 }
710
711 pub fn get_start_dialogue(
718 self,
719 tgt_npc_name_str: &str,
720 tgt_site_name: &str,
721 ) -> (Content, Content) {
722 const COURIER_GNARLING_CARVING_START_STMT: &str =
723 "npc-response-quest-courier-gnarling-carving";
724 const COURIER_LEGOOM_LEAF_START_STMT: &str = "npc-response-quest-courier-legoom-leaf";
725 const MESSENGER_SEND_WORD_START_STMT: &str = "npc-response-quest-messenger-send-word";
726
727 match self.kind {
728 CourierQuest::Deliver {
729 payload: Payload::GnarlingCarving,
730 recipient: Recipient::Other,
731 } => (
732 Content::localized(COURIER_GNARLING_CARVING_START_STMT),
733 Content::localized("npc-response-quest-spot-courier-ask")
734 .with_arg("spot", self.get_spot_name())
735 .with_arg("coins", self.get_reward() as u64)
736 .with_arg("name", tgt_npc_name_str)
737 .with_arg("site", tgt_site_name),
738 ),
739 CourierQuest::Deliver {
740 payload: Payload::GnarlingCarving,
741 recipient: Recipient::Giver,
742 } => (
743 Content::localized(COURIER_GNARLING_CARVING_START_STMT),
744 Content::localized("npc-response-quest-spot-fetch-ask")
745 .with_arg("spot", self.get_spot_name())
746 .with_arg("coins", self.get_reward() as u64),
747 ),
748 CourierQuest::Deliver {
749 payload: Payload::LegoomLeaf,
750 recipient: Recipient::Other,
751 } => (
752 Content::localized(COURIER_LEGOOM_LEAF_START_STMT),
753 Content::localized("npc-response-quest-courier-ask")
754 .with_arg("coins", self.get_reward() as u64)
755 .with_arg("name", tgt_npc_name_str)
756 .with_arg("site", tgt_site_name),
757 ),
758 CourierQuest::Deliver {
759 payload: Payload::LegoomLeaf,
760 recipient: Recipient::Giver,
761 } => (
762 Content::localized(COURIER_LEGOOM_LEAF_START_STMT),
763 Content::localized("npc-response-quest-fetch-ask")
764 .with_arg("coins", self.get_reward() as u64),
765 ),
766 CourierQuest::Message => (
767 Content::localized(MESSENGER_SEND_WORD_START_STMT),
768 Content::localized("npc-response-quest-messenger-ask")
769 .with_arg("coins", self.get_reward() as u64)
770 .with_arg("name", tgt_npc_name_str)
771 .with_arg("site", tgt_site_name),
772 ),
773 }
774 }
775
776 pub fn get_dialogue_where_target(
779 self,
780 npc_name: &str,
781 at: Vec2<f32>,
782 target: Actor,
783 ) -> (Content, Marker, Content) {
784 (
785 Content::localized("dialogue-question-quest-courier-where").with_arg("name", npc_name),
786 Marker::at(at)
787 .with_label(
788 Content::localized("hud-map-character-label").with_arg("name", npc_name),
789 )
790 .with_kind(MarkerKind::Character)
791 .with_id(target)
792 .with_quest_flag(true),
793 Content::localized("npc-response-quest-courier-where").with_arg("name", npc_name),
794 )
795 }
796
797 pub fn get_courier_claim_dialogue(self) -> (Content, Content) {
801 (
802 Content::localized("dialogue-question-quest-courier-claim"),
803 Content::localized("npc-response-quest-courier-thanks"),
804 )
805 }
806
807 pub fn get_quest_spot_start_marker(
810 self,
811 at: Vec2<f32>,
812 tgt_npc_name: &str,
813 quest_id: QuestId,
814 ) -> Marker {
815 Marker::at(at)
816 .with_id(quest_id)
817 .with_kind(MarkerKind::Unknown)
818 .with_quest_flag(true)
819 .with_label(self.get_spot_map_label(tgt_npc_name))
820 }
821
822 pub fn get_quest_npc_target_marker(
825 self,
826 at: Vec2<f32>,
827 tgt_npc_name: &str,
828 target_npc_id: NpcId,
829 ) -> Marker {
830 Marker::at(at)
831 .with_id(target_npc_id)
832 .with_kind(MarkerKind::Character)
833 .with_quest_flag(true)
834 .with_label(
835 Content::localized("hud-map-character-label").with_arg("name", tgt_npc_name),
836 )
837 }
838}
839
840fn roll_courier_quest(ctx: &mut NpcCtx, messenger: Actor) -> Option<CourierQuestInstance> {
842 let kind = COURIER_QUEST_VARIANTS
843 .choose(&mut ctx.rng)
844 .copied()
845 .unwrap_or(CourierQuest::Message);
846
847 let (target_site, target_actor, distance) = match kind {
848 CourierQuest::Deliver {
850 recipient: Recipient::Giver,
851 ..
852 } => (ctx.npc.current_site, Actor::from(ctx.npc_id), 0.0),
853 CourierQuest::Deliver {
856 recipient: Recipient::Other,
857 ..
858 }
859 | CourierQuest::Message => ctx
860 .data
861 .npcs
862 .iter()
863 .filter_map(|(npc_id, npc)| match &npc.role {
864 Role::Civilised(Some(Profession::Hunter))
865 | Role::Civilised(Some(Profession::Farmer))
866 | Role::Civilised(Some(Profession::Blacksmith))
867 | Role::Civilised(Some(Profession::Alchemist))
868 | Role::Civilised(Some(Profession::Chef))
869 | Role::Civilised(Some(Profession::Herbalist))
870 | Role::Civilised(Some(Profession::Guard)) => {
871 let distance = ctx.npc.wpos.xy().distance(npc.wpos.xy());
872 (distance <= MAX_COURIER_QUEST_DISTANCE).then_some((npc_id, npc, distance))
873 },
874 _ => None,
875 })
876 .choose(&mut ctx.rng)
877 .and_then(|(tgt_npc_id, tgt_npc, distance)| {
878 ctx.data
879 .sites
880 .iter()
881 .filter_map(|(site_id, site)| {
882 (tgt_npc.wpos.xy().distance(site.wpos.as_()) <= 512.0).then_some(site_id)
883 })
884 .choose(&mut ctx.rng)
885 .map(|site_id| (Some(site_id), Actor::from(tgt_npc_id), distance))
886 })?,
887 };
888
889 let spot = get_nearest_spot(ctx, kind, ctx.npc.wpos.xy().wpos_to_cpos().as_());
890
891 if match kind.payload() {
895 Some(Payload::GnarlingCarving) => spot.is_none(),
896 Some(Payload::LegoomLeaf) | None => false,
897 } {
898 return None;
899 }
900
901 const ONE: NonZeroU32 = NonZeroU32::new(1).unwrap();
902 Some(CourierQuestInstance {
903 kind,
904 spot,
905 source_site: ctx.npc.current_site,
906 source_actor: Actor::from(ctx.npc_id),
907 target_actor,
908 target_site,
909 messenger,
910 distance: NonZeroU32::new(distance as u32).unwrap_or(ONE),
911 })
912}