Skip to main content

manabrew_engine/ability/effects/
dig_until_effect.rs

1use forge_foundation::ZoneType;
2
3use super::{
4    emit_zone_trigger, matches_change_type, resolve_defined_player, resolve_numeric_svar,
5    EffectContext,
6};
7use crate::card::valid_filter;
8use crate::parsing::keys;
9
10/// `SP$ DigUntil` — reveal cards from the top of library until finding N matching cards.
11///
12/// Mirrors Java's `DigUntilEffect.java`.
13/// - `Amount$` — how many matching cards to find (default 1).
14/// - `Valid$` — filter for matching cards (e.g. "Land", "Creature").
15/// - `FoundDestination$` — where found cards go (default Hand).
16/// - `RevealedDestination$` — where non-matching cards go (default Library bottom).
17///
18/// # Card script examples
19/// ```text
20/// A:SP$ DigUntil | Valid$ Land | FoundDestination$ Hand | RevealedDestination$ Graveyard
21/// A:SP$ DigUntil | Valid$ Creature | Amount$ 2 | FoundDestination$ Battlefield
22/// ```
23/// Struct form of this effect so it can participate in the
24/// `SpellAbilityEffect` trait hierarchy — mirrors Java's
25/// `DigUntilEffect` class extending `SpellAbilityEffect`.
26#[manabrew_engine_macros::spell_effect(DigUntilEffect)]
27fn resolve(ctx: &mut EffectContext, sa: &crate::spellability::SpellAbility) {
28    let amount = resolve_numeric_svar(ctx.game, sa, keys::AMOUNT, 1).max(0) as usize;
29
30    let valid_selector = sa.ir.valid_filter_selector.as_ref();
31    let valid_filter = sa.ir.valid_filter_text.as_deref().unwrap_or("Card");
32
33    let found_dest = sa.ir.found_destination_zone.unwrap_or(ZoneType::Hand);
34    let revealed_dest = sa.ir.revealed_destination_zone.unwrap_or(ZoneType::Library);
35
36    let target_player = sa
37        .target_chosen
38        .target_player
39        .or_else(|| {
40            sa.defined()
41                .and_then(|d| resolve_defined_player(d, sa.activating_player, ctx.game))
42        })
43        .unwrap_or(sa.activating_player);
44
45    let lib_len = ctx
46        .game
47        .cards_in_zone(ZoneType::Library, target_player)
48        .len();
49    if lib_len == 0 {
50        return;
51    }
52
53    let mut found = Vec::new();
54    let mut revealed = Vec::new();
55
56    // Walk from top of library down
57    let lib_cards: Vec<_> = ctx
58        .game
59        .cards_in_zone(ZoneType::Library, target_player)
60        .to_vec();
61    // Library is stored bottom→top, so iterate from end (top) backwards
62    for &cid in lib_cards.iter().rev() {
63        if found.len() >= amount {
64            break;
65        }
66        revealed.push(cid);
67        let card = ctx.game.card(cid);
68        let matches = match (valid_selector, sa.source) {
69            (Some(selector), Some(source_id)) => valid_filter::matches_valid_card_selector_in_game(
70                selector,
71                card,
72                ctx.game.card(source_id),
73                ctx.game,
74            ),
75            _ => matches_change_type(card, valid_filter, &[]),
76        };
77        if matches {
78            found.push(cid);
79            if let Some(source_id) = sa.source {
80                if crate::parsing::raw_has_key(&sa.ability_text, keys::FORGET_OTHER_REMEMBERED) {
81                    ctx.game.card_mut(source_id).clear_remembered();
82                }
83                if sa.ir.remember_found {
84                    ctx.game.card_mut(source_id).add_remembered_card(cid);
85                }
86                if sa.ir.imprint_found {
87                    ctx.game.card_mut(source_id).add_imprinted_card(cid);
88                }
89            }
90        } else {
91        }
92    }
93    let rest: Vec<_> = revealed
94        .iter()
95        .copied()
96        .filter(|cid| !found.contains(cid))
97        .collect();
98    if let Some(source_id) = sa.source {
99        if sa.ir.imprint_revealed {
100            ctx.game
101                .card_mut(source_id)
102                .add_imprinted_cards(rest.iter().copied());
103        }
104        if sa.ir.remember_revealed {
105            ctx.game
106                .card_mut(source_id)
107                .add_remembered_cards(rest.iter().copied());
108        }
109    }
110
111    // Remove found + rest cards from library
112    let removed: Vec<_> = revealed.to_vec();
113    for card_id in removed {
114        ctx.game
115            .remove_card_from_zone(ZoneType::Library, target_player, card_id);
116    }
117
118    // Move found cards to destination
119    for &id in &found {
120        let owner = ctx.game.card(id).owner;
121        let dest_owner = if found_dest == ZoneType::Battlefield {
122            sa.activating_player
123        } else {
124            owner
125        };
126        ctx.move_card(id, found_dest, dest_owner);
127        if found_dest == ZoneType::Battlefield {
128            let _ = super::add_to_combat(ctx, sa, id, keys::ATTACKING);
129        }
130        emit_zone_trigger(ctx.trigger_handler, id, ZoneType::Library, found_dest);
131    }
132
133    // Move rest to revealed destination
134    for &id in &rest {
135        let owner = ctx.game.card(id).owner;
136        if revealed_dest == ZoneType::Library {
137            // Put on bottom
138            ctx.game
139                .add_card_to_zone_bottom(ZoneType::Library, owner, id);
140            ctx.game.card_mut(id).set_zone(ZoneType::Library);
141        } else {
142            ctx.move_card(id, revealed_dest, owner);
143            emit_zone_trigger(ctx.trigger_handler, id, ZoneType::Library, revealed_dest);
144        }
145    }
146}