overlord_event_system/
event.rs

1use chrono::Utc;
2use configs::cheats::CheatScriptId;
3use essences::ability_presets::AbilityPresetId;
4use essences::ad_usage::AdPlacement;
5use essences::autochest::{AutoChestFilter, AutoChestFilterId};
6use essences::currency::{CurrencyConsumer, CurrencySource, CurrencyUnit};
7use essences::dungeons::DungeonTemplateId;
8use essences::entity::EssencesCustomEventData;
9use essences::entity::{Coordinates, EntityAttributes, EntityId};
10use essences::fighting::{EntityTeam, FightTemplateId};
11use essences::game::EntityTemplateId;
12use essences::gift::Gift;
13use essences::items::{Item, ItemTemplateId, ItemType};
14use essences::offers::{Offer, OfferId, OfferTemplateId};
15use essences::pets::{EquippedPets, PetCaseRollType, PetDrop, PetId, PetSlotId, UpgradedPetsMap};
16use essences::prelude::*;
17use essences::pvp::PVPState;
18use essences::quest::QuestGroupType;
19use essences::skins::SkinId;
20use essences::talent_tree::TalentId;
21use essences::vassals::Suzerain;
22use essences::vassals::VassalTask;
23use essences::{
24    abilities::{
25        AbilityCaseRollType, AbilityDrop, AbilityId, AbilitySlotId, EquippedAbilities,
26        UpgradedAbilitiesMap,
27    },
28    bundles::BundleId,
29};
30use event_system::derive::Event;
31use event_system::derive::EventTypeName;
32pub use event_system::event::Event;
33use event_system::event::EventStruct;
34use schemars::JsonSchema;
35use std::collections::BTreeMap;
36
37#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Tsify)]
38#[tsify(from_wasm_abi, into_wasm_abi)]
39pub enum AdRewardType {
40    AfkInstant,
41    AfkBoost,
42    DailyBooster,
43    UpgradeSpeedup,
44    FreeAbilityCaseOpen,
45    FreePetCaseOpen,
46    DungeonRaid {
47        dungeon_id: DungeonTemplateId,
48        difficulty: i64,
49    },
50    ArenaRefresh,
51    ClaimBirdReward {
52        variant_id: configs::ads_settings::BirdVariantId,
53    },
54}
55
56#[derive(
57    PartialEq,
58    Eq,
59    Clone,
60    Debug,
61    Event,
62    EventTypeName,
63    JsonSchema,
64    Tsify,
65    Serialize,
66    Deserialize,
67    strum_macros::Display,
68    strum_macros::VariantNames,
69)]
70#[tsify(into_wasm_abi, from_wasm_abi, namespace)]
71pub enum OverlordEvent {
72    // Item Case
73    #[event_type(client)]
74    OpenItemCase {
75        batch_size: i64,
76    },
77    AutoChestOpenItemCase {
78        batch_size: i64,
79    },
80    PlayerNewItems {
81        items: Vec<Item>,
82    },
83    #[event_type(client)]
84    PlayerEquipItem {
85        item_id: Uuid,
86    },
87    #[event_type(client)]
88    SellItem {
89        item_id: Uuid,
90    },
91    ItemSold {
92        item_id: Uuid,
93    },
94    /// Сервер→клиент: предметы с истёкшим TTL удалены (само удаление — в патче).
95    ItemsExpired {
96        item_ids: Vec<Uuid>,
97    },
98    #[event_type(client)]
99    UpgradeItemCase {},
100    ItemCaseUpgraded {},
101    #[event_type(client)]
102    SpeedupUpgradeItemCase {},
103    #[event_type(client)]
104    SkipUpgradeItemCase {},
105    #[event_type(client)]
106    ClaimUpgradeItemCase {},
107
108    #[event_type(client)]
109    EnableAutoSell {},
110
111    #[event_type(client)]
112    DisableAutoSell {},
113
114    #[event_type(client)]
115    SetGearOverrideEnabled {
116        item_type: ItemType,
117        enabled: bool,
118    },
119
120    // Auto-chest
121    #[event_type(client)]
122    EnableAutoChest {},
123
124    #[event_type(client)]
125    DisableAutoChest {},
126
127    #[event_type(client)]
128    EnableAutoChestFilter {
129        filter_id: AutoChestFilterId,
130    },
131
132    #[event_type(client)]
133    DisableAutoChestFilter {},
134
135    #[event_type(client)]
136    EnableAutoChestPowerCompare {},
137
138    #[event_type(client)]
139    DisableAutoChestPowerCompare {},
140
141    #[event_type(client)]
142    UpdateAutoChestBatchSize {
143        batch_size: i64,
144    },
145
146    #[event_type(client)]
147    NewAutoChestFilter {
148        // Boxed: `AutoChestFilter` is ~104 bytes — keeping it inline pushes the
149        // enum's stack size up across all 191 variants. Serde sees `Box<T>` as
150        // `T`, so the wire format is unchanged.
151        filter: Box<AutoChestFilter>,
152    },
153
154    #[event_type(client)]
155    UpdateAutoChestFilter {
156        updated_filter: Box<AutoChestFilter>,
157    },
158
159    #[event_type(client)]
160    RemoveAutoChestFilter {
161        filter_id: AutoChestFilterId,
162    },
163
164    // Ability Case
165    #[event_type(client)]
166    OpenAbilityCase {
167        roll_type: AbilityCaseRollType,
168    },
169
170    /// Обновляет вишлист гачи способностей.
171    /// Список ограничен числом слотов и разрешенными редкостями текущего уровня гачи.
172    #[event_type(client)]
173    SetAbilityGachaWishlist {
174        ability_ids: Vec<AbilityId>,
175    },
176
177    /// Повышает уровень указанного слота способностей за Stardust.
178    #[event_type(client)]
179    UpgradeAbilitySlot {
180        slot_id: u64,
181    },
182
183    AbilityCaseOpened {
184        batch_size: u8,
185    },
186
187    // For frontend display and quest progression
188    NewAbilities {
189        abilities: Vec<AbilityDrop>,
190    },
191
192    #[event_type(client)]
193    FastEquipAbilities {},
194
195    #[event_type(client)]
196    EquipAbility {
197        slot_id: u64,
198        ability_id: AbilityId,
199    },
200
201    #[event_type(client)]
202    UnequipAbility {
203        slot_id: AbilitySlotId,
204    },
205
206    EquipAbilities {
207        equipped_abilities: EquippedAbilities,
208    },
209
210    #[event_type(client)]
211    UpgradeAbility {
212        ability_id: AbilityId,
213    },
214
215    #[event_type(client)]
216    UpgradeAllAbilities {},
217
218    // For frontend display and quests progression
219    UpgradedAbilities {
220        upgraded_abilities: UpgradedAbilitiesMap,
221    },
222
223    UpgradeAbilityCase {},
224
225    CurrencyIncrease {
226        currencies: Vec<CurrencyUnit>,
227        currency_source: CurrencySource,
228    },
229
230    CurrencyDecrease {
231        currencies: Vec<CurrencyUnit>,
232        currency_consumer: CurrencyConsumer,
233    },
234
235    // Level
236    NewCharacterLevel {
237        level: i64,
238    },
239
240    // Fight management
241    // Server-only: emitted by the backend on the `ClientReady` handshake,
242    // never accepted from a client `ClientEvents` envelope.
243    StartGame {},
244    #[event_type(client)]
245    PrepareFight {
246        prepare_fight_type: PrepareFightType,
247    },
248    #[event_type(client)]
249    StartFight {
250        fight_id: Uuid,
251    },
252    #[event_type(client)]
253    FightProgress {},
254    EndFight {
255        fight_id: Uuid,
256        is_win: bool,
257        // Boxed: `PVPState` is 256 bytes, which alone determined sizeof
258        // OverlordEvent (~280 bytes including discriminant). Boxing this one
259        // field shrinks the enum's footprint across every queue/Vec/future
260        // that carries an OverlordEvent. Serde-transparent over Box.
261        pvp_state: Option<Box<PVPState>>,
262    },
263    StageCleared {},
264
265    #[event_type(client)]
266    RaidDungeon {
267        dungeon_id: DungeonTemplateId,
268        difficulty: i64,
269        amount: i64,
270    },
271
272    WaveCleared {},
273
274    // PVP
275    #[event_type(client)]
276    StartVassalPVPSync {
277        opponent_id: Uuid,
278        opponent_suzerain_id: Option<Uuid>,
279    },
280
281    #[event_type(client)]
282    StartArenaPVPSync {
283        opponent_id: Uuid,
284        is_bot: bool,
285    },
286
287    #[event_type(client)]
288    StartArenaRematchSync {
289        match_id: Uuid,
290    },
291
292    #[event_type(client)]
293    RefreshArenaMatchmaking {},
294
295    #[event_type(client)]
296    BuyArenaTicket {},
297
298    // Moving
299    StartMove {
300        entity_id: Uuid,
301        to: Coordinates,
302        duration_ticks: u64,
303    },
304    // Event is delayed, so it must be non_deterministic
305    EndMove {
306        entity_id: Uuid,
307    },
308
309    // Fighting
310    SpawnEntity {
311        id: EntityId,
312        entity_template_id: EntityTemplateId,
313        position: Coordinates,
314        entity_team: EntityTeam,
315        has_big_hp_bar: bool,
316        entity_attributes: EntityAttributes,
317    },
318
319    StartCastAbility {
320        by_entity_id: Uuid,
321        ability_id: AbilityId,
322        pet_id: Option<PetId>,
323    },
324    // Event is delayed, so it must be non_deterministic
325    StartedCastAbility {
326        by_entity_id: Uuid,
327        ability_id: AbilityId,
328        duration_ticks: u64,
329    },
330    CastAbility {
331        by_entity_id: Uuid,
332        to_entity_id: Uuid,
333        ability_id: AbilityId,
334    },
335    StartCastProjectile {
336        by_entity_id: Uuid,
337        to_entity_id: Uuid,
338        projectile_id: Uuid,
339        level: i64,
340        delay: u64,
341    },
342    StartedCastProjectile {
343        by_entity_id: Uuid,
344        to_entity_id: Uuid,
345        projectile_id: Uuid,
346        duration_ticks: u64,
347    },
348    // TODO move all projectiles into state, and make fight_progress manage them, so we dont need deterministic
349    // Event is delayed, so it must be non_deterministic
350    CastProjectile {
351        by_entity_id: Uuid,
352        to_entity_id: Uuid,
353        projectile_id: Uuid,
354        level: i64,
355        projectile_data: CustomEventData,
356    },
357    Damage {
358        entity_id: Uuid,
359        damage: u64,
360        damage_data: CustomEventData,
361    },
362    Heal {
363        entity_id: Uuid,
364        heal: u64,
365    },
366    CounterAttack {
367        by_entity_id: Uuid,
368        to_entity_id: Uuid,
369        duration_ticks: u64,
370    },
371    Multicast {
372        entity_id: Uuid,
373        amount: u64,
374    },
375    Evasion {
376        entity_id: Uuid,
377    },
378    PlayerDeath {},
379    EntityDeath {
380        entity_id: Uuid,
381        reward: Vec<CurrencyUnit>,
382    },
383    EntityIncrAttribute {
384        entity_id: Uuid,
385        attribute: String,
386        delta: i64,
387    },
388    EntityAddAbilityCooldown {
389        entity_id: Uuid,
390        ability_id: Uuid,
391        delta_ticks: i64,
392    },
393    EntityCancelCastWithCooldown {
394        entity_id: Uuid,
395        ability_id: Uuid,
396    },
397    /// Stuns an entity for `duration_ticks`. Internally cancels any in-flight cast (sending it
398    /// to its full cooldown), and freezes every ability's cooldown progression by extending
399    /// each one by `duration_ticks`. Off-cooldown abilities get a fresh stun-only cooldown.
400    /// Designers fire this from the `stun` effect's apply script — no per-ability iteration in
401    EntityStun {
402        entity_id: Uuid,
403        duration_ticks: u64,
404    },
405    EntityApplyEffect {
406        entity_id: Uuid,
407        effect_id: Uuid,
408    },
409    CastEffect {
410        entity_id: Uuid,
411        effect_id: Uuid,
412    },
413    CastEffectFromEvent {
414        entity_id: Uuid,
415        effect_id: Uuid,
416        caller_event: Box<OverlordEvent>,
417    },
418    FightCustomEvent {
419        entity_id: u64,
420        value: String,
421    },
422    FightVisualEvent {
423        effect_type: String,
424        effect_data: CustomEventData,
425    },
426
427    SetMaxHp {
428        entity_id: EntityId,
429        new_max_hp: u64,
430        new_hp: u64,
431    },
432
433    // Vassal Links
434    #[event_type(client)]
435    ClaimVassalReward {
436        character_id: Uuid,
437    },
438
439    #[event_type(client)]
440    ClaimSuzerainReward {},
441
442    NewSuzerain {
443        new_suzerain: Option<Box<Suzerain>>,
444    },
445
446    RemoveVassal {
447        vassal_id: Uuid,
448    },
449
450    // Vassal Tasks
451    #[event_type(client)]
452    GiveTask {
453        vassal_id: Uuid,
454        template_task_id: Uuid,
455    },
456
457    NewTask {
458        new_task: Box<VassalTask>,
459    },
460
461    #[event_type(client)]
462    AcceptTask {
463        task_id: Uuid,
464        is_good: bool,
465    },
466
467    TaskAccepted {
468        task_id: Uuid,
469        started_good: bool,
470        started_at: chrono::DateTime<Utc>,
471        finish_at: chrono::DateTime<Utc>,
472    },
473
474    #[event_type(client)]
475    HitHands {
476        task_id: Uuid,
477    },
478
479    HandsHitted {
480        task_id: Uuid,
481    },
482
483    TaskFinished {
484        task_id: Uuid,
485    },
486
487    #[event_type(client)]
488    ClaimTaskReward {
489        task_id: Uuid,
490    },
491
492    // Resist Task
493    GiveResistTask {
494        new_resist_task: Box<VassalTask>,
495    },
496
497    #[event_type(client)]
498    AcceptResistTask {},
499
500    ResistTaskAccepted {
501        new_resist_task: Box<VassalTask>,
502    },
503
504    #[event_type(client)]
505    CatchResistTask {
506        task_id: Uuid,
507    },
508
509    ResistTaskCatched {
510        task_id: Uuid,
511    },
512
513    ResistTaskFinished {
514        task_id: Uuid,
515    },
516
517    #[event_type(client)]
518    ClaimResistTaskReward {
519        task_id: Uuid,
520    },
521
522    // Custom value
523    SetCustomValue {
524        key: String,
525        value: i64,
526    },
527
528    // Connection store
529    SetConnectionStore {
530        key: String,
531        value: i64,
532    },
533
534    // Quest
535    #[event_type(client)]
536    ClaimQuest {
537        quest_id: Uuid,
538    },
539
540    #[event_type(client)]
541    ClaimAllQuests {
542        quest_group_type: QuestGroupType,
543    },
544
545    PatronQuestCompleted {
546        quest_id: Uuid,
547    },
548
549    HiddenQuestCompleted {
550        quest_id: Uuid,
551    },
552
553    QuestCompleted {
554        quest_id: Uuid,
555    },
556
557    NewQuests {
558        quest_ids: Vec<Uuid>,
559    },
560
561    // Triggered when loop task flow breaks
562    UpdateActiveLoopTaskId {
563        quest_id: Uuid,
564    },
565
566    ResetRepeatingQuests {
567        quest_ids: Vec<Uuid>,
568    },
569
570    #[event_type(client)]
571    ClaimQuestProgressionReward {
572        quest_group_type: QuestGroupType,
573    },
574
575    // Referrals
576    #[event_type(client)]
577    ClaimReferralLvlUpReward {
578        level: i64,
579    },
580
581    #[event_type(client)]
582    ClaimReferralDailyReward {},
583
584    ReferralDailyRewardStatusUpdate {
585        referral_daily_reward_status: bool,
586    },
587
588    // Gifts
589    #[event_type(client)]
590    SendGift {
591        receiver_id: Uuid,
592        config_gift_id: Uuid,
593    },
594
595    NewGift {
596        new_gift: Box<Gift>,
597    },
598
599    #[event_type(client)]
600    AcceptGift {
601        gift_id: Uuid,
602    },
603
604    // AfkReward
605    #[event_type(client)]
606    ClaimAfkReward {},
607
608    AfkRewardClaimed {},
609
610    /// Emitted when the player crosses the AFK rewards unlock chapter threshold
611    /// and the async handler has adjusted `last_afk_reward_claimed_at` so that
612    /// the elapsed time is not less than `min_required_time_sec`.
613    /// The side effect handler persists the new value to the database.
614    AfkRewardsGatingUnlocked {},
615
616    #[event_type(client)]
617    ClaimAfkInstantRewardGems {},
618
619    // Ad Rewards
620    #[event_type(client)]
621    WatchAd {
622        ad_reward_type: AdRewardType,
623    },
624
625    ShowBird {
626        variant_id: configs::ads_settings::BirdVariantId,
627    },
628
629    /// Отправляется клиентом, когда птица была успешно показана игроку.
630    /// Переводит птицу в полный кулдаун.
631    #[event_type(client)]
632    BirdShown {},
633
634    ResetAdUsage {
635        placements: Vec<AdPlacement>,
636    },
637
638    ResetInstantRewardGemsPressCount {},
639
640    // Bundles
641    #[event_type(client)]
642    ClaimBundleStepGeneric {},
643
644    AddBundleGroup {
645        bundle_ids: Vec<BundleId>,
646        // Origin of the bundle — propagates through claim so currency analytics
647        // sees the real source (e.g. `QuestClaim`) instead of the generic
648        // `BundleClaim`.
649        //
650        // `#[serde(skip)]` keeps this server-only. The event still flows
651        // through `ServerTick.events` over postcard to the client, but the field
652        // is omitted from the wire so the layout matches what pre-PR clients
653        // already know. The server always constructs this event with `source`
654        // populated at the call site, then consumes it in
655        // `handle_add_bundle_group` before any wire encoding happens, so the
656        // skip never loses real data.
657        #[serde(skip)]
658        source: essences::currency::CurrencySource,
659    },
660
661    // Classes
662    #[event_type(client)]
663    LevelUpClass {
664        class_id: uuid::Uuid,
665    },
666
667    #[event_type(client)]
668    Respec {
669        new_class_id: uuid::Uuid,
670    },
671
672    // User Account
673    #[event_type(client)]
674    LinkGuestAccount {
675        token: String,
676    },
677
678    #[event_type(client)]
679    SetUsername {
680        username: String,
681    },
682
683    #[event_type(client)]
684    SetCharacterBlocked {
685        character_id: Uuid,
686        blocked: bool,
687    },
688
689    // Ability Presets
690    #[event_type(client)]
691    CreateAbilityPreset {
692        name: Option<String>,
693        ability_ids: Vec<AbilityId>,
694        index: i64,
695    },
696
697    #[event_type(client)]
698    EditAbilityPreset {
699        preset_id: AbilityPresetId,
700        name: String,
701        ability_ids: Vec<AbilityId>,
702    },
703
704    EnableCaseUpgradePopUp {},
705
706    #[event_type(client)]
707    DisableCaseUpgradePopUp {},
708
709    // Skins
710    #[event_type(client)]
711    BuySkins {
712        skin_ids: Vec<SkinId>,
713        equip: bool,
714    },
715
716    #[event_type(client)]
717    EquipAndUnequipSkins {
718        equip_skin_ids: Vec<SkinId>,
719        unequip_skin_ids: Vec<SkinId>,
720    },
721
722    // Cheat
723    #[event_type(client)]
724    RunCheat {
725        cheat: Cheat,
726    },
727
728    // Mail
729    #[event_type(client)]
730    ClaimMail {
731        mail_id: essences::mail::MailId,
732    },
733
734    #[event_type(client)]
735    ClaimAllMails {},
736
737    #[event_type(client)]
738    MakeRead {
739        mail_id: essences::mail::MailId,
740    },
741
742    #[event_type(client)]
743    MakeAllRead {},
744
745    #[event_type(client)]
746    DeleteMail {
747        mail_id: essences::mail::MailId,
748    },
749
750    #[event_type(client)]
751    DeleteAllMails {},
752
753    NewMail {
754        new_mail: Box<essences::mail::Mail>,
755    },
756
757    // Offers
758    #[event_type(client)]
759    NewOffer {
760        offer_template_id: OfferTemplateId,
761    },
762
763    #[event_type(client)]
764    BuyOffer {
765        purchase_token: Option<String>,
766        offer_id: OfferId,
767    },
768
769    OfferPurchaseCompleted {
770        purchase_token: String,
771        offer_id: OfferId,
772        is_test_purchase: bool,
773    },
774
775    OfferPurchaseFailed {
776        purchase_token: String,
777        offer_id: OfferId,
778    },
779
780    PurchasesBanned {},
781
782    ResetOffers {
783        new_offers: Vec<Offer>,
784    },
785
786    // Pets
787    #[event_type(client)]
788    EquipPet {
789        slot_id: u64,
790        pet_id: PetId,
791    },
792
793    #[event_type(client)]
794    UnequipPet {
795        slot_id: PetSlotId,
796    },
797
798    #[event_type(client)]
799    FastEquipPets {},
800
801    EquipPets {
802        equipped_pets: EquippedPets,
803    },
804
805    #[event_type(client)]
806    UpgradePet {
807        pet_id: PetId,
808    },
809
810    #[event_type(client)]
811    UpgradeAllPets {},
812
813    // For frontend display and quests progression
814    UpgradedPets {
815        upgraded_pets: UpgradedPetsMap,
816    },
817
818    #[event_type(client)]
819    UpgradePetSlot {
820        slot_id: u64,
821    },
822
823    // Pet Case (Gacha)
824    #[event_type(client)]
825    OpenPetCase {
826        roll_type: PetCaseRollType,
827    },
828
829    /// Обновляет вишлист гачи петов.
830    #[event_type(client)]
831    SetPetGachaWishlist {
832        pet_ids: Vec<PetId>,
833    },
834
835    PetCaseOpened {
836        batch_size: u8,
837    },
838
839    // For frontend display and quest progression
840    NewPets {
841        pets: Vec<PetDrop>,
842    },
843
844    UpgradePetCase {},
845
846    // Tutorial
847    #[event_type(client)]
848    TutorialShown {
849        step_number: i16,
850    },
851    #[event_type(client)]
852    TutorialStepCompleted {
853        step_number: i16,
854    },
855
856    /// Client lifecycle transition used for churn diagnostics.
857    /// Sent when the Unity app is paused/resumed or loses/regains focus.
858    #[event_type(client)]
859    ClientLifecycle {
860        lifecycle_status: String,
861        active_screen_code: String,
862        session_uptime_secs: i64,
863        current_chapter_level: i64,
864        current_character_level: i64,
865        current_power: i64,
866        active_fight: bool,
867        active_fight_current_wave: i64,
868    },
869
870    // Party
871    #[event_type(client)]
872    AddCharacterToParty {
873        character_id: uuid::Uuid,
874    },
875    #[event_type(client)]
876    RemoveCharacterFromParty {},
877    #[event_type(client)]
878    RefreshPartyPlayers {},
879    RefreshPartyMemberState {},
880
881    // Talent Tree
882    /// Начать изучение следующего уровня таланта (клиент -> бэкенд).
883    #[event_type(client)]
884    StartTalentResearch {
885        talent_id: TalentId,
886    },
887    /// Бэкенд подтверждает начало изучения таланта.
888    TalentResearchStarted {
889        talent_id: TalentId,
890        finish_at: chrono::DateTime<chrono::Utc>,
891    },
892    /// Ускорить изучение таланта с помощью таймскип-билетика.
893    #[event_type(client)]
894    SpeedupTalentResearch {},
895    /// Пропустить оставшееся время изучения за валюту (брюли/таймскипы).
896    #[event_type(client)]
897    SkipTalentResearch {},
898    /// Забрать завершённый талант (после истечения таймера). Эмитируется бэкендом автоматически.
899    ClaimTalentResearch {},
900
901    // Statue
902    /// Откатить незаблокированные слоты статуи. Списывает валюту, начисляет опыт статуе.
903    #[event_type(client)]
904    StatueRoll {
905        set_index: u8,
906        locked_slot_indices: Vec<u8>,
907    },
908    /// Сделать указанный сет статуи активным ("In Use").
909    #[event_type(client)]
910    StatueActivateSet {
911        set_index: u8,
912    },
913    /// Добавить новый сет статуи (если уровень статуи позволяет).
914    #[event_type(client)]
915    StatueAddSet {},
916    /// Переименовать сет статуи.
917    #[event_type(client)]
918    StatueRenameSet {
919        set_index: u8,
920        name: String,
921    },
922    /// Заблокировать или разблокировать слот статуи.
923    #[event_type(client)]
924    StatueLockSlot {
925        set_index: u8,
926        slot_index: u8,
927        is_locked: bool,
928    },
929    /// Бесплатный первичный ролл для нового слота на любом сете (после создания сета или повышения уровня статуи).
930    #[event_type(client)]
931    StatueRollNewSlot {
932        set_index: u8,
933        slot_index: u8,
934    },
935
936    // Progress Pass
937    #[event_type(client)]
938    ClaimProgressPassFreeReward {
939        tier: u32,
940    },
941
942    #[event_type(client)]
943    ClaimProgressPassPaidReward {
944        tier: u32,
945    },
946
947    #[event_type(client)]
948    ClaimAllProgressPassRewards {
949        is_paid: bool,
950    },
951
952    // User Rating
953    /// User rated the game (1–5) or declined to rate (rating = None).
954    #[event_type(client)]
955    UserRating {
956        rating: Option<i8>,
957    },
958
959    /// Server tells the client to display the rate-us prompt.
960    /// Emitted whenever `current_chapter_level >= game_settings.rate_us_chapter`
961    /// and `character.rate_us_shown` is still false. Re-emitted on every new
962    /// session start until the client acknowledges with `RateUsShown`.
963    ShowRateUs {},
964
965    /// Client confirms the rate-us prompt was displayed.
966    /// Flips `character.rate_us_shown` to true so the server stops emitting.
967    #[event_type(client)]
968    RateUsShown {},
969
970    /// Client reports the rate-us star selection (1–5).
971    #[event_type(client)]
972    RateUs {
973        stars: i32,
974    },
975
976    // Error
977    Error {
978        code: String,
979        message: String,
980    },
981
982    #[event_type(client)]
983    CustomEvent {
984        event_type: String,
985        data: CustomEventData,
986    },
987
988    /// Server-only: a multi-cell run passes its next cell. Keeps the runner's
989    /// coordinates ticking cell-by-cell during a single `StartMove` so
990    /// opponents target the cell he is actually passing, not the run's
991    /// destination. Scheduled by `handle_start_move`; appended at the enum
992    /// tail to keep postcard variant tags of older events stable.
993    MoveProgress {
994        entity_id: Uuid,
995        to: Coordinates,
996    },
997}
998
999#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Tsify)]
1000#[tsify(from_wasm_abi, into_wasm_abi)]
1001pub enum PrepareFightType {
1002    PVEFight,
1003    PVPFight {
1004        fight_id: FightTemplateId,
1005        pvp_state: Box<PVPState>,
1006    },
1007    RetryBossFight,
1008    DungeonFight {
1009        dungeon_id: DungeonTemplateId,
1010        difficulty: i64,
1011    },
1012    ForfeitDungeonFight,
1013    SingleFight {
1014        fight_templated_id: FightTemplateId,
1015    },
1016}
1017
1018#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Tsify)]
1019#[tsify(from_wasm_abi, into_wasm_abi)]
1020pub enum Cheat {
1021    PauseCombat,
1022    UnpauseCombat,
1023    GodModeOn,
1024    GodModeOff,
1025    GetRich,
1026    ClearInventory,
1027    ClearSlot {
1028        item_id: Uuid,
1029    },
1030    GetAllSpells,
1031    SetChapter {
1032        chapter: i64,
1033    },
1034    StartFight {
1035        templated_id: FightTemplateId,
1036    },
1037    SpawnEntity {
1038        entity_id: EntityTemplateId,
1039        x: i64,
1040        y: i64,
1041        team: EntityTeam,
1042        attributes: Vec<(String, i64)>,
1043    },
1044    WearItems {
1045        items_ids: Vec<ItemTemplateId>,
1046    },
1047    EquipSkin {
1048        skin_id: SkinId,
1049    },
1050    UnequipSkin {
1051        skin_id: SkinId,
1052    },
1053    NewLevel {
1054        level: i64,
1055    },
1056    Script {
1057        script_id: CheatScriptId,
1058    },
1059}
1060
1061#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
1062pub struct CustomEventData(pub BTreeMap<String, i64>);
1063
1064impl CustomEventData {
1065    pub fn add(&mut self, key: &str, delta: i64) {
1066        let value = self
1067            .0
1068            .entry(key.to_owned())
1069            .and_modify(|x| *x += delta)
1070            .or_insert(delta);
1071        if *value == 0 {
1072            self.0.remove(key);
1073        }
1074    }
1075}
1076
1077impl From<EssencesCustomEventData> for CustomEventData {
1078    fn from(value: EssencesCustomEventData) -> Self {
1079        CustomEventData(value.0)
1080    }
1081}
1082
1083impl From<CustomEventData> for EssencesCustomEventData {
1084    fn from(value: CustomEventData) -> Self {
1085        EssencesCustomEventData(value.0)
1086    }
1087}