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                    card.add_counter(&crate::card::CounterType::P1P1, 1);
103                }
104
105                // Fire TurnFaceUp trigger
106                ctx.trigger_handler.run_trigger(
107                    crate::trigger::TriggerType::TurnFaceUp,
108                    crate::event::RunParams {
109                        card: Some(source_id),
110                        ..Default::default()
111                    },
112                    false,
113                );
114
115                // Re-scan active triggers for the revealed card
116                ctx.trigger_handler.reset_active_triggers(ctx.game);
117            }
118        }
119        Some(SpellAbilityMode::TurnFaceDown) => {
120            let card = ctx.game.card_mut(source_id);
121            if !card.face_down {
122                card.set_face_down(true);
123            }
124        }
125        _ => {
126            let err = crate::ability::IllegalAbilityException::new(format!(
127                "Unknown SetState mode: {:?}",
128                mode.map(ToString::to_string)
129            ));
130            eprintln!("{}", err);
131        }
132    }
133}
134
135/// Check if a card matches a comma-separated type filter (OR semantics).
136/// E.g. `"Card.Instant,Card.Sorcery"` → true if the card is an Instant or Sorcery.
137fn matches_type_filter(ctx: &EffectContext, card_id: CardId, filter: &str) -> bool {
138    if filter.is_empty() {
139        return true;
140    }
141    for part in filter.split(',') {
142        let type_name = part.trim().strip_prefix("Card.").unwrap_or(part.trim());
143        let card = ctx.game.card(card_id);
144        if card
145            .type_line
146            .core_types
147            .iter()
148            .any(|t| t.name().eq_ignore_ascii_case(type_name))
149        {
150            return true;
151        }
152    }
153    false
154}
155
156/// Evaluate a `ConditionCompare` expression (e.g. `"EQ1"`, `"GT0"`) against a count.
157fn evaluate_compare(compare: &str, count: usize) -> bool {
158    if compare.len() < 3 {
159        return true;
160    }
161    let op = &compare[..2];
162    let num: usize = compare[2..].parse().unwrap_or(0);
163    match op {
164        "EQ" => count == num,
165        "GT" => count > num,
166        "GE" => count >= num,
167        "LT" => count < num,
168        "LE" => count <= num,
169        "NE" => count != num,
170        _ => true,
171    }
172}