fix(engine): foundation→tableau drag hints, z-lift, and Android battery drain

Fixes #34, #35, #36

- all_hints: add Foundation as source for Tableau hints (guarded by
  take_from_foundation); previously H key never suggested Foundation→Tableau
- end_drag / touch_end_drag: enforce take_from_foundation at input layer
  so a rejected-by-core MoveRequestEvent is never fired
- animation_plugin: pub CARD_ANIM_Z_LIFT so card_plugin can consume it
- update_card_entity: set CardAnim start.z = z + CARD_ANIM_Z_LIFT to
  eliminate 1-frame z artifact where animated card appeared behind resting cards
- solitaire_app: use AutoVsync on Android (caps GPU at display Hz vs
  spinning at 200+ fps); add WinitSettings unfocused reactive_low_power
  so app draws ~1fps when backgrounded

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
funman300
2026-05-27 13:17:28 -07:00
parent a2f02e1cbc
commit e3188faddc
4 changed files with 76 additions and 13 deletions
+1 -1
View File
@@ -81,7 +81,7 @@ const VOLUME_TOAST_SECS: f32 = 1.4;
///
/// 50.0 sits comfortably above the highest pile depth (~1.04) and well below
/// `DRAG_Z` (500), so a dragged card always renders above an animated one.
const CARD_ANIM_Z_LIFT: f32 = 50.0;
pub const CARD_ANIM_Z_LIFT: f32 = 50.0;
/// Per-card stagger interval for the win cascade at Normal speed (seconds).
///
+7 -2
View File
@@ -23,7 +23,7 @@ use solitaire_core::pile::PileType;
use solitaire_core::rules::{can_place_on_foundation, can_place_on_tableau};
use crate::animation_plugin::{CardAnim, EffectiveSlideDuration};
use crate::animation_plugin::{CardAnim, EffectiveSlideDuration, CARD_ANIM_Z_LIFT};
use crate::card_animation::CardAnimation;
use crate::events::{CardFaceRevealedEvent, CardFlippedEvent, StateChangedEvent};
use crate::game_plugin::GameMutation;
@@ -963,7 +963,12 @@ fn update_card_entity(
if !has_card_animation {
// Slide to the new position when it differs meaningfully; snap otherwise.
if (cur.truncate() - target.truncate()).length() > 1.0 && slide_secs > 0.0 {
let start = Vec3::new(cur.x, cur.y, z); // update Z immediately
// Lift the card immediately on the first frame of the animation so
// it never appears behind a card that is already resting at the
// destination slot. `advance_card_anims` will maintain this lift
// throughout the tween and snap to `target` (without lift) on
// completion.
let start = Vec3::new(cur.x, cur.y, z + CARD_ANIM_Z_LIFT);
commands
.entity(entity)
.insert(Transform::from_translation(start))
+34 -4
View File
@@ -734,8 +734,13 @@ fn end_drag(
.is_some_and(|p| can_place_on_foundation(&bottom_card, p))
}
PileType::Tableau(_) => {
game.0.piles.get(&target)
.is_some_and(|p| can_place_on_tableau(&bottom_card, p))
// Enforce the take-from-foundation rule at the input layer so the
// engine never fires a MoveRequestEvent that game_state would reject.
let foundation_allowed = !matches!(&origin, PileType::Foundation(_))
|| game.0.take_from_foundation;
foundation_allowed
&& game.0.piles.get(&target)
.is_some_and(|p| can_place_on_tableau(&bottom_card, p))
}
_ => false,
};
@@ -988,8 +993,13 @@ fn touch_end_drag(
.is_some_and(|p| can_place_on_foundation(&bottom_card, p))
}
PileType::Tableau(_) => {
game.0.piles.get(&target)
.is_some_and(|p| can_place_on_tableau(&bottom_card, p))
// Enforce the take-from-foundation rule at the input layer so the
// engine never fires a MoveRequestEvent that game_state would reject.
let foundation_allowed = !matches!(&origin, PileType::Foundation(_))
|| game.0.take_from_foundation;
foundation_allowed
&& game.0.piles.get(&target)
.is_some_and(|p| can_place_on_tableau(&bottom_card, p))
}
_ => false,
};
@@ -1591,6 +1601,26 @@ pub fn all_hints(game: &GameState) -> Vec<(PileType, PileType, usize)> {
}
}
// Pass 2b — Foundation → Tableau moves (only when the rule allows it).
// Foundation piles are excluded from Pass 1 & 2's source list because they
// should never hint Foundation→Foundation. Here we handle the return path
// separately so the guarded `take_from_foundation` rule is respected.
if game.take_from_foundation {
for slot in 0..4_u8 {
let from = PileType::Foundation(slot);
let Some(from_pile) = game.piles.get(&from) else { continue };
let Some(card) = from_pile.cards.last().filter(|c| c.face_up) else { continue };
for i in 0..7_usize {
let dest = PileType::Tableau(i);
if let Some(dest_pile) = game.piles.get(&dest)
&& can_place_on_tableau(card, dest_pile) {
hints.push((from.clone(), dest, 1));
break;
}
}
}
}
// Pass 3 — suggest drawing from the stock when no other hint was found.
if hints.is_empty() {
let stock_non_empty = game.piles.get(&PileType::Stock)