1use std::collections::HashMap;
8
9use forge_foundation::{CardTypeLine, ColorSet, ManaCost, ZoneType};
10
11use super::{emit_zone_trigger, EffectContext};
12use crate::agent::types::GameEntity;
13use crate::card::card_zone_table::CardZoneTable;
14use crate::card::Card;
15use crate::event::RunParams;
16use crate::ids::{CardId, PlayerId};
17use crate::parsing::{keys, split_param_list_value};
18use crate::replacement::replacement_handler::{apply_replacements_with_agents, ReplacementEvent};
19use crate::replacement::replacement_result::ReplacementResult;
20use crate::spellability::SpellAbility;
21use crate::trigger::TriggerType;
22
23#[derive(Clone)]
24pub struct TokenTableCell {
25 pub owner: PlayerId,
26 pub prototype: Card,
27 pub amount: usize,
28}
29
30#[derive(Clone, Default)]
31pub struct TokenCreateTable {
32 cells: Vec<TokenTableCell>,
33}
34
35impl TokenCreateTable {
36 pub fn put(&mut self, owner: PlayerId, prototype: Card, amount: usize) {
37 self.cells.push(TokenTableCell {
38 owner,
39 prototype,
40 amount,
41 });
42 }
43
44 pub fn cells(&self) -> &[TokenTableCell] {
45 &self.cells
46 }
47
48 pub fn retain_players(&mut self, keep: impl Fn(PlayerId) -> bool) {
49 self.cells.retain(|cell| keep(cell.owner));
50 }
51
52 pub fn is_empty(&self) -> bool {
53 self.cells.is_empty()
54 }
55}
56
57#[derive(Debug, Clone, Default)]
58pub struct TokenCreateResult {
59 pub created: Vec<CardId>,
60 pub combat_changed: bool,
61}
62
63#[derive(Clone, Copy)]
64pub enum TokenAttachmentTarget {
65 Card(CardId),
66 Player(PlayerId),
67}
68
69pub trait TokenEffectBase {
70 fn token_scripts(&self, sa: &SpellAbility) -> Vec<String> {
71 split_param_list_value(sa.ir.token_script.as_deref(), ",")
72 }
73
74 fn get_token_template<'a>(
75 &self,
76 templates: &'a HashMap<String, Card>,
77 script: &str,
78 ) -> Option<&'a Card> {
79 templates.get(script).or_else(|| {
80 let lower = script.to_ascii_lowercase();
81 templates
82 .iter()
83 .find(|(key, _)| key.to_ascii_lowercase() == lower)
84 .map(|(_, value)| value)
85 })
86 }
87
88 fn require_token_template(&self, templates: &HashMap<String, Card>, script: &str) -> Card {
89 self.get_token_template(templates, script)
90 .cloned()
91 .unwrap_or_else(|| panic!("don't find Token for TokenScript: {script}"))
92 }
93
94 fn create_token_table(
95 &self,
96 ctx: &EffectContext,
97 players: &[PlayerId],
98 token_scripts: &[String],
99 final_amount: usize,
100 sa: &SpellAbility,
101 ) -> TokenCreateTable {
102 let mut token_table = TokenCreateTable::default();
103 for &owner in players {
104 if !ctx.game.player(owner).is_alive() {
105 continue;
106 }
107 for script in token_scripts {
108 let mut result = self.require_token_template(ctx.token_templates, script);
109 result.set_owner(owner);
110 result.set_controller(owner);
111 result.set_is_token(true);
112 result.set_s_var("TokenScript", script);
113 result.set_s_var("TokenSpawningAbility", sa.ability_text.clone());
114 token_table.put(owner, result, final_amount);
115 }
116 }
117 token_table
118 }
119
120 fn make_token_table_internal_from_script(
121 &self,
122 ctx: &EffectContext,
123 owner: PlayerId,
124 script: &str,
125 final_amount: usize,
126 sa: &SpellAbility,
127 ) -> TokenCreateTable {
128 let mut result = self.require_token_template(ctx.token_templates, script);
129 result.set_owner(owner);
130 result.set_controller(owner);
131 result.set_is_token(true);
132 result.set_s_var("TokenScript", script);
133 result.set_s_var("TokenSpawningAbility", sa.ability_text.clone());
134 self.make_token_table_internal(owner, result, final_amount)
135 }
136
137 fn make_token_table_internal(
138 &self,
139 owner: PlayerId,
140 result: Card,
141 final_amount: usize,
142 ) -> TokenCreateTable {
143 let mut token_table = TokenCreateTable::default();
144 token_table.put(owner, result, final_amount);
145 token_table
146 }
147
148 fn has_inline_token_params(&self, sa: &SpellAbility) -> bool {
149 sa.ir.token_power.is_some()
150 || sa.ir.token_toughness.is_some()
151 || sa.ir.token_types_text.is_some()
152 || sa.ir.token_name_text.is_some()
153 }
154
155 fn build_inline_token(&self, sa: &SpellAbility, owner: PlayerId) -> Card {
156 let name = sa
157 .ir
158 .token_name_text
159 .clone()
160 .unwrap_or_else(|| "Token".to_string());
161 let power = sa.ir.token_power;
162 let toughness = sa.ir.token_toughness;
163 let type_line = sa
164 .ir
165 .token_types_text
166 .as_deref()
167 .map(CardTypeLine::parse)
168 .unwrap_or_else(|| CardTypeLine::parse("Creature"));
169 let colors = sa
170 .ir
171 .token_colors_text
172 .as_deref()
173 .map(|s| {
174 if s.eq_ignore_ascii_case("Colorless") {
175 ColorSet::COLORLESS
176 } else {
177 ColorSet::from_names(s)
178 }
179 })
180 .unwrap_or(ColorSet::COLORLESS);
181 let keywords = split_param_list_value(sa.ir.token_keywords_text.as_deref(), "&");
182
183 Card::new(
184 CardId(0),
185 name,
186 owner,
187 type_line,
188 ManaCost::parse(""),
189 colors,
190 power,
191 toughness,
192 keywords,
193 vec![],
194 )
195 }
196
197 fn make_token_table_from_scripts(
198 &self,
199 ctx: &mut EffectContext,
200 players: &[PlayerId],
201 token_scripts: &[String],
202 final_amount: usize,
203 clone_origin: bool,
204 trigger_list: &mut CardZoneTable,
205 sa: &SpellAbility,
206 ) -> TokenCreateResult {
207 let token_table = self.create_token_table(ctx, players, token_scripts, final_amount, sa);
208 self.make_token_table(ctx, token_table, clone_origin, trigger_list, sa)
209 }
210
211 fn make_token_table(
212 &self,
213 ctx: &mut EffectContext,
214 mut token_table: TokenCreateTable,
215 clone_origin: bool,
216 trigger_list: &mut CardZoneTable,
217 sa: &SpellAbility,
218 ) -> TokenCreateResult {
219 if token_table.is_empty() {
220 return TokenCreateResult::default();
221 }
222
223 self.apply_create_token_replacements(ctx, &mut token_table);
224
225 let original_tokens: Vec<Card> = token_table
226 .cells()
227 .iter()
228 .map(|cell| cell.prototype.clone())
229 .collect();
230 let pump_keywords = self.pump_keywords(sa);
231 let mut result = TokenCreateResult::default();
232
233 for cell in token_table.cells().iter().cloned() {
234 let script = cell.prototype.get_s_var("TokenScript").map(str::to_owned);
235 if let Some(script) = script.as_deref() {
236 ctx.sync_token_art_rng(script, sa);
237 }
238
239 let controller = cell.prototype.controller;
240 for _ in 0..cell.amount {
241 let Some(token_id) = self.create_single_token(
242 ctx,
243 sa,
244 &cell.prototype,
245 cell.owner,
246 controller,
247 clone_origin,
248 &pump_keywords,
249 trigger_list,
250 &original_tokens,
251 ) else {
252 continue;
255 };
256 result.created.push(token_id);
257 if self.add_token_to_combat(ctx, sa, token_id) {
258 result.combat_changed = true;
259 }
260 }
261 }
262
263 if let Some(action) = sa.ir.at_eot.as_deref() {
264 crate::ability::spell_ability_effect::register_at_eot(
265 ctx.trigger_handler,
266 ctx.game,
267 sa,
268 action,
269 result.created.clone(),
270 );
271 }
272
273 result
274 }
275
276 fn create_single_token(
277 &self,
278 ctx: &mut EffectContext,
279 sa: &SpellAbility,
280 prototype: &Card,
281 creator: PlayerId,
282 controller: PlayerId,
283 clone_origin: bool,
284 pump_keywords: &[String],
285 trigger_list: &mut CardZoneTable,
286 original_tokens: &[Card],
287 ) -> Option<CardId> {
288 let attachment_before_move = if sa.ir.attach_after_text.is_none() {
289 self.attachment_target(ctx, sa)
290 } else {
291 None
292 };
293 if sa.ir.attached_to.is_some()
294 && attachment_before_move.is_none()
295 && prototype.type_line.has_subtype("Aura")
296 {
297 return None;
298 }
299
300 let mut token = prototype.clone();
301 token.set_owner(creator);
302 token.set_controller(controller);
303 token.set_is_token(true);
304
305 if let Some(counter_type) = sa.ir.with_counters_type.as_ref() {
306 let amount = super::resolve_numeric_svar(ctx.game, sa, keys::WITH_COUNTERS_AMOUNT, 1);
307 token.add_counter(counter_type, amount.max(0));
308 }
309
310 if let Some(add_triggers_from) = sa.ir.add_triggers_from_text.as_deref() {
311 if let Some(source_id) = sa.source {
312 let cards = crate::ability::ability_utils::get_defined_cards(
313 ctx.game,
314 Some(source_id),
315 add_triggers_from,
316 Some(sa.activating_player),
317 );
318 for card_id in cards {
319 for trigger in ctx.game.card(card_id).copiable_triggers() {
320 token.add_trigger(trigger);
321 }
322 }
323 }
324 }
325
326 let token_id = ctx.game.create_card(token);
327 self.after_token_created(ctx, token_id);
328 if let Some(attachment) = attachment_before_move {
329 if !self.attach_token_to(ctx, token_id, attachment)
330 && ctx.game.card(token_id).type_line.has_subtype("Aura")
331 {
332 return None;
333 }
334 }
335
336 ctx.move_card(token_id, ZoneType::Battlefield, controller);
337
338 if sa.ir.token_tapped {
339 ctx.game.tap(token_id);
340 }
341
342 if clone_origin {
343 if let Some(source_id) = sa.source {
344 ctx.game.card_mut(token_id).clone_origin = Some(source_id);
345 }
346 }
347 if clone_origin || prototype.copied_permanent.is_some() {
348 ctx.game.card_mut(token_id).copied_permanent =
349 prototype.copied_permanent.or_else(|| {
350 if prototype.id != CardId(0) {
351 Some(prototype.id)
352 } else {
353 None
354 }
355 });
356 }
357
358 if !pump_keywords.is_empty() {
359 for keyword in pump_keywords {
360 ctx.game.card_mut(token_id).add_pump_keyword(keyword);
361 }
362 self.add_pump_until(ctx, sa, token_id);
363 }
364
365 if let Some(location) = sa.ir.at_eot_trig_text.as_deref() {
366 crate::ability::spell_ability_effect::add_self_trigger_at_eot(
367 ctx.trigger_handler,
368 ctx.game,
369 location,
370 token_id,
371 );
372 }
373
374 if sa.ir.remember_tokens {
375 if let Some(source_id) = sa.source {
376 ctx.game.card_mut(source_id).add_remembered_card(token_id);
377 }
378 }
379 if sa.ir.remember_original_tokens
380 && original_tokens.iter().any(|original| {
381 original.card_name == prototype.card_name
382 && original.type_line == prototype.type_line
383 })
384 {
385 if let Some(source_id) = sa.source {
386 ctx.game.card_mut(source_id).add_remembered_card(token_id);
387 }
388 }
389 if sa.ir.imprint_tokens {
390 if let Some(source_id) = sa.source {
391 ctx.game.card_mut(source_id).add_imprinted_card(token_id);
392 }
393 }
394 if sa.ir.remember_source {
395 if let Some(source_id) = sa.source {
396 ctx.game.card_mut(token_id).add_remembered_card(source_id);
397 }
398 }
399 if let Some(defined) = sa.ir.token_remembered.as_deref() {
400 if let Some(source_id) = sa.source {
401 let remembered_cards = crate::ability::ability_utils::get_defined_cards(
402 ctx.game,
403 Some(source_id),
404 defined,
405 Some(sa.activating_player),
406 );
407 ctx.game
408 .card_mut(token_id)
409 .add_remembered_cards(remembered_cards);
410 }
411 }
412 if sa.ir.cleanup_for_each {
413 let remembered = prototype.remembered_cards.clone();
414 for card_id in remembered {
415 ctx.game.card_mut(token_id).remove_remembered(card_id);
416 }
417 }
418
419 if sa.ir.attach_after_text.is_some() {
420 if let Some(attachment) = self.attachment_target(ctx, sa) {
421 let _ = self.attach_token_to(ctx, token_id, attachment);
422 }
423 }
424
425 ctx.trigger_handler
426 .register_active_trigger(ctx.game, token_id);
427 ctx.trigger_handler.run_trigger(
428 TriggerType::TokenCreated,
429 RunParams {
430 card: Some(token_id),
431 player: Some(creator),
432 ..Default::default()
433 },
434 false,
435 );
436 emit_zone_trigger(
437 ctx.trigger_handler,
438 token_id,
439 ZoneType::None,
440 ZoneType::Battlefield,
441 );
442
443 let token_lki = crate::card::card_copy_service::get_lki_copy(ctx.game.card(token_id));
444 trigger_list.put(Some(ZoneType::None), Some(ZoneType::Battlefield), token_id);
445 let first_time = ctx.game.player(creator).tokens_created_this_turn == 0;
446 let _ = token_lki;
449 trigger_list.add_token(token_id, creator, first_time);
450 crate::player::add_tokens_created_this_turn(ctx.game, creator, 1);
451
452 Some(token_id)
453 }
454
455 fn after_token_created(&self, ctx: &mut EffectContext, token_id: CardId) {
456 let token = ctx.game.card_mut(token_id);
457 token.update_spell_abilities();
458
459 let bound_host = ctx.game.card(token_id).clone();
460 let token = ctx.game.card_mut(token_id);
461 for trigger in &mut token.triggers {
462 trigger.bind_host_card_id(bound_host.id);
463 }
464 for static_ability in &mut token.static_abilities {
465 static_ability.base.set_host_card_id(bound_host.id);
466 }
467 for replacement_effect in &mut token.replacement_effects {
468 replacement_effect.base.set_host_card_id(bound_host.id);
469 }
470 }
471
472 fn attachment_target(
473 &self,
474 ctx: &mut EffectContext,
475 sa: &SpellAbility,
476 ) -> Option<TokenAttachmentTarget> {
477 let attached_to = sa.ir.attached_to.as_deref()?;
478 let (players, cards) =
479 crate::ability::ability_utils::get_defined_entities(attached_to, sa, ctx.game);
480 let mut entities = Vec::with_capacity(players.len() + cards.len());
481 entities.extend(players.into_iter().map(GameEntity::Player));
482 entities.extend(
483 cards
484 .into_iter()
485 .filter(|&card_id| ctx.game.card(card_id).zone == ZoneType::Battlefield)
486 .map(GameEntity::Card),
487 );
488 if entities.is_empty() {
489 return None;
490 }
491
492 ctx.agents[sa.activating_player.index()].snapshot_state(ctx.game, ctx.mana_pools);
493 let chosen = ctx.agents[sa.activating_player.index()].choose_single_entity_for_effect(
494 sa.activating_player,
495 &entities,
496 false,
497 )?;
498 match chosen {
499 GameEntity::Card(card_id) => Some(TokenAttachmentTarget::Card(card_id)),
500 GameEntity::Player(player_id) => Some(TokenAttachmentTarget::Player(player_id)),
501 }
502 }
503
504 fn attach_token_to(
505 &self,
506 ctx: &mut EffectContext,
507 token_id: CardId,
508 target: TokenAttachmentTarget,
509 ) -> bool {
510 match target {
511 TokenAttachmentTarget::Card(target_id) => {
512 if ctx.game.card(target_id).zone != ZoneType::Battlefield {
513 return false;
514 }
515 if crate::staticability::static_ability_cant_attach::cant_attach(
516 &ctx.game.cards,
517 ctx.game.card(token_id),
518 ctx.game.card(target_id),
519 false,
520 ) {
521 return false;
522 }
523 ctx.game.attach_to(token_id, target_id);
524 true
525 }
526 TokenAttachmentTarget::Player(player_id) => {
527 if !ctx.game.player(player_id).is_alive() {
528 return false;
529 }
530 ctx.game.attach_to_player(token_id, player_id);
531 true
532 }
533 }
534 }
535
536 fn add_token_to_combat(
537 &self,
538 ctx: &mut EffectContext,
539 sa: &SpellAbility,
540 token_id: CardId,
541 ) -> bool {
542 if super::add_to_combat(ctx, sa, token_id, keys::TOKEN_ATTACKING) {
543 return true;
544 }
545 self.add_token_blocking(ctx, sa, token_id)
546 }
547
548 fn add_token_blocking(
549 &self,
550 ctx: &mut EffectContext,
551 sa: &SpellAbility,
552 token_id: CardId,
553 ) -> bool {
554 if !ctx.game.turn.is_combat() || !ctx.game.card(token_id).is_creature() {
555 return false;
556 }
557 let Some(blocking) = sa.ir.token_blocking_text.as_deref() else {
558 return false;
559 };
560 let attackers = crate::ability::ability_utils::get_defined_cards(
561 ctx.game,
562 sa.source,
563 blocking,
564 Some(sa.activating_player),
565 );
566 let Some(combat) = ctx.combat.as_deref_mut() else {
567 return false;
568 };
569 let Some(attacker_id) = attackers
570 .into_iter()
571 .find(|&attacker_id| combat.is_attacking(attacker_id))
572 else {
573 return false;
574 };
575 if !crate::combat::combat_util::can_creature_block(ctx.game, token_id, attacker_id) {
576 return false;
577 }
578 if combat
579 .blockers
580 .iter()
581 .any(|&(blocker, attacker)| blocker == token_id && attacker == attacker_id)
582 {
583 return false;
584 }
585 combat.declare_blocker(
586 token_id,
587 attacker_id,
588 ctx.game.card(token_id).zone_timestamp,
589 );
590 true
591 }
592
593 fn pump_keywords(&self, sa: &SpellAbility) -> Vec<String> {
594 split_param_list_value(sa.ir.pump_keywords.as_deref(), " & ")
595 }
596
597 fn add_pump_until(&self, ctx: &mut EffectContext, sa: &SpellAbility, token_id: CardId) {
598 if let Some(duration) = sa.ir.pump_duration_text.as_deref() {
599 ctx.game
600 .card_mut(token_id)
601 .set_s_var("PumpDuration", duration);
602 }
603 }
604
605 fn apply_create_token_replacements(
606 &self,
607 ctx: &mut EffectContext,
608 token_table: &mut TokenCreateTable,
609 ) {
610 let mut retained = Vec::with_capacity(token_table.cells.len());
611 for mut cell in token_table.cells.drain(..) {
612 let mut event = ReplacementEvent::CreateToken {
613 player: cell.owner,
614 count: cell.amount as i32,
615 is_effect: true,
616 };
617 match apply_replacements_with_agents(&mut *ctx.game, ctx.agents, &mut event) {
618 ReplacementResult::NotReplaced => retained.push(cell),
619 ReplacementResult::Updated => {
620 if let ReplacementEvent::CreateToken { count, .. } = event {
621 cell.amount = count.max(0) as usize;
622 }
623 retained.push(cell);
624 }
625 ReplacementResult::Replaced
626 | ReplacementResult::Prevented
627 | ReplacementResult::Skipped => {}
628 }
629 }
630 token_table.cells = retained;
631 }
632}
633
634pub struct TokenEffectBaseImpl;
635
636impl TokenEffectBase for TokenEffectBaseImpl {}
637
638pub const TOKEN_EFFECT_BASE: TokenEffectBaseImpl = TokenEffectBaseImpl;