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}