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::GameEntity;
14use crate::agent::PlayerAgent;
15use crate::card::{Card, CounterType};
16use crate::event::RunParams;
17use crate::game::GameState;
18use crate::game_entity_counter_table::GameEntityCounterTable;
19use crate::ids::{CardId, PlayerId};
20use crate::mana::ManaPool;
21use crate::spellability::SpellAbility;
22use crate::trigger::handler::TriggerHandler;
23
24/// Everything an effect needs to resolve.
25pub struct EffectContext<'a> {
26    pub game: &'a mut GameState,
27    pub combat: Option<&'a mut crate::combat::CombatState>,
28    pub agents: &'a mut [Box<dyn PlayerAgent>],
29    pub trigger_handler: &'a mut TriggerHandler,
30    pub token_templates: &'a HashMap<String, Card>,
31    /// Token art variant counts for game-RNG parity with Java.
32    pub token_art_variants: &'a HashMap<(String, String), usize>,
33    /// Token fallback codes: edition_code → fallback_edition_code.
34    pub token_fallback: &'a HashMap<String, String>,
35    /// Edition release dates: edition_code → "YYYY-MM-DD". Used to sort
36    /// editions newest-first for token fallback (Java parity).
37    pub edition_dates: &'a HashMap<String, String>,
38    pub mana_pools: &'a mut Vec<ManaPool>,
39    /// CardId of the parent SA's chosen target card, propagated through the
40    /// sub-ability chain so that `Defined$ ParentTarget` effects can resolve it.
41    /// Mirrors Java's `SpellAbility.getParentTargetCard()` (via getRootAbility()).
42    pub parent_target_card: Option<CardId>,
43    /// Pluggable RNG for game effects (shuffles, coin flips, dice rolls).
44    /// Parity tests inject a JavaRandom-backed implementation; normal gameplay
45    /// uses the default ThreadRngAdapter.
46    pub rng: &'a mut dyn crate::game_rng::GameRng,
47}
48
49pub(crate) fn add_counter_with_context(
50    game: &mut GameState,
51    trigger_handler: Option<&mut TriggerHandler>,
52    agents: Option<&mut [Box<dyn PlayerAgent>]>,
53    card_id: CardId,
54    counter_type: &CounterType,
55    amount: i32,
56    params: RunParams,
57    is_effect: bool,
58) -> i32 {
59    let source = params.source_player.or(params.cause_player);
60    let cause = params.cause.clone();
61    let mut table = GameEntityCounterTable::default();
62    table.put(
63        source,
64        GameEntity::Card(card_id),
65        counter_type.clone(),
66        amount,
67    );
68    table
69        .replace_counter_effect(
70            game,
71            trigger_handler,
72            agents,
73            cause.as_ref(),
74            is_effect,
75            params,
76        )
77        .get(source, GameEntity::Card(card_id), counter_type)
78}
79
80impl EffectContext<'_> {
81    /// Get the number of art variants for a token in a given edition,
82    /// following TokenFallbackCode chains. Returns 1 if not found.
83    /// When edition_code is empty, scans all editions and returns the first
84    /// match (mirrors Java's `fallbackToken` which iterates all editions).
85    pub fn token_art_variant_count(&self, token_script: &str, edition_code: &str) -> usize {
86        let script_lower = token_script.to_lowercase();
87        if !edition_code.is_empty() {
88            let key = (script_lower.clone(), edition_code.to_uppercase());
89            if let Some(&count) = self.token_art_variants.get(&key) {
90                return count;
91            }
92            if let Some(fallback) = self.token_fallback.get(&edition_code.to_uppercase()) {
93                return self.token_art_variant_count(token_script, fallback);
94            }
95        }
96        // Fallback: host edition doesn't have this token. Java's
97        // `fallbackToken` iterates editions in a specific order that's
98        // hard to reproduce exactly. In practice Java almost always
99        // resolves to an edition with 1 art variant for common tokens.
100        // Default to 1 to match the typical Java behavior.
101        1
102    }
103
104    /// Consume game-RNG calls to match Java's token prototype creation.
105    /// Java calls Aggregates.random(Set) which does nextInt() per element,
106    /// plus PaperToken.getImageKey() which does nextInt(artIndex).
107    pub fn sync_token_art_rng(&mut self, token_script: &str, sa: &SpellAbility) {
108        // Java's TokenDb caches token prototypes globally. The first creation
109        // of a token type consumes game RNG (Aggregates.random + getImageKey);
110        // subsequent creations reuse the cached prototype without RNG.
111        let host_edition = sa
112            .source
113            .and_then(|cid| self.game.card(cid).set_code.as_deref())
114            .unwrap_or("");
115        let art_count = self.token_art_variant_count(token_script, host_edition);
116        // Java's Aggregates.random(Collection<PaperToken>) uses min-random
117        // selection: for each element, call nextInt() (unbounded). Collection
118        // size = number of art variants in the resolved edition.
119        for _ in 0..art_count {
120            self.rng.next_int(1);
121        }
122        // PaperToken.getImageKey(): nextInt(artIndex)
123        self.rng.next_int(1);
124    }
125
126    pub fn move_card(&mut self, card_id: CardId, dest_zone: ZoneType, dest_owner: PlayerId) {
127        let mut runtime = crate::replacement::replacement_handler::ReplacementRuntime {
128            trigger_handler: self.trigger_handler,
129            token_templates: self.token_templates,
130            token_art_variants: self.token_art_variants,
131            token_fallback: self.token_fallback,
132            edition_dates: self.edition_dates,
133            mana_pools: self.mana_pools,
134            rng: self.rng,
135        };
136        self.game.move_card_with_agents_and_replacement_runtime(
137            card_id,
138            dest_zone,
139            dest_owner,
140            self.agents,
141            &mut runtime,
142        );
143    }
144
145    pub(crate) fn add_counter(
146        &mut self,
147        card_id: CardId,
148        counter_type: &CounterType,
149        amount: i32,
150        sa: &SpellAbility,
151        mut params: RunParams,
152    ) -> i32 {
153        params.source_player.get_or_insert(sa.activating_player);
154        params.cause.get_or_insert_with(|| sa.clone());
155        add_counter_with_context(
156            self.game,
157            Some(self.trigger_handler),
158            Some(self.agents),
159            card_id,
160            counter_type,
161            amount,
162            params,
163            true,
164        )
165    }
166
167    pub(crate) fn add_player_counter(
168        &mut self,
169        player: PlayerId,
170        counter_type: &CounterType,
171        amount: i32,
172        sa: &SpellAbility,
173        mut params: RunParams,
174    ) -> i32 {
175        params.source_player.get_or_insert(sa.activating_player);
176        params.cause.get_or_insert_with(|| sa.clone());
177        let source = params.source_player;
178        let mut table = GameEntityCounterTable::default();
179        table.put(
180            source,
181            GameEntity::Player(player),
182            counter_type.clone(),
183            amount,
184        );
185        table
186            .replace_counter_effect(
187                self.game,
188                Some(self.trigger_handler),
189                Some(self.agents),
190                Some(sa),
191                true,
192                params,
193            )
194            .get(source, GameEntity::Player(player), counter_type)
195    }
196}