Skip to main content

manabrew_engine/ability/effects/
permanent_effect.rs

1//! PermanentEffect — move spell to battlefield.
2//!
3//! Mirrors Java's `PermanentEffect.java`.
4//! Handles moving a spell from the stack to the battlefield as a permanent.
5//! This is the parent logic for both PermanentCreatureEffect and
6//! PermanentNoncreatureEffect.
7
8use forge_foundation::ZoneType;
9
10use super::EffectContext;
11use crate::parsing::keys;
12use crate::spellability::SpellAbility;
13
14/// Resolve a permanent entering the battlefield.
15/// Struct form of this effect so it can participate in the
16/// `SpellAbilityEffect` trait hierarchy — mirrors Java's
17/// `PermanentEffect` class extending `SpellAbilityEffect`.
18#[manabrew_engine_macros::spell_effect(PermanentEffect)]
19fn resolve(ctx: &mut EffectContext, sa: &crate::spellability::SpellAbility) {
20    resolve_permanent_common(ctx, sa);
21}
22
23/// Shared implementation for Permanent, PermanentCreature and PermanentNoncreature.
24/// Both extend PermanentEffect in Java, which simply moves the host to play.
25pub fn resolve_permanent_common(ctx: &mut EffectContext, sa: &SpellAbility) {
26    let source = match sa.source {
27        Some(s) => s,
28        None => return,
29    };
30
31    let controller = sa.activating_player;
32
33    // Check if it should enter tapped (sneak/dash)
34    if sa.param_is_true(keys::SNEAK) || sa.param_is_true(keys::TAPPED) {
35        ctx.game.card_mut(source).set_tapped(true);
36    }
37
38    // Move host card to battlefield.
39    //
40    // Java parity: PermanentEffect.resolve() calls game.getAction().moveToPlay
41    // which moves the card AND fires the ChangesZone trigger as a single op.
42    // In Rust the corresponding `emit_zone_trigger` lives at the spell
43    // resolution site (game_loop/stack_resolution.rs after move_card_with_runtime),
44    // so we must NOT emit here — doing so double-fires the ETB trigger
45    // (e.g. Rottenmouth Viper enters → 2 PutCounter + 2 DBRepeat iterations).
46    let old_zone = ctx.game.card(source).zone;
47    if old_zone != ZoneType::Battlefield {
48        ctx.game
49            .move_card(source, ZoneType::Battlefield, controller);
50    }
51}