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 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 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 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 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 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}