Skip to main content

manabrew_engine/ability/effects/
cast_from_effect.rs

1//! Shared "cast from effect" pipeline.
2//!
3//! Provides infrastructure for effects that cast cards without paying mana cost
4//! (Discover, Cascade, Suspend, etc.) or with alternate costs.
5//!
6//! Mirrors Java's:
7//! - `AbilityUtils.getBasicSpellsFromPlayEffect()`
8//! - `SpellAbility.copyWithNoManaCost()`
9//! - `PlayerControllerHuman.playSaFromPlayEffect()`
10
11use forge_foundation::ZoneType;
12
13use super::EffectContext;
14use crate::agent::GameLogEvent;
15use crate::event::RunParams;
16use crate::ids::{CardId, PlayerId};
17use crate::spellability::{build_spell_ability, SpellAbility, StackEntry};
18use crate::trigger::TriggerType;
19
20/// Cast a card from a play effect, optionally without paying its mana cost.
21///
22/// This is the main entry point for effects like Discover, Cascade, Suspend,
23/// Rebound, etc. that need to cast a spell from a non-standard zone.
24///
25/// # Arguments
26/// * `card_id` — the card to cast
27/// * `controller` — the player casting it
28/// * `without_mana_cost` — if true, strip mana from the cost (Discover, Cascade)
29/// * `label` — display label for log ("Discover", "Cascade", etc.)
30///
31/// # Returns
32/// `true` if the spell was successfully cast (pushed to stack).
33pub fn cast_card_from_effect(
34    ctx: &mut EffectContext,
35    card_id: CardId,
36    controller: PlayerId,
37    without_mana_cost: bool,
38    label: &str,
39) -> bool {
40    // Build spell ability from the card's first ability
41    let abilities = ctx.game.card(card_id).abilities.clone();
42    let ability_text = match abilities.first() {
43        Some(text) => text.clone(),
44        None => return false,
45    };
46
47    let mut spell_sa = build_spell_ability(ctx.game, card_id, &ability_text, controller);
48    spell_sa.is_spell = true;
49
50    // Remove zone restriction — allow casting from exile/library/etc.
51    // Java: newSA.getRestrictions().setZone(null)
52    // In Rust, the zone check happens at a higher level; we bypass by
53    // marking the spell as cast-from-effect.
54    spell_sa.ir.cast_from_play_effect = true;
55
56    // Strip mana cost if requested (Discover, Cascade, etc.)
57    // Java: copyWithNoManaCost() — removes Mana cost parts, keeps non-mana costs
58    if without_mana_cost {
59        if let Some(ref mut cost) = spell_sa.pay_costs {
60            cost.parts
61                .retain(|part| !matches!(part, crate::cost::CostPart::Mana { .. }));
62        }
63        spell_sa.ir.without_mana_cost = true;
64    }
65
66    // Make costs mandatory (Java: setMandatory(true) for 118.8c)
67    if let Some(ref mut cost) = spell_sa.pay_costs {
68        cost.mandatory = true;
69    }
70
71    // Setup targets
72    spell_sa.setup_targets(ctx.game, ctx.agents, ctx.mana_pools);
73
74    // Push to stack
75    push_spell_to_stack(ctx, card_id, spell_sa, label);
76    true
77}
78
79/// Get the list of basic spell abilities from a card that can be cast via a play effect.
80///
81/// Mirrors Java's `AbilityUtils.getBasicSpellsFromPlayEffect()`.
82/// Returns the ability texts that can be cast (non-land spells).
83pub fn get_basic_spells(ctx: &EffectContext, card_id: CardId) -> Vec<String> {
84    let card = ctx.game.card(card_id);
85    let mut spells = Vec::new();
86
87    for ability_text in &card.abilities {
88        // Skip non-spell abilities (activated, triggered)
89        if ability_text.contains("AB$") || ability_text.contains("T$") {
90            continue;
91        }
92        // Skip land abilities
93        if ability_text.contains("LandAbility") {
94            continue;
95        }
96        spells.push(ability_text.clone());
97    }
98
99    // If no spell abilities found but card is a permanent, it can be cast as-is
100    if spells.is_empty() && card.is_permanent() {
101        if let Some(first) = card.abilities.first() {
102            spells.push(first.clone());
103        }
104    }
105
106    spells
107}
108
109/// Offer the player a choice: cast a card or put it somewhere else.
110///
111/// Returns `true` if the player chose to cast, `false` for the alternative.
112pub fn offer_cast_or_alternative(
113    ctx: &mut EffectContext,
114    card_id: CardId,
115    controller: PlayerId,
116    cast_label: &str,
117    alt_label: &str,
118) -> bool {
119    let card_name = ctx.game.card(card_id).card_name.clone();
120    ctx.agents[controller.index()].snapshot_state(ctx.game, ctx.mana_pools);
121    ctx.agents[controller.index()].confirm_action(
122        controller,
123        Some("CastFromEffect"),
124        &format!("{}: {} or {}?", card_name, cast_label, alt_label),
125        &[cast_label.to_string(), alt_label.to_string()],
126        Some(card_id),
127        None,
128    )
129}
130
131// ─── Internal ────────────────────────────────────────────────────────────────
132
133/// Push a spell to the stack, move card to Stack zone, fire triggers.
134/// Shared implementation extracted from play_effect.rs.
135fn push_spell_to_stack(
136    ctx: &mut EffectContext,
137    card_id: CardId,
138    spell_sa: SpellAbility,
139    label: &str,
140) {
141    let controller = spell_sa.activating_player;
142    let is_creature = ctx.game.card(card_id).is_creature();
143    let is_permanent = ctx.game.card(card_id).is_permanent();
144    let cast_zone = Some(ctx.game.card(card_id).zone);
145    let card_name = ctx.game.card(card_id).card_name.clone();
146    let chosen_target = spell_sa.target_chosen.target_card;
147
148    let entry = StackEntry {
149        id: 0,
150        spell_ability: spell_sa,
151        is_pending_cast: false,
152        is_creature_spell: is_creature,
153        is_permanent_spell: is_permanent,
154        cast_from_zone: cast_zone,
155        optional_trigger_decider: None,
156        optional_trigger_description: None,
157        optional_trigger_source_name: None,
158    };
159    let trigger_sa = entry.spell_ability.clone();
160
161    ctx.game.stack.push(entry);
162    ctx.move_card(card_id, ZoneType::Stack, controller);
163    ctx.game.player_record_spell_cast(controller, card_id);
164
165    ctx.trigger_handler.run_trigger(
166        TriggerType::SpellCast,
167        RunParams {
168            spell_card: Some(card_id),
169            spell_controller: Some(controller),
170            source_sa: Some(trigger_sa.clone()),
171            ..Default::default()
172        },
173        false,
174    );
175    super::emit_targeting_triggers(ctx, card_id, &trigger_sa);
176
177    let mut event = GameLogEvent::stack(format!("{}: cast {}", label, card_name))
178        .with_player(controller)
179        .with_source_card(card_id);
180    if let Some(target_id) = chosen_target {
181        event = event.with_target_card(target_id);
182    }
183    crate::agent::notify_all_agents(ctx.agents, event);
184}