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
197 .as_deref()
198 .is_some_and(|v| v.eq_ignore_ascii_case("Spell"))
199 {
200 !card.is_land()
201 } else {
202 true
203 }
204 })
205 .map(|card| card.id)
206 .collect();
207 }
208
209 let defined = sa.ir.defined_text.clone().unwrap_or_default();
210 if let Some(uid_str) = defined.strip_prefix("CardUID_") {
211 uid_str
212 .parse::<u32>()
213 .ok()
214 .map(CardId)
215 .into_iter()
216 .collect()
217 } else {
218 let defined = if defined.is_empty() {
219 "Self"
220 } else {
221 defined.as_str()
222 };
223 crate::ability::ability_utils::get_defined_cards(
224 ctx.game,
225 sa.source,
226 defined,
227 Some(sa.activating_player),
228 )
229 }
230}
231
232fn push_spell_to_stack(
234 ctx: &mut EffectContext,
235 card_id: CardId,
236 spell_sa: SpellAbility,
237 label: &str,
238) {
239 let controller = spell_sa.activating_player;
240 let is_creature = ctx.game.card(card_id).is_creature();
241 let is_permanent = ctx.game.card(card_id).is_permanent();
242 let cast_zone = Some(ctx.game.card(card_id).zone);
243 let card_name = ctx.game.card(card_id).card_name.clone();
244 let chosen_target = spell_sa.target_chosen.target_card;
245
246 let entry = StackEntry {
247 id: 0,
248 spell_ability: spell_sa,
249 is_pending_cast: false,
250 is_creature_spell: is_creature,
251 is_permanent_spell: is_permanent,
252 cast_from_zone: cast_zone,
253 optional_trigger_decider: None,
254 optional_trigger_description: None,
255 optional_trigger_source_name: None,
256 };
257 let trigger_sa = entry.spell_ability.clone();
258
259 ctx.game.stack.push(entry);
260 ctx.move_card(card_id, ZoneType::Stack, controller);
261 ctx.game.player_record_spell_cast(controller, card_id);
262
263 ctx.trigger_handler.run_trigger(
264 TriggerType::SpellCast,
265 RunParams {
266 spell_card: Some(card_id),
267 spell_controller: Some(controller),
268 source_sa: Some(trigger_sa.clone()),
269 ..Default::default()
270 },
271 false,
272 );
273 super::emit_targeting_triggers(ctx, card_id, &trigger_sa);
274
275 let mut event = GameLogEvent::stack(format!("{}: cast {}", label, card_name))
276 .with_player(controller)
277 .with_source_card(card_id);
278 if let Some(target_id) = chosen_target {
279 event = event.with_target_card(target_id);
280 }
281 crate::agent::notify_all_agents(ctx.agents, event);
282}
283
284pub fn add_replace_graveyard_effect(
288 ctx: &mut EffectContext,
289 card_id: CardId,
290 _host_card: CardId,
291 sa: &SpellAbility,
292 zone: &str,
293) {
294 let controller = sa.activating_player;
295 let dest_zone = if zone.is_empty() { "Exile" } else { zone };
296
297 let mut effect = crate::player::player_factory_util::new_player_effect_card(
298 controller,
299 "ReplaceGraveyard Effect",
300 None,
301 );
302 effect.remembered_cards.push(card_id);
303 let raw = format!(
304 "R$ Event$ Moved | ValidCard$ Card.IsRemembered | Origin$ Stack | Destination$ Graveyard | NewDestination$ {} | ForgetOnMoved$ Origin | Description$ If that card would be put into your graveyard this turn, put it into {} instead.",
305 dest_zone, dest_zone
306 );
307 crate::player::player_factory_util::add_replacement_effect(&mut effect, &raw);
308 effect.exile_when_no_remembered = true;
309 effect.forget_on_moved_origin = Some(ZoneType::Stack);
310
311 let effect_id = ctx.game.create_card(effect);
312 ctx.game.move_card(effect_id, ZoneType::Command, controller);
313}