Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4df13695fc | |||
| df22338c8a | |||
| 7f450aab17 |
@@ -6,6 +6,52 @@ project follows [Semantic Versioning](https://semver.org/).
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.33.0] — 2026-05-16
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **Face-down cards render as tiny red squares (startup ordering bug)**. The
|
||||||
|
`load_initial_theme` system fell back to `"dark"` when `SettingsResource` was
|
||||||
|
not yet available at `Startup`, which happens on every fresh run before the
|
||||||
|
settings file is read. The dark theme's near-black card back (#151515) renders
|
||||||
|
as fully-off pixels on AMOLED screens, leaving only a 24×32 px red badge
|
||||||
|
visible. Changed the fallback to `"classic"` so startup behaviour matches the
|
||||||
|
`default_theme_id()` set in v0.31.0. Cascade-collapse and top-row legibility
|
||||||
|
issues were visual consequences of the same invisible-card-back problem, not
|
||||||
|
separate layout bugs.
|
||||||
|
|
||||||
|
## [0.32.0] — 2026-05-16
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **Stock-count badge overlaps waste pile on Android** (Bug 1). The badge was
|
||||||
|
centred 12 px inward from the stock pile's right edge, but its half-width of
|
||||||
|
17 px pushed it 5 px past the edge. On Android (`H_GAP_DIVISOR = 32`) the
|
||||||
|
inter-pile gap is only ~4 px, so the badge's top-right corner covered the
|
||||||
|
left edge of the adjacent waste card at `Z_STOCK_BADGE = 30` (above the
|
||||||
|
card's Z ≈ 1). Fixed by moving the inset to 20 px so the badge right edge
|
||||||
|
sits 3 px inside the stock card on every device.
|
||||||
|
- **Oversized grey header bar** (Bug 2). The top HUD band was a full-width
|
||||||
|
`Node` with an opaque dark-grey `BackgroundColor` sized to `HUD_BAND_HEIGHT`
|
||||||
|
(64 px desktop / 80 px Android). Typical gameplay only shows one tier of
|
||||||
|
score text (~30 px), leaving a large empty grey block. Removed the
|
||||||
|
`BackgroundColor` from the band entity; the green felt now shows through and
|
||||||
|
only the score text and avatar button are visible in the header area.
|
||||||
|
|
||||||
|
## [0.31.0] — 2026-05-16
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **Face-down cards rendered as solid red squares on AMOLED phones** (Bug 1).
|
||||||
|
The dark theme's card back (`back.svg`) uses a near-black background
|
||||||
|
(`#151515`) which AMOLED screens render as fully-off pixels, leaving only a
|
||||||
|
tiny `#a54242` red badge visible — exactly what was reported. Fixed by
|
||||||
|
changing the fresh-install default theme from "dark" to "classic" (white
|
||||||
|
background with navy diamond pattern, clearly readable on all display types).
|
||||||
|
Also corrected stale asset paths in `load_card_images` (`cards/backs/back_N`
|
||||||
|
→ `cards/backs/classic/back_N`, `cards/faces/XY` → `cards/faces/classic/XY`)
|
||||||
|
so the PNG fallback loads correctly when the embedded theme hasn't arrived yet.
|
||||||
|
|
||||||
## [0.30.0] — 2026-05-16
|
## [0.30.0] — 2026-05-16
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
@@ -275,7 +275,7 @@ fn default_music_volume() -> f32 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn default_theme_id() -> String {
|
fn default_theme_id() -> String {
|
||||||
"dark".to_string()
|
"classic".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Default tooltip-hover dwell delay in seconds. Mirrors
|
/// Default tooltip-hover dwell delay in seconds. Mirrors
|
||||||
@@ -402,11 +402,10 @@ impl Settings {
|
|||||||
/// their respective ranges after deserialization or hand-editing of
|
/// their respective ranges after deserialization or hand-editing of
|
||||||
/// `settings.json`.
|
/// `settings.json`.
|
||||||
pub fn sanitized(self) -> Self {
|
pub fn sanitized(self) -> Self {
|
||||||
// Migrate stale theme IDs: "default" was removed when the theme was
|
// Migrate stale theme IDs: "default" was the original name before it
|
||||||
// renamed to "dark"; "classic" was briefly the default before "dark"
|
// was renamed to "dark".
|
||||||
// was restored as the shipped default.
|
|
||||||
let selected_theme_id = match self.selected_theme_id.as_str() {
|
let selected_theme_id = match self.selected_theme_id.as_str() {
|
||||||
"default" | "classic" => "dark".to_string(),
|
"default" => "dark".to_string(),
|
||||||
_ => self.selected_theme_id,
|
_ => self.selected_theme_id,
|
||||||
};
|
};
|
||||||
Self {
|
Self {
|
||||||
|
|||||||
@@ -484,13 +484,13 @@ fn load_card_images(asset_server: Option<Res<AssetServer>>, mut commands: Comman
|
|||||||
let faces: [[Handle<Image>; 13]; 4] = std::array::from_fn(|suit| {
|
let faces: [[Handle<Image>; 13]; 4] = std::array::from_fn(|suit| {
|
||||||
std::array::from_fn(|rank| {
|
std::array::from_fn(|rank| {
|
||||||
asset_server.load(format!(
|
asset_server.load(format!(
|
||||||
"cards/faces/{}{}.png",
|
"cards/faces/classic/{}{}.png",
|
||||||
RANK_STRS[rank], SUIT_CHARS[suit]
|
RANK_STRS[rank], SUIT_CHARS[suit]
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
let backs = std::array::from_fn(|i| {
|
let backs = std::array::from_fn(|i| {
|
||||||
asset_server.load(format!("cards/backs/back_{i}.png"))
|
asset_server.load(format!("cards/backs/classic/back_{i}.png"))
|
||||||
});
|
});
|
||||||
commands.insert_resource(CardImageSet {
|
commands.insert_resource(CardImageSet {
|
||||||
faces,
|
faces,
|
||||||
@@ -1614,10 +1614,11 @@ fn update_stock_empty_indicator(
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
/// Inset (in pixels) from the top-right corner of the stock pile sprite to
|
/// Inset (in pixels) from the top-right corner of the stock pile sprite to
|
||||||
/// the centre of the count badge. A small inward offset keeps the chip from
|
/// the centre of the count badge. Must satisfy `|x| >= STOCK_BADGE_SIZE.x / 2`
|
||||||
/// drifting half-off the card while still reading as "attached" to the
|
/// so the badge right edge stays inside the stock pile and never overlaps the
|
||||||
/// corner.
|
/// adjacent waste pile — critical on Android where `H_GAP_DIVISOR = 32` gives
|
||||||
const STOCK_BADGE_INSET: Vec2 = Vec2::new(-12.0, -8.0);
|
/// an inter-pile gap of only ~4 px.
|
||||||
|
const STOCK_BADGE_INSET: Vec2 = Vec2::new(-20.0, -8.0);
|
||||||
|
|
||||||
/// Width / height of the badge background sprite, in world pixels. Sized so
|
/// Width / height of the badge background sprite, in world pixels. Sized so
|
||||||
/// a 2-digit count (max "24") fits comfortably with `TYPE_BODY` (14 pt) text.
|
/// a 2-digit count (max "24") fits comfortably with `TYPE_BODY` (14 pt) text.
|
||||||
|
|||||||
@@ -479,16 +479,13 @@ impl Plugin for HudPlugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Spawns the translucent HUD band that anchors the action buttons
|
/// Spawns the invisible HUD band that reserves vertical space at the top of
|
||||||
/// and primary readouts visually. Sits behind every other HUD element
|
/// the screen so the card layout (computed by `layout::compute_layout` using
|
||||||
/// (one z-rung below `Z_HUD`) so it reads as the band's "container"
|
/// `HUD_BAND_HEIGHT`) aligns correctly below the score readouts.
|
||||||
/// without intercepting clicks from the buttons it sits under.
|
|
||||||
///
|
///
|
||||||
/// Width is full-window, height matches `layout::HUD_BAND_HEIGHT` (the
|
/// The entity carries no `BackgroundColor` — the green felt shows through.
|
||||||
/// same constant the card layout reserves at the top), so the band's
|
/// A slim grey background is handled by each content section individually
|
||||||
/// bottom edge lines up exactly with the top edge of the highest
|
/// (the bottom action bar has its own `BG_HUD_BAND` background).
|
||||||
/// playable card. The fill is `BG_HUD_BAND` — midnight purple at 0.70
|
|
||||||
/// alpha, so the green felt reads through subtly.
|
|
||||||
fn spawn_hud_band(insets: Option<Res<SafeAreaInsets>>, mut commands: Commands) {
|
fn spawn_hud_band(insets: Option<Res<SafeAreaInsets>>, mut commands: Commands) {
|
||||||
const BASE_TOP: f32 = 0.0;
|
const BASE_TOP: f32 = 0.0;
|
||||||
let top_inset = insets.as_deref().copied().unwrap_or_default().top;
|
let top_inset = insets.as_deref().copied().unwrap_or_default().top;
|
||||||
@@ -501,10 +498,6 @@ fn spawn_hud_band(insets: Option<Res<SafeAreaInsets>>, mut commands: Commands) {
|
|||||||
height: Val::Px(HUD_BAND_HEIGHT),
|
height: Val::Px(HUD_BAND_HEIGHT),
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
BackgroundColor(BG_HUD_BAND),
|
|
||||||
// Sit one z-rung below the HUD content so the buttons and text
|
|
||||||
// paint on top, but above the card sprites (which are 2D-world
|
|
||||||
// entities and rendered behind UI regardless).
|
|
||||||
ZIndex(Z_HUD - 1),
|
ZIndex(Z_HUD - 1),
|
||||||
SafeAreaAnchoredTop { base_top: BASE_TOP },
|
SafeAreaAnchoredTop { base_top: BASE_TOP },
|
||||||
HudBand,
|
HudBand,
|
||||||
|
|||||||
@@ -1785,9 +1785,8 @@ mod tests {
|
|||||||
let layout = compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true);
|
let layout = compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true);
|
||||||
// Tableau 6 has 7 cards.
|
// Tableau 6 has 7 cards.
|
||||||
let (_, size) = pile_drop_rect(&PileType::Tableau(6), &layout, &game);
|
let (_, size) = pile_drop_rect(&PileType::Tableau(6), &layout, &game);
|
||||||
// Expected: card_height + 6 * fan. fan = 0.25 * card_height, so
|
// Expected: card_height + 6 fan steps.
|
||||||
// size.y = card_height * (1 + 6 * 0.25) = card_height * 2.5.
|
let expected = layout.card_size.y * (1.0 + 6.0 * layout.tableau_fan_frac);
|
||||||
let expected = layout.card_size.y * 2.5;
|
|
||||||
assert!(
|
assert!(
|
||||||
(size.y - expected).abs() < 1e-3,
|
(size.y - expected).abs() < 1e-3,
|
||||||
"expected {expected}, got {}",
|
"expected {expected}, got {}",
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ fn load_initial_theme(
|
|||||||
let id = settings
|
let id = settings
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.map(|s| s.0.selected_theme_id.as_str())
|
.map(|s| s.0.selected_theme_id.as_str())
|
||||||
.unwrap_or("dark");
|
.unwrap_or("classic");
|
||||||
let url = bundled_theme_url(id)
|
let url = bundled_theme_url(id)
|
||||||
.map(str::to_string)
|
.map(str::to_string)
|
||||||
.unwrap_or_else(|| format!("themes://{id}/theme.ron"));
|
.unwrap_or_else(|| format!("themes://{id}/theme.ron"));
|
||||||
|
|||||||
Reference in New Issue
Block a user