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