Compare commits

..

7 Commits

Author SHA1 Message Date
funman300 4df13695fc fix(engine): use classic theme fallback in load_initial_theme
Android Release / build-apk (push) Successful in 3m21s
SettingsResource is not yet available at Startup, so load_initial_theme
fell back to "dark" on every run. On AMOLED the dark back (▒151515) is
invisible, showing only a 24×32 px red badge — the "tiny red squares"
bug. Cascade-collapse and top-row legibility were visual consequences of
the same invisible face-down cards, not layout bugs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 14:06:34 -07:00
funman300 df22338c8a fix(ui): remove grey HUD band background and constrain stock badge to pile bounds
Android Release / build-apk (push) Successful in 4m30s
Bug 1: StockCountBadge was centred 12 px inward from the stock pile's right
edge but its half-width of 17 px pushed the right edge 5 px past the pile
boundary. On Android (H_GAP_DIVISOR=32, inter-pile gap ~4 px) the badge
corner covered the waste pile's left edge at Z=30, making the waste card
appear clipped. STOCK_BADGE_INSET.x: -12 → -20 keeps the right edge 3 px
inside the stock pile on every device.

Bug 2: The top HUD band Node had an opaque dark-grey BackgroundColor sized to
HUD_BAND_HEIGHT (64/80 px). With only Tier-1 content (~30 px) visible in
typical gameplay the grey block appeared far taller than its content. Removed
BackgroundColor from the band entity; layout reservation in compute_layout is
unchanged and the bottom action bar retains its own background.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 13:48:52 -07:00
funman300 7f450aab17 fix(android): default to classic theme to fix AMOLED card-back invisibility
Android Release / build-apk (push) Successful in 4m7s
Dark theme back.svg uses #151515 (near-black) as the card back background,
which AMOLED screens render as fully-off pixels, leaving only the tiny
#a54242 red badge visible — user sees solid red squares instead of card backs.

Fix: change fresh-install default theme from "dark" to "classic" (white
background with navy diamond pattern, clearly visible on all display types).
Also remove the stale "classic" -> "dark" sanitize migration, correct wrong
asset paths in load_card_images (classic/ subdirectory was missing), and
update tests that hardcoded the old TABLEAU_FAN_FRAC=0.25 constant.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 13:24:25 -07:00
funman300 d8f67dcad3 fix(ci): collapse multi-line Python to one-liner to fix YAML block scalar indentation error
Android Release / build-apk (push) Successful in 4m3s
2026-05-16 12:34:40 -07:00
funman300 ccb77f76b8 chore(release): promote Unreleased to 0.30.0 2026-05-16 12:31:51 -07:00
funman300 da54faf8e2 feat(engine): tighten tableau card fan offset (0.25→0.18, 0.20→0.14)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 12:31:18 -07:00
funman300 f3d01b5890 fix(ci): delete existing APK assets before upload to avoid duplicates on re-runs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 12:20:10 -07:00
8 changed files with 89 additions and 32 deletions
+13 -2
View File
@@ -92,7 +92,18 @@ jobs:
- name: Upload APK to release - name: Upload APK to release
run: | run: |
BASE="${{ env.GITEA_URL }}/api/v1/repos/${{ env.REPO }}"
AUTH="Authorization: token ${{ secrets.CI_TOKEN }}"
RELEASE_ID="${{ steps.release.outputs.id }}"
# Remove any existing APK assets so re-runs don't accumulate duplicates.
curl -sf -H "$AUTH" "$BASE/releases/$RELEASE_ID/assets" \
| python3 -c "import sys,json; [print(a['id']) for a in json.load(sys.stdin) if a['name'].endswith('.apk')]" \
| while read AID; do
curl -sf -X DELETE -H "$AUTH" "$BASE/releases/$RELEASE_ID/assets/$AID"
done
curl -sf -X POST \ curl -sf -X POST \
-H "Authorization: token ${{ secrets.CI_TOKEN }}" \ -H "$AUTH" \
-F "attachment=@${{ env.APK_OUT }};type=application/vnd.android.package-archive" \ -F "attachment=@${{ env.APK_OUT }};type=application/vnd.android.package-archive" \
"${{ env.GITEA_URL }}/api/v1/repos/${{ env.REPO }}/releases/${{ steps.release.outputs.id }}/assets" "$BASE/releases/$RELEASE_ID/assets"
+54
View File
@@ -6,6 +6,60 @@ 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
### Changed
- **Tableau card spacing tightened.** Face-up card fan reduced from 25% to 18%
of card height; face-down from 20% to 14%. Cards on tableau piles sit closer
together while still showing enough of each card to read the pile depth.
## [0.29.0] — 2026-05-16 ## [0.29.0] — 2026-05-16
### Fixed ### Fixed
+4 -5
View File
@@ -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 {
+7 -6
View File
@@ -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.
+6 -13
View File
@@ -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,
+2 -3
View File
@@ -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 {}",
+2 -2
View File
@@ -75,7 +75,7 @@ const VERTICAL_GAP_FRAC: f32 = 0.2;
/// column must fit at this fraction). On desktop (height-limited) windows the /// column must fit at this fraction). On desktop (height-limited) windows the
/// adaptive computation returns this value exactly; on portrait phones it /// adaptive computation returns this value exactly; on portrait phones it
/// expands to fill available vertical space. /// expands to fill available vertical space.
const TABLEAU_FAN_FRAC: f32 = 0.25; const TABLEAU_FAN_FRAC: f32 = 0.18;
/// Minimum fraction for face-down tableau cards. Scales proportionally with /// Minimum fraction for face-down tableau cards. Scales proportionally with
/// the adaptive face-up fraction so hit-testing and rendering stay in sync. /// the adaptive face-up fraction so hit-testing and rendering stay in sync.
@@ -84,7 +84,7 @@ const TABLEAU_FAN_FRAC: f32 = 0.25;
/// enough of each card back to read as a meaningful stack rather than a /// enough of each card back to read as a meaningful stack rather than a
/// thin sliver. The ratio to TABLEAU_FAN_FRAC (0.80) is preserved by /// thin sliver. The ratio to TABLEAU_FAN_FRAC (0.80) is preserved by
/// the adaptive scaling in `compute_layout`. /// the adaptive scaling in `compute_layout`.
const TABLEAU_FACEDOWN_FAN_FRAC: f32 = 0.20; const TABLEAU_FACEDOWN_FAN_FRAC: f32 = 0.14;
/// Largest possible face-up tableau column in Klondike: a King down to an Ace /// Largest possible face-up tableau column in Klondike: a King down to an Ace
/// after every face-down card has flipped on column 7. Layout sizing must keep /// after every face-down card has flipped on column 7. Layout sizing must keep
+1 -1
View File
@@ -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"));