overlord_event_system/logic/
cheats.rs

1use crate::{
2    cases::try_finalize_item,
3    entities::create_pve_entity,
4    event::{Cheat, OverlordEvent, PrepareFightType},
5    gacha::item_case::generate_item_from_template,
6    game_config_helpers::GameConfigLookup,
7    logic::handler::OverlordLogic,
8    state::OverlordState,
9};
10
11use configs::cheats::CheatScriptId;
12use essences::{
13    abilities::AbilityShard,
14    entity::{ActionWithDeadline, Coordinates, EntityAttributes},
15    fighting::{EntityType, FightEntity, FightTemplateId},
16    items::ItemTemplateId,
17    skins::SkinId,
18};
19use essences::{
20    currency::{CurrencySource, CurrencyUnit},
21    fighting::EntityTeam,
22    game::EntityTemplateId,
23};
24use event_system::{event::EventPluginized, system::EventHandleResult};
25use rand::Rng;
26
27impl OverlordLogic {
28    pub fn handle_run_cheat(
29        &mut self,
30        cheat: &Cheat,
31        current_tick: u64,
32        rand_gen: rand::rngs::StdRng,
33        state: OverlordState,
34    ) -> EventHandleResult<OverlordEvent, OverlordState> {
35        match cheat {
36            Cheat::PauseCombat => self.handle_cheat_pause_combat(state),
37            Cheat::UnpauseCombat => self.handle_cheat_unpause_combat(state),
38            Cheat::GodModeOn => self.handle_cheat_god_mode(state),
39            Cheat::GodModeOff => self.handle_cheat_god_mode_off(state),
40            Cheat::GetRich => self.handle_cheat_get_rich(state),
41            Cheat::StartFight { templated_id } => {
42                self.handle_cheat_start_fight(*templated_id, state)
43            }
44            Cheat::SpawnEntity {
45                entity_id,
46                x,
47                y,
48                team,
49                attributes,
50            } => self.handle_cheat_spawn_entity(
51                *entity_id,
52                *x,
53                *y,
54                team.clone(),
55                attributes,
56                current_tick,
57                state,
58                rand_gen,
59            ),
60            Cheat::WearItems { items_ids } => {
61                self.handle_cheat_wear_equipment_set(items_ids, state, rand_gen)
62            }
63            Cheat::ClearInventory => self.handle_cheat_clear_inventory(state),
64            Cheat::ClearSlot { item_id } => self.handle_cheat_clear_slot(*item_id, state),
65            Cheat::GetAllSpells => self.handle_cheat_get_all_spells(state),
66            Cheat::SetChapter { chapter } => self.handle_cheat_set_chapter(*chapter, state),
67            Cheat::EquipSkin { skin_id } => self.handle_cheat_equip_skin(*skin_id, state),
68            Cheat::UnequipSkin { skin_id } => self.handle_cheat_unequip_skin(*skin_id, state),
69            Cheat::NewLevel { level } => self.handle_cheat_new_level(*level, state),
70            Cheat::Script { script_id } => self.handle_cheat_script(*script_id, rand_gen, state),
71        }
72    }
73
74    fn handle_cheat_pause_combat(
75        &self,
76        mut state: OverlordState,
77    ) -> EventHandleResult<OverlordEvent, OverlordState> {
78        if let Some(fight) = &mut state.active_fight {
79            fight.paused = true;
80        }
81
82        EventHandleResult::ok(state)
83    }
84
85    fn handle_cheat_unpause_combat(
86        &self,
87        mut state: OverlordState,
88    ) -> EventHandleResult<OverlordEvent, OverlordState> {
89        if let Some(fight) = &mut state.active_fight {
90            fight.paused = false;
91        }
92
93        EventHandleResult::ok(state)
94    }
95
96    fn handle_cheat_god_mode(
97        &mut self,
98        mut state: OverlordState,
99    ) -> EventHandleResult<OverlordEvent, OverlordState> {
100        let Some(active_fight) = &mut state.active_fight else {
101            return EventHandleResult::ok(state);
102        };
103
104        let Some(entity) = active_fight
105            .entities
106            .iter_mut()
107            .find(|e| e.id == active_fight.player_id)
108        else {
109            tracing::error!(
110                "Couldn't find player entity with id = {}",
111                active_fight.player_id
112            );
113            return EventHandleResult::fail(state);
114        };
115
116        entity.attributes.set("godmode", 1);
117
118        EventHandleResult::ok(state)
119    }
120
121    fn handle_cheat_god_mode_off(
122        &mut self,
123        mut state: OverlordState,
124    ) -> EventHandleResult<OverlordEvent, OverlordState> {
125        let Some(active_fight) = &mut state.active_fight else {
126            return EventHandleResult::ok(state);
127        };
128
129        let Some(entity) = active_fight
130            .entities
131            .iter_mut()
132            .find(|e| e.id == active_fight.player_id)
133        else {
134            tracing::error!(
135                "Couldn't find player entity with id = {}",
136                active_fight.player_id
137            );
138            return EventHandleResult::fail(state);
139        };
140
141        entity.attributes.set("godmode", 0);
142
143        EventHandleResult::ok(state)
144    }
145
146    fn handle_cheat_get_rich(
147        &mut self,
148        state: OverlordState,
149    ) -> EventHandleResult<OverlordEvent, OverlordState> {
150        let game_config = self.game_config.get();
151
152        let currencies: Vec<CurrencyUnit> = game_config
153            .currencies
154            .iter()
155            .map(|currency| CurrencyUnit {
156                currency_id: currency.id,
157                amount: 1000000,
158            })
159            .collect();
160
161        let events = vec![Self::currency_increase(&currencies, CurrencySource::Cheat)];
162        EventHandleResult::ok_events(state, events)
163    }
164
165    fn handle_cheat_clear_inventory(
166        &mut self,
167        mut state: OverlordState,
168    ) -> EventHandleResult<OverlordEvent, OverlordState> {
169        // Remove all items from inventory
170        state.character_state.inventory.clear();
171
172        EventHandleResult::ok(state)
173    }
174
175    fn handle_cheat_clear_slot(
176        &mut self,
177        item_id: uuid::Uuid,
178        mut state: OverlordState,
179    ) -> EventHandleResult<OverlordEvent, OverlordState> {
180        let Some(inv_item_idx) = state
181            .character_state
182            .inventory
183            .iter()
184            .position(|x| x.id == item_id)
185        else {
186            tracing::error!("Tried clearing undefined item: item_id={}", item_id);
187            return EventHandleResult::fail(state);
188        };
189
190        state.character_state.inventory.remove(inv_item_idx);
191
192        EventHandleResult::ok(state)
193    }
194
195    fn handle_cheat_get_all_spells(
196        &mut self,
197        mut state: OverlordState,
198    ) -> EventHandleResult<OverlordEvent, OverlordState> {
199        let game_config = self.game_config.get();
200
201        // Collect all gacha abilities with 1 shard each
202        let ability_shards: Vec<AbilityShard> = game_config
203            .abilities
204            .iter()
205            .filter(|ability| ability.is_gacha_ability)
206            .map(|ability| AbilityShard {
207                ability_id: ability.id,
208                shards_amount: 1,
209            })
210            .collect();
211
212        // Update existing abilities or add new ones
213        for shard in &ability_shards {
214            if let Some(existing_ability) = state
215                .character_state
216                .all_abilities
217                .iter_mut()
218                .find(|a| a.template_id == shard.ability_id)
219            {
220                // Add shards to existing ability
221                existing_ability.shards_amount += shard.shards_amount;
222            } else {
223                // This is a new ability, we need to create it
224                let Some(template) = game_config.ability_template(shard.ability_id) else {
225                    tracing::error!(
226                        "Failed to find ability template with id={}",
227                        shard.ability_id
228                    );
229                    continue;
230                };
231
232                let new_ability = essences::abilities::Ability::from_template(template, None, None);
233                state.character_state.all_abilities.push(new_ability);
234            }
235        }
236
237        EventHandleResult::ok(state)
238    }
239
240    fn handle_cheat_set_chapter(
241        &mut self,
242        chapter: i64,
243        mut state: OverlordState,
244    ) -> EventHandleResult<OverlordEvent, OverlordState> {
245        let game_config = self.game_config.get();
246
247        let prev_chapter_level = state.character_state.character.current_chapter_level;
248        state.character_state.character.current_chapter_level = chapter;
249        state.character_state.character.current_fight_number = 0;
250        state.character_state.character.last_boss_fight_won = true;
251
252        let Ok(current_chapter) = game_config
253            .require_chapter_by_level(state.character_state.character.current_chapter_level)
254            .cloned()
255        else {
256            tracing::error!(
257                "Failed to get chapter with chapter_level={}",
258                state.character_state.character.current_chapter_level
259            );
260            return EventHandleResult::fail(state);
261        };
262
263        let prepare_fight_delay_ticks =
264            self.get_prepare_fight_delay(true, &current_chapter, &state);
265
266        let mut events = vec![];
267        if self.should_reset_afk_timer_on_gating_unlock(prev_chapter_level, &state) {
268            events.push(EventPluginized::now(
269                OverlordEvent::AfkRewardsGatingUnlocked {},
270            ));
271        }
272        self.fight_clock.schedule(
273            OverlordEvent::PrepareFight {
274                prepare_fight_type: PrepareFightType::PVEFight,
275            },
276            prepare_fight_delay_ticks,
277        );
278
279        EventHandleResult::ok_events(state, events)
280    }
281
282    fn handle_cheat_start_fight(
283        &mut self,
284        fight_templated_id: FightTemplateId,
285        state: OverlordState,
286    ) -> EventHandleResult<OverlordEvent, OverlordState> {
287        let game_config = self.game_config.get();
288
289        self.fight_clock.schedule(
290            OverlordEvent::PrepareFight {
291                prepare_fight_type: PrepareFightType::SingleFight { fight_templated_id },
292            },
293            game_config
294                .fight_settings
295                .prepare_fight_win_delay_ticks_default,
296        );
297
298        EventHandleResult::ok(state)
299    }
300
301    #[allow(clippy::too_many_arguments)]
302    fn handle_cheat_spawn_entity(
303        &mut self,
304        entity_template_id: EntityTemplateId,
305        x: i64,
306        y: i64,
307        team: EntityTeam,
308        attributes: &Vec<(String, i64)>,
309        current_tick: u64,
310        mut state: OverlordState,
311        mut rand_gen: rand::rngs::StdRng,
312    ) -> EventHandleResult<OverlordEvent, OverlordState> {
313        let game_config = self.game_config.get();
314
315        let Some(active_fight) = &mut state.active_fight else {
316            return EventHandleResult::ok(state);
317        };
318
319        let entity_id = uuid::Builder::from_random_bytes(rand_gen.random()).into_uuid();
320
321        if active_fight
322            .entities
323            .iter()
324            .any(|entity| entity.id == entity_id)
325        {
326            tracing::error!("There is already an entity with id: {entity_id}");
327            return EventHandleResult::fail(state);
328        }
329
330        let Some(player) = active_fight.get_player() else {
331            tracing::error!("No player in fight");
332            return EventHandleResult::fail(state);
333        };
334
335        let position = Coordinates {
336            x: player.coordinates.x + x,
337            y: (player.coordinates.y + y).clamp(0, 2),
338        };
339
340        let fight_entity = FightEntity {
341            entity_type: EntityType::PVEEntity { entity_template_id },
342            position,
343            has_big_hp_bar: false,
344            team,
345        };
346
347        let mut entity_attributes = EntityAttributes::default();
348
349        for (key, value) in attributes {
350            entity_attributes.add(key, *value);
351        }
352
353        let mut created_entity = match create_pve_entity(
354            entity_id,
355            &fight_entity,
356            &game_config,
357            Some(entity_attributes),
358        ) {
359            Ok(entity) => entity,
360            Err(err) => {
361                tracing::error!("Couldn't create entity: {}", err.to_string());
362                return EventHandleResult::fail(state);
363            }
364        };
365
366        created_entity.abilities.iter().for_each(|ability| {
367            let cooldown = game_config
368                .ability_template(ability.ability.template_id)
369                .map(|t| t.cooldown)
370                .unwrap_or(0);
371            created_entity.actions_queue.push(&ActionWithDeadline {
372                action: self
373                    .make_start_cast_ability_action(created_entity.id, ability.ability.template_id),
374                deadline_tick: current_tick + cooldown,
375            })
376        });
377
378        active_fight.entities.push(created_entity);
379
380        EventHandleResult::ok(state)
381    }
382
383    fn handle_cheat_wear_equipment_set(
384        &mut self,
385        items_ids: &Vec<ItemTemplateId>,
386        mut state: OverlordState,
387        mut rand_gen: rand::rngs::StdRng,
388    ) -> EventHandleResult<OverlordEvent, OverlordState> {
389        let game_config = self.game_config.get();
390
391        state.character_state.inventory.clear();
392
393        for item_id in items_ids {
394            let item_template = game_config
395                .require_item_template(*item_id)
396                .unwrap_or_else(|_| panic!("Failed to get item with id={item_id}"));
397
398            let rarity = game_config
399                .require_item_rarity(item_template.rarity_id)
400                .unwrap_or_else(|_| {
401                    panic!("Failed to get rarity with id={}", item_template.rarity_id)
402                })
403                .clone();
404
405            let mut item = generate_item_from_template(
406                item_template,
407                rarity,
408                state.character_state.character.character_level,
409                &game_config,
410                &mut rand_gen,
411            );
412
413            let mut finalized_item =
414                match try_finalize_item(&mut item, &game_config, &self.behaviors) {
415                    Ok(()) => item,
416                    Err(e) => {
417                        tracing::error!("Failed to finalize item: {}", e);
418                        return EventHandleResult::fail(state);
419                    }
420                };
421
422            finalized_item.is_equipped = true;
423
424            state.character_state.inventory.push(finalized_item);
425        }
426
427        EventHandleResult::ok(state)
428    }
429
430    fn handle_cheat_equip_skin(
431        &mut self,
432        skin_id: SkinId,
433        mut state: OverlordState,
434    ) -> EventHandleResult<OverlordEvent, OverlordState> {
435        let game_config = self.game_config.get();
436
437        let Some(config_skin) = game_config.skin(skin_id) else {
438            tracing::error!("Failed to find skin with id: {skin_id}");
439            return EventHandleResult::fail(state);
440        };
441
442        let new_skin_type = config_skin.skin_type;
443
444        if let Some(pos) = state
445            .character_state
446            .character_skins
447            .equipped
448            .iter()
449            .position(|&s| {
450                game_config
451                    .skin(s)
452                    .is_some_and(|cs| cs.skin_type == new_skin_type)
453            })
454        {
455            let old_skin_id = state.character_state.character_skins.equipped.remove(pos);
456            state
457                .character_state
458                .character_skins
459                .available
460                .push(old_skin_id);
461        }
462
463        state.character_state.character_skins.equipped.push(skin_id);
464
465        EventHandleResult::ok(state)
466    }
467
468    fn handle_cheat_unequip_skin(
469        &mut self,
470        skin_id: SkinId,
471        mut state: OverlordState,
472    ) -> EventHandleResult<OverlordEvent, OverlordState> {
473        if let Some(pos) = state
474            .character_state
475            .character_skins
476            .equipped
477            .iter()
478            .position(|&s| s == skin_id)
479        {
480            state.character_state.character_skins.equipped.remove(pos);
481            state
482                .character_state
483                .character_skins
484                .available
485                .push(skin_id);
486        }
487
488        EventHandleResult::ok(state)
489    }
490
491    fn handle_cheat_new_level(
492        &mut self,
493        level: i64,
494        mut state: OverlordState,
495    ) -> EventHandleResult<OverlordEvent, OverlordState> {
496        state.character_state.character.character_level = level;
497        EventHandleResult::ok(state)
498    }
499
500    /// Native port of the dev-only `Cheat::Script` cheats.
501    ///
502    /// `BehaviorRegistry::run_event` (scoped with `CharacterState`/`Random`). The four
503    /// deployed cheat bodies use only loop-task building blocks that already have
504    /// native implementations, so we dispatch on `script_id` and reproduce the
505    /// produced. No new mechanics — purely a faithful 1:1 port of the shipped
506    /// bodies in `overlord/admin/config/scripts/content.templates.cheat_scripts.*`.
507    fn handle_cheat_script(
508        &mut self,
509        script_id: CheatScriptId,
510        rand_gen: rand::rngs::StdRng,
511        state: OverlordState,
512    ) -> EventHandleResult<OverlordEvent, OverlordState> {
513        let game_config = self.game_config.get();
514
515        let Some(_cheat_script) = game_config.cheat_script(script_id) else {
516            tracing::error!("Failed to find cheat_script with id: {script_id}");
517            return EventHandleResult::fail(state);
518        };
519
520        // The shipped UUID literals are well-formed; `expect` on a literal mirrors
521        let parse_uuid =
522            |s: &str| uuid::Uuid::parse_str(s).expect("valid cheat-script uuid literal");
523
524        let mut events: Vec<OverlordEvent> = Vec::new();
525
526        match script_id.to_string().as_str() {
527            // 019be635: give_quests([...]) + prepare_loop(RNG) + advance_loop.
528            // `give_quests([..])` maps to a single `NewQuests` carrying the whole
529            // list (matches the old `register_fn("give_quests", ..)`), then the
530            // loop-task prepare/advance pair (prepare consumes RNG).
531            "019be635-9bc8-7a67-b7df-2cc01c2001c5" => {
532                let quest_ids = [
533                    "019a4b43-be66-76d6-838f-153a717f82ff",
534                    "019a6ef3-740e-7150-b795-b0cbd681ceef",
535                    "019b561c-4f34-76c2-8a53-7b55667e7aea",
536                    "019b561e-5450-7d29-ae94-d2ddc340030d",
537                    "019b561f-bea7-71cc-834f-0909dead9d4b",
538                    "019b5621-6475-794b-b850-502d8e124422",
539                    "019bc1e6-394d-7c7e-b70c-55764b7455cd",
540                    "019bc29c-572e-7c7d-a9e3-8b10a6d28550",
541                    "019bc29c-b647-7288-a6ba-668bd37e1c5b",
542                    "019bdb53-7ed5-731c-a056-06df0b628065",
543                    "019be138-4fee-7ad6-a1a3-7b1539a7bf69",
544                    "019be13b-ba5f-71b1-8982-ca1734a7367b",
545                ]
546                .into_iter()
547                .map(parse_uuid)
548                .collect::<Vec<_>>();
549                events.push(OverlordEvent::NewQuests { quest_ids });
550
551                let rng = event_system::script::random::GameRng::new(rand_gen);
552                crate::mechanics::loop_tasks::prepare_loop(
553                    &mut events,
554                    &game_config,
555                    &state.character_state,
556                    &rng,
557                );
558                crate::mechanics::loop_tasks::advance_loop(
559                    &mut events,
560                    &game_config,
561                    self.behaviors.lookups(),
562                    &state.character_state,
563                );
564            }
565
566            // 019bea59: loop_tasks::advance_loop(Result, CharacterState).
567            "019bea59-c643-7a11-8985-f02ba314bfd6" => {
568                crate::mechanics::loop_tasks::advance_loop(
569                    &mut events,
570                    &game_config,
571                    self.behaviors.lookups(),
572                    &state.character_state,
573                );
574            }
575
576            // 019bfca8: push OverlordEventCustomEvent("CompleteAllLoopTasks", CustomEventData()).
577            "019bfca8-953e-76c5-8b33-d83662f9dbb5" => {
578                events.push(OverlordEvent::CustomEvent {
579                    event_type: "CompleteAllLoopTasks".to_string(),
580                    data: crate::event::CustomEventData(Default::default()),
581                });
582            }
583
584            // 019c1f08: push OverlordEventUpdateActiveLoopTaskId(uuid("019b561e-..")).
585            "019c1f08-d84d-7c5f-b71d-1368ac514461" => {
586                events.push(OverlordEvent::UpdateActiveLoopTaskId {
587                    quest_id: parse_uuid("019b561e-5450-7d29-ae94-d2ddc340030d"),
588                });
589            }
590
591            // branch also produced no events, so this stays a no-op.
592            other => {
593                tracing::warn!(
594                    "Cheat::Script (id={other}) has no native implementation; \
595                     producing no events"
596                );
597            }
598        }
599
600        let events = events.into_iter().map(EventPluginized::now).collect();
601        EventHandleResult::ok_events(state, events)
602    }
603}