fix(engine): hide previous-game positions during new-game deal
Reported leak: when a new game starts, every card sprite tweens from its previous-game Transform to its new dealt position. A careful observer can track those origin points and deduce face-down cards in the new layout — the tween's start frame literally renders the prior game's geometry. Fix: in handle_new_game, after replacing the GameState, snap every existing card Transform to the stock pile's position before writing StateChangedEvent. The downstream slide tween in card_plugin then reads the stock position as its source, so all 52 cards animate out from a single point — reads as "dealing from the deck" with no information leak. No layout reach in headless test contexts so the snap is gated on Option<Res<LayoutResource>>. Reported by Quat. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -168,6 +168,8 @@ fn handle_new_game(
|
|||||||
font_res: Option<Res<FontResource>>,
|
font_res: Option<Res<FontResource>>,
|
||||||
confirm_screens: Query<Entity, With<ConfirmNewGameScreen>>,
|
confirm_screens: Query<Entity, With<ConfirmNewGameScreen>>,
|
||||||
game_over_screens: Query<Entity, With<GameOverScreen>>,
|
game_over_screens: Query<Entity, With<GameOverScreen>>,
|
||||||
|
layout: Option<Res<crate::layout::LayoutResource>>,
|
||||||
|
mut card_transforms: Query<&mut Transform, With<crate::card_plugin::CardEntity>>,
|
||||||
) {
|
) {
|
||||||
for ev in new_game.read() {
|
for ev in new_game.read() {
|
||||||
// If an active game is in progress, intercept and show a confirm dialog.
|
// If an active game is in progress, intercept and show a confirm dialog.
|
||||||
@@ -209,6 +211,24 @@ fn handle_new_game(
|
|||||||
&& let Err(e) = delete_game_state_at(p) {
|
&& let Err(e) = delete_game_state_at(p) {
|
||||||
warn!("game_state: failed to delete saved game: {e}");
|
warn!("game_state: failed to delete saved game: {e}");
|
||||||
}
|
}
|
||||||
|
// Snap every existing card sprite to the stock position before the
|
||||||
|
// deal animation starts. Without this the per-card slide tween reads
|
||||||
|
// each card's previous-game Transform as its source, which lets a
|
||||||
|
// careful observer track origin points to deduce where face-down
|
||||||
|
// cards came from. Funnelling all sprites through the deck position
|
||||||
|
// hides that information and reads naturally as "dealt from the
|
||||||
|
// deck." Skipped when LayoutResource isn't present (headless tests).
|
||||||
|
if let Some(layout) = layout.as_ref()
|
||||||
|
&& let Some(stock) = layout
|
||||||
|
.0
|
||||||
|
.pile_positions
|
||||||
|
.get(&solitaire_core::pile::PileType::Stock)
|
||||||
|
{
|
||||||
|
for mut tx in &mut card_transforms {
|
||||||
|
tx.translation.x = stock.x;
|
||||||
|
tx.translation.y = stock.y;
|
||||||
|
}
|
||||||
|
}
|
||||||
changed.write(StateChangedEvent);
|
changed.write(StateChangedEvent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user