Skip to content

feat(parser): dynamic damage-modification replacements (Fated Firepower, Neriv)#6066

Merged
matthewevans merged 2 commits into
phase-rs:mainfrom
lgray:feat/std-dynqty-a-damage-mod
Jul 17, 2026
Merged

feat(parser): dynamic damage-modification replacements (Fated Firepower, Neriv)#6066
matthewevans merged 2 commits into
phase-rs:mainfrom
lgray:feat/std-dynqty-a-damage-mod

Conversation

@lgray

@lgray lgray commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

🤖 AI text below 🤖

Summary

Adds two composable damage-modification replacement parser arms (CR 614.1a — "instead" replacement effects) so damage-doubling and dynamic additive-offset cards parse to typed DamageModification values. Fixes a DynamicQty coverage-classifier false-positive that flagged damage-Double/Triple carriers as unsupported. Standard-legal cards unlocked: Fated Firepower and Neriv, Heart of the Storm; Ojer Axonil, Deepest Might (SetToSourcePower, already handled) gains an unchanged-behavior regression test.

DynQty subgroup A of the Standard rollout.

Implementation method (required)

Method: /engine-implementer

CR references

  • CR 614.1a — a replacement effect that reads "instead" replaces the damage event with the modified amount.
  • CR 701.10g — to double an amount of damage, the source instead deals twice that much damage.

What changed

parser/oracle_replacement.rs — two arms, both composed from existing combinators (no verbatim Oracle-string match):

  • parse_that_much_damage_offset — new arm (inserted before the numeric arm so "an"→1 does not shadow it):
    preceded(tag("plus an amount of damage equal to "), terminated(take_until(" instead"), tag(" instead")))
    map_optparse_cda_quantity(q)DamageModification::Plus { value }. Fail-closed: an unrecognized quantity makes map_opt fail and the arm yields.
  • parse_damage_modification_phrase — new leaf tag("twice that much damage") added to the pre-existing Double alt (siblings "double that damage", "deals double that damage") → DamageModification::Double.

Before → after AST (Fated Firepower):

  • Before: it deals that much damage plus an amount of damage equal to the number of fire counters on ~ insteadReplacementDefinition { damage_modification: None, .. } (dropped / unsupported).
  • After: damage_modification: Some(DamageModification::Plus { value: <fire-counters-on-source ref> }).

parser/swallow_check.rs — the detect_dynamic_qty coverage classifier was flagging "twice that much damage" (a static-typed Double, not a runtime QuantityExpr) as an unsupported dynamic clause. Added a suppression clause: when the only dynamic marker is the damage-double phrase and the parsed replacement carries DamageModification::Double | Triple, do not warn (CR 614.1a + CR 701.10g). Deduplicated the triplicated 8-phrase marker list into a single module-scope OTHER_DYNAMIC_MARKERS const.

Verification

  • Required checks ran clean, or the exact CI-owned alternative is stated below.
  • Gate A output below is for the current committed head.
  • Final review-impl below is clean for the current committed head.
  • Both anchors cite existing analogous code at the same seam.

All commands run at the committed head f62acde5f (rebased onto upstream/main b387f9c66).

check command result
parser combinator gate ./scripts/check-parser-combinators.sh Gate G PASS + Gate A PASS
targeted lib tests (parser arms + swallow gates) cargo test -p engine --lib -- fated_firepower neriv ojer_axonil dynamic_qty_…_damage_double twice_is_only_dynamic_marker 8 passed; 0 failed
runtime cast tests (doubling / offset scaling) cargo test -p engine --test integration -- neriv_entered_this_turn_source_doubles neriv_prior_turn_source_not_doubled fated_firepower_offset_scales_with_fire_counters 3 passed; 0 failed
clippy cargo clippy -p engine --all-targets 0 warnings, 0 errors

Runtime discriminators (the honesty arbiter):

  • neriv_entered_this_turn_source_doubles — source that entered this turn deals 2 → 4 marked damage (Double applied).
  • neriv_prior_turn_source_not_doubled — source present since a prior turn deals 2 → 2 (replacement's EnteredThisTurn source filter correctly excludes it; CR 400.7).
  • fated_firepower_offset_scales_with_fire_counters — base 2 + 3 fire counters → 5 (Plus { value = fire-counters-on-source } resolves dynamically).

Coverage delta — isolated measurement. Measured at base d59d1c3e2 where base and tip differ only by this 4-file diff (perfect isolation of this change): exactly {Fated Firepower, Neriv, Heart of the Storm} False→True, 0 True→False, 0 appeared/disappeared. supported_cards 31394 → 31396.

Rebase-invariance of that delta (base advanced 10 commits to b387f9c66): established by measurement + causal orthogonality — (1) no upstream commit in the range touches parser/oracle_replacement.rs or parser/swallow_check.rs (this change's only production edits); (2) the one upstream commit that touches a dependency of this change, #6045 (parse_cda_quantity), is loyalty-counter-only (~'s loyaltyCountersOn{Source, Loyalty}), disjoint from Fated Firepower's fire-counter quantity — and the post-rebase lib test fated_firepower_assembled_replacement confirms FF still parses to Plus{fire-counters}. The card-data coverage-regression CI check runs the authoritative clean diff against upstream/main.

Gate A

Gate A PASS head=f62acde5fbf46516f08db6cddea0e4387d480da2 base=be22e078bb1b05661d1ce34a21f3fbc07a136cab

Anchored on

  • crates/engine/src/parser/oracle_replacement.rsparse_damage_modification_phrase already maps "double that damage" / "deals double that damage"DamageModification::Double; the new "twice that much damage" leaf is a sibling in the same alt.
  • crates/engine/src/parser/oracle_replacement.rsparse_that_much_damage_offset already maps numeric offsets to DamageModification::Plus; the new dynamic arm mirrors it, delegating the quantity to parse_cda_quantity.

Final review-impl

Final review-impl PASS head=f62acde5fbf46516f08db6cddea0e4387d480da2

Claimed parse impact

  • Fated Firepower
  • Neriv, Heart of the Storm

…er, Neriv)

Add two damage-replacement parser arms (CR 614.1a — "instead"
replacement effects) emitting typed DamageModification values:

- "plus an amount of damage equal to <quantity> instead" ->
  DamageModification::Plus { value } via parse_cda_quantity
  (Fated Firepower: +N where N = fire counters on the source).
- "twice that much damage" -> DamageModification::Double
  (CR 701.10g double-damage; Neriv, Heart of the Storm).

Both compose existing combinators (preceded/alt/take_until +
parse_cda_quantity) — no verbatim Oracle-string matching.

Extend the DynamicQty swallow classifier so a damage Double/Triple
carrier is no longer flagged as an unsupported dynamic clause: a
new suppression clause plus the shared OTHER_DYNAMIC_MARKERS const
(deduplicated from three prior copies). Ojer Axonil
(SetToSourcePower) is covered by an unchanged-behavior regression
test.

Coverage delta vs base: exactly {Fated Firepower, Neriv, Heart of
the Storm} False->True, zero other transitions.

Assisted-by: ClaudeCode:claude-opus-4.8
@lgray
lgray requested a review from matthewevans as a code owner July 17, 2026 02:49
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Warning

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

@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown

Parse changes introduced by this PR

Baseline pending for 311a0b4f9c328771dbbc885ac8098f295adf99e8 — this populates once main publishes its coverage snapshot (a few minutes after that commit landed).

@matthewevans matthewevans self-assigned this Jul 17, 2026
@matthewevans matthewevans removed their assignment Jul 17, 2026
@matthewevans matthewevans added the enhancement New feature or request label Jul 17, 2026
@matthewevans matthewevans self-assigned this Jul 17, 2026
@matthewevans matthewevans added the quality For high-quality minimal to no-churn PRs label Jul 17, 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.

Maintainer sign-off: current head independently reviewed; required checks green. Runtime coverage discriminates the dynamic offset and entered-this-turn damage-doubling paths.

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

Labels

enhancement New feature or request quality For high-quality minimal to no-churn PRs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants