Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fae5933d29 | |||
| 6cd8c6c013 | |||
| ec94cb34aa |
@@ -1147,7 +1147,7 @@ fn add_android_corner_label(
|
|||||||
let bg_w = font_size * 2.0;
|
let bg_w = font_size * 2.0;
|
||||||
let bg_h = font_size * 1.25;
|
let bg_h = font_size * 1.25;
|
||||||
|
|
||||||
// Background covers the PNG's baked-in small corner text.
|
// Background covers the PNG's baked-in small corner text (top-left).
|
||||||
// Classic PNG cards have a white face, so the background must be white too.
|
// Classic PNG cards have a white face, so the background must be white too.
|
||||||
// (CARD_FACE_COLOUR is the Terminal theme's dark face colour — wrong here.)
|
// (CARD_FACE_COLOUR is the Terminal theme's dark face colour — wrong here.)
|
||||||
parent.spawn((
|
parent.spawn((
|
||||||
@@ -1163,6 +1163,20 @@ fn add_android_corner_label(
|
|||||||
0.015,
|
0.015,
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
|
// Cover the matching rotated baked-in text at the bottom-right corner.
|
||||||
|
parent.spawn((
|
||||||
|
AndroidCornerBg,
|
||||||
|
Sprite {
|
||||||
|
color: Color::WHITE,
|
||||||
|
custom_size: Some(Vec2::new(bg_w, bg_h)),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
Transform::from_xyz(
|
||||||
|
card_size.x / 2.0 - inset - bg_w / 2.0,
|
||||||
|
-card_size.y / 2.0 + inset + bg_h / 2.0,
|
||||||
|
0.015,
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
// Large rank+suit text drawn on top of the background. FiraMono must be
|
// 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
|
// wired here explicitly — the suit glyphs (U+2660–U+2666) are not in
|
||||||
|
|||||||
@@ -202,6 +202,8 @@ impl Plugin for GamePlugin {
|
|||||||
.add_message::<FoundationCompletedEvent>()
|
.add_message::<FoundationCompletedEvent>()
|
||||||
.add_message::<InfoToastEvent>()
|
.add_message::<InfoToastEvent>()
|
||||||
.add_message::<AppLifecycle>()
|
.add_message::<AppLifecycle>()
|
||||||
|
// add_message is idempotent; SettingsPlugin also registers this.
|
||||||
|
.add_message::<crate::settings_plugin::SettingsChangedEvent>()
|
||||||
.add_systems(
|
.add_systems(
|
||||||
Update,
|
Update,
|
||||||
poll_pending_new_game_seed.before(GameMutation),
|
poll_pending_new_game_seed.before(GameMutation),
|
||||||
@@ -228,6 +230,7 @@ impl Plugin for GamePlugin {
|
|||||||
// GameMutation flow.
|
// GameMutation flow.
|
||||||
.add_systems(Update, spawn_restore_prompt_if_pending)
|
.add_systems(Update, spawn_restore_prompt_if_pending)
|
||||||
.add_systems(Update, handle_restore_prompt.before(GameMutation))
|
.add_systems(Update, handle_restore_prompt.before(GameMutation))
|
||||||
|
.add_systems(Update, sync_settings_to_game.before(GameMutation))
|
||||||
.init_resource::<AutoSaveTimer>()
|
.init_resource::<AutoSaveTimer>()
|
||||||
.add_systems(Update, tick_elapsed_time)
|
.add_systems(Update, tick_elapsed_time)
|
||||||
.add_systems(Update, auto_save_game_state)
|
.add_systems(Update, auto_save_game_state)
|
||||||
@@ -235,6 +238,23 @@ impl Plugin for GamePlugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Forwards `take_from_foundation` from [`SettingsResource`] to the live
|
||||||
|
/// [`GameStateResource`] every time [`SettingsChangedEvent`] fires.
|
||||||
|
///
|
||||||
|
/// This covers two cases that the new-game path misses:
|
||||||
|
/// 1. The initial settings load at startup: saves on disk default to `false`
|
||||||
|
/// but `Settings` defaults to `true`; the event fires once when the
|
||||||
|
/// settings file is first read.
|
||||||
|
/// 2. A user toggling the setting mid-session in the Settings panel.
|
||||||
|
fn sync_settings_to_game(
|
||||||
|
mut events: MessageReader<crate::settings_plugin::SettingsChangedEvent>,
|
||||||
|
mut game: ResMut<GameStateResource>,
|
||||||
|
) {
|
||||||
|
for ev in events.read() {
|
||||||
|
game.0.take_from_foundation = ev.0.take_from_foundation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Pure, testable helper. Updates `elapsed_seconds` and drains the
|
/// Pure, testable helper. Updates `elapsed_seconds` and drains the
|
||||||
/// fractional accumulator into whole-second ticks. No-op when `is_won`.
|
/// fractional accumulator into whole-second ticks. No-op when `is_won`.
|
||||||
pub fn advance_elapsed(
|
pub fn advance_elapsed(
|
||||||
@@ -614,6 +634,7 @@ fn handle_restore_prompt(
|
|||||||
new_game_buttons: Query<&Interaction, (With<RestoreNewGameButton>, Changed<Interaction>)>,
|
new_game_buttons: Query<&Interaction, (With<RestoreNewGameButton>, Changed<Interaction>)>,
|
||||||
mut pending: ResMut<PendingRestoredGame>,
|
mut pending: ResMut<PendingRestoredGame>,
|
||||||
mut game: ResMut<GameStateResource>,
|
mut game: ResMut<GameStateResource>,
|
||||||
|
settings: Option<Res<crate::settings_plugin::SettingsResource>>,
|
||||||
mut changed: MessageWriter<StateChangedEvent>,
|
mut changed: MessageWriter<StateChangedEvent>,
|
||||||
mut new_game: MessageWriter<NewGameRequestEvent>,
|
mut new_game: MessageWriter<NewGameRequestEvent>,
|
||||||
mut launch_home_shown: Option<ResMut<crate::home_plugin::LaunchHomeShown>>,
|
mut launch_home_shown: Option<ResMut<crate::home_plugin::LaunchHomeShown>>,
|
||||||
@@ -639,6 +660,10 @@ fn handle_restore_prompt(
|
|||||||
let resolved = if key_continue || click_continue {
|
let resolved = if key_continue || click_continue {
|
||||||
if let Some(restored) = pending.0.take() {
|
if let Some(restored) = pending.0.take() {
|
||||||
game.0 = restored;
|
game.0 = restored;
|
||||||
|
// Patch setting that serialized with the old core default of `false`.
|
||||||
|
if let Some(s) = settings.as_ref() {
|
||||||
|
game.0.take_from_foundation = s.0.take_from_foundation;
|
||||||
|
}
|
||||||
changed.write(StateChangedEvent);
|
changed.write(StateChangedEvent);
|
||||||
}
|
}
|
||||||
for entity in &screens {
|
for entity in &screens {
|
||||||
|
|||||||
@@ -989,7 +989,7 @@ fn spawn_action_button<M: Component>(
|
|||||||
// centred with room to breathe. On desktop, keep the comfortable 48 dp
|
// centred with room to breathe. On desktop, keep the comfortable 48 dp
|
||||||
// floor and 8 dp side padding.
|
// floor and 8 dp side padding.
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
let (pad, min_w, min_h) = (UiRect::axes(Val::Px(4.0), Val::Px(4.0)), Val::Px(44.0), Val::Px(44.0));
|
let (pad, min_w, min_h) = (UiRect::axes(Val::Px(4.0), Val::Px(4.0)), Val::Px(52.0), Val::Px(44.0));
|
||||||
#[cfg(not(target_os = "android"))]
|
#[cfg(not(target_os = "android"))]
|
||||||
let (pad, min_w, min_h) = (UiRect::axes(VAL_SPACE_2, VAL_SPACE_2), Val::Px(48.0), Val::Px(48.0));
|
let (pad, min_w, min_h) = (UiRect::axes(VAL_SPACE_2, VAL_SPACE_2), Val::Px(48.0), Val::Px(48.0));
|
||||||
|
|
||||||
|
|||||||
@@ -107,6 +107,23 @@ pub const HUD_BAND_HEIGHT: f32 = 64.0;
|
|||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
pub const HUD_BAND_HEIGHT: f32 = 112.0;
|
pub const HUD_BAND_HEIGHT: f32 = 112.0;
|
||||||
|
|
||||||
|
/// Height of the bottom action-bar (the row of ≡ ← || ? ! M + buttons).
|
||||||
|
///
|
||||||
|
/// The action bar sits *above* the OS gesture/navigation zone, so it is NOT
|
||||||
|
/// covered by `safe_area_bottom`. `compute_layout` adds this constant to
|
||||||
|
/// `safe_area_bottom` before computing the height-based card-size candidate
|
||||||
|
/// and the available tableau height, ensuring the deepest fanned column
|
||||||
|
/// never scrolls behind the button row.
|
||||||
|
///
|
||||||
|
/// Derivation (Android): `min_height 44 px` buttons
|
||||||
|
/// + `padding.top 8 px` + `padding.bottom 8 px` outer bar padding = **60 px**.
|
||||||
|
///
|
||||||
|
/// Desktop: no persistent bottom bar, so 0.
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
|
const BOTTOM_BAR_HEIGHT: f32 = 0.0;
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
const BOTTOM_BAR_HEIGHT: f32 = 60.0;
|
||||||
|
|
||||||
/// Table background colour (dark green felt).
|
/// Table background colour (dark green felt).
|
||||||
pub const TABLE_COLOUR: [f32; 3] = [0.059, 0.322, 0.196];
|
pub const TABLE_COLOUR: [f32; 3] = [0.059, 0.322, 0.196];
|
||||||
|
|
||||||
@@ -190,9 +207,13 @@ pub fn compute_layout(window: Vec2, safe_area_top: f32, safe_area_bottom: f32, h
|
|||||||
// Substituting h_gap = w/4 and h = CARD_ASPECT * w and solving for the
|
// Substituting h_gap = w/4 and h = CARD_ASPECT * w and solving for the
|
||||||
// largest w that fits gives:
|
// largest w that fits gives:
|
||||||
// (window.y - HUD_BAND_HEIGHT) = w * (0.5 + (1 + fan_factor + VERTICAL_GAP_FRAC) * CARD_ASPECT)
|
// (window.y - HUD_BAND_HEIGHT) = w * (0.5 + (1 + fan_factor + VERTICAL_GAP_FRAC) * CARD_ASPECT)
|
||||||
|
// Reserve space for both the OS gesture/nav bar and the app's own action
|
||||||
|
// bar, which sits above it and is invisible to safe_area_bottom.
|
||||||
|
let effective_safe_bottom = safe_area_bottom + BOTTOM_BAR_HEIGHT;
|
||||||
|
|
||||||
let fan_factor = 1.0 + (MAX_TABLEAU_CARDS - 1.0) * TABLEAU_FAN_FRAC;
|
let fan_factor = 1.0 + (MAX_TABLEAU_CARDS - 1.0) * TABLEAU_FAN_FRAC;
|
||||||
let height_denom = 0.5 + (1.0 + fan_factor + VERTICAL_GAP_FRAC) * CARD_ASPECT;
|
let height_denom = 0.5 + (1.0 + fan_factor + VERTICAL_GAP_FRAC) * CARD_ASPECT;
|
||||||
let card_width_height_based = (window.y - safe_area_top - safe_area_bottom - band_h).max(0.0) / height_denom;
|
let card_width_height_based = (window.y - safe_area_top - effective_safe_bottom - band_h).max(0.0) / height_denom;
|
||||||
|
|
||||||
let card_width = card_width_width_based.min(card_width_height_based);
|
let card_width = card_width_width_based.min(card_width_height_based);
|
||||||
let card_height = card_width * CARD_ASPECT;
|
let card_height = card_width * CARD_ASPECT;
|
||||||
@@ -241,7 +262,7 @@ pub fn compute_layout(window: Vec2, safe_area_top: f32, safe_area_bottom: f32, h
|
|||||||
//
|
//
|
||||||
// avail = distance from the top of the first tableau card to the bottom
|
// avail = distance from the top of the first tableau card to the bottom
|
||||||
// margin — i.e. the space available for 12 fan steps.
|
// margin — i.e. the space available for 12 fan steps.
|
||||||
let avail = (tableau_y - (-window.y / 2.0 + safe_area_bottom + h_gap) - card_height / 2.0).max(0.0);
|
let avail = (tableau_y - (-window.y / 2.0 + effective_safe_bottom + h_gap) - card_height / 2.0).max(0.0);
|
||||||
let ideal_fan_frac = if card_height > 0.0 {
|
let ideal_fan_frac = if card_height > 0.0 {
|
||||||
avail / ((MAX_TABLEAU_CARDS - 1.0) * card_height)
|
avail / ((MAX_TABLEAU_CARDS - 1.0) * card_height)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -473,8 +473,11 @@ fn radial_open_on_long_press(
|
|||||||
mut state: ResMut<RightClickRadialState>,
|
mut state: ResMut<RightClickRadialState>,
|
||||||
) {
|
) {
|
||||||
// Guard: only count while a touch is down, uncommitted, and radial is idle.
|
// Guard: only count while a touch is down, uncommitted, and radial is idle.
|
||||||
let active_id = drag.active_touch_id;
|
let Some(active_id) = drag.active_touch_id else {
|
||||||
if active_id.is_none() || drag.committed || state.is_active() || paused.is_some_and(|p| p.0) {
|
*hold_timer = 0.0;
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if drag.committed || state.is_active() || paused.is_some_and(|p| p.0) {
|
||||||
*hold_timer = 0.0;
|
*hold_timer = 0.0;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -487,7 +490,7 @@ fn radial_open_on_long_press(
|
|||||||
|
|
||||||
// Resolve current touch world position.
|
// Resolve current touch world position.
|
||||||
let Some(touches) = touches else { return };
|
let Some(touches) = touches else { return };
|
||||||
let Some(touch) = touches.iter().find(|t| t.id() == active_id.unwrap()) else {
|
let Some(touch) = touches.iter().find(|t| t.id() == active_id) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let Some((camera, cam_xf)) = cameras.single().ok() else { return };
|
let Some((camera, cam_xf)) = cameras.single().ok() else { return };
|
||||||
|
|||||||
Reference in New Issue
Block a user