manabrew_engine/ability/effects/
control_gain_effect.rs1use forge_foundation::ZoneType;
2
3use super::EffectContext;
4use crate::event::RunParams;
5use crate::trigger::TriggerType;
6
7pub fn run(game: &mut crate::game::GameState, card_id: crate::ids::CardId) {
13 if game.card(card_id).zone != ZoneType::Battlefield {
14 return;
15 }
16 revert(game, card_id);
17}
18
19fn revert(game: &mut crate::game::GameState, card_id: crate::ids::CardId) {
20 if let Some(original) = game.card(card_id).original_controller_eot {
21 game.change_controller(card_id, original);
22 game.card_mut(card_id).set_original_controller_eot(None);
23 }
24 game.card_mut(card_id).lose_control_condition = None;
25 game.card_mut(card_id).clear_granted_keywords();
26}
27
28pub fn untap_hook(game: &mut crate::game::GameState, card_id: crate::ids::CardId) {
31 if game.card(card_id).lose_control_condition
32 == Some(crate::card::LoseControlCondition::NextUntap)
33 {
34 revert(game, card_id);
35 }
36}
37
38pub fn end_of_combat_hook(game: &mut crate::game::GameState) {
41 let targets: Vec<crate::ids::CardId> = game
42 .cards
43 .iter()
44 .filter(|c| {
45 c.lose_control_condition == Some(crate::card::LoseControlCondition::EndOfCombat)
46 })
47 .map(|c| c.id)
48 .collect();
49 for cid in targets {
50 if game.card(cid).zone == ZoneType::Battlefield {
51 revert(game, cid);
52 }
53 }
54}
55
56pub fn leaves_play_hook(game: &mut crate::game::GameState, card_id: crate::ids::CardId) {
60 if game.card(card_id).lose_control_condition
61 == Some(crate::card::LoseControlCondition::LeavesPlay)
62 {
63 game.card_mut(card_id).lose_control_condition = None;
64 game.card_mut(card_id).set_original_controller_eot(None);
65 }
66}
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71 use crate::ability::spell_ability_effect::SpellAbilityEffect;
72 use crate::agent::{PassAgent, PlayerAgent};
73 use crate::card::Card;
74 use crate::game::GameState;
75 use crate::ids::{CardId, PlayerId};
76 use crate::mana::ManaPool;
77 use crate::spellability::SpellAbility;
78 use crate::trigger::TriggerHandler;
79 use forge_foundation::{CardTypeLine, ColorSet, ManaCost, ZoneType};
80 use std::collections::HashMap;
81
82 fn creature(owner: PlayerId, name: &str) -> Card {
83 Card::new(
84 CardId(0),
85 name.to_string(),
86 owner,
87 CardTypeLine::parse("Creature Goblin"),
88 ManaCost::parse("R"),
89 ColorSet::RED,
90 Some(1),
91 Some(1),
92 vec![],
93 vec![],
94 )
95 }
96
97 #[test]
98 fn repeated_eot_control_gain_restores_first_controller() {
99 let p0 = PlayerId(0);
100 let p1 = PlayerId(1);
101 let mut game = GameState::new(&["Alice", "Bob"], 20);
102 let goblin = game.create_card(creature(p0, "Raging Goblin"));
103 game.move_card(goblin, ZoneType::Battlefield, p0);
104
105 let mut agents: Vec<Box<dyn PlayerAgent>> = vec![Box::new(PassAgent), Box::new(PassAgent)];
106 let mut trigger_handler = TriggerHandler::new();
107 let templates = HashMap::new();
108 let token_art_variants = HashMap::new();
109 let token_fallback = HashMap::new();
110 let edition_dates = HashMap::new();
111 let mut mana_pools = vec![ManaPool::new(), ManaPool::new()];
112 let mut rng = crate::game_rng::ThreadRngAdapter;
113
114 {
115 let mut ctx = EffectContext {
116 game: &mut game,
117 combat: None,
118 agents: &mut agents,
119 trigger_handler: &mut trigger_handler,
120 token_templates: &templates,
121 token_art_variants: &token_art_variants,
122 token_fallback: &token_fallback,
123 edition_dates: &edition_dates,
124 mana_pools: &mut mana_pools,
125 parent_target_card: None,
126 rng: &mut rng,
127 };
128
129 let mut steal = SpellAbility::new_simple(
130 Some(goblin),
131 p1,
132 "SP$ GainControl | ValidTgts$ Creature.OppCtrl | LoseControl$ EOT",
133 );
134 steal.target_chosen.target_card = Some(goblin);
135 ControlGainEffect::resolve(&mut ctx, &steal);
136 assert_eq!(ctx.game.card(goblin).controller, p1);
137
138 let mut steal_back = SpellAbility::new_simple(
139 Some(goblin),
140 p0,
141 "SP$ GainControl | ValidTgts$ Creature.OppCtrl | LoseControl$ EOT",
142 );
143 steal_back.target_chosen.target_card = Some(goblin);
144 ControlGainEffect::resolve(&mut ctx, &steal_back);
145 assert_eq!(ctx.game.card(goblin).controller, p0);
146 }
147
148 assert_eq!(game.card(goblin).original_controller_eot, Some(p0));
149 run(&mut game, goblin);
150 assert_eq!(game.card(goblin).controller, p0);
151 assert_eq!(game.card(goblin).original_controller_eot, None);
152 }
153}
154
155#[manabrew_engine_macros::spell_effect(ControlGainEffect)]
162fn resolve(ctx: &mut EffectContext, sa: &crate::spellability::SpellAbility) {
163 let target_card = match sa.target_chosen.target_card {
164 Some(c) => c,
165 None => return,
166 };
167
168 if ctx.game.card(target_card).zone != ZoneType::Battlefield {
170 return;
171 }
172
173 let new_controller = sa.activating_player;
174
175 if !ctx
177 .game
178 .card(target_card)
179 .can_be_controlled_by(new_controller)
180 {
181 return;
182 }
183
184 let old_controller = ctx.game.card(target_card).controller;
186 ctx.game.change_controller(target_card, new_controller);
187
188 if old_controller != new_controller {
190 ctx.trigger_handler.run_trigger(
191 TriggerType::ChangesController,
192 RunParams {
193 card: Some(target_card),
194 player: Some(new_controller),
195 original_controller: Some(old_controller),
196 ..Default::default()
197 },
198 false,
199 );
200 }
201
202 let already_scheduled = ctx.game.card(target_card).original_controller_eot.is_some();
206 if let Some(cond) = sa.ir.lose_control {
207 if !already_scheduled {
208 ctx.game
209 .card_mut(target_card)
210 .set_original_controller_eot(Some(old_controller));
211 }
212 ctx.game.card_mut(target_card).lose_control_condition = Some(cond);
213 }
214
215 if sa.ir.untap_on_resolve {
217 ctx.game.untap(target_card);
218 }
219
220 if let Some(kws_str) = sa.ir.add_kws.as_deref() {
222 let keywords: Vec<String> = kws_str.split(" & ").map(|s| s.to_string()).collect();
223 for kw in keywords {
224 ctx.game.card_mut(target_card).add_granted_keyword(&kw);
225 }
226 }
227}