From 310ad92d1798e8971872d6d54061f350ac2841af Mon Sep 17 00:00:00 2001 From: matthewevans Date: Wed, 29 Jul 2026 20:07:18 -0700 Subject: [PATCH 1/7] feat: prompt for sacrificial mana sources --- .../manaSourceSelectionWireTypes.test.ts | 1 + .../adapter/generated/interaction/index.ts | 2 +- client/src/adapter/types.ts | 14 +- .../board/__tests__/PermanentCard.test.tsx | 1 + client/src/components/mana/ManaPaymentUI.tsx | 63 ++++ .../mana/__tests__/ManaPaymentUI.test.tsx | 63 +++- .../components/settings/PreferencesModal.tsx | 21 +- .../game/__tests__/castPaymentMode.test.ts | 31 ++ client/src/game/castPaymentMode.ts | 31 +- client/src/game/controllers/aiController.ts | 11 +- client/src/game/waitingForRegistry.ts | 2 + .../__tests__/useKeyboardShortcuts.test.tsx | 1 + client/src/i18n/locales/de/game.json | 1 + client/src/i18n/locales/de/settings.json | 1 + client/src/i18n/locales/en/game.json | 7 + client/src/i18n/locales/en/settings.json | 5 + client/src/i18n/locales/es/game.json | 1 + client/src/i18n/locales/es/settings.json | 1 + client/src/i18n/locales/fr/game.json | 1 + client/src/i18n/locales/fr/settings.json | 1 + client/src/i18n/locales/it/game.json | 1 + client/src/i18n/locales/it/settings.json | 1 + client/src/i18n/locales/pl/game.json | 1 + client/src/i18n/locales/pl/settings.json | 1 + client/src/i18n/locales/pt/game.json | 1 + client/src/i18n/locales/pt/settings.json | 1 + client/src/network/__tests__/protocol.test.ts | 2 +- client/src/network/protocol.ts | 3 +- client/src/pages/GamePage.tsx | 4 +- client/src/stores/preferencesStore.ts | 18 +- .../__tests__/cardActionChoice.test.ts | 1 + crates/engine/src/ai_support/candidates.rs | 26 +- crates/engine/src/ai_support/filter.rs | 2 +- crates/engine/src/ai_support/mod.rs | 37 ++ .../src/ai_support/payment_continuation.rs | 4 +- crates/engine/src/game/casting.rs | 1 + crates/engine/src/game/casting_costs.rs | 94 ++++- crates/engine/src/game/costs.rs | 5 + crates/engine/src/game/derived.rs | 3 + crates/engine/src/game/effects/prepare.rs | 4 +- crates/engine/src/game/engine.rs | 85 +++++ crates/engine/src/game/interaction.rs | 29 +- crates/engine/src/game/mana_sources.rs | 235 +++++++++++- crates/engine/src/game/replay.rs | 31 +- crates/engine/src/game/scenario.rs | 12 + .../engine/src/types/action_stable_order.rs | 12 + crates/engine/src/types/actions.rs | 14 + crates/engine/src/types/game_state.rs | 58 ++- crates/engine/src/types/interaction.rs | 2 + crates/engine/src/types/mana.rs | 71 +++- crates/engine/src/types/mod.rs | 4 +- crates/engine/tests/integration/main.rs | 1 + .../integration/sacrificial_mana_choice.rs | 341 ++++++++++++++++++ crates/phase-ai/src/decision_kind.rs | 11 +- crates/phase-ai/src/search.rs | 25 +- .../src/client_message_wire_guard.rs | 2 + .../src/game_action_payload_guard.rs | 3 +- crates/server-core/src/protocol.rs | 17 + .../tests/game_action_payload_guard.rs | 1 + 59 files changed, 1361 insertions(+), 62 deletions(-) create mode 100644 crates/engine/tests/integration/sacrificial_mana_choice.rs diff --git a/client/src/adapter/__tests__/manaSourceSelectionWireTypes.test.ts b/client/src/adapter/__tests__/manaSourceSelectionWireTypes.test.ts index 09741924d1..18bd28f042 100644 --- a/client/src/adapter/__tests__/manaSourceSelectionWireTypes.test.ts +++ b/client/src/adapter/__tests__/manaSourceSelectionWireTypes.test.ts @@ -59,6 +59,7 @@ describe("mana-source selection wire types", () => { source: { object_id: 17, incarnation: 3 }, ability_index: 0, mana_type: "Blue", + output: { type: "Concrete", data: "Blue" }, atomic_combination: null, restrictions, penalty: "None", diff --git a/client/src/adapter/generated/interaction/index.ts b/client/src/adapter/generated/interaction/index.ts index 0e71b51b46..fff6e5b18f 100644 --- a/client/src/adapter/generated/interaction/index.ts +++ b/client/src/adapter/generated/interaction/index.ts @@ -57,7 +57,7 @@ export type SelectionConstraint = { "type": "count", "data": { min: number, max: export type ConfirmSemantics = "immediate" | "explicit"; -export type InteractionActionCode = "passPriority" | "chooseMeldPair" | "chooseEntryAttackTarget" | "playLand" | "castSpell" | "foretell" | "activateAbility" | "declareAttackers" | "declareBlockers" | "chooseUntap" | "chooseExert" | "chooseEnlist" | "chooseClashOpponent" | "chooseZoneOpponentChooser" | "choosePileOpponent" | "chooseAnnouncingOpponent" | "chooseGiftRecipient" | "chooseAssistPlayer" | "commitAssistPayment" | "mulliganDecision" | "reorderHand" | "tapLandForMana" | "untapLandForMana" | "spendPoolMana" | "unspendPoolMana" | "selectCards" | "chooseRemoveCounterCostDistribution" | "selectCoinFlips" | "chooseOutsideGameCards" | "selectTargets" | "chooseTarget" | "chooseReplacement" | "orderTriggers" | "cancelCast" | "equip" | "crewVehicle" | "activateStation" | "saddleMount" | "transform" | "playFaceDown" | "turnFaceUp" | "submitSideboard" | "choosePlayDraw" | "chooseOption" | "submitVoteCandidate" | "submitSpellbookDraft" | "submitPilePartition" | "choosePile" | "chooseBranch" | "submitLifeRedistribution" | "chooseDamageSource" | "selectModes" | "decideOptionalCost" | "chooseAdventureFace" | "chooseModalFace" | "chooseAlternativeCast" | "chooseCastingVariant" | "keepAllCopyTargets" | "choosePermanentTypeSlot" | "activateNinjutsu" | "castSpellAsSneak" | "castSpellAsWebSlinging" | "castSpellForFree" | "castSpellAsMiracle" | "castSpellAsMadness" | "decideOptionalEffect" | "respondToSpliceOffer" | "decideOptionalEffectAndRemember" | "payUnlessCost" | "chooseUnlessCostBranch" | "chooseActivationCostBranch" | "payCombatTax" | "chooseRingBearer" | "choosePair" | "chooseDungeon" | "chooseDungeonRoom" | "unlockRoomDoor" | "rollPlanarDie" | "chooseRoomDoor" | "tapForConvoke" | "harmonizeTap" | "declareCompanion" | "companionToHand" | "discoverChoice" | "graveyardPaidCastChoice" | "cascadeChoice" | "rippleChoice" | "freeCastWindowChoice" | "chooseTopOrBottom" | "chooseMutateMergeSide" | "cipherEncode" | "chooseLegend" | "chooseBattleProtector" | "setAutoPass" | "cancelAutoPass" | "setPhaseStops" | "setPriorityPassingMode" | "setPriorityYield" | "setMayTriggerAutoChoice" | "setTriggerOrderTemplate" | "assignCombatDamage" | "assignBlockerDamage" | "distributeAmong" | "chooseCounterMoveDistribution" | "chooseCountersToRemove" | "submitPayAmount" | "retargetSpell" | "learnDecision" | "selectCategoryPermanents" | "chooseKeptCreatures" | "chooseKeptPermanents" | "chooseX" | "submitPhyrexianChoices" | "chooseManaColor" | "payManaAbilityMana" | "castPreparedCopy" | "chooseSpecializeColor" | "castParadigmCopy" | "passParadigmOffer" | "grantDebugPermission" | "revokeDebugPermission" | "concede" | "declareShortcut" | "respondToShortcut" | "declineShortcut" | "precastCopyShortcut" | "endContinuousEffect" | "debug"; +export type InteractionActionCode = "passPriority" | "chooseMeldPair" | "chooseEntryAttackTarget" | "playLand" | "castSpell" | "foretell" | "activateAbility" | "declareAttackers" | "declareBlockers" | "chooseUntap" | "chooseExert" | "chooseEnlist" | "chooseClashOpponent" | "chooseZoneOpponentChooser" | "choosePileOpponent" | "chooseAnnouncingOpponent" | "chooseGiftRecipient" | "chooseAssistPlayer" | "commitAssistPayment" | "mulliganDecision" | "reorderHand" | "tapLandForMana" | "activateManaSource" | "backToManaPayment" | "untapLandForMana" | "spendPoolMana" | "unspendPoolMana" | "selectCards" | "chooseRemoveCounterCostDistribution" | "selectCoinFlips" | "chooseOutsideGameCards" | "selectTargets" | "chooseTarget" | "chooseReplacement" | "orderTriggers" | "cancelCast" | "equip" | "crewVehicle" | "activateStation" | "saddleMount" | "transform" | "playFaceDown" | "turnFaceUp" | "submitSideboard" | "choosePlayDraw" | "chooseOption" | "submitVoteCandidate" | "submitSpellbookDraft" | "submitPilePartition" | "choosePile" | "chooseBranch" | "submitLifeRedistribution" | "chooseDamageSource" | "selectModes" | "decideOptionalCost" | "chooseAdventureFace" | "chooseModalFace" | "chooseAlternativeCast" | "chooseCastingVariant" | "keepAllCopyTargets" | "choosePermanentTypeSlot" | "activateNinjutsu" | "castSpellAsSneak" | "castSpellAsWebSlinging" | "castSpellForFree" | "castSpellAsMiracle" | "castSpellAsMadness" | "decideOptionalEffect" | "respondToSpliceOffer" | "decideOptionalEffectAndRemember" | "payUnlessCost" | "chooseUnlessCostBranch" | "chooseActivationCostBranch" | "payCombatTax" | "chooseRingBearer" | "choosePair" | "chooseDungeon" | "chooseDungeonRoom" | "unlockRoomDoor" | "rollPlanarDie" | "chooseRoomDoor" | "tapForConvoke" | "harmonizeTap" | "declareCompanion" | "companionToHand" | "discoverChoice" | "graveyardPaidCastChoice" | "cascadeChoice" | "rippleChoice" | "freeCastWindowChoice" | "chooseTopOrBottom" | "chooseMutateMergeSide" | "cipherEncode" | "chooseLegend" | "chooseBattleProtector" | "setAutoPass" | "cancelAutoPass" | "setPhaseStops" | "setPriorityPassingMode" | "setPriorityYield" | "setMayTriggerAutoChoice" | "setTriggerOrderTemplate" | "assignCombatDamage" | "assignBlockerDamage" | "distributeAmong" | "chooseCounterMoveDistribution" | "chooseCountersToRemove" | "submitPayAmount" | "retargetSpell" | "learnDecision" | "selectCategoryPermanents" | "chooseKeptCreatures" | "chooseKeptPermanents" | "chooseX" | "submitPhyrexianChoices" | "chooseManaColor" | "payManaAbilityMana" | "castPreparedCopy" | "chooseSpecializeColor" | "castParadigmCopy" | "passParadigmOffer" | "grantDebugPermission" | "revokeDebugPermission" | "concede" | "declareShortcut" | "respondToShortcut" | "declineShortcut" | "precastCopyShortcut" | "endContinuousEffect" | "debug"; export type InteractionRoleCode = "source" | "candidate" | "partner" | "attackTarget" | "target" | "paymentMode" | "abilityIndex" | "attacker" | "bandCount" | "blocker" | "blocked" | "untap" | "exert" | "enlistTarget" | "enlist" | "opponent" | "assistPlayer" | "assist" | "genericMana" | "mulligan" | "serumPowder" | "handCard" | "selected" | "counterSource" | "counterType" | "amount" | "coinFlipIndex" | "sideboardIndex" | "faceUpExile" | "optionIndex" | "triggerIndex" | "crewMember" | "stationCrew" | "x" | "mainCard" | "sideboardCard" | "playFirst" | "option" | "candidateIndex" | "cardName" | "pileA" | "pile" | "modeIndex" | "pay" | "face" | "castCost" | "permanentType" | "returnCreature" | "permissionSource" | "accept" | "spliceCard" | "splice" | "choice" | "costBranch" | "costBranchIndex" | "pair" | "dungeon" | "roomIndex" | "door" | "operation" | "convokeMana" | "harmonizeCreature" | "harmonize" | "companion" | "castChoice" | "castCard" | "placement" | "mergeSide" | "encodeCreature" | "encode" | "defender" | "protector" | "assignmentMode" | "damageTarget" | "damageAmount" | "trampleDamage" | "controllerDamage" | "destination" | "discardCard" | "learn" | "category" | "kept" | "phyrexianPayment" | "manaChoice" | "count" | "manaPayment" | "producedMana" | "color" | "player" | "castingVariant" | "mode" | "modeCost" | "castingCost" | "voteOption" | "voteCandidate"; diff --git a/client/src/adapter/types.ts b/client/src/adapter/types.ts index ec938837a8..ddc7c48e62 100644 --- a/client/src/adapter/types.ts +++ b/client/src/adapter/types.ts @@ -668,7 +668,10 @@ export interface CastingVariantChoiceOption { mana_cost: ManaCost; } -export type CastPaymentMode = { type: "Auto" } | { type: "Manual" }; +export type CastPaymentMode = + | { type: "Auto" } + | { type: "AutoExceptSacrificialMana" } + | { type: "Manual" }; export type UnlessCost = | { type: "Fixed"; cost: ManaCost } @@ -1158,6 +1161,10 @@ export type ManaSourcePenalty = | { PaysLifeOnActivation: { fixed_amount: number | null } } | "Sacrifices"; +export type ManaSourceOutput = + | { type: "Concrete"; data: ManaType } + | { type: "DeferredColorChoice" }; + export type ProductionOverride = | { type: "SingleColor"; data: ManaType } | { type: "Combination"; data: ManaType[] }; @@ -1172,6 +1179,7 @@ export interface ManaSourceSelection { source: ObjectIncarnationRef; ability_index: number | null; mana_type: ManaType; + output: ManaSourceOutput; atomic_combination: ManaType[] | null; restrictions: ManaRestriction[]; penalty: ManaSourcePenalty; @@ -1659,6 +1667,7 @@ export type WaitingFor = }; } | { type: "ManaPayment"; data: { player: PlayerId; convoke_mode?: ConvokeMode } } + | { type: "ManaSourceSelection"; data: { player: PlayerId; options: ManaSourceSelection[]; convoke_mode?: ConvokeMode } } | { type: "ChooseXValue"; data: { @@ -2186,6 +2195,8 @@ export type GameAction = | { type: "MulliganDecision"; data: { choice: MulliganChoice } } | { type: "ReorderHand"; data: { order: ObjectId[] } } | { type: "TapLandForMana"; data: { selection: ManaSourceSelection } } + | { type: "ActivateManaSource"; data: { selection: ManaSourceSelection } } + | { type: "BackToManaPayment" } | { type: "UntapLandForMana"; data: { object_id: ObjectId } } // CR 118.3a: pin / unpin a specific pool unit during manual mana payment. | { type: "SpendPoolMana"; data: { pip_id: number } } @@ -2927,6 +2938,7 @@ export interface GameState { combat: CombatState | null; waiting_for: WaitingFor; has_pending_cast: boolean; + allows_cancel_cast?: boolean; /** * CR 601.2f: The locked-in pending cast (cost, ability, object) while the * caster is mid-cast. Present during ManaPayment / cost-choice WaitingFor diff --git a/client/src/components/board/__tests__/PermanentCard.test.tsx b/client/src/components/board/__tests__/PermanentCard.test.tsx index 7c8d3f6038..0da939609d 100644 --- a/client/src/components/board/__tests__/PermanentCard.test.tsx +++ b/client/src/components/board/__tests__/PermanentCard.test.tsx @@ -1169,6 +1169,7 @@ describe("PermanentCard", () => { source: { object_id: 39, incarnation: 1 }, ability_index: null, mana_type: "Green", + output: { type: "Concrete", data: "Green" }, atomic_combination: null, restrictions: [], penalty: "None", diff --git a/client/src/components/mana/ManaPaymentUI.tsx b/client/src/components/mana/ManaPaymentUI.tsx index ac9b24a55c..834eb1d0af 100644 --- a/client/src/components/mana/ManaPaymentUI.tsx +++ b/client/src/components/mana/ManaPaymentUI.tsx @@ -621,6 +621,69 @@ export function ManaPaymentUI() { ); } +/** CR 605.3b: The engine has reached a mana ability that sacrifices a + * permanent. It supplies every legal capability; this display layer only + * renders those rows and returns the selected opaque action. */ +export function ManaSourceSelectionUI() { + const { t } = useTranslation("game"); + const waitingFor = useGameStore((s) => s.waitingFor); + const gameState = useGameStore((s) => s.gameState); + const dispatch = useGameStore((s) => s.dispatch); + + if (waitingFor?.type !== "ManaSourceSelection") return null; + + return ( + + + +

{t("manaSourceSelection.title")}

+

{t("manaSourceSelection.description")}

+
+ {waitingFor.data.options.map((selection, index) => { + const source = gameState?.objects[selection.source.object_id]; + return ( + + ); + })} +
+ +
+
+
+ ); +} + // Color → shard symbol code for `ManaSymbol` (White→"W", …, Colorless→"C"). const COLOR_SHARD: Record = { White: "W", diff --git a/client/src/components/mana/__tests__/ManaPaymentUI.test.tsx b/client/src/components/mana/__tests__/ManaPaymentUI.test.tsx index 2c68afa18e..f4e851e17a 100644 --- a/client/src/components/mana/__tests__/ManaPaymentUI.test.tsx +++ b/client/src/components/mana/__tests__/ManaPaymentUI.test.tsx @@ -2,7 +2,7 @@ import { act } from "react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { cleanup, fireEvent, render, screen } from "@testing-library/react"; -import { ManaPaymentUI } from "../ManaPaymentUI"; +import { ManaPaymentUI, ManaSourceSelectionUI } from "../ManaPaymentUI"; import { useGameStore } from "../../../stores/gameStore"; import type { GameState } from "../../../adapter/types"; import { buildGameObjectWithCoreTypes, buildObjectMap } from "../../../test/factories/gameObjectFactory.ts"; @@ -421,3 +421,64 @@ describe("ManaPaymentUI", () => { }); }); }); + +describe("ManaSourceSelectionUI", () => { + beforeEach(() => { + useGameStore.getState().reset(); + }); + + afterEach(() => { + cleanup(); + }); + + it("dispatches only the engine-issued sacrificial source and Back action", () => { + const dispatch = vi.fn().mockResolvedValue([]); + const source = buildGameObjectWithCoreTypes(["Artifact"], { + id: 91, + card_id: 91, + name: "Basal Sliver", + zone: "Battlefield", + }); + const selection = { + source: { object_id: 91, incarnation: 0 }, + ability_index: 0, + mana_type: "Black" as const, + output: { type: "Concrete" as const, data: "Black" as const }, + atomic_combination: null, + restrictions: [], + penalty: "Sacrifices" as const, + taps_for_mana: [], + }; + const gameState = createGameState({ + objects: buildObjectMap(source), + waiting_for: { + type: "ManaSourceSelection", + data: { + player: 0, + options: [selection], + convoke_mode: null, + }, + }, + }); + + act(() => { + useGameStore.setState({ + gameState, + waitingFor: gameState.waiting_for, + dispatch, + legalActions: [], + }); + }); + + render(); + + fireEvent.click(screen.getByRole("button", { name: /basal sliver/i })); + expect(dispatch).toHaveBeenCalledWith({ + type: "ActivateManaSource", + data: { selection }, + }); + + fireEvent.click(screen.getByRole("button", { name: /back to mana payment/i })); + expect(dispatch).toHaveBeenCalledWith({ type: "BackToManaPayment" }); + }); +}); diff --git a/client/src/components/settings/PreferencesModal.tsx b/client/src/components/settings/PreferencesModal.tsx index 400e761eef..89c682a38b 100644 --- a/client/src/components/settings/PreferencesModal.tsx +++ b/client/src/components/settings/PreferencesModal.tsx @@ -34,6 +34,7 @@ import type { CommandZoneDisplay, LogDefaultState, MultiplayerBoardLayout, + SpellPaymentMode, ZoneCollapseMode, } from "../../stores/preferencesStore.ts"; import type { SupportedLng } from "../../i18n/resources.ts"; @@ -72,6 +73,7 @@ const CARD_SIZES: CardSizePreference[] = ["small", "medium", "large"]; const COMMAND_ZONE_DISPLAYS: CommandZoneDisplay[] = ["auto", "inline", "compact"]; const ZONE_COLLAPSE_MODES: ZoneCollapseMode[] = ["auto", "on", "off"]; const CARD_PREVIEW_MODES: CardPreviewMode[] = ["follow", "side", "shift"]; +const SPELL_PAYMENT_MODES: SpellPaymentMode[] = ["auto", "autoExceptSacrificialMana", "manual"]; const LOG_DEFAULTS: LogDefaultState[] = ["open", "closed"]; const VFX_QUALITIES: VfxQuality[] = ["full", "reduced", "minimal"]; const MULTIPLAYER_BOARD_LAYOUTS: MultiplayerBoardLayout[] = ["focused", "split"]; @@ -428,15 +430,12 @@ export function PreferencesModal({ - + t(`gameplay.spellPaymentOptions.${option}`)} + /> {isTauri() && ( @@ -723,7 +722,7 @@ export function PreferencesModal({ type="button" onClick={handleImportTheme} disabled={themeImportStatus === "loading" || !themeImportUrl.trim()} - className="rounded-[14px] border border-white/10 bg-sky-600/30 px-4 py-2 text-sm text-slate-100 hover:bg-sky-600/50 disabled:opacity-50" + className="rounded-[14px] border border-white/10 bg-sky-600/30 px-4 py-2 text-sm text-white hover:bg-sky-600/50 disabled:opacity-50" > {themeImportStatus === "loading" ? t("audioTheme.loading") : t("audioTheme.import")} @@ -1537,7 +1536,7 @@ function ArtChainEditor({ type="button" onClick={handleAddSet} disabled={!resolveSetCode(setInput)} - className="rounded-[14px] border border-white/10 bg-sky-600/30 px-4 py-2 text-sm text-slate-100 hover:bg-sky-600/50 disabled:opacity-50" + className="rounded-[14px] border border-white/10 bg-sky-600/30 px-4 py-2 text-sm text-white hover:bg-sky-600/50 disabled:opacity-50" > {t("artChain.addSet")} diff --git a/client/src/game/__tests__/castPaymentMode.test.ts b/client/src/game/__tests__/castPaymentMode.test.ts index 7d9c5be5b8..f717b30312 100644 --- a/client/src/game/__tests__/castPaymentMode.test.ts +++ b/client/src/game/__tests__/castPaymentMode.test.ts @@ -38,4 +38,35 @@ describe("applySpellPaymentPreference", () => { type: "Manual", }); }); + + it("stamps every cast-family action with the sacrificial-mana preference", () => { + usePreferencesStore.setState({ spellPaymentMode: "autoExceptSacrificialMana" }); + const castTypes = [ + "CastSpell", + "CastSpellForFree", + "CastSpellAsMiracle", + "CastSpellAsMadness", + "CastSpellAsSneak", + "CastSpellAsWebSlinging", + ] as const; + + for (const type of castTypes) { + const action = { type, data: {} } as GameAction; + const result = applySpellPaymentPreference(action) as GameAction & { + data: { payment_mode?: unknown }; + }; + expect(result.data.payment_mode).toEqual({ type: "AutoExceptSacrificialMana" }); + } + }); + + it("lets the per-game manual override dominate the sacrificial-mana preference", () => { + usePreferencesStore.setState({ spellPaymentMode: "autoExceptSacrificialMana" }); + useUiStore.setState({ manualManaOverride: true }); + + const result = applySpellPaymentPreference(castAction); + + expect((result as Extract).data.payment_mode).toEqual({ + type: "Manual", + }); + }); }); diff --git a/client/src/game/castPaymentMode.ts b/client/src/game/castPaymentMode.ts index 42ffebe37b..a94c7e6836 100644 --- a/client/src/game/castPaymentMode.ts +++ b/client/src/game/castPaymentMode.ts @@ -3,45 +3,52 @@ import { usePreferencesStore } from "../stores/preferencesStore"; import { useUiStore } from "../stores/uiStore"; const MANUAL_CAST_PAYMENT_MODE: CastPaymentMode = { type: "Manual" }; +const AUTO_EXCEPT_SACRIFICIAL_MANA_PAYMENT_MODE: CastPaymentMode = { + type: "AutoExceptSacrificialMana", +}; export function applySpellPaymentPreference(action: GameAction): GameAction { - // Two intended sources of truth: the durable `spellPaymentMode` preference and - // the ephemeral per-game `manualManaOverride` toggle. Manual wins if EITHER is on. - const manual = - usePreferencesStore.getState().spellPaymentMode === "manual" || - useUiStore.getState().manualManaOverride; - if (!manual) return action; + // Manual is the per-game escape hatch and deliberately dominates the saved + // preference. Otherwise stamp every cast-family action with the exact engine + // mode so all alternate casting routes share the same payment semantics. + const preference = usePreferencesStore.getState().spellPaymentMode; + const mode = useUiStore.getState().manualManaOverride || preference === "manual" + ? MANUAL_CAST_PAYMENT_MODE + : preference === "autoExceptSacrificialMana" + ? AUTO_EXCEPT_SACRIFICIAL_MANA_PAYMENT_MODE + : null; + if (!mode) return action; switch (action.type) { case "CastSpell": return { ...action, - data: { ...action.data, payment_mode: MANUAL_CAST_PAYMENT_MODE }, + data: { ...action.data, payment_mode: mode }, }; case "CastSpellForFree": return { ...action, - data: { ...action.data, payment_mode: MANUAL_CAST_PAYMENT_MODE }, + data: { ...action.data, payment_mode: mode }, }; case "CastSpellAsMiracle": return { ...action, - data: { ...action.data, payment_mode: MANUAL_CAST_PAYMENT_MODE }, + data: { ...action.data, payment_mode: mode }, }; case "CastSpellAsMadness": return { ...action, - data: { ...action.data, payment_mode: MANUAL_CAST_PAYMENT_MODE }, + data: { ...action.data, payment_mode: mode }, }; case "CastSpellAsSneak": return { ...action, - data: { ...action.data, payment_mode: MANUAL_CAST_PAYMENT_MODE }, + data: { ...action.data, payment_mode: mode }, }; case "CastSpellAsWebSlinging": return { ...action, - data: { ...action.data, payment_mode: MANUAL_CAST_PAYMENT_MODE }, + data: { ...action.data, payment_mode: mode }, }; default: return action; diff --git a/client/src/game/controllers/aiController.ts b/client/src/game/controllers/aiController.ts index a52f1c9eae..21e0586f62 100644 --- a/client/src/game/controllers/aiController.ts +++ b/client/src/game/controllers/aiController.ts @@ -280,7 +280,16 @@ export function createAIController(config: AIControllerConfig): AIController { waitingFor: WaitingFor, state: GameState, ): Promise { - if (state.has_pending_cast) { + if (waitingFor.type === "ManaSourceSelection") { + const { adapter } = useGameStore.getState(); + if (!adapter) return Promise.resolve(null); + return adapter.getLegalActions().then((result) => { + return result.actions.find((action) => action.type === "ActivateManaSource") + ?? result.actions.find((action) => action.type === "BackToManaPayment") + ?? null; + }); + } + if (state.has_pending_cast && state.allows_cancel_cast) { return Promise.resolve({ type: "CancelCast" }); } const { adapter } = useGameStore.getState(); diff --git a/client/src/game/waitingForRegistry.ts b/client/src/game/waitingForRegistry.ts index b70f979ca5..e8dfb6b406 100644 --- a/client/src/game/waitingForRegistry.ts +++ b/client/src/game/waitingForRegistry.ts @@ -38,6 +38,7 @@ export const HANDLED_WAITING_FOR_TYPES: ReadonlySet = "MeldAttackTargetChoice", // Cast / activation chain — ManaPayment + PhyrexianPayment share ManaPaymentUI. ...MANA_PAYMENT_WAITING_FOR_TYPES, + "ManaSourceSelection", "ChooseXValue", "PayAmountChoice", "TargetSelection", @@ -247,6 +248,7 @@ export function waitingForReason( case "RetargetChoice": return { key: "status.reason.choosingTargets" }; case "ManaPayment": + case "ManaSourceSelection": case "PhyrexianPayment": case "PayCost": case "PayManaAbilityMana": diff --git a/client/src/hooks/__tests__/useKeyboardShortcuts.test.tsx b/client/src/hooks/__tests__/useKeyboardShortcuts.test.tsx index dce6c74109..9f3d26bcbc 100644 --- a/client/src/hooks/__tests__/useKeyboardShortcuts.test.tsx +++ b/client/src/hooks/__tests__/useKeyboardShortcuts.test.tsx @@ -132,6 +132,7 @@ describe("useKeyboardShortcuts", () => { source: { object_id: objectId, incarnation: 1 }, ability_index: null, mana_type: "Green", + output: { type: "Concrete", data: "Green" }, atomic_combination: null, restrictions: [], penalty: "None", diff --git a/client/src/i18n/locales/de/game.json b/client/src/i18n/locales/de/game.json index fb7f3e5988..fdba287aa7 100644 --- a/client/src/i18n/locales/de/game.json +++ b/client/src/i18n/locales/de/game.json @@ -5,6 +5,7 @@ "cantActivateDuring": "Kann derzeit nicht aktiviert werden", "prohibited": "Das Aktivieren dieser Fähigkeit ist verboten" }, + "manaSourceSelection": { "title": "Manaquelle wählen", "description": "Diese Manafähigkeit erfordert das Opfern einer bleibenden Karte.", "sacrifice": "Opfern", "back": "Zurück zur Manabezahlung", "unknownSource": "Manaquelle" }, "comboShortcut": { "declareTitle": "Loop Shortcut", "declareSubtitle": "You control a loop with a determined outcome. Take the shortcut to resolve it.", diff --git a/client/src/i18n/locales/de/settings.json b/client/src/i18n/locales/de/settings.json index 70847ac630..d5699bd6af 100644 --- a/client/src/i18n/locales/de/settings.json +++ b/client/src/i18n/locales/de/settings.json @@ -29,6 +29,7 @@ "logDefault": "Protokoll-Standard", "spellPayment": "Zauberspruch-Bezahlung", "manualManaPayment": "Manuelle Manabezahlung für Zaubersprüche", + "spellPaymentOptions": { "auto": "Automatisch", "autoExceptSacrificialMana": "Vor dem Opfern fragen", "manual": "Manuell" }, "nativeEngine": "Native Engine für lokale Spiele und gehostete Mehrspielerpartien verwenden", "nativeEngineDescription": "Verwende die heruntergeladene Engine für lokale KI-Spiele und Spiele, die du über eine Lobby hostest. Gäste verbinden sich weiterhin über die Peer-to-Peer-Verbindung der Lobby. Fällt auf die integrierte Engine zurück, wenn sie nicht verfügbar ist.", "boardBackground": "Spielfeldhintergrund", diff --git a/client/src/i18n/locales/en/game.json b/client/src/i18n/locales/en/game.json index 2276c376b6..d22455282f 100644 --- a/client/src/i18n/locales/en/game.json +++ b/client/src/i18n/locales/en/game.json @@ -5,6 +5,13 @@ "cantActivateDuring": "Can't be activated right now", "prohibited": "Activating this ability is prohibited" }, + "manaSourceSelection": { + "title": "Choose a mana source", + "description": "This mana ability requires sacrificing a permanent.", + "sacrifice": "Sacrifice", + "back": "Back to mana payment", + "unknownSource": "Mana source" + }, "comboShortcut": { "declareTitle": "Loop Shortcut", "declareSubtitle": "You control a loop with a determined outcome. Take the shortcut to resolve it.", diff --git a/client/src/i18n/locales/en/settings.json b/client/src/i18n/locales/en/settings.json index c24b30e2a7..c8dd7ccf2e 100644 --- a/client/src/i18n/locales/en/settings.json +++ b/client/src/i18n/locales/en/settings.json @@ -29,6 +29,11 @@ "logDefault": "Log Default", "spellPayment": "Spell Payment", "manualManaPayment": "Manual mana payment for spells", + "spellPaymentOptions": { + "auto": "Automatic", + "autoExceptSacrificialMana": "Ask before sacrificing", + "manual": "Manual" + }, "nativeEngine": "Use native engine for local games and hosted multiplayer", "nativeEngineDescription": "Use the downloaded engine for local AI games and games you host through a lobby. Guests still connect through the lobby's peer-to-peer connection. Falls back to the built-in engine when unavailable.", "boardBackground": "Board Background", diff --git a/client/src/i18n/locales/es/game.json b/client/src/i18n/locales/es/game.json index 3d15c692c8..26e0897409 100644 --- a/client/src/i18n/locales/es/game.json +++ b/client/src/i18n/locales/es/game.json @@ -5,6 +5,7 @@ "cantActivateDuring": "No puede activarse ahora", "prohibited": "Activar esta habilidad está prohibido" }, + "manaSourceSelection": { "title": "Elige una fuente de maná", "description": "Esta habilidad de maná requiere sacrificar un permanente.", "sacrifice": "Sacrificar", "back": "Volver al pago de maná", "unknownSource": "Fuente de maná" }, "comboShortcut": { "declareTitle": "Loop Shortcut", "declareSubtitle": "You control a loop with a determined outcome. Take the shortcut to resolve it.", diff --git a/client/src/i18n/locales/es/settings.json b/client/src/i18n/locales/es/settings.json index 56d809adff..e23ea850ed 100644 --- a/client/src/i18n/locales/es/settings.json +++ b/client/src/i18n/locales/es/settings.json @@ -29,6 +29,7 @@ "logDefault": "Registro predeterminado", "spellPayment": "Pago de hechizos", "manualManaPayment": "Pago manual de maná para hechizos", + "spellPaymentOptions": { "auto": "Automático", "autoExceptSacrificialMana": "Preguntar antes de sacrificar", "manual": "Manual" }, "nativeEngine": "Usar el motor nativo para partidas locales y multijugador alojado", "nativeEngineDescription": "Usa el motor descargado para partidas locales contra IA y partidas que alojes mediante una sala. Los invitados siguen conectándose mediante la conexión punto a punto de la sala. Usa el motor integrado cuando no está disponible.", "boardBackground": "Fondo del tablero", diff --git a/client/src/i18n/locales/fr/game.json b/client/src/i18n/locales/fr/game.json index 4435e8d67d..af9fcad115 100644 --- a/client/src/i18n/locales/fr/game.json +++ b/client/src/i18n/locales/fr/game.json @@ -5,6 +5,7 @@ "cantActivateDuring": "Ne peut pas être activée pour l'instant", "prohibited": "L'activation de cette capacité est interdite" }, + "manaSourceSelection": { "title": "Choisir une source de mana", "description": "Cette capacité de mana demande de sacrifier un permanent.", "sacrifice": "Sacrifier", "back": "Retour au paiement de mana", "unknownSource": "Source de mana" }, "comboShortcut": { "declareTitle": "Loop Shortcut", "declareSubtitle": "You control a loop with a determined outcome. Take the shortcut to resolve it.", diff --git a/client/src/i18n/locales/fr/settings.json b/client/src/i18n/locales/fr/settings.json index f3c25aaf37..35f06d13eb 100644 --- a/client/src/i18n/locales/fr/settings.json +++ b/client/src/i18n/locales/fr/settings.json @@ -29,6 +29,7 @@ "logDefault": "Journal par défaut", "spellPayment": "Paiement des sorts", "manualManaPayment": "Paiement manuel du mana pour les sorts", + "spellPaymentOptions": { "auto": "Automatique", "autoExceptSacrificialMana": "Demander avant de sacrifier", "manual": "Manuel" }, "nativeEngine": "Utiliser le moteur natif pour les parties locales et le multijoueur hébergé", "nativeEngineDescription": "Utilisez le moteur téléchargé pour les parties locales contre l’IA et les parties que vous hébergez via un salon. Les invités se connectent toujours via la connexion pair à pair du salon. Utilise le moteur intégré lorsqu’il n’est pas disponible.", "boardBackground": "Arrière-plan du plateau", diff --git a/client/src/i18n/locales/it/game.json b/client/src/i18n/locales/it/game.json index 30a3c768c1..24aff1b0f9 100644 --- a/client/src/i18n/locales/it/game.json +++ b/client/src/i18n/locales/it/game.json @@ -5,6 +5,7 @@ "cantActivateDuring": "Non può essere attivata ora", "prohibited": "L'attivazione di questa abilità è proibita" }, + "manaSourceSelection": { "title": "Scegli una fonte di mana", "description": "Questa abilità di mana richiede di sacrificare un permanente.", "sacrifice": "Sacrifica", "back": "Torna al pagamento del mana", "unknownSource": "Fonte di mana" }, "comboShortcut": { "declareTitle": "Loop Shortcut", "declareSubtitle": "You control a loop with a determined outcome. Take the shortcut to resolve it.", diff --git a/client/src/i18n/locales/it/settings.json b/client/src/i18n/locales/it/settings.json index 0f969c6722..ab047477e3 100644 --- a/client/src/i18n/locales/it/settings.json +++ b/client/src/i18n/locales/it/settings.json @@ -29,6 +29,7 @@ "logDefault": "Registro predefinito", "spellPayment": "Pagamento magie", "manualManaPayment": "Pagamento manuale del mana per le magie", + "spellPaymentOptions": { "auto": "Automatico", "autoExceptSacrificialMana": "Chiedi prima di sacrificare", "manual": "Manuale" }, "nativeEngine": "Usa il motore nativo per partite locali e multiplayer ospitato", "nativeEngineDescription": "Usa il motore scaricato per le partite locali contro l'IA e le partite che ospiti tramite una lobby. Gli ospiti continuano a connettersi tramite la connessione peer-to-peer della lobby. Usa il motore integrato quando non è disponibile.", "boardBackground": "Sfondo del campo", diff --git a/client/src/i18n/locales/pl/game.json b/client/src/i18n/locales/pl/game.json index c7acb281b3..1b28510c77 100644 --- a/client/src/i18n/locales/pl/game.json +++ b/client/src/i18n/locales/pl/game.json @@ -5,6 +5,7 @@ "cantActivateDuring": "Nie można teraz aktywować", "prohibited": "Aktywacja tej zdolności jest zabroniona" }, + "manaSourceSelection": { "title": "Wybierz źródło many", "description": "Ta zdolność many wymaga poświęcenia permanenta.", "sacrifice": "Poświęć", "back": "Wróć do płatności many", "unknownSource": "Źródło many" }, "comboShortcut": { "declareTitle": "Loop Shortcut", "declareSubtitle": "You control a loop with a determined outcome. Take the shortcut to resolve it.", diff --git a/client/src/i18n/locales/pl/settings.json b/client/src/i18n/locales/pl/settings.json index 969d3dd454..1f3db39bf0 100644 --- a/client/src/i18n/locales/pl/settings.json +++ b/client/src/i18n/locales/pl/settings.json @@ -29,6 +29,7 @@ "logDefault": "Domyślny dziennik", "spellPayment": "Płatność za czary", "manualManaPayment": "Ręczna płatność many za czary", + "spellPaymentOptions": { "auto": "Automatycznie", "autoExceptSacrificialMana": "Pytaj przed poświęceniem", "manual": "Ręcznie" }, "nativeEngine": "Używaj natywnego silnika w lokalnych grach i hostowanym trybie wieloosobowym", "nativeEngineDescription": "Używaj pobranego silnika do lokalnych gier z AI i gier hostowanych przez lobby. Goście nadal łączą się przez połączenie peer-to-peer lobby. Gdy silnik jest niedostępny, używany jest wbudowany silnik.", "boardBackground": "Tło pola bitwy", diff --git a/client/src/i18n/locales/pt/game.json b/client/src/i18n/locales/pt/game.json index be12235a48..71fab73662 100644 --- a/client/src/i18n/locales/pt/game.json +++ b/client/src/i18n/locales/pt/game.json @@ -5,6 +5,7 @@ "cantActivateDuring": "Não pode ser ativada agora", "prohibited": "Ativar esta habilidade é proibido" }, + "manaSourceSelection": { "title": "Escolha uma fonte de mana", "description": "Esta habilidade de mana exige sacrificar uma permanente.", "sacrifice": "Sacrificar", "back": "Voltar ao pagamento de mana", "unknownSource": "Fonte de mana" }, "comboShortcut": { "declareTitle": "Loop Shortcut", "declareSubtitle": "You control a loop with a determined outcome. Take the shortcut to resolve it.", diff --git a/client/src/i18n/locales/pt/settings.json b/client/src/i18n/locales/pt/settings.json index 508924e125..a7d3cac0ca 100644 --- a/client/src/i18n/locales/pt/settings.json +++ b/client/src/i18n/locales/pt/settings.json @@ -29,6 +29,7 @@ "logDefault": "Registro Padrão", "spellPayment": "Pagamento de Mágicas", "manualManaPayment": "Pagamento manual de mana para mágicas", + "spellPaymentOptions": { "auto": "Automático", "autoExceptSacrificialMana": "Perguntar antes de sacrificar", "manual": "Manual" }, "nativeEngine": "Usar o motor nativo para partidas locais e multijogador hospedado", "nativeEngineDescription": "Use o motor baixado para partidas locais contra IA e partidas hospedadas por você em um lobby. Os convidados continuam a se conectar pela conexão ponto a ponto do lobby. Usa o motor integrado quando não está disponível.", "boardBackground": "Plano de Fundo do Tabuleiro", diff --git a/client/src/network/__tests__/protocol.test.ts b/client/src/network/__tests__/protocol.test.ts index 8ba5036ed3..af7a4ad0e7 100644 --- a/client/src/network/__tests__/protocol.test.ts +++ b/client/src/network/__tests__/protocol.test.ts @@ -37,7 +37,7 @@ const viewerInteractionWithProducedMana = { describe("encodeWireMessage / decodeWireMessage", () => { it("pins the P2P wire protocol to v16", () => { - expect(WIRE_PROTOCOL_VERSION).toBe(16); + expect(WIRE_PROTOCOL_VERSION).toBe(17); }); it("defaults shortcut actions for a legacy payload created before the additive field", () => { diff --git a/client/src/network/protocol.ts b/client/src/network/protocol.ts index 1ed4258c2e..4288b9f8c2 100644 --- a/client/src/network/protocol.ts +++ b/client/src/network/protocol.ts @@ -80,6 +80,7 @@ export function legalActionsFromWire(wire: LegalActionsWire): LegalActionsResult * 4 — Archenemy derived view and scheme deck payloads * 5 — CardPredicateGuessMade game event shape * 13 — Actor-scoped priority-passing settings and filtered per-player state. + * 17 — Sacrificial-mana source selection action and waiting-state snapshots. * 12 — Connive exact subject snapshots and resident paused post-replacement * drains changed P2P GameState snapshots. * 11 — Serialized GameState trigger provenance and paused logical zone-change owners. @@ -91,7 +92,7 @@ export function legalActionsFromWire(wire: LegalActionsWire): LegalActionsResult * sub-phase on WaitingFor::MulliganDecision; the MulliganBottomCards * variant was removed */ -export const WIRE_PROTOCOL_VERSION = 16 as const; +export const WIRE_PROTOCOL_VERSION = 17 as const; export type P2PMessage = | { type: "guest_deck"; deckData: unknown; displayName?: string; reservationToken?: string } diff --git a/client/src/pages/GamePage.tsx b/client/src/pages/GamePage.tsx index 4564b8d73a..37a9084642 100644 --- a/client/src/pages/GamePage.tsx +++ b/client/src/pages/GamePage.tsx @@ -71,7 +71,7 @@ import { HelpSheet } from "../components/help/HelpSheet.tsx"; import { GameLogPanel } from "../components/log/GameLogPanel.tsx"; import { ChooseXValueUI } from "../components/mana/ChooseXValueUI.tsx"; import { AssistPaymentUI } from "../components/mana/AssistPaymentUI.tsx"; -import { ManaPaymentUI } from "../components/mana/ManaPaymentUI.tsx"; +import { ManaPaymentUI, ManaSourceSelectionUI } from "../components/mana/ManaPaymentUI.tsx"; import { PayAmountChoiceUI } from "../components/mana/PayAmountChoiceUI.tsx"; import { RichLabel } from "../components/mana/RichLabel.tsx"; import { CardDataMissingModal } from "../components/modal/CardDataMissingModal.tsx"; @@ -1770,6 +1770,8 @@ function GamePageContent({ {waitingFor != null && MANA_PAYMENT_WAITING_FOR_TYPES.has(waitingFor.type) && canActForWaitingState && } + {waitingFor?.type === "ManaSourceSelection" && + canActForWaitingState && } {waitingFor?.type === "ChooseXValue" && canActForWaitingState && } {waitingFor?.type === "PayAmountChoice" && diff --git a/client/src/stores/preferencesStore.ts b/client/src/stores/preferencesStore.ts index 3e81640d5f..31beb1c239 100644 --- a/client/src/stores/preferencesStore.ts +++ b/client/src/stores/preferencesStore.ts @@ -89,7 +89,7 @@ export type CommandZoneDisplay = "compact" | "inline" | "auto"; * tri-state "auto" precedent. Lands and support each carry their own value. */ export type ZoneCollapseMode = "auto" | "on" | "off"; export type TapRotation = "mtga" | "classic"; -export type SpellPaymentMode = "auto" | "manual"; +export type SpellPaymentMode = "auto" | "autoExceptSacrificialMana" | "manual"; /** Which screen edge the resolving-stack panel docks to (and collapses toward). * User-chosen so a player can keep the stack off whichever side of the * battlefield they care about — e.g. dock left to free the right action rail. */ @@ -785,7 +785,7 @@ export const usePreferencesStore = create }), { name: "phase-preferences", - version: 28, + version: 29, // v0 → v1: flat aiDifficulty + aiDeckName become aiSeats[0]. // v1 → v2: discrete animationSpeed/combatPacing enums become numeric // animationSpeedMultiplier/combatPacingMultiplier. @@ -845,6 +845,9 @@ export const usePreferencesStore = create // preserves the intended enabled-by-default behavior. // v27 → v28: Add showCardPreviewFooter; legacy stores default to true // via the shallow merge, preserving the prior presentation. + // v28 → v29: Add the sacrificial-mana-aware automatic mode. Existing + // values remain valid; malformed persisted values normalize to + // the legacy automatic behavior below. migrate: (persisted: unknown, version: number) => { if (!persisted || typeof persisted !== "object") return persisted; let migrated = persisted as Record; @@ -938,6 +941,17 @@ export const usePreferencesStore = create migrated = { ...migrated, spellPaymentMode: "auto" }; } + if (version < 29) { + const mode = (migrated as { spellPaymentMode?: unknown }).spellPaymentMode; + migrated = { + ...migrated, + spellPaymentMode: + mode === "auto" || mode === "autoExceptSacrificialMana" || mode === "manual" + ? mode + : "auto", + }; + } + if (version < 9) { const lng = (migrated as { language?: unknown }).language; migrated = { diff --git a/client/src/viewmodel/__tests__/cardActionChoice.test.ts b/client/src/viewmodel/__tests__/cardActionChoice.test.ts index 869935a1a9..484728f715 100644 --- a/client/src/viewmodel/__tests__/cardActionChoice.test.ts +++ b/client/src/viewmodel/__tests__/cardActionChoice.test.ts @@ -66,6 +66,7 @@ function tapLandAction(objectId: number): Extract mana_payment_actions(state, *player, *convoke_mode), + WaitingFor::ManaSourceSelection { + player, options, .. + } => { + let mut actions = options + .iter() + .cloned() + .map(|selection| { + candidate( + GameAction::ActivateManaSource { selection }, + TacticalClass::Mana, + Some(*player), + ) + }) + .collect::>(); + actions.push(candidate( + GameAction::BackToManaPayment, + TacticalClass::Pass, + Some(*player), + )); + actions + } WaitingFor::MoveCountersDistribution { player, available, @@ -3362,7 +3383,10 @@ fn semantic_candidate_actions_with_probe( let has_pending_cast = state.waiting_for.has_pending_cast() || (matches!(state.waiting_for, WaitingFor::DistributeAmong { .. }) && state.pending_cast.is_some()); - if has_pending_cast { + let allows_cancel_cast = state.waiting_for.allows_cancel_cast() + || (matches!(state.waiting_for, WaitingFor::DistributeAmong { .. }) + && state.pending_cast.is_some()); + if has_pending_cast && allows_cancel_cast { if let Some(player) = state.waiting_for.acting_player() { actions.push(candidate( GameAction::CancelCast, diff --git a/crates/engine/src/ai_support/filter.rs b/crates/engine/src/ai_support/filter.rs index e833014569..f3b72621c6 100644 --- a/crates/engine/src/ai_support/filter.rs +++ b/crates/engine/src/ai_support/filter.rs @@ -1219,7 +1219,7 @@ fn legality_equivalence_key( key.is_blocked = crate::game::restrictions::is_source_blocked(state, *source_id); Some(key) } - GameAction::TapLandForMana { selection } => { + GameAction::TapLandForMana { selection } | GameAction::ActivateManaSource { selection } => { if poison.has_activation { return None; } diff --git a/crates/engine/src/ai_support/mod.rs b/crates/engine/src/ai_support/mod.rs index f27f8d2e27..8442b45b47 100644 --- a/crates/engine/src/ai_support/mod.rs +++ b/crates/engine/src/ai_support/mod.rs @@ -188,6 +188,16 @@ fn cheap_reject_candidate(state: &GameState, action: &GameAction) -> bool { }, }, ) + | ( + WaitingFor::Priority { .. }, + GameAction::ActivateManaSource { + selection: + crate::types::mana::ManaSourceSelection { + source: crate::types::identifiers::ObjectIncarnationRef { object_id, .. }, + .. + }, + }, + ) | (WaitingFor::Priority { .. }, GameAction::UntapLandForMana { object_id }) | ( WaitingFor::Priority { .. }, @@ -1010,6 +1020,18 @@ fn grouped_mana_requires_priority(state: &GameState, player: PlayerId) -> bool { &aura_sources, &mana_activation_gates, ), + GameAction::ActivateManaSource { selection } => { + selection.ability_index.is_some_and(|ability_index| { + activate_mana_action_would_queue_non_mana_trigger( + state, + player, + selection.source.object_id, + ability_index, + &aura_sources, + &mana_activation_gates, + ) + }) + } _ => false, }) } @@ -1436,6 +1458,21 @@ fn beneficial_mana_tap_trigger_hold( } *source_id } + GameAction::ActivateManaSource { selection } => { + let Some(ability_index) = selection.ability_index else { + return false; + }; + let object_id = selection.source.object_id; + let has_tap = state + .objects + .get(&object_id) + .and_then(|obj| obj.abilities.get(ability_index)) + .is_some_and(|ability| mana_sources::has_tap_component(&ability.cost)); + if !has_tap { + return false; + } + object_id + } _ => return false, }; diff --git a/crates/engine/src/ai_support/payment_continuation.rs b/crates/engine/src/ai_support/payment_continuation.rs index c102286df8..f97ac9e45b 100644 --- a/crates/engine/src/ai_support/payment_continuation.rs +++ b/crates/engine/src/ai_support/payment_continuation.rs @@ -92,7 +92,9 @@ pub fn classify_payment_continuation(state: &GameState) -> PaymentContinuationSt match &state.waiting_for { // CR 601.2g–h: during the ordinary mana-payment window, the visible // payer and live pending cast jointly identify the payment root. - WaitingFor::ManaPayment { player, .. } => classify_global_root(state, *player), + WaitingFor::ManaPayment { player, .. } | WaitingFor::ManaSourceSelection { player, .. } => { + classify_global_root(state, *player) + } // CR 601.2f–h: submitting Phyrexian choices remains part of the same // cost payment. The prompt's object must agree with the announced root. WaitingFor::PhyrexianPayment { diff --git a/crates/engine/src/game/casting.rs b/crates/engine/src/game/casting.rs index a932575ca3..7b567be050 100644 --- a/crates/engine/src/game/casting.rs +++ b/crates/engine/src/game/casting.rs @@ -15060,6 +15060,7 @@ fn auto_tap_and_pay_cost_excluding( Some(source_id), ctx, excluded_sources, + None, resume, parent, ); diff --git a/crates/engine/src/game/casting_costs.rs b/crates/engine/src/game/casting_costs.rs index fb10c8f5a8..3a7eaf1e3e 100644 --- a/crates/engine/src/game/casting_costs.rs +++ b/crates/engine/src/game/casting_costs.rs @@ -8654,6 +8654,26 @@ pub(super) fn pay_and_push_adventure( } state.pending_cast = Some(Box::new(pending)); + if payment_mode == CastPaymentMode::AutoExceptSacrificialMana { + auto_tap_non_sacrificial_mana_sources(state, player, cost, events, object_id); + if pending_cost_is_payable_from_pool(state, player) { + return finalize_automatic_mana_payment(state, player, events); + } + let options = super::mana_sources::activatable_mana_source_selections(state, player) + .into_iter() + .filter(|selection| { + selection.penalty == super::mana_sources::ManaSourcePenalty::Sacrifices + }) + .collect::>(); + if options.is_empty() { + return enter_payment_step(state, player, None, events); + } + return Ok(WaitingFor::ManaSourceSelection { + player, + options, + convoke_mode: None, + }); + } finalize_automatic_mana_payment(state, player, events) } @@ -10375,6 +10395,7 @@ pub(super) fn auto_tap_mana_sources_excluding( None, None, None, + None, ); } @@ -10416,6 +10437,7 @@ pub(super) fn auto_tap_mana_sources_with_context_and_resume( deprioritize_source, payment_context, &HashSet::new(), + None, resume, None, ); @@ -10432,6 +10454,7 @@ pub(super) fn auto_tap_mana_sources_with_context_excluding_and_resume( deprioritize_source: Option, payment_context: Option<&PaymentContext<'_>>, excluded_sources: &HashSet, + excluded_penalty: Option, resume: Option<&ManaAbilityResume>, parent: Option<&ManaAbilityCostParent>, ) { @@ -10442,6 +10465,7 @@ pub(super) fn auto_tap_mana_sources_with_context_excluding_and_resume( events, deprioritize_source, excluded_sources, + excluded_penalty, payment_context, None, None, @@ -10466,6 +10490,7 @@ pub(super) fn auto_tap_mana_sources_with_context_excluding( events, deprioritize_source, excluded_sources, + None, payment_context, None, None, @@ -10474,6 +10499,63 @@ pub(super) fn auto_tap_mana_sources_with_context_excluding( ); } +/// CR 601.2g-h + CR 605.3b: Apply the existing automatic planner while +/// excluding sacrificial activation rows. This is the safe first leg of +/// `AutoExceptSacrificialMana`; the caller retains the pending cast and offers +/// the excluded capabilities explicitly if this leg cannot finish payment. +pub(super) fn auto_tap_non_sacrificial_mana_sources( + state: &mut GameState, + player: PlayerId, + cost: &crate::types::mana::ManaCost, + events: &mut Vec, + source_id: ObjectId, +) { + let spell_meta = super::casting::build_spell_meta(state, player, source_id); + let spell_ctx = spell_meta.as_ref().map(PaymentContext::Spell); + auto_tap_mana_sources_inner( + state, + player, + cost, + events, + Some(source_id), + &HashSet::new(), + Some(mana_sources::ManaSourcePenalty::Sacrifices), + spell_ctx.as_ref(), + None, + None, + None, + None, + ); +} + +pub(super) fn pending_cost_is_payable_from_pool(state: &GameState, player: PlayerId) -> bool { + let Some(pending) = state.pending_cast.as_deref() else { + return false; + }; + let spell_meta = super::casting::build_spell_meta(state, player, pending.object_id); + let spell_ctx = spell_meta.as_ref().map(PaymentContext::Spell); + let any_color = super::casting::player_can_spend_as_any_color_for_payment( + state, + player, + Some(pending.object_id), + spell_ctx.as_ref(), + ); + let permissions = + super::static_abilities::build_cost_permission_context(state, player, any_color); + state + .players + .iter() + .find(|candidate| candidate.id == player) + .is_some_and(|candidate| { + mana_payment::can_pay_for_spell( + &candidate.mana_pool, + &pending.cost, + spell_ctx.as_ref(), + permissions, + ) + }) +} + #[derive(Debug, Clone)] pub(super) struct AutoTapSourceCache { player: PlayerId, @@ -10503,7 +10585,7 @@ pub(super) fn build_auto_tap_source_cache( crate::game::perf_counters::record_auto_tap_source_cache_build(); AutoTapSourceCache { player, - sources: collect_sorted_auto_tap_source_options(state, player, None, &HashSet::new()), + sources: collect_sorted_auto_tap_source_options(state, player, None, &HashSet::new(), None), } } @@ -10525,6 +10607,7 @@ pub(super) fn auto_tap_mana_sources_with_context_excluding_cached( events, deprioritize_source, excluded_sources, + None, payment_context, None, source_cache, @@ -10538,6 +10621,7 @@ fn collect_sorted_auto_tap_source_options( player: PlayerId, deprioritize_source: Option, excluded_sources: &HashSet, + excluded_penalty: Option, ) -> Vec { use crate::types::card_type::{CoreType, Supertype}; @@ -10593,6 +10677,7 @@ fn collect_sorted_auto_tap_source_options( } }) .flatten() + .filter(|option| excluded_penalty.is_none_or(|penalty| option.penalty != penalty)) .collect(); // CR 605.3b: Auto-tap sort key. Tier layout (the enum factors the two @@ -10675,11 +10760,13 @@ fn cached_auto_tap_sources<'a>( player: PlayerId, deprioritize_source: Option, excluded_sources: &HashSet, + excluded_penalty: Option, sub_cost_demand: Option<&crate::game::mana_payment::ColorDemand>, ) -> Option<&'a [ManaSourceOption]> { let cache = source_cache?; if cache.is_for_player(player) && excluded_sources.is_empty() + && excluded_penalty.is_none() && sub_cost_demand.is_none() && deprioritize_source.is_none_or(|source_id| !cache.contains_source(source_id)) { @@ -10699,6 +10786,7 @@ fn auto_tap_mana_sources_inner( events: &mut Vec, deprioritize_source: Option, excluded_sources: &HashSet, + excluded_penalty: Option, payment_context: Option<&PaymentContext<'_>>, sub_cost_demand: Option<&crate::game::mana_payment::ColorDemand>, source_cache: Option<&AutoTapSourceCache>, @@ -10756,6 +10844,7 @@ fn auto_tap_mana_sources_inner( player, deprioritize_source, excluded_sources, + excluded_penalty, sub_cost_demand, ) { cached @@ -10765,6 +10854,7 @@ fn auto_tap_mana_sources_inner( player, deprioritize_source, excluded_sources, + excluded_penalty, ); &available_buf }; @@ -11111,6 +11201,7 @@ fn auto_tap_mana_sources_inner( events, Some(option.object_id), excluded, + excluded_penalty, Some(&activation_ctx), demand.as_ref(), None, @@ -12871,6 +12962,7 @@ pub(super) fn maybe_pause_for_phyrexian_choice( Some(source_id), payment_context, excluded_sources, + None, resume, None, ); diff --git a/crates/engine/src/game/costs.rs b/crates/engine/src/game/costs.rs index 5174665a0a..48dfdb0649 100644 --- a/crates/engine/src/game/costs.rs +++ b/crates/engine/src/game/costs.rs @@ -423,6 +423,11 @@ fn effect_pay_cost_mana_resume( if let WaitingFor::ManaPayment { player, convoke_mode, + } + | WaitingFor::ManaSourceSelection { + player, + convoke_mode, + .. } = &state.waiting_for { return Some(ManaAbilityResume::ManaPayment { diff --git a/crates/engine/src/game/derived.rs b/crates/engine/src/game/derived.rs index 314a726d0d..f353b01279 100644 --- a/crates/engine/src/game/derived.rs +++ b/crates/engine/src/game/derived.rs @@ -289,6 +289,9 @@ pub fn derive_display_state(state: &mut GameState) { state.has_pending_cast = state.waiting_for.has_pending_cast() || (matches!(state.waiting_for, WaitingFor::DistributeAmong { .. }) && state.pending_cast.is_some()); + state.allows_cancel_cast = state.waiting_for.allows_cancel_cast() + || (matches!(state.waiting_for, WaitingFor::DistributeAmong { .. }) + && state.pending_cast.is_some()); // Invariant: the two storage sites for "am I mid-cast" must agree. If // `waiting_for` says we're mid-cast, `GameState::pending_cast` must be diff --git a/crates/engine/src/game/effects/prepare.rs b/crates/engine/src/game/effects/prepare.rs index 9998257446..de3bbc98a4 100644 --- a/crates/engine/src/game/effects/prepare.rs +++ b/crates/engine/src/game/effects/prepare.rs @@ -344,7 +344,9 @@ fn mark_prepare_copy_cancel_rollback( if matches!( waiting, - WaitingFor::ManaPayment { .. } | WaitingFor::PhyrexianPayment { .. } + WaitingFor::ManaPayment { .. } + | WaitingFor::ManaSourceSelection { .. } + | WaitingFor::PhyrexianPayment { .. } ) { if let Some(pending) = state.pending_cast.as_mut() { debug_assert_eq!( diff --git a/crates/engine/src/game/engine.rs b/crates/engine/src/game/engine.rs index 7ff86d769a..d92b89085d 100644 --- a/crates/engine/src/game/engine.rs +++ b/crates/engine/src/game/engine.rs @@ -4477,6 +4477,48 @@ fn apply_action( ); waiting_for } + (WaitingFor::Priority { player }, GameAction::ActivateManaSource { selection }) => { + if state.priority_player + != turn_control::authorized_submitter_for_player(state, *player) + { + return Err(EngineError::NotYourPriority); + } + if state + .objects + .get(&selection.source.object_id) + .is_some_and(|object| { + object + .card_types + .core_types + .contains(&crate::types::card_type::CoreType::Land) + }) + { + return Err(EngineError::ActionNotAllowed( + "Land mana abilities use TapLandForMana".to_string(), + )); + } + let events_before = events.len(); + let waiting_for = mana_sources::activate_mana_source_selection( + state, + *player, + &selection, + &mut events, + ManaAbilityResume::Priority, + )?; + triggers::resolve_tap_mana_triggers_inline(state, &mut events, events_before); + if let Some(ability_index) = selection.ability_index { + record_mana_loop_action_step( + state, + *player, + selection.source.object_id, + crate::types::game_state::LoopAction::Activate { + source_id: selection.source.object_id, + ability_index, + }, + ); + } + waiting_for + } (WaitingFor::Priority { player }, GameAction::UntapLandForMana { object_id }) => { if state.priority_player != turn_control::authorized_submitter_for_player(state, *player) @@ -6055,6 +6097,49 @@ fn apply_action( None => WaitingFor::Priority { player }, } } + ( + WaitingFor::ManaSourceSelection { + player, + options, + convoke_mode, + }, + GameAction::BackToManaPayment, + ) => { + // The selection window never consumes mana or changes pins. Restore + // the exact payment state rather than re-running the planner. + let _ = options; + WaitingFor::ManaPayment { + player: *player, + convoke_mode: *convoke_mode, + } + } + ( + WaitingFor::ManaSourceSelection { + player, + options, + convoke_mode, + }, + GameAction::ActivateManaSource { selection }, + ) => { + if !options.contains(&selection) { + return Err(EngineError::ActionNotAllowed( + "Mana source was not offered for this payment".to_string(), + )); + } + let events_before = events.len(); + let waiting_for = mana_sources::activate_mana_source_selection( + state, + *player, + &selection, + &mut events, + ManaAbilityResume::ManaPayment { + outer_player: Some(*player), + convoke_mode: *convoke_mode, + }, + )?; + triggers::resolve_tap_mana_triggers_inline(state, &mut events, events_before); + waiting_for + } (WaitingFor::ChooseXValue { player, .. }, GameAction::CancelCast) => { // CR 601.2f + CR 601.2i: Caster may back out before committing to an // X value. Pop the stack entry placed at announcement and restore. diff --git a/crates/engine/src/game/interaction.rs b/crates/engine/src/game/interaction.rs index 68a1a2b932..51f9342f35 100644 --- a/crates/engine/src/game/interaction.rs +++ b/crates/engine/src/game/interaction.rs @@ -248,7 +248,9 @@ fn human_response_model(waiting_for: &WaitingFor, semantic_owner: PlayerId) -> H | WaitingFor::CommanderZoneChoice { .. } | WaitingFor::UntapChoice { .. } => HumanResponseModel::DirectChoices, WaitingFor::BetweenGamesSideboard { .. } => HumanResponseModel::SideboardPartition, - WaitingFor::ManaPayment { .. } => HumanResponseModel::DirectChoices, + WaitingFor::ManaPayment { .. } | WaitingFor::ManaSourceSelection { .. } => { + HumanResponseModel::DirectChoices + } WaitingFor::LoopShortcut { .. } => HumanResponseModel::LoopShortcut, WaitingFor::Priority { .. } | WaitingFor::MeldPairChoice { .. } @@ -334,6 +336,7 @@ fn classify_waiting_for(waiting_for: &WaitingFor) -> WaitingClassification { Some(InteractionSlotKind::OpeningBottom), ), WaitingFor::ManaPayment { .. } + | WaitingFor::ManaSourceSelection { .. } | WaitingFor::AssistPayment { .. } | WaitingFor::DefilerPayment { .. } | WaitingFor::UnlessPayment { .. } @@ -2008,6 +2011,20 @@ fn direct_choice_projection( } mana_payment_direct_actions(state, *player, *convoke_mode)? } + WaitingFor::ManaSourceSelection { + player, options, .. + } => { + if *player != semantic_owner { + return Err(InteractionReasonCode::InvalidAuthorityState); + } + let mut actions = options + .iter() + .cloned() + .map(|selection| GameAction::ActivateManaSource { selection }) + .collect::>(); + actions.push(GameAction::BackToManaPayment); + actions + } WaitingFor::PrecastCopyShortcutOffer { epoch, route_count, .. } => { @@ -3351,6 +3368,7 @@ fn selection_projection( | WaitingFor::MeldPairChoice { .. } | WaitingFor::MeldAttackTargetChoice { .. } | WaitingFor::ManaPayment { .. } + | WaitingFor::ManaSourceSelection { .. } | WaitingFor::AssistChoosePlayer { .. } | WaitingFor::AssistPayment { .. } | WaitingFor::ChooseXValue { .. } @@ -3873,6 +3891,7 @@ fn object_property_code(property: ObjectProperty) -> InteractionObjectProperty { fn cast_payment_mode_code(mode: CastPaymentMode) -> &'static str { match mode { CastPaymentMode::Auto => "auto", + CastPaymentMode::AutoExceptSacrificialMana => "autoExceptSacrificialMana", CastPaymentMode::Manual => "manual", } } @@ -4157,7 +4176,7 @@ fn project_action_payload( GameAction::ChooseEntryAttackTarget { target } => { push_attack_target_surface(surfaces, state, target, InteractionRoleCode::AttackTarget) } - GameAction::TapLandForMana { selection } => { + GameAction::TapLandForMana { selection } | GameAction::ActivateManaSource { selection } => { let Some(player) = state .objects .get(&selection.source.object_id) @@ -4166,7 +4185,7 @@ fn project_action_payload( return; }; let Ok(option) = - mana_sources::live_land_mana_option_for_selection(state, player, selection) + mana_sources::live_mana_source_option_for_selection(state, player, selection) else { return; }; @@ -4969,6 +4988,8 @@ fn action_code(action: &GameAction) -> InteractionActionCode { GameAction::MulliganDecision { .. } => InteractionActionCode::MulliganDecision, GameAction::ReorderHand { .. } => InteractionActionCode::ReorderHand, GameAction::TapLandForMana { .. } => InteractionActionCode::TapLandForMana, + GameAction::ActivateManaSource { .. } => InteractionActionCode::ActivateManaSource, + GameAction::BackToManaPayment => InteractionActionCode::BackToManaPayment, GameAction::UntapLandForMana { .. } => InteractionActionCode::UntapLandForMana, GameAction::SpendPoolMana { .. } => InteractionActionCode::SpendPoolMana, GameAction::UnspendPoolMana { .. } => InteractionActionCode::UnspendPoolMana, @@ -5137,7 +5158,7 @@ fn actor_candidates( .filter_map(|candidate| { let mut manual = candidate.clone(); let payment_mode = manual.action.payment_mode_mut()?; - if *payment_mode != CastPaymentMode::Auto { + if *payment_mode == CastPaymentMode::Manual { return None; } *payment_mode = CastPaymentMode::Manual; diff --git a/crates/engine/src/game/mana_sources.rs b/crates/engine/src/game/mana_sources.rs index e9370ebee7..aeabe1a72a 100644 --- a/crates/engine/src/game/mana_sources.rs +++ b/crates/engine/src/game/mana_sources.rs @@ -26,8 +26,8 @@ use crate::types::events::{GameEvent, ManaTapState}; use crate::types::game_state::{GameState, ManaAbilityResume, ProductionOverride, WaitingFor}; use crate::types::identifiers::ObjectId; use crate::types::mana::{ - ManaColor, ManaCostShard, ManaPip, ManaRestriction, ManaSourceSelection, ManaType, - PaymentContext, TapsForManaSelection, + ManaColor, ManaCostShard, ManaPip, ManaRestriction, ManaSourceOutput, ManaSourceSelection, + ManaType, PaymentContext, TapsForManaSelection, }; use crate::types::player::PlayerId; use crate::types::zones::Zone; @@ -203,6 +203,7 @@ impl ManaSourceOption { source: crate::types::identifiers::ObjectIncarnationRef::from_object(source), ability_index: self.ability_index, mana_type: self.mana_type, + output: ManaSourceOutput::Concrete(self.mana_type), atomic_combination: self.atomic_combination.clone(), restrictions: self.restrictions.clone(), penalty: self.penalty, @@ -297,6 +298,10 @@ pub fn activatable_mana_actions_for_player(state: &GameState, player: PlayerId) &mana_activation_gates, ) { + // Interactive costs (for example, "Sacrifice an artifact") + // deliberately remain the ordinary activation action during a + // normal ManaPayment window. Its established cost resolver is + // the authority for the subsequent PayCost choice. actions.push(GameAction::ActivateAbility { source_id: object_id, ability_index, @@ -307,6 +312,181 @@ pub fn activatable_mana_actions_for_player(state: &GameState, player: PlayerId) actions } +/// CR 605.3a: Complete semantic capabilities for mana activation, across lands +/// and nonlands. This is intentionally separate from `GameAction` generation: +/// callers can freeze these engine-authored candidates in an interaction and +/// later require an exact fresh match before activation. +pub fn activatable_mana_source_selections( + state: &GameState, + player: PlayerId, +) -> Vec { + let aura_sources = taps_for_mana_trigger_sources(state); + let gates = mana_abilities::ManaActivationGates::compute(state); + let mut selections = Vec::new(); + + for &object_id in &state.battlefield { + let Some(object) = state.objects.get(&object_id) else { + continue; + }; + if object.controller != player { + continue; + } + + let options = current_mana_source_options(state, player, object_id, &aura_sources, &gates); + for option in options { + let Some(selection) = manual_selection_for_option(state, &option) else { + continue; + }; + if !selections.contains(&selection) { + selections.push(selection); + } + } + } + selections.sort_by(|left, right| left.cmp_stable(right)); + selections +} + +/// CR 605.3a: Return the source rows a frozen, explicit mana selection may +/// name. Auto-tap rows stay the base because they preserve land fallback and +/// tap-trigger metadata; then append every other currently legal activated +/// mana ability. Unlike the automatic planner, this includes interactive costs +/// such as "Sacrifice an artifact", because the selection is explicit and the +/// normal `PayCost` flow still resolves that cost after activation. +fn current_mana_source_options( + state: &GameState, + player: PlayerId, + object_id: ObjectId, + aura_sources: &[ObjectId], + gates: &mana_abilities::ManaActivationGates, +) -> Vec { + let Some(object) = state.objects.get(&object_id) else { + return Vec::new(); + }; + if object.zone != Zone::Battlefield || object.controller != player { + return Vec::new(); + } + + let mut options = if object.card_types.core_types.contains(&CoreType::Land) { + activatable_land_mana_options_indexed_gated(state, object_id, player, aura_sources, gates) + } else { + auto_tap_mana_options(state, object_id, player) + }; + + for (ability_index, ability) in object.abilities.iter().enumerate() { + if options + .iter() + .any(|option| option.ability_index == Some(ability_index)) + || ability.kind != AbilityKind::Activated + || !mana_abilities::is_mana_ability(ability) + || !mana_abilities::can_activate_mana_ability_now_gated( + state, + player, + object_id, + ability_index, + ability, + gates, + ) + || !activation_condition_satisfied(state, player, object_id, ability_index, ability) + { + continue; + } + + let penalty = object_mana_ability_penalty(state, object_id, ability); + let source_could_produce_two_or_more_colors = + source_could_produce_two_or_more_colors(state, object_id, player); + for row in emit_source_rows(state, player, object_id, ability_index, ability, true) { + let option = ManaSourceOption { + object_id, + ability_index: Some(ability_index), + mana_type: row.mana_type, + source_could_produce_two_or_more_colors, + penalty, + atomic_combination: row.atomic_combination, + restrictions: row.restrictions, + taps_for_mana_overrides: Vec::new(), + }; + if !options.contains(&option) { + options.push(option); + } + } + } + + options +} + +fn manual_selection_for_option( + state: &GameState, + option: &ManaSourceOption, +) -> Option { + let mut selection = option.semantic_selection(state)?; + let flexible_output = option.ability_index.is_some_and(|ability_index| { + state + .objects + .get(&option.object_id) + .and_then(|object| object.abilities.get(ability_index)) + .is_some_and(|ability| { + matches!( + &*ability.effect, + Effect::Mana { + produced: ManaProduction::AnyOneColor { .. } + | ManaProduction::AnyCombination { .. } + | ManaProduction::ChoiceAmongExiledColors { .. } + | ManaProduction::AnyOneColorAmongPermanents { .. } + | ManaProduction::OpponentLandColors { .. } + | ManaProduction::AnyTypeProduceableBy { .. } + | ManaProduction::AnyCombinationOfObjectColors { .. } + | ManaProduction::AnyInCommandersColorIdentity { .. }, + .. + } + ) + }) + }); + if flexible_output { + // The planner emits one concrete row per color, but a manual activation + // must retain the source capability and let the normal mana-choice + // resolver ask for its color. The colorless marker is intentionally + // inert while `output` is deferred and canonicalizes all planner rows. + selection.mana_type = ManaType::Colorless; + selection.output = ManaSourceOutput::DeferredColorChoice; + } + Some(selection) +} + +/// Resolve an exact generic semantic source selection from fresh candidates. +/// A deferred flexible output legitimately corresponds to several concrete +/// planner rows; they all name the same object/ability capability and therefore +/// collapse to one canonical manual selection above. +pub(crate) fn live_mana_source_option_for_selection( + state: &GameState, + player: PlayerId, + selection: &ManaSourceSelection, +) -> Result { + if !activatable_mana_source_selections(state, player).contains(selection) { + return Err(EngineError::ActionNotAllowed( + "Mana source selection is stale or no longer legal".to_string(), + )); + } + + let aura_sources = taps_for_mana_trigger_sources(state); + let gates = mana_abilities::ManaActivationGates::compute(state); + let object_id = selection.source.object_id; + let options = current_mana_source_options(state, player, object_id, &aura_sources, &gates); + let mut matches = options + .into_iter() + .filter(|option| manual_selection_for_option(state, option).as_ref() == Some(selection)); + let option = matches.next().ok_or_else(|| { + EngineError::ActionNotAllowed( + "Mana source selection is stale or no longer legal".to_string(), + ) + })?; + if matches.any(|other| other.ability_index != option.ability_index) { + return Err(EngineError::InvalidAction( + "Mana source selection ambiguously matches multiple live capabilities".to_string(), + )); + } + Ok(option) +} + /// Re-enumerate the live land-mana rows and resolve one exact semantic /// selection to its current engine-owned option. Zero matches means the /// submitted choice is stale or illegal; multiple matches mean the selector @@ -551,6 +731,27 @@ pub(crate) fn activate_mana_source_option( option: &ManaSourceOption, events: &mut Vec, resume: ManaAbilityResume, +) -> Result { + activate_mana_source_option_with_output( + state, + player, + option, + ManaSourceOutput::Concrete(option.mana_type), + events, + resume, + ) +} + +/// Activate a live source using the preserved output provenance. Deferred +/// flexible outputs deliberately pass no production override, so the existing +/// ManaChoice flow remains the single authority for the eventual color choice. +pub(crate) fn activate_mana_source_option_with_output( + state: &mut GameState, + player: PlayerId, + option: &ManaSourceOption, + output: ManaSourceOutput, + events: &mut Vec, + resume: ManaAbilityResume, ) -> Result { for (trigger_ref, production_override) in &option.taps_for_mana_overrides { state @@ -569,8 +770,12 @@ pub(crate) fn activate_mana_source_option( "Selected mana ability is no longer available".to_string(), ) })?; - let production_override = - super::casting_costs::production_override_for_option(&ability, option); + let production_override = match output { + ManaSourceOutput::Concrete(_) => { + super::casting_costs::production_override_for_option(&ability, option) + } + ManaSourceOutput::DeferredColorChoice => None, + }; mana_abilities::activate_mana_ability( state, option.object_id, @@ -630,6 +835,28 @@ pub(crate) fn activate_mana_source_option( Ok(waiting_for) } +/// CR 605.3a-b: Revalidate and activate a generic mana capability. The caller +/// supplies its resume because this entry point is shared by priority and a +/// spell's mana-payment interaction; it never translates back into +/// `ActivateAbility`, preserving the frozen selection identity. +pub(crate) fn activate_mana_source_selection( + state: &mut GameState, + player: PlayerId, + selection: &ManaSourceSelection, + events: &mut Vec, + resume: ManaAbilityResume, +) -> Result { + let option = live_mana_source_option_for_selection(state, player, selection)?; + activate_mana_source_option_with_output( + state, + player, + &option, + selection.output, + events, + resume, + ) +} + /// CR 107.6 + CR 302.6: True when the cost includes the untap symbol ({Q}). /// Like {T}, a {Q} cost on a creature is gated by summoning sickness (CR 302.6 /// names both symbols) and requires the source to currently be tapped. Matches a diff --git a/crates/engine/src/game/replay.rs b/crates/engine/src/game/replay.rs index 466c72d61a..107a8de281 100644 --- a/crates/engine/src/game/replay.rs +++ b/crates/engine/src/game/replay.rs @@ -212,7 +212,7 @@ mod tests { use crate::types::game_state::{ProductionOverride, WaitingFor}; use crate::types::identifiers::{ObjectId, ObjectIncarnationRef}; use crate::types::mana::{ - ManaSourcePenalty, ManaSourceSelection, ManaType, TapsForManaSelection, + ManaSourceOutput, ManaSourcePenalty, ManaSourceSelection, ManaType, TapsForManaSelection, }; use crate::types::match_config::MatchConfig; @@ -267,6 +267,7 @@ mod tests { source, ability_index: None, mana_type: ManaType::Green, + output: crate::types::mana::ManaSourceOutput::Concrete(ManaType::Green), atomic_combination: None, restrictions: Vec::new(), penalty: ManaSourcePenalty::None, @@ -291,6 +292,34 @@ mod tests { assert_eq!(restored.actions[0].action, action); } + #[test] + fn legacy_colored_tap_land_action_preserves_its_selected_output() { + let action: GameAction = serde_json::from_value(serde_json::json!({ + "type": "TapLandForMana", + "data": { + "selection": { + "source": { "object_id": 7, "incarnation": 3 }, + "ability_index": null, + "mana_type": "Green", + "atomic_combination": null, + "restrictions": [], + "penalty": "None", + "taps_for_mana": [] + } + } + })) + .expect("pre-output replay actions should deserialize"); + + let GameAction::TapLandForMana { selection } = action else { + panic!("legacy action must retain its TapLandForMana shape"); + }; + assert_eq!( + selection.output, + ManaSourceOutput::Concrete(ManaType::Green), + "a legacy colored row selected its mana_type, not colorless mana" + ); + } + #[test] fn replay_player_reconstructs_every_recorded_index() { let header = two_player_header(99); diff --git a/crates/engine/src/game/scenario.rs b/crates/engine/src/game/scenario.rs index 819c29200b..d9df01b955 100644 --- a/crates/engine/src/game/scenario.rs +++ b/crates/engine/src/game/scenario.rs @@ -1704,6 +1704,7 @@ impl GameRunner { WaitingFor::MulliganDecision { .. } => "MulliganDecision", WaitingFor::OpeningHandBottomCards { .. } => "OpeningHandBottomCards", WaitingFor::ManaPayment { .. } => "ManaPayment", + WaitingFor::ManaSourceSelection { .. } => "ManaSourceSelection", WaitingFor::TargetSelection { .. } => "TargetSelection", WaitingFor::DeclareAttackers { .. } => "DeclareAttackers", WaitingFor::DeclareBlockers { .. } => "DeclareBlockers", @@ -2475,6 +2476,16 @@ impl<'a> SpellCast<'a> { // CR 601.2h: finalize the (now fully convoke-paid) cost. act_collect(runner, GameAction::PassPriority, &mut events)?; } + WaitingFor::ManaSourceSelection { options, .. } => { + let selection = options.first().cloned().unwrap_or_else(|| { + panic!("ManaSourceSelection must offer at least one source") + }); + act_collect( + runner, + GameAction::ActivateManaSource { selection }, + &mut events, + )?; + } // CR 601.2c: declare one target per slot, in written order. WaitingFor::TargetSelection { pending_cast, @@ -2724,6 +2735,7 @@ fn waiting_for_variant_name(waiting: &WaitingFor) -> &'static str { // borrow-free match. Kept in sync with `GameRunner::waiting_for_kind`. match waiting { WaitingFor::ManaPayment { .. } => "ManaPayment", + WaitingFor::ManaSourceSelection { .. } => "ManaSourceSelection", WaitingFor::ChooseXValue { .. } => "ChooseXValue", WaitingFor::TargetSelection { .. } => "TargetSelection", WaitingFor::MultiTargetSelection { .. } => "MultiTargetSelection", diff --git a/crates/engine/src/types/action_stable_order.rs b/crates/engine/src/types/action_stable_order.rs index 27eb8c2719..016086f7a2 100644 --- a/crates/engine/src/types/action_stable_order.rs +++ b/crates/engine/src/types/action_stable_order.rs @@ -227,6 +227,18 @@ fn cmp_payload(a: &GameAction, b: &GameAction) -> Ordering { }; a0.cmp_stable(b0) } + GameAction::ActivateManaSource { selection: a0 } => { + let GameAction::ActivateManaSource { selection: b0 } = b else { + unreachable!("cmp_payload: same-variant invariant"); + }; + a0.cmp_stable(b0) + } + GameAction::BackToManaPayment => { + let GameAction::BackToManaPayment = b else { + unreachable!("cmp_payload: same-variant invariant"); + }; + Ordering::Equal + } GameAction::UntapLandForMana { object_id: a0 } => { let GameAction::UntapLandForMana { object_id: b0 } = b else { unreachable!("cmp_payload: same-variant invariant"); diff --git a/crates/engine/src/types/actions.rs b/crates/engine/src/types/actions.rs index 42d795b702..0d338c8b76 100644 --- a/crates/engine/src/types/actions.rs +++ b/crates/engine/src/types/actions.rs @@ -256,6 +256,15 @@ pub enum GameAction { TapLandForMana { selection: ManaSourceSelection, }, + /// CR 605.3a: Activate one exact engine-authored mana-source capability. + /// Unlike the legacy land-only action, this covers mana abilities on every + /// permanent type and preserves the selected output provenance. + ActivateManaSource { + selection: ManaSourceSelection, + }, + /// Return from a sacrificial-mana choice to the exact saved payment state + /// without re-planning or mutating the mana pool. + BackToManaPayment, /// CR 605.3a: Undo a manual mana ability activation — untap source, remove produced mana. /// Only valid for lands in `lands_tapped_for_mana` whose mana hasn't been spent. UntapLandForMana { @@ -1497,6 +1506,7 @@ impl GameAction { matches!( self, GameAction::TapLandForMana { .. } + | GameAction::ActivateManaSource { .. } | GameAction::UntapLandForMana { .. } // CR 118.3a: pinning/unpinning a pool unit is a mana-payment-window // action; classifying it here keeps it out of AI priority-action @@ -1551,6 +1561,7 @@ impl GameAction { | GameAction::CastSpellAsMadness { object_id, .. } => Some(*object_id), GameAction::ActivateAbility { source_id, .. } => Some(*source_id), GameAction::TapLandForMana { selection } => Some(selection.source.object_id), + GameAction::ActivateManaSource { selection } => Some(selection.source.object_id), GameAction::UntapLandForMana { object_id } => Some(*object_id), // CR 118.3a: act on a pool pip, not a battlefield object. GameAction::SpendPoolMana { .. } | GameAction::UnspendPoolMana { .. } => None, @@ -1870,6 +1881,9 @@ mod tests { }, ability_index: None, mana_type: crate::types::mana::ManaType::Green, + output: crate::types::mana::ManaSourceOutput::Concrete( + crate::types::mana::ManaType::Green, + ), atomic_combination: None, restrictions: Vec::new(), penalty: crate::types::mana::ManaSourcePenalty::None, diff --git a/crates/engine/src/types/game_state.rs b/crates/engine/src/types/game_state.rs index 14b04f9678..aa840d6b6b 100644 --- a/crates/engine/src/types/game_state.rs +++ b/crates/engine/src/types/game_state.rs @@ -5352,6 +5352,10 @@ fn default_copy_retarget_effect_kind() -> EffectKind { pub enum CastPaymentMode { #[default] Auto, + /// CR 601.2g-h: auto-pay every safe mana row, but stop before an + /// activation whose cost sacrifices a permanent so its controller makes + /// that irreversible payment decision explicitly. + AutoExceptSacrificialMana, Manual, } @@ -6138,6 +6142,16 @@ pub enum ManaAbilityResume { #[serde(default, skip_serializing_if = "Option::is_none")] convoke_mode: Option, }, + /// CR 601.2g-h + CR 605.3b: automatic payment reached a mana source that + /// sacrifices a permanent. The options are the frozen, engine-authored + /// semantic capability snapshot; submission revalidates one exact current + /// candidate before it can mutate the payment. + ManaSourceSelection { + player: PlayerId, + options: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + convoke_mode: Option, + }, UnlessPayment { /// The player who owns the outer unless-payment poll. It can differ /// from the controller of a helper mana source activated while paying. @@ -9838,6 +9852,7 @@ impl WaitingFor { WaitingFor::MulliganDecision { .. } => "MulliganDecision", WaitingFor::OpeningHandBottomCards { .. } => "OpeningHandBottomCards", WaitingFor::ManaPayment { .. } => "ManaPayment", + WaitingFor::ManaSourceSelection { .. } => "ManaSourceSelection", WaitingFor::ChooseXValue { .. } => "ChooseXValue", WaitingFor::TargetSelection { .. } => "TargetSelection", WaitingFor::DeclareAttackers { .. } => "DeclareAttackers", @@ -9988,6 +10003,7 @@ impl WaitingFor { | WaitingFor::MeldPairChoice { player, .. } | WaitingFor::MeldAttackTargetChoice { player, .. } | WaitingFor::ManaPayment { player, .. } + | WaitingFor::ManaSourceSelection { player, .. } | WaitingFor::ChooseXValue { player, .. } | WaitingFor::TargetSelection { player, .. } | WaitingFor::DeclareAttackers { player, .. } @@ -10148,12 +10164,13 @@ impl WaitingFor { /// /// Runtime drift detector: the `debug_assert!` in `game::derived` trips /// in tests if a new variant populates `GameState::pending_cast` without - /// being covered here (or by the `ManaPayment` exception in + /// being covered here (or by an external payment-state exception in /// `has_pending_cast`). That is the practical safeguard — the `_ => None` /// wildcard below does not compile-enforce variant coverage on its own. /// - /// Note: `ManaPayment` is the one casting-flow variant that does NOT embed - /// its `PendingCast`. It reads from `GameState::pending_cast` instead so + /// Note: `ManaPayment` and `ManaSourceSelection` are casting-flow variants + /// that do NOT embed their `PendingCast`. They read from + /// `GameState::pending_cast` instead so /// multiplayer visibility filtering (`game::visibility`) can clear /// mid-payment detail for opponents while preserving the public "spell on /// the stack" view elsewhere. `has_pending_cast()` accounts for this. @@ -10228,8 +10245,8 @@ impl WaitingFor { /// Whether this state is part of the casting flow and can be backed out of /// with `CancelCast` (CR 601.2). /// - /// Derived from `pending_cast_ref()` plus the single `ManaPayment` - /// exception (which externalizes its `PendingCast` into + /// Derived from `pending_cast_ref()` plus the external payment states + /// (which externalize their `PendingCast` into /// `GameState::pending_cast`). Centralizing the predicate here guarantees /// that every variant carrying a `PendingCast` is covered — drift between /// data model and predicate is structurally prevented. @@ -10243,9 +10260,18 @@ impl WaitingFor { self.pending_cast_ref().is_some() || matches!( self, - WaitingFor::ManaPayment { .. } | WaitingFor::PhyrexianPayment { .. } + WaitingFor::ManaPayment { .. } + | WaitingFor::ManaSourceSelection { .. } + | WaitingFor::PhyrexianPayment { .. } ) } + + /// CR 601.2i: Whether the current prompt permits withdrawing the pending + /// spell. Kept distinct from `has_pending_cast`: a cast can be pending at + /// a mandatory choice without granting a cancellation action. + pub fn allows_cancel_cast(&self) -> bool { + self.has_pending_cast() && !matches!(self, WaitingFor::ManaSourceSelection { .. }) + } } /// CR 102.1 + CR 500.1: which turn boundary ends an auto-pass session. @@ -11482,6 +11508,11 @@ pub struct GameState { /// frontend doesn't need to maintain a parallel list of casting states. #[serde(skip_deserializing, default)] pub has_pending_cast: bool, + /// CR 601.2i: display-only authority for whether the current pending cast + /// may be withdrawn. Separate from `has_pending_cast` so choice prompts + /// can expose an in-flight cast without encouraging an illegal cancel. + #[serde(skip_deserializing, default)] + pub allows_cancel_cast: bool, pub lands_played_this_turn: u8, pub max_lands_per_turn: u8, pub priority_pass_count: u8, @@ -16615,6 +16646,7 @@ impl GameState { next_interaction_serial: default_interaction_serial(), active_interaction_slots: Vec::new(), has_pending_cast: false, + allows_cancel_cast: false, lands_played_this_turn: 0, max_lands_per_turn: 1, priority_pass_count: 0, @@ -18064,6 +18096,7 @@ fn _gamestate_partition_is_total(s: &GameState) { next_interaction_serial: _, active_interaction_slots: _, has_pending_cast: _, + allows_cancel_cast: _, lands_played_this_turn: _, max_lands_per_turn: _, priority_pass_count: _, @@ -21409,6 +21442,11 @@ mod tests { player: PlayerId(0), convoke_mode: None, })); + variants.push(Box::new(WaitingFor::ManaSourceSelection { + player: PlayerId(0), + options: Vec::new(), + convoke_mode: None, + })); variants.push(Box::new(WaitingFor::DeclareAttackers { player: PlayerId(0), valid_attacker_ids: vec![], @@ -21785,6 +21823,14 @@ mod tests { }; assert!(mana_payment.pending_cast_ref().is_none()); assert!(mana_payment.has_pending_cast()); + + let source_selection = WaitingFor::ManaSourceSelection { + player: PlayerId(0), + options: Vec::new(), + convoke_mode: None, + }; + assert!(source_selection.has_pending_cast()); + assert!(!source_selection.allows_cancel_cast()); } #[test] diff --git a/crates/engine/src/types/interaction.rs b/crates/engine/src/types/interaction.rs index 830f083bb9..b26e041cd2 100644 --- a/crates/engine/src/types/interaction.rs +++ b/crates/engine/src/types/interaction.rs @@ -500,6 +500,8 @@ pub enum InteractionActionCode { MulliganDecision, ReorderHand, TapLandForMana, + ActivateManaSource, + BackToManaPayment, UntapLandForMana, SpendPoolMana, UnspendPoolMana, diff --git a/crates/engine/src/types/mana.rs b/crates/engine/src/types/mana.rs index be6749780d..515ae98c9d 100644 --- a/crates/engine/src/types/mana.rs +++ b/crates/engine/src/types/mana.rs @@ -669,6 +669,19 @@ pub enum ManaSourcePenalty { Sacrifices, } +/// CR 106.1a: The output choice captured for a mana-source activation. +/// +/// `Concrete` is the planner-selected output used by automatic payment. A +/// manually selected flexible producer instead retains `DeferredColorChoice`, +/// so the normal mana-choice interaction chooses its color at activation time +/// rather than silently committing to an arbitrary planner row. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(tag = "type", content = "data")] +pub enum ManaSourceOutput { + Concrete(ManaType), + DeferredColorChoice, +} + /// Exact identity of one triggered mana ability that augments a land's own /// production when that land is tapped for mana. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -678,24 +691,62 @@ pub struct TapsForManaSelection { pub production_override: ProductionOverride, } -/// Stable semantic selection for one currently activatable land-mana row. +/// Stable semantic selection for one currently activatable mana-source row. /// /// The action carries every field that can change legality or resolution, but /// the reducer never trusts those fields directly: it enumerates current live /// options and requires one exact semantic match before activating that live /// option. `ObjectIncarnationRef` prevents a stale choice from rebinding to an /// object that left and re-entered the battlefield (CR 400.7). -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct ManaSourceSelection { pub source: ObjectIncarnationRef, pub ability_index: Option, pub mana_type: ManaType, + pub output: ManaSourceOutput, pub atomic_combination: Option>, pub restrictions: Vec, pub penalty: ManaSourcePenalty, pub taps_for_mana: Vec, } +impl<'de> Deserialize<'de> for ManaSourceSelection { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + struct WireSelection { + source: ObjectIncarnationRef, + ability_index: Option, + mana_type: ManaType, + #[serde(default)] + output: Option, + atomic_combination: Option>, + restrictions: Vec, + penalty: ManaSourcePenalty, + taps_for_mana: Vec, + } + + let wire = WireSelection::deserialize(deserializer)?; + Ok(Self { + source: wire.source, + ability_index: wire.ability_index, + mana_type: wire.mana_type, + // `TapLandForMana` actions saved before output provenance existed + // selected this exact row's `mana_type`; preserve that choice on + // replay instead of silently turning every old colored row colorless. + output: wire + .output + .unwrap_or(ManaSourceOutput::Concrete(wire.mana_type)), + atomic_combination: wire.atomic_combination, + restrictions: wire.restrictions, + penalty: wire.penalty, + taps_for_mana: wire.taps_for_mana, + }) + } +} + impl ManaSourceSelection { /// Stable total order for the action wire. The non-`Ord` semantic fields /// are compared by explicit closed-enum comparators below; debug strings or @@ -705,6 +756,7 @@ impl ManaSourceSelection { .cmp(&other.source) .then_with(|| self.ability_index.cmp(&other.ability_index)) .then_with(|| self.mana_type.cmp(&other.mana_type)) + .then_with(|| cmp_mana_source_output(self.output, other.output)) .then_with(|| self.atomic_combination.cmp(&other.atomic_combination)) .then_with(|| cmp_mana_restriction_slices(&self.restrictions, &other.restrictions)) .then_with(|| cmp_mana_source_penalty(self.penalty, other.penalty)) @@ -712,6 +764,21 @@ impl ManaSourceSelection { } } +fn cmp_mana_source_output(left: ManaSourceOutput, right: ManaSourceOutput) -> std::cmp::Ordering { + match (left, right) { + (ManaSourceOutput::Concrete(a), ManaSourceOutput::Concrete(b)) => a.cmp(&b), + (ManaSourceOutput::DeferredColorChoice, ManaSourceOutput::DeferredColorChoice) => { + std::cmp::Ordering::Equal + } + (ManaSourceOutput::Concrete(_), ManaSourceOutput::DeferredColorChoice) => { + std::cmp::Ordering::Less + } + (ManaSourceOutput::DeferredColorChoice, ManaSourceOutput::Concrete(_)) => { + std::cmp::Ordering::Greater + } + } +} + fn cmp_slice_by( left: &[T], right: &[T], diff --git a/crates/engine/src/types/mod.rs b/crates/engine/src/types/mod.rs index 359b440d71..7e3c175c8b 100644 --- a/crates/engine/src/types/mod.rs +++ b/crates/engine/src/types/mod.rs @@ -63,8 +63,8 @@ pub use keywords::{Keyword, PartnerType, ProtectionTarget}; pub use layers::{ActiveContinuousEffect, Layer}; pub use log::{GameLogEntry, LogCategory, LogSegment}; pub use mana::{ - ManaColor, ManaCost, ManaCostShard, ManaPool, ManaRestriction, ManaSourcePenalty, - ManaSourceSelection, ManaType, ManaUnit, SpellMeta, TapsForManaSelection, + ManaColor, ManaCost, ManaCostShard, ManaPool, ManaRestriction, ManaSourceOutput, + ManaSourcePenalty, ManaSourceSelection, ManaType, ManaUnit, SpellMeta, TapsForManaSelection, }; pub use match_config::{ BetweenGamesPrompt, DeckCardCount, MatchConfig, MatchPhase, MatchScore, MatchType, diff --git a/crates/engine/tests/integration/main.rs b/crates/engine/tests/integration/main.rs index 446cbd2097..f708b60fb3 100644 --- a/crates/engine/tests/integration/main.rs +++ b/crates/engine/tests/integration/main.rs @@ -1134,6 +1134,7 @@ mod s07_coiling_rebirth_full_cast; mod s07_create_token_sequence_middle; mod s07_malamet_castpath; mod s07_this_way_conditions; +mod sacrificial_mana_choice; mod sakashima_of_a_thousand_faces_retains_other_abilities; mod same_is_true_type_statics; mod sandman_reanimate_self_and_land_s25; diff --git a/crates/engine/tests/integration/sacrificial_mana_choice.rs b/crates/engine/tests/integration/sacrificial_mana_choice.rs new file mode 100644 index 0000000000..12ef9a6510 --- /dev/null +++ b/crates/engine/tests/integration/sacrificial_mana_choice.rs @@ -0,0 +1,341 @@ +use engine::ai_support::candidate_actions; +use engine::game::scenario::{GameRunner, GameScenario}; +use engine::types::ability::{ + AbilityCost, AbilityDefinition, AbilityKind, Effect, ManaContribution, ManaProduction, + QuantityExpr, SacrificeCost, TargetFilter, TypeFilter, TypedFilter, +}; +use engine::types::actions::GameAction; +use engine::types::game_state::{CastPaymentMode, ManaChoice, PayCostKind, WaitingFor}; +use engine::types::identifiers::ObjectId; +use engine::types::mana::{ManaColor, ManaCost, ManaType}; +use engine::types::phase::Phase; +use engine::types::player::PlayerId; +use engine::types::zones::Zone; + +const P0: PlayerId = PlayerId(0); + +fn sacrificial_mana_ability(cost: AbilityCost, color: ManaColor) -> AbilityDefinition { + AbilityDefinition::new( + AbilityKind::Activated, + Effect::Mana { + produced: ManaProduction::Fixed { + colors: vec![color], + contribution: ManaContribution::Base, + }, + restrictions: vec![], + grants: vec![], + expiry: None, + target: None, + }, + ) + .cost(cost) +} + +fn any_one_color_sacrificial_mana_ability(cost: AbilityCost) -> AbilityDefinition { + AbilityDefinition::new( + AbilityKind::Activated, + Effect::Mana { + produced: ManaProduction::AnyOneColor { + count: QuantityExpr::Fixed { value: 1 }, + color_options: vec![ManaColor::Black, ManaColor::Red], + contribution: ManaContribution::Base, + }, + restrictions: vec![], + grants: vec![], + expiry: None, + target: None, + }, + ) + .cost(cost) +} + +fn begin_sacrificial_payment(runner: &mut GameRunner, spell: ObjectId) { + let card_id = runner.state().objects[&spell].card_id; + runner + .act(GameAction::CastSpell { + object_id: spell, + card_id, + targets: vec![], + payment_mode: CastPaymentMode::AutoExceptSacrificialMana, + }) + .expect("the production cast path should stop before sacrificial mana"); +} + +fn offered_selection( + runner: &GameRunner, + source: ObjectId, +) -> engine::types::mana::ManaSourceSelection { + let WaitingFor::ManaSourceSelection { options, .. } = &runner.state().waiting_for else { + panic!( + "expected sacrificial mana prompt from the cast path, got {:?}", + runner.state().waiting_for + ); + }; + options + .iter() + .find(|selection| selection.source.object_id == source) + .cloned() + .expect("the prompt should retain the sacrificial source's exact capability") +} + +fn generic_spell(scenario: &mut GameScenario) -> ObjectId { + scenario + .add_spell_to_hand(P0, "Sacrificial Mana Payment Witness", true) + .with_mana_cost(ManaCost::generic(1)) + .id() +} + +/// A self-sacrificing mana ability is offered only after the real cast pipeline +/// has exhausted non-sacrificial payment rows. +#[test] +fn self_sacrificing_mana_source_pays_a_production_cast() { + let mut scenario = GameScenario::new(); + scenario.at_phase(Phase::PreCombatMain); + let spell = generic_spell(&mut scenario); + let source = scenario + .add_creature(P0, "Blood Pet Witness", 1, 1) + .with_ability_definition(sacrificial_mana_ability( + AbilityCost::Sacrifice(SacrificeCost::count(TargetFilter::SelfRef, 1)), + ManaColor::Black, + )) + .id(); + let mut runner = scenario.build(); + + begin_sacrificial_payment(&mut runner, spell); + let selection = offered_selection(&runner, source); + runner + .act(GameAction::ActivateManaSource { selection }) + .expect("the offered self-sacrificing source should activate during payment"); + + assert_eq!(runner.state().objects[&source].zone, Zone::Graveyard); + assert_eq!( + runner.state().players[P0.0 as usize] + .mana_pool + .count_color(ManaType::Black), + 1, + "the activated source's mana reaches the pending spell payment" + ); + assert!(matches!( + runner.state().waiting_for, + WaitingFor::ManaPayment { .. } + )); + + runner + .act(GameAction::PassPriority) + .expect("the selected mana should pay the spell through the ordinary payment reducer"); + assert!(matches!( + runner.state().waiting_for, + WaitingFor::Priority { .. } + )); + assert_eq!(runner.state().objects[&spell].zone, Zone::Stack); +} + +/// A frozen source selection must preserve an AnyOneColor activation's normal +/// color prompt, then resume and finish the original cast. +#[test] +fn any_one_color_self_sacrifice_selection_completes_production_cast() { + let mut scenario = GameScenario::new(); + scenario.at_phase(Phase::PreCombatMain); + let spell = generic_spell(&mut scenario); + let source = scenario + .add_creature(P0, "Gold Witness", 1, 1) + .with_ability_definition(any_one_color_sacrificial_mana_ability( + AbilityCost::Sacrifice(SacrificeCost::count(TargetFilter::SelfRef, 1)), + )) + .id(); + let mut runner = scenario.build(); + + begin_sacrificial_payment(&mut runner, spell); + let selection = offered_selection(&runner, source); + runner + .act(GameAction::ActivateManaSource { selection }) + .expect("the frozen self-sacrifice selection should enter the color prompt"); + assert!(matches!( + runner.state().waiting_for, + WaitingFor::ChooseManaColor { .. } + )); + + runner + .act(GameAction::ChooseManaColor { + choice: ManaChoice::SingleColor(ManaType::Red), + count: 1, + }) + .expect("choosing the source's color should resume the pending payment"); + assert_eq!(runner.state().objects[&source].zone, Zone::Graveyard); + assert!(matches!( + runner.state().waiting_for, + WaitingFor::ManaPayment { .. } + )); + + runner + .act(GameAction::PassPriority) + .expect("the chosen mana should finish the original cast"); + assert!(matches!( + runner.state().waiting_for, + WaitingFor::Priority { .. } + )); + assert_eq!(runner.state().objects[&spell].zone, Zone::Stack); +} + +/// A non-sacrificial row on the same permanent remains available to the +/// automatic planner; only the irreversible row is held for explicit consent. +#[test] +fn automatic_payment_keeps_a_non_sacrificial_row_on_a_sacrificial_source() { + let mut scenario = GameScenario::new(); + scenario.at_phase(Phase::PreCombatMain); + let spell = generic_spell(&mut scenario); + let source = scenario + .add_creature(P0, "Two-Row Mana Witness", 1, 1) + .as_artifact() + .with_ability_definition(sacrificial_mana_ability(AbilityCost::Tap, ManaColor::Red)) + .with_ability_definition(sacrificial_mana_ability( + AbilityCost::Sacrifice(SacrificeCost::count(TargetFilter::SelfRef, 1)), + ManaColor::Black, + )) + .id(); + let mut runner = scenario.build(); + + begin_sacrificial_payment(&mut runner, spell); + + assert!(matches!( + runner.state().waiting_for, + WaitingFor::Priority { .. } + )); + assert!(runner.state().objects[&source].tapped); + assert_eq!(runner.state().objects[&source].zone, Zone::Battlefield); + assert_eq!(runner.state().objects[&spell].zone, Zone::Stack); +} + +/// The pre-activation prompt does not bypass a mana ability's own sacrifice +/// choice; selecting another artifact pays that cost while its source remains +/// on the battlefield. +#[test] +fn sacrifice_another_permanent_mana_source_resumes_the_pending_cast() { + let mut scenario = GameScenario::new(); + scenario.at_phase(Phase::PreCombatMain); + let spell = generic_spell(&mut scenario); + let source = scenario + .add_creature(P0, "Krark-Clan Ironworks Witness", 1, 1) + .as_artifact() + .with_ability_definition(sacrificial_mana_ability( + AbilityCost::Sacrifice(SacrificeCost::count( + TargetFilter::Typed(TypedFilter::new(TypeFilter::Artifact)), + 1, + )), + ManaColor::Red, + )) + .id(); + let sacrifice = scenario + .add_creature(P0, "Sacrificial Artifact Witness", 1, 1) + .as_artifact() + .id(); + let mut runner = scenario.build(); + + begin_sacrificial_payment(&mut runner, spell); + let selection = offered_selection(&runner, source); + runner + .act(GameAction::ActivateManaSource { selection }) + .expect("the offered source should enter its normal interactive cost payment"); + assert!(matches!( + runner.state().waiting_for, + WaitingFor::PayCost { + kind: PayCostKind::Sacrifice, + ref choices, + .. + } if choices.contains(&sacrifice) + )); + + runner + .act(GameAction::SelectCards { + cards: vec![sacrifice], + }) + .expect("the source's ordinary sacrifice-cost reducer should accept another artifact"); + + assert_eq!(runner.state().objects[&source].zone, Zone::Battlefield); + assert_eq!(runner.state().objects[&sacrifice].zone, Zone::Graveyard); + assert_eq!( + runner.state().players[P0.0 as usize].mana_pool.total(), + 1, + "the selected ability's mana remains available to the original spell" + ); + assert!(matches!( + runner.state().waiting_for, + WaitingFor::ManaPayment { .. } + )); +} + +/// Returning from the safety prompt keeps the pending spell but neither spends +/// mana nor performs the irreversible source activation. +#[test] +fn back_from_sacrificial_mana_prompt_preserves_the_cast_without_cancellation() { + let mut scenario = GameScenario::new(); + scenario.at_phase(Phase::PreCombatMain); + let spell = generic_spell(&mut scenario); + let source = scenario + .add_creature(P0, "Back Button Witness", 1, 1) + .with_ability_definition(sacrificial_mana_ability( + AbilityCost::Sacrifice(SacrificeCost::count(TargetFilter::SelfRef, 1)), + ManaColor::Black, + )) + .id(); + let mut runner = scenario.build(); + + begin_sacrificial_payment(&mut runner, spell); + assert!( + !candidate_actions(runner.state()) + .iter() + .any(|candidate| matches!(candidate.action, GameAction::CancelCast)), + "the real safety prompt must not synthesize a cast-cancellation action" + ); + runner + .act(GameAction::BackToManaPayment) + .expect("the prompt's explicit back action should be accepted"); + + assert!(matches!( + runner.state().waiting_for, + WaitingFor::ManaPayment { .. } + )); + assert!(runner.state().pending_cast.is_some()); + assert_eq!(runner.state().objects[&source].zone, Zone::Battlefield); + assert_eq!(runner.state().players[P0.0 as usize].mana_pool.total(), 0); +} + +/// An engine-authored selection is revalidated at the reducer boundary, so a +/// source that became ineligible cannot be activated from a stale payment prompt. +#[test] +fn stale_sacrificial_mana_selection_is_rejected_without_mutating_payment() { + let mut scenario = GameScenario::new(); + scenario.at_phase(Phase::PreCombatMain); + let spell = generic_spell(&mut scenario); + let source = scenario + .add_creature(P0, "Stale Selection Witness", 1, 1) + .with_ability_definition(sacrificial_mana_ability( + AbilityCost::Sacrifice(SacrificeCost::count(TargetFilter::SelfRef, 1)), + ManaColor::Black, + )) + .id(); + let mut runner = scenario.build(); + + begin_sacrificial_payment(&mut runner, spell); + let selection = offered_selection(&runner, source); + runner + .state_mut() + .objects + .get_mut(&source) + .unwrap() + .controller = PlayerId(1); + + assert!( + runner + .act(GameAction::ActivateManaSource { selection }) + .is_err(), + "a stale source must fail before the payment reducer mutates state" + ); + assert!(matches!( + runner.state().waiting_for, + WaitingFor::ManaSourceSelection { .. } + )); + assert_eq!(runner.state().objects[&source].zone, Zone::Battlefield); + assert_eq!(runner.state().objects[&source].controller, PlayerId(1)); + assert_eq!(runner.state().players[P0.0 as usize].mana_pool.total(), 0); +} diff --git a/crates/phase-ai/src/decision_kind.rs b/crates/phase-ai/src/decision_kind.rs index 7b9d1b7e5d..82ad9727e2 100644 --- a/crates/phase-ai/src/decision_kind.rs +++ b/crates/phase-ai/src/decision_kind.rs @@ -20,7 +20,9 @@ pub fn classify(waiting_for: &WaitingFor, action: &GameAction) -> DecisionKind { WaitingFor::MulliganDecision { .. } | WaitingFor::OpeningHandBottomCards { .. } => { DecisionKind::Mulligan } - WaitingFor::ManaPayment { .. } | WaitingFor::PhyrexianPayment { .. } => { + WaitingFor::ManaPayment { .. } + | WaitingFor::ManaSourceSelection { .. } + | WaitingFor::PhyrexianPayment { .. } => { DecisionKind::ManaPayment } WaitingFor::ChooseXValue { .. } => DecisionKind::ChooseX, @@ -58,7 +60,9 @@ pub fn classify(waiting_for: &WaitingFor, action: &GameAction) -> DecisionKind { GameAction::PlayLand { .. } => DecisionKind::PlayLand, GameAction::CastSpell { .. } => DecisionKind::CastSpell, GameAction::ActivateAbility { .. } => DecisionKind::ActivateAbility, - GameAction::TapLandForMana { .. } | GameAction::UntapLandForMana { .. } => { + GameAction::TapLandForMana { .. } + | GameAction::ActivateManaSource { .. } + | GameAction::UntapLandForMana { .. } => { DecisionKind::ActivateManaAbility } // Default: any other priority-time action (PassPriority, special @@ -338,6 +342,9 @@ mod tests { }, ability_index: None, mana_type: engine::types::mana::ManaType::Green, + output: engine::types::mana::ManaSourceOutput::Concrete( + engine::types::mana::ManaType::Green, + ), atomic_combination: None, restrictions: Vec::new(), penalty: engine::types::mana::ManaSourcePenalty::None, diff --git a/crates/phase-ai/src/search.rs b/crates/phase-ai/src/search.rs index fc84ed5f34..70bba9440e 100644 --- a/crates/phase-ai/src/search.rs +++ b/crates/phase-ai/src/search.rs @@ -767,6 +767,19 @@ pub fn emit_trace_for_candidate( /// `config` supplies policy penalties used by selection escapes (e.g. sacrifice /// value ordering); difficulty/search knobs are unused here. pub fn fallback_action(state: &GameState, config: &AiConfig) -> Option { + // CR 605.3b: A sacrificial mana prompt is an explicit payment decision, + // not a generic pending-cast failure. Pick only an engine-issued source or + // the exact BackToManaPayment escape; never synthesize CancelCast here. + if matches!(state.waiting_for, WaitingFor::ManaSourceSelection { .. }) { + return engine::ai_support::legal_actions(state) + .into_iter() + .find(|action| { + matches!( + action, + GameAction::ActivateManaSource { .. } | GameAction::BackToManaPayment + ) + }); + } // CR 601.2c: A spell's target step must use the engine's current legal // target list. `target_slots` is a historical snapshot and can be stale // after earlier selections; if no current legal action remains, abort the @@ -781,7 +794,11 @@ pub fn fallback_action(state: &GameState, config: &AiConfig) -> Option Option Option { // These are all pending-cast states — the has_pending_cast guard - // above already returned CancelCast. This branch is unreachable - // at runtime but keeps the match exhaustive. + // above already returned CancelCast. ManaSourceSelection is + // intercepted above and never synthesizes CancelCast. This branch + // is unreachable at runtime but keeps the match exhaustive. Some(GameAction::CancelCast) } } diff --git a/crates/server-core/src/client_message_wire_guard.rs b/crates/server-core/src/client_message_wire_guard.rs index b66fc9f433..3fe9c116f4 100644 --- a/crates/server-core/src/client_message_wire_guard.rs +++ b/crates/server-core/src/client_message_wire_guard.rs @@ -304,6 +304,7 @@ mod tests { source: ObjectIncarnationRef::of(ObjectId(1), 1), ability_index: None, mana_type: ManaType::Green, + output: engine::types::mana::ManaSourceOutput::Concrete(ManaType::Green), atomic_combination: None, restrictions: vec![ManaRestriction::OnlyForAny(vec![ ManaRestriction::OnlyForSpell; @@ -328,6 +329,7 @@ mod tests { source: ObjectIncarnationRef::of(ObjectId(1), 1), ability_index: None, mana_type: ManaType::Green, + output: engine::types::mana::ManaSourceOutput::Concrete(ManaType::Green), atomic_combination: None, restrictions: Vec::new(), penalty: ManaSourcePenalty::None, diff --git a/crates/server-core/src/game_action_payload_guard.rs b/crates/server-core/src/game_action_payload_guard.rs index 29442cfc55..5844a3c693 100644 --- a/crates/server-core/src/game_action_payload_guard.rs +++ b/crates/server-core/src/game_action_payload_guard.rs @@ -565,7 +565,7 @@ pub fn guard_game_action_payload(action: &GameAction) -> Result<(), String> { bound_list("SetPhaseStops.stops", stops.len())?; } GameAction::SetPriorityPassingMode { .. } => {} - GameAction::TapLandForMana { selection } => { + GameAction::TapLandForMana { selection } | GameAction::ActivateManaSource { selection } => { guard_mana_source_selection_payload(selection)?; } GameAction::DistributeAmong { distribution, .. } => { @@ -618,6 +618,7 @@ pub fn guard_game_action_payload(action: &GameAction) -> Result<(), String> { | GameAction::ChooseAssistPlayer { .. } | GameAction::CommitAssistPayment { .. } | GameAction::MulliganDecision { .. } + | GameAction::BackToManaPayment | GameAction::UntapLandForMana { .. } | GameAction::SpendPoolMana { .. } | GameAction::UnspendPoolMana { .. } diff --git a/crates/server-core/src/protocol.rs b/crates/server-core/src/protocol.rs index ea2b06b7a0..4e9069a6f0 100644 --- a/crates/server-core/src/protocol.rs +++ b/crates/server-core/src/protocol.rs @@ -627,6 +627,7 @@ mod tests { source: ObjectIncarnationRef::of(ObjectId(7), 3), ability_index: None, mana_type: ManaType::Green, + output: engine::types::mana::ManaSourceOutput::Concrete(ManaType::Green), atomic_combination: None, restrictions: Vec::new(), penalty: ManaSourcePenalty::None, @@ -653,6 +654,22 @@ mod tests { } _ => panic!("wrong variant"), } + + let GameAction::TapLandForMana { selection } = action else { + unreachable!("fixture action is a land-mana selection"); + }; + let generic = GameAction::ActivateManaSource { selection }; + let json = serde_json::to_string(&ClientMessage::Action { + action: generic.clone(), + }) + .unwrap(); + let parsed: ClientMessage = serde_json::from_str(&json).unwrap(); + match parsed { + ClientMessage::Action { + action: restored_action, + } => assert_eq!(restored_action, generic), + _ => panic!("wrong variant"), + } } #[test] diff --git a/crates/server-core/tests/game_action_payload_guard.rs b/crates/server-core/tests/game_action_payload_guard.rs index 5a2c77fc86..9aa6d4bfe6 100644 --- a/crates/server-core/tests/game_action_payload_guard.rs +++ b/crates/server-core/tests/game_action_payload_guard.rs @@ -31,6 +31,7 @@ fn mana_source_selection() -> ManaSourceSelection { source: ObjectIncarnationRef::of(ObjectId(1), 1), ability_index: Some(0), mana_type: ManaType::Green, + output: engine::types::mana::ManaSourceOutput::Concrete(ManaType::Green), atomic_combination: None, restrictions: Vec::new(), penalty: ManaSourcePenalty::None, From dba518034e4656fd2055bd108dfa23272e5e2e4a Mon Sep 17 00:00:00 2001 From: matthewevans Date: Wed, 29 Jul 2026 20:15:32 -0700 Subject: [PATCH 2/7] test(client): fix mana source selection fixture type --- client/src/components/mana/__tests__/ManaPaymentUI.test.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/client/src/components/mana/__tests__/ManaPaymentUI.test.tsx b/client/src/components/mana/__tests__/ManaPaymentUI.test.tsx index f4e851e17a..639357af7c 100644 --- a/client/src/components/mana/__tests__/ManaPaymentUI.test.tsx +++ b/client/src/components/mana/__tests__/ManaPaymentUI.test.tsx @@ -456,7 +456,6 @@ describe("ManaSourceSelectionUI", () => { data: { player: 0, options: [selection], - convoke_mode: null, }, }, }); From 35292116a0f6dfa96eb04fe43bc082df33c9380e Mon Sep 17 00:00:00 2001 From: matthewevans Date: Wed, 29 Jul 2026 20:20:02 -0700 Subject: [PATCH 3/7] fix(engine): complete sacrificial mana selection state --- crates/engine/src/ai_support/mod.rs | 2 ++ .../src/ai_support/payment_continuation.rs | 12 +++++++++++ crates/engine/src/game/interaction.rs | 1 + crates/engine/src/game/mana_abilities.rs | 21 +++++++++++++++++++ crates/engine/src/types/actions.rs | 1 + crates/engine/src/types/game_state.rs | 10 +++++++++ 6 files changed, 47 insertions(+) diff --git a/crates/engine/src/ai_support/mod.rs b/crates/engine/src/ai_support/mod.rs index 8442b45b47..1c9d905387 100644 --- a/crates/engine/src/ai_support/mod.rs +++ b/crates/engine/src/ai_support/mod.rs @@ -1106,6 +1106,8 @@ fn classify_flat_priority_action(action: &GameAction) -> FlatPriorityActionClass | GameAction::MulliganDecision { .. } | GameAction::ReorderHand { .. } | GameAction::TapLandForMana { .. } + | GameAction::ActivateManaSource { .. } + | GameAction::BackToManaPayment | GameAction::UntapLandForMana { .. } | GameAction::SpendPoolMana { .. } | GameAction::UnspendPoolMana { .. } diff --git a/crates/engine/src/ai_support/payment_continuation.rs b/crates/engine/src/ai_support/payment_continuation.rs index f97ac9e45b..60cb96d141 100644 --- a/crates/engine/src/ai_support/payment_continuation.rs +++ b/crates/engine/src/ai_support/payment_continuation.rs @@ -444,6 +444,15 @@ fn classify_deferred_life_root( PaymentContinuationUnsupported::PayerMismatch, ) } + ManaAbilityResume::ManaSourceSelection { + player: selection_player, + .. + } if selection_player == player => classify_global_root(state, *player), + ManaAbilityResume::ManaSourceSelection { .. } => { + PaymentContinuationState::UnsupportedAffiliated( + PaymentContinuationUnsupported::PayerMismatch, + ) + } ManaAbilityResume::PhyrexianCastPayment { .. } | ManaAbilityResume::FinalizePendingManaPayment { .. } => { PaymentContinuationState::UnsupportedAffiliated( @@ -534,6 +543,9 @@ fn record_root_from_resume( ManaAbilityResume::ManaPayment { outer_player: None, .. } => return Err(PaymentContinuationUnsupported::MissingOuterPayer), + ManaAbilityResume::ManaSourceSelection { player, .. } => { + Some(root_from_global(state, *player)?) + } ManaAbilityResume::PhyrexianCastPayment { caster, .. } => { Some(root_from_global(state, *caster)?) } diff --git a/crates/engine/src/game/interaction.rs b/crates/engine/src/game/interaction.rs index 51f9342f35..b7826e2004 100644 --- a/crates/engine/src/game/interaction.rs +++ b/crates/engine/src/game/interaction.rs @@ -4165,6 +4165,7 @@ fn project_action_payload( match action { GameAction::PassPriority | GameAction::CancelCast + | GameAction::BackToManaPayment | GameAction::KeepAllCopyTargets | GameAction::RollPlanarDie | GameAction::CompanionToHand diff --git a/crates/engine/src/game/mana_abilities.rs b/crates/engine/src/game/mana_abilities.rs index 5795d85d37..941815d24a 100644 --- a/crates/engine/src/game/mana_abilities.rs +++ b/crates/engine/src/game/mana_abilities.rs @@ -2752,6 +2752,18 @@ pub(crate) fn finish_mana_root_after_deferred_life_payment( convoke_mode, }, )), + ManaAbilityResume::ManaSourceSelection { + player, + options, + convoke_mode, + } => Ok(resume_waiting_for( + player, + ManaAbilityResume::ManaSourceSelection { + player, + options, + convoke_mode, + }, + )), ManaAbilityResume::UnlessPayment { outer_player, cost, @@ -4152,6 +4164,15 @@ pub(crate) fn resume_waiting_for( player: outer_player.unwrap_or(mana_source_controller), convoke_mode, }, + ManaAbilityResume::ManaSourceSelection { + player, + options, + convoke_mode, + } => WaitingFor::ManaSourceSelection { + player, + options, + convoke_mode, + }, ManaAbilityResume::UnlessPayment { outer_player, cost, diff --git a/crates/engine/src/types/actions.rs b/crates/engine/src/types/actions.rs index 0d338c8b76..aa8df815ea 100644 --- a/crates/engine/src/types/actions.rs +++ b/crates/engine/src/types/actions.rs @@ -1599,6 +1599,7 @@ impl GameAction { | GameAction::ChooseReplacement { .. } | GameAction::OrderTriggers { .. } | GameAction::CancelCast + | GameAction::BackToManaPayment | GameAction::SubmitSideboard { .. } | GameAction::ChoosePlayDraw { .. } | GameAction::ChooseOption { .. } diff --git a/crates/engine/src/types/game_state.rs b/crates/engine/src/types/game_state.rs index aa840d6b6b..40ef02cfd0 100644 --- a/crates/engine/src/types/game_state.rs +++ b/crates/engine/src/types/game_state.rs @@ -7719,6 +7719,16 @@ pub enum WaitingFor { #[serde(default, skip_serializing_if = "Option::is_none")] convoke_mode: Option, }, + /// CR 601.2g-h + CR 605.3b: automatic payment reached a mana source that + /// sacrifices a permanent. The options are the frozen, engine-authored + /// semantic capability snapshot; submission revalidates one exact current + /// candidate before it can mutate the payment. + ManaSourceSelection { + player: PlayerId, + options: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + convoke_mode: Option, + }, /// CR 702.132a: Assist — when casting a spell with assist whose locked total /// cost has a generic component, before the caster pays they MAY choose /// another player to help pay the generic mana. The CASTER acts on this step From ed9f2e1a4604e612df83cb14a8edb1553e93d422 Mon Sep 17 00:00:00 2001 From: matthewevans Date: Wed, 29 Jul 2026 20:23:38 -0700 Subject: [PATCH 4/7] fix(ai): classify sacrificial mana actions --- crates/phase-ai/src/policies/discard_payoff.rs | 2 ++ crates/phase-ai/src/policies/draw_payoff.rs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/crates/phase-ai/src/policies/discard_payoff.rs b/crates/phase-ai/src/policies/discard_payoff.rs index 0330d1909a..8e409884a7 100644 --- a/crates/phase-ai/src/policies/discard_payoff.rs +++ b/crates/phase-ai/src/policies/discard_payoff.rs @@ -198,6 +198,8 @@ fn candidate_discards_controller(ctx: &PolicyContext<'_>) -> bool { | GameAction::MulliganDecision { .. } | GameAction::ReorderHand { .. } | GameAction::TapLandForMana { .. } + | GameAction::ActivateManaSource { .. } + | GameAction::BackToManaPayment | GameAction::UntapLandForMana { .. } | GameAction::SpendPoolMana { .. } | GameAction::UnspendPoolMana { .. } diff --git a/crates/phase-ai/src/policies/draw_payoff.rs b/crates/phase-ai/src/policies/draw_payoff.rs index 1523e59c75..bd3c2d682a 100644 --- a/crates/phase-ai/src/policies/draw_payoff.rs +++ b/crates/phase-ai/src/policies/draw_payoff.rs @@ -225,6 +225,8 @@ fn candidate_draws_structurally(ctx: &PolicyContext<'_>) -> bool { | GameAction::MulliganDecision { .. } | GameAction::ReorderHand { .. } | GameAction::TapLandForMana { .. } + | GameAction::ActivateManaSource { .. } + | GameAction::BackToManaPayment | GameAction::UntapLandForMana { .. } | GameAction::SpendPoolMana { .. } | GameAction::UnspendPoolMana { .. } From 50bf3d79b2393f61eb8e2ad2900193c56dadbf35 Mon Sep 17 00:00:00 2001 From: matthewevans Date: Wed, 29 Jul 2026 20:27:30 -0700 Subject: [PATCH 5/7] fix(manabrew): map sacrificial mana actions --- crates/manabrew-compat/src/lib.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/manabrew-compat/src/lib.rs b/crates/manabrew-compat/src/lib.rs index 4651f4588c..8fa9ce7fe8 100644 --- a/crates/manabrew-compat/src/lib.rs +++ b/crates/manabrew-compat/src/lib.rs @@ -2545,7 +2545,7 @@ pub fn convert_available_action( produced_mana: None, }), }), - GameAction::TapLandForMana { selection } => { + GameAction::TapLandForMana { selection } | GameAction::ActivateManaSource { selection } => { AvailableActionConversion::Available(AvailableAction { id, kind: AvailableActionKind::ActivateAbility(ActivatableAbilityInfo { @@ -2567,9 +2567,10 @@ pub fn convert_available_action( }, }) } - GameAction::PassPriority | GameAction::CancelCast | GameAction::Concede { .. } => { - AvailableActionConversion::Skip - } + GameAction::PassPriority + | GameAction::CancelCast + | GameAction::BackToManaPayment + | GameAction::Concede { .. } => AvailableActionConversion::Skip, GameAction::DeclareAttackers { .. } => AvailableActionConversion::Skip, GameAction::DeclareBlockers { .. } => AvailableActionConversion::Skip, GameAction::ChooseUntap { .. } => { From b95f803df35e89a6b8b30c72740152c5da992531 Mon Sep 17 00:00:00 2001 From: matthewevans Date: Wed, 29 Jul 2026 20:41:38 -0700 Subject: [PATCH 6/7] test(engine): count sacrificial mana prompt variant --- crates/engine/src/types/game_state.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/engine/src/types/game_state.rs b/crates/engine/src/types/game_state.rs index 40ef02cfd0..fb6eda280e 100644 --- a/crates/engine/src/types/game_state.rs +++ b/crates/engine/src/types/game_state.rs @@ -21739,7 +21739,7 @@ mod tests { mana_reduction: ManaCost::zero(), pending_cast: dummy_pending(), })); - assert_eq!(variants.len(), 36); + assert_eq!(variants.len(), 37); } #[test] From db4c458c0ec974bc4e330c3e521a90bcb2490dce Mon Sep 17 00:00:00 2001 From: matthewevans Date: Thu, 30 Jul 2026 07:29:49 -0700 Subject: [PATCH 7/7] test(engine): map cancel-cast display state authority --- .../fixtures/cr733/authority_matrix.json.gz | Bin 40716 -> 40792 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/crates/engine/tests/fixtures/cr733/authority_matrix.json.gz b/crates/engine/tests/fixtures/cr733/authority_matrix.json.gz index 3788c61d9d9f4b0efd0580ceb2c3f7bebb4444a6..e681f411b33f2ed48f6d3ae06312adde2a346590 100644 GIT binary patch literal 40792 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 literal 40716 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 Ud$d3O>7Rf4|0vnwtZHWl0I_322LJ#7