manabrew_engine/ability/effects/
play_effect.rs1use forge_foundation::ZoneType;
2
3use super::EffectContext;
4use crate::agent::{GameEntity, GameLogEvent};
5use crate::event::RunParams;
6use crate::ids::CardId;
7use crate::spellability::{SpellAbility, StackEntry};
8use crate::trigger::TriggerType;
9
10#[manabrew_engine_macros::spell_effect(PlayEffect)]
13fn resolve(ctx: &mut EffectContext, sa: &crate::spellability::SpellAbility) {
14 let mut candidates = resolve_target_cards(ctx, sa);
15 candidates.retain(|&cid| ctx.game.card(cid).zone != ZoneType::Stack);
16 if candidates.is_empty() {
17 return;
18 }
19
20 let controller = sa.activating_player;
21 let without_mana_cost = sa.ir.without_mana_cost;
22 let play_cost = sa.ir.play_cost_text.clone();
23 let remember = sa.ir.remember_played;
24 let optional = sa.ir.optional;
25 let is_madness = sa.ir.play_cost_text.is_some()
26 && sa
27 .source
28 .is_some_and(|src| ctx.game.card(src).has_keyword("Madness"));
29
30 let single_option = candidates.len() == 1 && optional;
33 let tgt_cards: Vec<_> = candidates.iter().copied().map(GameEntity::Card).collect();
34 let chosen = ctx.agents[controller.index()].choose_single_entity_for_effect(
35 controller,
36 &tgt_cards,
37 !single_option && optional,
38 );
39 let card_id = match chosen {
40 Some(GameEntity::Card(cid)) => cid,
41 None => return,
42 Some(GameEntity::Player(_)) => return,
43 };
44
45 if single_option {
50 let card_name = ctx.game.card(card_id).card_name.clone();
51 let accepted = ctx.agents[controller.index()].confirm_action(
52 controller,
53 None,
54 &format!("Do you want to play {card_name}?"),
55 &[],
56 Some(card_id),
57 Some(crate::ability::api_type::ApiType::Play),
58 );
59 if !accepted {
60 return;
61 }
62 }
63
64 let spell_sa_base =
66 crate::spellability::build_spell_ability_for_card_cast(ctx.game, card_id, controller);
67 let abilities = vec![spell_sa_base];
68 let sa_idx = ctx.agents[controller.index()].get_ability_to_play(controller, &abilities);
69 let mut spell_sa = match sa_idx {
70 Some(idx) => abilities.into_iter().nth(idx).unwrap(),
71 None => return,
72 };
73
74 if without_mana_cost {
76 if let Some(ref mut cost) = spell_sa.pay_costs {
77 cost.parts
78 .retain(|part| !matches!(part, crate::cost::CostPart::Mana { .. }));
79 }
80 } else if let Some(ref cost_str) = play_cost {
81 let alt_mc = forge_foundation::ManaCost::parse(cost_str);
82 if let Some(ref existing) = spell_sa.pay_costs {
83 spell_sa.pay_costs = Some(existing.copy_with_defined_mana(alt_mc));
84 }
85 }
86
87 if is_madness {
89 spell_sa.alt_cost = Some(crate::spellability::AlternativeCost::Madness);
90 }
91
92 if let Some(ref mut cost) = spell_sa.pay_costs {
93 cost.mandatory = true;
94 }
95
96 spell_sa.ir.cast_from_play_effect = true;
98
99 if !spell_sa.setup_targets(ctx.game, ctx.agents, ctx.mana_pools) {
100 return;
101 }
102
103 if !without_mana_cost {
105 let mc = if let Some(ref cost_str) = play_cost {
106 forge_foundation::ManaCost::parse(cost_str)
107 } else {
108 ctx.game.card(card_id).mana_cost.clone()
109 };
110
111 let available = crate::mana::calculate_available_mana(
112 &ctx.mana_pools[controller.index()],
113 ctx.game,
114 controller,
115 );
116 if !available.can_pay(&mc) {
117 return;
118 }
119 let tapped = crate::mana::auto_tap_lands(
120 ctx.game,
121 &mut ctx.mana_pools[controller.index()],
122 controller,
123 &mc,
124 Some(card_id),
125 );
126 for &land_id in &tapped {
127 ctx.trigger_handler.run_trigger(
128 TriggerType::TapsForMana,
129 RunParams {
130 card: Some(land_id),
131 player: Some(controller),
132 ..Default::default()
133 },
134 false,
135 );
136 }
137 ctx.mana_pools[controller.index()].try_pay(&mc);
138 }
139
140 if let Some(zone) = sa.ir.replace_graveyard.clone() {
144 let host_card = sa.source.unwrap_or(card_id);
145 add_replace_graveyard_effect(ctx, card_id, host_card, sa, &zone);
146 }
147
148 let label = if is_madness {
150 "Madness"
151 } else if without_mana_cost {
152 "Rebound"
153 } else {
154 "Play"
155 };
156 push_spell_to_stack(ctx, card_id, spell_sa, label);
157
158 if remember {
160 if let Some(source_id) = sa.source {
161 ctx.game.card_mut(source_id).remembered_cards.push(card_id);
162 }
163 }
164}
165
166fn resolve_target_cards(ctx: &EffectContext, sa: &SpellAbility) -> Vec<CardId> {
170 if let Some(valid) = crate::parsing::raw_get(&sa.ability_text, crate::parsing::keys::VALID) {
171 let zones = crate::parsing::raw_get(&sa.ability_text, crate::parsing::keys::VALID_ZONE)
172 .map(|raw| {
173 raw.split(',')
174 .filter_map(|part| crate::zone::zone_type::smart_value_of(part.trim()))
175 .collect::<Vec<_>>()
176 })
177 .filter(|zones| !zones.is_empty())
178 .unwrap_or_else(|| vec![ZoneType::Hand]);
179 let Some(source_id) = sa.source else {
180 return Vec::new();
181 };
182 let source = ctx.game.card(source_id);
183 let selector = crate::parsing::cached_compiled_selector(valid);
184 let valid_sa = crate::parsing::raw_get(&sa.ability_text, crate::parsing::keys::VALID_SA);
185 return ctx
186 .game
187 .cards
188 .iter()
189 .filter(|card| zones.contains(&card.zone))
190 .filter(|card| {
191 crate::card::valid_filter::matches_valid_card_selector_in_game(
192 &selector, card, source, ctx.game,
193 )
194 })
195 .filter(|card| {
196 if valid_sa.is_some_and(|v| v.eq_ignore_ascii_case("Spell")) {
197 !card.is_land()
198 } else {
199 true
200 }
201 })
202 .map(|card| card.id)
203 .collect();
204 }
205
206 let defined = sa.ir.defined_text.clone().unwrap_or_default();
207 if let Some(uid_str) = defined.strip_prefix("CardUID_") {
208 uid_str
209 .parse::<u32>()
210 .ok()
211 .map(CardId)
212 .into_iter()
213 .collect()
214 } else {
215 let defined = if defined.is_empty() {
216 "Self"
217 } else {
218 defined.as_str()
219 };
220 crate::ability::ability_utils::get_defined_cards(
221 ctx.game,
222 sa.source,
223 defined,
224 Some(sa.activating_player),
225 )
226 }
227}
228
229fn push_spell_to_stack(
231 ctx: &mut EffectContext,
232 card_id: CardId,
233 spell_sa: SpellAbility,
234 label: &str,
235) {
236 let controller = spell_sa.activating_player;
237 let is_creature = ctx.game.card(card_id).is_creature();
238 let is_permanent = ctx.game.card(card_id).is_permanent();
239 let cast_zone = Some(ctx.game.card(card_id).zone);
240 let card_name = ctx.game.card(card_id).card_name.clone();
241 let chosen_target = spell_sa.target_chosen.target_card;
242
243 let entry = StackEntry {
244 id: 0,
245 spell_ability: spell_sa,
246 is_pending_cast: false,
247 is_creature_spell: is_creature,
248 is_permanent_spell: is_permanent,
249 cast_from_zone: cast_zone,
250 optional_trigger_decider: None,
251 optional_trigger_description: None,
252 optional_trigger_source_name: None,
253 };
254 let trigger_sa = entry.spell_ability.clone();
255
256 ctx.game.stack.push(entry);
257 ctx.move_card(card_id, ZoneType::Stack, controller);
258 ctx.game.player_record_spell_cast(controller, card_id);
259
260 ctx.trigger_handler.run_trigger(
261 TriggerType::SpellCast,
262 RunParams {
263 spell_card: Some(card_id),
264 spell_controller: Some(controller),
265 source_sa: Some(trigger_sa.clone()),
266 ..Default::default()
267 },
268 false,
269 );
270 super::emit_targeting_triggers(ctx, card_id, &trigger_sa);
271
272 let mut event = GameLogEvent::stack(format!("{label}: cast {card_name}"))
273 .with_player(controller)
274 .with_source_card(card_id);
275 if let Some(target_id) = chosen_target {
276 event = event.with_target_card(target_id);
277 }
278 crate::agent::notify_all_agents(ctx.agents, event);
279}
280
281pub fn add_replace_graveyard_effect(
285 ctx: &mut EffectContext,
286 card_id: CardId,
287 _host_card: CardId,
288 sa: &SpellAbility,
289 zone: &str,
290) {
291 let controller = sa.activating_player;
292 let dest_zone = if zone.is_empty() { "Exile" } else { zone };
293
294 let mut effect = crate::player::player_factory_util::new_player_effect_card(
295 controller,
296 "ReplaceGraveyard Effect",
297 None,
298 );
299 effect.remembered_cards.push(card_id);
300 let raw = format!(
301 "R$ Event$ Moved | ValidCard$ Card.IsRemembered | Origin$ Stack | Destination$ Graveyard | NewDestination$ {dest_zone} | ForgetOnMoved$ Origin | Description$ If that card would be put into your graveyard this turn, put it into {dest_zone} instead."
302 );
303 crate::player::player_factory_util::add_replacement_effect(&mut effect, &raw);
304 effect.exile_when_no_remembered = true;
305 effect.forget_on_moved_origin = Some(ZoneType::Stack);
306
307 let effect_id = ctx.game.create_card(effect);
308 ctx.game.move_card(effect_id, ZoneType::Command, controller);
309}