overlord_event_system/async_handler/
quests.rs

1use crate::{
2    async_handler::handler::OverlordAsyncEventHandler, event::OverlordEvent,
3    game_config_helpers::GameConfigLookup, quests::make_quest_instance, state::OverlordState,
4};
5
6use essences::{
7    currency::CurrencySource,
8    quest::{QuestGroupType, QuestTemplate, QuestsTrackReward},
9};
10
11use event_system::{
12    event::EventPluginized, script::runner::ScriptRandom, system::EventHandleResult,
13};
14use uuid::Uuid;
15
16impl OverlordAsyncEventHandler {
17    pub fn handle_claim_quest(
18        &mut self,
19        quest_id: Uuid,
20        rand_gen: rand::rngs::StdRng,
21        mut state: OverlordState,
22    ) -> EventHandleResult<OverlordEvent, OverlordState> {
23        let Ok(quest) = self.get_quest(quest_id) else {
24            tracing::error!("Couldn't get quest with id = {} in config", quest_id);
25            return EventHandleResult::fail(state);
26        };
27
28        let Some(quest_instance) = state.quest_groups.find_in_non_patron(quest_id) else {
29            tracing::error!("Couldn't find quest with id = {} in state", quest_id);
30            return EventHandleResult::fail(state);
31        };
32
33        if quest.quest_group_type == QuestGroupType::PatronDaily
34            || quest.quest_group_type == QuestGroupType::PatronLifetime
35        {
36            tracing::error!("Quest with id = {} is a patron_quest", quest_id);
37            return EventHandleResult::fail(state);
38        }
39
40        if quest.quest_group_type == QuestGroupType::Hidden {
41            tracing::error!("Quest with id = {} is a hidden_quest", quest_id);
42            return EventHandleResult::fail(state);
43        }
44
45        if !quest_instance.is_completed(quest.progress_target) {
46            tracing::error!(
47                "Tried claiming quest with id = {}, that is not completed:\n Current: {}, Target: {}",
48                quest_id,
49                quest_instance.current,
50                quest.progress_target,
51            );
52            return EventHandleResult::fail(state);
53        }
54
55        if let Err(e) = state.quest_groups.mark_quest_claimed(quest_id) {
56            tracing::error!("Got error, trying to mark quest as claimed: {:?}", e);
57            return EventHandleResult::fail(state);
58        }
59
60        // TODO MAYBE tmp solution, because daily/weekly quests should be visible, even after claim
61        // state.quest_groups.retain_repeatable(quest_id);
62        state.quest_groups.retain_lifetime(quest_id);
63        state.quest_groups.reset_loop_task(quest_id);
64
65        match quest.quest_group_type {
66            QuestGroupType::Daily => {
67                state.quest_groups.daily.progress_track.current_points += quest.progression_points;
68            }
69            QuestGroupType::Weekly => {
70                state.quest_groups.weekly.progress_track.current_points += quest.progression_points;
71            }
72            QuestGroupType::Achievement => {
73                state
74                    .quest_groups
75                    .achievements
76                    .progress_track
77                    .current_points += quest.progression_points;
78            }
79            _ => {}
80        }
81
82        let mut events = vec![];
83
84        // Add bundle reward if quest has a bundle_id
85        if let Some(bundle_id) = quest.bundle_id {
86            events.push(EventPluginized::now(OverlordEvent::AddBundleGroup {
87                bundle_ids: vec![bundle_id],
88            }));
89        }
90
91        if !quest.next_quest_ids.is_empty() {
92            events.push(EventPluginized::now(OverlordEvent::NewQuests {
93                quest_ids: quest.next_quest_ids,
94            }));
95        }
96
97        if let Some(additional_quests_script) = quest.additional_quests_script {
98            match self.script_runner.run_event(
99                |mut scope_setter| {
100                    scope_setter.set_const("Random", ScriptRandom::new(rand_gen));
101                    scope_setter.set_const("CharacterState", state.character_state.clone());
102                    scope_setter
103                },
104                &state,
105                &additional_quests_script,
106            ) {
107                Ok(script_events) => {
108                    events.append(
109                        &mut script_events
110                            .into_iter()
111                            .map(EventPluginized::now)
112                            .collect(),
113                    );
114                }
115                Err(err) => {
116                    tracing::error!("Additional quests script failed with error: {err:?}");
117                    return EventHandleResult::fail(state);
118                }
119            }
120        }
121
122        EventHandleResult::ok_events(state, events)
123    }
124
125    pub fn handle_new_quests(
126        &self,
127        quest_ids: Vec<Uuid>,
128        mut state: OverlordState,
129    ) -> EventHandleResult<OverlordEvent, OverlordState> {
130        for quest_id in quest_ids {
131            let Ok(quest) = self.get_quest(quest_id) else {
132                tracing::error!("Couldn't get quest with id = {}", quest_id);
133                return EventHandleResult::fail(state);
134            };
135
136            if state.quest_groups.find_in_all(quest_id).is_some() {
137                tracing::warn!("Quest with id = {} is already in state, skipping", quest_id);
138                continue;
139            }
140
141            state.quest_groups.push(
142                &make_quest_instance(
143                    &quest,
144                    &state.character_state,
145                    &self.game_config.get(),
146                    &self.script_runner,
147                ),
148                &quest.quest_group_type,
149            );
150        }
151
152        EventHandleResult::ok(state)
153    }
154
155    pub fn handle_update_active_loop_task_id(
156        &self,
157        quest_id: Uuid,
158        mut state: OverlordState,
159    ) -> EventHandleResult<OverlordEvent, OverlordState> {
160        let resolved_id = if state
161            .quest_groups
162            .loop_tasks
163            .iter()
164            .any(|q| q.id == quest_id)
165        {
166            quest_id
167        } else {
168            tracing::warn!(
169                "Loop task quest_id={quest_id} not found in state.loop_tasks, falling back to default loop task from config"
170            );
171            let game_config = self.game_config.get();
172            let fallback_id = match self.script_runner.run_expression::<Uuid>(
173                |mut scope_setter| {
174                    scope_setter.set_const(
175                        "CustomValues",
176                        state.character_state.character.custom_values.clone(),
177                    );
178                    scope_setter
179                },
180                &game_config.game_settings.get_default_loop_task_id,
181            ) {
182                Ok(id) => id,
183                Err(err) => {
184                    tracing::error!("Failed to evaluate get_default_loop_task_id script: {err}");
185                    return EventHandleResult::fail(state);
186                }
187            };
188            if state
189                .quest_groups
190                .loop_tasks
191                .iter()
192                .any(|q| q.id == fallback_id)
193            {
194                fallback_id
195            } else if let Some(first_task) = state.quest_groups.loop_tasks.first() {
196                tracing::warn!(
197                    "Fallback quest {fallback_id} from config not found in state, \
198                     using first available loop task {}",
199                    first_task.id
200                );
201                first_task.id
202            } else {
203                tracing::error!("No loop tasks available in state");
204                return EventHandleResult::fail(state);
205            }
206        };
207
208        state.character_state.character.active_loop_task_id = Some(resolved_id);
209
210        EventHandleResult::ok(state)
211    }
212
213    pub fn handle_patron_quest_completed(
214        &self,
215        quest_id: Uuid,
216        mut state: OverlordState,
217    ) -> EventHandleResult<OverlordEvent, OverlordState> {
218        if state.patron.is_none() {
219            tracing::error!("Tried completing patron quest but there is no patron");
220            return EventHandleResult::fail(state);
221        }
222
223        let Ok(quest) = self.get_quest(quest_id) else {
224            tracing::error!("Couldn't get quest with id = {}", quest_id);
225            return EventHandleResult::fail(state);
226        };
227
228        let Some(quest_instance) = state.quest_groups.find_in_patron(quest_id) else {
229            tracing::error!("Couldn't find quest with id = {} in state", quest_id);
230            return EventHandleResult::fail(state);
231        };
232
233        if !(quest.quest_group_type == QuestGroupType::PatronDaily
234            || quest.quest_group_type == QuestGroupType::PatronLifetime)
235        {
236            tracing::error!("Quest with id = {} is not patron_quest", quest_id);
237            return EventHandleResult::fail(state);
238        }
239
240        if !quest_instance.is_completed(quest.progress_target) {
241            tracing::error!(
242                "Tried claiming quest with id = {}, that is not completed:\n Current: {}, Target: {}",
243                quest_id,
244                quest_instance.current,
245                quest.progress_target,
246            );
247            return EventHandleResult::fail(state);
248        }
249
250        state.quest_groups.retain_patron(quest.id);
251
252        let mut events = vec![];
253
254        if !quest.next_quest_ids.is_empty() {
255            events.push(EventPluginized::now(OverlordEvent::NewQuests {
256                quest_ids: quest.next_quest_ids,
257            }));
258        }
259
260        EventHandleResult::ok_events(state, events)
261    }
262
263    pub fn handle_hidden_quest_completed(
264        &self,
265        quest_id: Uuid,
266        rand_gen: rand::rngs::StdRng,
267        mut state: OverlordState,
268    ) -> EventHandleResult<OverlordEvent, OverlordState> {
269        let Ok(quest) = self.get_quest(quest_id) else {
270            tracing::error!("Couldn't get quest with id = {}", quest_id);
271            return EventHandleResult::fail(state);
272        };
273
274        let Some(quest_instance) = state.quest_groups.find_in_hidden(quest_id) else {
275            tracing::error!("Couldn't find quest with id = {} in state", quest_id);
276            return EventHandleResult::fail(state);
277        };
278
279        if quest.quest_group_type != QuestGroupType::Hidden {
280            tracing::error!("Quest with id = {} is not hidden", quest_id);
281            return EventHandleResult::fail(state);
282        }
283
284        if !quest_instance.is_completed(quest.progress_target) {
285            tracing::error!(
286                "Tried claiming quest with id = {}, that is not completed:\n Current: {}, Target: {}",
287                quest_id,
288                quest_instance.current,
289                quest.progress_target,
290            );
291            return EventHandleResult::fail(state);
292        }
293
294        state.quest_groups.retain_hidden(quest.id);
295
296        let mut events = vec![];
297
298        // Add bundle reward if quest has a bundle_id
299        if let Some(bundle_id) = quest.bundle_id {
300            events.push(EventPluginized::now(OverlordEvent::AddBundleGroup {
301                bundle_ids: vec![bundle_id],
302            }));
303        }
304
305        if !quest.next_quest_ids.is_empty() {
306            events.push(EventPluginized::now(OverlordEvent::NewQuests {
307                quest_ids: quest.next_quest_ids,
308            }));
309        }
310
311        if let Some(additional_quests_script) = quest.additional_quests_script {
312            match self.script_runner.run_event(
313                |mut scope_setter| {
314                    scope_setter.set_const("Random", ScriptRandom::new(rand_gen));
315                    scope_setter.set_const("CharacterState", state.character_state.clone());
316                    scope_setter
317                },
318                &state,
319                &additional_quests_script,
320            ) {
321                Ok(script_events) => {
322                    events.append(
323                        &mut script_events
324                            .into_iter()
325                            .map(EventPluginized::now)
326                            .collect(),
327                    );
328                }
329                Err(err) => {
330                    // TODOO should this fallback run always?
331                    tracing::error!("Additional quests script failed with error: {err:?}");
332                    return EventHandleResult::fail(state);
333                }
334            }
335        }
336
337        EventHandleResult::ok_events(state, events)
338    }
339
340    pub fn handle_claim_quest_progression_reward(
341        &self,
342        quest_group_type: QuestGroupType,
343        mut state: OverlordState,
344    ) -> EventHandleResult<OverlordEvent, OverlordState> {
345        let (current_points, rewards) = match quest_group_type {
346            QuestGroupType::Daily => (
347                state.quest_groups.daily.progress_track.current_points,
348                &mut state.quest_groups.daily.progress_track.rewards,
349            ),
350            QuestGroupType::Weekly => (
351                state.quest_groups.weekly.progress_track.current_points,
352                &mut state.quest_groups.weekly.progress_track.rewards,
353            ),
354            QuestGroupType::Achievement => (
355                state
356                    .quest_groups
357                    .achievements
358                    .progress_track
359                    .current_points,
360                &mut state.quest_groups.achievements.progress_track.rewards,
361            ),
362            _ => {
363                tracing::error!(
364                    "Tried claiming quest progression reward with quest_group_type = {quest_group_type:?}"
365                );
366                return EventHandleResult::fail(state);
367            }
368        };
369
370        let mut available_rewards: Vec<&mut QuestsTrackReward> = rewards
371            .iter_mut()
372            .filter(|x| !x.is_claimed && current_points >= x.points_required)
373            .collect();
374
375        if available_rewards.is_empty() {
376            tracing::error!(
377                "No available rewards found for current_points = {} and quest_group_type = {}",
378                current_points,
379                quest_group_type
380            );
381            return EventHandleResult::fail(state);
382        }
383
384        // For achievements, claim only the next (lowest) reward per call
385        let mut all_claimed_rewards = vec![];
386        if quest_group_type == QuestGroupType::Achievement {
387            available_rewards.sort_by_key(|r| r.points_required);
388            let reward = &mut available_rewards[0];
389            reward.is_claimed = true;
390            all_claimed_rewards.extend(reward.reward.iter().cloned());
391        } else {
392            for reward in available_rewards {
393                reward.is_claimed = true;
394                all_claimed_rewards.extend(reward.reward.iter().cloned());
395            }
396        }
397
398        let events = vec![Self::currency_increase(
399            &all_claimed_rewards,
400            CurrencySource::QuestsTrackReward,
401        )];
402
403        EventHandleResult::ok_events(state, events)
404    }
405
406    fn get_quest(&self, quest_id: Uuid) -> anyhow::Result<QuestTemplate> {
407        let game_config = self.game_config.get();
408
409        Ok(game_config.require_quest(quest_id)?.clone())
410    }
411
412    pub fn handle_reset_repeating_quests(
413        &self,
414        quest_ids: Vec<Uuid>,
415        mut state: OverlordState,
416    ) -> EventHandleResult<OverlordEvent, OverlordState> {
417        for quest_id in quest_ids {
418            let Some(quest) = state.quest_groups.find_in_repeatable_mut(quest_id) else {
419                tracing::error!(
420                    "Couldn't find repeatable quest with id = {}, in state",
421                    quest_id
422                );
423                return EventHandleResult::fail(state);
424            };
425
426            quest.current = 0;
427            quest.is_claimed = false;
428        }
429
430        EventHandleResult::ok(state)
431    }
432}