Skip to content

feat(engine): support Ozai conditional mana static - #6795

Merged
matthewevans merged 5 commits into
phase-rs:mainfrom
parthmishra:codex/ozai-phoenix-king
Jul 29, 2026
Merged

feat(engine): support Ozai conditional mana static#6795
matthewevans merged 5 commits into
phase-rs:mainfrom
parthmishra:codex/ozai-phoenix-king

Conversation

@parthmishra

@parthmishra parthmishra commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Parse total unspent-mana threshold conditions such as Ozai’s “six or more unspent mana.”
  • Evaluate controller-relative printed and granted static conditions at their application layer, so they use the object’s effective controller after layer-2 control changes.
  • Keep resolution-created continuous effects bound to their captured controller, including combat assignment rules.

Validation

  • Focused parser, layer, combat, and Ozai runtime regressions.
  • Pre-push Rust lint, parser suite (608 tests), phase-AI suite (1,854 tests), card-data validation, and coverage regression check passed.
  • Coverage adds Ozai, the Phoenix King as newly supported.

Validation note

The local frontend lint could not start because pnpm requires build-script approval while bootstrapping dependencies. No client code is changed; GitHub CI remains authoritative for this check.

Summary by CodeRabbit

  • New Features
    • Added Oracle parsing for “unspent mana” in “or more” quantity conditions.
    • Enhanced controller-aware condition evaluation for active/continuous/combat effects and off-zone keyword effects.
  • Bug Fixes
    • Fixed “you” resolution for conditions when effects originate from transient sources or combat-assignment effects, preserving the captured controller.
    • Corrected Ozai unspent-mana color replacement behavior at the Cleanup step after phase transitions.
  • Tests
    • Added and extended unit/integration scenarios covering unspent-mana parsing and controller correctness across layer timing.

@superagent-security

Copy link
Copy Markdown

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

@coderabbitai

coderabbitai Bot commented Jul 29, 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: 41711c94-bcdf-4e98-80e9-b036e39121f0

📥 Commits

Reviewing files that changed from the base of the PR and between 4daac3a and 2fbccc9.

📒 Files selected for processing (2)
  • crates/engine/src/game/layers.rs
  • crates/engine/tests/integration/main.rs

📝 Walkthrough

Walkthrough

The parser now supports “or more unspent mana” conditions. Layer evaluation resolves captured or current controllers for transient and non-transient effects, with updated consumers and tests covering mana gates, controller changes, combat assignments, and cleanup transitions.

Changes

Unspent mana conditions

Layer / File(s) Summary
Parse and lower unspent mana gates
crates/engine/src/parser/oracle_nom/condition.rs, crates/engine/src/parser/oracle_tests.rs
The parser recognizes unspent mana thresholds, and tests verify lowering into mana transformation and keyword-gating statics.
Controller-aware layer evaluation
crates/engine/src/game/layers.rs
Layered effects select captured controllers for transient effects and current source controllers for non-transient effects while retaining conditions for later evaluation.
Condition consumers and layer regression tests
crates/engine/src/game/layers.rs, crates/engine/src/game/off_zone_characteristics.rs, crates/engine/src/game/sba.rs
Condition consumers use the controller-selection helper, with tests covering printed, transient, granted, and combat-assignment conditions.
Ozai integration scenario
crates/engine/tests/integration/main.rs, crates/engine/tests/integration/ozai_phoenix_king_unspent_mana.rs
Integration tests validate unspent mana gates across mana payment, controller changes, and cleanup-phase color replacement.

Estimated code review effort: 4 (Complex) | ~45 minutes

Suggested labels: enhancement

Suggested reviewers: matthewevans, andriypolanski, claytonlin1110, invalidcards

Sequence Diagram(s)

sequenceDiagram
  participant OracleText
  participant Parser
  participant LayerSystem
  participant GameState
  OracleText->>Parser: parse unspent mana condition
  Parser->>LayerSystem: lower conditional static effect
  LayerSystem->>GameState: evaluate with selected controller
  GameState->>LayerSystem: return keyword applicability
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 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 main change: engine support for Ozai's conditional mana static abilities.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@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

Caution

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

⚠️ Outside diff range comments (1)
crates/engine/src/game/off_zone_characteristics.rs (1)

176-191: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use the transient controller in crates/engine/src/game/sba.rs#L1530-L1536. matches_target_filter still builds its context from FilterContext::from_source(...), so a transient grant can evaluate the recipient set under a different controller than active_effect_condition_controller(...) uses for the condition. Mirror the layer path here and thread the same controller into both checks.

🤖 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/game/off_zone_characteristics.rs` around lines 176 - 191,
The off-zone recipient filter and condition use different controllers for
transient grants. In crates/engine/src/game/off_zone_characteristics.rs lines
176-191, reuse the controller from active_effect_condition_controller for
FilterContext::from_source_with_controller and the condition evaluation; in
crates/engine/src/game/sba.rs lines 1530-1536, update matches_target_filter to
build its context with that same transient controller so both checks agree.
🧹 Nitpick comments (2)
crates/engine/src/game/layers.rs (2)

5985-5994: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Retained non-recipient conditions are now re-evaluated per candidate object on the layer hot path.

Because printed/granted statics no longer gate at collection time (Line 4306, Line 4471, Line 5092), every retained condition is evaluated inside the affected-set filter — once per candidate, per effect, per pass. For a board-wide affected filter with a condition that itself scans state (IsPresent { filter }, QuantityComparison over ObjectCount), this turns one gather-time evaluation into O(candidates) evaluations.

Deferring the evaluation is required for the layer-2 controller fix, but the result is invariant across recipients whenever condition_uses_recipient_context(condition) is false — that case can be evaluated once per effect (after the controller is resolved) and reused for the whole candidate scan, preserving the new semantics.

🤖 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/game/layers.rs` around lines 5985 - 5994, Update the
affected-set filtering around evaluate_condition_with_recipient to evaluate
non-recipient-dependent conditions once per effect after resolving the
controller, then reuse that result for every candidate; retain per-candidate
evaluation for conditions where condition_uses_recipient_context(condition) is
true, preserving the deferred layer-2 controller semantics.

922-953: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Collapse the two identical controller-selection helpers.

active_effect_condition_controller and combat_effect_condition_controller have byte-identical bodies and differ only in the effect type. A single private helper over the three fields keeps the CR 109.5 / CR 611.2c rule in one place, so a future change to the discrimination can't drift between the object path and the combat path.

♻️ Suggested shared helper
+fn condition_controller(
+    state: &GameState,
+    transient_id: Option<u64>,
+    controller: PlayerId,
+    source_id: ObjectId,
+) -> PlayerId {
+    if transient_id.is_some() {
+        controller
+    } else {
+        state
+            .objects
+            .get(&source_id)
+            .map_or(controller, |source| source.controller)
+    }
+}
+
 pub(crate) fn active_effect_condition_controller(
     state: &GameState,
     effect: &ActiveContinuousEffect,
 ) -> PlayerId {
-    if effect.transient_id.is_some() {
-        effect.controller
-    } else {
-        state
-            .objects
-            .get(&effect.source_id)
-            .map_or(effect.controller, |source| source.controller)
-    }
+    condition_controller(state, effect.transient_id, effect.controller, effect.source_id)
 }
 
 fn combat_effect_condition_controller(
     state: &GameState,
     effect: &ActiveCombatAssignmentRuleEffect,
 ) -> PlayerId {
-    if effect.transient_id.is_some() {
-        effect.controller
-    } else {
-        state
-            .objects
-            .get(&effect.source_id)
-            .map_or(effect.controller, |source| source.controller)
-    }
+    condition_controller(state, effect.transient_id, effect.controller, effect.source_id)
 }
🤖 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/game/layers.rs` around lines 922 - 953, Collapse
active_effect_condition_controller and combat_effect_condition_controller into
one private shared helper that accepts the needed transient_id, controller, and
source_id values, preserving the existing transient-versus-source-controller
selection behavior. Update both call sites to use the shared helper and remove
the duplicate function bodies.
🤖 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/tests/integration/ozai_phoenix_king_unspent_mana.rs`:
- Line 11: Extend the Ozai integration test around OZAI_ORACLE to advance
through a cleanup step with unspent mana and assert that the mana is converted
to red, while also verifying Ozai’s six-mana flying/indestructible condition at
the boundary. Preserve the existing casting-spend assertions and use the test’s
established game-step and mana assertion helpers.

---

Outside diff comments:
In `@crates/engine/src/game/off_zone_characteristics.rs`:
- Around line 176-191: The off-zone recipient filter and condition use different
controllers for transient grants. In
crates/engine/src/game/off_zone_characteristics.rs lines 176-191, reuse the
controller from active_effect_condition_controller for
FilterContext::from_source_with_controller and the condition evaluation; in
crates/engine/src/game/sba.rs lines 1530-1536, update matches_target_filter to
build its context with that same transient controller so both checks agree.

---

Nitpick comments:
In `@crates/engine/src/game/layers.rs`:
- Around line 5985-5994: Update the affected-set filtering around
evaluate_condition_with_recipient to evaluate non-recipient-dependent conditions
once per effect after resolving the controller, then reuse that result for every
candidate; retain per-candidate evaluation for conditions where
condition_uses_recipient_context(condition) is true, preserving the deferred
layer-2 controller semantics.
- Around line 922-953: Collapse active_effect_condition_controller and
combat_effect_condition_controller into one private shared helper that accepts
the needed transient_id, controller, and source_id values, preserving the
existing transient-versus-source-controller selection behavior. Update both call
sites to use the shared helper and remove the duplicate function bodies.
🪄 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: 42245289-1a46-4fa3-8591-1b021e51804e

📥 Commits

Reviewing files that changed from the base of the PR and between 6719cc5 and 8270e64.

📒 Files selected for processing (7)
  • crates/engine/src/game/layers.rs
  • crates/engine/src/game/off_zone_characteristics.rs
  • crates/engine/src/game/sba.rs
  • crates/engine/src/parser/oracle_nom/condition.rs
  • crates/engine/src/parser/oracle_tests.rs
  • crates/engine/tests/integration/main.rs
  • crates/engine/tests/integration/ozai_phoenix_king_unspent_mana.rs

Comment thread crates/engine/tests/integration/ozai_phoenix_king_unspent_mana.rs
@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown

Parse changes introduced by this PR · 1 card(s), 1 signature(s) (baseline: main fd53e6af9c07)

🟡 Modified fields (1 signature)

  • 1 card · 🔄 static/Continuous · changed field conditional: unrecognizedunspent mana you have ≥ 6
    • Affected (first 3): Ozai, the Phoenix King

@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/game/layers.rs`:
- Around line 950-966: Add a verified CR 109.5 citation to the documentation
comment directly above condition_controller, keeping the existing explanation of
the “you”/“your” resolution behavior and leaving the function implementation
unchanged.
🪄 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: 2aa1cfe3-c8a3-4c53-93f4-7068efa36c15

📥 Commits

Reviewing files that changed from the base of the PR and between 8270e64 and 956e33c.

📒 Files selected for processing (4)
  • crates/engine/src/game/layers.rs
  • crates/engine/src/game/off_zone_characteristics.rs
  • crates/engine/src/game/sba.rs
  • crates/engine/tests/integration/ozai_phoenix_king_unspent_mana.rs

Comment thread crates/engine/src/game/layers.rs

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

MED — This transient combat-assignment path loses the controller snapshot needed for controller-relative filters.

At crates/engine/src/game/layers.rs:4966, the effect builds FilterContext::from_source(...), which reads the source's current controller. The adjacent condition evaluation (lines 4972–4978) correctly uses the captured controller. If the source changes control after the assignment is created, a condition such as “creatures you control” can therefore apply to the new controller’s creatures instead of the controller that created the assignment.

Please compute the condition controller once and pass it to FilterContext::from_source_with_controller(...). Extend the test beyond SpecificObject: exercise ControllerRef::You with P0/P1, asserting that P0’s intended recipients receive the effect and P1’s do not after the source is stolen.

Please also audit the likely copied branch at layers.rs:7134/7142 for the same snapshot/live-controller mismatch.

@matthewevans matthewevans self-assigned this Jul 29, 2026
@matthewevans matthewevans added enhancement New feature or request quality For high-quality minimal to no-churn PRs labels Jul 29, 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 review complete: controller provenance and runtime regression coverage are sound.

@matthewevans
matthewevans enabled auto-merge July 29, 2026 23:11
@matthewevans matthewevans removed their assignment Jul 29, 2026
@matthewevans
matthewevans added this pull request to the merge queue Jul 29, 2026
Merged via the queue into phase-rs:main with commit bf3b9d9 Jul 29, 2026
14 of 15 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