overlord_event_system/logic/
quests.rs

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