overlord_event_system/logic/
quests.rs1use crate::{
2 event::OverlordEvent, game_config_helpers::GameConfigLookup, logic::handler::OverlordLogic,
3 quests::make_quest_instance, state::OverlordState,
4};
5
6use essences::{
7 currency::CurrencySource,
8 quest::{QuestGroupType, QuestTemplate, QuestsTrackReward},
9};
10
11use event_system::{event::EventPluginized, script::random::GameRng, system::EventHandleResult};
12use uuid::Uuid;
13
14impl OverlordLogic {
15 pub fn handle_claim_quest(
16 &mut self,
17 quest_id: Uuid,
18 rand_gen: rand::rngs::StdRng,
19 mut state: OverlordState,
20 ) -> EventHandleResult<OverlordEvent, OverlordState> {
21 let Ok(quest) = self.get_quest(quest_id) else {
22 tracing::error!("Couldn't get quest with id = {} in config", quest_id);
23 return EventHandleResult::fail(state);
24 };
25
26 let Some(quest_instance) = state.quest_groups.find_in_non_patron(quest_id) else {
27 tracing::error!("Couldn't find quest with id = {} in state", quest_id);
28 return EventHandleResult::fail(state);
29 };
30
31 if quest.quest_group_type == QuestGroupType::PatronDaily
32 || quest.quest_group_type == QuestGroupType::PatronLifetime
33 {
34 tracing::error!("Quest with id = {} is a patron_quest", quest_id);
35 return EventHandleResult::fail(state);
36 }
37
38 if quest.quest_group_type == QuestGroupType::Hidden {
39 tracing::error!("Quest with id = {} is a hidden_quest", quest_id);
40 return EventHandleResult::fail(state);
41 }
42
43 if !quest_instance.is_completed(quest.progress_target) {
44 tracing::error!(
45 "Tried claiming quest with id = {}, that is not completed:\n Current: {}, Target: {}",
46 quest_id,
47 quest_instance.current,
48 quest.progress_target,
49 );
50 return EventHandleResult::fail(state);
51 }
52
53 if let Err(e) = state.quest_groups.mark_quest_claimed(quest_id) {
54 tracing::error!("Got error, trying to mark quest as claimed: {:?}", e);
55 return EventHandleResult::fail(state);
56 }
57
58 state.quest_groups.retain_lifetime(quest_id);
61 state.quest_groups.reset_loop_task(quest_id);
62
63 let previous_points = match quest.quest_group_type {
64 QuestGroupType::Daily => state.quest_groups.daily.progress_track.current_points,
65 QuestGroupType::Weekly => state.quest_groups.weekly.progress_track.current_points,
66 QuestGroupType::Achievement => {
67 state
68 .quest_groups
69 .achievements
70 .progress_track
71 .current_points
72 }
73 _ => 0,
74 };
75
76 match quest.quest_group_type {
77 QuestGroupType::Daily => {
78 state.quest_groups.daily.progress_track.current_points += quest.progression_points;
79 }
80 QuestGroupType::Weekly => {
81 state.quest_groups.weekly.progress_track.current_points += quest.progression_points;
82 }
83 QuestGroupType::Achievement => {
84 state
85 .quest_groups
86 .achievements
87 .progress_track
88 .current_points += quest.progression_points;
89 }
90 _ => {}
91 }
92
93 snapshot_core_progression_thresholds(
94 &self.game_config.get(),
95 quest.quest_group_type,
96 previous_points,
97 &mut state,
98 );
99
100 let mut events = vec![];
101
102 if let Some(bundle_id) = quest.bundle_id {
104 events.push(EventPluginized::now(OverlordEvent::AddBundleGroup {
105 bundle_ids: vec![bundle_id],
106 source: essences::currency::CurrencySource::QuestClaim,
107 }));
108 }
109
110 if !quest.next_quest_ids.is_empty() {
111 events.push(EventPluginized::now(OverlordEvent::NewQuests {
112 quest_ids: quest.next_quest_ids,
113 }));
114 }
115
116 if let Some(native_name) = quest.additional_quests_behavior.as_deref() {
117 match self.run_additional_quests(native_name, rand_gen, &state.character_state) {
118 Ok(mut script_events) => {
119 events.append(&mut script_events.drain(..).map(EventPluginized::now).collect());
120 }
121 Err(err) => {
122 tracing::error!("Additional quests script failed with error: {err:?}");
123 return EventHandleResult::fail(state);
124 }
125 }
126 }
127
128 EventHandleResult::ok_events(state, events)
129 }
130
131 fn run_additional_quests(
134 &self,
135 native_name: &str,
136 rand_gen: rand::rngs::StdRng,
137 character_state: &essences::character_state::CharacterState,
138 ) -> anyhow::Result<Vec<OverlordEvent>> {
139 let Some(f) = self.behaviors.additional_quests_fn(native_name) else {
140 anyhow::bail!("No registered additional_quests native fn named {native_name}");
141 };
142 let game_config = self.game_config.get();
143 let rng = GameRng::new(rand_gen);
144 f(&crate::behaviors::quests::loop_tasks::AdditionalQuestsCtx {
145 character_state,
146 rng: &rng,
147 config: &game_config,
148 lookups: self.behaviors.lookups(),
149 })
150 }
151
152 pub fn handle_new_quests(
153 &self,
154 quest_ids: Vec<Uuid>,
155 mut state: OverlordState,
156 ) -> EventHandleResult<OverlordEvent, OverlordState> {
157 for quest_id in quest_ids {
158 let Ok(quest) = self.get_quest(quest_id) else {
159 tracing::error!("Couldn't get quest with id = {}", quest_id);
160 return EventHandleResult::fail(state);
161 };
162
163 if state.quest_groups.find_in_all(quest_id).is_some() {
164 tracing::warn!("Quest with id = {} is already in state, skipping", quest_id);
165 continue;
166 }
167
168 state.quest_groups.push(
169 &make_quest_instance(
170 &quest,
171 &state.character_state,
172 &self.game_config.get(),
173 &self.behaviors,
174 ),
175 &quest.quest_group_type,
176 );
177 }
178
179 EventHandleResult::ok(state)
180 }
181
182 pub fn handle_update_active_loop_task_id(
183 &self,
184 quest_id: Uuid,
185 mut state: OverlordState,
186 ) -> EventHandleResult<OverlordEvent, OverlordState> {
187 let resolved_id = if state
188 .quest_groups
189 .loop_tasks
190 .iter()
191 .any(|q| q.id == quest_id)
192 {
193 quest_id
194 } else {
195 tracing::warn!(
196 "Loop task quest_id={quest_id} not found in state.loop_tasks, falling back to default loop task from config"
197 );
198 let Some(f) = self
199 .behaviors
200 .default_loop_task_fn("default_loop_task_const")
201 else {
202 tracing::error!(
203 "No registered default_loop_task native fn named default_loop_task_const"
204 );
205 return EventHandleResult::fail(state);
206 };
207 let fallback_id = match f(&crate::behaviors::quests::loop_tasks::DefaultLoopTaskCtx) {
208 Ok(id) => id,
209 Err(err) => {
210 tracing::error!("Failed to evaluate default_loop_task native fn: {err}");
211 return EventHandleResult::fail(state);
212 }
213 };
214 if state
215 .quest_groups
216 .loop_tasks
217 .iter()
218 .any(|q| q.id == fallback_id)
219 {
220 fallback_id
221 } else if let Some(first_task) = state.quest_groups.loop_tasks.first() {
222 tracing::warn!(
223 "Fallback quest {fallback_id} from config not found in state, \
224 using first available loop task {}",
225 first_task.id
226 );
227 first_task.id
228 } else {
229 tracing::error!("No loop tasks available in state");
230 return EventHandleResult::fail(state);
231 }
232 };
233
234 state.character_state.character.active_loop_task_id = Some(resolved_id);
235
236 EventHandleResult::ok(state)
237 }
238
239 pub fn handle_patron_quest_completed(
240 &self,
241 quest_id: Uuid,
242 mut state: OverlordState,
243 ) -> EventHandleResult<OverlordEvent, OverlordState> {
244 if state.patron.is_none() {
245 tracing::error!("Tried completing patron quest but there is no patron");
246 return EventHandleResult::fail(state);
247 }
248
249 let Ok(quest) = self.get_quest(quest_id) else {
250 tracing::error!("Couldn't get quest with id = {}", quest_id);
251 return EventHandleResult::fail(state);
252 };
253
254 let Some(quest_instance) = state.quest_groups.find_in_patron(quest_id) else {
255 tracing::error!("Couldn't find quest with id = {} in state", quest_id);
256 return EventHandleResult::fail(state);
257 };
258
259 if !(quest.quest_group_type == QuestGroupType::PatronDaily
260 || quest.quest_group_type == QuestGroupType::PatronLifetime)
261 {
262 tracing::error!("Quest with id = {} is not patron_quest", quest_id);
263 return EventHandleResult::fail(state);
264 }
265
266 if !quest_instance.is_completed(quest.progress_target) {
267 tracing::error!(
268 "Tried claiming quest with id = {}, that is not completed:\n Current: {}, Target: {}",
269 quest_id,
270 quest_instance.current,
271 quest.progress_target,
272 );
273 return EventHandleResult::fail(state);
274 }
275
276 state.quest_groups.retain_patron(quest.id);
277
278 let mut events = vec![];
279
280 if !quest.next_quest_ids.is_empty() {
281 events.push(EventPluginized::now(OverlordEvent::NewQuests {
282 quest_ids: quest.next_quest_ids,
283 }));
284 }
285
286 EventHandleResult::ok_events(state, events)
287 }
288
289 pub fn handle_hidden_quest_completed(
290 &self,
291 quest_id: Uuid,
292 rand_gen: rand::rngs::StdRng,
293 mut state: OverlordState,
294 ) -> EventHandleResult<OverlordEvent, OverlordState> {
295 let Ok(quest) = self.get_quest(quest_id) else {
296 tracing::error!("Couldn't get quest with id = {}", quest_id);
297 return EventHandleResult::fail(state);
298 };
299
300 let Some(quest_instance) = state.quest_groups.find_in_hidden(quest_id) else {
301 tracing::error!("Couldn't find quest with id = {} in state", quest_id);
302 return EventHandleResult::fail(state);
303 };
304
305 if quest.quest_group_type != QuestGroupType::Hidden {
306 tracing::error!("Quest with id = {} is not hidden", quest_id);
307 return EventHandleResult::fail(state);
308 }
309
310 if !quest_instance.is_completed(quest.progress_target) {
311 tracing::error!(
312 "Tried claiming quest with id = {}, that is not completed:\n Current: {}, Target: {}",
313 quest_id,
314 quest_instance.current,
315 quest.progress_target,
316 );
317 return EventHandleResult::fail(state);
318 }
319
320 state.quest_groups.retain_hidden(quest.id);
321
322 let mut events = vec![];
323
324 if let Some(bundle_id) = quest.bundle_id {
326 events.push(EventPluginized::now(OverlordEvent::AddBundleGroup {
327 bundle_ids: vec![bundle_id],
328 source: essences::currency::CurrencySource::QuestClaim,
329 }));
330 }
331
332 if !quest.next_quest_ids.is_empty() {
333 events.push(EventPluginized::now(OverlordEvent::NewQuests {
334 quest_ids: quest.next_quest_ids,
335 }));
336 }
337
338 if let Some(native_name) = quest.additional_quests_behavior.as_deref() {
339 match self.run_additional_quests(native_name, rand_gen, &state.character_state) {
340 Ok(mut script_events) => {
341 events.append(&mut script_events.drain(..).map(EventPluginized::now).collect());
342 }
343 Err(err) => {
344 tracing::error!("Additional quests script failed with error: {err:?}");
345 return EventHandleResult::fail(state);
346 }
347 }
348 }
349
350 EventHandleResult::ok_events(state, events)
351 }
352
353 pub fn handle_claim_quest_progression_reward(
354 &self,
355 quest_group_type: QuestGroupType,
356 mut state: OverlordState,
357 ) -> EventHandleResult<OverlordEvent, OverlordState> {
358 let (current_points, rewards) = match quest_group_type {
359 QuestGroupType::Daily => (
360 state.quest_groups.daily.progress_track.current_points,
361 &mut state.quest_groups.daily.progress_track.rewards,
362 ),
363 QuestGroupType::Weekly => (
364 state.quest_groups.weekly.progress_track.current_points,
365 &mut state.quest_groups.weekly.progress_track.rewards,
366 ),
367 QuestGroupType::Achievement => (
368 state
369 .quest_groups
370 .achievements
371 .progress_track
372 .current_points,
373 &mut state.quest_groups.achievements.progress_track.rewards,
374 ),
375 _ => {
376 tracing::error!(
377 "Tried claiming quest progression reward with quest_group_type = {quest_group_type:?}"
378 );
379 return EventHandleResult::fail(state);
380 }
381 };
382
383 let mut available_rewards: Vec<&mut QuestsTrackReward> = rewards
384 .iter_mut()
385 .filter(|x| !x.is_claimed && current_points >= x.points_required)
386 .collect();
387
388 if available_rewards.is_empty() {
389 tracing::error!(
390 "No available rewards found for current_points = {} and quest_group_type = {}",
391 current_points,
392 quest_group_type
393 );
394 return EventHandleResult::fail(state);
395 }
396
397 let mut all_claimed_rewards = vec![];
399 if quest_group_type == QuestGroupType::Achievement {
400 available_rewards.sort_by_key(|r| r.points_required);
401 let reward = &mut available_rewards[0];
402 reward.is_claimed = true;
403 all_claimed_rewards.extend(reward.reward.iter().cloned());
404 } else {
405 for reward in available_rewards {
406 reward.is_claimed = true;
407 all_claimed_rewards.extend(reward.reward.iter().cloned());
408 }
409 }
410
411 let events = vec![Self::currency_increase(
412 &all_claimed_rewards,
413 CurrencySource::QuestsTrackReward,
414 )];
415
416 EventHandleResult::ok_events(state, events)
417 }
418
419 fn get_quest(&self, quest_id: Uuid) -> anyhow::Result<QuestTemplate> {
420 let game_config = self.game_config.get();
421
422 Ok(game_config.require_quest(quest_id)?.clone())
423 }
424
425 pub fn handle_reset_repeating_quests(
426 &self,
427 quest_ids: Vec<Uuid>,
428 mut state: OverlordState,
429 ) -> EventHandleResult<OverlordEvent, OverlordState> {
430 use crate::mechanics::quest_board::is_board_managed;
431
432 let game_config = self.game_config.get();
433
434 for quest_id in &quest_ids {
441 if let Some(quest) = state.quest_groups.find_in_repeatable_mut(*quest_id) {
442 quest.current = 0;
443 quest.is_claimed = false;
444 continue;
445 }
446 let Some(tpl) = game_config.quest(*quest_id) else {
447 tracing::warn!("ResetRepeatingQuests: quest {quest_id} not in config, skipping");
448 continue;
449 };
450 if is_board_managed(tpl) {
451 state.quest_groups.push(
452 &make_quest_instance(
453 tpl,
454 &state.character_state,
455 &game_config,
456 &self.behaviors,
457 ),
458 &tpl.quest_group_type,
459 );
460 } else {
461 tracing::warn!("ResetRepeatingQuests: no instance for quest {quest_id}");
465 }
466 }
467
468 let listed: std::collections::HashSet<Uuid> = quest_ids.iter().copied().collect();
472 for group in [QuestGroupType::Daily, QuestGroupType::Weekly] {
473 let covers_group = quest_ids.iter().any(|id| {
474 game_config
475 .quest(*id)
476 .is_some_and(|t| t.quest_group_type == group && is_board_managed(t))
477 });
478 if !covers_group {
479 continue;
480 }
481 let instances = match group {
482 QuestGroupType::Daily => &mut state.quest_groups.daily.quests,
483 _ => &mut state.quest_groups.weekly.quests,
484 };
485 instances.retain(|q| {
486 listed.contains(&q.id)
487 || game_config.quest(q.id).is_none_or(|t| !is_board_managed(t))
488 });
489 }
490
491 for group in [QuestGroupType::Daily, QuestGroupType::Weekly] {
499 let group_flipped = quest_ids.iter().any(|id| {
500 game_config
501 .quest(*id)
502 .is_some_and(|t| t.quest_group_type == group)
503 });
504 if !group_flipped {
505 continue;
506 }
507 let track = match group {
508 QuestGroupType::Daily => &mut state.quest_groups.daily.progress_track,
509 _ => &mut state.quest_groups.weekly.progress_track,
510 };
511 track.current_points = 0;
512 for reward in &mut track.rewards {
513 reward.is_claimed = false;
514 }
515 }
516
517 EventHandleResult::ok(state)
518 }
519}
520
521pub fn snapshot_core_progression_thresholds(
526 game_config: &configs::game_config::GameConfig,
527 quest_group_type: QuestGroupType,
528 previous_points: u64,
529 state: &mut OverlordState,
530) {
531 let track = match quest_group_type {
532 QuestGroupType::Daily => &mut state.quest_groups.daily.progress_track,
533 QuestGroupType::Weekly => &mut state.quest_groups.weekly.progress_track,
534 QuestGroupType::Achievement => &mut state.quest_groups.achievements.progress_track,
535 _ => return,
536 };
537 let current_points = track.current_points;
538 let core_id = game_config.cores_settings.upgrade_currency_id;
539 let core_unlocked = state.character_state.character.current_chapter_level
540 >= game_config.cores_settings.unlock_chapter;
541
542 for rung in &mut track.rewards {
543 if rung.points_required <= previous_points || rung.points_required > current_points {
544 continue;
545 }
546 if let Some(core) = rung
547 .reward
548 .iter_mut()
549 .find(|unit| unit.currency_id == core_id)
550 && !core_unlocked
551 {
552 core.amount = 0;
553 }
554 }
555}