Skip to main content

manabrew_engine/ability/effects/
airbend_effect.rs

1//! Airbend — exile target cards, their owner may cast them for {2}.
2//! Mirrors Java `AirbendEffect`: exiles each card and creates a per-card
3//! Effect in the Command zone that grants `MayPlay` with an alternate
4//! mana cost of `{2}`, so the owner may cast the exiled card from exile.
5
6use forge_foundation::{CardTypeLine, ColorSet, ManaCost, ZoneType};
7
8use super::EffectContext;
9use crate::ability::ability_ir::DefinedRef;
10use crate::card::Card;
11use crate::ids::CardId;
12use crate::staticability::parse_static_ability;
13
14/// Struct form of this effect so it can participate in the
15/// `SpellAbilityEffect` trait hierarchy — mirrors Java's
16/// `AirbendEffect` class extending `SpellAbilityEffect`.
17#[manabrew_engine_macros::spell_effect(AirbendEffect)]
18fn resolve(ctx: &mut EffectContext, sa: &crate::spellability::SpellAbility) {
19    let parent_target = ctx.parent_target_card.or(sa.target_chosen.target_card);
20    let primary_def = sa.ir.defined.as_ref().and_then(|d| d.refs.first());
21    let targets: Vec<CardId> = match primary_def {
22        None => {
23            if let Some(target) = sa.target_chosen.target_card {
24                vec![target]
25            } else if let Some(source) = sa.source {
26                vec![source]
27            } else {
28                return;
29            }
30        }
31        Some(DefinedRef::SelfCard) => sa.source.into_iter().collect(),
32        Some(DefinedRef::Remembered) => sa
33            .source
34            .map(|sid| ctx.game.card(sid).remembered_cards.clone())
35            .unwrap_or_default(),
36        Some(DefinedRef::Targeted) => sa.target_chosen.target_card.into_iter().collect(),
37        Some(DefinedRef::Unsupported(raw)) if raw.starts_with("Valid ") => {
38            // `Defined$ Valid <selector>` (Avatar's Wrath uses
39            // `Valid Creature.NotDefinedTargeted`). Java's
40            // `AirbendEffect` resolves these via `getCardsfromTargets`,
41            // which delegates to `knownDetermineDefined`.
42            let filter = raw.strip_prefix("Valid ").unwrap_or("").trim();
43            let stripped: String = filter
44                .split('.')
45                .map(|seg| {
46                    seg.split('+')
47                        .filter(|term| !term.eq_ignore_ascii_case("NotDefinedTargeted"))
48                        .collect::<Vec<_>>()
49                        .join("+")
50                })
51                .filter(|seg| !seg.is_empty())
52                .collect::<Vec<_>>()
53                .join(".");
54            let mut out = Vec::new();
55            for &pid in &ctx.game.player_order {
56                for &cid in ctx.game.cards_in_zone(ZoneType::Battlefield, pid) {
57                    if filter.contains("NotDefinedTargeted")
58                        && parent_target.is_some_and(|t| t == cid)
59                    {
60                        continue;
61                    }
62                    if super::matches_change_type(ctx.game.card(cid), &stripped, &[]) {
63                        out.push(cid);
64                    }
65                }
66            }
67            out
68        }
69        _ => sa.source.into_iter().collect(),
70    };
71
72    let host_id = sa.source;
73    let host_image = host_id.map(|id| ctx.game.card(id).card_name.clone());
74
75    // Cumulative list of cards this resolution has moved into Exile and not
76    // since left it. Mirrors Java `zoneMovements.filterCards(null, [Exile], …)`
77    // observed from inside the per-target loop: each new effect remembers
78    // every airbent card so far, so an early target ends up "remembered" by
79    // every subsequently-created effect.
80    let mut exiled_so_far: Vec<CardId> = Vec::new();
81
82    for card_id in targets {
83        if ctx.game.card(card_id).zone == ZoneType::None {
84            continue;
85        }
86
87        // Exile the card
88        let old_zone = ctx.game.card(card_id).zone;
89        let owner = ctx.game.card(card_id).owner;
90        ctx.move_card(card_id, ZoneType::Exile, owner);
91        super::emit_zone_trigger(ctx.trigger_handler, card_id, old_zone, ZoneType::Exile);
92
93        if ctx.game.card(card_id).is_token {
94            continue;
95        }
96        if ctx.game.card(card_id).zone != ZoneType::Exile {
97            continue;
98        }
99
100        // Java's `removeIf(Card::isToken)` mirrored above; the cumulative list
101        // only collects non-token cards that ended this iteration in Exile.
102        exiled_so_far.push(card_id);
103
104        // Per-card persistent Effect in the Command zone granting
105        // `MayPlay$ True | MayPlayAltManaCost$ 2` to the exiled card while
106        // it's still exiled. Mirrors Java `AirbendEffect.resolve`.
107        let card_name_for_effect = ctx.game.card(card_id).card_name.clone();
108        let host_name = host_image.clone().unwrap_or_else(|| "Airbend".to_string());
109        let effect_name = format!("{} ({})", host_name, card_name_for_effect);
110
111        let mut effect = Card::new(
112            CardId(0),
113            effect_name,
114            owner,
115            CardTypeLine::parse("Effect"),
116            ManaCost::parse("0"),
117            ColorSet::COLORLESS,
118            None,
119            None,
120            vec![],
121            vec![],
122        );
123        effect.set_controller(owner);
124        effect.set_effect_source(host_id);
125        let static_text = "S$ Mode$ Continuous | MayPlay$ True | MayPlayAltManaCost$ 2 \
126            | EffectZone$ Command | Affected$ Card.IsRemembered+nonLand \
127            | AffectedZone$ Exile | Description$ You may cast the card.";
128        if let Some(static_ab) = parse_static_ability(static_text) {
129            effect.set_static_abilities(vec![static_ab]);
130        }
131        effect.add_remembered_cards(exiled_so_far.iter().copied());
132        effect.set_forget_on_moved_origin(Some(ZoneType::Exile));
133
134        let effect_id = ctx.game.create_card(effect);
135        ctx.move_card(effect_id, ZoneType::Command, owner);
136    }
137}