manabrew_engine/ability/effects/
change_text_effect.rs1use super::EffectContext;
6use crate::game::TypeRegistry;
7use crate::ids::{CardId, PlayerId};
8use crate::spellability::{AbilityDuration, SpellAbility};
9
10const COLORS: [&str; 5] = ["White", "Blue", "Black", "Red", "Green"];
11const BASIC_LAND_TYPES: [&str; 5] = ["Plains", "Island", "Swamp", "Mountain", "Forest"];
12const NO_PRIOR_TEXT_CHANGE: &str = "__forge_no_prior_text_change__";
13
14pub fn run(game: &mut crate::game::GameState, card_id: CardId) {
21 restore_text_changes(game, card_id, "TextColor:", "TextColorEOT:");
22 restore_text_changes(game, card_id, "TextType:", "TextTypeEOT:");
23}
24
25#[manabrew_engine_macros::spell_effect(ChangeTextEffect)]
26fn resolve(ctx: &mut EffectContext, sa: &SpellAbility) {
27 let color_change = sa
28 .ir
29 .change_color_word_text
30 .as_deref()
31 .and_then(|raw| resolve_color_word_change(ctx, sa, raw));
32 let type_change = sa
33 .ir
34 .change_type_word_text
35 .as_deref()
36 .and_then(|raw| resolve_type_word_change(ctx, sa, raw));
37
38 if color_change.is_none() && type_change.is_none() {
39 return;
40 }
41
42 let permanent = matches!(sa.ir.duration, Some(AbilityDuration::Permanent));
43 let targets = resolve_target_cards(ctx, sa);
44 for target in targets {
45 if let Some((from, to)) = color_change.as_ref() {
46 if !permanent {
47 mark_eot_text_change(ctx.game, target, "TextColor:", "TextColorEOT:", from);
48 }
49 ctx.game
50 .card_mut(target)
51 .add_changed_text_color_word(from, to);
52 }
53 if let Some((from, to)) = type_change.as_ref() {
54 if !permanent {
55 mark_eot_text_change(ctx.game, target, "TextType:", "TextTypeEOT:", from);
56 }
57 ctx.game
58 .card_mut(target)
59 .add_changed_text_type_word(from, to);
60 }
61 }
62}
63
64fn resolve_color_word_change(
65 ctx: &mut EffectContext,
66 sa: &SpellAbility,
67 raw: &str,
68) -> Option<(String, String)> {
69 let (from_raw, to_raw) = split_word_change(raw)?;
70 let controller = sa.activating_player;
71
72 let from = if from_raw == "Choose" {
73 choose_color(ctx, controller, COLORS.iter().copied())
74 } else {
75 Some(from_raw.to_string())
76 }?;
77
78 let to = if to_raw == "Choose" {
79 let choices = COLORS
80 .iter()
81 .copied()
82 .filter(|color| !color.eq_ignore_ascii_case(&from));
83 choose_color(ctx, controller, choices)
84 } else {
85 Some(to_raw.to_string())
86 }?;
87
88 Some((from, to))
89}
90
91fn resolve_type_word_change(
92 ctx: &mut EffectContext,
93 sa: &SpellAbility,
94 raw: &str,
95) -> Option<(String, String)> {
96 let (from_raw, to_raw) = split_word_change(raw)?;
97 let controller = sa.activating_player;
98
99 let from = match from_raw {
100 "ChooseBasicLandType" => choose_type(ctx, controller, "basic land", basic_land_types()),
101 "ChooseCreatureType" => choose_type(ctx, controller, "Creature", creature_types()),
102 _ => Some(from_raw.to_string()),
103 }?;
104
105 let to = if to_raw.starts_with("Choose") {
106 let mut valid_types = match to_raw {
107 "ChooseBasicLandType" => basic_land_types(),
108 "ChooseCreatureType" => creature_types(),
109 _ => Vec::new(),
110 };
111 let mut forbidden = forbidden_new_types(sa);
112 forbidden.push(from.clone());
113 valid_types.retain(|ty| {
114 !forbidden
115 .iter()
116 .any(|forbid| forbid.eq_ignore_ascii_case(ty))
117 });
118 choose_type(
119 ctx,
120 controller,
121 if to_raw == "ChooseBasicLandType" {
122 "basic land"
123 } else {
124 "Creature"
125 },
126 valid_types,
127 )
128 } else {
129 Some(to_raw.to_string())
130 }?;
131
132 Some((from, to))
133}
134
135fn split_word_change(raw: &str) -> Option<(&str, &str)> {
136 let mut parts = raw.split_whitespace();
137 let from = parts.next()?;
138 let to = parts.next()?;
139 Some((from, to))
140}
141
142fn choose_color<'a>(
143 ctx: &mut EffectContext,
144 player: PlayerId,
145 colors: impl IntoIterator<Item = &'a str>,
146) -> Option<String> {
147 let choices = colors.into_iter().map(str::to_string).collect::<Vec<_>>();
148 if choices.is_empty() {
149 return None;
150 }
151 ctx.agents[player.index()].choose_color(player, &choices)
152}
153
154fn choose_type(
155 ctx: &mut EffectContext,
156 player: PlayerId,
157 type_category: &str,
158 valid_types: Vec<String>,
159) -> Option<String> {
160 if valid_types.is_empty() {
161 return None;
162 }
163 ctx.agents[player.index()].choose_type(player, type_category, &valid_types)
164}
165
166fn basic_land_types() -> Vec<String> {
167 BASIC_LAND_TYPES.iter().map(|ty| ty.to_string()).collect()
168}
169
170fn creature_types() -> Vec<String> {
171 TypeRegistry::creature_types().to_vec()
172}
173
174fn forbidden_new_types(sa: &SpellAbility) -> Vec<String> {
175 sa.ir
176 .forbidden_new_types_text
177 .as_deref()
178 .unwrap_or("")
179 .split(',')
180 .map(str::trim)
181 .filter(|ty| !ty.is_empty())
182 .map(str::to_string)
183 .collect()
184}
185
186fn resolve_target_cards(ctx: &EffectContext, sa: &SpellAbility) -> Vec<CardId> {
187 let mut targets =
188 crate::ability::spell_ability_effect::get_defined_cards_or_targeted(ctx.game, sa);
189
190 if let Some(stack_id) = sa.target_chosen.target_stack_entry {
191 if let Some(source) = ctx
192 .game
193 .stack
194 .find_by_id(stack_id)
195 .and_then(|entry| entry.spell_ability.source)
196 {
197 targets.push(source);
198 }
199 }
200
201 targets.sort_unstable_by_key(|id| id.0);
202 targets.dedup();
203 targets
204}
205
206fn mark_eot_text_change(
207 game: &mut crate::game::GameState,
208 card_id: CardId,
209 active_prefix: &str,
210 marker_prefix: &str,
211 from: &str,
212) {
213 let active_key = format!("{active_prefix}{from}");
214 let marker_key = format!("{marker_prefix}{from}");
215 if game.card(card_id).svars.contains_key(&marker_key) {
216 return;
217 }
218
219 let previous = game
220 .card(card_id)
221 .svars
222 .get(&active_key)
223 .cloned()
224 .unwrap_or_else(|| NO_PRIOR_TEXT_CHANGE.to_string());
225 game.card_mut(card_id).set_s_var(marker_key, previous);
226}
227
228fn restore_text_changes(
229 game: &mut crate::game::GameState,
230 card_id: CardId,
231 active_prefix: &str,
232 marker_prefix: &str,
233) {
234 let markers = game
235 .card(card_id)
236 .svars
237 .iter()
238 .filter_map(|(key, value)| {
239 key.strip_prefix(marker_prefix)
240 .map(|from| (from.to_string(), value.clone()))
241 })
242 .collect::<Vec<_>>();
243
244 for (from, previous) in markers {
245 let active_key = format!("{active_prefix}{from}");
246 let marker_key = format!("{marker_prefix}{from}");
247 game.card_mut(card_id).remove_s_var(&marker_key);
248 if previous == NO_PRIOR_TEXT_CHANGE {
249 game.card_mut(card_id).remove_s_var(&active_key);
250 } else {
251 game.card_mut(card_id).set_s_var(active_key, previous);
252 }
253 }
254}