fix(engine): cancel stale win-cascade CardAnimation on new-game; refresh Android corner label text on resize (closes #6, closes #7)
Issue #7 — new game during win cascade: sync_cards now stores each in-flight CardAnimation's end position instead of a plain bool. Before calling update_card_entity, the end position is compared against the game-state target. If they differ by more than 2 px (stale cascade scatter vs. new-game dealt position) the CardAnimation is removed immediately so the card slides to its correct dealt position. Drag-rejection tweens are unaffected because their end equals the card's current game-state position. Issue #6 — Android stale corner label text: AndroidCornerLabel now carries the label string as AndroidCornerLabel(String). resize_android_corner_labels refreshes Text2d content from the stored value alongside the existing font-size and transform updates, closing the narrow race where a layout change could display a previous card's rank/suit. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -178,8 +178,8 @@ pub struct CardLabel;
|
||||
/// readable at phone scale. Only exists when `CardImageSet` is present
|
||||
/// (the fallback solid-colour path uses a plain `CardLabel` instead).
|
||||
#[cfg(target_os = "android")]
|
||||
#[derive(Component, Debug, Clone, Copy)]
|
||||
struct AndroidCornerLabel;
|
||||
#[derive(Component, Debug, Clone)]
|
||||
struct AndroidCornerLabel(pub String);
|
||||
|
||||
/// Solid-colour background sprite behind [`AndroidCornerLabel`].
|
||||
///
|
||||
@@ -707,15 +707,20 @@ fn sync_cards(
|
||||
.map(|c| c.id)
|
||||
};
|
||||
|
||||
// Map card_id -> (Entity, current_translation, has_card_animation) for
|
||||
// in-place updates. The `has_card_animation` flag lets `update_card_entity`
|
||||
// skip the snap/slide path on cards that are already being driven by a
|
||||
// curve-based `CardAnimation` tween (e.g. the drag-rejection return tween
|
||||
// — see `input_plugin::end_drag`). Otherwise the StateChangedEvent that
|
||||
// accompanies a rejection would race the tween and the card would jump.
|
||||
let mut existing: HashMap<u32, (Entity, Vec3, bool)> = HashMap::new();
|
||||
// Map card_id -> (Entity, current_translation, anim_end) for in-place
|
||||
// updates. `anim_end` is `Some(end_xy)` when a curve-based `CardAnimation`
|
||||
// is currently driving the card (e.g. a drag-rejection return tween).
|
||||
//
|
||||
// In the position loop below we compare `anim_end` against the new game-
|
||||
// state target position to decide whether to honour or cancel the tween:
|
||||
// • end ≈ target → animation is still heading to the right place; let
|
||||
// it finish (skip the snap/slide path).
|
||||
// • end ≠ target → the game state has changed (e.g. a new game started
|
||||
// while the win-cascade was mid-flight); cancel the
|
||||
// stale `CardAnimation` and apply the new position.
|
||||
let mut existing: HashMap<u32, (Entity, Vec3, Option<Vec2>)> = HashMap::new();
|
||||
for (entity, marker, transform, anim) in entities.iter() {
|
||||
existing.insert(marker.card_id, (entity, transform.translation, anim.is_some()));
|
||||
existing.insert(marker.card_id, (entity, transform.translation, anim.map(|a| a.end)));
|
||||
}
|
||||
|
||||
let live_ids: HashSet<u32> = positions.iter().map(|(c, _, _)| c.id).collect();
|
||||
@@ -732,7 +737,19 @@ fn sync_cards(
|
||||
// behind the incoming top card during the draw slide animation.
|
||||
for (card, position, z) in positions {
|
||||
let entity = match existing.get(&card.id) {
|
||||
Some(&(entity, cur, has_anim)) => {
|
||||
Some(&(entity, cur, anim_end)) => {
|
||||
// If a CardAnimation is in flight, check whether its destination
|
||||
// still matches the game-state target. If the game moved the card
|
||||
// elsewhere (e.g. new game started during a win-cascade scatter),
|
||||
// cancel the stale tween so the card snaps/slides to its new home.
|
||||
let has_anim = match anim_end {
|
||||
Some(end_xy) if (end_xy - position).length() > 2.0 => {
|
||||
commands.entity(entity).remove::<CardAnimation>();
|
||||
false
|
||||
}
|
||||
Some(_) => true,
|
||||
None => false,
|
||||
};
|
||||
update_card_entity(
|
||||
&mut commands, entity, card, position, z, layout,
|
||||
slide_secs, back_colour, color_blind, high_contrast, cur, has_anim, card_images, selected_back, font_handle,
|
||||
@@ -1142,10 +1159,11 @@ fn add_android_corner_label(
|
||||
// Large rank+suit text drawn on top of the background. FiraMono must be
|
||||
// wired here explicitly — the suit glyphs (U+2660–U+2666) are not in
|
||||
// Bevy's built-in font and render as a coloured rectangle without it.
|
||||
let label_text = mobile_label_for(card);
|
||||
parent.spawn((
|
||||
AndroidCornerLabel,
|
||||
AndroidCornerLabel(label_text.clone()),
|
||||
CardLabel,
|
||||
Text2d::new(mobile_label_for(card)),
|
||||
Text2d::new(label_text),
|
||||
TextFont {
|
||||
font: font_handle.cloned().unwrap_or_default(),
|
||||
font_size,
|
||||
@@ -2089,7 +2107,7 @@ fn resize_cards_in_place(
|
||||
fn resize_android_corner_labels(
|
||||
layout: Res<LayoutResource>,
|
||||
card_images: Option<Res<CardImageSet>>,
|
||||
mut text_query: Query<(&mut TextFont, &mut Transform), With<AndroidCornerLabel>>,
|
||||
mut text_query: Query<(&AndroidCornerLabel, &mut Text2d, &mut TextFont, &mut Transform)>,
|
||||
mut bg_query: Query<
|
||||
(&mut Sprite, &mut Transform),
|
||||
(With<AndroidCornerBg>, Without<AndroidCornerLabel>),
|
||||
@@ -2105,7 +2123,8 @@ fn resize_android_corner_labels(
|
||||
let text_x = -layout.0.card_size.x / 2.0 + inset;
|
||||
let text_y = layout.0.card_size.y / 2.0 - inset;
|
||||
|
||||
for (mut font, mut transform) in text_query.iter_mut() {
|
||||
for (label, mut text2d, mut font, mut transform) in text_query.iter_mut() {
|
||||
text2d.0 = label.0.clone();
|
||||
font.font_size = font_size;
|
||||
transform.translation.x = text_x;
|
||||
transform.translation.y = text_y;
|
||||
|
||||
Reference in New Issue
Block a user