Skip to content

fix(crew): pilot tokens crew for adjusted power, not raw or doubled#5154

Merged
matthewevans merged 3 commits into
phase-rs:mainfrom
JacobWoodson:ship/fix-pilot-token-crew-power
Jul 6, 2026
Merged

fix(crew): pilot tokens crew for adjusted power, not raw or doubled#5154
matthewevans merged 3 commits into
phase-rs:mainfrom
JacobWoodson:ship/fix-pilot-token-crew-power

Conversation

@JacobWoodson

Copy link
Copy Markdown
Contributor

A Pilot token that "crews Vehicles as though its power were 2 greater"
(Shorikai, Genesis Engine / Kotori, Pilot Prodigy) crewed for the wrong
amount through two independent bugs:

  • Double-grant: the token carried the crew static twice -- once from the
    creating effect's with "..." clause and again from the catalog art
    preset's rules_text -- so a 1/1 Pilot contributed 5 (1+2+2). When the
    1/1 body matched a different printing's art (e.g. the Aetherdrift
    "saddles Mounts and crews Vehicles" Pilot), the two statics had
    different keyword actions and slipped past an exact-match de-dupe. Per
    CR 111.4 the creating effect's grant is authoritative, so the catalog
    rules_text fallback is now display-only and injects no functional
    abilities when the token already carries granted statics.

  • Frontend re-derived crew power: the crew-selection UI summed each
    creature's raw printed power (1) instead of the engine's adjusted crew
    contribution, gating the lone Pilot at 1 < N and blocking the crew
    ("crews for just 1"). The engine now sends per-creature contributions
    on the CrewVehicle choice and the view-model gates on those.

Result: a 1/1 Pilot crews for 3; a 2/2 (with a +1/+1 counter) for 4.

Co-Authored-By: Claude Opus 4.8 [email protected]

@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 introduces index-aligned contributions to WaitingFor::CrewVehicle to ensure the UI gates vehicle crewing on engine-validated adjusted power (e.g., for Pilot tokens or Giant Ox) rather than raw printed power. It also fixes a bug where catalog presets duplicated abilities already granted by a token's creating effect. Feedback highlights that SaddleMount is missing a parallel contributions field to support its own power modifiers, and that skipping catalog injection entirely when static_definitions is populated will break predefined tokens (like Treasure or Food) when they are created with additional abilities (e.g., haste).

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.

/// power were N greater" (Pilot tokens) and "using its toughness" (Giant
/// Ox) mean the contribution differs from the creature's printed power, so
/// the UI MUST sum these values, not raw power, when gating the selection.
contributions: Vec<i32>,

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.

high

[HIGH] SaddleMount is missing parallel contributions field. Evidence: crates/engine/src/types/game_state.rs:3262.\n> Why it matters: The direct sibling SaddleMount (CR 702.171a) also supports power contribution modifiers (e.g., Aetherdrift Pilot's "saddles Mounts ... as though its power were 2 greater"), but lacks the contributions field, which will cause the UI to incorrectly gate saddle selection on raw power.\n> Suggested fix: Add contributions: Vec<i32> to WaitingFor::SaddleMount and wire it through the engine and frontend similarly to CrewVehicle.

References
  1. Universal Review Lenses -> L2. Sibling coverage: If a fix landed in one site of a class, check if the siblings need the same fix. (link)

Comment on lines +2541 to +2546
if !obj.static_definitions.is_empty() {
if obj.token_rules_text.is_none() {
obj.token_rules_text = Some(rules_text.to_string());
}
return;
}

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.

high

[HIGH] Skipping catalog injection when static_definitions is not empty breaks predefined tokens with additional abilities. Evidence: crates/engine/src/game/effects/token.rs:2541.\n> Why it matters: If a predefined token (such as a Treasure, Food, or Clue) is created with an additional granted ability (e.g., "Create a Treasure token with haste"), static_definitions will not be empty, causing the engine to skip injecting the token's defining catalog abilities (like the Treasure's mana ability).\n> Suggested fix: Only skip functional injection if the token is not a predefined token (e.g., by checking against a list of predefined token names like "Treasure", "Food", "Clue", "Blood", "Map", "Powerstone", "Junk", "Incubator"), or by checking if the preset itself is marked as predefined.

    let is_predefined = matches!(
        obj.name.as_str(),
        "Treasure" | "Food" | "Clue" | "Blood" | "Map" | "Powerstone" | "Junk" | "Incubator"
    );
    if !is_predefined && !obj.static_definitions.is_empty() {
        if obj.token_rules_text.is_none() {
            obj.token_rules_text = Some(rules_text.to_string());
        }
        return;
    }
References
  1. Strict fidelity to the MTG Comprehensive Rules (CR) — every game rule, validation, and computed value matches the CR exactly. (link)

@matthewevans matthewevans added bug Bug fix defer-fe Frontend/client/UI PR deferred to Matt's direct review labels Jul 5, 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.

Thanks for the PR. I found a few blockers before this can move forward.

  • crates/phase-ai/src/policies/payment_selection.rs still constructs WaitingFor::CrewVehicle without the new required contributions field, so the all-targets/test build path will fail before the behavior can be validated. Please thread contributions into that test helper, ideally from the same contribution calculation used by the engine.
  • The engine now computes saddle contribution through object_crew_power_contribution(..., CrewAction::Saddle), but client/src/viewmodel/gameStateView.ts still sends no contribution map for WaitingFor::SaddleMount. That leaves the frontend confirm gate on raw power for saddle even when the engine accepts adjusted Pilot-token saddle contribution.
  • WaitingFor::CrewVehicle.contributions is a new serialized field without a serde default. Saved/debug-exported games paused in a CrewVehicle prompt before this PR would fail to deserialize on restore. Please add a safe default/normalization path for old waiting states.
  • The token ability comment in crates/engine/src/game/effects/token.rs cites CR 111.4, but that rule is about token names/subtypes, not the token abilities/characteristics being implemented here. Please verify and cite the correct CR rule before landing.

Because this PR also changes frontend/view-model files, frontend acceptance is deferred to Matt under the current sweep policy; I’m not approving or enqueueing it from the automation sweep.

@github-actions

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown

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

4 card(s) · ability/RemoveCounter · field target: permanentin battlefield permanent or in exile with suspend 1+ time counters card

Examples: Fury Charm, Shivan Sand-Mage, Timebender (+1 more)

1 card(s) · ability/ChangeZone · removed: ChangeZone (target=parent target, to=exile)

Examples: Containment Priest

1 card(s) · replacement/ChangeZone · added: ChangeZone

Examples: Containment Priest

1 card(s) · ability/ChooseCounterAdjustment · added: ChooseCounterAdjustment

Examples: Clockspinning

1 card(s) · ability/TargetOnly · field target: any targetin battlefield permanent or in exile with suspend 1+ time counters card

Examples: Clockspinning

1 card(s) · ability/TargetOnly · field target: any targetpermanent

Examples: Ichormoon Gauntlet

1 card(s) · ability/TargetOnly · field target: any targetpermanent or player

Examples: Animation Module

1 card(s) · ability/TargetOnly · field target: any targetyou control permanent

Examples: Aven Courier

1 card(s) · ability/remove · removed: remove

Examples: Clockspinning

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

@JacobWoodson JacobWoodson force-pushed the ship/fix-pilot-token-crew-power branch from b85f92e to aaf7c4a Compare July 5, 2026 18:17

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

Thanks for the update. The phase-ai constructor issue is resolved, but the current head still has blockers.

  • SaddleMount still gates the frontend on raw power. The engine computes saddle eligibility with object_crew_power_contribution(..., CrewAction::Saddle), but client/src/viewmodel/gameStateView.ts maps SaddleMount without contributions. A Pilot-style saddle modifier can be accepted by the engine while the frontend blocks confirm. Please add index-aligned saddle contributions to WaitingFor::SaddleMount and wire them through the view model the same way as crew.
  • CrewVehicle.contributions is still a required serialized field without a serde default. Older saved/debug-exported games paused in a CrewVehicle prompt can fail to deserialize. Please add a default and normalize/recompute when needed.
  • The token ability comments still cite CR 111.4, which is about token names/subtypes. The ability/characteristics authority is CR 111.3; please update the annotation.

Frontend acceptance remains deferred to Matt under the current sweep policy.

A Pilot token that "crews Vehicles as though its power were 2 greater"
(Shorikai, Genesis Engine / Kotori, Pilot Prodigy) crewed for the wrong
amount through two independent bugs:

- Double-grant: the token carried the crew static twice -- once from the
  creating effect's `with "..."` clause and again from the catalog art
  preset's rules_text -- so a 1/1 Pilot contributed 5 (1+2+2). When the
  1/1 body matched a different printing's art (e.g. the Aetherdrift
  "saddles Mounts and crews Vehicles" Pilot), the two statics had
  different keyword actions and slipped past an exact-match de-dupe. Per
  CR 111.4 the creating effect's grant is authoritative, so the catalog
  `rules_text` fallback is now display-only and injects no functional
  abilities when the token already carries granted statics.

- Frontend re-derived crew power: the crew-selection UI summed each
  creature's raw printed power (1) instead of the engine's adjusted crew
  contribution, gating the lone Pilot at 1 < N and blocking the crew
  ("crews for just 1"). The engine now sends per-creature contributions
  on the CrewVehicle choice and the view-model gates on those.

Result: a 1/1 Pilot crews for 3; a 2/2 (with a +1/+1 counter) for 4.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
@JacobWoodson JacobWoodson force-pushed the ship/fix-pilot-token-crew-power branch from aaf7c4a to c9d5300 Compare July 5, 2026 20:44

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

Thanks for the update. The phase-ai constructor and predefined-token injection path look addressed on current head c9d5300c, but two blockers from the last review are still present.

  1. SaddleMount still does not carry index-aligned adjusted contributions. The engine computes saddle eligibility using object_crew_power_contribution(..., CrewAction::Saddle), but WaitingFor::SaddleMount still only serializes eligible_creatures (crates/engine/src/types/game_state.rs) and handle_saddle_activation still returns no contribution array. The frontend saddle path still builds selection: { type: "totalPowerAtLeast", power: waitingFor.data.saddle_power }, so boardChoiceSelectedPower falls back to raw object power. Any saddle modifier can still be accepted by the engine and blocked by the UI.

  2. CrewVehicle.contributions is still a required serialized field. The new field on WaitingFor::CrewVehicle has no serde default, so older saved/debug-exported states with an in-flight crew prompt and no contributions field will fail to deserialize. Please make this field backward-tolerant and keep the frontend’s existing fallback behavior for absent or mismatched contribution data.

@matthewevans matthewevans self-assigned this Jul 6, 2026
# Conflicts:
#	crates/engine/tests/integration/main.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.

Current head f90cf0b resolves the outstanding blockers: SaddleMount now carries engine-computed contributions through the serialized waiting state and frontend board-choice gate, CrewVehicle contributions are backward-tolerant for older pause-state payloads, and frontend fallback remains raw power when contribution data is absent or mismatched. CI is green on the current head after rerunning the canceled Rust shard.

@matthewevans matthewevans removed the defer-fe Frontend/client/UI PR deferred to Matt's direct review label Jul 6, 2026
@matthewevans matthewevans added this pull request to the merge queue Jul 6, 2026
@matthewevans matthewevans removed their assignment Jul 6, 2026
@matthewevans

Copy link
Copy Markdown
Member

Maintainer update: I pushed a small fixup on top of this branch () to correct the token ability CR citation and add a production-path Saddle regression proving adjusted saddle contribution reaches the engine waiting state and payment flow. Holding this for the refreshed CI on the new head before clearing the prior request-changes review.

@matthewevans

Copy link
Copy Markdown
Member

Correction: the maintainer fixup SHA is 51738534fa.

@matthewevans matthewevans removed this pull request from the merge queue due to a manual request Jul 6, 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 on current head after maintainer fixup review: crew/saddle now use engine-computed contribution values through the production activation/payment paths, token catalog ability injection no longer double-grants functional statics, and there is runtime coverage for both Shorikai/Pilot crewing and adjusted Saddle contribution. CI can finish through the merge queue.

@matthewevans matthewevans enabled auto-merge July 6, 2026 03:01
@matthewevans matthewevans added this pull request to the merge queue Jul 6, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jul 6, 2026
@matthewevans matthewevans self-assigned this Jul 6, 2026
@matthewevans matthewevans added this pull request to the merge queue Jul 6, 2026
@matthewevans matthewevans removed their assignment Jul 6, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jul 6, 2026
@matthewevans matthewevans self-assigned this Jul 6, 2026
@matthewevans matthewevans added this pull request to the merge queue Jul 6, 2026
@matthewevans matthewevans removed their assignment Jul 6, 2026
Merged via the queue into phase-rs:main with commit 8762a84 Jul 6, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants