Skip to content

fix(engine): bind combat-damage sacrifice pools and effect-sacrificed referents (Descendants' Fury #4795)#5862

Merged
matthewevans merged 1 commit into
phase-rs:mainfrom
shin-core:fix-descendants-fury-sacrificed-referent-4795
Jul 16, 2026
Merged

fix(engine): bind combat-damage sacrifice pools and effect-sacrificed referents (Descendants' Fury #4795)#5862
matthewevans merged 1 commit into
phase-rs:mainfrom
shin-core:fix-descendants-fury-sacrificed-referent-4795

Conversation

@shin-core

Copy link
Copy Markdown
Contributor

Fixes #4795 (claimed there with the corrected root-cause). Verified the Oracle text on Scryfall and reproduced end-to-end on current main before building.

Reproduction (real apply pipeline)

Combat → trigger → accept "you may sacrifice one of them" → two divergences:

  1. The sacrifice was a silent no-op (attacker stayed on the battlefield, EffectResolved{Sacrifice, subject: None}).
  2. After fixing that, the reveal dug through the entire library and bottomed everything — the shared-type creature never entered.

The parse is correct; the triage's fast-path hypothesis was stale (the effect_context_object capture machinery works). The real breaks are two bypassed single authorities:

Defect 1 — sacrifice pool never bound the combat-damage set

sacrifice::resolve matched the raw TrackedSet(0) sentinel via matches_target_filter, whose id-level ladder has no combat-damage rung. "One of them" on a combat-damage trigger lives only in the CombatDamageDealtToPlayer event, reachable via resolve_tracked_set_sentinel's current_combat_damage_source_filter rung (CR 510.2) — which the sacrifice resolver, alone among tracked-set consumers (change_zone, shuffle, token_copy, cloak, …), never called. Fix: route the filter through resolve_tracked_set_sentinel before deriving the pool (non-sentinel filters pass through unchanged). Same unification discipline as #5512.

Defect 2 — CostPaidObject readers ignored effect-sacrificed referents

Both runtime TargetFilter::CostPaidObject arms (filter eval + resolved_targets) read only ability.cost_paid_object. An effect sacrifice (CR 608.2c later-instruction referent) is captured into effect_context_object — and the documented cost_paid_object → effect_context_object ladder was already implemented for the ObjectScope::CostPaidObject quantity arms (P/T, mana value) but missing from the filter layer. Fix: implement the same fallback in both arms so every CostPaidObject reader binds the same referent (CR 608.2k). Class-level: the whole "sacrifice one → later instruction reads the sacrificed object's properties" family.

Tests (each fails on revert of its seam; revert-probed)

  • Single attacker: sacrifice happens, reveal digs past a non-matching card, the shared-type creature enters the battlefield, rest to the bottom.
  • Multi-attacker: drives the real EffectZoneChoice — the chosen creature's type (not a board rescan) drives the reveal; the unchosen type's match stays in the library. This is the identity-binding hostile fixture.
  • Negative: no shared-type creature in the library → whole library revealed, nothing enters, all to the bottom (fail-closed referent, with a sacrifice reach-guard so the negative isn't vacuous).

Verification

Full engine lib: 16672 passed, 0 failed. Integration: 3130 passed, 0 failed (incl. issue_735_cost_paid_object_non_regression). Clippy -D warnings clean; fmt clean. CR annotations (510.2, 608.2c, 608.2k, 400.7) grep-verified against docs/MagicCompRules.txt.

🤖 Generated with Claude Code

… referents (Descendants' Fury phase-rs#4795)

"Whenever one or more creatures you control deal combat damage to a player,
you may sacrifice one of them. If you do, reveal cards ... until you reveal
a creature card that shares a creature type with the sacrificed creature.
Put that card onto the battlefield ..." never put the revealed card onto
the battlefield. Two class-level defects, both a bypassed single authority:

1. The sacrifice was a silent no-op: sacrifice::resolve built its eligible
   pool from the raw TrackedSet(0) sentinel via matches_target_filter,
   whose id-level ladder (resolve_tracked_set_id) has no combat-damage
   rung. "One of them" on a combat-damage trigger lives only in the
   CombatDamageDealtToPlayer event, reachable via
   resolve_tracked_set_sentinel's current_combat_damage_source_filter rung
   (CR 510.2) — which the sacrifice resolver, alone among tracked-set
   consumers (change_zone, shuffle, token_copy, cloak, ...), never called.
   Route the filter through resolve_tracked_set_sentinel before deriving
   the pool; non-sentinel filters pass through unchanged.

2. Even with the sacrifice landed, SharesQuality{CreatureType, reference:
   CostPaidObject} matched nothing: both runtime TargetFilter::
   CostPaidObject arms (filter.rs eval + targeting.rs resolved_targets)
   read only ability.cost_paid_object. An EFFECT sacrifice (CR 608.2c
   later-instruction referent, captured by parent_referent_context_from_
   events) lands in effect_context_object; the documented cost_paid_object
   -> effect_context_object ladder (ability.rs) was implemented for the
   ObjectScope::CostPaidObject quantity arms but missing from the filter
   layer. Implement the same fallback in both arms so every CostPaidObject
   reader binds the same referent (CR 608.2k).

The triage's original hypothesis (fast path not stamping
effect_context_object) was stale: the capture machinery works — the breaks
were the pool binder and the reader side.

Tests (integration, real combat -> trigger -> optional-accept -> reveal
pipeline, verbatim Oracle text; each fails on revert of its seam):
- single attacker: sacrifice happens, reveal digs past a non-match, the
  shared-type creature enters the battlefield, rest to the bottom;
- multi-attacker: the interactive EffectZoneChoice pick's creature type
  (not a board rescan) drives the reveal — the unchosen type stays in the
  library;
- negative: no shared-type creature -> whole library revealed, nothing
  enters, all to the bottom (fail-closed referent, with sacrifice
  reach-guard).

Full lib: 16672 passed. Integration: 3130 passed. Clippy clean.

Fixes phase-rs#4795.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@shin-core shin-core requested a review from matthewevans as a code owner July 15, 2026 13:12
@superagent-security superagent-security Bot added the contributor:flagged Contributor flagged for review by trust analysis. label Jul 15, 2026
@superagent-security

Copy link
Copy Markdown

🚨 Contributor flagged. Click here for more info: Superagent Dashboard

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request resolves runtime issues with Descendants' Fury (#4795) by ensuring that TrackedSetId(0) sentinels are resolved to concrete filters using resolve_tracked_set_sentinel (per CR 608.2c and CR 510.2) and implementing a fallback from cost_paid_object to effect_context_object for TargetFilter::CostPaidObject (per CR 608.2k). It also adds a comprehensive integration test suite to prevent regressions. I have no feedback to provide as the changes are well-implemented and adhere to the repository's architectural guidelines.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

@github-actions

Copy link
Copy Markdown

Parse changes introduced by this PR

✓ No card-parse changes detected.

@matthewevans matthewevans self-assigned this Jul 16, 2026

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved after current-head implementation review.

@matthewevans matthewevans added the bug Bug fix label Jul 16, 2026
@matthewevans matthewevans added this pull request to the merge queue Jul 16, 2026
@matthewevans matthewevans removed their assignment Jul 16, 2026
Merged via the queue into phase-rs:main with commit 62f998c Jul 16, 2026
12 of 13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Bug fix contributor:flagged Contributor flagged for review by trust analysis.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Descendants' Fury does not put the revealed creature onto the battlefield

2 participants