fix(engine): split cards report wrong mana value and colors off the stack (CR 202.3d / 709.4b)#5093
Conversation
|
Warning Gemini encountered an error creating the review. You can try again by commenting |
Parse changes introduced by this PR · 13 card(s), 16 signature(s) (baseline: main
|
matthewevans
left a comment
There was a problem hiding this comment.
[MED] Fused split spells on the stack still report only the chosen/front half's mana value. Evidence: crates/engine/src/game/game_object.rs:1213 gates combined split-card mana value to zone != Zone::Stack, while crates/engine/src/game/casting_costs.rs:6145 only unions fused stack object types/colors, not mana cost/value. Why it matters: CR 202.3d, CR 709.4d, and CR 702.102b require a fused split spell's on-stack mana value to be determined from both halves, so stack CMC filters and ObjectManaValue reads can undercount fused spells. Suggested fix: carry an authoritative fused-split state onto the stack object, or otherwise make effective_mana_value() combine both halves for CastingVariant::Fuse while preserving chosen-half behavior for non-fused split casts; add a production-path fused-spell test that asserts combined on-stack mana value.
[MED] AI collect-evidence candidates still use front-only mana value. Evidence: crates/engine/src/ai_support/candidates.rs:95 and crates/engine/src/ai_support/candidates.rs:116 sum/sort with obj.mana_cost.mana_value() instead of the new split-aware helper. Why it matters: the engine resolver now accepts off-stack split cards by combined mana value, but AI production choices for WaitingFor::CollectEvidenceChoice can omit a legal evidence payment. Suggested fix: use obj.effective_mana_value() in collect_evidence_candidate_combos and add an AI candidate test with a graveyard split card whose combined mana value alone satisfies the threshold.
[MED] Exiled-card color mana production still reads only exiled.color. Evidence: crates/engine/src/game/effects/mana.rs:785 iterates raw exiled.color for linked exiled cards. Why it matters: effects parsed as "add mana of any of the exiled card's colors" flow through ChoiceAmongExiledColors, so an exiled split card exposes only its front-half color instead of combined off-stack colors. Suggested fix: iterate exiled.effective_colors() in exiled_color_options and add a production-path mana ability test using a linked exiled split card.
Donw. |
matthewevans
left a comment
There was a problem hiding this comment.
Thanks for the follow-up. I re-reviewed the current head and the previous split-card fixes did land for several off-stack paths, but I found two remaining blockers around fused spells:
-
Fused split spells still use front-half metadata during mana payment.
build_spell_metabuilds restricted-mana metadata from rawobj.mana_cost/obj.color(crates/engine/src/game/casting.rs:1578-1584), andpay_mana_cost_with_choicesuses that beforefinalize_cast_with_phyrexian_choicesmarksfused_split_spell(crates/engine/src/game/casting_costs.rs:5867, marker at6145-6150). Restricted mana such as "spend this mana only to cast a spell with mana value/color count N" can therefore accept/reject fused split spells using only the front half. -
Spell-cast history still records fused split spells with front-half mana value. The fuse marker is set before
record_spell_cast_from_zone(crates/engine/src/game/casting_costs.rs:6145-6150, call at6320), but the recorder storesobj.mana_cost.mana_value_with_x(...)instead ofobj.effective_mana_value()(crates/engine/src/game/restrictions.rs:233). That means "cast a spell with mana value N" history/turn filters can see Breaking // Entering as MV 2 rather than 8.
Please route both paths through the same authoritative split-aware characteristics and add discriminating tests for fused split spell restricted-mana/payment metadata and spell-cast-history mana value.
|
Thanks for the update. I re-reviewed head I can't approve or enqueue this from the sweep yet because GitHub currently reports the PR as conflicting with base ( |
…tack (CR 202.3d / 709.4b) A split card (Fire // Ice, Assault // Battery, Wear // Tear — 109 pairs) in any zone other than the stack reported only its FRONT half's mana value and colors instead of the CR-mandated COMBINED value of both halves. CR 202.3d: a split card not on the stack has the combined mana value of its halves; CR 709.4/709.4b: its colors and mana value off the stack are determined from the combined mana cost. ~30 off-stack consumers read obj.mana_cost directly and got the front half: mana-value/color search & filter (FilterProp::Cmc/ManaValueParity/HasColor/...), clash/discover/cascade/collect-evidence, library search, free-cast-from-zone, exile-cast legality, protection/hexproof source characteristics, and — worst — the leave-play LKI snapshot, so "whenever a creature with mana value N dies" look-back triggers misfired. Only the on-stack Fuse path combined. Adds authoritative GameObject::effective_mana_value() and effective_colors() that combine both halves (read from the already-populated back_face) when the card is off the stack and is not a battlefield Room, and otherwise return the existing single-face value byte-for-byte. Migrates the ~30 off-stack call sites to the helpers; leaves on-stack/spell, stateless-ManaCost, deck-validation, and the Fuse merge unchanged (chosen-half is correct on the stack, CR 202.3d). Room handling (CR 709.5): a Room permanent on the battlefield is characterized by its unlocked halves, not the naive combine, so the gate excludes zone==Battlefield && room_unlocks.is_some() — off-battlefield Rooms still combine (709.4), battlefield Rooms do not (709.5c). (room_unlocks is populated in every zone by card creation, so the gate is zone-explicit, not room_unlocks-presence.) Tests: 10 discriminating runtime tests through the real GameScenario/GameRunner path — combined MV/colors off-stack in library/graveyard/hand/exile; MV/color filters and searches match by combined value; the leave-play LKI snapshot records combined MV so MV-gated dies triggers fire; the exile-cast (impulse-draw) legality gate uses combined MV; NEGATIVE on-stack split reports the chosen half; Fuse still combines; a battlefield Room is not over-combined while the same Room in hand is; single-face cards unchanged. Full cargo test -p engine green (15023 lib + 1758 integration / 0 fail); clippy --all-targets -D warnings clean. CR 202.2, CR 202.3d, CR 202.3e, CR 709.4, CR 709.4b, CR 709.4d, CR 709.5, CR 709.5c.
…ect-evidence, and exiled-color mana (CR 202.3d/709.4d)
…na payment metadata and spell-cast history (CR 202.3d/702.102b)
52b768e to
bf70e0a
Compare
matthewevans
left a comment
There was a problem hiding this comment.
[MED] Fused split spells still use front-half mana value/colors when live spell filters evaluate the spell being cast. Evidence: crates/engine/src/game/filter.rs:2555 builds the live SpellCastRecord from raw spell_obj.color and spell_obj.mana_cost.mana_value_with_x(...), and crates/engine/src/game/casting.rs:15203 duplicates the same raw record for per-turn cast-limit filters. Why it matters: cost modifiers, cast prohibitions, and turn-limit filters can use TargetFilter properties such as Cmc, HasColor, ColorCount, or multicolored quality against the current spell, so a fused Breaking // Entering can still be seen as front-half MV/colors instead of the CR 202.3d + CR 702.102b combined spell. Suggested fix: route live spell-filter records through the same spell_mana_value() / spell_colors() authority after the fuse choice is known, preferably via one shared record constructor, and add a production-path fused split spell test against a mana-value or color spell filter.
Now I am fixing on it. |
… spell_mana_value/spell_colors authority (CR 202.3d/702.102b)
|
@matthewevans |
matthewevans
left a comment
There was a problem hiding this comment.
[MED] Fused split spells still use front-half metadata in pre-payment live spell filters because the shared record helper keys on a marker that is set too late. Evidence: crates/engine/src/game/casting.rs:3673-3680 builds Fuse choices by preparing the variant and immediately calling can_cast_prepared_now, which runs cast prohibitions and per-turn limits at crates/engine/src/game/casting.rs:4078-4110; but the fused_split_spell marker that spell_cast_record() now relies on is not set until crates/engine/src/game/casting_costs.rs:5867-5876, inside finalization just before mana payment. The new tests manually set fused_split_spell (crates/engine/src/game/filter.rs:5828-5834, crates/engine/src/game/casting_tests.rs:462-470), so they do not exercise the production pre-payment path that decides whether the fused cast is legal or whether a cost modifier/prohibition applies. Why it matters: a Cmc/HasColor/ColorCount filter used by a cost modifier, cast prohibition, or per-turn limit can still evaluate a fused Breaking // Entering as the front half while the engine is deciding whether the Fuse option is available. Suggested fix: make the live spell-record projection take the selected CastingVariant (or otherwise bind the fuse state before all pre-payment legality/cost-modifier checks) and add a production-path test that casts via Fuse through a spell filter before finalization, without manually setting fused_split_spell.
…ll pre-payment spell filters (CR 202.3d/702.102b)
matthewevans
left a comment
There was a problem hiding this comment.
Thanks for the update. I think the fused projection is now threaded through the preparation/record seam, but there is still a candidate-enumeration issue that can expose the wrong cast path.
- [MED]
casting_variant_candidatescomputesis_fuse_capableonce for any fuse-capable split card (crates/engine/src/game/casting.rs:3836) and then uses that combined projection for the separate hand alternative-cost candidate gates (crates/engine/src/game/casting.rs:3955,:4005,:4017,:4040, etc.). That means aCastWithKeyword { Dash }/Evoke/Overload/etc. grant whose filter matches only the fused combined characteristics can surface a non-FuseCastingVariant::Dash/Evoke/Overload option. The later preparation path does not prepare those variants as fused:is_fuse_variantis only true forCastingVariant::Fuse(crates/engine/src/game/casting.rs:4118), and the alternative-cost readers for these variants use the non-fusedeffective_spell_keywords(...)path (crates/engine/src/game/casting.rs:4595,:4640,:4860). So the option is admitted by fused characteristics but then executes as a separate non-fused/front-half alternative-cost cast, potentially falling back to printed cost when the non-fused keyword read no longer finds the grant.
Please keep non-Fuse alternative-cost candidate discovery on the normal/front-half projection, and route the combined projection only through the actual CastingVariant::Fuse preparation/check path. I would also add regression coverage for both directions: a combined-only grant must not add a separate non-Fuse alternative-cost option, and a front-half-only grant must still be visible to the corresponding non-Fuse cast.
…ojection, not combined (CR 601.2b)
matthewevans
left a comment
There was a problem hiding this comment.
Thanks for the latest update. The earlier fused split blockers I had called out are mostly addressed, but there is still a fused-stack consumer using the raw front-half mana cost.
[MED] Fused split spells with cascade still cascade from the front-half mana value. Evidence: crates/engine/src/game/effects/cascade.rs:32 computes source_mv with obj.mana_cost.mana_value_with_x(obj.zone, obj.cost_x_paid), even though this PR adds GameObject::spell_mana_value() / spell_mana_value_for(...) for fused stack spells in crates/engine/src/game/game_object.rs. Why it matters: a fused Breaking // Entering-style spell that gains cascade has combined mana value 8, but this resolver would still seed the cascade threshold from the front half and carry that undercount into CastOfferKind::Cascade { source_mv }. Suggested fix: route the cascade source threshold through the fused-aware spell mana-value authority, and add a regression where a fused split spell granted cascade can hit a card whose MV is between the front-half MV and the combined MV.
… front half (CR 202.3d/702.102b/702.85a)
matthewevans
left a comment
There was a problem hiding this comment.
[MED] Delayed "that spell's mana value" snapshots still use front-half mana value for fused split spells. Evidence: crates/engine/src/game/effects/delayed_trigger.rs:579. Why it matters: a fused split spell's SpellCast event is created after fused_split_spell is set, but this event-context snapshot bypasses spell_mana_value() and freezes the front half for delayed/reflexive effects that later use "that spell's mana value." Suggested fix: snapshot this branch through the fused-aware spell mana-value helper and add a fused split spell production-path delayed-trigger regression.
[MED] Damage source history still snapshots fused spell mana value/colors from the raw front-half object metadata. Evidence: crates/engine/src/game/effects/deal_damage.rs:737. Why it matters: damage-history source filters reconstruct a synthetic source from DamageRecord::source_mana_value/source_colors (crates/engine/src/game/filter.rs:1311), so mana-value/color-gated "damage dealt by a source" checks can misclassify damage from a fused split spell. Suggested fix: snapshot damage source mana value/colors through the same fused-aware/effective metadata helpers and cover a fused split damage source matched by a mana-value or color source filter.
…ayed-trigger and damage-source snapshots (CR 202.3d/702.102b)
|
@matthewevans |
matthewevans
left a comment
There was a problem hiding this comment.
Thanks for the follow-up. I re-reviewed the current head and there are still split-card off-stack consumers using a single face instead of the combined split-card characteristics.
crates/engine/src/database/search.rsstill filters and reports split cards through oneCardFacefor mana value and color search. Off stack, split cards need to use the combined halves, socmc_maxand color filters can still include/exclude the wrong cards and report the wrong mana value.crates/engine/src/game/companion.rsandcrates/engine/src/game/deck_validation.rsstill validate companion / deck legality using a half's mana value. That can accept or reject split cards incorrectly for companion restrictions and Tiny Leaders-style checks.
Please centralize a split-aware off-stack mana-value/color projection using the sibling face/oracle-id data, then route deck-builder search, CardSearchResult, companion validation, and deck validation through that helper. That keeps the rule fix at one seam instead of leaving parallel single-face paths behind.
Summary
A split card (Fire // Ice, Assault // Battery, Wear // Tear — 109 pairs) in any zone other than the stack reported only its FRONT half's mana value and colors instead of the combined value of both halves (CR 202.3d / 709.4b). ~30 off-stack consumers were affected: MV/color search & filters, clash/discover/cascade/collect-evidence, library search, exile-cast legality, protection/hexproof, and the leave-play LKI snapshot (so MV-gated "creature with mana value N dies" look-back triggers misfired). Adds authoritative
GameObject::effective_mana_value()/effective_colors()(combine both halves off-stack, single-face otherwise) and migrates the off-stack call sites; on-stack/Fuse/deck-validation left unchanged.Files changed
CR references
CR 202.2, CR 202.3d, CR 202.3e, CR 709.4, CR 709.4b, CR 709.4d, CR 709.5, CR 709.5c
Track
Developer
LLM
Model: claude-opus-4-8
Thinking: high
Verification
cargo fmt --all— cleancargo clippy -p engine --all-targets --features proptest -- -D warnings— cleancargo test -p engine— 15023 lib + 1758 integration pass / 0 failgen-card-data/coverage/semantic-audit— N/A: engine-runtime characteristic fix, no card parsing changedScope Expansion
None.
Validation Failures
None.
CI Failures
None.