overlord_event_system/async_handler/
quests.rs1use 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 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 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 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 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 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}