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}