Skip to main content

weiss_core/db/ability/
models.rs

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