Skip to main content

manabrew_engine/ability/effects/
amass_effect.rs

1//! Amass effect — create or grow an Army token.
2//!
3//! Ported from Java's `AmassEffect.java`.
4//!
5//! Amass N {Type}: Put N +1/+1 counters on an Army you control.
6//! If you don't control one, create a 0/0 black {Type} Army creature token first.
7//! If the Army isn't a {Type}, it becomes a {Type} in addition to its other types.
8
9use crate::parsing::keys;
10use forge_foundation::{CardTypeLine, ColorSet, ManaCost, ZoneType};
11
12use super::token_effect_base::{TokenEffectBase, TOKEN_EFFECT_BASE};
13use super::{parse_counter_type, EffectContext};
14use crate::card::card_zone_table::CardZoneTable;
15use crate::card::Card;
16use crate::ids::CardId;
17use crate::spellability::SpellAbility;
18use crate::staticability::parse_static_ability;
19
20/// Struct form of this effect so it can participate in the
21/// `SpellAbilityEffect` trait hierarchy — mirrors Java's
22/// `AmassEffect` class extending `SpellAbilityEffect`.
23#[manabrew_engine_macros::spell_effect(AmassEffect)]
24fn resolve(ctx: &mut EffectContext, sa: &crate::spellability::SpellAbility) {
25    let controller = sa.activating_player;
26    let amount = super::resolve_numeric_svar(ctx.game, sa, "Num", 1).max(0);
27    let amass_type = sa.ir.type_filter.as_deref().unwrap_or("Zombie");
28
29    // Step 1: If no Army on battlefield, create one
30    let has_army = ctx.game.cards.iter().any(|c| {
31        c.zone == ZoneType::Battlefield
32            && c.controller == controller
33            && c.type_line
34                .subtypes
35                .iter()
36                .any(|s| s.eq_ignore_ascii_case("Army"))
37    });
38
39    if !has_army {
40        create_army_token(ctx, sa, controller, amass_type);
41    }
42
43    // Step 2: Find all Armies
44    let armies: Vec<CardId> = ctx
45        .game
46        .cards
47        .iter()
48        .filter(|c| {
49            c.zone == ZoneType::Battlefield
50                && c.controller == controller
51                && c.type_line
52                    .subtypes
53                    .iter()
54                    .any(|s| s.eq_ignore_ascii_case("Army"))
55        })
56        .map(|c| c.id)
57        .collect();
58
59    if armies.is_empty() {
60        return;
61    }
62
63    // Step 3: Choose an Army (auto-select if only one)
64    let target = if armies.len() == 1 {
65        armies[0]
66    } else {
67        ctx.agents[controller.index()].snapshot_state(ctx.game, ctx.mana_pools);
68        ctx.agents[controller.index()]
69            .choose_single_card_for_zone_change(
70                ctx.game,
71                controller,
72                &armies,
73                "Choose an Army",
74                false,
75            )
76            .unwrap_or(armies[0])
77    };
78
79    // RememberAmass$
80    if sa.param_is_true(keys::REMEMBER_AMASS) {
81        if let Some(source_id) = sa.source {
82            ctx.game.card_mut(source_id).add_remembered_card(target);
83        }
84    }
85
86    // Step 4: Add +1/+1 counters
87    let counter_type = parse_counter_type("P1P1");
88    ctx.add_counter(
89        target,
90        &counter_type,
91        amount,
92        sa,
93        crate::event::RunParams::default(),
94    );
95
96    // Step 5: If Army doesn't have the amass type, add it via effect
97    let has_type = ctx
98        .game
99        .card(target)
100        .type_line
101        .subtypes
102        .iter()
103        .any(|s| s.eq_ignore_ascii_case(amass_type));
104
105    if !has_type {
106        add_type_effect(ctx, sa, controller, target, amass_type);
107    }
108}
109
110/// Create a 0/0 black {Type} Army creature token.
111fn create_army_token(
112    ctx: &mut EffectContext,
113    _sa: &SpellAbility,
114    controller: crate::ids::PlayerId,
115    amass_type: &str,
116) {
117    let army_script = format!("b_0_0_{}_army", amass_type.to_lowercase());
118    let mut token = TOKEN_EFFECT_BASE.require_token_template(ctx.token_templates, &army_script);
119    token.set_owner(controller);
120    token.set_controller(controller);
121    token.set_is_token(true);
122    token.set_s_var("TokenScript", army_script);
123    token.set_s_var("TokenSpawningAbility", _sa.ability_text.clone());
124    token.card_name = format!("{} Army Token", amass_type);
125    token.type_line = CardTypeLine::parse(&format!("Creature - {} Army", amass_type));
126
127    let token_table = TOKEN_EFFECT_BASE.make_token_table_internal(controller, token, 1);
128    let mut trigger_list = CardZoneTable::default();
129    let result =
130        TOKEN_EFFECT_BASE.make_token_table(ctx, token_table, false, &mut trigger_list, _sa);
131    if !result.created.is_empty() {
132        trigger_list.trigger_changes_zone_all(ctx.trigger_handler, ctx.game, Some(_sa));
133    }
134}
135
136/// Add a creature type to the Army via a command-zone continuous effect.
137/// Mirrors Java's createEffect + AddType$ static ability (lines 101-110).
138fn add_type_effect(
139    ctx: &mut EffectContext,
140    sa: &SpellAbility,
141    controller: crate::ids::PlayerId,
142    target: CardId,
143    amass_type: &str,
144) {
145    let mut effect = Card::new(
146        CardId(0),
147        "Amass Effect".to_string(),
148        controller,
149        CardTypeLine::parse("Effect"),
150        ManaCost::parse("0"),
151        ColorSet::COLORLESS,
152        None,
153        None,
154        vec![],
155        vec![],
156    );
157    effect.set_controller(controller);
158    effect.set_effect_source(sa.source);
159    effect.add_remembered_card(target);
160    effect.set_temp_effect_host(Some(target)); // Removed when target leaves play
161
162    let static_text = format!(
163        "Mode$ Continuous | Affected$ Card.IsRemembered | EffectZone$ Command | AddType$ {}",
164        amass_type
165    );
166    if let Some(parsed) = parse_static_ability(&format!("S$ {}", static_text)) {
167        effect.add_static_ability(parsed);
168    }
169
170    let effect_id = ctx.game.create_card(effect);
171    ctx.move_card(effect_id, ZoneType::Command, controller);
172}