Skip to main content

manabrew_engine/ability/effects/
explore_effect.rs

1use forge_foundation::ZoneType;
2
3use super::{emit_zone_trigger, EffectContext};
4use crate::ability::ability_ir::DefinedRef;
5use crate::card::CounterType;
6use crate::event::RunParams;
7use crate::parsing::keys;
8use crate::replacement::replacement_handler::{apply_replacements, ReplacementEvent};
9use crate::replacement::ReplacementResult;
10use crate::trigger::TriggerType;
11
12/// `SP$ Explore` — target creature explores.
13///
14/// Mirrors Java's `ExploreEffect.java`.
15/// Explore: Reveal the top card of your library. If it's a land, put it into your hand.
16/// Otherwise, put a +1/+1 counter on this creature, then you may put the card into
17/// your graveyard.
18///
19/// # Card script examples
20/// ```text
21/// A:SP$ Explore | Defined$ Self
22/// A:SP$ Explore | Defined$ Targeted | Num$ 2
23/// ```
24/// Struct form of this effect so it can participate in the
25/// `SpellAbilityEffect` trait hierarchy — mirrors Java's
26/// `ExploreEffect` class extending `SpellAbilityEffect`.
27#[manabrew_engine_macros::spell_effect(ExploreEffect)]
28fn resolve(ctx: &mut EffectContext, sa: &crate::spellability::SpellAbility) {
29    let controller = sa.activating_player;
30
31    // Determine the exploring creature
32    let explorer = sa
33        .target_chosen
34        .target_card
35        .or_else(|| match sa.defined_ref() {
36            Some(DefinedRef::SelfCard) => sa.source,
37            Some(DefinedRef::ParentTarget) => ctx.parent_target_card,
38            _ => sa.source,
39        });
40
41    let explorer_id = match explorer {
42        Some(id) if ctx.game.card(id).zone == ZoneType::Battlefield => id,
43        _ => return,
44    };
45
46    // Run Explore replacement effects before exploring.
47    let mut event = ReplacementEvent::Explore { card: explorer_id };
48    let result = apply_replacements(ctx.game, &mut event);
49    if result == ReplacementResult::Skipped || result == ReplacementResult::Replaced {
50        return;
51    }
52
53    // Parse Num parameter for multiple explores (e.g. Jadelight Ranger explores twice).
54    // Mirrors Java's `AbilityUtils.calculateAmount(host, sa.getParamOrDefault("Num", "1"), sa)`.
55    let amount = super::resolve_numeric_svar(ctx.game, sa, keys::NUM, 1).max(1);
56
57    for _ in 0..amount {
58        // Re-check explorer is still on battlefield (may have been removed by a trigger)
59        if ctx.game.card(explorer_id).zone != ZoneType::Battlefield {
60            return;
61        }
62        ctx.game.player_record_explore(controller, 1);
63
64        // Check if library has cards
65        let lib = ctx.game.cards_in_zone(ZoneType::Library, controller);
66        if lib.is_empty() {
67            // Explorer still gets the +1/+1 counter per rules
68            if !crate::staticability::static_ability_cant_put_counter::any_cant_put_counter_on_card(
69                &ctx.game.cards,
70                ctx.game.card(explorer_id),
71                &CounterType::P1P1,
72            ) {
73                ctx.game
74                    .card_mut(explorer_id)
75                    .add_counter(&CounterType::P1P1, 1);
76            }
77            ctx.trigger_handler.run_trigger(
78                TriggerType::CounterAdded,
79                RunParams {
80                    card: Some(explorer_id),
81                    counter_type: Some("P1P1".to_string()),
82                    counter_amount: Some(1),
83                    ..Default::default()
84                },
85                false,
86            );
87            continue;
88        }
89
90        // Reveal top card
91        let top_card = *lib.last().unwrap();
92
93        let is_land = ctx.game.card(top_card).is_land();
94
95        if is_land {
96            // Land → put into hand
97            let owner = ctx.game.card(top_card).owner;
98            ctx.move_card(top_card, ZoneType::Hand, owner);
99            emit_zone_trigger(
100                ctx.trigger_handler,
101                top_card,
102                ZoneType::Library,
103                ZoneType::Hand,
104            );
105        } else {
106            // Nonland → put +1/+1 counter on explorer
107            if !crate::staticability::static_ability_cant_put_counter::any_cant_put_counter_on_card(
108                &ctx.game.cards,
109                ctx.game.card(explorer_id),
110                &CounterType::P1P1,
111            ) {
112                ctx.game
113                    .card_mut(explorer_id)
114                    .add_counter(&CounterType::P1P1, 1);
115            }
116            ctx.trigger_handler.run_trigger(
117                TriggerType::CounterAdded,
118                RunParams {
119                    card: Some(explorer_id),
120                    counter_type: Some("P1P1".to_string()),
121                    counter_amount: Some(1),
122                    ..Default::default()
123                },
124                false,
125            );
126
127            // Player may put revealed card into graveyard (otherwise it stays on top).
128            // Java's ExploreEffect calls controller.confirmAction() which in the
129            // harness DeterministicController uses a random boolean (pickBool).
130            // Use confirm_action here to match that RNG-consuming path.
131            let card_name = ctx.game.card(top_card).card_name.clone();
132            let explorer_name = ctx.game.card(explorer_id).card_name.clone();
133            let msg = format!("Put {} into your graveyard?", card_name);
134            let put_in_gy = ctx.agents[controller.index()].confirm_action(
135                controller,
136                None,
137                &msg,
138                &[],
139                sa.source,
140                Some(crate::ability::api_type::ApiType::Explore),
141            );
142
143            if put_in_gy {
144                let owner = ctx.game.card(top_card).owner;
145                ctx.move_card(top_card, ZoneType::Graveyard, owner);
146                emit_zone_trigger(
147                    ctx.trigger_handler,
148                    top_card,
149                    ZoneType::Library,
150                    ZoneType::Graveyard,
151                );
152            }
153        }
154        ctx.trigger_handler.run_trigger(
155            TriggerType::Explored,
156            RunParams {
157                card: Some(explorer_id),
158                explored: Some(top_card),
159                ..Default::default()
160            },
161            false,
162        );
163    }
164}