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