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