Skip to main content

manabrew_engine/ability/effects/
set_state_effect.rs

1use super::EffectContext;
2use crate::ids::CardId;
3use crate::parsing::keys;
4use crate::replacement::replacement_handler::{apply_replacements, ReplacementEvent};
5use crate::replacement::ReplacementResult;
6use crate::spellability::SpellAbilityMode;
7
8/// Mirrors Java's `SetStateEffect.java`.
9///
10/// `DB$ SetState | Defined$ Self | Mode$ Transform`
11///
12/// Optionally gated by:
13///   `ConditionDefined$ Remembered | ConditionPresent$ Card.Instant,Card.Sorcery | ConditionCompare$ EQ1`
14///
15/// If the condition passes, transforms the source DFC card to its other face.
16/// Struct form of this effect so it can participate in the
17/// `SpellAbilityEffect` trait hierarchy — mirrors Java's
18/// `SetStateEffect` class extending `SpellAbilityEffect`.
19#[manabrew_engine_macros::spell_effect(SetStateEffect)]
20fn resolve(ctx: &mut EffectContext, sa: &crate::spellability::SpellAbility) {
21    let mode = sa.ir.mode.as_ref();
22
23    let source_id = match sa.source {
24        Some(id) => id,
25        None => return,
26    };
27
28    match mode {
29        Some(SpellAbilityMode::Transform) => {
30            // Evaluate optional condition.
31            if let Some(cond_defined) = sa.ir.condition_defined.as_ref() {
32                if cond_defined.refs.first().is_some_and(|defined| {
33                    matches!(defined, crate::ability::ability_ir::DefinedRef::Remembered)
34                }) {
35                    let cond_present = sa.ir.condition_present.clone().unwrap_or_default();
36                    let cond_compare = sa.ir.condition_compare.clone().unwrap_or_default();
37
38                    let remembered: Vec<CardId> = ctx.game.card(source_id).remembered_cards.clone();
39                    let match_count = remembered
40                        .iter()
41                        .filter(|&&cid| matches_type_filter(ctx, cid, &cond_present))
42                        .count();
43
44                    if !evaluate_compare(&cond_compare, match_count) {
45                        return; // Condition not met.
46                    }
47                }
48            }
49
50            // Run Transform replacement effects before transforming.
51            let mut transform_event = ReplacementEvent::Transform { card: source_id };
52            let transform_result = apply_replacements(ctx.game, &mut transform_event);
53            if transform_result == ReplacementResult::Skipped
54                || transform_result == ReplacementResult::Replaced
55            {
56                return;
57            }
58
59            // Perform the transform.
60            ctx.game.card_mut(source_id).transform();
61
62            // Fire Transformed trigger
63            ctx.trigger_handler.run_trigger(
64                crate::trigger::TriggerType::Transformed,
65                crate::event::RunParams {
66                    card: Some(source_id),
67                    ..Default::default()
68                },
69                false,
70            );
71
72            // Re-scan active triggers so the new face's trigger list takes effect.
73            ctx.trigger_handler.reset_active_triggers(ctx.game);
74        }
75        Some(SpellAbilityMode::Flip) => {
76            // Toggle the flipped state.
77            let card = ctx.game.card_mut(source_id);
78            card.set_flipped(!card.flipped);
79        }
80        Some(SpellAbilityMode::TurnFaceUp) => {
81            // Run TurnFaceUp replacement effects before turning face up.
82            let mut faceup_event = ReplacementEvent::TurnFaceUp { card: source_id };
83            let faceup_result = apply_replacements(ctx.game, &mut faceup_event);
84            if faceup_result == ReplacementResult::Skipped
85                || faceup_result == ReplacementResult::Replaced
86            {
87                return;
88            }
89
90            let card = ctx.game.card_mut(source_id);
91            if card.face_down {
92                card.set_face_down(false);
93                // Restore original P/T by clearing the face-down overrides
94                card.set_static_set_pt(None, None);
95
96                // Remove the synthetic morph turn-face-up ability
97                card.activated_abilities
98                    .retain(|ab| !ab.ability_text.contains("Mode$ TurnFaceUp"));
99
100                // Megamorph: add a +1/+1 counter when turning face-up
101                if sa.param_is_true(keys::MEGA) {
102                    ctx.add_counter(
103                        source_id,
104                        &crate::card::CounterType::P1P1,
105                        1,
106                        sa,
107                        crate::event::RunParams::default(),
108                    );
109                }
110
111                // Fire TurnFaceUp trigger
112                ctx.trigger_handler.run_trigger(
113                    crate::trigger::TriggerType::TurnFaceUp,
114                    crate::event::RunParams {
115                        card: Some(source_id),
116                        ..Default::default()
117                    },
118                    false,
119                );
120
121                // Re-scan active triggers for the revealed card
122                ctx.trigger_handler.reset_active_triggers(ctx.game);
123            }
124        }
125        Some(SpellAbilityMode::TurnFaceDown) => {
126            let card = ctx.game.card_mut(source_id);
127            if !card.face_down {
128                card.set_face_down(true);
129            }
130        }
131        _ => {
132            let err = crate::ability::IllegalAbilityException::new(format!(
133                "Unknown SetState mode: {:?}",
134                mode.map(ToString::to_string)
135            ));
136            eprintln!("{}", err);
137        }
138    }
139}
140
141/// Check if a card matches a comma-separated type filter (OR semantics).
142/// E.g. `"Card.Instant,Card.Sorcery"` → true if the card is an Instant or Sorcery.
143fn matches_type_filter(ctx: &EffectContext, card_id: CardId, filter: &str) -> bool {
144    if filter.is_empty() {
145        return true;
146    }
147    for part in filter.split(',') {
148        let type_name = part.trim().strip_prefix("Card.").unwrap_or(part.trim());
149        let card = ctx.game.card(card_id);
150        if card
151            .type_line
152            .core_types
153            .iter()
154            .any(|t| t.name().eq_ignore_ascii_case(type_name))
155        {
156            return true;
157        }
158    }
159    false
160}
161
162/// Evaluate a `ConditionCompare` expression (e.g. `"EQ1"`, `"GT0"`) against a count.
163fn evaluate_compare(compare: &str, count: usize) -> bool {
164    if compare.len() < 3 {
165        return true;
166    }
167    let op = &compare[..2];
168    let num: usize = compare[2..].parse().unwrap_or(0);
169    match op {
170        "EQ" => count == num,
171        "GT" => count > num,
172        "GE" => count >= num,
173        "LT" => count < num,
174        "LE" => count <= num,
175        "NE" => count != num,
176        _ => true,
177    }
178}