Skip to main content

manabrew_engine/event/
mod.rs

1use forge_foundation::{PhaseType, ZoneType};
2use serde::{Deserialize, Serialize};
3use std::collections::{BTreeMap, HashMap};
4
5use crate::ability::AbilityKey;
6use crate::card::card_damage_map::CardDamageMap;
7use crate::card::card_zone_table::CardZoneTable;
8use crate::ids::{CardId, PlayerId};
9use strum_macros::Display;
10
11// `TriggerType` was moved to `crate::trigger::trigger_type`. Nothing in this
12// module references it directly — callers must use `crate::trigger::TriggerType`.
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct ZoneChangeRecord {
16    pub origin: ZoneType,
17    pub destination: ZoneType,
18    pub card: CardId,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize, Display)]
22#[allow(clippy::large_enum_variant)]
23pub enum AbilityValue {
24    Card(CardId),
25    Player(PlayerId),
26    Cards(Vec<CardId>),
27    Players(Vec<PlayerId>),
28    VoteMap(Vec<(String, Vec<PlayerId>)>),
29    SpellAbility(crate::spellability::SpellAbility),
30    CardZoneTable(CardZoneTable),
31    DamageMap(CardDamageMap),
32    CounterMap(BTreeMap<String, i32>),
33    String(String),
34    Int(i32),
35    Bool(bool),
36    Zone(ZoneType),
37    Phase(PhaseType),
38}
39
40impl Default for AbilityValue {
41    fn default() -> Self {
42        AbilityValue::String(String::new())
43    }
44}
45
46impl AbilityValue {
47    pub fn as_str(&self) -> &str {
48        match self {
49            AbilityValue::String(value) => value.as_str(),
50            _ => "",
51        }
52    }
53}
54
55impl std::ops::Deref for AbilityValue {
56    type Target = str;
57
58    fn deref(&self) -> &Self::Target {
59        self.as_str()
60    }
61}
62
63impl From<CardId> for AbilityValue {
64    fn from(value: CardId) -> Self {
65        AbilityValue::Card(value)
66    }
67}
68
69impl From<PlayerId> for AbilityValue {
70    fn from(value: PlayerId) -> Self {
71        AbilityValue::Player(value)
72    }
73}
74
75impl From<Vec<CardId>> for AbilityValue {
76    fn from(value: Vec<CardId>) -> Self {
77        AbilityValue::Cards(value)
78    }
79}
80
81impl From<Vec<PlayerId>> for AbilityValue {
82    fn from(value: Vec<PlayerId>) -> Self {
83        AbilityValue::Players(value)
84    }
85}
86
87impl From<Vec<(String, Vec<PlayerId>)>> for AbilityValue {
88    fn from(value: Vec<(String, Vec<PlayerId>)>) -> Self {
89        AbilityValue::VoteMap(value)
90    }
91}
92
93impl From<crate::spellability::SpellAbility> for AbilityValue {
94    fn from(value: crate::spellability::SpellAbility) -> Self {
95        AbilityValue::SpellAbility(value)
96    }
97}
98
99impl From<CardZoneTable> for AbilityValue {
100    fn from(value: CardZoneTable) -> Self {
101        AbilityValue::CardZoneTable(value)
102    }
103}
104
105impl From<CardDamageMap> for AbilityValue {
106    fn from(value: CardDamageMap) -> Self {
107        AbilityValue::DamageMap(value)
108    }
109}
110
111impl From<BTreeMap<String, i32>> for AbilityValue {
112    fn from(value: BTreeMap<String, i32>) -> Self {
113        AbilityValue::CounterMap(value)
114    }
115}
116
117impl From<String> for AbilityValue {
118    fn from(value: String) -> Self {
119        AbilityValue::String(value)
120    }
121}
122
123impl From<&str> for AbilityValue {
124    fn from(value: &str) -> Self {
125        AbilityValue::String(value.to_string())
126    }
127}
128
129impl From<&String> for AbilityValue {
130    fn from(value: &String) -> Self {
131        AbilityValue::String(value.clone())
132    }
133}
134
135impl From<i32> for AbilityValue {
136    fn from(value: i32) -> Self {
137        AbilityValue::Int(value)
138    }
139}
140
141impl From<bool> for AbilityValue {
142    fn from(value: bool) -> Self {
143        AbilityValue::Bool(value)
144    }
145}
146
147impl From<ZoneType> for AbilityValue {
148    fn from(value: ZoneType) -> Self {
149        AbilityValue::Zone(value)
150    }
151}
152
153impl From<PhaseType> for AbilityValue {
154    fn from(value: PhaseType) -> Self {
155        AbilityValue::Phase(value)
156    }
157}
158
159/// Typed event parameter keys — mirrors Java AbilityKey enum.
160/// In Java this is Map<AbilityKey, Object>. In Rust we use a struct
161/// because Rust has no Object type (justified deviation).
162#[derive(Debug, Clone, Default, Serialize, Deserialize)]
163pub struct RunParams {
164    pub card: Option<CardId>,
165    pub card_lki: Option<CardId>,
166    /// Additional card collection payload used by many Java triggers (AbilityKey.Cards).
167    pub cards: Option<Vec<CardId>>,
168    /// Batched zone-change payload used by Java's CardZoneTable triggers.
169    pub zone_changes: Option<Vec<ZoneChangeRecord>>,
170    /// Java-style `CardZoneTable` object payload for batch zone-change triggers.
171    pub change_zone_table: Option<CardZoneTable>,
172    pub origin: Option<ZoneType>,
173    pub destination: Option<ZoneType>,
174    /// CSV destination payload used by TriggerAbilityTriggered for batch triggers.
175    pub destinations: Option<String>,
176    /// Java AbilityKey.Activator.
177    pub activator: Option<PlayerId>,
178    pub cause_player: Option<PlayerId>,
179    pub player: Option<PlayerId>,
180    pub phase: Option<PhaseType>,
181    pub damage_source: Option<CardId>,
182    pub damage_target_player: Option<PlayerId>,
183    pub damage_target_card: Option<CardId>,
184    /// Java AbilityKey.Target payload split by target type.
185    pub target_player: Option<PlayerId>,
186    pub target_card: Option<CardId>,
187    pub damage_amount: Option<i32>,
188    pub is_combat_damage: Option<bool>,
189    /// Java AbilityKey.FirstTime marker.
190    pub first_time: Option<bool>,
191    /// Java AbilityKey.Fizzle marker.
192    pub fizzle: Option<bool>,
193    /// Java AbilityKey.Valiant marker.
194    pub valiant: Option<bool>,
195    pub attacker: Option<CardId>,
196    /// Java AbilityKey.Attacked split by entity type.
197    pub attacked_player: Option<PlayerId>,
198    pub attacked_card: Option<CardId>,
199    /// Java AbilityKey.OtherAttackers.
200    pub other_attacker_ids: Option<Vec<CardId>>,
201    /// Java AbilityKey.Defenders split by entity type.
202    pub defenders_player_ids: Option<Vec<PlayerId>>,
203    pub defenders_card_ids: Option<Vec<CardId>>,
204    /// Java AbilityKey.AttackingPlayer.
205    pub attacking_player: Option<PlayerId>,
206    pub defending_player: Option<PlayerId>,
207    pub spell_card: Option<CardId>,
208    pub spell_controller: Option<PlayerId>,
209    /// Second card involved (e.g. second creature in a Fight trigger).
210    pub card2: Option<CardId>,
211    /// Java AbilityKey.Explored.
212    pub explored: Option<CardId>,
213    /// SpellAbility that was countered
214    pub spell_ability: Option<crate::spellability::SpellAbility>,
215    /// Java AbilityKey.SourceSA.
216    pub source_sa: Option<crate::spellability::SpellAbility>,
217    /// Java AbilityKey.AbilityMana.
218    pub ability_mana: Option<crate::spellability::SpellAbility>,
219    /// Cause of the event (e.g. counterspell)
220    pub cause: Option<crate::spellability::SpellAbility>,
221    /// Java AbilityKey.Causer payload.
222    pub causer: Option<CardId>,
223    /// Java AbilityKey.Produced.
224    pub produced: Option<String>,
225    /// Java AbilityKey.Mode.
226    pub mode: Option<String>,
227    /// Java AbilityKey.Num.
228    pub num: Option<i32>,
229    /// Java AbilityKey.Number.
230    pub number: Option<i32>,
231    // ── New fields (issue #19) ──
232    /// Blocking creature (for Blocks trigger).
233    pub blocker: Option<CardId>,
234    /// Attacker being blocked (for Blocks trigger).
235    pub blocked_attacker: Option<CardId>,
236    /// Life amount gained or lost (for LifeGained/LifeLost triggers).
237    pub life_amount: Option<i32>,
238    /// Counter type name (for CounterAdded/CounterRemoved triggers).
239    pub counter_type: Option<String>,
240    /// Number of counters added/removed.
241    pub counter_amount: Option<i32>,
242    // ── New fields (issue #54) ──
243    /// Batch of attacker IDs (for AttackersDeclared).
244    pub attacker_ids: Option<Vec<CardId>>,
245    /// Batch of blocker IDs (for BlockersDeclared).
246    pub blocker_ids: Option<Vec<CardId>>,
247    /// Original controller before a control change.
248    pub original_controller: Option<PlayerId>,
249    /// Cumulative mana expend amount (for ManaExpend trigger).
250    pub mana_expend_amount: Option<i32>,
251    /// Enlisted card (for TriggerType::Enlisted).
252    pub enlisted: Option<CardId>,
253    /// The spell/ability card that caused the event (for BecomesTarget — the targeting spell).
254    pub cause_card: Option<CardId>,
255    /// Coin-flip outcome (true = win/heads).
256    pub coin_flip_won: Option<bool>,
257    /// Rolled die result (modified).
258    pub die_result: Option<i32>,
259    /// Batch of rolled die results (for RolledDieOnce aggregate triggers).
260    pub die_results: Option<Vec<i32>>,
261    /// Rolled die natural result before modifiers.
262    pub natural_result: Option<i32>,
263    /// Number of sides on the rolled die.
264    pub die_sides: Option<i32>,
265    /// Number of attackers declared this combat (for Exalted `Alone$ True` check).
266    pub num_attackers: Option<usize>,
267    /// The creature that was exploited (for Exploited trigger).
268    pub exploited_card: Option<CardId>,
269    /// LKI +1/+1 counter count on a card that just left the battlefield.
270    /// Used by Modular triggers to know how many counters to move.
271    pub lki_p1p1_counters: Option<i32>,
272    /// LKI power on a card that just left the battlefield.
273    /// Used for TriggeredCard$CardPower without depending on mutable card state.
274    pub lki_power: Option<i32>,
275    /// LKI toughness on a card that just left the battlefield.
276    /// Used for TriggeredCard$CardToughness without depending on mutable card state.
277    pub lki_toughness: Option<i32>,
278    /// Whether cumulative upkeep was paid (for PayCumulativeUpkeep trigger).
279    pub cumulative_upkeep_paid: Option<bool>,
280    /// Whether echo was paid (for PayEcho trigger).
281    pub echo_paid: Option<bool>,
282    /// Gained class level value.
283    pub class_level: Option<i32>,
284    /// Room name payload for room-enter triggers.
285    pub room_name: Option<String>,
286    /// Cards that crewed/saddled another card.
287    pub crew_cards: Option<Vec<CardId>>,
288    /// Championed card payload.
289    pub championed_card: Option<CardId>,
290    /// Generic source card payload for triggers like Mentored.
291    pub source_card: Option<CardId>,
292    /// Generic source player payload for triggers like CounterPlayerAddedAll.
293    pub source_player: Option<PlayerId>,
294    /// Generic object card payload for triggers like CounterTypeAddedAll.
295    pub object_card: Option<CardId>,
296    /// Generic object player payload for triggers like CounterTypeAddedAll.
297    pub object_player: Option<PlayerId>,
298    /// Counter type -> amount map payload.
299    pub counter_map: Option<BTreeMap<String, i32>>,
300    /// Java AbilityKey.DamageMap.
301    pub damage_map: Option<CardDamageMap>,
302    /// Clash outcome.
303    pub clash_won: Option<bool>,
304    /// Card state name payload (for door/room state specific checks).
305    pub card_state_name: Option<String>,
306    /// Snapshot of drawn_this_turn at the time a Drawn event fires.
307    /// Used by `Number$ N` triggers to compare against the exact draw count
308    /// at fire time (not at deferred match time).
309    pub drawn_this_turn_snapshot: Option<i32>,
310    /// Players for whom this was the first relevant event this turn.
311    pub first_time_players: Option<Vec<PlayerId>>,
312    /// Java AbilityKey.AllVotes.
313    pub all_votes: Option<Vec<(String, Vec<PlayerId>)>>,
314    /// Java AbilityKey.DiscardedBefore.
315    pub discarded_before: Option<Vec<CardId>>,
316    /// Java AbilityKey.RolledToVisitAttractions.
317    pub rolled_to_visit_attractions: Option<bool>,
318}
319
320impl RunParams {
321    pub fn add_common_trigger_objects(&self, sa: &mut crate::spellability::SpellAbility) {
322        if let Some(card_id) = self.card {
323            sa.set_triggering_object(crate::ability::AbilityKey::Card, card_id.0.to_string());
324            sa.set_triggering_object(crate::ability::AbilityKey::NewCard, card_id.0.to_string());
325        }
326        if let Some(card_id) = self.card_lki {
327            sa.set_triggering_object(crate::ability::AbilityKey::CardLKI, card_id.0.to_string());
328        }
329        if let Some(player_id) = self.activator.or(self.cause_player) {
330            sa.set_triggering_object(
331                crate::ability::AbilityKey::Activator,
332                player_id.0.to_string(),
333            );
334        }
335        if let Some(player_id) = self.player {
336            sa.set_triggering_object(crate::ability::AbilityKey::Player, player_id.0.to_string());
337        }
338        if let Some(player_id) = self.attacking_player {
339            sa.set_triggering_object(
340                crate::ability::AbilityKey::AttackingPlayer,
341                player_id.0.to_string(),
342            );
343        }
344        if let Some(player_id) = self.defending_player {
345            sa.set_triggering_object(
346                crate::ability::AbilityKey::DefendingPlayer,
347                player_id.0.to_string(),
348            );
349        }
350        if let Some(card_id) = self.causer.or(self.cause_card) {
351            sa.set_triggering_object(crate::ability::AbilityKey::Causer, card_id.0.to_string());
352        }
353        if let Some(card_id) = self.source_card.or(self.spell_card) {
354            sa.set_triggering_object(crate::ability::AbilityKey::Source, card_id.0.to_string());
355        }
356        if let Some(card_id) = self.attacker {
357            sa.set_triggering_object(crate::ability::AbilityKey::Attacker, card_id.0.to_string());
358        }
359        if let Some(card_id) = self.blocker {
360            sa.set_triggering_object(crate::ability::AbilityKey::Blocker, card_id.0.to_string());
361        }
362        if let Some(card_id) = self.attacked_card {
363            sa.set_triggering_object(crate::ability::AbilityKey::Attacked, card_id.0.to_string());
364        }
365        if let Some(player_id) = self.attacked_player {
366            sa.set_triggering_object(
367                crate::ability::AbilityKey::AttackedTarget,
368                player_id.0.to_string(),
369            );
370        }
371        if let Some(card_id) = self.target_card {
372            let value = card_id.0.to_string();
373            sa.set_triggering_object(crate::ability::AbilityKey::Target, &value);
374            sa.set_triggering_object(crate::ability::AbilityKey::TargetCard, &value);
375        }
376        if let Some(player_id) = self.target_player {
377            let value = player_id.0.to_string();
378            sa.set_triggering_object(crate::ability::AbilityKey::Target, &value);
379            sa.set_triggering_object(crate::ability::AbilityKey::TargetPlayer, &value);
380        }
381        if self.target_player.is_none() {
382            if let Some(player_id) = self.damage_target_player {
383                let value = player_id.0.to_string();
384                sa.set_triggering_object(crate::ability::AbilityKey::Target, &value);
385                sa.set_triggering_object(crate::ability::AbilityKey::TargetPlayer, &value);
386            }
387        }
388        if self.target_card.is_none() {
389            if let Some(card_id) = self.damage_target_card {
390                let value = card_id.0.to_string();
391                sa.set_triggering_object(crate::ability::AbilityKey::Target, &value);
392                sa.set_triggering_object(crate::ability::AbilityKey::TargetCard, &value);
393            }
394        }
395        if let Some(card_id) = self.explored {
396            sa.set_triggering_object(crate::ability::AbilityKey::Explored, card_id.0.to_string());
397        }
398        if let Some(cards) = self.cards.as_deref() {
399            let csv = cards
400                .iter()
401                .map(|card_id| card_id.0.to_string())
402                .collect::<Vec<_>>()
403                .join(",");
404            if !csv.is_empty() {
405                sa.set_triggering_object(crate::ability::AbilityKey::Cards, &csv);
406            }
407        }
408        if let Some(cards) = self.attacker_ids.as_deref() {
409            let csv = cards
410                .iter()
411                .map(|card_id| card_id.0.to_string())
412                .collect::<Vec<_>>()
413                .join(",");
414            if !csv.is_empty() {
415                sa.set_triggering_object(crate::ability::AbilityKey::Attackers, &csv);
416            }
417        }
418        if let Some(value) = self.life_amount {
419            sa.set_triggering_object(crate::ability::AbilityKey::LifeAmount, value.to_string());
420        }
421        if let Some(value) = self.natural_result {
422            sa.set_triggering_object(crate::ability::AbilityKey::NaturalResult, value.to_string());
423        }
424        if let Some(value) = self.card_state_name.as_deref() {
425            sa.set_triggering_object(crate::ability::AbilityKey::CardState, value);
426        }
427        if let Some(value) = self.room_name.as_deref() {
428            sa.set_triggering_object(crate::ability::AbilityKey::RoomName, value);
429        }
430        if let Some(value) = self.spell_ability.as_ref() {
431            sa.set_triggering_spell_ability("SpellAbility", value.clone());
432        }
433        if let Some(value) = self.source_sa.as_ref() {
434            sa.set_triggering_spell_ability("SourceSA", value.clone());
435        }
436        if let Some(value) = self.ability_mana.as_ref() {
437            sa.set_triggering_spell_ability("AbilityMana", value.clone());
438        }
439        if let Some(value) = self.cause.as_ref() {
440            sa.set_triggering_spell_ability("Cause", value.clone());
441        }
442        if let Some(results) = self.die_results.as_deref() {
443            let csv = results
444                .iter()
445                .map(i32::to_string)
446                .collect::<Vec<_>>()
447                .join(",");
448            if !csv.is_empty() {
449                sa.set_triggering_object(crate::ability::AbilityKey::Result, &csv);
450            }
451        } else if let Some(value) = self.die_result {
452            sa.set_triggering_object(crate::ability::AbilityKey::Result, value.to_string());
453        }
454        if let Some(value) = self.die_sides {
455            sa.set_triggering_object(crate::ability::AbilityKey::Sides, value.to_string());
456        }
457        if let Some(value) = self.number {
458            sa.set_triggering_object(crate::ability::AbilityKey::Number, value.to_string());
459        }
460    }
461
462    pub fn get_value(&self, key: AbilityKey) -> Option<AbilityValue> {
463        use AbilityKey::*;
464        match key {
465            AbilityMana => self.ability_mana.clone().map(AbilityValue::SpellAbility),
466            AllVotes => self.all_votes.clone().map(AbilityValue::VoteMap),
467            Activator => self
468                .activator
469                .or(self.cause_player)
470                .map(AbilityValue::Player),
471            Amount => self
472                .cards
473                .as_ref()
474                .map(|cards| AbilityValue::Int(cards.len() as i32))
475                .or_else(|| self.counter_amount.map(AbilityValue::Int))
476                .or_else(|| self.damage_amount.map(AbilityValue::Int))
477                .or_else(|| self.life_amount.map(AbilityValue::Int)),
478            Attacked => self
479                .attacked_card
480                .map(AbilityValue::Card)
481                .or_else(|| self.attacked_player.map(AbilityValue::Player)),
482            Attacker => self.attacker.map(AbilityValue::Card),
483            Attackers => self.attacker_ids.clone().map(AbilityValue::Cards),
484            AttackingPlayer => self
485                .attacking_player
486                .or(self.spell_controller)
487                .map(AbilityValue::Player),
488            Blocker => self.blocker.map(AbilityValue::Card),
489            Blockers => self.blocker_ids.clone().map(AbilityValue::Cards),
490            Card => self.card.map(AbilityValue::Card),
491            CardState => self.card_state_name.clone().map(AbilityValue::String),
492            Cards => self
493                .change_zone_table
494                .clone()
495                .map(AbilityValue::CardZoneTable)
496                .or_else(|| self.cards.clone().map(AbilityValue::Cards)),
497            CardLKI => self.card_lki.map(AbilityValue::Card),
498            Causer => self
499                .causer
500                .map(AbilityValue::Card)
501                .or_else(|| self.cause_card.map(AbilityValue::Card))
502                .or_else(|| {
503                    self.cause
504                        .as_ref()
505                        .and_then(|sa| sa.source)
506                        .map(AbilityValue::Card)
507                }),
508            Cause => self
509                .cause
510                .clone()
511                .map(AbilityValue::SpellAbility)
512                .or_else(|| self.cause_card.map(AbilityValue::Card))
513                .or_else(|| self.cards.clone().map(AbilityValue::Cards)),
514            Championed => self.championed_card.map(AbilityValue::Card),
515            ClassLevel => self.class_level.map(AbilityValue::Int),
516            CounterAmount | CounterNum | NewCounterAmount => {
517                self.counter_amount.map(AbilityValue::Int)
518            }
519            CounterMap => self.counter_map.clone().map(AbilityValue::CounterMap),
520            CounterType => self.counter_type.clone().map(AbilityValue::String),
521            Crew => self.crew_cards.clone().map(AbilityValue::Cards),
522            CumulativeUpkeepPaid => self.cumulative_upkeep_paid.map(AbilityValue::Bool),
523            DamageAmount | LifeGained | LifeAmount | Num | Number | PreventedAmount => self
524                .damage_amount
525                .or(self.life_amount)
526                .or(self.num)
527                .or(self.number)
528                .or(self.drawn_this_turn_snapshot)
529                .map(AbilityValue::Int),
530            DamageMap => self.damage_map.clone().map(AbilityValue::DamageMap),
531            DamageSource => self.damage_source.map(AbilityValue::Card),
532            DamageTarget | Target => self
533                .damage_target_card
534                .map(AbilityValue::Card)
535                .or_else(|| self.damage_target_player.map(AbilityValue::Player))
536                .or_else(|| self.target_card.map(AbilityValue::Card))
537                .or_else(|| self.target_player.map(AbilityValue::Player)),
538            Defenders => self
539                .defenders_card_ids
540                .clone()
541                .map(AbilityValue::Cards)
542                .or_else(|| self.defenders_player_ids.clone().map(AbilityValue::Players)),
543            DefendingPlayer => self.defending_player.map(AbilityValue::Player),
544            Destination => self
545                .destinations
546                .clone()
547                .map(AbilityValue::String)
548                .or_else(|| self.destination.map(AbilityValue::Zone)),
549            EchoPaid => self.echo_paid.map(AbilityValue::Bool),
550            Enlisted => self.enlisted.map(AbilityValue::Card),
551            Exploited => self.exploited_card.map(AbilityValue::Card),
552            Explored => self.explored.map(AbilityValue::Card),
553            Explorer => self.card.map(AbilityValue::Card),
554            FirstTime => self
555                .first_time_players
556                .clone()
557                .map(AbilityValue::Players)
558                .or_else(|| self.first_time.map(AbilityValue::Bool)),
559            Fizzle => self.fizzle.map(AbilityValue::Bool),
560            IsCombat | IsCombatDamage => self.is_combat_damage.map(AbilityValue::Bool),
561            LastStateBattlefield => self
562                .change_zone_table
563                .as_ref()
564                .map(|table| AbilityValue::Cards(table.last_state_battlefield().to_vec())),
565            LastStateGraveyard => self
566                .change_zone_table
567                .as_ref()
568                .map(|table| AbilityValue::Cards(table.last_state_graveyard().to_vec())),
569            Mana | Produced => self.produced.clone().map(AbilityValue::String),
570            Mode => self.mode.clone().map(AbilityValue::String),
571            NewCard => self.card2.map(AbilityValue::Card),
572            Object => self
573                .object_card
574                .map(AbilityValue::Card)
575                .or_else(|| self.object_player.map(AbilityValue::Player))
576                .or_else(|| self.card.map(AbilityValue::Card)),
577            OtherAttackers => self.other_attacker_ids.clone().map(AbilityValue::Cards),
578            Origin => self.origin.map(AbilityValue::Zone),
579            OriginalController => self.original_controller.map(AbilityValue::Player),
580            Phase => self.phase.map(AbilityValue::Phase),
581            Player => self.player.map(AbilityValue::Player),
582            Result | Won => self
583                .coin_flip_won
584                .map(AbilityValue::Bool)
585                .or_else(|| self.clash_won.map(AbilityValue::Bool))
586                .or_else(|| self.mode.clone().map(AbilityValue::String))
587                .or_else(|| self.die_result.map(AbilityValue::Int)),
588            NaturalResult => self.natural_result.map(AbilityValue::Int),
589            RoomName => self.room_name.clone().map(AbilityValue::String),
590            RolledToVisitAttractions => self.rolled_to_visit_attractions.map(AbilityValue::Bool),
591            Scheme => self.card.map(AbilityValue::Card),
592            Sides => self.die_sides.map(AbilityValue::Int),
593            Source => self
594                .source_card
595                .map(AbilityValue::Card)
596                .or_else(|| self.source_player.map(AbilityValue::Player))
597                .or_else(|| self.spell_card.map(AbilityValue::Card)),
598            SourceSA | SpellAbility | StackSa => self
599                .source_sa
600                .clone()
601                .or_else(|| self.spell_ability.clone())
602                .map(AbilityValue::SpellAbility),
603            Valiant => self.valiant.map(AbilityValue::Bool),
604            InternalTriggerTable => self
605                .change_zone_table
606                .clone()
607                .map(AbilityValue::CardZoneTable),
608            _ => None,
609        }
610    }
611
612    pub fn as_ability_map(&self) -> HashMap<AbilityKey, AbilityValue> {
613        crate::ability::ability_key::all_ability_keys()
614            .iter()
615            .filter_map(|key| self.get_value(*key).map(|value| (*key, value)))
616            .collect()
617    }
618
619    /// Java-parity alias for `Map<AbilityKey, Object>.get(...)`.
620    pub fn get(&self, key: AbilityKey) -> Option<AbilityValue> {
621        self.get_value(key)
622    }
623
624    /// Get a card ID from run-params by AbilityKey.
625    /// Provides a generic accessor so code can use `AbilityKey` enum values
626    /// to pull data from the typed struct.
627    pub fn get_card(&self, key: crate::ability::AbilityKey) -> Option<CardId> {
628        match self.get_value(key) {
629            Some(AbilityValue::Card(card)) => Some(card),
630            _ => None,
631        }
632    }
633
634    /// Get a player ID from run-params by AbilityKey.
635    pub fn get_player(&self, key: crate::ability::AbilityKey) -> Option<PlayerId> {
636        match self.get_value(key) {
637            Some(AbilityValue::Player(player)) => Some(player),
638            _ => None,
639        }
640    }
641
642    /// Get an integer amount from run-params by AbilityKey.
643    pub fn get_amount(&self, key: crate::ability::AbilityKey) -> Option<i32> {
644        match self.get_value(key) {
645            Some(AbilityValue::Int(value)) => Some(value),
646            _ => None,
647        }
648    }
649
650    /// Get a bool marker by AbilityKey.
651    pub fn get_bool(&self, key: crate::ability::AbilityKey) -> Option<bool> {
652        use crate::ability::AbilityKey;
653        match key {
654            AbilityKey::IsCombatDamage => self.is_combat_damage,
655            AbilityKey::FirstTime => self.first_time,
656            AbilityKey::Valiant => self.valiant,
657            _ => None,
658        }
659    }
660
661    /// Get a SpellAbility by AbilityKey.
662    pub fn get_spell_ability(
663        &self,
664        key: crate::ability::AbilityKey,
665    ) -> Option<&crate::spellability::SpellAbility> {
666        use crate::ability::AbilityKey;
667        match key {
668            AbilityKey::SpellAbility => self.spell_ability.as_ref(),
669            AbilityKey::SourceSA => self.source_sa.as_ref(),
670            AbilityKey::AbilityMana => self.ability_mana.as_ref(),
671            AbilityKey::Cause => self.cause.as_ref(),
672            _ => None,
673        }
674    }
675
676    /// Get a card list by AbilityKey.
677    pub fn get_cards(&self, key: crate::ability::AbilityKey) -> Option<&[CardId]> {
678        use crate::ability::AbilityKey;
679        match key {
680            AbilityKey::Cards => self.cards.as_deref(),
681            AbilityKey::DamageTargets => self.cards.as_deref(),
682            AbilityKey::Attackers => self.attacker_ids.as_deref(),
683            AbilityKey::Blockers => self.blocker_ids.as_deref(),
684            AbilityKey::OtherAttackers => self.other_attacker_ids.as_deref(),
685            AbilityKey::Defenders => self.defenders_card_ids.as_deref(),
686            _ => None,
687        }
688    }
689
690    /// Get a string payload by AbilityKey.
691    pub fn get_string(&self, key: crate::ability::AbilityKey) -> Option<&str> {
692        use crate::ability::AbilityKey;
693        match key {
694            AbilityKey::Produced => self.produced.as_deref(),
695            AbilityKey::Mode => self.mode.as_deref(),
696            _ => None,
697        }
698    }
699}