weiss_core/
effects.rs

1use serde::{Deserialize, Serialize};
2
3use crate::db::{
4    BattleOpponentMoveDestination, BattleOpponentMovePreludeAction, BrainstormMode, CardId,
5    ConditionTurn, GrantDuration, TriggerIcon, ZoneCountCondition,
6};
7use crate::events::RevealAudience;
8use crate::state::{
9    DamageType, ModifierDuration, ModifierKind, TargetSide, TargetSpec, TargetZone,
10};
11
12/// Source category for an effect.
13#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
14pub enum EffectSourceKind {
15    /// Trigger resolution.
16    Trigger,
17    /// Auto ability.
18    Auto,
19    /// Activated ability.
20    Activated,
21    /// Continuous modifier.
22    Continuous,
23    /// Event card play.
24    EventPlay,
25    /// Counter timing.
26    Counter,
27    /// Replacement effect.
28    Replacement,
29    /// System-generated effect.
30    System,
31}
32
33/// Stable identifier for an effect instance.
34#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
35pub struct EffectId {
36    /// Effect source category.
37    pub source_kind: EffectSourceKind,
38    /// Source card id (0 means none; see EffectSourceKind).
39    pub source_card: CardId,
40    /// Ability index on the source card.
41    pub ability_index: u8,
42    /// Effect index within the ability.
43    pub effect_index: u8,
44}
45
46impl EffectId {
47    /// Build an effect id from its components.
48    pub fn new(
49        source_kind: EffectSourceKind,
50        source_card: CardId,
51        ability_index: u8,
52        effect_index: u8,
53    ) -> Self {
54        Self {
55            source_kind,
56            source_card,
57            ability_index,
58            effect_index,
59        }
60    }
61}
62
63/// Fully specified effect with targeting metadata.
64#[derive(Clone, Debug, Hash, Serialize, Deserialize)]
65pub struct EffectSpec {
66    /// Stable effect id.
67    pub id: EffectId,
68    /// Effect kind.
69    pub kind: EffectKind,
70    /// Optional target specification.
71    pub target: Option<TargetSpec>,
72    /// Whether this effect is optional.
73    pub optional: bool,
74}
75
76/// Terminal outcome specified relative to the effect controller.
77#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
78pub enum TerminalOutcomeSpec {
79    /// Controller wins.
80    WinSelf,
81    /// Controller loses (opponent wins).
82    WinOpponent,
83    /// Game ends in draw.
84    Draw,
85    /// Game ends in timeout.
86    Timeout,
87}
88
89/// Turn-scoped rule-action override selectors.
90#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
91pub enum RuleOverrideKind {
92    /// Skip deck-empty refresh/loss processing in rule actions.
93    SkipDeckRefreshOrLoss,
94    /// Skip level-4 loss checks in rule actions.
95    SkipLevelFourLoss,
96    /// Skip non-character stage cleanup in rule actions.
97    SkipNonCharacterStageCleanup,
98    /// Skip non-positive-power stage cleanup in rule actions.
99    SkipZeroOrNegativePowerCleanup,
100}
101
102/// Effect kinds that can be executed by the engine.
103#[derive(Clone, Debug, Hash, Serialize, Deserialize)]
104pub enum EffectKind {
105    /// Draw cards.
106    Draw {
107        /// Number of cards to draw.
108        count: u8,
109    },
110    /// Deal damage.
111    Damage {
112        /// Damage magnitude to deal.
113        amount: i32,
114        /// Whether damage can be canceled by revealing a climax.
115        cancelable: bool,
116        /// Damage classification (e.g., battle vs effect damage).
117        damage_type: DamageType,
118    },
119    /// Add a modifier to the target.
120    AddModifier {
121        /// Modifier kind (power, soul, etc).
122        kind: ModifierKind,
123        /// Signed magnitude of the modifier.
124        magnitude: i32,
125        /// Duration of the modifier.
126        duration: ModifierDuration,
127    },
128    /// Grant an ability definition to the target.
129    GrantAbilityDef {
130        /// Ability definition to grant.
131        ability: Box<crate::db::AbilityDef>,
132        /// Duration for which the ability is granted.
133        duration: GrantDuration,
134    },
135    /// Add power when the target's level is at least a threshold.
136    AddPowerIfTargetLevelAtLeast {
137        /// Power magnitude to add.
138        amount: i32,
139        /// Minimum level threshold for the target.
140        min_level: u8,
141        /// Duration of the modifier.
142        duration: ModifierDuration,
143    },
144    /// Add power scaled by the target's (computed) level.
145    AddPowerByTargetLevel {
146        /// Power multiplier per level.
147        multiplier: i32,
148        /// Duration of the modifier.
149        duration: ModifierDuration,
150    },
151    /// Add power if the battle opponent's level is at least a threshold.
152    AddPowerIfBattleOpponentLevelAtLeast {
153        /// Power magnitude to add.
154        amount: i32,
155        /// Minimum level threshold for the battle opponent.
156        min_level: u8,
157        /// Duration of the modifier.
158        duration: ModifierDuration,
159    },
160    /// Add soul if the battle opponent's level is at least a threshold.
161    AddSoulIfBattleOpponentLevelAtLeast {
162        /// Soul magnitude to add.
163        amount: i32,
164        /// Minimum level threshold for the battle opponent.
165        min_level: u8,
166        /// Duration of the modifier.
167        duration: ModifierDuration,
168    },
169    /// Add power if the battle opponent's level matches exactly.
170    AddPowerIfBattleOpponentLevelExact {
171        /// Power magnitude to add.
172        amount: i32,
173        /// Required battle opponent level.
174        level: u8,
175        /// Duration of the modifier.
176        duration: ModifierDuration,
177    },
178    /// Add power when another attacking character matches one of the provided card ids.
179    AddPowerIfOtherAttackerMatches {
180        /// Power magnitude to add.
181        amount: i32,
182        /// Duration of the modifier.
183        duration: ModifierDuration,
184        /// Allowed attacker card ids to match against.
185        attacker_card_ids: Vec<CardId>,
186    },
187    /// Add soul while this card occupies the middle center-stage position.
188    AddSoulIfMiddleCenter {
189        /// Soul magnitude to add.
190        amount: i32,
191    },
192    /// Add soul to the character facing the source card.
193    FacingOpponentAddSoul {
194        /// Soul magnitude to add.
195        amount: i32,
196    },
197    /// Add a modifier to the character facing the source card.
198    FacingOpponentAddModifier {
199        /// Modifier kind to apply.
200        kind: ModifierKind,
201        /// Signed magnitude of the modifier.
202        magnitude: i32,
203        /// Duration of the modifier.
204        duration: ModifierDuration,
205    },
206    /// Add a modifier to the source card if it is facing a matching opponent.
207    SelfAddModifierIfFacingOpponent {
208        /// Modifier kind to apply.
209        kind: ModifierKind,
210        /// Signed magnitude of the modifier.
211        magnitude: i32,
212        /// Duration of the modifier.
213        duration: ModifierDuration,
214        /// Optional maximum opponent level allowed.
215        max_level: Option<u8>,
216        /// Optional maximum opponent cost allowed.
217        max_cost: Option<u8>,
218        /// If true, require opponent level to exceed the source level.
219        level_gt_source_level: bool,
220    },
221    /// Add a modifier to targets when a conditional context is satisfied.
222    ConditionalAddModifier {
223        /// Modifier kind to apply.
224        kind: ModifierKind,
225        /// Signed magnitude of the modifier.
226        magnitude: i32,
227        /// Duration of the modifier.
228        duration: ModifierDuration,
229        /// Optional turn condition.
230        turn: Option<ConditionTurn>,
231        /// Optional zone-count condition.
232        zone_count: Option<ZoneCountCondition>,
233        /// Whether the source must have at least one marker.
234        require_source_marker: bool,
235        /// If true, scale magnitude by the number of markers under the source.
236        per_source_marker: bool,
237        /// If true, scale magnitude by the zone-count value.
238        per_zone_count: bool,
239        /// If true, skip applying the modifier to the source card itself.
240        exclude_source: bool,
241    },
242    /// Move target to hand.
243    MoveToHand,
244    /// Move target to waiting room.
245    MoveToWaitingRoom,
246    /// Move target to stock.
247    MoveToStock,
248    /// Move target to clock.
249    MoveToClock,
250    /// Move target to memory.
251    MoveToMemory,
252    /// Move target to bottom of deck.
253    MoveToDeckBottom,
254    /// Move a waiting-room card to the source card's stage slot.
255    MoveWaitingRoomCardToSourceSlot,
256    /// Return all cards from waiting room to deck, then shuffle.
257    RecycleWaitingRoomToDeckShuffle,
258    /// Move all stock to waiting room, then refill stock from deck top by the same count.
259    ResetStockFromDeckTop {
260        /// Side whose stock is reset.
261        target: TargetSide,
262    },
263    /// Move target card under the source card as a marker.
264    MoveToMarker,
265    /// Move the top card of your deck under this card as a marker.
266    MoveTopDeckToMarker,
267    /// Heal (move top clock to waiting room).
268    Heal,
269    /// Heal only if the source card was played from hand this turn and remains on stage.
270    HealIfSourcePlayedFromHandThisTurn,
271    /// Rest the target.
272    RestTarget,
273    /// Stand the target.
274    StandTarget,
275    /// Stock charge by count.
276    StockCharge {
277        /// Number of cards to stock-charge.
278        count: u8,
279    },
280    /// Mill top cards from deck.
281    MillTop {
282        /// Side whose deck is milled.
283        target: TargetSide,
284        /// Number of cards to mill.
285        count: u8,
286    },
287    /// Move target to a specific stage slot.
288    MoveStageSlot {
289        /// Stage slot index.
290        slot: u8,
291    },
292    /// Move the source card to the first open center-stage slot.
293    MoveThisToOpenCenter {
294        /// Whether the source must currently be facing an opponent.
295        require_facing: bool,
296    },
297    /// Move the source card to the first open back-stage slot.
298    MoveThisToOpenBack,
299    /// Swap two stage slots.
300    SwapStageSlots,
301    /// Random discard from hand.
302    RandomDiscardFromHand {
303        /// Side whose hand is discarded from.
304        target: TargetSide,
305        /// Number of cards to discard.
306        count: u8,
307    },
308    /// Random mill from deck.
309    RandomMill {
310        /// Side whose deck is milled.
311        target: TargetSide,
312        /// Number of cards to mill.
313        count: u8,
314    },
315    /// Reveal the top of a zone.
316    RevealZoneTop {
317        /// Side whose zone is revealed.
318        target: TargetSide,
319        /// Zone to reveal cards from.
320        zone: TargetZone,
321        /// Number of cards to reveal.
322        count: u8,
323        /// Reveal visibility audience.
324        audience: RevealAudience,
325    },
326    /// Reveal the top card of your deck; if its level is at least `min_level`,
327    /// move this card to hand. (Climax is treated as level 0.)
328    RevealTopIfLevelAtLeastMoveThisToHand {
329        /// Minimum level threshold for success.
330        min_level: u8,
331    },
332    /// Reveal the top card of your deck; if its level is at least `min_level`,
333    /// rest this card. (Climax is treated as level 0.)
334    RevealTopIfLevelAtLeastRestThis {
335        /// Minimum level threshold for success.
336        min_level: u8,
337    },
338    /// Reveal the top card of your deck; if its level is at least `min_level`,
339    /// move that revealed card to stock. (Climax is treated as level 0.)
340    RevealTopIfLevelAtLeastMoveTopToStock {
341        /// Minimum level threshold for success.
342        min_level: u8,
343    },
344    /// Look at the top `count` cards of your deck and reorder them on top.
345    LookTopDeckReorder {
346        /// Number of cards to look at and reorder.
347        count: u8,
348    },
349    /// Look at the top card and either leave it on top or move it to waiting room.
350    LookTopCardTopOrWaitingRoom,
351    /// Look at the top card and either leave it on top or move it to deck bottom.
352    LookTopCardTopOrBottom,
353    /// Look at top cards, move up to `choose_count` cards with level at least `min_level` to hand,
354    /// and send the rest to waiting room.
355    SearchTopDeckToHandLevelAtLeastMillRest {
356        /// Number of cards to look at from the top of deck.
357        look_count: u8,
358        /// Maximum number of cards to move to hand.
359        choose_count: u8,
360        /// Minimum level threshold for cards eligible to move to hand.
361        min_level: u8,
362    },
363    /// Reveal top deck card, then salvage up to `count` waiting-room characters with level at most
364    /// the revealed card's level.
365    RevealTopAndSalvageByRevealedLevel {
366        /// Number of cards to salvage.
367        count: u8,
368        /// Level to treat a climax as during level comparisons.
369        climax_level: u8,
370    },
371    /// Move the trigger card to hand.
372    MoveTriggerCardToHand,
373    /// Move the trigger card to stock.
374    MoveTriggerCardToStock,
375    /// Change controller of a card.
376    ChangeController {
377        /// New controller side.
378        new_controller: TargetSide,
379    },
380    /// Standby trigger resolution (place a character from waiting room onto stage).
381    Standby {
382        /// Destination stage slot index.
383        target_slot: u8,
384    },
385    /// Treasure trigger resolution (optionally take the stock).
386    TreasureStock {
387        /// Whether to take stock when resolving the treasure trigger.
388        take_stock: bool,
389    },
390    /// Modify pending attack damage by a delta.
391    ModifyPendingAttackDamage {
392        /// Signed delta to apply.
393        delta: i32,
394    },
395    /// Enable shot damage for the current attack.
396    EnableShotDamage {
397        /// Shot damage amount.
398        amount: u8,
399    },
400    /// Resolve a trigger icon effect directly.
401    TriggerIcon {
402        /// Trigger icon to resolve.
403        icon: TriggerIcon,
404    },
405    /// Reveal the top of the deck.
406    RevealDeckTop {
407        /// Number of cards to reveal.
408        count: u8,
409        /// Reveal visibility audience.
410        audience: RevealAudience,
411    },
412    /// Brainstorm resolver (reveal/mill then payoff per climax).
413    Brainstorm {
414        /// Number of cards to reveal/mill.
415        reveal_count: u8,
416        /// Payoff multiplier per climax revealed.
417        per_climax: u8,
418        /// Brainstorm payoff mode.
419        mode: BrainstormMode,
420    },
421    /// Optional brainstorm choice hook for draw-mode resolution.
422    BrainstormDrawChoice,
423    /// Set the total trigger checks to perform this attack's trigger step.
424    SetTriggerCheckCount {
425        /// Trigger check count to use.
426        count: u8,
427    },
428    /// Rest the source card if no other rested center-stage character is present.
429    RestThisIfNoOtherRestCenter,
430    /// Reverse this card's current battle opponent when a condition is met.
431    BattleOpponentReverseIf {
432        /// Optional maximum opponent level allowed.
433        max_level: Option<u8>,
434        /// Optional maximum opponent cost allowed.
435        max_cost: Option<u8>,
436        /// If true, require this card's level to exceed opponent level.
437        level_gt_opponent_level: bool,
438    },
439    /// Move this card's current battle opponent to the bottom of deck when a condition is met.
440    BattleOpponentMoveToDeckBottomIf {
441        /// Optional maximum opponent level allowed.
442        max_level: Option<u8>,
443        /// Optional maximum opponent cost allowed.
444        max_cost: Option<u8>,
445        /// If true, require this card's level to exceed opponent level.
446        level_gt_opponent_level: bool,
447    },
448    /// Move this card's current battle opponent to stock, then move the bottom stock card to
449    /// waiting room.
450    BattleOpponentMoveToStockThenBottomStockToWaitingRoomIf {
451        /// Optional maximum opponent level allowed.
452        max_level: Option<u8>,
453        /// Optional maximum opponent cost allowed.
454        max_cost: Option<u8>,
455        /// If true, require this card's level to exceed opponent level.
456        level_gt_opponent_level: bool,
457    },
458    /// Move top opponent clock to waiting room, then move this card's current battle opponent to
459    /// clock when a condition is met.
460    BattleOpponentMoveToClockAfterClockTopToWaitingRoomIf {
461        /// Optional maximum opponent level allowed.
462        max_level: Option<u8>,
463        /// Optional maximum opponent cost allowed.
464        max_cost: Option<u8>,
465        /// If true, require this card's level to exceed opponent level.
466        level_gt_opponent_level: bool,
467    },
468    /// Move this card's current battle opponent to memory when a condition is met.
469    BattleOpponentMoveToMemoryIf {
470        /// Optional maximum opponent level allowed.
471        max_level: Option<u8>,
472        /// Optional maximum opponent cost allowed.
473        max_cost: Option<u8>,
474        /// If true, require this card's level to exceed opponent level.
475        level_gt_opponent_level: bool,
476    },
477    /// Move this card's current battle opponent to clock when a condition is met.
478    BattleOpponentMoveToClockIf {
479        /// Optional maximum opponent level allowed.
480        max_level: Option<u8>,
481        /// Optional maximum opponent cost allowed.
482        max_cost: Option<u8>,
483        /// If true, require this card's level to exceed opponent level.
484        level_gt_opponent_level: bool,
485    },
486    /// Generalized battle-opponent movement effect.
487    BattleOpponentMove {
488        /// Destination zone for the battle opponent.
489        destination: BattleOpponentMoveDestination,
490        /// Optional prelude action applied before the destination move.
491        prelude: Option<BattleOpponentMovePreludeAction>,
492        /// Optional maximum opponent level allowed.
493        max_level: Option<u8>,
494        /// Optional maximum opponent cost allowed.
495        max_cost: Option<u8>,
496        /// If true, require this card's level to exceed opponent level.
497        level_gt_opponent_level: bool,
498    },
499    /// Put the top card of your deck into your stock if this card's battle opponent meets a level
500    /// threshold.
501    BattleOpponentTopDeckToStockIf {
502        /// Minimum opponent level threshold for success.
503        min_level: u8,
504    },
505    /// Prevent a player from using AUTO Encore for the rest of the turn.
506    CannotUseAutoEncoreForPlayer {
507        /// Side to apply the restriction to.
508        target: TargetSide,
509    },
510    /// Counter backup (power).
511    CounterBackup {
512        /// Power magnitude to add.
513        power: i32,
514    },
515    /// Counter damage reduction.
516    CounterDamageReduce {
517        /// Damage reduction magnitude.
518        amount: u8,
519    },
520    /// Counter damage cancel.
521    CounterDamageCancel,
522    /// Set terminal game outcome immediately.
523    SetTerminalOutcome {
524        /// Terminal outcome to set.
525        outcome: TerminalOutcomeSpec,
526    },
527    /// Apply a turn-scoped rule-action override.
528    ApplyRuleOverride {
529        /// Rule override kind to apply.
530        kind: RuleOverrideKind,
531    },
532}
533
534impl EffectKind {
535    /// Whether this effect expects a target to be selected.
536    pub fn expects_target(&self) -> bool {
537        matches!(
538            self,
539            EffectKind::AddModifier { .. }
540                | EffectKind::GrantAbilityDef { .. }
541                | EffectKind::AddPowerIfTargetLevelAtLeast { .. }
542                | EffectKind::AddPowerByTargetLevel { .. }
543                | EffectKind::ConditionalAddModifier { .. }
544                | EffectKind::MoveToHand
545                | EffectKind::MoveToWaitingRoom
546                | EffectKind::MoveToStock
547                | EffectKind::MoveToClock
548                | EffectKind::MoveToMemory
549                | EffectKind::MoveToDeckBottom
550                | EffectKind::MoveWaitingRoomCardToSourceSlot
551                | EffectKind::MoveToMarker
552                | EffectKind::Heal
553                | EffectKind::RestTarget
554                | EffectKind::StandTarget
555                | EffectKind::MoveStageSlot { .. }
556                | EffectKind::SwapStageSlots
557                | EffectKind::ChangeController { .. }
558                | EffectKind::Standby { .. }
559                | EffectKind::LookTopDeckReorder { .. }
560                | EffectKind::LookTopCardTopOrWaitingRoom
561                | EffectKind::LookTopCardTopOrBottom
562        )
563    }
564
565    /// Whether this effect can target a card in the given zone.
566    pub fn requires_target_zone(&self, zone: TargetZone) -> bool {
567        match self {
568            EffectKind::MoveToHand => {
569                matches!(
570                    zone,
571                    TargetZone::Stage | TargetZone::WaitingRoom | TargetZone::DeckTop
572                )
573            }
574            EffectKind::MoveToWaitingRoom => matches!(
575                zone,
576                TargetZone::Stage
577                    | TargetZone::Hand
578                    | TargetZone::DeckTop
579                    | TargetZone::Clock
580                    | TargetZone::Level
581                    | TargetZone::Stock
582                    | TargetZone::Memory
583                    | TargetZone::Climax
584                    | TargetZone::Resolution
585                    | TargetZone::WaitingRoom
586            ),
587            EffectKind::MoveToStock => matches!(
588                zone,
589                TargetZone::Stage
590                    | TargetZone::Hand
591                    | TargetZone::DeckTop
592                    | TargetZone::Clock
593                    | TargetZone::Level
594                    | TargetZone::WaitingRoom
595                    | TargetZone::Memory
596                    | TargetZone::Climax
597                    | TargetZone::Resolution
598                    | TargetZone::Stock
599            ),
600            EffectKind::MoveToClock => matches!(
601                zone,
602                TargetZone::Stage
603                    | TargetZone::Hand
604                    | TargetZone::DeckTop
605                    | TargetZone::WaitingRoom
606                    | TargetZone::Resolution
607                    | TargetZone::Clock
608            ),
609            EffectKind::MoveToMemory => matches!(
610                zone,
611                TargetZone::Stage
612                    | TargetZone::Hand
613                    | TargetZone::DeckTop
614                    | TargetZone::Clock
615                    | TargetZone::Level
616                    | TargetZone::Stock
617                    | TargetZone::WaitingRoom
618                    | TargetZone::Climax
619                    | TargetZone::Resolution
620                    | TargetZone::Memory
621            ),
622            EffectKind::MoveToDeckBottom => {
623                matches!(zone, TargetZone::Stage | TargetZone::DeckTop)
624            }
625            EffectKind::MoveWaitingRoomCardToSourceSlot => matches!(zone, TargetZone::WaitingRoom),
626            EffectKind::Heal => matches!(zone, TargetZone::Clock),
627            EffectKind::ChangeController { .. } => matches!(zone, TargetZone::Stage),
628            EffectKind::AddModifier { .. }
629            | EffectKind::GrantAbilityDef { .. }
630            | EffectKind::AddPowerIfTargetLevelAtLeast { .. }
631            | EffectKind::AddPowerByTargetLevel { .. }
632            | EffectKind::ConditionalAddModifier { .. } => {
633                matches!(zone, TargetZone::Stage)
634            }
635            EffectKind::MoveToMarker => matches!(zone, TargetZone::WaitingRoom),
636            EffectKind::LookTopDeckReorder { .. }
637            | EffectKind::LookTopCardTopOrWaitingRoom
638            | EffectKind::LookTopCardTopOrBottom => matches!(zone, TargetZone::DeckTop),
639            EffectKind::RestTarget
640            | EffectKind::StandTarget
641            | EffectKind::MoveStageSlot { .. }
642            | EffectKind::SwapStageSlots => matches!(zone, TargetZone::Stage),
643            EffectKind::Standby { .. } => matches!(zone, TargetZone::WaitingRoom),
644            EffectKind::RandomDiscardFromHand { .. } => matches!(zone, TargetZone::Hand),
645            EffectKind::RandomMill { .. } => matches!(zone, TargetZone::DeckTop),
646            EffectKind::RevealZoneTop {
647                zone: reveal_zone, ..
648            } => zone == *reveal_zone,
649            _ => true,
650        }
651    }
652}
653
654/// Effect with resolved targets ready for execution.
655#[derive(Clone, Debug, Hash, Serialize, Deserialize)]
656pub struct EffectPayload {
657    /// Underlying effect specification.
658    pub spec: EffectSpec,
659    /// Resolved targets for this effect.
660    pub targets: Vec<crate::state::TargetRef>,
661    /// Source reference for source-relative effects.
662    #[serde(default)]
663    pub source_ref: Option<crate::state::TargetRef>,
664}
665
666/// Hook point for replacement effects.
667#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
668pub enum ReplacementHook {
669    /// Damage resolution hook.
670    Damage,
671}
672
673/// Replacement behavior for a hook.
674#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
675pub enum ReplacementKind {
676    /// Cancel damage entirely.
677    CancelDamage,
678    /// Redirect damage to a new target.
679    RedirectDamage {
680        /// Target side to receive redirected damage.
681        new_target: TargetSide,
682    },
683}
684
685/// Replacement specification with priority ordering.
686#[derive(Clone, Debug, Hash, Serialize, Deserialize)]
687pub struct ReplacementSpec {
688    /// Stable effect id.
689    pub id: EffectId,
690    /// Source card id.
691    pub source: CardId,
692    /// Hook point for the replacement.
693    pub hook: ReplacementHook,
694    /// Replacement behavior.
695    pub kind: ReplacementKind,
696    /// Priority ordering (higher first).
697    pub priority: i16,
698    /// Insertion order for stable sorting.
699    pub insertion: u32,
700}