Skip to main content

manabrew_engine/ability/effects/
draw_effect.rs

1use super::{resolve_numeric_svar, EffectContext};
2use crate::ability::ability_ir::EffectIr;
3use crate::ability::spell_ability_effect::get_target_players;
4use crate::event::RunParams;
5use crate::parsing::keys;
6use crate::replacement::replacement_handler::{apply_replacements_with_agents, ReplacementEvent};
7use crate::replacement::ReplacementResult;
8use crate::spellability::SpellAbility;
9use crate::trigger::TriggerType;
10
11/// Struct form of this effect so it can participate in the
12/// `SpellAbilityEffect` trait hierarchy — mirrors Java's
13/// `DrawEffect` class extending `SpellAbilityEffect`.
14#[manabrew_engine_macros::spell_effect(DrawEffect)]
15fn resolve(ctx: &mut EffectContext, sa: &crate::spellability::SpellAbility) {
16    let num = resolve_draw_amount(ctx, sa);
17    let targets = get_target_players(ctx.game, sa);
18
19    for target in targets {
20        draw_for_player(ctx, sa, target, num);
21    }
22}
23
24fn draw_for_player(
25    ctx: &mut EffectContext,
26    sa: &crate::spellability::SpellAbility,
27    target: crate::ids::PlayerId,
28    num: i32,
29) {
30    let mut actual_num = num;
31    if actual_num > 0 {
32        let mut event = ReplacementEvent::DrawCards {
33            player: target,
34            count: actual_num,
35        };
36        let result = apply_replacements_with_agents(ctx.game, ctx.agents, &mut event);
37        if result == ReplacementResult::Skipped || result == ReplacementResult::Replaced {
38            return;
39        }
40        if let ReplacementEvent::DrawCards { count, .. } = event {
41            actual_num = count;
42        }
43    }
44
45    // Mirrors Java `DrawEffect.resolve` (DrawEffect.java:60-92):
46    //   final boolean upto = sa.hasParam("Upto");
47    //   final boolean optional = sa.hasParam("OptionalDecider") || upto;
48    //   if (optional) confirm "draw N cards?" — return on decline.
49    //   if (upto)     chooseNumber(0..=N) for the actual count.
50    let upto = sa.ir.upto;
51    let optional = sa.ir.optional_present || sa.ir.optional_decider.is_some() || upto;
52    if optional {
53        let source_name = sa.source.map(|cid| ctx.game.card(cid).card_name.as_str());
54        let accepted = ctx.agents[target.index()].confirm_action(
55            target,
56            None,
57            &format!("Do you want to draw {} card(s)?", actual_num),
58            &[],
59            sa.source,
60            Some(crate::ability::api_type::ApiType::Draw),
61        );
62        if !accepted {
63            return;
64        }
65    }
66    if upto {
67        let prompt = format!("Draw up to {actual_num} card(s).");
68        match ctx.agents[target.index()].choose_number(
69            target,
70            sa.source,
71            "Draw cards",
72            Some(&prompt),
73            0,
74            actual_num,
75        ) {
76            Some(picked) => {
77                actual_num = picked.clamp(0, actual_num);
78                if actual_num == 0 {
79                    return;
80                }
81            }
82            None => return,
83        }
84    }
85    // Draw cards one at a time and fire Drawn trigger after each draw.
86    // This ensures `drawn_this_turn` is correct for triggers with `Number$ N`
87    // (e.g. Sneaky Snacker: "When you draw your third card in a turn...").
88    let remember_drawn = sa.ir.remember_drawn;
89    let mut drawn: Vec<crate::ids::CardId> = Vec::new();
90    for _ in 0..actual_num {
91        if let Some(card_id) = ctx.game.draw_card_with_agents(target, ctx.agents) {
92            drawn.push(card_id);
93            // Snapshot drawn_this_turn AFTER draw_card increments it.
94            // This captures the exact count at draw time for Number$ N matching.
95            let drawn_snapshot = ctx.game.player(target).drawn_this_turn;
96            ctx.trigger_handler.run_trigger(
97                TriggerType::Drawn,
98                RunParams {
99                    card: Some(card_id),
100                    player: Some(target),
101                    drawn_this_turn_snapshot: Some(drawn_snapshot),
102                    ..Default::default()
103                },
104                false,
105            );
106            // Flush/match Drawn triggers immediately so that triggers with
107            // Number$ N (e.g. Sneaky Snacker "3rd card") see the correct game
108            // state at draw time. Only flush if there's a Number$ Drawn trigger
109            // that needs fire-time matching (to avoid disrupting other triggers).
110            if ctx.trigger_handler.has_number_drawn_triggers(ctx.game) {
111                ctx.trigger_handler.flush_waiting_triggers(ctx.game);
112            }
113        }
114    }
115
116    // `RememberDrawn$ True` — attach each drawn card to the source card's
117    // remembered list (e.g. Mystic Remora, Dark Confidant variants).
118    if remember_drawn {
119        if let Some(source_id) = sa.source {
120            let card_mut = ctx.game.card_mut(source_id);
121            for cid in &drawn {
122                card_mut.add_remembered_card(*cid);
123            }
124        }
125    }
126}
127
128fn resolve_draw_amount(ctx: &EffectContext, sa: &SpellAbility) -> i32 {
129    if let Some(EffectIr::Draw(ir)) = &sa.ir.effect {
130        if let Some(amount) = &ir.amount {
131            let resolved = amount.resolve_for_spell_ability(ctx.game, sa, 1);
132            #[cfg(debug_assertions)]
133            debug_assert_eq!(
134                resolved,
135                resolve_numeric_svar(ctx.game, sa, keys::NUM_CARDS, 1),
136                "compiled Draw amount diverged from string params"
137            );
138            return resolved;
139        }
140    }
141
142    resolve_numeric_svar(ctx.game, sa, keys::NUM_CARDS, 1)
143}