manabrew_engine/ability/effects/change_zone_effect/mod.rs
1//! ChangeZone effect — moves cards between zones.
2//!
3//! Ported from Java's `ChangeZoneEffect.java`. Sub-modules:
4//! - [`helpers`] — matching, pre/post move, destination resolution, search restrictions
5//! - [`known`] — known-origin resolve (Battlefield, Graveyard, targeted cards)
6//! - [`hidden`] — hidden-origin resolve (Library/Hand searches)
7//! - [`search`] — search sub-routines (single, multi, each, random, player choice)
8//! - [`stack`] — stack removal (bouncing/exiling spells)
9//! - [`move_cards`] — shared move + post-processing logic
10
11pub(super) mod helpers;
12mod hidden;
13mod known;
14pub(super) mod move_cards;
15pub(super) mod search;
16mod stack;
17
18use forge_foundation::ZoneType;
19
20use super::EffectContext;
21use crate::spellability::SpellAbility;
22
23/// Struct form so this directory-module effect can participate in the
24/// `SpellAbilityEffect` trait hierarchy alongside the single-file effects.
25#[manabrew_engine_macros::spell_effect(ChangeZoneEffect)]
26fn resolve(ctx: &mut EffectContext, sa: &SpellAbility) {
27 resolve(ctx, sa);
28}
29
30/// Configure the spell ability during construction.
31/// Mirrors Java `ChangeZoneEffect.buildSpellAbility` — calls
32/// `adjustChangeZoneTarget` to set the target zone to the origin zone.
33pub fn build_spell_ability(sa: &mut SpellAbility) {
34 if let Some(zone) = sa.origin_zone() {
35 if let Some(ref mut tr) = sa.target_restrictions {
36 if !tr.can_tgt_player() {
37 tr.tgt_zone = vec![zone];
38 }
39 }
40 }
41}
42
43/// Top-level resolve dispatcher — mirrors Java's `resolve()` which splits on
44/// `sa.isHidden()` into hidden-origin (library search) vs known-origin paths.
45pub fn resolve(ctx: &mut EffectContext, sa: &SpellAbility) {
46 let dest_zone = match sa.destination_zone() {
47 Some(z) => z,
48 None => return,
49 };
50
51 // Multi-origin: Origin$ can be comma-separated (e.g. "Library,Graveyard")
52 let origins: Vec<ZoneType> = sa.origin_zones();
53
54 if origins.is_empty() {
55 // `Origin$ All` — Java's `ZoneType.listValueOf("All")` is empty and
56 // `ZoneType.isHidden(origin)` returns true for that case. Resolve
57 // the SA's `Defined$` (or targets) to concrete cards, then dispatch
58 // the known-origin path once per zone they currently occupy.
59 // `NoShuffle` because this is not a library search (CR 701.18).
60 let is_origin_all = sa.origin().is_some_and(|o| o.eq_ignore_ascii_case("All"));
61 if !is_origin_all {
62 return;
63 }
64 let defined_cards =
65 crate::ability::spell_ability_effect::get_defined_cards_or_targeted(ctx.game, sa);
66 if defined_cards.is_empty() {
67 return;
68 }
69 let mut zones: Vec<ZoneType> = Vec::new();
70 for cid in &defined_cards {
71 let zone = ctx.game.card(*cid).zone;
72 if !zones.contains(&zone) {
73 zones.push(zone);
74 }
75 }
76 let mut sa_no_shuffle = sa.clone();
77 sa_no_shuffle.ir.no_shuffle = true;
78 for zone in zones {
79 known::resolve_known_origin(ctx, &sa_no_shuffle, zone, dest_zone);
80 }
81 return;
82 }
83
84 let primary_origin = origins[0];
85
86 // Java parity: sa.isHidden() && !sa.isNinjutsu() → hidden path
87 if (primary_origin.is_hidden() || sa.is_hidden()) && !sa.ir.ninjutsu {
88 hidden::resolve_hidden_origin(ctx, sa, primary_origin, dest_zone);
89 } else {
90 known::resolve_known_origin(ctx, sa, primary_origin, dest_zone);
91 }
92}