overlord_event_system/
event.rs

1use chrono::Utc;
2use configs::cheats::CheatScriptId;
3use essences::ability_presets::AbilityPresetId;
4use essences::ability_stones::{
5    AbilityStoneDrop, AbilityStoneId, AbilityStoneSocketIndex, UpgradedAbilityStonesMap,
6};
7use essences::ad_usage::AdPlacement;
8use essences::artifacts::{ArtifactSocketSlot, ArtifactStoneTemplateId, ArtifactTemplateId};
9use essences::autochest::{AutoChestFilter, AutoChestFilterId};
10use essences::combat_origin::CombatEventOrigin;
11use essences::cores::LawTemplateId;
12use essences::currency::{CurrencyConsumer, CurrencySource, CurrencyUnit};
13use essences::dungeons::DungeonTemplateId;
14use essences::entity::EssencesCustomEventData;
15use essences::entity::{Coordinates, EntityAttributes, EntityId};
16use essences::fight_breakdown::CombatSource;
17use essences::fighting::{EntityTeam, FightTemplateId};
18use essences::flip::{FlipProgressSource, WorldSide};
19use essences::game::EntityTemplateId;
20use essences::gift::Gift;
21use essences::items::{Item, ItemTemplateId, ItemType};
22use essences::offers::{Offer, OfferId, OfferTemplateId};
23use essences::pet_facets::PetFacetLawRole;
24use essences::pets::{EquippedPets, PetCaseRollType, PetDrop, PetId, PetSlotId, UpgradedPetsMap};
25use essences::plinko::PlinkoDirection;
26use essences::prelude::*;
27use essences::pvp::PVPState;
28use essences::quest::QuestGroupType;
29use essences::skins::SkinId;
30use essences::stones::{StoneSocketSlot, StoneTemplateId, UpgradedStonesMap};
31use essences::talent_tree::TalentId;
32use essences::vassals::Suzerain;
33use essences::vassals::VassalTask;
34use essences::{
35    abilities::{
36        AbilityCaseRollType, AbilityDrop, AbilityId, AbilitySlotId, EquippedAbilities,
37        UpgradedAbilitiesMap,
38    },
39    bundles::BundleId,
40};
41use event_system::derive::Event;
42use event_system::derive::EventTypeName;
43pub use event_system::event::Event;
44use event_system::event::EventStruct;
45use schemars::JsonSchema;
46use std::collections::BTreeMap;
47
48#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Tsify)]
49#[tsify(from_wasm_abi, into_wasm_abi)]
50pub enum AdRewardType {
51    AfkInstant,
52    AfkBoost,
53    DailyBooster,
54    UpgradeSpeedup,
55    FreeAbilityCaseOpen,
56    FreePetCaseOpen,
57    DungeonRaid {
58        dungeon_id: DungeonTemplateId,
59        difficulty: i64,
60    },
61    ArenaRefresh,
62    ClaimBirdReward {
63        variant_id: configs::ads_settings::BirdVariantId,
64    },
65}
66
67#[derive(
68    PartialEq,
69    Eq,
70    Clone,
71    Debug,
72    Event,
73    EventTypeName,
74    JsonSchema,
75    Tsify,
76    Serialize,
77    Deserialize,
78    strum_macros::Display,
79    strum_macros::VariantNames,
80)]
81#[tsify(into_wasm_abi, from_wasm_abi, namespace)]
82pub enum OverlordEvent {
83    // Item Case
84    #[event_type(client)]
85    OpenItemCase {
86        batch_size: i64,
87    },
88    AutoChestOpenItemCase {
89        batch_size: i64,
90    },
91    PlayerNewItems {
92        items: Vec<Item>,
93    },
94    #[event_type(client)]
95    PlayerEquipItem {
96        item_id: Uuid,
97    },
98    #[event_type(client)]
99    SellItem {
100        item_id: Uuid,
101    },
102    ItemSold {
103        item_id: Uuid,
104    },
105    /// Сервер→клиент: предметы с истёкшим TTL удалены (само удаление — в патче).
106    ItemsExpired {
107        item_ids: Vec<Uuid>,
108    },
109    /// Сервер→клиент: путь шарика Plinko для одного выданного предмета.
110    ///
111    /// Путь катит сервер (он же начисляет статы за задетые колышки), клиент
112    /// только проигрывает анимацию ровно по этим шагам. Событие идёт СЛЕДОМ
113    /// за `PlayerNewItems`, чтобы предмет уже был в инвентаре; связь с
114    /// наградой — по `item_id`. `PlayerNewItems` выдаётся и вне Plinko
115    /// (временные предметы при входе, награды) — там этого события нет.
116    PlinkoBallDropped {
117        item_id: Uuid,
118        path: Vec<PlinkoDirection>,
119    },
120    #[event_type(client)]
121    UpgradeItemCase {},
122    ItemCaseUpgraded {},
123    #[event_type(client)]
124    SpeedupUpgradeItemCase {},
125    #[event_type(client)]
126    SkipUpgradeItemCase {},
127    #[event_type(client)]
128    ClaimUpgradeItemCase {},
129
130    #[event_type(client)]
131    EnableAutoSell {},
132
133    #[event_type(client)]
134    DisableAutoSell {},
135
136    #[event_type(client)]
137    SetGearOverrideEnabled {
138        item_type: ItemType,
139        enabled: bool,
140    },
141
142    // Auto-chest
143    #[event_type(client)]
144    EnableAutoChest {},
145
146    #[event_type(client)]
147    DisableAutoChest {},
148
149    #[event_type(client)]
150    EnableAutoChestFilter {
151        filter_id: AutoChestFilterId,
152    },
153
154    #[event_type(client)]
155    DisableAutoChestFilter {},
156
157    #[event_type(client)]
158    EnableAutoChestPowerCompare {},
159
160    #[event_type(client)]
161    DisableAutoChestPowerCompare {},
162
163    #[event_type(client)]
164    UpdateAutoChestBatchSize {
165        batch_size: i64,
166    },
167
168    #[event_type(client)]
169    NewAutoChestFilter {
170        // Boxed: `AutoChestFilter` is ~104 bytes — keeping it inline pushes the
171        // enum's stack size up across all 191 variants. Serde sees `Box<T>` as
172        // `T`, so the wire format is unchanged.
173        filter: Box<AutoChestFilter>,
174    },
175
176    #[event_type(client)]
177    UpdateAutoChestFilter {
178        updated_filter: Box<AutoChestFilter>,
179    },
180
181    #[event_type(client)]
182    RemoveAutoChestFilter {
183        filter_id: AutoChestFilterId,
184    },
185
186    // Ability Case
187    #[event_type(client)]
188    OpenAbilityCase {
189        roll_type: AbilityCaseRollType,
190    },
191
192    /// Обновляет вишлист гачи способностей.
193    /// Список ограничен числом слотов и разрешенными редкостями текущего уровня гачи.
194    #[event_type(client)]
195    SetAbilityGachaWishlist {
196        ability_ids: Vec<AbilityId>,
197    },
198
199    /// Повышает уровень указанного слота способностей за Stardust.
200    #[event_type(client)]
201    UpgradeAbilitySlot {
202        slot_id: u64,
203    },
204
205    AbilityCaseOpened {
206        batch_size: u8,
207    },
208
209    // For frontend display and quest progression
210    NewAbilities {
211        abilities: Vec<AbilityDrop>,
212    },
213
214    #[event_type(client)]
215    FastEquipAbilities {},
216
217    #[event_type(client)]
218    EquipAbility {
219        slot_id: u64,
220        ability_id: AbilityId,
221    },
222
223    #[event_type(client)]
224    UnequipAbility {
225        slot_id: AbilitySlotId,
226    },
227
228    EquipAbilities {
229        equipped_abilities: EquippedAbilities,
230    },
231
232    #[event_type(client)]
233    UpgradeAbility {
234        ability_id: AbilityId,
235    },
236
237    #[event_type(client)]
238    UpgradeAllAbilities {},
239
240    // For frontend display and quests progression
241    UpgradedAbilities {
242        upgraded_abilities: UpgradedAbilitiesMap,
243    },
244
245    UpgradeAbilityCase {},
246
247    // Ability stones
248    /// Ставит камень способности в сокет. Сокет несёт сторону (Real/Fantasy),
249    /// поэтому один и тот же камень подходит в любой из сокетов.
250    #[event_type(client)]
251    SocketAbilityStone {
252        ability_id: AbilityId,
253        socket_index: AbilityStoneSocketIndex,
254        stone_id: AbilityStoneId,
255    },
256
257    /// Вынимает камень из сокета способности.
258    #[event_type(client)]
259    UnsocketAbilityStone {
260        ability_id: AbilityId,
261        socket_index: AbilityStoneSocketIndex,
262    },
263
264    /// Повышает уровень камня способности, тратя его сырые копии.
265    #[event_type(client)]
266    UpgradeAbilityStone {
267        stone_id: AbilityStoneId,
268    },
269
270    /// Выдача копий камней способностей (дроп/награда). Серверное событие.
271    NewAbilityStones {
272        stones: Vec<AbilityStoneDrop>,
273    },
274
275    CurrencyIncrease {
276        currencies: Vec<CurrencyUnit>,
277        currency_source: CurrencySource,
278    },
279
280    CurrencyDecrease {
281        currencies: Vec<CurrencyUnit>,
282        currency_consumer: CurrencyConsumer,
283    },
284
285    // Level
286    NewCharacterLevel {
287        level: i64,
288    },
289
290    // Fight management
291    // Server-only: emitted by the backend on the `ClientReady` handshake,
292    // never accepted from a client `ClientEvents` envelope.
293    StartGame {},
294    #[event_type(client)]
295    PrepareFight {
296        prepare_fight_type: PrepareFightType,
297    },
298    #[event_type(client)]
299    StartFight {
300        fight_id: Uuid,
301    },
302    #[event_type(client)]
303    FightProgress {},
304    EndFight {
305        fight_id: Uuid,
306        is_win: bool,
307        // Boxed: `PVPState` is 256 bytes, which alone determined sizeof
308        // OverlordEvent (~280 bytes including discriminant). Boxing this one
309        // field shrinks the enum's footprint across every queue/Vec/future
310        // that carries an OverlordEvent. Serde-transparent over Box.
311        pvp_state: Option<Box<PVPState>>,
312    },
313    StageCleared {},
314
315    #[event_type(client)]
316    RaidDungeon {
317        dungeon_id: DungeonTemplateId,
318        difficulty: i64,
319        amount: i64,
320    },
321
322    WaveCleared {},
323
324    // PVP
325    #[event_type(client)]
326    StartVassalPVPSync {
327        opponent_id: Uuid,
328        opponent_suzerain_id: Option<Uuid>,
329    },
330
331    #[event_type(client)]
332    StartArenaPVPSync {
333        opponent_id: Uuid,
334        is_bot: bool,
335    },
336
337    #[event_type(client)]
338    StartArenaRematchSync {
339        match_id: Uuid,
340    },
341
342    #[event_type(client)]
343    RefreshArenaMatchmaking {},
344
345    #[event_type(client)]
346    BuyArenaTicket {},
347
348    // Moving
349    StartMove {
350        entity_id: Uuid,
351        to: Coordinates,
352        duration_ticks: u64,
353    },
354    // Event is delayed, so it must be non_deterministic
355    EndMove {
356        entity_id: Uuid,
357    },
358
359    // Fighting
360    SpawnEntity {
361        id: EntityId,
362        entity_template_id: EntityTemplateId,
363        position: Coordinates,
364        entity_team: EntityTeam,
365        has_big_hp_bar: bool,
366        entity_attributes: EntityAttributes,
367    },
368
369    StartCastAbility {
370        by_entity_id: Uuid,
371        ability_id: AbilityId,
372        pet_id: Option<PetId>,
373        /// Core/Proc provenance carried to this event's own output — see
374        /// [`OverlordEvent::carried_origin`].
375        origin: CombatEventOrigin,
376    },
377    // Event is delayed, so it must be non_deterministic
378    StartedCastAbility {
379        by_entity_id: Uuid,
380        ability_id: AbilityId,
381        duration_ticks: u64,
382        /// Core/Proc provenance carried to this event's own output — see
383        /// [`OverlordEvent::carried_origin`].
384        origin: CombatEventOrigin,
385    },
386    CastAbility {
387        by_entity_id: Uuid,
388        to_entity_id: Uuid,
389        ability_id: AbilityId,
390        /// Core/Proc provenance carried to this event's own output — see
391        /// [`OverlordEvent::carried_origin`].
392        origin: CombatEventOrigin,
393    },
394    StartCastProjectile {
395        by_entity_id: Uuid,
396        to_entity_id: Uuid,
397        projectile_id: Uuid,
398        level: i64,
399        delay: u64,
400        /// What launched this projectile, carried to the landing so the hit is
401        /// attributed to the ability rather than to the projectile template —
402        /// a projectile is how a skill delivers its payload, and the catalog
403        /// gives projectiles no display name of their own.
404        source: CombatSource,
405        /// Core/Proc provenance carried to this event's own output — see
406        /// [`OverlordEvent::carried_origin`].
407        origin: CombatEventOrigin,
408    },
409    StartedCastProjectile {
410        by_entity_id: Uuid,
411        to_entity_id: Uuid,
412        projectile_id: Uuid,
413        duration_ticks: u64,
414        /// See [`OverlordEvent::StartCastProjectile::source`].
415        source: CombatSource,
416        /// Core/Proc provenance carried to this event's own output — see
417        /// [`OverlordEvent::carried_origin`].
418        origin: CombatEventOrigin,
419    },
420    /// A DERIVED copy of an ability's payload produced by a support stone
421    /// (Repeat / Pulse): the ability's own payload, scaled by
422    /// `payload_permille`, landing `delay` ticks after the original cast.
423    ///
424    /// It is deliberately NOT a cast: it never runs `on_cast`, never pays mana,
425    /// never touches a cooldown and never charges the pet bar, and the damage it
426    /// produces is tagged `derived` — so no Law, Resonance, Mastery or trigger
427    /// reader may count it as a Core event.
428    DerivedAbilityStrike {
429        by_entity_id: Uuid,
430        to_entity_id: Uuid,
431        ability_id: AbilityId,
432        level: i64,
433        /// Payload share in permille (500 = 50% of the ability's payload).
434        payload_permille: i64,
435        /// What the SOURCE cast finally paid for its mana, in x100 fixed point,
436        /// or `-1` when it never paid (no pool, a pet ult). A delayed copy
437        /// resolves after other casts may have landed, so it carries the price
438        /// with it instead of reading whatever the caster paid last — that is
439        /// what lets a Mana-conditioned law or trigger judge the RE-trigger by
440        /// the same cost as the original (BAL-019).
441        source_paid_mana_x100: i64,
442        /// Ticks (ms) to wait before the copy resolves. Routed onto the fight
443        /// clock at the cast site; the scheduled copy carries 0.
444        delay: u64,
445    },
446    /// `EntityIncrAttribute` that lands `delay` ticks later. Used by the
447    /// Shelter support to close its damage-reduction window; the fight clock
448    /// owns the timer, so nothing has to be polled per tick.
449    EntityIncrAttributeDelayed {
450        entity_id: Uuid,
451        attribute: String,
452        delta: i64,
453        delay: u64,
454    },
455    // TODO move all projectiles into state, and make fight_progress manage them, so we dont need deterministic
456    // Event is delayed, so it must be non_deterministic
457    CastProjectile {
458        by_entity_id: Uuid,
459        to_entity_id: Uuid,
460        projectile_id: Uuid,
461        level: i64,
462        projectile_data: CustomEventData,
463        /// See [`OverlordEvent::StartCastProjectile::source`].
464        source: CombatSource,
465        /// Core/Proc provenance carried to this event's own output — see
466        /// [`OverlordEvent::carried_origin`].
467        origin: CombatEventOrigin,
468    },
469    Damage {
470        /// Combatant responsible for this damage. Ownerless environment and
471        /// legacy effects use `None` and do not grant flip progress.
472        by_entity_id: Option<EntityId>,
473        entity_id: Uuid,
474        damage: u64,
475        damage_data: CustomEventData,
476        /// Core/Proc provenance — see [`OverlordEvent::combat_origin`].
477        origin: CombatEventOrigin,
478        /// WHICH producer this damage belongs to — the attribution the
479        /// end-of-fight breakdown is built from. Stamped at the emit site,
480        /// where the producing id is in scope; `origin` answers "was this a
481        /// modifier's work", this answers "whose".
482        source: CombatSource,
483    },
484    /// Server-authored marker for one completed global equipment-side flip.
485    GlobalFlip {
486        /// Fight-local entity whose equipment presentation changed. This is
487        /// required because PvP and party fights can contain several players,
488        /// each with an independent flip state.
489        entity_id: EntityId,
490        /// Producer that supplied the threshold-crossing progress gain.
491        source: FlipProgressSource,
492        /// Side presented before this transition.
493        from_side: WorldSide,
494        /// Side presented after this transition.
495        to_side: WorldSide,
496        /// Monotonic transition key copied from the resulting `FlipState`.
497        revision: u64,
498    },
499    /// Server-authored marker for one Trigger-Stone fire. The combat outcomes a
500    /// fire produces are deliberately anonymous (`origin: Proc` carries no
501    /// source), so this is the only client-visible record of WHICH socketed
502    /// trigger fired — the UI attributes the proc to its equipment slot with it.
503    /// Nothing reads it server-side.
504    StoneTriggerFired {
505        /// Fight-local combatant whose socketed trigger fired.
506        entity_id: EntityId,
507        /// Equipment slot whose Trigger socket held the fired stone.
508        item_type: ItemType,
509        /// Catalog id of the fired Trigger Stone.
510        trigger_stone_id: StoneTemplateId,
511    },
512    Heal {
513        /// Combatant responsible for this heal. `None` for ownerless healing
514        /// (regeneration on a mob, environment effects).
515        by_entity_id: Option<EntityId>,
516        entity_id: Uuid,
517        heal: u64,
518        /// Core/Proc provenance — see [`OverlordEvent::combat_origin`].
519        origin: CombatEventOrigin,
520        /// Producer of this heal — see [`OverlordEvent::Damage::source`].
521        source: CombatSource,
522    },
523    CounterAttack {
524        by_entity_id: Uuid,
525        to_entity_id: Uuid,
526        duration_ticks: u64,
527        /// Core/Proc provenance — see [`OverlordEvent::combat_origin`].
528        origin: CombatEventOrigin,
529    },
530    Multicast {
531        entity_id: Uuid,
532        amount: u64,
533        /// Core/Proc provenance — see [`OverlordEvent::combat_origin`].
534        origin: CombatEventOrigin,
535    },
536    Evasion {
537        entity_id: Uuid,
538        /// Core/Proc provenance — see [`OverlordEvent::combat_origin`].
539        origin: CombatEventOrigin,
540    },
541    PlayerDeath {},
542    EntityDeath {
543        entity_id: Uuid,
544        reward: Vec<CurrencyUnit>,
545        /// Core/Proc provenance — see [`OverlordEvent::combat_origin`].
546        origin: CombatEventOrigin,
547    },
548    EntityIncrAttribute {
549        entity_id: Uuid,
550        attribute: String,
551        delta: i64,
552    },
553    EntityAddAbilityCooldown {
554        entity_id: Uuid,
555        ability_id: Uuid,
556        delta_ticks: i64,
557    },
558    EntityCancelCastWithCooldown {
559        entity_id: Uuid,
560        ability_id: Uuid,
561    },
562    /// Stuns an entity for `duration_ticks`. Internally cancels any in-flight cast (sending it
563    /// to its full cooldown), and freezes every ability's cooldown progression by extending
564    /// each one by `duration_ticks`. Off-cooldown abilities get a fresh stun-only cooldown.
565    /// Designers fire this from the `stun` effect's apply script — no per-ability iteration in
566    EntityStun {
567        entity_id: Uuid,
568        duration_ticks: u64,
569    },
570    EntityApplyEffect {
571        entity_id: Uuid,
572        effect_id: Uuid,
573        /// Core/Proc provenance carried to this event's own output — see
574        /// [`OverlordEvent::carried_origin`].
575        origin: CombatEventOrigin,
576    },
577    CastEffect {
578        entity_id: Uuid,
579        effect_id: Uuid,
580        /// Core/Proc provenance carried to this event's own output — see
581        /// [`OverlordEvent::carried_origin`].
582        origin: CombatEventOrigin,
583    },
584    CastEffectFromEvent {
585        entity_id: Uuid,
586        effect_id: Uuid,
587        caller_event: Box<OverlordEvent>,
588    },
589    FightCustomEvent {
590        entity_id: u64,
591        value: String,
592    },
593    FightVisualEvent {
594        effect_type: String,
595        effect_data: CustomEventData,
596    },
597
598    SetMaxHp {
599        entity_id: EntityId,
600        new_max_hp: u64,
601        new_hp: u64,
602    },
603
604    // Vassal Links
605    #[event_type(client)]
606    ClaimVassalReward {
607        character_id: Uuid,
608    },
609
610    #[event_type(client)]
611    ClaimSuzerainReward {},
612
613    NewSuzerain {
614        new_suzerain: Option<Box<Suzerain>>,
615    },
616
617    RemoveVassal {
618        vassal_id: Uuid,
619    },
620
621    // Vassal Tasks
622    #[event_type(client)]
623    GiveTask {
624        vassal_id: Uuid,
625        template_task_id: Uuid,
626    },
627
628    NewTask {
629        new_task: Box<VassalTask>,
630    },
631
632    #[event_type(client)]
633    AcceptTask {
634        task_id: Uuid,
635        is_good: bool,
636    },
637
638    TaskAccepted {
639        task_id: Uuid,
640        started_good: bool,
641        started_at: chrono::DateTime<Utc>,
642        finish_at: chrono::DateTime<Utc>,
643    },
644
645    #[event_type(client)]
646    HitHands {
647        task_id: Uuid,
648    },
649
650    HandsHitted {
651        task_id: Uuid,
652    },
653
654    TaskFinished {
655        task_id: Uuid,
656    },
657
658    #[event_type(client)]
659    ClaimTaskReward {
660        task_id: Uuid,
661    },
662
663    // Resist Task
664    GiveResistTask {
665        new_resist_task: Box<VassalTask>,
666    },
667
668    #[event_type(client)]
669    AcceptResistTask {},
670
671    ResistTaskAccepted {
672        new_resist_task: Box<VassalTask>,
673    },
674
675    #[event_type(client)]
676    CatchResistTask {
677        task_id: Uuid,
678    },
679
680    ResistTaskCatched {
681        task_id: Uuid,
682    },
683
684    ResistTaskFinished {
685        task_id: Uuid,
686    },
687
688    #[event_type(client)]
689    ClaimResistTaskReward {
690        task_id: Uuid,
691    },
692
693    // Custom value
694    SetCustomValue {
695        key: String,
696        value: i64,
697    },
698
699    // Connection store
700    SetConnectionStore {
701        key: String,
702        value: i64,
703    },
704
705    // Quest
706    #[event_type(client)]
707    ClaimQuest {
708        quest_id: Uuid,
709    },
710
711    #[event_type(client)]
712    ClaimAllQuests {
713        quest_group_type: QuestGroupType,
714    },
715
716    PatronQuestCompleted {
717        quest_id: Uuid,
718    },
719
720    HiddenQuestCompleted {
721        quest_id: Uuid,
722    },
723
724    QuestCompleted {
725        quest_id: Uuid,
726    },
727
728    NewQuests {
729        quest_ids: Vec<Uuid>,
730    },
731
732    // Triggered when loop task flow breaks
733    UpdateActiveLoopTaskId {
734        quest_id: Uuid,
735    },
736
737    ResetRepeatingQuests {
738        quest_ids: Vec<Uuid>,
739    },
740
741    #[event_type(client)]
742    ClaimQuestProgressionReward {
743        quest_group_type: QuestGroupType,
744    },
745
746    // Referrals
747    #[event_type(client)]
748    ClaimReferralLvlUpReward {
749        level: i64,
750    },
751
752    #[event_type(client)]
753    ClaimReferralDailyReward {},
754
755    ReferralDailyRewardStatusUpdate {
756        referral_daily_reward_status: bool,
757    },
758
759    // Gifts
760    #[event_type(client)]
761    SendGift {
762        receiver_id: Uuid,
763        config_gift_id: Uuid,
764    },
765
766    NewGift {
767        new_gift: Box<Gift>,
768    },
769
770    #[event_type(client)]
771    AcceptGift {
772        gift_id: Uuid,
773    },
774
775    // AfkReward
776    #[event_type(client)]
777    ClaimAfkReward {},
778
779    AfkRewardClaimed {},
780
781    /// Emitted when the player crosses the AFK rewards unlock chapter threshold
782    /// and the async handler has adjusted `last_afk_reward_claimed_at` so that
783    /// the elapsed time is not less than `min_required_time_sec`.
784    /// The side effect handler persists the new value to the database.
785    AfkRewardsGatingUnlocked {},
786
787    #[event_type(client)]
788    ClaimAfkInstantRewardGems {},
789
790    // Ad Rewards
791    #[event_type(client)]
792    WatchAd {
793        ad_reward_type: AdRewardType,
794    },
795
796    ShowBird {
797        variant_id: configs::ads_settings::BirdVariantId,
798    },
799
800    /// Отправляется клиентом, когда птица была успешно показана игроку.
801    /// Переводит птицу в полный кулдаун.
802    #[event_type(client)]
803    BirdShown {},
804
805    ResetAdUsage {
806        placements: Vec<AdPlacement>,
807    },
808
809    ResetInstantRewardGemsPressCount {},
810
811    // Bundles
812    #[event_type(client)]
813    ClaimBundleStepGeneric {},
814
815    AddBundleGroup {
816        bundle_ids: Vec<BundleId>,
817        // Origin of the bundle — propagates through claim so currency analytics
818        // sees the real source (e.g. `QuestClaim`) instead of the generic
819        // `BundleClaim`.
820        //
821        // `#[serde(skip)]` keeps this server-only. The event still flows
822        // through `ServerTick.events` over postcard to the client, but the field
823        // is omitted from the wire so the layout matches what pre-PR clients
824        // already know. The server always constructs this event with `source`
825        // populated at the call site, then consumes it in
826        // `handle_add_bundle_group` before any wire encoding happens, so the
827        // skip never loses real data.
828        #[serde(skip)]
829        source: essences::currency::CurrencySource,
830    },
831
832    // Classes
833    #[event_type(client)]
834    LevelUpClass {
835        class_id: uuid::Uuid,
836    },
837
838    #[event_type(client)]
839    Respec {
840        new_class_id: uuid::Uuid,
841    },
842
843    // User Account
844    #[event_type(client)]
845    LinkGuestAccount {
846        token: String,
847    },
848
849    #[event_type(client)]
850    SetUsername {
851        username: String,
852    },
853
854    #[event_type(client)]
855    SetCharacterBlocked {
856        character_id: Uuid,
857        blocked: bool,
858    },
859
860    // Ability Presets
861    #[event_type(client)]
862    CreateAbilityPreset {
863        name: Option<String>,
864        ability_ids: Vec<AbilityId>,
865        index: i64,
866    },
867
868    #[event_type(client)]
869    EditAbilityPreset {
870        preset_id: AbilityPresetId,
871        name: String,
872        ability_ids: Vec<AbilityId>,
873    },
874
875    EnableCaseUpgradePopUp {},
876
877    #[event_type(client)]
878    DisableCaseUpgradePopUp {},
879
880    // Skins
881    #[event_type(client)]
882    BuySkins {
883        skin_ids: Vec<SkinId>,
884        equip: bool,
885    },
886
887    #[event_type(client)]
888    EquipAndUnequipSkins {
889        equip_skin_ids: Vec<SkinId>,
890        unequip_skin_ids: Vec<SkinId>,
891    },
892
893    // Cheat
894    #[event_type(client)]
895    RunCheat {
896        cheat: Cheat,
897    },
898
899    // Mail
900    #[event_type(client)]
901    ClaimMail {
902        mail_id: essences::mail::MailId,
903    },
904
905    #[event_type(client)]
906    ClaimAllMails {},
907
908    #[event_type(client)]
909    MakeRead {
910        mail_id: essences::mail::MailId,
911    },
912
913    #[event_type(client)]
914    MakeAllRead {},
915
916    #[event_type(client)]
917    DeleteMail {
918        mail_id: essences::mail::MailId,
919    },
920
921    #[event_type(client)]
922    DeleteAllMails {},
923
924    NewMail {
925        new_mail: Box<essences::mail::Mail>,
926    },
927
928    // Offers
929    #[event_type(client)]
930    NewOffer {
931        offer_template_id: OfferTemplateId,
932    },
933
934    #[event_type(client)]
935    BuyOffer {
936        purchase_token: Option<String>,
937        offer_id: OfferId,
938    },
939
940    OfferPurchaseCompleted {
941        purchase_token: String,
942        offer_id: OfferId,
943        is_test_purchase: bool,
944    },
945
946    OfferPurchaseFailed {
947        purchase_token: String,
948        offer_id: OfferId,
949    },
950
951    PurchasesBanned {},
952
953    ResetOffers {
954        new_offers: Vec<Offer>,
955    },
956
957    // Pets
958    #[event_type(client)]
959    EquipPet {
960        slot_id: u64,
961        pet_id: PetId,
962    },
963
964    #[event_type(client)]
965    UnequipPet {
966        slot_id: PetSlotId,
967    },
968
969    #[event_type(client)]
970    FastEquipPets {},
971
972    EquipPets {
973        equipped_pets: EquippedPets,
974    },
975
976    #[event_type(client)]
977    UpgradePet {
978        pet_id: PetId,
979    },
980
981    #[event_type(client)]
982    UpgradeAllPets {},
983
984    /// Pet Facets: the out-of-combat pre-selection the three "one chosen Law"
985    /// facets act on (`Lead Reading`, `Wild Reading`, `Dream Reader`).
986    ///
987    /// `None` clears the slot, which is legal: an unset selection falls back to
988    /// the lowest-slot eligible Law rather than switching the facet off.
989    #[event_type(client)]
990    SetPetFacetLaw {
991        role: PetFacetLawRole,
992        law_template_id: Option<LawTemplateId>,
993    },
994
995    // For frontend display and quests progression
996    UpgradedPets {
997        upgraded_pets: UpgradedPetsMap,
998    },
999
1000    #[event_type(client)]
1001    UpgradePetSlot {
1002        slot_id: u64,
1003    },
1004
1005    // Pet Case (Gacha)
1006    #[event_type(client)]
1007    OpenPetCase {
1008        roll_type: PetCaseRollType,
1009    },
1010
1011    /// Обновляет вишлист гачи петов.
1012    #[event_type(client)]
1013    SetPetGachaWishlist {
1014        pet_ids: Vec<PetId>,
1015    },
1016
1017    PetCaseOpened {
1018        batch_size: u8,
1019    },
1020
1021    // For frontend display and quest progression
1022    NewPets {
1023        pets: Vec<PetDrop>,
1024    },
1025
1026    UpgradePetCase {},
1027
1028    // Tutorial
1029    #[event_type(client)]
1030    TutorialShown {
1031        step_number: i16,
1032    },
1033    #[event_type(client)]
1034    TutorialStepCompleted {
1035        step_number: i16,
1036    },
1037
1038    /// Client lifecycle transition used for churn diagnostics.
1039    /// Sent when the Unity app is paused/resumed or loses/regains focus.
1040    #[event_type(client)]
1041    ClientLifecycle {
1042        lifecycle_status: String,
1043        active_screen_code: String,
1044        session_uptime_secs: i64,
1045        current_chapter_level: i64,
1046        current_character_level: i64,
1047        current_power: i64,
1048        active_fight: bool,
1049        active_fight_current_wave: i64,
1050    },
1051
1052    // Party
1053    #[event_type(client)]
1054    AddCharacterToParty {
1055        character_id: uuid::Uuid,
1056    },
1057    #[event_type(client)]
1058    RemoveCharacterFromParty {},
1059    #[event_type(client)]
1060    RefreshPartyPlayers {},
1061    RefreshPartyMemberState {},
1062
1063    // Talent Tree
1064    /// Начать изучение следующего уровня таланта (клиент -> бэкенд).
1065    #[event_type(client)]
1066    StartTalentResearch {
1067        talent_id: TalentId,
1068    },
1069    /// Бэкенд подтверждает начало изучения таланта.
1070    TalentResearchStarted {
1071        talent_id: TalentId,
1072        finish_at: chrono::DateTime<chrono::Utc>,
1073    },
1074    /// Ускорить изучение таланта с помощью таймскип-билетика.
1075    #[event_type(client)]
1076    SpeedupTalentResearch {},
1077    /// Пропустить оставшееся время изучения за валюту (брюли/таймскипы).
1078    #[event_type(client)]
1079    SkipTalentResearch {},
1080    /// Забрать завершённый талант (после истечения таймера). Эмитируется бэкендом автоматически.
1081    ClaimTalentResearch {},
1082
1083    // Statue
1084    /// Откатить незаблокированные слоты статуи. Списывает валюту, начисляет опыт статуе.
1085    #[event_type(client)]
1086    StatueRoll {
1087        set_index: u8,
1088        locked_slot_indices: Vec<u8>,
1089    },
1090    /// Сделать указанный сет статуи активным ("In Use").
1091    #[event_type(client)]
1092    StatueActivateSet {
1093        set_index: u8,
1094    },
1095    /// Добавить новый сет статуи (если уровень статуи позволяет).
1096    #[event_type(client)]
1097    StatueAddSet {},
1098    /// Переименовать сет статуи.
1099    #[event_type(client)]
1100    StatueRenameSet {
1101        set_index: u8,
1102        name: String,
1103    },
1104    /// Заблокировать или разблокировать слот статуи.
1105    #[event_type(client)]
1106    StatueLockSlot {
1107        set_index: u8,
1108        slot_index: u8,
1109        is_locked: bool,
1110    },
1111    /// Бесплатный первичный ролл для нового слота на любом сете (после создания сета или повышения уровня статуи).
1112    #[event_type(client)]
1113    StatueRollNewSlot {
1114        set_index: u8,
1115        slot_index: u8,
1116    },
1117
1118    // Cores / laws / bridges (OVT-2517)
1119    /// Поднять уровень одного ядра на 1 за валюту ядер.
1120    #[event_type(client)]
1121    UpgradeCore {
1122        side: WorldSide,
1123    },
1124    /// Поднять уровень закона, потратив сырые копии этого же закона. Если бой
1125    /// уже идёт, новая сборка применяется со следующего боя.
1126    #[event_type(client)]
1127    UpgradeLaw {
1128        law_template_id: LawTemplateId,
1129    },
1130    /// Поставить закон в слот ядра своей стороны. Текущий бой не пересчитывается.
1131    #[event_type(client)]
1132    SlotLaw {
1133        law_template_id: LawTemplateId,
1134        slot_index: i64,
1135    },
1136    /// Снять закон со слота. Снимает и все его мосты; текущий бой не
1137    /// пересчитывается.
1138    #[event_type(client)]
1139    UnslotLaw {
1140        law_template_id: LawTemplateId,
1141    },
1142    /// Создать мост между двумя законами из разных ядер. Текущий бой сохраняет
1143    /// свой снимок.
1144    #[event_type(client)]
1145    CreateLawBridge {
1146        first_law_template_id: LawTemplateId,
1147        second_law_template_id: LawTemplateId,
1148    },
1149    /// Снять мост между двумя законами. Текущий бой сохраняет свой снимок.
1150    #[event_type(client)]
1151    RemoveLawBridge {
1152        first_law_template_id: LawTemplateId,
1153        second_law_template_id: LawTemplateId,
1154    },
1155    /// Серверный маркер: игрок получил копии закона (дроп).
1156    NewLawCopies {
1157        law_template_id: LawTemplateId,
1158        amount: i64,
1159    },
1160
1161    // Trigger/Effect Stones
1162    /// Вставить камень в сокет предмета. Камень адресуется своим шаблоном:
1163    /// у игрока не больше одного экземпляра каждого шаблона. Можно менять и в
1164    /// бою.
1165    #[event_type(client)]
1166    InsertStone {
1167        template_id: StoneTemplateId,
1168        item_type: ItemType,
1169        socket: StoneSocketSlot,
1170    },
1171    /// Вынуть камень из сокета. Камень возвращается в инвентарь без потерь.
1172    /// Можно менять и в бою.
1173    #[event_type(client)]
1174    RemoveStone {
1175        template_id: StoneTemplateId,
1176    },
1177    /// Поднять камень на уровень за сырые копии. Игрок называет камень, сервер
1178    /// сам списывает нужное число копий по лестнице из конфига. Триггеры и
1179    /// эффекты качаются раздельно. Можно качать и в бою.
1180    #[event_type(client)]
1181    UpgradeStone {
1182        template_id: StoneTemplateId,
1183    },
1184    /// Быстро надеть в предмет лучшие свободные камни: один Trigger и два
1185    /// Effect — или столько, сколько сокетов открыто. Закрытые и уже занятые
1186    /// сокеты пропускаются, камни из других предметов не трогаются, «лучший»
1187    /// считается сначала по тиру, потом по уровню. Можно менять и в бою.
1188    #[event_type(client)]
1189    QuickEquipStones {
1190        item_type: ItemType,
1191    },
1192    /// Прокачать все Trigger-камни, которым хватает сырых копий, за один вызов —
1193    /// столько уровней подряд, сколько позволяет лестница. Камни в сокетах и в
1194    /// инвентаре обрабатываются одинаково. Можно качать и в бою.
1195    ///
1196    /// Каталоги разделены на два события, потому что кнопка на экране качает ту
1197    /// вкладку, которая открыта, а не обе сразу.
1198    #[event_type(client)]
1199    UpgradeAllTriggerStones {},
1200
1201    /// То же самое для Effect-камней. См. `UpgradeAllTriggerStones`.
1202    #[event_type(client)]
1203    UpgradeAllEffectStones {},
1204
1205    /// Что именно подняла прокачка камней: id шаблона -> (уровень до, после).
1206    /// Как `UpgradedAbilities` — фронт показывает по нему окно результата, а
1207    /// квесты читают прогресс. Пустым не отправляется.
1208    UpgradedStones {
1209        upgraded_stones: UpgradedStonesMap,
1210    },
1211    /// Сервер выдал новые сырые копии камней (ВРЕМЕННО — с открытия сундука,
1212    /// см. `configs::stones::StoneDropSettings`). Каждый элемент — одна копия;
1213    /// повторы допустимы и считаются штуками.
1214    PlayerNewStones {
1215        trigger_stones: Vec<StoneTemplateId>,
1216        effect_stones: Vec<StoneTemplateId>,
1217    },
1218
1219    // Progress Pass
1220    #[event_type(client)]
1221    ClaimProgressPassFreeReward {
1222        tier: u32,
1223    },
1224
1225    #[event_type(client)]
1226    ClaimProgressPassPaidReward {
1227        tier: u32,
1228    },
1229
1230    #[event_type(client)]
1231    ClaimAllProgressPassRewards {
1232        is_paid: bool,
1233    },
1234
1235    // User Rating
1236    /// User rated the game (1–5) or declined to rate (rating = None).
1237    #[event_type(client)]
1238    UserRating {
1239        rating: Option<i8>,
1240    },
1241
1242    /// Server tells the client to display the rate-us prompt.
1243    /// Emitted whenever `current_chapter_level >= game_settings.rate_us_chapter`
1244    /// and `character.rate_us_shown` is still false. Re-emitted on every new
1245    /// session start until the client acknowledges with `RateUsShown`.
1246    ShowRateUs {},
1247
1248    /// Client confirms the rate-us prompt was displayed.
1249    /// Flips `character.rate_us_shown` to true so the server stops emitting.
1250    #[event_type(client)]
1251    RateUsShown {},
1252
1253    /// Client reports the rate-us star selection (1–5).
1254    #[event_type(client)]
1255    RateUs {
1256        stars: i32,
1257    },
1258
1259    // Error
1260    Error {
1261        code: String,
1262        message: String,
1263    },
1264
1265    #[event_type(client)]
1266    CustomEvent {
1267        event_type: String,
1268        data: CustomEventData,
1269    },
1270
1271    /// Server-only: a multi-cell run passes its next cell. Keeps the runner's
1272    /// coordinates ticking cell-by-cell during a single `StartMove` so
1273    /// opponents target the cell he is actually passing, not the run's
1274    /// destination. Scheduled by `handle_start_move`; appended at the enum
1275    /// tail to keep postcard variant tags of older events stable.
1276    MoveProgress {
1277        entity_id: Uuid,
1278        to: Coordinates,
1279    },
1280
1281    /// Temporarily pauses the matching active fight while the client downloads
1282    /// required visual content, or a completed fight while it preloads the next
1283    /// campaign location. Kept at the enum tail so postcard variant tags of
1284    /// existing events remain stable.
1285    #[event_type(client)]
1286    SetFightContentLoading {
1287        fight_id: Uuid,
1288        loading: bool,
1289    },
1290
1291    /// Claims every unclaimed Daily and Weekly entitlement for one rating as
1292    /// a single standard bundle group. Duplicate requests are a no-op.
1293    /// Appended at the enum tail to preserve postcard tags of existing events.
1294    #[event_type(client)]
1295    ClaimRatingRewards {
1296        rating_type: essences::ratings::RatingType,
1297    },
1298
1299    /// Server-only projection event used by cron wake-ups to refresh one
1300    /// rating's authoritative notification flags on a connected client.
1301    /// Appended at the enum tail to preserve postcard tags of existing events.
1302    SetRatingRewardAvailability {
1303        availability: essences::ratings::RatingRewardAvailability,
1304    },
1305
1306    // Артефакты. Все пять событий добавлены в хвост enum, чтобы не сдвинуть
1307    // postcard-теги уже существующих.
1308    /// Надеть артефакт. Одновременно надет ровно один: событие заменяет
1309    /// предыдущий, снимая только его World Law. Владение и бонус за владение не
1310    /// меняются. Можно менять в бою.
1311    #[event_type(client)]
1312    EquipArtifact {
1313        template_id: ArtifactTemplateId,
1314    },
1315    /// Вставить камень артефакта в один из шести сокетов. Сокет, который камень
1316    /// принимает, задан его шаблоном; чужой сокет отклоняется. Можно менять в бою.
1317    #[event_type(client)]
1318    InsertArtifactStone {
1319        template_id: ArtifactStoneTemplateId,
1320        socket: ArtifactSocketSlot,
1321    },
1322    /// Вынуть камень артефакта из сокета. Камень остаётся в коллекции с
1323    /// уровнем и копиями. Можно менять в бою.
1324    #[event_type(client)]
1325    RemoveArtifactStone {
1326        template_id: ArtifactStoneTemplateId,
1327    },
1328    /// Поднять камень артефакта на уровень за сырые копии. Правило не меняется,
1329    /// растёт только величина. Можно менять в бою.
1330    #[event_type(client)]
1331    UpgradeArtifactStone {
1332        template_id: ArtifactStoneTemplateId,
1333    },
1334    /// Сервер выдал артефакты и/или камни артефакта. Первая копия создаёт
1335    /// предмет первого уровня; каждый дубликат только банкуется сырой копией,
1336    /// в том числе на максимальном уровне. Уровни меняют отдельные клиентские
1337    /// события улучшения.
1338    PlayerNewArtifacts {
1339        artifacts: Vec<ArtifactTemplateId>,
1340        artifact_stones: Vec<ArtifactStoneTemplateId>,
1341    },
1342    /// Запустить эффект-камень от имени артефакта — серверное событие, клиент
1343    /// его не шлёт.
1344    ///
1345    /// Существует ради одного правила: `FA-05 Staggered Entrance` разносит свои
1346    /// пять эффектов по полсекунды. Взвод в v0.2 — прямая запись в атрибут, а не
1347    /// событие, поэтому отложить сам эффект нечем; откладывается весь запуск
1348    /// целиком, и отложенный запуск ничем не отличается от обычного, кроме
1349    /// момента. Боевые показания перечитываются на исполнении: ближайший враг
1350    /// через полсекунды может быть другим. Если боя уже нет — событие ничего не
1351    /// делает.
1352    FireArtifactEffect {
1353        /// Боец, чей артефакт запускает эффект: локальный герой, союзник по
1354        /// party или PvP-соперник. Показания боя перечитываются от его имени.
1355        owner_id: EntityId,
1356        effect_template_id: StoneTemplateId,
1357        effect_level: i64,
1358        /// Доля силы правила, посчитанная на флипе. В десятитысячных, потому что
1359        /// enum событий выводит `Eq`, а `f64` его не реализует: `7000` — это 0,7.
1360        share_permyriad: i64,
1361        /// Камень-аспект, чьё правило отложило этот прогон. Нужен, чтобы
1362        /// отложенный запуск попал в ту же строку разбора боя, что и мгновенный.
1363        artifact_stone_id: ArtifactStoneTemplateId,
1364    },
1365    /// Назначить камню правой колонки закон, на который он действует, или снять
1366    /// назначение (`law_template_id: None`). Можно менять в бою.
1367    ///
1368    /// Адресом служит один закон, и этого хватает в том числе камням про мосты:
1369    /// у моста нет своего идентификатора, но в v0.2 у закона ровно один партнёр,
1370    /// поэтому «мост, в котором состоит закон X» — однозначный адрес. Отсюда
1371    /// следствие: если игрок разберёт мост выбранного закона и построит ему
1372    /// другой, камень переедет на новый мост сам.
1373    #[event_type(client)]
1374    SetArtifactStoneLawTarget {
1375        template_id: ArtifactStoneTemplateId,
1376        law_template_id: Option<LawTemplateId>,
1377    },
1378
1379    /// Заполняет свободные открытые сокеты ОДНОЙ способности лучшими камнями из
1380    /// коллекции. Закрытые и уже занятые сокеты пропускаются, правила приёма те
1381    /// же, что у ручной установки: совместимость по тегам, одно семейство на
1382    /// один Imprint, один шаблон на способность. Сокет, которому нечего
1383    /// предложить, остаётся пустым без ошибки.
1384    /// Добавлено в хвост enum, чтобы не сдвинуть postcard-теги существующих.
1385    #[event_type(client)]
1386    QuickEquipAbilityStones {
1387        ability_id: AbilityId,
1388    },
1389
1390    /// Поднимает уровень каждого камня способности, которому хватает сырых
1391    /// копий, — столько уровней подряд, сколько позволяет лестница. Камни в
1392    /// сокетах и в коллекции обрабатываются одинаково; камень, который не может
1393    /// оплатить следующий уровень, пропускается, а не роняет весь вызов.
1394    /// Добавлено в хвост enum, чтобы не сдвинуть postcard-теги существующих.
1395    #[event_type(client)]
1396    UpgradeAllAbilityStones {},
1397
1398    /// Что именно подняла массовая прокачка камней способностей: id шаблона ->
1399    /// (уровень до, после). Как `UpgradedStones` — фронт показывает по нему окно
1400    /// результата. Пустым не отправляется.
1401    /// Добавлено в хвост enum, чтобы не сдвинуть postcard-теги существующих.
1402    UpgradedAbilityStones {
1403        upgraded_ability_stones: UpgradedAbilityStonesMap,
1404    },
1405
1406    /// Raises one owned artifact by one level and atomically spends the exact
1407    /// raw-copy cost configured for that next level. Appended at the enum tail
1408    /// to preserve postcard tags of every existing event.
1409    #[event_type(client)]
1410    UpgradeArtifact {
1411        template_id: ArtifactTemplateId,
1412    },
1413
1414    /// Сбрасывает уровни ОБОИХ ядер в 0 и возвращает всю валюту, потраченную
1415    /// на их подъём (точная сумма `core_level_cost` по обеим лестницам).
1416    /// Уровень 0 не даёт слотов законов, поэтому все законы снимаются со
1417    /// слотов путём `UnslotLaw`: мосты уходят, камни артефакта забывают
1418    /// выбранные законы. Сами законы, их уровни и копии не трогаются.
1419    /// Добавлено в хвост enum ради стабильности postcard-тегов.
1420    #[event_type(client)]
1421    ResetCores {},
1422
1423    /// Выбор мира (Real/Fantasy), с которого игрок стартует арена-бои — и как
1424    /// атакующий, и как снапшот-оппонент в чужих боях. Валиден только при
1425    /// разлоченном флипе. Добавлено в хвост enum ради стабильности
1426    /// postcard-тегов.
1427    #[event_type(client)]
1428    SetArenaWorldSide {
1429        side: WorldSide,
1430    },
1431}
1432
1433impl OverlordEvent {
1434    /// Provenance of a trigger-eligible combat **outcome**, or `None` when this
1435    /// event is not one.
1436    ///
1437    /// The outcome vocabulary is exactly what a trigger can subscribe to:
1438    /// `Damage` (an attack, a crit — the crit flag rides in `damage_data`),
1439    /// `Heal`, `Evasion` (a dodge), `CounterAttack`, `Multicast` and
1440    /// `EntityDeath` (a mob death). Everything else answers `None`, including
1441    /// the *carriers* of [`OverlordEvent::carried_origin`]: a trigger fires on
1442    /// the damage a cast produced, never on the cast itself. `GlobalFlip`
1443    /// answers `None` too — a flip is a state transition, not a combat action,
1444    /// and must never read as Core.
1445    pub fn combat_origin(&self) -> Option<CombatEventOrigin> {
1446        match self {
1447            Self::Damage { origin, .. }
1448            | Self::Heal { origin, .. }
1449            | Self::Evasion { origin, .. }
1450            | Self::CounterAttack { origin, .. }
1451            | Self::Multicast { origin, .. }
1452            | Self::EntityDeath { origin, .. } => Some(*origin),
1453            _ => None,
1454        }
1455    }
1456
1457    /// Whether a trigger may react to this event. False for every non-combat
1458    /// event, for every carrier, and for anything a modifier produced.
1459    pub fn is_core_combat_event(&self) -> bool {
1460        self.combat_origin().is_some_and(CombatEventOrigin::is_core)
1461    }
1462
1463    /// Provenance this event hands down to whatever it produces — the outcomes
1464    /// above *plus* the carriers that forward a producer's work.
1465    ///
1466    /// A modifier's work does not reach its outcome in one hop: a stone that
1467    /// grants an extra swing emits a cast, the cast schedules a projectile, the
1468    /// projectile lands damage. Each link therefore carries provenance, so the
1469    /// mark survives every hop, including the delayed ones that leave through
1470    /// [`crate::fight::FightClock`]. The carriers are the ability-cast chain
1471    /// (`StartCastAbility` / `StartedCastAbility` / `CastAbility`), the
1472    /// projectile chain (`StartCastProjectile` / `StartedCastProjectile` /
1473    /// `CastProjectile`) and effect dispatch (`EntityApplyEffect` / `CastEffect`
1474    /// / `CastEffectFromEvent`).
1475    ///
1476    /// `CastEffectFromEvent` derives instead of storing: it already boxes the
1477    /// event the subscription fired on, which is the exact producer to inherit
1478    /// from. This is what keeps the "Proc damage → subscribed effect → heal"
1479    /// hop from resetting to Core, at any depth.
1480    ///
1481    /// Two things are deliberately *not* carriers. A combatant spawned by a
1482    /// modifier fights with its own Core actions — it is a real fighter whose
1483    /// rate is bounded by its own cadence, not an instant re-entry. And
1484    /// `FightProgress`, the global heartbeat, drives every entity's turn, so it
1485    /// belongs to no single producer.
1486    pub fn carried_origin(&self) -> Option<CombatEventOrigin> {
1487        match self {
1488            Self::CastEffectFromEvent { caller_event, .. } => caller_event.carried_origin(),
1489            Self::StartCastAbility { origin, .. }
1490            | Self::StartedCastAbility { origin, .. }
1491            | Self::CastAbility { origin, .. }
1492            | Self::StartCastProjectile { origin, .. }
1493            | Self::StartedCastProjectile { origin, .. }
1494            | Self::CastProjectile { origin, .. }
1495            | Self::EntityApplyEffect { origin, .. }
1496            | Self::CastEffect { origin, .. } => Some(*origin),
1497            _ => self.combat_origin(),
1498        }
1499    }
1500
1501    /// Restamps provenance on any event that carries it — outcome or carrier.
1502    /// Returns `false` (and changes nothing) for events that carry none,
1503    /// `CastEffectFromEvent` included: it derives from its boxed caller and has
1504    /// no field of its own.
1505    ///
1506    /// This is how a modifier's output is marked without every emit site
1507    /// knowing about modifiers: the producing scope restamps whatever the fight
1508    /// primitives emitted.
1509    pub fn set_origin(&mut self, new_origin: CombatEventOrigin) -> bool {
1510        match self {
1511            Self::Damage { origin, .. }
1512            | Self::Heal { origin, .. }
1513            | Self::Evasion { origin, .. }
1514            | Self::CounterAttack { origin, .. }
1515            | Self::Multicast { origin, .. }
1516            | Self::EntityDeath { origin, .. }
1517            | Self::StartCastAbility { origin, .. }
1518            | Self::StartedCastAbility { origin, .. }
1519            | Self::CastAbility { origin, .. }
1520            | Self::StartCastProjectile { origin, .. }
1521            | Self::StartedCastProjectile { origin, .. }
1522            | Self::CastProjectile { origin, .. }
1523            | Self::EntityApplyEffect { origin, .. }
1524            | Self::CastEffect { origin, .. } => {
1525                *origin = new_origin;
1526                true
1527            }
1528            _ => false,
1529        }
1530    }
1531
1532    /// Owned form of [`OverlordEvent::set_origin`].
1533    pub fn with_origin(mut self, new_origin: CombatEventOrigin) -> Self {
1534        self.set_origin(new_origin);
1535        self
1536    }
1537
1538    /// Whether this event already carries provenance, i.e. whether
1539    /// [`OverlordEvent::set_origin`] can mark it. A non-Core producer scope may
1540    /// only emit events for which this holds — see
1541    /// [`crate::mechanics::fight::OriginSink`].
1542    pub fn carries_origin(&self) -> bool {
1543        self.carried_origin().is_some()
1544    }
1545}
1546
1547#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Tsify)]
1548#[tsify(from_wasm_abi, into_wasm_abi)]
1549pub enum PrepareFightType {
1550    PVEFight,
1551    PVPFight {
1552        fight_id: FightTemplateId,
1553        pvp_state: Box<PVPState>,
1554    },
1555    RetryBossFight,
1556    DungeonFight {
1557        dungeon_id: DungeonTemplateId,
1558        difficulty: i64,
1559    },
1560    ForfeitDungeonFight,
1561    SingleFight {
1562        fight_templated_id: FightTemplateId,
1563    },
1564}
1565
1566#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Tsify)]
1567#[tsify(from_wasm_abi, into_wasm_abi)]
1568pub enum Cheat {
1569    PauseCombat,
1570    UnpauseCombat,
1571    GodModeOn,
1572    GodModeOff,
1573    GetRich,
1574    ClearInventory,
1575    ClearSlot {
1576        item_id: Uuid,
1577    },
1578    GetAllSpells,
1579    SetChapter {
1580        chapter: i64,
1581    },
1582    StartFight {
1583        templated_id: FightTemplateId,
1584    },
1585    SpawnEntity {
1586        entity_id: EntityTemplateId,
1587        x: i64,
1588        y: i64,
1589        team: EntityTeam,
1590        attributes: Vec<(String, i64)>,
1591    },
1592    WearItems {
1593        items_ids: Vec<ItemTemplateId>,
1594    },
1595    EquipSkin {
1596        skin_id: SkinId,
1597    },
1598    UnequipSkin {
1599        skin_id: SkinId,
1600    },
1601    NewLevel {
1602        level: i64,
1603    },
1604    Script {
1605        script_id: CheatScriptId,
1606    },
1607    /// Reopens the latest Daily Arena and PvE rating reward entitlements.
1608    /// Appended at the enum tail to preserve postcard tags of existing cheats.
1609    MakeDailyRatingRewardsReady,
1610    /// Reopens the latest Weekly Arena and PvE rating reward entitlements.
1611    /// Appended at the enum tail to preserve postcard tags of existing cheats.
1612    MakeWeeklyRatingRewardsReady,
1613    /// Grants one copy of every Trigger and Effect Stone in the catalog.
1614    /// A template already owned gains a raw copy instead of a second instance,
1615    /// so running it repeatedly feeds upgrades.
1616    /// Appended at the enum tail to preserve postcard tags of existing cheats.
1617    GetAllStones,
1618    /// Grants one raw copy of every ability support stone in the catalog.
1619    /// Ownership is a per-template stack, so running it repeatedly banks the
1620    /// copies an upgrade costs — the only other source is the chapter faucet,
1621    /// which hands out one copy per `chapter_reward_period` cleared chapters.
1622    /// Appended at the enum tail to preserve postcard tags of existing cheats.
1623    GetAllAbilityStones,
1624    /// Grants one copy of every pet in the catalog. A pet already owned gains a
1625    /// shard instead of a second entry, so running it repeatedly feeds upgrades
1626    /// exactly as the gacha does.
1627    /// Appended at the enum tail to preserve postcard tags of existing cheats.
1628    GetAllPets,
1629    /// Grants one copy of every artifact and every artifact stone in the
1630    /// catalog through the normal grant path, so duplicates bank upgrade
1631    /// copies exactly as a repeat bundle/drop would.
1632    /// Appended at the enum tail to preserve postcard tags of existing cheats.
1633    GetAllArtifacts,
1634    /// Grants every catalog law as an owned level-1 law (already-owned laws
1635    /// are untouched — the cheat unlocks, copy income levels).
1636    /// Appended at the enum tail to preserve postcard tags of existing cheats.
1637    GetAllLaws,
1638    /// Rolls `batch_size` items through the normal case pipeline with the
1639    /// chest-level batch cap lifted — any size is accepted — but PAID for out
1640    /// of the character's own chest keys, so it fails on an empty wallet
1641    /// exactly as a manual open does. With auto-chest off it then applies the
1642    /// client's own keep/sell rule over the batch: per logical slot only the
1643    /// strongest roll survives.
1644    /// Appended at the enum tail to preserve postcard tags of existing cheats.
1645    OpenItemCase {
1646        batch_size: i64,
1647    },
1648    /// [`Cheat::OpenItemCase`] with the key price lifted as well: the keys the
1649    /// open spends are granted by the cheat, so the wallet nets zero and a
1650    /// character with no keys can still roll. Everything else — the batch cap,
1651    /// the roll, the keep/sell pass — is identical.
1652    /// Appended at the enum tail to preserve postcard tags of existing cheats.
1653    OpenItemCaseFree {
1654        batch_size: i64,
1655    },
1656}
1657
1658#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
1659pub struct CustomEventData(pub BTreeMap<String, i64>);
1660
1661impl CustomEventData {
1662    pub fn add(&mut self, key: &str, delta: i64) {
1663        let value = self
1664            .0
1665            .entry(key.to_owned())
1666            .and_modify(|x| *x += delta)
1667            .or_insert(delta);
1668        if *value == 0 {
1669            self.0.remove(key);
1670        }
1671    }
1672}
1673
1674impl From<EssencesCustomEventData> for CustomEventData {
1675    fn from(value: EssencesCustomEventData) -> Self {
1676        CustomEventData(value.0)
1677    }
1678}
1679
1680impl From<CustomEventData> for EssencesCustomEventData {
1681    fn from(value: CustomEventData) -> Self {
1682        EssencesCustomEventData(value.0)
1683    }
1684}
1685
1686#[cfg(test)]
1687mod combat_origin_tests {
1688    use super::*;
1689
1690    fn id() -> Uuid {
1691        Uuid::from_u128(1)
1692    }
1693
1694    fn damage(origin: CombatEventOrigin) -> OverlordEvent {
1695        OverlordEvent::Damage {
1696            by_entity_id: Some(id()),
1697            entity_id: id(),
1698            damage: 5,
1699            damage_data: CustomEventData::default(),
1700            origin,
1701            source: essences::fight_breakdown::CombatSource::Other,
1702        }
1703    }
1704
1705    /// The whole combat-outcome vocabulary carries provenance — a trigger asks
1706    /// one question of any of them.
1707    #[test]
1708    fn every_marked_combat_event_reports_its_origin() {
1709        let marked = [
1710            damage(CombatEventOrigin::Core),
1711            OverlordEvent::Heal {
1712                entity_id: id(),
1713                heal: 1,
1714                origin: CombatEventOrigin::Core,
1715                by_entity_id: None,
1716                source: essences::fight_breakdown::CombatSource::Other,
1717            },
1718            OverlordEvent::Evasion {
1719                entity_id: id(),
1720                origin: CombatEventOrigin::Core,
1721            },
1722            OverlordEvent::CounterAttack {
1723                by_entity_id: id(),
1724                to_entity_id: id(),
1725                duration_ticks: 1,
1726                origin: CombatEventOrigin::Core,
1727            },
1728            OverlordEvent::Multicast {
1729                entity_id: id(),
1730                amount: 1,
1731                origin: CombatEventOrigin::Core,
1732            },
1733            OverlordEvent::EntityDeath {
1734                entity_id: id(),
1735                reward: Vec::new(),
1736                origin: CombatEventOrigin::Core,
1737            },
1738        ];
1739
1740        for event in marked {
1741            assert_eq!(
1742                event.combat_origin(),
1743                Some(CombatEventOrigin::Core),
1744                "{event}"
1745            );
1746            assert!(event.is_core_combat_event(), "{event}");
1747            let procced = event.with_origin(CombatEventOrigin::Proc);
1748            assert_eq!(procced.combat_origin(), Some(CombatEventOrigin::Proc));
1749            assert!(!procced.is_core_combat_event());
1750        }
1751    }
1752
1753    /// A flip is a state transition, not a combat action. It must never read as
1754    /// Core, or a "flip → trigger → effect → flip" loop becomes expressible.
1755    #[test]
1756    fn a_flip_is_not_a_core_event() {
1757        let flip = OverlordEvent::GlobalFlip {
1758            entity_id: id(),
1759            source: FlipProgressSource::TriggerFired,
1760            from_side: WorldSide::Fantasy,
1761            to_side: WorldSide::Real,
1762            revision: 1,
1763        };
1764
1765        assert_eq!(flip.combat_origin(), None);
1766        assert!(!flip.is_core_combat_event());
1767    }
1768
1769    /// Restamping is a no-op for events that carry no origin — including the
1770    /// flip, which stays unmarked even inside a Proc cascade.
1771    #[test]
1772    fn unmarked_events_cannot_be_stamped() {
1773        let mut flip = OverlordEvent::GlobalFlip {
1774            entity_id: id(),
1775            source: FlipProgressSource::TriggerFired,
1776            from_side: WorldSide::Fantasy,
1777            to_side: WorldSide::Real,
1778            revision: 1,
1779        };
1780        assert!(!flip.set_origin(CombatEventOrigin::Proc));
1781        assert_eq!(flip.combat_origin(), None);
1782
1783        let mut player_death = OverlordEvent::PlayerDeath {};
1784        assert!(!player_death.set_origin(CombatEventOrigin::Proc));
1785
1786        let mut stage = OverlordEvent::StageCleared {};
1787        assert!(!stage.set_origin(CombatEventOrigin::Proc));
1788        assert!(!stage.is_core_combat_event());
1789    }
1790
1791    /// Provenance is the only thing restamping touches.
1792    #[test]
1793    fn restamping_preserves_the_payload() {
1794        let core = damage(CombatEventOrigin::Core);
1795        let procced = core.clone().with_origin(CombatEventOrigin::Proc);
1796
1797        let (
1798            OverlordEvent::Damage {
1799                by_entity_id: core_by,
1800                entity_id: core_entity,
1801                damage: core_damage,
1802                damage_data: core_data,
1803                ..
1804            },
1805            OverlordEvent::Damage {
1806                by_entity_id: proc_by,
1807                entity_id: proc_entity,
1808                damage: proc_damage,
1809                damage_data: proc_data,
1810                ..
1811            },
1812        ) = (core, procced)
1813        else {
1814            panic!("both events are Damage");
1815        };
1816
1817        assert_eq!(core_by, proc_by);
1818        assert_eq!(core_entity, proc_entity);
1819        assert_eq!(core_damage, proc_damage);
1820        assert_eq!(core_data, proc_data);
1821    }
1822}