fix: gate cast DistributeAmong on ChooseTarget completion (closes #5145)#5148
Conversation
…oses phase-rs#5145) Slot-by-slot target selection used by the client skipped the divided-damage assignment step that bulk SelectTargets already triggered. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Code Review
This pull request addresses issue #5145 by refactoring target distribution logic into a shared helper function, maybe_pause_for_cast_distribution, ensuring that slot-by-slot target selection (handle_choose_target) correctly pauses for distribution just like bulk target selection. An integration test for 'Violent Eruption' has been added to verify this behavior. The review feedback recommends passing pending and ability by reference to maybe_pause_for_cast_distribution instead of by value, which avoids expensive cloning on the common execution path where no distribution is required.
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.
| fn maybe_pause_for_cast_distribution( | ||
| state: &mut GameState, | ||
| player: PlayerId, | ||
| pending: PendingCast, | ||
| ability: ResolvedAbility, | ||
| ) -> Result<Option<WaitingFor>, EngineError> { | ||
| let Some(unit) = pending.distribute.clone() else { | ||
| return Ok(None); | ||
| }; | ||
| let Some(total) = extract_distribution_total(state, &ability, &ability.effect) else { | ||
| // X-spell: distribution deferred to after mana payment. | ||
| return Ok(None); | ||
| }; | ||
| let assigned_targets = distribution_targets(&ability); | ||
| let mut pending_dist = pending; | ||
| pending_dist.ability = ability; | ||
| state.pending_cast = Some(Box::new(pending_dist)); | ||
| Ok(Some(WaitingFor::DistributeAmong { | ||
| player, | ||
| total, | ||
| targets: assigned_targets, | ||
| unit, | ||
| })) | ||
| } |
There was a problem hiding this comment.
[MEDIUM] Avoid unnecessary cloning of PendingCast and ResolvedAbility
Passing pending and ability by value forces cloning them at every call site, even for spells that do not require distribution (which is the vast majority of casts). Changing the signature to accept references allows us to avoid these expensive clones entirely on the fast path where no distribution is needed.
fn maybe_pause_for_cast_distribution(
state: &mut GameState,
player: PlayerId,
pending: &PendingCast,
ability: &ResolvedAbility,
) -> Result<Option<WaitingFor>, EngineError> {
let Some(unit) = &pending.distribute else {
return Ok(None);
};
let Some(total) = extract_distribution_total(state, ability, &ability.effect) else {
// X-spell: distribution deferred to after mana payment.
return Ok(None);
};
let assigned_targets = distribution_targets(ability);
let mut pending_dist = pending.clone();
pending_dist.ability = ability.clone();
state.pending_cast = Some(Box::new(pending_dist));
Ok(Some(WaitingFor::DistributeAmong {
player,
total,
targets: assigned_targets,
unit: unit.clone(),
}))
}| if let Some(waiting_for) = | ||
| maybe_pause_for_cast_distribution(state, player, pending.clone(), ability.clone())? | ||
| { | ||
| return Ok(waiting_for); | ||
| } |
There was a problem hiding this comment.
[MEDIUM] Pass references to avoid cloning PendingCast and ResolvedAbility
By passing references to maybe_pause_for_cast_distribution, we avoid cloning pending and ability when no distribution is required.
| if let Some(waiting_for) = | |
| maybe_pause_for_cast_distribution(state, player, pending.clone(), ability.clone())? | |
| { | |
| return Ok(waiting_for); | |
| } | |
| if let Some(waiting_for) = | |
| maybe_pause_for_cast_distribution(state, player, &pending, &ability)? | |
| { | |
| return Ok(waiting_for); | |
| } |
| if let Some(waiting_for) = | ||
| maybe_pause_for_cast_distribution(state, player, pending.clone(), ability.clone())? | ||
| { | ||
| return Ok(waiting_for); | ||
| } |
There was a problem hiding this comment.
[MEDIUM] Pass references to avoid cloning PendingCast and ResolvedAbility
By passing references to maybe_pause_for_cast_distribution, we avoid cloning pending and ability when no distribution is required.
| if let Some(waiting_for) = | |
| maybe_pause_for_cast_distribution(state, player, pending.clone(), ability.clone())? | |
| { | |
| return Ok(waiting_for); | |
| } | |
| if let Some(waiting_for) = | |
| maybe_pause_for_cast_distribution(state, player, &pending, &ability)? | |
| { | |
| return Ok(waiting_for); | |
| } |
Co-authored-by: Cursor <cursoragent@cursor.com>
matthewevans
left a comment
There was a problem hiding this comment.
Thanks for the focused fix. I’m stopping this head at the mechanical gate before deeper review because exact-head Rust lint is already failing on formatting.
[HIGH] Current head fails cargo fmt --all -- --check. Evidence: the Rust lint (fmt, clippy, parser gate) job for head 5938befab59c7d4ec94c4bb521de59949f0866bb reports diffs in crates/engine/tests/integration/issue_5145_violent_eruption_choose_target_distribution.rs at lines 41, 96, and 105, plus crates/engine/tests/integration/main.rs at lines 346 and 417. Why it matters: this head cannot pass the required Rust lint gate, and I’m not going to approve/enqueue a branch with a known exact-head formatting failure. Suggested fix: run cargo fmt --all, push the formatted result, and let CI rerun; I’ll continue the architectural review on the formatted head.
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
matthewevans
left a comment
There was a problem hiding this comment.
The formatting blocker is still present on the new head, so I’m keeping this blocked at the same mechanical gate.
[HIGH] Current head still fails rustfmt. Evidence: crates/engine/tests/integration/issue_5145_violent_eruption_choose_target_distribution.rs:95; cargo fmt --all -- --check on head a3bff20e35ae45bf54769253396910bffa2b1328 rewrites the other => panic!(...) match arm. Why it matters: the exact PR head still cannot pass the required Rust lint gate, so I’m not proceeding to approval/enqueue. Suggested fix: run cargo fmt --all and push the formatted test.
Parse changes introduced by this PR✓ No card-parse changes detected. |
Avoid cloning PendingCast and ResolvedAbility on the no-distribution fast path. Reformat the issue 5145 let-else test arm for rustfmt. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Addressed review feedback on head
Prior CI on |
matthewevans
left a comment
There was a problem hiding this comment.
The formatting blocker is still present on the current head, so I’m keeping this blocked at the mechanical gate.
[HIGH] Current head still fails rustfmt. Evidence: crates/engine/src/game/casting_targets.rs:281 and crates/engine/tests/integration/issue_5145_violent_eruption_choose_target_distribution.rs:83; cargo fmt --all -- --check on head d7bdef38a94921bacb7d2ae011d035ffd46fd32c rewrites both spots. Why it matters: this exact head cannot pass the Rust lint gate, so deeper review/merge handling would be premature. Suggested fix: run cargo fmt --all, commit the formatting result, and repush.
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
matthewevans
left a comment
There was a problem hiding this comment.
The exact current head still fails the mechanical Rust formatting gate.
[HIGH] Current head fails rustfmt. Evidence: crates/engine/src/game/casting_targets.rs:281, crates/engine/src/game/casting_targets.rs:404, and crates/engine/tests/integration/issue_5145_violent_eruption_choose_target_distribution.rs:80; cargo fmt --all -- --check on head 19b8fcecfe26b5ec2f8b4aad3c054f9a1a9e9f42 rewrites those spots. Why it matters: this exact head cannot pass the required Rust lint gate, so it is not ready for deeper approval/enqueue handling. Suggested fix: run cargo fmt --all, commit the formatting result, and repush.
Co-authored-by: Cursor <cursoragent@cursor.com>
Ref-pass shortened the call enough for single-line formatting. Co-authored-by: Cursor <cursoragent@cursor.com>
Ref-pass shortened the call enough for single-line formatting. Co-authored-by: Cursor <cursoragent@cursor.com>
|
CI green on head
Ready for re-review. |
matthewevans
left a comment
There was a problem hiding this comment.
Maintainer sweep: reviewed current head after the follow-up fixes. The ChooseTarget-to-DistributeAmong pause flow is at the casting target seam, has a discriminating integration test, and current CI is green.
Summary
GameAction::ChooseTarget(one slot at a time), but only the bulkSelectTargetspath paused forWaitingFor::DistributeAmongbefore payment.maybe_pause_for_cast_distributionused by both target-selection completion paths; adds an integration test that drivesChooseTargetlike the client.Test plan
issue_5145_violent_eruption_choose_target_distributionassertsChooseTarget→DistributeAmong→ per-target damage shares