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        }
91    }
92    let rest: Vec<_> = revealed
93        .iter()
94        .copied()
95        .filter(|cid| !found.contains(cid))
96        .collect();
97    if let Some(source_id) = sa.source {
98        if sa.ir.imprint_revealed {
99            ctx.game
100                .card_mut(source_id)
101                .add_imprinted_cards(rest.iter().copied());
102        }
103        if sa.ir.remember_revealed {
104            ctx.game
105                .card_mut(source_id)
106                .add_remembered_cards(rest.iter().copied());
107        }
108    }
109
110    // Remove found + rest cards from library
111    let removed: Vec<_> = revealed.to_vec();
112    for card_id in removed {
113        ctx.game
114            .remove_card_from_zone(ZoneType::Library, target_player, card_id);
115    }
116
117    // Move found cards to destination
118    for &id in &found {
119        let owner = ctx.game.card(id).owner;
120        let dest_owner = if found_dest == ZoneType::Battlefield {
121            sa.activating_player
122        } else {
123            owner
124        };
125        ctx.move_card(id, found_dest, dest_owner);
126        if found_dest == ZoneType::Battlefield {
127            let _ = super::add_to_combat(ctx, sa, id, keys::ATTACKING);
128        }
129        emit_zone_trigger(ctx.trigger_handler, id, ZoneType::Library, found_dest);
130    }
131
132    // Move rest to revealed destination
133    for &id in &rest {
134        let owner = ctx.game.card(id).owner;
135        if revealed_dest == ZoneType::Library {
136            // Put on bottom
137            ctx.game
138                .add_card_to_zone_bottom(ZoneType::Library, owner, id);
139            ctx.game.card_mut(id).set_zone(ZoneType::Library);
140        } else {
141            ctx.move_card(id, revealed_dest, owner);
142            emit_zone_trigger(ctx.trigger_handler, id, ZoneType::Library, revealed_dest);
143        }
144    }
145}