weiss_core/db/ability/
models.rs

1/// Cost requirements for an activated ability.
2#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
3pub enum AbilityCostStep {
4    #[serde(alias = "restOther", alias = "rest_other")]
5    /// Rest another character as part of the activation cost.
6    RestOther,
7    #[serde(alias = "sacrificeFromStage", alias = "sacrifice_from_stage")]
8    /// Put a character from stage into waiting room as part of the activation cost.
9    SacrificeFromStage,
10    #[serde(alias = "discardFromHand", alias = "discard_from_hand")]
11    /// Discard a card from hand as part of the activation cost.
12    DiscardFromHand,
13    #[serde(alias = "clockFromHand", alias = "clock_from_hand")]
14    /// Clock a card from hand as part of the activation cost.
15    ClockFromHand,
16    #[serde(alias = "clockFromDeckTop", alias = "clock_from_deck_top")]
17    /// Clock the top card(s) of the deck as part of the activation cost.
18    ClockFromDeckTop,
19    #[serde(alias = "revealFromHand", alias = "reveal_from_hand")]
20    /// Reveal a card from hand as part of the activation cost.
21    RevealFromHand,
22}
23
24/// Cost requirements for an activated ability.
25#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
26pub struct AbilityCost {
27    #[serde(default)]
28    /// Stock cost to pay.
29    pub stock: u8,
30    #[serde(default)]
31    /// Whether the source must rest itself.
32    pub rest_self: bool,
33    #[serde(default)]
34    /// Number of other characters to rest.
35    pub rest_other: u8,
36    #[serde(default, alias = "sacrificeFromStage")]
37    /// Characters to put from stage into waiting room as cost.
38    pub sacrifice_from_stage: u8,
39    #[serde(default)]
40    /// Cards to discard from hand.
41    pub discard_from_hand: u8,
42    #[serde(default)]
43    /// Cards to clock from hand.
44    pub clock_from_hand: u8,
45    #[serde(default)]
46    /// Cards to clock from top of deck.
47    pub clock_from_deck_top: u8,
48    #[serde(default)]
49    /// Cards to reveal from hand.
50    pub reveal_from_hand: u8,
51    #[serde(default, alias = "moveSelfToWaitingRoom")]
52    /// Whether the source card must be put into waiting room as cost.
53    pub move_self_to_waiting_room: bool,
54    #[serde(default, alias = "returnSelfToHand")]
55    /// Whether the source card must be returned to hand as cost.
56    pub return_self_to_hand: bool,
57    #[serde(default, alias = "stepOrder")]
58    /// Optional explicit ordering for staged cost steps.
59    pub step_order: Vec<AbilityCostStep>,
60}
61
62impl AbilityCost {
63    /// Whether this cost is empty (no payments required).
64    pub fn is_empty(&self) -> bool {
65        self.stock == 0
66            && !self.rest_self
67            && self.rest_other == 0
68            && self.sacrifice_from_stage == 0
69            && self.discard_from_hand == 0
70            && self.clock_from_hand == 0
71            && self.clock_from_deck_top == 0
72            && self.reveal_from_hand == 0
73            && !self.move_self_to_waiting_room
74            && !self.return_self_to_hand
75    }
76
77    /// Return the next pending staged step in explicit order, if any.
78    pub fn next_explicit_step(&self) -> Option<crate::state::CostStepKind> {
79        for step in &self.step_order {
80            match step {
81                AbilityCostStep::RestOther if self.rest_other > 0 => {
82                    return Some(crate::state::CostStepKind::RestOther);
83                }
84                AbilityCostStep::SacrificeFromStage if self.sacrifice_from_stage > 0 => {
85                    return Some(crate::state::CostStepKind::SacrificeFromStage);
86                }
87                AbilityCostStep::DiscardFromHand if self.discard_from_hand > 0 => {
88                    return Some(crate::state::CostStepKind::DiscardFromHand);
89                }
90                AbilityCostStep::ClockFromHand if self.clock_from_hand > 0 => {
91                    return Some(crate::state::CostStepKind::ClockFromHand);
92                }
93                AbilityCostStep::ClockFromDeckTop if self.clock_from_deck_top > 0 => {
94                    return Some(crate::state::CostStepKind::ClockFromDeckTop);
95                }
96                AbilityCostStep::RevealFromHand if self.reveal_from_hand > 0 => {
97                    return Some(crate::state::CostStepKind::RevealFromHand);
98                }
99                _ => {}
100            }
101        }
102        None
103    }
104}
105
106const fn default_target_side_self() -> TargetSide {
107    TargetSide::SelfSide
108}
109
110/// Ability-level conditional requirements.
111#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, Hash)]
112pub struct AbilityDefConditions {
113    #[serde(default, alias = "requiresApproxEffects")]
114    /// If true, this ability only runs when curriculum approximation effects are enabled.
115    pub requires_approx_effects: bool,
116    #[serde(default, alias = "climaxArea")]
117    /// Optional climax-area gate for this ability.
118    pub climax_area: Option<AbilityDefClimaxAreaCondition>,
119    #[serde(default)]
120    /// Optional turn gate for this ability.
121    pub turn: Option<ConditionTurn>,
122    #[serde(default, alias = "handLevelDelta")]
123    /// Optional play-from-hand level adjustment (typically negative).
124    pub hand_level_delta: i8,
125    #[serde(default, alias = "selfWaitingRoomClimaxAtMost")]
126    /// Optional gate on self waiting-room climax count.
127    pub self_waiting_room_climax_at_most: Option<u8>,
128    #[serde(default, alias = "selfClockCardIdsAny")]
129    /// Optional gate requiring at least one of these card ids in self clock.
130    pub self_clock_card_ids_any: Vec<CardId>,
131    #[serde(default, alias = "selfMemoryCardIdsAny")]
132    /// Optional gate requiring at least one of these card ids in self memory.
133    pub self_memory_card_ids_any: Vec<CardId>,
134    #[serde(default, alias = "selfMemoryAtMost")]
135    /// Optional gate requiring self memory count to be at most this value.
136    pub self_memory_at_most: Option<u8>,
137    #[serde(default, alias = "opponentStageHasLevelAtLeast")]
138    /// Optional gate requiring the opponent to have a stage character at or above this level.
139    pub opponent_stage_has_level_at_least: Option<u8>,
140    #[serde(default, alias = "triggerCheckRevealedClimax")]
141    /// Optional gate requiring the current trigger check card to be a climax.
142    pub trigger_check_revealed_climax: bool,
143    #[serde(default, alias = "triggerCheckRevealedIcon")]
144    /// Optional gate requiring the current trigger check card to include this trigger icon.
145    pub trigger_check_revealed_icon: Option<TriggerIcon>,
146    #[serde(default, alias = "ignoreColorRequirement")]
147    /// If true, this card can be played without color requirements while in hand.
148    pub ignore_color_requirement: bool,
149    #[serde(default, alias = "zoneCount")]
150    /// Optional generic zone-count gate for this ability.
151    pub zone_count: Option<ZoneCountCondition>,
152    #[serde(default, alias = "sourceRuleId")]
153    /// Optional parser-v2 rule-pack provenance identifier.
154    pub source_rule_id: Option<String>,
155}
156
157/// Climax-area gate configuration.
158#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
159pub struct AbilityDefClimaxAreaCondition {
160    #[serde(default = "default_target_side_self")]
161    /// Side whose climax area is checked.
162    pub side: TargetSide,
163    #[serde(default, alias = "cardIds")]
164    /// Allowed climax ids. Empty means any climax card satisfies the gate.
165    pub card_ids: Vec<CardId>,
166}
167
168impl Default for AbilityDefClimaxAreaCondition {
169    fn default() -> Self {
170        Self {
171            side: default_target_side_self(),
172            card_ids: Vec::new(),
173        }
174    }
175}
176
177impl AbilityDefConditions {
178    fn has_play_requirement_overrides(&self) -> bool {
179        self.hand_level_delta != 0 || self.ignore_color_requirement
180    }
181}
182
183/// Fully specified ability definition.
184#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
185pub struct AbilityDef {
186    /// Ability kind (continuous/activated/auto).
187    pub kind: AbilityKind,
188    /// Optional timing for auto/continuous effects.
189    pub timing: Option<AbilityTiming>,
190    /// Effect templates executed by this ability.
191    pub effects: Vec<EffectTemplate>,
192    #[serde(default)]
193    /// Optionality flags per effect index.
194    pub effect_optional: Vec<bool>,
195    /// Target templates for the ability.
196    pub targets: Vec<TargetTemplate>,
197    #[serde(default)]
198    /// Costs required to activate (only for activated abilities).
199    pub cost: AbilityCost,
200    #[serde(default, alias = "condition")]
201    /// Optional ability-level conditions.
202    pub conditions: AbilityDefConditions,
203    #[serde(default)]
204    /// Optional target card type restriction.
205    pub target_card_type: Option<CardType>,
206    #[serde(default)]
207    /// Optional target trait restriction.
208    pub target_trait: Option<u16>,
209    #[serde(default)]
210    /// Optional target max level restriction.
211    pub target_level_max: Option<u8>,
212    #[serde(default)]
213    /// Optional target max cost restriction.
214    pub target_cost_max: Option<u8>,
215    #[serde(default, alias = "targetCardIds")]
216    /// Optional target card-id restriction.
217    pub target_card_ids: Vec<CardId>,
218    #[serde(default)]
219    /// Optional target count limit.
220    pub target_limit: Option<u8>,
221}
222
223impl AbilityDef {
224    /// Validate structural constraints for the definition.
225    pub fn validate(&self) -> Result<()> {
226        if self.effects.is_empty() && !self.conditions.has_play_requirement_overrides() {
227            anyhow::bail!("AbilityDef must contain at least one effect");
228        }
229        if self.effects.len() > u8::MAX as usize {
230            anyhow::bail!("AbilityDef has too many effects");
231        }
232        if self.effect_optional.len() > self.effects.len() {
233            anyhow::bail!("AbilityDef optional flags exceed effects length");
234        }
235        if self.targets.len() > u8::MAX as usize {
236            anyhow::bail!("AbilityDef has too many targets");
237        }
238        if self.kind == AbilityKind::Continuous && !self.cost.is_empty() {
239            anyhow::bail!("AbilityDef cost is invalid for continuous abilities");
240        }
241        Ok(())
242    }
243}
244
245/// Timing windows for triggered abilities.
246#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
247pub enum AbilityTiming {
248    /// At the beginning of the turn.
249    BeginTurn,
250    /// At the beginning of the stand phase.
251    BeginStandPhase,
252    /// After the stand phase completes.
253    AfterStandPhase,
254    /// At the beginning of the draw phase.
255    BeginDrawPhase,
256    /// After the draw phase completes.
257    AfterDrawPhase,
258    /// At the beginning of the clock phase.
259    BeginClockPhase,
260    /// After the clock phase completes.
261    AfterClockPhase,
262    /// During the level-up procedure.
263    LevelUp,
264    /// At the beginning of the main phase.
265    BeginMainPhase,
266    /// At the beginning of the climax phase.
267    BeginClimaxPhase,
268    /// After the climax phase completes.
269    AfterClimaxPhase,
270    /// At the beginning of the attack phase.
271    BeginAttackPhase,
272    /// At the beginning of the attack declaration step.
273    BeginAttackDeclarationStep,
274    /// At the beginning of the encore step.
275    BeginEncoreStep,
276    /// During the end phase.
277    EndPhase,
278    /// During end-phase cleanup.
279    EndPhaseCleanup,
280    /// After an attack finishes resolving.
281    EndOfAttack,
282    /// When declaring an attack.
283    AttackDeclaration,
284    /// When the opponent declares an attack.
285    OtherAttackDeclaration,
286    /// During trigger resolution.
287    TriggerResolution,
288    /// During counter timing.
289    Counter,
290    /// When using an ACT ability.
291    UseAct,
292    /// During damage resolution.
293    DamageResolution,
294    /// During encore timing.
295    Encore,
296    /// When the source is played.
297    OnPlay,
298    /// When the source becomes reversed.
299    OnReverse,
300    /// When the battle opponent becomes reversed.
301    BattleOpponentReverse,
302    /// After dealing damage that was not canceled.
303    DamageDealtNotCanceled,
304    /// After receiving damage that was not canceled.
305    DamageReceivedNotCanceled,
306    /// After dealing damage that was canceled.
307    DamageDealtCanceled,
308    /// After receiving damage that was canceled.
309    DamageReceivedCanceled,
310}
311
312/// Template-driven ability definitions used by the DB loader.
313#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
314#[allow(clippy::large_enum_variant)]
315pub enum AbilityTemplate {
316    /// No special behavior (placeholder template).
317    Vanilla,
318    /// Continuous power modifier while on stage.
319    ContinuousPower {
320        /// Power delta to apply.
321        amount: i32,
322    },
323    /// Continuous "cannot attack" modifier while on stage.
324    ContinuousCannotAttack,
325    /// Continuous attack cost modifier while on stage.
326    ContinuousAttackCost {
327        /// Additional stock cost to declare an attack.
328        cost: u8,
329    },
330    /// Auto ability: on play, draw cards.
331    AutoOnPlayDraw {
332        /// Number of cards to draw.
333        count: u8,
334    },
335    /// Auto ability: on play, salvage cards from waiting room.
336    AutoOnPlaySalvage {
337        /// Number of cards to salvage.
338        count: u8,
339        /// Whether salvaging is optional.
340        optional: bool,
341        /// Optional card type restriction.
342        card_type: Option<CardType>,
343    },
344    /// Auto ability: on play, search the top of the deck and take cards.
345    AutoOnPlaySearchDeckTop {
346        /// Maximum number of cards to take.
347        count: u8,
348        /// Whether taking a card is optional.
349        optional: bool,
350        /// Optional card type restriction.
351        card_type: Option<CardType>,
352    },
353    /// Auto ability: on play, reveal the top cards of the deck.
354    AutoOnPlayRevealDeckTop {
355        /// Number of cards to reveal.
356        count: u8,
357    },
358    /// Auto ability: on play, stock charge.
359    AutoOnPlayStockCharge {
360        /// Number of cards to stock charge.
361        count: u8,
362    },
363    /// Auto ability: on play, mill cards from the top of the deck.
364    AutoOnPlayMillTop {
365        /// Number of cards to mill.
366        count: u8,
367    },
368    /// Auto ability: on play, heal.
369    AutoOnPlayHeal {
370        /// Number of clock cards to heal.
371        count: u8,
372    },
373    /// Auto ability: on attack, deal effect damage.
374    AutoOnAttackDealDamage {
375        /// Damage amount.
376        amount: u8,
377        /// Whether the damage is cancelable.
378        cancelable: bool,
379    },
380    /// Auto ability: end of phase draw.
381    AutoEndPhaseDraw {
382        /// Number of cards to draw.
383        count: u8,
384    },
385    /// Auto ability: on reverse, draw.
386    AutoOnReverseDraw {
387        /// Number of cards to draw.
388        count: u8,
389    },
390    /// Auto ability: on reverse, salvage cards from waiting room.
391    AutoOnReverseSalvage {
392        /// Number of cards to salvage.
393        count: u8,
394        /// Whether salvaging is optional.
395        optional: bool,
396        /// Optional card type restriction.
397        card_type: Option<CardType>,
398    },
399    /// Event ability: deal effect damage.
400    EventDealDamage {
401        /// Damage amount.
402        amount: u8,
403        /// Whether the damage is cancelable.
404        cancelable: bool,
405    },
406    /// Placeholder for an activated ability without a concrete template.
407    ActivatedPlaceholder,
408    /// Activated ability: grant power to targets.
409    ActivatedTargetedPower {
410        /// Power delta to apply.
411        amount: i32,
412        /// Number of targets to select.
413        count: u8,
414        /// Target template to select from.
415        target: TargetTemplate,
416    },
417    /// Activated ability (paid): grant power to targets.
418    ActivatedPaidTargetedPower {
419        /// Stock cost to pay.
420        cost: u8,
421        /// Power delta to apply.
422        amount: i32,
423        /// Number of targets to select.
424        count: u8,
425        /// Target template to select from.
426        target: TargetTemplate,
427    },
428    /// Activated ability: move selected targets to hand.
429    ActivatedTargetedMoveToHand {
430        /// Number of targets to select.
431        count: u8,
432        /// Target template to select from.
433        target: TargetTemplate,
434    },
435    /// Activated ability (paid): move selected targets to hand.
436    ActivatedPaidTargetedMoveToHand {
437        /// Stock cost to pay.
438        cost: u8,
439        /// Number of targets to select.
440        count: u8,
441        /// Target template to select from.
442        target: TargetTemplate,
443    },
444    /// Activated ability: change controller of selected targets.
445    ActivatedChangeController {
446        /// Number of targets to select.
447        count: u8,
448        /// Target template to select from.
449        target: TargetTemplate,
450    },
451    /// Activated ability (paid): change controller of selected targets.
452    ActivatedPaidChangeController {
453        /// Stock cost to pay.
454        cost: u8,
455        /// Number of targets to select.
456        count: u8,
457        /// Target template to select from.
458        target: TargetTemplate,
459    },
460    /// Counter ability: power backup.
461    CounterBackup {
462        /// Power amount to add.
463        power: i32,
464    },
465    /// Counter ability: reduce incoming damage.
466    CounterDamageReduce {
467        /// Reduction amount.
468        amount: u8,
469    },
470    /// Counter ability: cancel the next damage instance.
471    CounterDamageCancel,
472    /// Activated ability: "bond" search with structured cost.
473    Bond {
474        /// Activation cost specification.
475        cost: AbilityCost,
476        /// Number of cards to search for.
477        count: u8,
478        #[serde(default)]
479        /// Optional card id whitelist for bond targets.
480        target_ids: Vec<CardId>,
481    },
482    /// Encore ability variant with structured cost.
483    EncoreVariant {
484        /// Encore cost specification.
485        cost: AbilityCost,
486    },
487    /// Fully specified ability definition parsed from a rule pack.
488    AbilityDef(
489        /// Definition payload.
490        AbilityDef,
491    ),
492    /// Unknown/unsupported ability template id.
493    Unsupported {
494        /// Raw template id encountered during parsing.
495        id: u32,
496    },
497}
498
499/// High-level ability kind.
500#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
501pub enum AbilityKind {
502    /// Continuous modifiers that always apply.
503    Continuous,
504    /// Activated abilities with explicit costs.
505    Activated,
506    /// Auto abilities that trigger at timings.
507    Auto,
508}
509
510/// Canonical ability specification after parsing.
511#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
512pub struct AbilitySpec {
513    /// Ability kind (continuous/activated/auto).
514    pub kind: AbilityKind,
515    /// Template describing behavior.
516    pub template: AbilityTemplate,
517}
518
519/// Lightweight tags for ability templates (used in analytics/validation).
520#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
521pub enum AbilityTemplateTag {
522    /// Tag for `AbilityTemplate::Vanilla`.
523    Vanilla,
524    /// Tag for `AbilityTemplate::ContinuousPower`.
525    ContinuousPower,
526    /// Tag for `AbilityTemplate::ContinuousCannotAttack`.
527    ContinuousCannotAttack,
528    /// Tag for `AbilityTemplate::ContinuousAttackCost`.
529    ContinuousAttackCost,
530    /// Tag for `AbilityTemplate::AutoOnPlayDraw`.
531    AutoOnPlayDraw,
532    /// Tag for `AbilityTemplate::AutoOnPlaySalvage`.
533    AutoOnPlaySalvage,
534    /// Tag for `AbilityTemplate::AutoOnPlaySearchDeckTop`.
535    AutoOnPlaySearchDeckTop,
536    /// Tag for `AbilityTemplate::AutoOnPlayRevealDeckTop`.
537    AutoOnPlayRevealDeckTop,
538    /// Tag for `AbilityTemplate::AutoOnPlayStockCharge`.
539    AutoOnPlayStockCharge,
540    /// Tag for `AbilityTemplate::AutoOnPlayMillTop`.
541    AutoOnPlayMillTop,
542    /// Tag for `AbilityTemplate::AutoOnPlayHeal`.
543    AutoOnPlayHeal,
544    /// Tag for `AbilityTemplate::AutoOnAttackDealDamage`.
545    AutoOnAttackDealDamage,
546    /// Tag for `AbilityTemplate::AutoEndPhaseDraw`.
547    AutoEndPhaseDraw,
548    /// Tag for `AbilityTemplate::AutoOnReverseDraw`.
549    AutoOnReverseDraw,
550    /// Tag for `AbilityTemplate::AutoOnReverseSalvage`.
551    AutoOnReverseSalvage,
552    /// Tag for `AbilityTemplate::EventDealDamage`.
553    EventDealDamage,
554    /// Tag for `AbilityTemplate::ActivatedPlaceholder`.
555    ActivatedPlaceholder,
556    /// Tag for `AbilityTemplate::ActivatedTargetedPower`.
557    ActivatedTargetedPower,
558    /// Tag for `AbilityTemplate::ActivatedPaidTargetedPower`.
559    ActivatedPaidTargetedPower,
560    /// Tag for `AbilityTemplate::ActivatedTargetedMoveToHand`.
561    ActivatedTargetedMoveToHand,
562    /// Tag for `AbilityTemplate::ActivatedPaidTargetedMoveToHand`.
563    ActivatedPaidTargetedMoveToHand,
564    /// Tag for `AbilityTemplate::ActivatedChangeController`.
565    ActivatedChangeController,
566    /// Tag for `AbilityTemplate::ActivatedPaidChangeController`.
567    ActivatedPaidChangeController,
568    /// Tag for `AbilityTemplate::CounterBackup`.
569    CounterBackup,
570    /// Tag for `AbilityTemplate::CounterDamageReduce`.
571    CounterDamageReduce,
572    /// Tag for `AbilityTemplate::CounterDamageCancel`.
573    CounterDamageCancel,
574    /// Tag for `AbilityTemplate::Bond`.
575    Bond,
576    /// Tag for `AbilityTemplate::EncoreVariant`.
577    EncoreVariant,
578    /// Tag for `AbilityTemplate::AbilityDef`.
579    AbilityDef,
580    /// Tag for `AbilityTemplate::Unsupported`.
581    Unsupported,
582}
583
584impl AbilityTemplate {
585    /// Return the template tag for this ability.
586    pub fn tag(&self) -> AbilityTemplateTag {
587        match self {
588            AbilityTemplate::Vanilla => AbilityTemplateTag::Vanilla,
589            AbilityTemplate::ContinuousPower { .. } => AbilityTemplateTag::ContinuousPower,
590            AbilityTemplate::ContinuousCannotAttack => AbilityTemplateTag::ContinuousCannotAttack,
591            AbilityTemplate::ContinuousAttackCost { .. } => {
592                AbilityTemplateTag::ContinuousAttackCost
593            }
594            AbilityTemplate::AutoOnPlayDraw { .. } => AbilityTemplateTag::AutoOnPlayDraw,
595            AbilityTemplate::AutoOnPlaySalvage { .. } => AbilityTemplateTag::AutoOnPlaySalvage,
596            AbilityTemplate::AutoOnPlaySearchDeckTop { .. } => {
597                AbilityTemplateTag::AutoOnPlaySearchDeckTop
598            }
599            AbilityTemplate::AutoOnPlayRevealDeckTop { .. } => {
600                AbilityTemplateTag::AutoOnPlayRevealDeckTop
601            }
602            AbilityTemplate::AutoOnPlayStockCharge { .. } => {
603                AbilityTemplateTag::AutoOnPlayStockCharge
604            }
605            AbilityTemplate::AutoOnPlayMillTop { .. } => AbilityTemplateTag::AutoOnPlayMillTop,
606            AbilityTemplate::AutoOnPlayHeal { .. } => AbilityTemplateTag::AutoOnPlayHeal,
607            AbilityTemplate::AutoOnAttackDealDamage { .. } => {
608                AbilityTemplateTag::AutoOnAttackDealDamage
609            }
610            AbilityTemplate::AutoEndPhaseDraw { .. } => AbilityTemplateTag::AutoEndPhaseDraw,
611            AbilityTemplate::AutoOnReverseDraw { .. } => AbilityTemplateTag::AutoOnReverseDraw,
612            AbilityTemplate::AutoOnReverseSalvage { .. } => {
613                AbilityTemplateTag::AutoOnReverseSalvage
614            }
615            AbilityTemplate::EventDealDamage { .. } => AbilityTemplateTag::EventDealDamage,
616            AbilityTemplate::ActivatedPlaceholder => AbilityTemplateTag::ActivatedPlaceholder,
617            AbilityTemplate::ActivatedTargetedPower { .. } => {
618                AbilityTemplateTag::ActivatedTargetedPower
619            }
620            AbilityTemplate::ActivatedPaidTargetedPower { .. } => {
621                AbilityTemplateTag::ActivatedPaidTargetedPower
622            }
623            AbilityTemplate::ActivatedTargetedMoveToHand { .. } => {
624                AbilityTemplateTag::ActivatedTargetedMoveToHand
625            }
626            AbilityTemplate::ActivatedPaidTargetedMoveToHand { .. } => {
627                AbilityTemplateTag::ActivatedPaidTargetedMoveToHand
628            }
629            AbilityTemplate::ActivatedChangeController { .. } => {
630                AbilityTemplateTag::ActivatedChangeController
631            }
632            AbilityTemplate::ActivatedPaidChangeController { .. } => {
633                AbilityTemplateTag::ActivatedPaidChangeController
634            }
635            AbilityTemplate::CounterBackup { .. } => AbilityTemplateTag::CounterBackup,
636            AbilityTemplate::CounterDamageReduce { .. } => AbilityTemplateTag::CounterDamageReduce,
637            AbilityTemplate::CounterDamageCancel => AbilityTemplateTag::CounterDamageCancel,
638            AbilityTemplate::Bond { .. } => AbilityTemplateTag::Bond,
639            AbilityTemplate::EncoreVariant { .. } => AbilityTemplateTag::EncoreVariant,
640            AbilityTemplate::AbilityDef(_) => AbilityTemplateTag::AbilityDef,
641            AbilityTemplate::Unsupported { .. } => AbilityTemplateTag::Unsupported,
642        }
643    }
644
645    /// Return the stock cost for activated templates (if any).
646    pub fn activation_cost(&self) -> Option<u8> {
647        match self {
648            AbilityTemplate::ActivatedPaidTargetedPower { cost, .. }
649            | AbilityTemplate::ActivatedPaidTargetedMoveToHand { cost, .. }
650            | AbilityTemplate::ActivatedPaidChangeController { cost, .. } => Some(*cost),
651            _ => None,
652        }
653    }
654
655    /// Return a full cost spec for activated templates.
656    pub fn activation_cost_spec(&self) -> AbilityCost {
657        match self {
658            AbilityTemplate::ActivatedPaidTargetedPower { cost, .. }
659            | AbilityTemplate::ActivatedPaidTargetedMoveToHand { cost, .. }
660            | AbilityTemplate::ActivatedPaidChangeController { cost, .. } => AbilityCost {
661                stock: *cost,
662                ..AbilityCost::default()
663            },
664            AbilityTemplate::Bond { cost, .. } => cost.clone(),
665            AbilityTemplate::AbilityDef(def) => def.cost.clone(),
666            _ => AbilityCost::default(),
667        }
668    }
669
670    /// Return encore variant cost for keyword encore templates.
671    pub fn encore_variant_cost(&self) -> Option<AbilityCost> {
672        match self {
673            AbilityTemplate::EncoreVariant { cost } => Some(cost.clone()),
674            _ => None,
675        }
676    }
677
678    /// Return the implied timing for this template, if any.
679    pub fn timing(&self) -> Option<AbilityTiming> {
680        match self {
681            AbilityTemplate::AutoOnPlayDraw { .. }
682            | AbilityTemplate::AutoOnPlaySalvage { .. }
683            | AbilityTemplate::AutoOnPlaySearchDeckTop { .. }
684            | AbilityTemplate::AutoOnPlayRevealDeckTop { .. }
685            | AbilityTemplate::AutoOnPlayStockCharge { .. }
686            | AbilityTemplate::AutoOnPlayMillTop { .. }
687            | AbilityTemplate::AutoOnPlayHeal { .. }
688            | AbilityTemplate::Bond { .. } => Some(AbilityTiming::OnPlay),
689            AbilityTemplate::AutoOnAttackDealDamage { .. } => {
690                Some(AbilityTiming::AttackDeclaration)
691            }
692            AbilityTemplate::AutoEndPhaseDraw { .. } => Some(AbilityTiming::EndPhase),
693            AbilityTemplate::AutoOnReverseDraw { .. } => Some(AbilityTiming::OnReverse),
694            AbilityTemplate::AutoOnReverseSalvage { .. } => Some(AbilityTiming::OnReverse),
695            AbilityTemplate::CounterBackup { .. }
696            | AbilityTemplate::CounterDamageReduce { .. }
697            | AbilityTemplate::CounterDamageCancel => Some(AbilityTiming::Counter),
698            AbilityTemplate::EncoreVariant { .. } => Some(AbilityTiming::Encore),
699            AbilityTemplate::EventDealDamage { .. } => Some(AbilityTiming::OnPlay),
700            AbilityTemplate::AbilityDef(def) => def.timing,
701            _ => None,
702        }
703    }
704
705    /// Whether this template represents an event play.
706    pub fn is_event_play(&self) -> bool {
707        matches!(self, AbilityTemplate::EventDealDamage { .. })
708    }
709}
710
711impl AbilitySpec {
712    /// Build an ability spec from a template.
713    pub fn from_template(template: &AbilityTemplate) -> Self {
714        let kind = match template {
715            AbilityTemplate::ContinuousPower { .. }
716            | AbilityTemplate::ContinuousCannotAttack
717            | AbilityTemplate::ContinuousAttackCost { .. } => AbilityKind::Continuous,
718            AbilityTemplate::ActivatedPlaceholder
719            | AbilityTemplate::ActivatedTargetedPower { .. }
720            | AbilityTemplate::ActivatedPaidTargetedPower { .. }
721            | AbilityTemplate::ActivatedTargetedMoveToHand { .. }
722            | AbilityTemplate::ActivatedPaidTargetedMoveToHand { .. }
723            | AbilityTemplate::ActivatedChangeController { .. }
724            | AbilityTemplate::ActivatedPaidChangeController { .. } => AbilityKind::Activated,
725            AbilityTemplate::AbilityDef(def) => def.kind,
726            _ => AbilityKind::Auto,
727        };
728        Self {
729            kind,
730            template: template.clone(),
731        }
732    }
733
734    /// Return the implied timing for this spec, if any.
735    pub fn timing(&self) -> Option<AbilityTiming> {
736        self.template.timing()
737    }
738}