Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions crates/engine/src/analysis/ability_graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,7 @@ fn project_mana_production(p: &ManaProduction) -> (Vec<(usize, i64)>, AxisMagnit
(vec![(slot, a)], mag)
}
ManaProduction::ChosenColor { count, .. }
| ManaProduction::NotedType { count, .. }
| ManaProduction::OpponentLandColors { count, .. }
| ManaProduction::AnyTypeProduceableBy { count, .. }
| ManaProduction::AnyInCommandersColorIdentity { count, .. }
Expand Down Expand Up @@ -969,6 +970,7 @@ fn effect_projection(effect: &Effect) -> Projection {
| Effect::GrantCastingPermission { .. }
| Effect::ChooseFromZone { .. }
| Effect::RememberCard { .. }
| Effect::NoteManaSpent
| Effect::ForEachCategory { .. }
| Effect::ChooseObjectsIntoTrackedSet { .. }
| Effect::ChooseAndSacrificeRest { .. }
Expand Down
3 changes: 3 additions & 0 deletions crates/engine/src/game/ability_rw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3409,6 +3409,7 @@ fn legacy_effect(x: &Effect) -> bool {
| Effect::RegisterBending { .. }
| Effect::Cleanup { .. }
| Effect::Learn
| Effect::NoteManaSpent
| Effect::Forage
| Effect::Harness
| Effect::CollectEvidence { .. }
Expand Down Expand Up @@ -3743,6 +3744,7 @@ fn walk_ability(
distribution: _,
chosen_x: _,
cost_paid_object: _,
noted_mana_payment: _, // concrete captured payment snapshot, no read/write effect
cost_paid_object_ids: _,
effect_context_object: _,
amassed_army_object: _,
Expand Down Expand Up @@ -5654,6 +5656,7 @@ fn rw_effect(
| Effect::Harness
| Effect::ChooseAndSacrificeRest { .. }
| Effect::RememberCard { .. }
| Effect::NoteManaSpent
| Effect::ForEachCategory { .. }
| Effect::VentureInto { .. }
| Effect::TakeTheInitiative
Expand Down
8 changes: 8 additions & 0 deletions crates/engine/src/game/ability_scan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@
/// "unread aux field" hole class at compile time (not just `multi_target` /
/// `target_constraints`).
fn resolved_ability_axes(a: &ResolvedAbility, mode: ScanMode) -> Axes {
let ResolvedAbility {

Check failure on line 209 in crates/engine/src/game/ability_scan.rs

View workflow job for this annotation

GitHub Actions / Card data (generate, validate, coverage)

pattern does not mention field `noted_mana_payment`

Check failure on line 209 in crates/engine/src/game/ability_scan.rs

View workflow job for this annotation

GitHub Actions / WASM compile check

pattern does not mention field `noted_mana_payment`

Check failure on line 209 in crates/engine/src/game/ability_scan.rs

View workflow job for this annotation

GitHub Actions / Paired-seed AI gate

pattern does not mention field `noted_mana_payment`

Check failure on line 209 in crates/engine/src/game/ability_scan.rs

View workflow job for this annotation

GitHub Actions / Decision-cost perf gate

pattern does not mention field `noted_mana_payment`

Check failure on line 209 in crates/engine/src/game/ability_scan.rs

View workflow job for this annotation

GitHub Actions / Rust lint (fmt, clippy, parser gate)

pattern does not mention field `noted_mana_payment`

Check failure on line 209 in crates/engine/src/game/ability_scan.rs

View workflow job for this annotation

GitHub Actions / Rust tests (shard 1/2)

pattern does not mention field `noted_mana_payment`

Check failure on line 209 in crates/engine/src/game/ability_scan.rs

View workflow job for this annotation

GitHub Actions / Rust tests (shard 2/2)

pattern does not mention field `noted_mana_payment`
// ---- read-bearing: scanned into `acc` below ----
effect,
sub_ability,
Expand Down Expand Up @@ -405,7 +405,7 @@
// `SnapshotOrEvent` for their structural owner/attach/recipient selectors (single-
// player/object references, not board censuses) so a vanilla token stays read-free.
let target_ctx = effect_target_ctx(x, mode);
match x {

Check failure on line 408 in crates/engine/src/game/ability_scan.rs

View workflow job for this annotation

GitHub Actions / Card data (generate, validate, coverage)

non-exhaustive patterns: `&Effect::NoteManaSpent` not covered

Check failure on line 408 in crates/engine/src/game/ability_scan.rs

View workflow job for this annotation

GitHub Actions / WASM compile check

non-exhaustive patterns: `&Effect::NoteManaSpent` not covered

Check failure on line 408 in crates/engine/src/game/ability_scan.rs

View workflow job for this annotation

GitHub Actions / Paired-seed AI gate

non-exhaustive patterns: `&Effect::NoteManaSpent` not covered

Check failure on line 408 in crates/engine/src/game/ability_scan.rs

View workflow job for this annotation

GitHub Actions / Decision-cost perf gate

non-exhaustive patterns: `&Effect::NoteManaSpent` not covered

Check failure on line 408 in crates/engine/src/game/ability_scan.rs

View workflow job for this annotation

GitHub Actions / Rust lint (fmt, clippy, parser gate)

non-exhaustive patterns: `&types::ability::Effect::NoteManaSpent` not covered

Check failure on line 408 in crates/engine/src/game/ability_scan.rs

View workflow job for this annotation

GitHub Actions / Rust tests (shard 1/2)

non-exhaustive patterns: `&Effect::NoteManaSpent` not covered

Check failure on line 408 in crates/engine/src/game/ability_scan.rs

View workflow job for this annotation

GitHub Actions / Rust tests (shard 2/2)

non-exhaustive patterns: `&Effect::NoteManaSpent` not covered
Effect::StartYourEngines { player_scope } => {
let mut acc = Axes::NONE;
acc = acc.or(scan_player_filter(player_scope, mode));
Expand Down Expand Up @@ -4999,6 +4999,14 @@
| ManaProduction::AnyInCommandersColorIdentity { count, .. } => {
scan_quantity_expr(count, mode)
}
// `NotedManaSpent` is mutable per-object state written by a companion
// `Effect::NoteManaSpent`, so sibling activations can affect its value.
ManaProduction::NotedType { count } => Axes {
event: false,
sibling: true,
projected: false,
}
.or(scan_quantity_expr(count, mode)),
// SCOPED-OBJECT (Omnath, Locus of All): a SINGLE scoped object's colors,
// NOT a board aggregate — the scope's own read surface is the sole sibling
// source (CR 202.2c). NO own sibling literal.
Expand Down Expand Up @@ -5207,7 +5215,7 @@
if mode != ScanMode::LoopFirewall {
return FilterReadContext::SnapshotOrEvent;
}
match e {

Check failure on line 5218 in crates/engine/src/game/ability_scan.rs

View workflow job for this annotation

GitHub Actions / Card data (generate, validate, coverage)

non-exhaustive patterns: `&Effect::NoteManaSpent` not covered

Check failure on line 5218 in crates/engine/src/game/ability_scan.rs

View workflow job for this annotation

GitHub Actions / WASM compile check

non-exhaustive patterns: `&Effect::NoteManaSpent` not covered

Check failure on line 5218 in crates/engine/src/game/ability_scan.rs

View workflow job for this annotation

GitHub Actions / Paired-seed AI gate

non-exhaustive patterns: `&Effect::NoteManaSpent` not covered

Check failure on line 5218 in crates/engine/src/game/ability_scan.rs

View workflow job for this annotation

GitHub Actions / Decision-cost perf gate

non-exhaustive patterns: `&Effect::NoteManaSpent` not covered

Check failure on line 5218 in crates/engine/src/game/ability_scan.rs

View workflow job for this annotation

GitHub Actions / Rust lint (fmt, clippy, parser gate)

non-exhaustive patterns: `&types::ability::Effect::NoteManaSpent` not covered

Check failure on line 5218 in crates/engine/src/game/ability_scan.rs

View workflow job for this annotation

GitHub Actions / Rust tests (shard 1/2)

non-exhaustive patterns: `&Effect::NoteManaSpent` not covered

Check failure on line 5218 in crates/engine/src/game/ability_scan.rs

View workflow job for this annotation

GitHub Actions / Rust tests (shard 2/2)

non-exhaustive patterns: `&Effect::NoteManaSpent` not covered
// ── GENUINELY-CENSUS effects (CR 732.2a / CR 120.3): a target filter is a
// MASS POPULATION read — enumerated over EVERY matching battlefield object (an
// AllX/Each/aggregate slot, `target_filter()==None`), so its read SCALES with the
Expand Down Expand Up @@ -5922,7 +5930,7 @@
/// compiler enforces that the two lists stay in lockstep. (A2 determinism gate —
/// the static, compile-time-exhaustive half.)
pub(crate) fn effect_is_randomness_bearing(e: &Effect) -> bool {
match e {

Check failure on line 5933 in crates/engine/src/game/ability_scan.rs

View workflow job for this annotation

GitHub Actions / Card data (generate, validate, coverage)

non-exhaustive patterns: `&Effect::NoteManaSpent` not covered

Check failure on line 5933 in crates/engine/src/game/ability_scan.rs

View workflow job for this annotation

GitHub Actions / WASM compile check

non-exhaustive patterns: `&Effect::NoteManaSpent` not covered

Check failure on line 5933 in crates/engine/src/game/ability_scan.rs

View workflow job for this annotation

GitHub Actions / Paired-seed AI gate

non-exhaustive patterns: `&Effect::NoteManaSpent` not covered

Check failure on line 5933 in crates/engine/src/game/ability_scan.rs

View workflow job for this annotation

GitHub Actions / Decision-cost perf gate

non-exhaustive patterns: `&Effect::NoteManaSpent` not covered

Check failure on line 5933 in crates/engine/src/game/ability_scan.rs

View workflow job for this annotation

GitHub Actions / Rust lint (fmt, clippy, parser gate)

non-exhaustive patterns: `&types::ability::Effect::NoteManaSpent` not covered

Check failure on line 5933 in crates/engine/src/game/ability_scan.rs

View workflow job for this annotation

GitHub Actions / Rust tests (shard 1/2)

non-exhaustive patterns: `&Effect::NoteManaSpent` not covered

Check failure on line 5933 in crates/engine/src/game/ability_scan.rs

View workflow job for this annotation

GitHub Actions / Rust tests (shard 2/2)

non-exhaustive patterns: `&Effect::NoteManaSpent` not covered
// --- auto-resolved randomness (no `WaitingFor`; the recast injector cannot
// abort on these — they draw the seeded RNG and continue) ---
Effect::FlipCoin { .. }
Expand Down
18 changes: 18 additions & 0 deletions crates/engine/src/game/casting.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15577,6 +15577,24 @@ fn pay_ability_mana_cost_with_choices_excluding_and_parent(
parent,
)?;

// CR 106.1b + CR 602.2b (issue #6504): stamp the mana type(s) just spent
// onto this ability's own source, mirroring `colors_spent_to_cast`'s
// cast-side stamp-then-read idiom. This is the single authority where an
// activated ability's mana sub-cost is paid (both the direct-activation
// and interactive/PendingCast routes funnel through here). PURELY A
// BRIDGE: `push_ability_entry` drains this field synchronously into
// THIS activation's own `ResolvedAbility::noted_mana_payment` snapshot
// moments later, before any later activation of the same permanent
// could occur — see `GameObject::mana_spent_to_activate` for why a
// companion "note the type of mana spent to pay this activation cost"
// effect (Jeweled Amulet) never reads this field directly.
let spent_units = match &payment {
ManaCostPayment::Paid(units) | ManaCostPayment::Paused { value: units, .. } => units,
};
if let Some(obj) = state.objects.get_mut(&source_id) {
obj.mana_spent_to_activate = spent_units.iter().map(|unit| unit.color).collect();
}

Ok(match payment {
ManaCostPayment::Paid(_) => ManaCostPayment::Paid(()),
ManaCostPayment::Paused {
Expand Down
33 changes: 31 additions & 2 deletions crates/engine/src/game/casting_costs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ use crate::types::ability::{
is_chosen_remove_counter_cost_count, AbilityCondition, AbilityCost, AbilityDefinition,
AbilityKind, AdditionalCost, AdditionalCostInstance, AdditionalCostOrigin, AggregateFunction,
BeholdCostAction, CastTimingPermission, Comparator, CostPaidObjectSnapshot,
CounterCostSelection, Effect, KickerVariant, ObjectProperty, QuantityExpr, QuantityRef,
ReplacementDefinition, ResolvedAbility, SacrificeCost, SacrificeRequirement,
CounterCostSelection, Effect, KickerVariant, NotedManaPayment, ObjectProperty, QuantityExpr,
QuantityRef, ReplacementDefinition, ResolvedAbility, SacrificeCost, SacrificeRequirement,
SpellCastingOptionKind, SpellContext, SpellStackToGraveyardReplacement, StaticCondition,
TapCreaturesAggregate, TargetFilter, ThisWayCause, TypeFilter, TypedFilter, EXILE_COST_X,
};
Expand Down Expand Up @@ -5406,6 +5406,30 @@ pub(super) fn push_ability_entry(
// also clears any stale value.
state.announced_source_x = resolved.chosen_x.map(|x| (source_id, x));

// CR 106.1b + CR 400.7 + CR 602.2b (issue #6504): consume the source's
// transient mana-spent-to-activate latch into THIS activation's own
// `ResolvedAbility` snapshot (and every sub/else branch — `Effect::
// NoteManaSpent` is typically a `sub_ability`, which resolves as its own
// separate node, so the stamp must recurse; see
// `set_noted_mana_payment_recursive`), paired with the source's
// incarnation at this exact moment. Since `push_ability_entry` is the
// single authority where an activated ability reaches the stack, this
// capture happens synchronously, immediately after cost payment
// completed and before any later activation of the same permanent could
// occur — so a permanent untapped and reactivated while this ability
// still sits unresolved on the stack cannot corrupt what THIS instance
// observed. The latch is cleared immediately after, so it never appears
// to hold a stale value between activations.
if let Some(obj) = state.objects.get_mut(&source_id) {
if !obj.mana_spent_to_activate.is_empty() {
let payment = NotedManaPayment {
types: std::mem::take(&mut obj.mana_spent_to_activate),
source_incarnation: obj.incarnation,
};
resolved.set_noted_mana_payment_recursive(payment);
}
}

// CR 603.4: Stamp the printed-ability index for per-turn resolution tracking.
resolved.ability_index = Some(ability_index);
stack::push_to_stack(
Expand Down Expand Up @@ -11400,6 +11424,11 @@ pub(crate) fn production_override_for_option(
}
| crate::types::ability::ManaProduction::ChoiceAmongCombinations { .. }
| crate::types::ability::ManaProduction::DistinctColorsAmongPermanents { .. }
// CR 106.1b + CR 106.5: like `ChosenColor { fixed_alternative: None }`,
// the produced type is fixed by engine-set state read at production
// time (`noted_mana_type_for`), not chosen per auto-tap option — no
// override needed, and CR 106.5 governs the no-noted-type case.
| crate::types::ability::ManaProduction::NotedType { .. }
| crate::types::ability::ManaProduction::TriggerEventManaType => None,
}
}
Expand Down
4 changes: 4 additions & 0 deletions crates/engine/src/game/coverage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1976,6 +1976,9 @@ fn fmt_mana_production(mp: &ManaProduction) -> String {
ManaProduction::ChosenColor { count, .. } => {
format!("{} of chosen color", fmt_quantity(count))
}
ManaProduction::NotedType { count } => {
format!("{} of noted type", fmt_quantity(count))
}
ManaProduction::OpponentLandColors { count } => {
format!("{} of opponent land colors", fmt_quantity(count))
}
Expand Down Expand Up @@ -3692,6 +3695,7 @@ fn effect_details(effect: &Effect) -> Vec<(String, String)> {
| Effect::Forage
| Effect::Harness
| Effect::Learn
| Effect::NoteManaSpent
| Effect::SwitchPT { .. }
| Effect::Myriad
| Effect::Encore
Expand Down
1 change: 1 addition & 0 deletions crates/engine/src/game/effects/additional_phase.rs
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,7 @@ mod tests {
starting_with: None,
chosen_x: None,
cost_paid_object: None,
noted_mana_payment: None,
cost_paid_object_ids: Vec::new(),
effect_context_object: None,
amassed_army_object: None,
Expand Down
11 changes: 11 additions & 0 deletions crates/engine/src/game/effects/copy_spell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -827,9 +827,20 @@ pub(crate) fn set_resolved_source_recursive(ability: &mut ResolvedAbility, sourc
}
}

/// CR 707.10 + CR 707.10b: Normalize a copied activated/triggered ability.
/// The copy keeps the original ability's source (unlike a spell copy, which
/// sources itself), so this only re-stamps `source_id` uniformly through the
/// chain — but it must ALSO clear `noted_mana_payment` (issue #6504):
/// CR 707.10 says a copy of an activated ability is not itself activated, so
/// it never paid a mana cost. A naive struct clone otherwise carries the
/// ORIGINAL activation's payment snapshot along for the ride, and
/// `Effect::NoteManaSpent` resolving on the copy (Jeweled Amulet's first
/// ability copied via Rings of Brighthearth or similar) would falsely note
/// mana colors the copy never paid.
fn preserve_ability_copy_source_recursive(ability: &mut ResolvedAbility) {
let source_id = ability.source_id;
set_resolved_source_recursive(ability, source_id);
ability.clear_noted_mana_payment_recursive();
}

/// CR 707.10d: Replace every object target on a copied spell with `new_target`.
Expand Down
1 change: 1 addition & 0 deletions crates/engine/src/game/effects/double.rs
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,7 @@ mod tests {
starting_with: None,
chosen_x: None,
cost_paid_object: None,
noted_mana_payment: None,
cost_paid_object_ids: Vec::new(),
effect_context_object: None,
amassed_army_object: None,
Expand Down
1 change: 1 addition & 0 deletions crates/engine/src/game/effects/extra_turn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ mod tests {
starting_with: None,
chosen_x: None,
cost_paid_object: None,
noted_mana_payment: None,
cost_paid_object_ids: Vec::new(),
effect_context_object: None,
amassed_army_object: None,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ mod tests {
starting_with: None,
chosen_x: None,
cost_paid_object: None,
noted_mana_payment: None,
cost_paid_object_ids: Vec::new(),
effect_context_object: None,
amassed_army_object: None,
Expand Down
32 changes: 32 additions & 0 deletions crates/engine/src/game/effects/mana.rs
Original file line number Diff line number Diff line change
Expand Up @@ -707,6 +707,23 @@ fn resolve_mana_types_impl(
(None, None) => Vec::new(),
}
}
// CR 106.1b + CR 106.5: Jeweled Amulet — "Add one mana of this
// artifact's last noted type." Unlike `ChosenColor` (a
// player-prompted `ManaColor`), the noted value is engine-set
// (`Effect::NoteManaSpent`) and `ManaType`-valued (colorless is a
// real noted type per the card's ruling). A card in this class always
// notes exactly one type — its cost's own generic mana is spent as a
// single unit-worth of one type — so, mirroring `AnyOneColor`'s
// repeat-by-count idiom, the first noted type repeats `count` times.
// No noted type (never activated the noting ability, or a fresh
// incarnation after a zone change) produces no mana.
ManaProduction::NotedType { count } => {
let amount = resolve_count(count, state, ability, controller, source_id);
match noted_mana_type_for(state, source_id) {
Some(mana_type) => vec![mana_type; amount],
None => Vec::new(),
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
// CR 106.7: Produce mana of any color that a land an opponent controls could produce.
// Delegates to mana_sources::opponent_land_color_options for the shared computation.
ManaProduction::OpponentLandColors { count } => {
Expand Down Expand Up @@ -998,6 +1015,21 @@ pub(crate) fn chosen_color_for_mana(
})
}

/// CR 106.1b: The first mana type noted by a past `Effect::NoteManaSpent`
/// resolution on `source_id` ("this artifact's last noted type" — Jeweled
/// Amulet). Unlike `chosen_color_for_mana`, this is never player-prompted —
/// engine-set state only, with no `last_named_choice` fallback.
pub(crate) fn noted_mana_type_for(
state: &GameState,
source_id: crate::types::identifiers::ObjectId,
) -> Option<ManaType> {
state
.objects
.get(&source_id)
.and_then(|obj| obj.noted_mana_spent())
.and_then(|types| types.first().copied())
}

/// Convert a ManaColor to the runtime ManaType.
/// CR 106.1a: There are five colors of mana: white, blue, black, red, and green.
/// CR 106.1b: There are six types of mana: white, blue, black, red, green, and colorless.
Expand Down
2 changes: 2 additions & 0 deletions crates/engine/src/game/effects/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ pub mod manifest_dread;
pub mod mill;
pub mod monstrosity;
pub mod myriad;
pub mod note_mana_spent;
pub mod opponent_guess;
pub mod overload;
pub mod pair_with;
Expand Down Expand Up @@ -4410,6 +4411,7 @@ pub fn resolve_effect(
Effect::GrantCastingPermission { .. } => grant_permission::resolve(state, ability, events),
Effect::ChooseFromZone { .. } => choose_from_zone::resolve(state, ability, events),
Effect::RememberCard { .. } => remember_card::resolve(state, ability, events),
Effect::NoteManaSpent => note_mana_spent::resolve(state, ability, events),
Effect::ForEachCategory { .. } => {
choose_from_zone::resolve_for_each_category(state, ability, events)
}
Expand Down
73 changes: 73 additions & 0 deletions crates/engine/src/game/effects/note_mana_spent.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
use crate::types::ability::{ChosenAttribute, Effect, EffectError, ResolvedAbility};
use crate::types::events::GameEvent;
use crate::types::game_state::GameState;

/// CR 106.1b + CR 602.2b + CR 608.2c: `Effect::NoteManaSpent` — record the mana
/// type(s) spent to pay this resolving ability's own activation cost onto its
/// source as `ChosenAttribute::NotedManaSpent` ("Note the type of mana spent to
/// pay this activation cost" — Jeweled Amulet). Scoped to the singular-type
/// wording only: Ice Cauldron's sibling "note the type AND AMOUNT..." needs an
/// exact stored multiset plus a spend restriction, which this building block
/// does not model — that text is intentionally left unmatched by the parser
/// and still reports `Effect::unimplemented`.
///
/// Composable building block: cost payment stays in the mana-payment funnel;
/// `push_ability_entry` (the single authority where an activated ability
/// reaches the stack) snapshots what was spent, paired with the source's
/// incarnation at that moment, directly onto THIS activation's own
/// `ResolvedAbility::noted_mana_payment` (issue #6504) — never a per-object
/// mutable field, so a permanent untapped and reactivated with a different
/// payment while this ability still sits unresolved on the stack cannot
/// corrupt what this instance observed. This effect is the persistent writer,
/// read back by `ManaProduction::NotedType`. Doing the write at resolution —
/// not at payment time — means a countered or otherwise removed-from-stack
/// ability never notes anything (CR 608.2c: instructions are followed only on
/// resolution).
///
/// "The last noted type" is singular per card, so this replaces any prior
/// `ChosenAttribute::NotedManaSpent` before pushing (replace-on-rechoose).
///
/// CR 400.7: a source that leaves and returns (bounce/flicker) while this
/// SAME activation is still unresolved on the stack becomes a new object at
/// the same storage id — a new incarnation with no memory of the old
/// payment. Refuses to write unless the object's current incarnation still
/// matches `noted_mana_payment.source_incarnation`, mirroring the engine's
/// existing incarnation-pairing idiom (`ResolvedAbility::source_is_current`,
/// `TargetFilter::SelfRef` resolution).
pub fn resolve(
state: &mut GameState,
ability: &ResolvedAbility,
_events: &mut Vec<GameEvent>,
) -> Result<(), EffectError> {
let Effect::NoteManaSpent = &ability.effect else {
return Ok(());
};

let Some(payment) = ability.noted_mana_payment.as_ref() else {
// Nothing was captured at activation (e.g. the cost had no mana
// component to observe) — nothing to note.
return Ok(());
};

let Some(src) = state.objects.get(&ability.source_id) else {
// CR 608.2c: the source has left the zone it was in — nothing to note.
return Ok(());
};
if src.incarnation != payment.source_incarnation {
// CR 400.7: this activation's payment was captured on a prior
// incarnation of this object (bounced/flickered since). The CURRENT
// incarnation never paid anything itself — nothing to note.
return Ok(());
}
let spent_types = payment.types.clone();

let Some(src) = state.objects.get_mut(&ability.source_id) else {
return Ok(());
};
src.chosen_attributes
.retain(|a| !matches!(a, ChosenAttribute::NotedManaSpent(_)));
src.chosen_attributes
.push(ChosenAttribute::NotedManaSpent(spent_types));

Ok(())
}
2 changes: 2 additions & 0 deletions crates/engine/src/game/effects/player_counter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,7 @@ mod tests {
starting_with: None,
chosen_x: None,
cost_paid_object: None,
noted_mana_payment: None,
cost_paid_object_ids: Vec::new(),
effect_context_object: None,
amassed_army_object: None,
Expand Down Expand Up @@ -550,6 +551,7 @@ mod tests {
starting_with: None,
chosen_x: None,
cost_paid_object: None,
noted_mana_payment: None,
cost_paid_object_ids: Vec::new(),
effect_context_object: None,
amassed_army_object: None,
Expand Down
1 change: 1 addition & 0 deletions crates/engine/src/game/effects/reverse_turn_order.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ mod tests {
starting_with: None,
chosen_x: None,
cost_paid_object: None,
noted_mana_payment: None,
cost_paid_object_ids: Vec::new(),
effect_context_object: None,
amassed_army_object: None,
Expand Down
1 change: 1 addition & 0 deletions crates/engine/src/game/effects/skip_next_step.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ mod tests {
starting_with: None,
chosen_x: None,
cost_paid_object: None,
noted_mana_payment: None,
cost_paid_object_ids: Vec::new(),
effect_context_object: None,
amassed_army_object: None,
Expand Down
1 change: 1 addition & 0 deletions crates/engine/src/game/effects/skip_next_turn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ mod tests {
starting_with: None,
chosen_x: None,
cost_paid_object: None,
noted_mana_payment: None,
cost_paid_object_ids: Vec::new(),
effect_context_object: None,
amassed_army_object: None,
Expand Down
Loading
Loading