Skip to main content

manabrew_engine/ability/effects/
discover_effect.rs

1//! Discover effect — exile from library until a nonland card with CMC ≤ N.
2//!
3//! Ported 1:1 from Java's `DiscoverEffect.java`.
4//! Discover N: Exile cards from the top of your library until you exile a nonland
5//! card with mana value N or less. Cast it without paying its mana cost or put it
6//! into your hand. Put the rest on the bottom of your library in a random order.
7
8use forge_foundation::ZoneType;
9
10use super::cast_from_effect;
11use super::{emit_zone_trigger, EffectContext};
12use crate::ids::{CardId, PlayerId};
13use crate::parsing::keys;
14use crate::spellability::SpellAbility;
15
16/// Struct form of this effect so it can participate in the
17/// `SpellAbilityEffect` trait hierarchy — mirrors Java's
18/// `DiscoverEffect` class extending `SpellAbilityEffect`.
19#[manabrew_engine_macros::spell_effect(DiscoverEffect)]
20fn resolve(ctx: &mut EffectContext, sa: &crate::spellability::SpellAbility) {
21    let num = super::resolve_numeric_svar(ctx.game, sa, "Num", 1).max(0);
22    let controller = sa.activating_player;
23
24    let players = if let Some(def) = sa.defined_player() {
25        super::resolve_defined_players(def, controller, ctx.game)
26    } else {
27        vec![controller]
28    };
29
30    for pid in players {
31        discover_for_player(ctx, sa, pid, num);
32    }
33}
34
35fn discover_for_player(ctx: &mut EffectContext, sa: &SpellAbility, player: PlayerId, max_cmc: i32) {
36    let mut exiled_rest: Vec<CardId> = Vec::new();
37    let mut found: Option<CardId> = None;
38
39    // Exile cards from top of library one at a time until we find
40    // a nonland card with mana value ≤ N
41    loop {
42        let lib = ctx.game.cards_in_zone(ZoneType::Library, player).to_vec();
43        let Some(&top) = lib.last() else { break };
44
45        let card = ctx.game.card(top);
46        let is_land = card
47            .type_line
48            .core_types
49            .iter()
50            .any(|ct| matches!(ct, forge_foundation::CoreType::Land));
51        let cmc = card.mana_cost.cmc();
52
53        // Exile one at a time (Java: exileSeq = true)
54        let old_zone = ctx.game.card(top).zone;
55        ctx.move_card(top, ZoneType::Exile, player);
56        emit_zone_trigger(ctx.trigger_handler, top, old_zone, ZoneType::Exile);
57
58        if !is_land && cmc <= max_cmc {
59            found = Some(top);
60            if sa.param_is_true(keys::REMEMBER_DISCOVERED) {
61                if let Some(sid) = sa.source {
62                    ctx.game.card_mut(sid).add_remembered_card(top);
63                }
64            }
65            break;
66        } else {
67            exiled_rest.push(top);
68        }
69    }
70
71    // Cast or put in hand — full pipeline via cast_from_effect
72    if let Some(card_id) = found {
73        let has_spells = !cast_from_effect::get_basic_spells(ctx, card_id).is_empty();
74
75        if has_spells {
76            let cast = cast_from_effect::offer_cast_or_alternative(
77                ctx,
78                card_id,
79                player,
80                "Cast without paying its mana cost",
81                "Put into your hand",
82            );
83
84            if cast {
85                let ok =
86                    cast_from_effect::cast_card_from_effect(ctx, card_id, player, true, "Discover");
87                if !ok {
88                    // Failed to cast — put in hand instead
89                    let old = ctx.game.card(card_id).zone;
90                    ctx.move_card(card_id, ZoneType::Hand, player);
91                    emit_zone_trigger(ctx.trigger_handler, card_id, old, ZoneType::Hand);
92                }
93            } else {
94                let old = ctx.game.card(card_id).zone;
95                ctx.move_card(card_id, ZoneType::Hand, player);
96                emit_zone_trigger(ctx.trigger_handler, card_id, old, ZoneType::Hand);
97            }
98        } else {
99            let old = ctx.game.card(card_id).zone;
100            ctx.move_card(card_id, ZoneType::Hand, player);
101            emit_zone_trigger(ctx.trigger_handler, card_id, old, ZoneType::Hand);
102        }
103    }
104
105    // Put the rest on bottom of library in random order
106    ctx.rng.shuffle_cards(&mut exiled_rest);
107    for card_id in exiled_rest {
108        let old = ctx.game.card(card_id).zone;
109        ctx.move_card(card_id, ZoneType::Library, player);
110        ctx.game
111            .reorder_card_in_zone(ZoneType::Library, player, card_id, 0);
112        emit_zone_trigger(ctx.trigger_handler, card_id, old, ZoneType::Library);
113    }
114}