Skip to content

ship/t104 special action x#5788

Merged
matthewevans merged 2 commits into
mainfrom
ship/t104-special-action-x
Jul 14, 2026
Merged

ship/t104 special action x#5788
matthewevans merged 2 commits into
mainfrom
ship/t104-special-action-x

Conversation

@matthewevans

Copy link
Copy Markdown
Member
  • refactor(engine): rename activated_ability_x -> announced_source_x (CR 107.3a + CR 107.3d)
  • fix(engine): announce, pay, and carry X for the turn-face-up special action (CR 107.3d + CR 702.37f)

…R 107.3a + CR 107.3d)

The source-keyed announced-X carrier is about to gain its SECOND announce surface: a
turn-face-up SPECIAL ACTION (CR 116.2b), whose X is fixed by CR 107.3d rather than by
CR 107.3a. The old name would then be a lie -- the field carries "the X announced for an
in-flight cost of this source", not specifically an activated ability's.

Behaviour-free on its own: the activated-ability publication and the trigger-instantiation
consumer are unchanged. Doc widened to name both CR announce surfaces.

serde: #[serde(alias = "activated_ability_x")]. GameState sets no deny_unknown_fields, so
WITHOUT the alias a mid-activation save written by a pre-rename binary would be accepted
and its live X SILENTLY DROPPED -- the worst failure mode, since it looks like a clean
load. The save-compat test gains a third direction (a pre-rename key fixture) that pins
this; deleting the alias makes it fail with `left: None`, watched.
…action (CR 107.3d + CR 702.37f)

Turning a face-down permanent face up is a SPECIAL ACTION (CR 116.2b): it uses no stack and
never passes through push_ability_entry, so neither existing X channel could reach it by
construction. Three faces in the pool take their X from that action -- Warbreak Trumpeter
(Morph {X}{X}{R}), Bane of the Living (Morph {X}{B}{B}) and Aurelia's Vindicator (Disguise
{X}{3}{W}) -- and all three were LYING GREENS: zero Unimplemented, 100% "supported", and
every one resolving X to 0.

MEASURED on pristine main, through the real GameAction::TurnFaceUp path: flipping Warbreak
Trumpeter cost 1 mana and created 0 goblins. TWO defects, not one:

(A) THE COST. GameAction::TurnFaceUp carried no X at all, so the player never chose one and
    the raw {X}{X}{R} reached mana payment with its ManaCostShard::X shards unconcretized --
    where they are not payable and were dropped outright. The permanent flipped for its
    non-X remainder ({R}) alone. Fixing (B) without this would be strictly WORSE than the
    status quo: pay {R}, collect X goblins.

(B) THE CARRIER. Nothing recorded the announced X, so the turn-face-up trigger had nothing
    to bind. (cost_x_paid is the CR 107.3m *cast* channel and reads 0 here -- a face-down
    permanent was cast for {3}, CR 702.37c.)

CR 107.3d: X is "chosen by the player taking the special action immediately before they pay
that cost" -- with no priority window between the choice and the payment. So X rides on the
action (#[serde(default)]; an x-less client announces X=0, itself a legal choice) rather
than on a WaitingFor round-trip, which would model a pause the rules do not have.

CR 702.37f (morph) / CR 702.168e (disguise) then bind it: "other abilities of that permanent
may also refer to X ... equal to the value of X chosen as the morph special action was
taken." The three faces spend X in three DIFFERENT slots -- Token.count, PtValue::Variable
("-X"), and multi_target.max -- but all three read ResolvedAbility::chosen_x, so publishing
to the existing source-keyed carrier binds every one of them through the existing consumer
(triggers::build_triggered_ability). No new carrier, no new enum variant, no parser change,
no read-path change.

The stamp must land at trigger INSTANTIATION, not resolution: Aurelia's Vindicator spends X
in multi_target.max ("exile up to X other target creatures"), consumed during TARGET
SELECTION -- a resolution-time stamp would arrive too late.

Scoping: published ONLY when the cost actually has an {X} (CR 107.3d grants a choice only
then). A no-X flip leaves the carrier untouched rather than clobbering it with Some((..,0)):
an unrelated activated ability of ANOTHER object may have its own announced X in flight.

Witnesses (crates/engine/tests/integration/special_action_x_runtime.rs, all watched red ->
green, cards synthesized from VERBATIM Oracle text):
  * Warbreak Trumpeter X=3 -> 3 Goblins (effect slot)
  * Bane of the Living X=2 -> all creatures -2/-2 (negated PtValue slot)
  * Aurelia's Vindicator X=2 -> 2 creatures exiled (target-count slot)
  * payment: {X}{X}{R} at X=3 charges 2*3 + {R} = 7 mana, not 1
  * X=0 announces a REAL zero (chosen_x == Some(0) on the stack, not None) and costs {R}
  * unpayable X and a non-zero X on a no-X cost are both REJECTED
  * CONTROL Hooded Hydra (Morph {3}{G}{G}, no X): still flips, still gets its five +1/+1
    counters (non-vacuity), and publishes NOTHING

Reds watched independently: disabling the publication fails exactly the 4 carrier witnesses
while all 4 controls stay green; disabling concretize_x fails exactly the payment witness.
#96/#97 pins (11/11) and #99 pins (5/5) stay green.

NOTE (filed separately): the turn-face-up special action is not enumerated by
ai_support::legal_actions and is dispatched nowhere in the client, so it is currently
reachable only from engine tests. That reachability gap is pre-existing and independent of X.
@matthewevans
matthewevans enabled auto-merge July 14, 2026 05:56
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@matthewevans
matthewevans added this pull request to the merge queue Jul 14, 2026
@github-actions

Copy link
Copy Markdown

Parse changes introduced by this PR

✓ No card-parse changes detected.

Merged via the queue into main with commit efc76ca Jul 14, 2026
13 checks passed
@matthewevans
matthewevans deleted the ship/t104-special-action-x branch July 14, 2026 06:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant