use forge_foundation::ZoneType;
use super::{emit_zone_trigger, matches_change_type, EffectContext};
use crate::ids::{CardId, PlayerId};
fn matches_change_zone_all_filter(
cid: CardId,
game: &crate::game::GameState,
filter: &str,
source_chosen_colors: &[String],
effective_target: Option<CardId>,
) -> bool {
if filter.trim().is_empty() {
return true;
}
for clause in filter.split(',') {
let clause = clause.trim();
if clause.is_empty() {
continue;
}
let mut clause_ok = true;
let targeted = effective_target;
for term in clause.split('+') {
let term = term.trim();
if term.is_empty() {
continue;
}
if term.eq_ignore_ascii_case("NotDefinedTargeted") {
if let Some(t) = targeted {
if cid == t {
clause_ok = false;
break;
}
} else {
clause_ok = false;
break;
}
continue;
}
if term.eq_ignore_ascii_case("TargetedCard.Self") {
if targeted != Some(cid) {
clause_ok = false;
break;
}
continue;
}
if term.starts_with("sharesNameWith") {
let arg = term
.strip_prefix("sharesNameWith")
.unwrap_or("")
.trim_start();
if arg.eq_ignore_ascii_case("Targeted") {
if let Some(t) = targeted {
if game.card(cid).card_name != game.card(t).card_name {
clause_ok = false;
break;
}
} else {
clause_ok = false;
break;
}
continue;
}
}
if term.starts_with("ControlledBy") {
let arg = term.strip_prefix("ControlledBy").unwrap_or("").trim_start();
if arg.eq_ignore_ascii_case("TargetedController") {
if let Some(t) = targeted {
if game.card(cid).controller != game.card(t).controller {
clause_ok = false;
break;
}
} else {
clause_ok = false;
break;
}
continue;
}
}
if !matches_change_type(game.card(cid), term, source_chosen_colors) {
clause_ok = false;
break;
}
}
if clause_ok {
return true;
}
}
false
}
pub fn build_spell_ability(sa: &mut crate::spellability::SpellAbility) {
if let Some(zone) = sa.origin_zone() {
if let Some(ref mut tr) = sa.target_restrictions {
if !tr.can_tgt_player() {
tr.tgt_zone = vec![zone];
}
}
}
}
#[manabrew_engine_macros::spell_effect(ChangeZoneAllEffect)]
fn resolve(ctx: &mut EffectContext, sa: &crate::spellability::SpellAbility) {
let Some(origin_zone) = sa
.origin_zone()
.or_else(|| sa.origin().is_none().then_some(ZoneType::Battlefield))
else {
return;
};
let Some(dest_zone) = sa
.destination_zone()
.or_else(|| sa.destination().is_none().then_some(ZoneType::Graveyard))
else {
return;
};
let valid_cards_filter = sa
.change_type()
.or(sa.ir.valid_cards_text.as_deref())
.map(|s| s.to_string())
.unwrap_or_else(|| "Card".to_string());
let tapped = sa.ir.tapped;
let source_chosen_colors: Vec<String> = sa
.source
.map(|src| ctx.game.card(src).chosen_colors.clone())
.unwrap_or_default();
let effective_target = sa.target_chosen.target_card.or(ctx.parent_target_card);
let until_host_leaves = sa.ir.duration.as_ref().is_some_and(|duration| {
matches!(
duration,
crate::spellability::AbilityDuration::UntilHostLeavesPlay
| crate::spellability::AbilityDuration::UntilHostLeavesPlayOrEot
)
});
let exile_source = if until_host_leaves { sa.source } else { None };
{
let player_ids: Vec<PlayerId> = if let Some(pid) = sa.target_chosen.target_player {
vec![pid]
} else if let Some(defined) = sa.ir.defined_text.as_deref() {
let resolved = crate::ability::ability_utils::resolve_defined_players_with_sa(
defined,
sa,
sa.activating_player,
ctx.game,
);
if resolved.is_empty() {
ctx.game.player_order.clone()
} else {
resolved
}
} else {
ctx.game.player_order.clone()
};
let mut to_move: Vec<(CardId, PlayerId)> = Vec::new();
for &pid in &player_ids {
let zone_cards = ctx.game.cards_in_zone(origin_zone, pid).to_vec();
for cid in zone_cards {
if matches_change_zone_all_filter(
cid,
ctx.game,
&valid_cards_filter,
&source_chosen_colors,
effective_target,
) {
let dest_owner = if dest_zone == ZoneType::Battlefield {
sa.activating_player
} else {
ctx.game.card(cid).owner
};
to_move.push((cid, dest_owner));
}
}
}
if dest_zone == ZoneType::Library && to_move.len() > 1 && sa.ir.random_order {
let mut cards = to_move.iter().map(|(cid, _)| *cid).collect::<Vec<_>>();
ctx.rng.shuffle_cards(&mut cards);
let mut ordered = Vec::with_capacity(to_move.len());
for cid in cards {
if let Some((_, owner)) = to_move.iter().find(|(card_id, _)| *card_id == cid) {
ordered.push((cid, *owner));
}
}
to_move = ordered;
}
let mut moved_to_library: Vec<(CardId, PlayerId)> = Vec::new();
for (card_id, dest_owner) in to_move {
if ctx.game.card(card_id).zone != origin_zone {
continue; }
let old_zone = ctx.game.card(card_id).zone;
ctx.move_card(card_id, dest_zone, dest_owner);
if dest_zone == ZoneType::Library {
moved_to_library.push((card_id, dest_owner));
}
if dest_zone == ZoneType::Exile {
if let Some(src_id) = exile_source {
ctx.game.card_mut(card_id).set_exiled_by(Some(src_id));
}
}
if dest_zone == ZoneType::Battlefield {
if tapped {
ctx.game.tap(card_id);
}
ctx.trigger_handler
.register_active_trigger(ctx.game, card_id);
}
emit_zone_trigger(ctx.trigger_handler, card_id, old_zone, dest_zone);
}
if dest_zone == ZoneType::Library && sa.library_position() == Some("-1") {
for &pid in &player_ids {
let cards = moved_to_library
.iter()
.filter_map(|(cid, owner)| (*owner == pid).then_some(*cid))
.collect::<Vec<_>>();
if !cards.is_empty() {
ctx.game
.move_cards_to_zone_bottom(ZoneType::Library, pid, &cards);
}
}
}
if sa.ir.shuffle_raw.is_some() {
for &pid in &player_ids {
ctx.game.shuffle_zone_cards(ZoneType::Library, pid, ctx.rng);
}
}
}
}
#[cfg(test)]
mod tests {
use super::matches_change_zone_all_filter;
use crate::card::Card;
use crate::game::GameState;
use crate::ids::{CardId, PlayerId};
use forge_foundation::{CardTypeLine, ColorSet, ManaCost, ZoneType};
#[test]
fn deputy_filter_only_hits_targeted_name_group() {
let mut game = GameState::new(&["A", "B"], 20);
let p0 = PlayerId(0);
let p1 = PlayerId(1);
let make = |name: &str, owner: PlayerId, type_line: &str| {
Card::new(
CardId(0),
name.to_string(),
owner,
CardTypeLine::parse(type_line),
ManaCost::no_cost(),
ColorSet::COLORLESS,
None,
None,
vec![],
vec![],
)
};
let t1 = game.create_card(make("Token Engine", p1, "Artifact"));
let t2 = game.create_card(make("Token Engine", p1, "Artifact"));
let opp_land = game.create_card(make("Island", p1, "Land Island"));
let my_land = game.create_card(make("Forest", p0, "Land Forest"));
game.move_card(t1, ZoneType::Battlefield, p1);
game.move_card(t2, ZoneType::Battlefield, p1);
game.move_card(opp_land, ZoneType::Battlefield, p1);
game.move_card(my_land, ZoneType::Battlefield, p0);
let filter = "TargetedCard.Self,Permanent.nonLand+NotDefinedTargeted+sharesNameWith Targeted+ControlledBy TargetedController";
assert!(matches_change_zone_all_filter(
t1,
&game,
filter,
&[],
Some(t1)
));
assert!(matches_change_zone_all_filter(
t2,
&game,
filter,
&[],
Some(t1)
));
assert!(!matches_change_zone_all_filter(
opp_land,
&game,
filter,
&[],
Some(t1)
));
assert!(!matches_change_zone_all_filter(
my_land,
&game,
filter,
&[],
Some(t1)
));
}
}