manabrew_engine/ability/effects/
copy_permanent_effect.rs1use forge_foundation::color::ColorSet;
2use forge_foundation::ZoneType;
3
4use super::token_effect_base::{TokenCreateTable, TokenEffectBase, TOKEN_EFFECT_BASE};
5use super::EffectContext;
6use crate::card::card_zone_table::CardZoneTable;
7use crate::card::Card;
8use crate::ids::CardId;
9use crate::spellability::SpellAbility;
10
11#[manabrew_engine_macros::spell_effect(CopyPermanentEffect)]
15fn resolve(ctx: &mut EffectContext, sa: &crate::spellability::SpellAbility) {
16 let amount = super::resolve_numeric_svar(ctx.game, sa, "NumCopies", 1).max(0) as usize;
17 let controllers = resolve_copy_controllers(ctx, sa);
18 if controllers.is_empty() {
19 return;
20 }
21
22 let mut token_table = TokenCreateTable::default();
23 for controller in controllers {
24 if !ctx.game.player(controller).is_alive() {
25 continue;
26 }
27
28 let originals = resolve_originals(ctx, sa, controller);
29 for original_id in originals {
30 if ctx.game.card(original_id).type_line.is_instant()
31 || ctx.game.card(original_id).type_line.is_sorcery()
32 {
33 continue;
34 }
35 if sa.ir.defined_text.is_none()
36 && sa.ir.choices.is_none()
37 && ctx.game.card(original_id).zone != ZoneType::Battlefield
38 {
39 continue;
40 }
41
42 if let Some(for_each) = sa.ir.for_each_text.as_deref() {
43 let players = crate::ability::ability_utils::resolve_defined_players_with_sa(
44 for_each,
45 sa,
46 sa.activating_player,
47 ctx.game,
48 );
49 for player in players {
50 let mut proto = get_proto_type(sa, ctx.game.card(original_id), controller);
51 proto.copied_permanent = Some(original_id);
52 proto.add_remembered_player(player);
53 token_table.put(controller, proto, amount);
54 }
55 } else {
56 let mut proto = get_proto_type(sa, ctx.game.card(original_id), controller);
57 proto.copied_permanent = Some(original_id);
58 token_table.put(controller, proto, amount);
59 }
60 }
61 }
62
63 if token_table.is_empty() {
64 return;
65 }
66
67 let mut trigger_list = CardZoneTable::default();
68 let result = TOKEN_EFFECT_BASE.make_token_table(ctx, token_table, true, &mut trigger_list, sa);
69 if !result.created.is_empty() {
70 trigger_list.trigger_changes_zone_all(ctx.trigger_handler, ctx.game, Some(sa));
71 }
72}
73
74pub fn get_proto_type(sa: &SpellAbility, original: &Card, new_owner: crate::ids::PlayerId) -> Card {
84 let mut copy = Card::new(
85 CardId(0),
86 original.card_name.clone(),
87 new_owner,
88 original.type_line.clone(),
89 original.mana_cost.clone(),
90 original.color,
91 original.base_power,
92 original.base_toughness,
93 original.keywords.as_string_list(),
94 original.abilities.clone(),
95 );
96 copy.set_triggers(original.copiable_triggers());
97 copy.set_svars_map(original.svars.clone());
98 copy.set_static_abilities(original.static_abilities.clone());
99 copy.set_replacement_effects(original.copiable_replacement_effects());
100 copy.set_perpetual(original, false);
101 copy.initial_loyalty = original.initial_loyalty.clone();
102 copy.set_code = original.set_code.clone();
103 copy.set_is_token(true);
105
106 if let Some(set_color) = sa.ir.set_color.as_deref() {
108 copy.set_color(ColorSet::from_names(set_color));
109 }
110
111 if let Some(add_types) = sa.ir.add_types.as_deref() {
113 for t in add_types.split(" & ") {
114 let t = t.trim();
115 if !t.is_empty() {
116 copy.add_type(t);
117 }
118 }
119 }
120
121 if let Some(p) = sa
123 .ir
124 .set_power
125 .as_deref()
126 .and_then(|value| value.parse().ok())
127 {
128 copy.set_base_power(Some(p));
129 }
130 if let Some(t) = sa
131 .ir
132 .set_toughness
133 .as_deref()
134 .and_then(|value| value.parse().ok())
135 {
136 copy.set_base_toughness(Some(t));
137 }
138
139 if let Some(add_kws) = sa.ir.add_keywords.as_deref() {
148 for kw in add_kws.split(" & ") {
149 let kw = kw.trim();
150 if !kw.is_empty() {
151 copy.add_intrinsic_keyword(kw);
152 }
153 }
154 }
155
156 if sa
158 .ir
159 .set_mana_cost
160 .as_deref()
161 .is_some_and(|v| v == "0" || v.is_empty())
162 {
163 copy.set_mana_cost(forge_foundation::mana::ManaCost::no_cost());
164 }
165
166 copy
167}
168
169fn resolve_copy_controllers(ctx: &EffectContext, sa: &SpellAbility) -> Vec<crate::ids::PlayerId> {
170 if let Some(controller) = sa.ir.controller_text.as_deref() {
171 let players = crate::ability::ability_utils::resolve_defined_players_with_sa(
172 controller,
173 sa,
174 sa.activating_player,
175 ctx.game,
176 );
177 if !players.is_empty() {
178 return players;
179 }
180 }
181 vec![sa.activating_player]
182}
183
184fn resolve_originals(
185 ctx: &mut EffectContext,
186 sa: &SpellAbility,
187 controller: crate::ids::PlayerId,
188) -> Vec<CardId> {
189 if let Some(choices) = sa.ir.choices.as_deref() {
190 let candidates: Vec<CardId> = ctx
191 .game
192 .cards
193 .iter()
194 .filter(|card| card.zone == ZoneType::Battlefield)
195 .map(|card| card.id)
196 .filter(|&card_id| {
197 let Some(source_id) = sa.source else {
198 return false;
199 };
200 crate::card::valid_filter::matches_valid(
201 choices,
202 Some(ctx.game.card(card_id)),
203 None,
204 ctx.game.card(source_id),
205 sa.activating_player,
206 )
207 })
208 .collect();
209 if candidates.is_empty() {
210 return Vec::new();
211 }
212
213 let chooser = sa
214 .ir
215 .chooser
216 .as_deref()
217 .and_then(|defined| {
218 crate::ability::ability_utils::resolve_defined_players_with_sa(
219 defined,
220 sa,
221 sa.activating_player,
222 ctx.game,
223 )
224 .into_iter()
225 .next()
226 })
227 .unwrap_or(sa.activating_player);
228 ctx.agents[chooser.index()].snapshot_state(ctx.game, ctx.mana_pools);
229 return ctx.agents[chooser.index()]
230 .choose_single_card_for_zone_change(
231 ctx.game,
232 chooser,
233 &candidates,
234 "Choose a card",
235 false,
236 )
237 .into_iter()
238 .collect();
239 }
240
241 if let Some(defined) = sa.defined() {
242 match defined {
243 "Self" => return sa.source.into_iter().collect(),
244 "ParentTarget" => return sa.target_chosen.target_card.into_iter().collect(),
245 "TriggeredCard" | "TriggeredCardLKICopy" | "TriggeredSacrificedCard" => {
246 return sa
247 .get_triggering_card(crate::ability::AbilityKey::Card)
248 .into_iter()
249 .collect();
250 }
251 _ => {
252 return crate::ability::ability_utils::get_defined_cards(
253 ctx.game,
254 sa.source,
255 defined,
256 Some(controller),
257 );
258 }
259 }
260 }
261
262 sa.target_chosen.target_card.into_iter().collect()
265}