Skip to main content

manabrew_engine/ability/effects/
discard_effect.rs

1use forge_foundation::ZoneType;
2use serde::{Deserialize, Serialize};
3
4use super::EffectContext;
5use crate::ability::spell_ability_effect::get_target_players;
6
7/// `Mode$` vocabulary for `DB$ Discard`. Mirrors Java
8/// `DiscardEffect.resolve`'s mode dispatch. Default is [`Self::TgtChoose`].
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
10pub enum DiscardMode {
11    #[default]
12    TgtChoose,
13    YouChoose,
14    RevealTgtChoose,
15    RevealYouChoose,
16    Random,
17    Hand,
18}
19
20impl DiscardMode {
21    pub fn parse(raw: &str) -> Option<Self> {
22        match raw {
23            "TgtChoose" => Some(Self::TgtChoose),
24            "YouChoose" => Some(Self::YouChoose),
25            "RevealTgtChoose" => Some(Self::RevealTgtChoose),
26            "RevealYouChoose" => Some(Self::RevealYouChoose),
27            "Random" => Some(Self::Random),
28            "Hand" => Some(Self::Hand),
29            _ => None,
30        }
31    }
32
33    fn chooser_style_optional(self) -> bool {
34        matches!(
35            self,
36            Self::TgtChoose | Self::YouChoose | Self::RevealYouChoose | Self::RevealTgtChoose
37        )
38    }
39
40    fn reveals_hand(self) -> bool {
41        matches!(self, Self::RevealTgtChoose | Self::RevealYouChoose)
42    }
43
44    fn chooser_is_activator(self) -> bool {
45        matches!(self, Self::YouChoose | Self::RevealYouChoose)
46    }
47}
48
49/// SP$ Discard — target/defined players discard N cards.
50///
51/// Mirrors Java's `DiscardEffect.resolve()`.
52#[manabrew_engine_macros::spell_effect(DiscardEffect)]
53fn resolve(ctx: &mut EffectContext, sa: &crate::spellability::SpellAbility) {
54    let num = super::resolve_numeric_svar(ctx.game, sa, crate::parsing::keys::NUM_CARDS, 1).max(0)
55        as usize;
56    let mode = sa.ir.discard_mode.unwrap_or_default();
57
58    // AnyNumber$ True — discarder may pick 0..=hand.len (Cavalier of Flame,
59    // Careful Study). Routes through a distinct agent method so deterministic
60    // agents sample count + selection with the same RNG trajectory Java uses.
61    let any_number = sa.ir.any_number;
62
63    let target_players = get_target_players(ctx.game, sa);
64    let first_target = target_players.first().copied();
65
66    for target_player in target_players.iter().copied() {
67        let mut hand: Vec<_> = ctx
68            .game
69            .cards_in_zone(ZoneType::Hand, target_player)
70            .to_vec();
71        if let Some(valid_filter) = sa.ir.discard_valid_text.as_deref() {
72            let valid_selector = sa.ir.discard_valid_selector.as_ref();
73            hand.retain(|&card_id| {
74                super::matches_valid_cards_for_sa(
75                    ctx.game,
76                    sa,
77                    ctx.game.card(card_id),
78                    valid_selector,
79                    valid_filter,
80                )
81            });
82        }
83
84        // AnyNumber$ True implicitly subsumes Optional — picking 0 = decline.
85        // Java's DiscardEffect doesn't fire a separate confirm_action there.
86        let chooser_style_optional = mode.chooser_style_optional();
87
88        // Java DiscardEffect.resolve(): chooser defaults to the discarder,
89        // *YouChoose routes to the activator, RevealTgtChoose pins to the
90        // first target. DiscardEffect.java:236-241.
91        let chooser = if mode.chooser_is_activator() {
92            sa.activating_player
93        } else if mode == DiscardMode::RevealTgtChoose {
94            first_target.unwrap_or(target_player)
95        } else {
96            target_player
97        };
98
99        // Reveal* modes broadcast the discarder's hand before the pick
100        // (DiscardEffect.java:244 `game.getAction().reveal(...)`).
101        if mode.reveals_hand() && !hand.is_empty() {
102            let source_name = sa.source.map(|cid| ctx.game.card(cid).card_name.clone());
103            for agent in ctx.agents.iter_mut() {
104                agent.reveal_cards(
105                    ctx.game,
106                    target_player,
107                    &hand,
108                    ZoneType::Hand,
109                    target_player,
110                    source_name.as_deref(),
111                );
112            }
113        }
114
115        if sa.ir.optional && !any_number && !chooser_style_optional {
116            let accepted = ctx.agents[target_player.index()].confirm_action(
117                target_player,
118                None,
119                "Do you want to discard?",
120                &[],
121                sa.source,
122                Some(crate::ability::api_type::ApiType::Discard),
123            );
124            if !accepted {
125                continue;
126            }
127        }
128
129        // Mode$ Hand — discard the whole hand, no chooser
130        // (DiscardEffect.java:154-174).
131        if mode == DiscardMode::Hand {
132            let remember_discarded = sa.ir.remember_discarded;
133            for card_id in hand.iter().copied() {
134                if ctx.game.card(card_id).zone == ZoneType::Hand {
135                    if remember_discarded {
136                        if let Some(sid) = sa.source {
137                            ctx.game.card_mut(sid).add_remembered_card(card_id);
138                        }
139                    }
140                    ctx.game.discard_card(
141                        card_id,
142                        target_player,
143                        Some(sa),
144                        Some(ctx.agents),
145                        ctx.trigger_handler,
146                    );
147                }
148            }
149            continue;
150        }
151
152        let to_discard = match mode {
153            DiscardMode::Random => {
154                ctx.agents[target_player.index()].choose_random_discard(target_player, &hand, num)
155            }
156            _ if any_number => ctx.agents[chooser.index()].choose_discard_any_number(
157                target_player,
158                &hand,
159                0,
160                hand.len(),
161            ),
162            _ if sa.ir.optional && chooser_style_optional => ctx.agents[chooser.index()]
163                .choose_discard_any_number(target_player, &hand, 0, num.min(hand.len())),
164            _ if chooser_style_optional => {
165                ctx.agents[chooser.index()].choose_discard(target_player, &hand, num)
166            }
167            _ => ctx.agents[target_player.index()].choose_discard(target_player, &hand, num),
168        };
169
170        // RememberDiscarded$ — source remembers each card actually discarded
171        // so downstream SubAbility effects (e.g. Cavalier of Flame's
172        // `NumCards$ Y` where Y = Remembered count) can compute amounts.
173        let remember_discarded = sa.ir.remember_discarded;
174
175        let to_discard = if to_discard.len() > 1 {
176            let reordered = ctx.agents[target_player.index()].choose_reorder_library(
177                ctx.game,
178                target_player,
179                &to_discard,
180            );
181            if reordered.len() == to_discard.len() {
182                reordered
183            } else {
184                to_discard
185            }
186        } else {
187            to_discard
188        };
189
190        for card_id in to_discard {
191            if ctx.game.card(card_id).zone == ZoneType::Hand {
192                if remember_discarded {
193                    if let Some(sid) = sa.source {
194                        ctx.game.card_mut(sid).add_remembered_card(card_id);
195                    }
196                }
197                ctx.game.discard_card(
198                    card_id,
199                    target_player,
200                    Some(sa),
201                    Some(ctx.agents),
202                    ctx.trigger_handler,
203                );
204            }
205        }
206    }
207}