Skip to main content

manabrew_engine/ability/effects/
effect_context.rs

1//! `EffectContext` — bundle of subsystem refs threaded through effect resolution.
2//!
3//! Rust-specific concession: Java reaches `Game.getTriggerHandler()`, agents,
4//! combat, mana pools via chained getters from `SpellAbility.getHostCard()`.
5//! The Rust engine deliberately owns those subsystems outside `GameState`
6//! (see `trigger_handler.rs` top comment), so every resolver needs a handful
7//! of mutable references. This struct packs them.
8
9use std::collections::HashMap;
10
11use forge_foundation::ZoneType;
12
13use crate::agent::PlayerAgent;
14use crate::card::Card;
15use crate::game::GameState;
16use crate::ids::{CardId, PlayerId};
17use crate::mana::ManaPool;
18use crate::spellability::SpellAbility;
19use crate::trigger::handler::TriggerHandler;
20
21/// Everything an effect needs to resolve.
22pub struct EffectContext<'a> {
23    pub game: &'a mut GameState,
24    pub combat: Option<&'a mut crate::combat::CombatState>,
25    pub agents: &'a mut [Box<dyn PlayerAgent>],
26    pub trigger_handler: &'a mut TriggerHandler,
27    pub token_templates: &'a HashMap<String, Card>,
28    /// Token art variant counts for game-RNG parity with Java.
29    pub token_art_variants: &'a HashMap<(String, String), usize>,
30    /// Token fallback codes: edition_code → fallback_edition_code.
31    pub token_fallback: &'a HashMap<String, String>,
32    /// Edition release dates: edition_code → "YYYY-MM-DD". Used to sort
33    /// editions newest-first for token fallback (Java parity).
34    pub edition_dates: &'a HashMap<String, String>,
35    pub mana_pools: &'a mut Vec<ManaPool>,
36    /// CardId of the parent SA's chosen target card, propagated through the
37    /// sub-ability chain so that `Defined$ ParentTarget` effects can resolve it.
38    /// Mirrors Java's `SpellAbility.getParentTargetCard()` (via getRootAbility()).
39    pub parent_target_card: Option<CardId>,
40    /// Pluggable RNG for game effects (shuffles, coin flips, dice rolls).
41    /// Parity tests inject a JavaRandom-backed implementation; normal gameplay
42    /// uses the default ThreadRngAdapter.
43    pub rng: &'a mut dyn crate::game_rng::GameRng,
44}
45
46impl EffectContext<'_> {
47    /// Get the number of art variants for a token in a given edition,
48    /// following TokenFallbackCode chains. Returns 1 if not found.
49    /// When edition_code is empty, scans all editions and returns the first
50    /// match (mirrors Java's `fallbackToken` which iterates all editions).
51    pub fn token_art_variant_count(&self, token_script: &str, edition_code: &str) -> usize {
52        let script_lower = token_script.to_lowercase();
53        if !edition_code.is_empty() {
54            let key = (script_lower.clone(), edition_code.to_uppercase());
55            if let Some(&count) = self.token_art_variants.get(&key) {
56                return count;
57            }
58            if let Some(fallback) = self.token_fallback.get(&edition_code.to_uppercase()) {
59                return self.token_art_variant_count(token_script, fallback);
60            }
61        }
62        // Fallback: host edition doesn't have this token. Java's
63        // `fallbackToken` iterates editions in a specific order that's
64        // hard to reproduce exactly. In practice Java almost always
65        // resolves to an edition with 1 art variant for common tokens.
66        // Default to 1 to match the typical Java behavior.
67        1
68    }
69
70    /// Consume game-RNG calls to match Java's token prototype creation.
71    /// Java calls Aggregates.random(Set) which does nextInt() per element,
72    /// plus PaperToken.getImageKey() which does nextInt(artIndex).
73    pub fn sync_token_art_rng(&mut self, token_script: &str, sa: &SpellAbility) {
74        // Java's TokenDb caches token prototypes globally. The first creation
75        // of a token type consumes game RNG (Aggregates.random + getImageKey);
76        // subsequent creations reuse the cached prototype without RNG.
77        let host_edition = sa
78            .source
79            .and_then(|cid| self.game.card(cid).set_code.as_deref())
80            .unwrap_or("");
81        let art_count = self.token_art_variant_count(token_script, host_edition);
82        // Java's Aggregates.random(Collection<PaperToken>) uses min-random
83        // selection: for each element, call nextInt() (unbounded). Collection
84        // size = number of art variants in the resolved edition.
85        for _ in 0..art_count {
86            self.rng.next_int(1);
87        }
88        // PaperToken.getImageKey(): nextInt(artIndex)
89        self.rng.next_int(1);
90    }
91
92    pub fn move_card(&mut self, card_id: CardId, dest_zone: ZoneType, dest_owner: PlayerId) {
93        let mut runtime = crate::replacement::replacement_handler::ReplacementRuntime {
94            trigger_handler: self.trigger_handler,
95            token_templates: self.token_templates,
96            token_art_variants: self.token_art_variants,
97            token_fallback: self.token_fallback,
98            edition_dates: self.edition_dates,
99            mana_pools: self.mana_pools,
100            rng: self.rng,
101        };
102        self.game.move_card_with_agents_and_replacement_runtime(
103            card_id,
104            dest_zone,
105            dest_owner,
106            self.agents,
107            &mut runtime,
108        );
109    }
110}