Skip to content

fix(engine): Licids attach themselves, not their enchant target (#605)#6668

Merged
matthewevans merged 7 commits into
phase-rs:mainfrom
mike-theDude:fix/issue-605-calming-licid
Jul 27, 2026
Merged

fix(engine): Licids attach themselves, not their enchant target (#605)#6668
matthewevans merged 7 commits into
phase-rs:mainfrom
mike-theDude:fix/issue-605-calming-licid

Conversation

@mike-theDude

@mike-theDude mike-theDude commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator

Fixes #605.

What was broken

Aura-form Licid activations bound the bare "it" in “Attach it to target creature” to the chosen enchant recipient. The engine therefore attempted to attach the target to itself, and the activation appeared to do nothing.

The same activation also lacked two pieces of its ongoing-effect lifecycle:

  • the no-duration Aura animation must persist under CR 611.2a;
  • “You may pay {…} to end this effect” is a later special action under CR 116.2c, not a resolution-time PayCost.

What changed

  • The Oracle parser tracks whether the current chain animated its source into an attachable Aura/Equipment. Only that animate-then-attach class binds bare "it" to SelfRef; Equipment ETBs and non-attachable sources keep their existing bindings.
  • The Aura animation receives Duration::Permanent.
  • Mana-only “pay {…} to end this effect” clauses are absorbed into the exact continuous-effect-producing GenericEffect.
  • Resolution gives every activation a stable effect-group identity and termination permission. The controller gets an affordable EndContinuousEffect special action at priority; paying through the restriction-aware mana pipeline removes only that group, restores layer-derived characteristics and abilities, and runs attachment SBAs.
  • Replay carries and validates the termination-group allocator high-water, preventing identity reuse.
  • CR 704.5p attachment cleanup now covers creatures and other non-Aura/Equipment/Fortification permanents, while preserving legal Aura, Equipment, reconfigure, and bestow attachments.
  • The engine projects an ordered, viewer-scoped pay-to-end offer list through WASM, P2P, WebSocket, and server-draft snapshots. The React client renders and dispatches those engine-authored actions unchanged.
  • The new action label is present in every supported locale.

Scope and parse evidence

There are two intentional card cohorts:

  • 12 Aura-form Licids change attachment binding and animation duration.
  • 13 cards have the obsolete PayCost removed: those 12 Licids plus Flanking Licid, whose pay-to-end clause belongs to the same CR 116.2c termination class.

The sticky <!-- coverage-parse-diff --> comment is generated by CI for the exact current head and reports these as separate signatures.

Verification

  • cargo fmt --all -- --check
  • targeted Clippy for engine, WASM, server-core, and phase-server with -D warnings
  • parser combinator Gates A and G
  • 28/28 issue [Card Bug] calming licid effect doesn't work #605 integration tests
  • focused parser, engine offer-order, CR 733 replay, action-order, and server transport/payload tests
  • frontend protocol/type-check
  • frontend locale parity: 67/67
  • focused frontend snapshot/wire tests: 114/114
  • ESLint: 0 errors (26 pre-existing warnings)

Runtime coverage drives both branches through the production activation/legal-action/payment pipeline. The pay branch spends the available white mana, removes the exact effect, restores the Licid ability and characteristics, and detaches it; the decline branch crosses cleanup without paying or ending the effect.

Summary by CodeRabbit

  • New Features

    • Added support for ending continuous effects (CR 116.2c) with engine-generated “End {{source}}’s effect” offers (cost + grouping/permission), including new end-event logging and deterministic offer ordering.
    • The UI now renders one priority button per permitted end offer, with full WebSocket/snapshot/wire plumbing.
  • Bug Fixes

    • Fixed chained bare “it” attachment for animate-then-attach so Auras/Equipment attach to the intended source.
    • Improved pay-to-end parsing/absorption and prevented incorrect clause folding across pay-to-end variants.
  • Tests

    • Expanded Licid + CR 116.2c/704.5p integration and added ordering/guarding/replay coverage.

…e-rs#605)

A Licid activation did nothing at all. "This creature loses this ability
and becomes an Aura enchantment with enchant creature. Attach it to
target creature" bound the bare "it" attachment anaphor to
TargetFilter::ParentTarget, which resolve_object_filter reads as the
ability's CHOSEN target -- the enchant recipient. The engine therefore
attached the targeted creature to itself; state-based actions undid the
nonsense attachment and the activation vanished silently. All 12 Licids
share the clause.

CR 608.2c: read the whole text and apply the rules of English. An earlier
clause in the chain has animated the ability's own source into an Aura,
so the chain has made exactly one object attachable and the following
"it" names that source.

Bind the attachment pronoun to SelfRef for exactly that animate-then-attach
class, via a new ParseContext flag source_becomes_attachment_in_chain
seeded from the chain's prior clauses. Every other chain keeps its
pre-existing parse_target binding:

  * chains with an earlier typed referent keep ParentTarget (Aura Graft,
    Ogre Geargrabber, Auriok Survivors)
  * the Equipment-ETB class keeps ParentTarget (Embercleave) -- it is
    already rescued at runtime by
    resolve_parent_target_attachment_from_trigger. Rebinding it would be
    inert for those cards and an active regression for cards whose source
    is not attachable at all (Adaptive Armorer, Stonehewer Giant, Quest
    for the Holy Relic), which would attach a creature/artifact/instant to
    a permanent -- a state neither attachment_illegality nor the SBA sweep
    cleans up, since CR 704.5p is unimplemented.

Also stamp Duration::Permanent on the animation itself. CR 611.2a: a
continuous effect that states no duration lasts until the end of the game.
Left duration-less it fell through to the UntilEndOfTurn default in
effects/effect.rs, so at cleanup the Licid stopped being an Aura while
staying attached -- and its "Enchanted creature can't attack" static, which
keys purely on attached_to, would have locked the victim permanently. The
global default in effect.rs is deliberately untouched; its None is
overloaded at that seam.

Verified: a full-corpus parse differential against origin/main changes
exactly 12 cards -- the 12 Licids. cargo nextest -p engine 21783/21783
green, clippy -D warnings clean, parser combinator Gates A and G pass.

Known follow-ups, unchanged by this commit: "loses this ability" emits no
ability-removal modification, and "you may pay {W} to end this effect"
lowers to a mandatory PayCost that neither prompts nor ends the effect
(see phase-rs#4000). Necromancy has the same animation-duration bug via
affected: OriginalSource and is deliberately out of scope here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MnZsf4Z7wgQCsDxbjh3ucK
@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 0694aeec-9359-450f-9085-a7ebabcfb7c1

📥 Commits

Reviewing files that changed from the base of the PR and between a29df8d and 34cac82.

📒 Files selected for processing (2)
  • crates/engine-wasm/src/lib.rs
  • crates/manabrew-compat/src/lib.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • crates/manabrew-compat/src/lib.rs
  • crates/engine-wasm/src/lib.rs

📝 Walkthrough

Walkthrough

The parser now supports Licid animate-then-attach binding and permanent Aura duration. The engine, protocol, UI, AI, replay state, and tests now support paying grouped costs to end continuous effects and generalized illegal-attachment cleanup.

Changes

Licid effect lifecycle

Layer / File(s) Summary
Parser attachment and termination lowering
crates/engine/src/parser/...
Source animation is detected across chains, bare “it” can bind to SelfRef, self-scoped attachable grants become permanent, and mana termination clauses are absorbed into end_cost.
Grouped continuous-effect installation
crates/engine/src/types/..., crates/engine/src/game/effects/effect.rs
Termination permissions receive resolution-scoped groups, propagate across installed effects, and participate in replay validation.
End-effect special action
crates/engine/src/game/end_continuous_effect.rs, crates/engine/src/game/engine.rs, client/src/...
Players and AI can discover, validate, pay, resume, and commit grouped termination actions through the interaction and client layers.
Attachment cleanup and regression coverage
crates/engine/src/game/sba.rs, crates/engine/tests/integration/issue_605_calming_licid.rs
Illegal attachments are detached under the generalized SBA sweep, while Licid parsing, persistence, payment, replay, authorization, AI, and mana restrictions are tested.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant EffectChain
  participant ParseContext
  participant AttachmentParser
  participant GameState
  EffectChain->>ParseContext: record source animation
  ParseContext->>AttachmentParser: provide source attachment flag
  AttachmentParser->>GameState: attach source via SelfRef
  GameState->>GameState: retain permanent Aura animation
Loading
sequenceDiagram
  participant Player
  participant Client
  participant Engine
  participant ManaSystem
  participant GameState
  Player->>Client: select termination offer
  Client->>Engine: submit group and cost
  Engine->>ManaSystem: pay special-action cost
  ManaSystem->>Engine: return payment result
  Engine->>GameState: remove matching effect group
  GameState->>Engine: emit ContinuousEffectEnded
Loading

Possibly related PRs

  • phase-rs/phase#6306: Both changes modify duration inference for duration-less continuous grants.
  • phase-rs/phase#6331: Both changes extend resolved-command and replay handling for continuous-effect installation.
  • phase-rs/phase#6649: Both changes modify resolved-command and replay handling for transient continuous-effect installation.

Suggested labels: bug

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR includes large pay-to-end continuous-effect, UI, transport, AI, and locale work that is unrelated to #605's Licid attachment bug. Move the end-continuous-effect feature set into a separate PR or link the matching issue, and keep this change set focused on the Calming Licid fix.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the Licid attachment fix and matches the main change in the PR.
Linked Issues check ✅ Passed The Calming Licid fix in #605 is implemented: bare "it" now resolves to the Licid source and the Aura form stays permanent.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@matthewevans matthewevans self-assigned this Jul 26, 2026
@github-actions

github-actions Bot commented Jul 26, 2026

Copy link
Copy Markdown

Parse changes introduced by this PR · 13 card(s), 2 signature(s) (baseline: main ee0532670c31)

🔴 Removed (1 signature)

  • 13 cards · ➖ ability/PayCost · removed: PayCost
    • Affected (first 3): Calming Licid, Convulsing Licid, Corrupting Licid (+10 more)

🟡 Modified fields (1 signature)

  • 12 cards · 🔄 ability/grant Enchant, set card types enchantment, remove all Enchantment subtypes, add… · changed field duration: permanent
    • Affected (first 3): Calming Licid, Convulsing Licid, Corrupting Licid (+9 more)

2 card(s) had Oracle-text changes (errata/reprint) — excluded as non-parser.

@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.

Request changes — the source/recipient attachment repair is correct, but this activation now installs an effect that the printed Licid payment cannot end.

🔴 Blocker

crates/engine/src/parser/oracle_effect/subject.rs:3649 stamps the self Aura/Equipment grant as Duration::Permanent, while the same Calming Licid Oracle text at crates/engine/tests/integration/issue_605_calming_licid.rs:34 says, “You may pay {W} to end this effect.” The parser recognizes that phrase only to avoid marking the generated PayCost optional (crates/engine/src/parser/oracle_effect/assembly.rs:1936-1965); the PayCost resolver then only performs payment (crates/engine/src/game/effects/pay.rs:30-191). It carries no identity for the installed continuous effect and has no cleanup/detach continuation. This makes the newly working Licid state permanent even after its printed termination payment.

Model the ongoing Licid effect with a termination/cleanup authority that is bound to the exact effect created by this activation. The production path must offer the payment, permit decline without payment, and on payment remove that effect, detach the Licid, and restore the resulting characteristics/ability state. Please prove both branches through the real activation/payment pipeline; the present runtime test at issue_605_calming_licid.rs:177-241 only proves persistence across cleanup and never exercises the payment or termination behavior.

🟡 Non-blocking

The required current-head parse-diff artifact is absent: this engine/parser PR claims exactly 12 Licids changed, but there is no <!-- coverage-parse-diff --> comment for d8393191746a51b78bda852141b314353f69248b. Regenerate or otherwise attach the current artifact and reconcile its normalized card set with the claimed 12 before re-review.

✅ Clean

The source-versus-recipient attachment distinction is made at the parser-chain seam, and the existing activation test does exercise the real target/resolve path for that repair.

Recommendation: implement the bound termination path and its discriminating runtime coverage, then provide the current parse-diff artifact for this head.

@matthewevans matthewevans removed their assignment Jul 26, 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.

Requested changes

HIGH — restore locale-key parity

client/src/i18n/locales/en/game.json:125 adds actionButton.endContinuousEffect, but the supported sibling game.json locale files (de, es, fr, it, pl, and pt) do not contain that key. The current frontend locale-parity check therefore fails. Add the corresponding key to every supported locale (with the appropriate translation) before re-review.

MED — regenerate the current-head parse evidence and reconcile the description

The <!-- coverage-parse-diff --> sticky comment was posted at 2026-07-26T13:50:03Z, before this head's 2026-07-26T21:59:04-05:00 commit. It reports 13 cards / 2 signatures against main 6e0701b384a7, while the PR body says the current change is exactly 12 Licids. Regenerate and attach the parse-diff artifact for this exact head (20d8b09e9dcd7c0f57dbcff7d641e74d4d22f4da), then reconcile the normalized card/signature set and the stated scope.

The PR body is also stale about code now present in this head: it describes the pay-to-end path and the relevant attachment/SBA work as unimplemented, while crates/engine/src/game/end_continuous_effect.rs implements the CR 116.2c special action and its termination pipeline. Update the description so it accurately describes the reviewed head rather than treating implemented work as a known follow-up.

@matthewevans matthewevans added the enhancement New feature or request label Jul 27, 2026

@coderabbitai coderabbitai 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.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
crates/engine/src/parser/oracle_effect/tests.rs (1)

13627-13635: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Missing positive test for the new end_cost field.

All ~23 sites touched by this diff add end_cost: _ to existing Effect::GenericEffect destructuring patterns (compile-fix for the new field), but none of them — nor any other test in this file — asserts a concrete end_cost value. Since this cohort ("Parser attachment and termination lowering") is specifically about parsing the Licid pay-to-end-effect cost, this file has no test locking in that extraction actually works (e.g. that a "you may pay {U} to end this effect" clause populates end_cost with the expected AbilityCost).

Consider adding at least one test asserting end_cost: Some(...) with the expected cost for a representative Licid-style clause, mirroring the existing GenericEffect assertion style already used throughout this file.

Also applies to: 20026-20033, 20197-20205

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/engine/src/parser/oracle_effect/tests.rs` around lines 13627 - 13635,
Add a positive parser test in the existing GenericEffect assertion areas, such
as the checks around the Licid-style branch cases, that parses a representative
“you may pay {U} to end this effect” clause and asserts end_cost is Some with
the expected AbilityCost. Keep the existing static_abilities, duration, and
target assertions intact, and apply the same concrete end_cost validation to the
related assertion sites rather than leaving wildcard destructuring.
crates/engine/src/parser/oracle_trigger_tests.rs (2)

3405-3417: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Add a verified CR annotation to the exact-threshold test.

This new rules-focused test has no CR <number>: <description> annotation. Verify the applicable rule in docs/MagicCompRules.txt and document it beside the test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/engine/src/parser/oracle_trigger_tests.rs` around lines 3405 - 3417,
Verify the applicable Magic Comprehensive Rules section for the exact-threshold
scry-bottom trigger behavior, then add a nearby “CR <number>: <description>”
annotation to completed_scry_bottom_trigger_preserves_exact_threshold
documenting that rule.

Source: Path instructions


3405-3417: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Assert the lowered effect, not only execute.is_some().

The test name claims exact-threshold behavior, but it passes even if the effect body is lowered incorrectly. Match the expected Effect::Draw shape so regressions cannot produce a false green.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/engine/src/parser/oracle_trigger_tests.rs` around lines 3405 - 3417,
Strengthen completed_scry_bottom_trigger_preserves_exact_threshold by replacing
the execute.is_some() check with an assertion that the lowered effect matches
the expected Effect::Draw shape. Preserve the existing Scry mode and exact
Comparator::EQ threshold assertions while validating the trigger’s actual effect
body.

Source: Path instructions

🧹 Nitpick comments (4)
crates/engine/tests/integration/issue_605_calming_licid.rs (2)

645-664: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Dead with_mana_pool call. Line 649 seeds 2 white and is superseded by the second call at Lines 660-664 (or, if the builder appends, silently makes the pool 4W+2R rather than the documented 2W+2R). Drop the first call and keep the combined seed adjacent to the activations it funds.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/engine/tests/integration/issue_605_calming_licid.rs` around lines 645
- 664, Remove the initial with_mana_pool call that seeds 2 white mana in
ending_one_licids_effect_leaves_the_other_licid_untouched, keeping only the
later combined 2W+2R mana setup adjacent to the activations it funds.

956-963: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

V12 pushes each March static into both the live and base lists, contradicting the invariant this same file documents at Lines 1098-1100.

V13b explicitly notes that the Step-1 layer reset rebuilds static_definitions from base_static_definitions, so pushing to both applies the static twice. March's SetCardTypes-style animation is idempotent, so the row still passes — but the fixture encodes a shape production never produces, and a future non-idempotent static installed this way would double-apply silently.

♻️ Align V12 with the documented base-only pattern
     {
         let obj = runner.state_mut().objects.get_mut(&march).unwrap();
         for def in march_static.statics.iter() {
-            obj.static_definitions.push(def.clone());
             std::sync::Arc::make_mut(&mut obj.base_static_definitions).push(def.clone());
         }
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/engine/tests/integration/issue_605_calming_licid.rs` around lines 956
- 963, Update the March static setup in the shown runner state mutation to
append each cloned definition only to base_static_definitions, removing the push
into static_definitions. Preserve the subsequent layers_dirty.mark_full() call
so the live list is rebuilt from the base definitions according to the
documented invariant.

Source: Path instructions

crates/engine/src/types/resolved_commands.rs (1)

308-332: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add a roundtrip/rejection test for the new resulting_next_end_effect_group_id high-water check.

Every other allocator-receipt field in this file (resulting_next_continuous_effect_id, resulting_next_timestamp, token/attachment/object ids, etc.) has a dedicated test proving both the accept and reject path (see semantic_commands_roundtrip_and_reject_malformed_payloads and friends). The new end_permission/resulting_next_end_effect_group_id high-water check and the two new command families (ResolvedUncommittedTriggerRemovalCommand, ResolvedStackRemovalCommand) have no equivalent test in this module's tests block, so a regression that stops enforcing EndEffectGroupAboveHighWater (or corrupts the new command's cause invariant) would go undetected by this file's own test suite.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/engine/src/types/resolved_commands.rs` around lines 308 - 332, Add
tests in this module’s tests block covering the new
resulting_next_end_effect_group_id high-water validation and the
ResolvedUncommittedTriggerRemovalCommand and ResolvedStackRemovalCommand
roundtrips. Assert valid payloads are accepted, malformed payloads with an
end-effect group at or above the recorded high-water are rejected with
EndEffectGroupAboveHighWater, and each new command preserves and validates its
cause invariant, following
semantic_commands_roundtrip_and_reject_malformed_payloads.
crates/engine/src/parser/oracle_trigger.rs (1)

14760-14774: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

"you"-only scope isn't flagged as intentional.

trigger_prefix hardcodes tag("you"), so "whenever an opponent/each opponent/a player chooses to put N cards on the bottom while scrying" won't be recognized by this new arm and will fall through to the generic path (or Unimplemented). As per path instructions, every new parser arm must have its pronoun/scope variants covered or explicitly documented as out of scope.

Based on path instructions: "For every new arm, verify the plural / possessive / 'an opponent's' / 'your' / 'their' / 'non-X' / 'another' and article-word (a/an/one/two/N/X/each) variants are covered or explicitly out of scope."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/engine/src/parser/oracle_trigger.rs` around lines 14760 - 14774, The
trigger_prefix parser only accepts the literal “you” scope and misses required
pronoun and scope variants. Update the new trigger arm around trigger_prefix to
recognize the applicable opponent, player, each, possessive, and other required
scope/article variants, or explicitly document each intentionally unsupported
variant according to the path instructions; preserve the existing “when/whenever
… chooses to put” structure.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@client/src/components/board/ActionButton.tsx`:
- Around line 83-94: Remove the frontend filtering in ActionButton and consume
an engine-provided ordered end-effect offers field directly, rendering it
unchanged. Add and thread this field through every relevant engine-to-client
adapter and verify the round trip with a test, preserving engine ordering and
display data.

In `@crates/engine/src/ai_support/mod.rs`:
- Around line 1023-1042: The two GameAction priority classifiers in
crates/engine/src/ai_support/mod.rs (lines 1023-1042 and 1066-1079) must use
exhaustive matches instead of wildcard fallbacks. Add an explicit
EndContinuousEffect classification to has_meaningful_priority_action while
preserving its required meaningful result, then enumerate every remaining
GameAction variant according to the existing auto-pass semantics in
auto_pass_recommended. Ensure both matches compile-fail when future enum
variants are added.

In `@crates/engine/tests/integration/issue_605_calming_licid.rs`:
- Around line 1513-1552: Replace the isolated ManaRestriction::allows assertions
in spell_restricted_mana_cannot_pay_a_termination_cost with an integration test
using the animated_calming_licid fixture and pay_end_continuous_effect_cost
flow. Seed only spell-restricted {W} mana, assert EndContinuousEffect is not
offered or payable, then replace it with unrestricted {W} mana and assert the
action is offered and succeeds, proving the pipeline supplies
PaymentContext::SpecialAction(EndContinuousEffect).
- Around line 1242-1278: Strengthen both negative cases in
only_mana_termination_costs_are_absorbed: assert parsed.abilities.first() is
present and has a GenericEffect root before checking root_end_cost is None.
Apply the same reach guard to the wrong case, avoiding the current and_then path
that allows an empty ability list to pass silently.

---

Outside diff comments:
In `@crates/engine/src/parser/oracle_effect/tests.rs`:
- Around line 13627-13635: Add a positive parser test in the existing
GenericEffect assertion areas, such as the checks around the Licid-style branch
cases, that parses a representative “you may pay {U} to end this effect” clause
and asserts end_cost is Some with the expected AbilityCost. Keep the existing
static_abilities, duration, and target assertions intact, and apply the same
concrete end_cost validation to the related assertion sites rather than leaving
wildcard destructuring.

In `@crates/engine/src/parser/oracle_trigger_tests.rs`:
- Around line 3405-3417: Verify the applicable Magic Comprehensive Rules section
for the exact-threshold scry-bottom trigger behavior, then add a nearby “CR
<number>: <description>” annotation to
completed_scry_bottom_trigger_preserves_exact_threshold documenting that rule.
- Around line 3405-3417: Strengthen
completed_scry_bottom_trigger_preserves_exact_threshold by replacing the
execute.is_some() check with an assertion that the lowered effect matches the
expected Effect::Draw shape. Preserve the existing Scry mode and exact
Comparator::EQ threshold assertions while validating the trigger’s actual effect
body.

---

Nitpick comments:
In `@crates/engine/src/parser/oracle_trigger.rs`:
- Around line 14760-14774: The trigger_prefix parser only accepts the literal
“you” scope and misses required pronoun and scope variants. Update the new
trigger arm around trigger_prefix to recognize the applicable opponent, player,
each, possessive, and other required scope/article variants, or explicitly
document each intentionally unsupported variant according to the path
instructions; preserve the existing “when/whenever … chooses to put” structure.

In `@crates/engine/src/types/resolved_commands.rs`:
- Around line 308-332: Add tests in this module’s tests block covering the new
resulting_next_end_effect_group_id high-water validation and the
ResolvedUncommittedTriggerRemovalCommand and ResolvedStackRemovalCommand
roundtrips. Assert valid payloads are accepted, malformed payloads with an
end-effect group at or above the recorded high-water are rejected with
EndEffectGroupAboveHighWater, and each new command preserves and validates its
cause invariant, following
semantic_commands_roundtrip_and_reject_malformed_payloads.

In `@crates/engine/tests/integration/issue_605_calming_licid.rs`:
- Around line 645-664: Remove the initial with_mana_pool call that seeds 2 white
mana in ending_one_licids_effect_leaves_the_other_licid_untouched, keeping only
the later combined 2W+2R mana setup adjacent to the activations it funds.
- Around line 956-963: Update the March static setup in the shown runner state
mutation to append each cloned definition only to base_static_definitions,
removing the push into static_definitions. Preserve the subsequent
layers_dirty.mark_full() call so the live list is rebuilt from the base
definitions according to the documented invariant.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: de4ffccb-17dc-4cee-8c7e-b4cc3b0e3a44

📥 Commits

Reviewing files that changed from the base of the PR and between d839319 and 20d8b09.

⛔ Files ignored due to path filters (2)
  • client/src/adapter/generated/interaction/index.ts is excluded by !**/generated/**
  • crates/engine/tests/fixtures/cr733/authority_matrix.json.gz is excluded by !**/*.gz
📒 Files selected for processing (87)
  • client/src/adapter/types.ts
  • client/src/components/board/ActionButton.tsx
  • client/src/i18n/locales/en/game.json
  • crates/engine/src/ai_support/candidates.rs
  • crates/engine/src/ai_support/mod.rs
  • crates/engine/src/database/synthesis.rs
  • crates/engine/src/database/unearth.rs
  • crates/engine/src/game/ability_rw.rs
  • crates/engine/src/game/ability_utils.rs
  • crates/engine/src/game/casting_tests.rs
  • crates/engine/src/game/combat.rs
  • crates/engine/src/game/combat_damage.rs
  • crates/engine/src/game/coverage.rs
  • crates/engine/src/game/dungeon.rs
  • crates/engine/src/game/effects/copy_spell.rs
  • crates/engine/src/game/effects/effect.rs
  • crates/engine/src/game/effects/mod.rs
  • crates/engine/src/game/end_continuous_effect.rs
  • crates/engine/src/game/engine.rs
  • crates/engine/src/game/engine_tests.rs
  • crates/engine/src/game/interaction.rs
  • crates/engine/src/game/layers.rs
  • crates/engine/src/game/log.rs
  • crates/engine/src/game/mana_abilities.rs
  • crates/engine/src/game/mana_sources.rs
  • crates/engine/src/game/meld_tests.rs
  • crates/engine/src/game/mod.rs
  • crates/engine/src/game/printed_cards.rs
  • crates/engine/src/game/public_state.rs
  • crates/engine/src/game/sba.rs
  • crates/engine/src/game/scenario.rs
  • crates/engine/src/game/trigger_index.rs
  • crates/engine/src/game/trigger_matchers.rs
  • crates/engine/src/game/triggers.rs
  • crates/engine/src/parser/clause_shell.rs
  • crates/engine/src/parser/oracle.rs
  • crates/engine/src/parser/oracle_effect/assembly.rs
  • crates/engine/src/parser/oracle_effect/imperative.rs
  • crates/engine/src/parser/oracle_effect/lower.rs
  • crates/engine/src/parser/oracle_effect/mod.rs
  • crates/engine/src/parser/oracle_effect/sequence.rs
  • crates/engine/src/parser/oracle_effect/snapshot_tests.rs
  • crates/engine/src/parser/oracle_effect/subject.rs
  • crates/engine/src/parser/oracle_effect/tests.rs
  • crates/engine/src/parser/oracle_ir/ast.rs
  • crates/engine/src/parser/oracle_modal.rs
  • crates/engine/src/parser/oracle_replacement.rs
  • crates/engine/src/parser/oracle_tests.rs
  • crates/engine/src/parser/oracle_trigger.rs
  • crates/engine/src/parser/oracle_trigger_tests.rs
  • crates/engine/src/parser/oracle_vote.rs
  • crates/engine/src/parser/swallow_check.rs
  • crates/engine/src/types/ability.rs
  • crates/engine/src/types/action_stable_order.rs
  • crates/engine/src/types/actions.rs
  • crates/engine/src/types/events.rs
  • crates/engine/src/types/game_state.rs
  • crates/engine/src/types/interaction.rs
  • crates/engine/src/types/mana.rs
  • crates/engine/src/types/resolved_commands.rs
  • crates/engine/src/types/statics.rs
  • crates/engine/tests/integration/issue_2904_life_of_the_party.rs
  • crates/engine/tests/integration/issue_605_calming_licid.rs
  • crates/engine/tests/integration/issue_6566_granted_leave_exile.rs
  • crates/engine/tests/integration/main.rs
  • crates/engine/tests/integration/mjolnir_hammer_double_damage.rs
  • crates/engine/tests/integration/najeela_extra_combat_grant_2898.rs
  • crates/engine/tests/integration/springheart_nantuko_bestow_landfall.rs
  • crates/engine/tests/integration/springheart_realdb_repro.rs
  • crates/engine/tests/integration/std_longtail_e.rs
  • crates/manabrew-compat/src/lib.rs
  • crates/mtgish-import/src/convert/action.rs
  • crates/mtgish-import/src/convert/mod.rs
  • crates/phase-ai/src/features/tests/poison.rs
  • crates/phase-ai/src/policies/anti_self_harm.rs
  • crates/phase-ai/src/policies/effect_classify.rs
  • crates/phase-ai/src/policies/land_animation.rs
  • crates/phase-ai/src/policies/planeswalker_loyalty.rs
  • crates/phase-ai/src/policies/reactive_self_protection.rs
  • crates/phase-ai/src/policies/redundancy_avoidance.rs
  • crates/phase-ai/src/policies/sacrifice_land_protection.rs
  • crates/phase-ai/src/policies/self_cost_value.rs
  • crates/phase-ai/src/policies/self_protection_classify.rs
  • crates/phase-ai/src/policies/tests/poison.rs
  • crates/phase-ai/src/policies/x_cast_gate.rs
  • crates/phase-ai/src/search.rs
  • crates/server-core/src/game_action_payload_guard.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • crates/engine/tests/integration/main.rs
  • crates/engine/src/parser/oracle_effect/imperative.rs

Comment thread client/src/components/board/ActionButton.tsx Outdated
Comment thread crates/engine/src/ai_support/mod.rs Outdated
Comment thread crates/engine/tests/integration/issue_605_calming_licid.rs
Comment thread crates/engine/tests/integration/issue_605_calming_licid.rs
@mike-theDude

Copy link
Copy Markdown
Collaborator Author

Addressed the requested Licid termination blocker in 53233d96e and updated the branch from main in 20d8b09e9.

  • The parser now absorbs “You may pay {…} to end this effect” into the exact continuous-effect-producing GenericEffect instead of emitting a resolution-time PayCost.
  • Resolution mints a stable per-resolution effect group; the engine offers a CR 116.2c special action only to the player meant by “you” and only when its cost is payable.
  • The action pays through the restriction-aware special-action mana pipeline, removes only that group, restores layer-derived types/abilities, and runs CR 704.5p SBAs to detach the reverted Licid.
  • The frontend renders the engine-authored action/cost without deriving game rules client-side.
  • CR 733 replay now carries the group allocator high-water so replay cannot reuse an effect-group identity.

Discriminating coverage uses the real activation/legal-action/payment pipeline for both branches: payment spends the sole white source, ends the exact effect, restores the activated ability, detaches the Licid, and restores attack legality; declining crosses cleanup without spending the termination mana or ending the effect. The suite also covers two independent Licids, wrong-player/affordability rejection, restricted mana, priority timing, replay, and generic CR 704.5p attachment behavior.

Local verification on the current merged head: parser gates pass; 28/28 issue #605 tests pass; CR 733 census/replay tests pass; frontend protocol/type-check passes; ESLint has 0 errors; focused server payload-bound and action-order tests pass. The card-data workflow is running now and should refresh the <!-- coverage-parse-diff --> artifact for this head.

@matthewevans ready for re-review once the refreshed checks settle.

@mike-theDude

Copy link
Copy Markdown
Collaborator Author

Current-head review follow-up is pushed in a29df8d25.

  • Added actionButton.endContinuousEffect to all six sibling locales; locale parity is 67/67.
  • Replaced the stale PR body with the implemented termination/SBA/replay/frontend behavior and reconciled the scope: 12 Aura-form Licids change attachment/duration, while 13 cards remove the obsolete PayCost (the same 12 plus Flanking Licid).
  • Moved the ordered pay-to-end offer projection into the engine and threaded it through every client adapter/snapshot path; the frontend renders it unchanged.
  • Made the two priority classifiers consume one exhaustive GameAction census.
  • Strengthened the positive parser assertion, both negative reach guards, and the restricted-mana production-pipeline test.
  • Removed the duplicate mana seed and fixed the March fixture to mutate only base_static_definitions.

The CodeRabbit notes about the scry-bottom trigger and its "you" scope refer to code already present on current main; the PR diff only adds the required end_cost destructuring there, so those unrelated upstream changes were left untouched. The CR 733 termination-group high-water already has a malformed-receipt rejection and replay-allocation regression in the issue #605 integration suite.

Local current-head verification: targeted Clippy with -D warnings; 28/28 issue tests; parser Gates A/G; focused parser, engine projection, and server round-trip tests; frontend protocol/type-check; 114 focused frontend tests; locale parity 67/67; ESLint 0 errors.

The card-data workflow will refresh the sticky <!-- coverage-parse-diff --> comment for exact head a29df8d2512471b78bd9355b13c3de7ec277db45 during this CI cycle.

@matthewevans re-review requested.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/engine/src/ai_support/mod.rs`:
- Around line 1058-1060: Update the comment near the loop-firewall logic to
retain CR 116.2c and CR 104.4b, and remove CR 732.4 unless you can replace it
with a verified current rule citation that directly supports the claim.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 196d6c34-67b8-476a-9d1f-140af3077ec7

📥 Commits

Reviewing files that changed from the base of the PR and between 20d8b09 and a29df8d.

📒 Files selected for processing (36)
  • client/src/adapter/server-draft-adapter.ts
  • client/src/adapter/types.ts
  • client/src/adapter/ws-adapter.ts
  • client/src/components/board/ActionButton.tsx
  • client/src/game/sessionCleanup.ts
  • client/src/i18n/locales/de/game.json
  • client/src/i18n/locales/es/game.json
  • client/src/i18n/locales/fr/game.json
  • client/src/i18n/locales/it/game.json
  • client/src/i18n/locales/pl/game.json
  • client/src/i18n/locales/pt/game.json
  • client/src/network/__tests__/protocol.test.ts
  • client/src/network/protocol.ts
  • client/src/providers/GameProvider.tsx
  • client/src/stores/__tests__/gameStoreCommitGate.test.ts
  • client/src/stores/gameStore.ts
  • client/src/stores/multiplayerDraftStore.ts
  • client/src/stores/replayStore.ts
  • client/src/test/factories/gameStateFactory.ts
  • crates/engine-wasm/src/lib.rs
  • crates/engine/src/ai_support/mod.rs
  • crates/engine/src/game/ability_rw.rs
  • crates/engine/src/game/coverage.rs
  • crates/engine/src/game/effects/mod.rs
  • crates/engine/src/game/triggers.rs
  • crates/engine/src/parser/oracle_effect/imperative.rs
  • crates/engine/src/parser/oracle_effect/lower.rs
  • crates/engine/src/parser/oracle_effect/tests.rs
  • crates/engine/src/parser/oracle_ir/ast.rs
  • crates/engine/src/parser/swallow_check.rs
  • crates/engine/src/types/ability.rs
  • crates/engine/src/types/events.rs
  • crates/engine/tests/integration/issue_605_calming_licid.rs
  • crates/engine/tests/integration/main.rs
  • crates/phase-server/src/main.rs
  • crates/server-core/src/protocol.rs
🚧 Files skipped from review as they are similar to previous changes (13)
  • crates/engine/tests/integration/main.rs
  • crates/engine/src/game/effects/mod.rs
  • crates/engine/src/game/triggers.rs
  • crates/engine/src/types/events.rs
  • crates/engine/src/parser/oracle_ir/ast.rs
  • client/src/components/board/ActionButton.tsx
  • crates/engine/src/parser/swallow_check.rs
  • crates/engine/src/types/ability.rs
  • crates/engine/src/game/ability_rw.rs
  • crates/engine/src/parser/oracle_effect/lower.rs
  • crates/engine/src/game/coverage.rs
  • crates/engine/src/parser/oracle_effect/imperative.rs
  • crates/engine/tests/integration/issue_605_calming_licid.rs

Comment on lines +1058 to +1060
// CR 116.2c + CR 732.4 + CR 104.4b: paying to end an effect is
// optional, so it is meaningful to the loop firewall even though the
// own-upkeep/draw classifier below deliberately auto-passes it.

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.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Correct the invalid CR citation.

CR 732.4 is not a current Comprehensive Rules provision. Keep CR 116.2c for the pay-to-end special action and CR 104.4b for the optional-action loop distinction, but remove or replace CR 732.4 with the verified rule that actually supports the claim. (magiccomprehensiverules.com)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/engine/src/ai_support/mod.rs` around lines 1058 - 1060, Update the
comment near the loop-firewall logic to retain CR 116.2c and CR 104.4b, and
remove CR 732.4 unless you can replace it with a verified current rule citation
that directly supports the claim.

Source: Path instructions

@mike-theDude

Copy link
Copy Markdown
Collaborator Author

The current-head parse-diff sticky comment has now refreshed from the successful card-data job: baseline main 12fb17a, 13 cards with PayCost removed, and 12 Aura-form Licids with duration changed to permanent. This matches the reconciled PR description. @matthewevans

@matthewevans matthewevans self-assigned this Jul 27, 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 — current head a29df8d2512471b78bd9355b13c3de7ec277db45

The prior requested changes are resolved at the reviewed head.

  • ActionButton renders the engine-projected ordered endContinuousEffectOffers unchanged; the value is threaded through the WASM, WebSocket, P2P, server-draft, snapshot, and cleanup paths.
  • actionButton.endContinuousEffect now exists in every supported locale.
  • The priority classifier is exhaustive over GameAction, including an explicit EndContinuousEffect classification.
  • The current parse-diff artifact was updated at 2026-07-27T11:25:07Z; its full artifact confirms exactly 13 PayCost removals (the 12 Aura-form Licids plus Flanking Licid) and exactly the 12 permanent-duration changes claimed in the description.

Manual Quality Gate: PASS. The change is at the engine action/priority authority, and the issue #605 integration suite drives the real activation, legal-action offer, payment, removal, layer/SBA cleanup, and decline paths. CI is green for Rust lint/tests, card-data/coverage, frontend, WASM, and Tauri; the remaining AI checks are in progress and auto-merge will wait for them.

The current CodeRabbit CR 732.4 concern is refuted by the checked-in Comprehensive Rules: docs/MagicCompRules.txt:6383 defines 732.4 for mandatory-action loops, which together with 104.4b directly supports the optional-action distinction in the comment.

@matthewevans
matthewevans added this pull request to the merge queue Jul 27, 2026
@matthewevans matthewevans removed their assignment Jul 27, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jul 27, 2026
@matthewevans matthewevans self-assigned this Jul 27, 2026
@matthewevans
matthewevans added this pull request to the merge queue Jul 27, 2026
@matthewevans matthewevans removed their assignment Jul 27, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jul 27, 2026
@matthewevans matthewevans self-assigned this Jul 27, 2026
matthewevans and others added 2 commits July 27, 2026 06:07
Port the Licid pay-to-end action across the current exhaustive Manabrew capability registry.

Co-authored-by: mike-theDude <mbriningstool@gmail.com>

@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 — current maintainer-port head 34cac82ce1767b735d830a464b2f7cb9b8e940fb

The current-head maintainer port is clean: the end-effect capability remains registered at the engine action/priority authority and its exhaustiveness contract discriminates the new action; no related emitter or sibling registry was missed.

The prior CodeRabbit CR 732.4 concern is refuted by the checked-in Comprehensive Rules (docs/MagicCompRules.txt:6383): it defines the mandatory-action loop rule, which is relevant alongside CR 116.2c and CR 104.4b for the optional-action distinction. CodeRabbit's review of this exact head reports no actionable comments.

No manual Quality Gate assertion is made for this maintainer-port head: the parse-diff sticky artifact was not refreshed after the port. The merge queue will wait for the current pending checks.

@matthewevans
matthewevans enabled auto-merge July 27, 2026 13:18
@matthewevans matthewevans removed their assignment Jul 27, 2026
@matthewevans
matthewevans added this pull request to the merge queue Jul 27, 2026
Merged via the queue into phase-rs:main with commit 6b29e0d Jul 27, 2026
18 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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Card Bug] calming licid effect doesn't work

3 participants