9ef5759f40376dde85236406d1f9b0c4b621656c
25 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3cffbc2c51 |
feat(engine): embed classic theme into binary like dark theme
Classic SVGs and manifest are now compiled in via include_bytes!(), making the theme available on all platforms (desktop, Android) without requiring filesystem assets. Removes the now-redundant Dockerfile COPY of solitaire_engine/assets/themes/classic. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
20b7a617e0 |
feat(engine): rename themes — Classic is default, Dark replaces Default
Build and Deploy / build-and-push (push) Successful in 33s
- Rename assets/themes/default/ → assets/themes/dark/; update theme.ron id/name to "dark"/"Dark" - Rename all DEFAULT_THEME_* constants → DARK_THEME_* and default_theme_svg_bytes / populate_embedded_default_theme → dark_* - Add bundled_theme_url() helper for URL resolution without needing the registry (used by Startup systems where ordering isn't guaranteed) - Registry now lists Classic first (new player default), Dark second - settings.rs default_theme_id() returns "classic" so fresh installs start on the white card theme Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
8325bf6cf7 |
chore: rename app from Solitaire Quest to Ferrous Solitaire
Replace all display-name occurrences across web pages, Rust source, docs, and Cargo metadata. Update localStorage token key from sq_token to fs_token. Tagline "Klondike Solitaire" retained as genre descriptor. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
859b69b3c5 |
fix(android): A2/A3/A4 — APK build doc, dead refs, modal hit targets
A2: docs/ANDROID.md — remove stale "permanent fix to come" note;
clarify --lib is the canonical command; root-cause the upstream
cargo-apk bug. SESSION_HANDOFF.md closes the open item.
A3: Remove dead CARD_PLAN.md references from four source module
doc comments (theme/importer.rs, theme/plugin.rs, assets/mod.rs,
assets/svg_loader.rs). Also fix stale "future picker UI" language
in plugin.rs (picker shipped in Phase 7).
A4: ui_modal.rs spawn_modal_button — add min_height: Val::Px(48.0)
so every modal action button meets Material's 48 dp touch target
minimum. Modal button height was 42 px (2×SPACE_3 + TYPE_BODY_LG);
now clamped to 48 px minimum. Cards at 40 dp on 360 dp phones are
layout-constrained (7 columns) and cannot be widened.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
||
|
|
dd970215cc |
fix(engine): drop card-face border to remove gray-corner artifact
Player feedback after the 2-colour revert: "I do not like the
grey corners on the cards." The visible artifact was anti-
aliasing physics — the 1 px suit-coloured stroke (red for
hearts/diamonds, near-white for clubs/spades) faded through
gray pixels into the dark play surface at each rounded corner,
producing a visible "gray sliver" at the four arcs of every
card.
Fix: drop the stroke entirely. The card body fill defines the
shape against the play surface; the 5-unit brightness gap
between `#1a1a1a` body and `#151515` surface is enough to read
as a card edge without an explicit stroke. Anti-aliasing on a
fill-only rounded rect blends `#1a1a1a → #151515` over a few
pixels — barely perceptible compared to the
`stroke → transparent` gradient that produced the artifact.
### Changes
- `card_face_svg.rs`: removed `stroke="{colour}" stroke-width="2"`
from the card body rect. Reverted the 1 px stroke inset back
to `(x=0, y=0, width=256, height=384)` since there's no
longer a stroke to keep inside the pixmap. Module-level
comment updated to document the reasoning.
- `design-system.md` § Game Cards line 225 updated: "Border:
1px solid in suit color" → "Border: none." with the
artifact rationale recorded as audit trail.
- `card_face_svg_pin.rs` rebaselined: all 52 face hashes drift
(every card's perimeter pixels changed); 5 back hashes
unchanged.
Workspace clippy + cargo test --workspace clean. 1191 passing.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
ddb65403c2 |
feat(engine): revert to traditional 2-colour deck with saturated red + near-white
Per player feedback after the brief 4-colour-deck experiment: "can we make the card suit colors the same as a regular solitaire game would." Reverts the 4-colour split (`62b61cc`) and bumps both 2-colour hues to read more like a real Microsoft-Solitaire-on-dark-mode deck. ### Constants - `RED_SUIT_COLOUR`: `#fb9fb1` (Terminal pink, then briefly hearts-only) → `#e35353` (saturated red). More chromatic, less pastel; reads as "the red suit" rather than "a Terminal- themed pink." Visually distinct from `ACCENT_PRIMARY` `#a54242` (the brick-red CTA accent) so chrome and suit don't collapse to the same hue. - `BLACK_SUIT_COLOUR`: `#d0d0d0` (matched `TEXT_PRIMARY`) → `#e8e8e8` (near-white). Bumped slightly brighter so it reads as a chromatic-neutral counterpart to the new saturated red, not as "the same gray as body text." `TEXT_PRIMARY_HC` (`#f5f5f5`) is still brighter for the high-contrast boost path. - `RED_SUIT_COLOUR_HC`: `#ff8aa0` (pinkish boost matching the v0.21.0 pink default) → `#ff6868` (brighter saturated red). Now reads as "more chromatic" than the new default red, not "less saturated." - `DIAMOND_SUIT_COLOUR` and `CLUB_SUIT_COLOUR` deleted — the 4-colour split is gone, hearts/diamonds re-pair under `RED_SUIT_COLOUR` and clubs/spades under `BLACK_SUIT_COLOUR`. ### `card_face_svg.rs` - Module-level constants collapse from four (`SUIT_HEART` / `SUIT_DIAMOND` / `SUIT_CLUB` / `SUIT_SPADE`) back to two (`SUIT_RED` / `SUIT_DARK`) at the new saturated-red / near-white values. - `suit_paint()` reverts to the 2-colour pairing: hearts filled-red, diamonds outlined-red, spades filled-near-white, clubs outlined-near-white. Filled-vs-outlined glyph differentiation stays the always-on CBM fallback. ### `card_plugin.rs` - `text_colour()` reverts to a `card.suit.is_red()` bifurcation. Comment block updated to reflect the new truth table: red suits → saturated red (or CBM lime / HC brighter red); dark suits → near-white (or HC brighter near-white). ### Tests Test block restructured back to the pre-4-colour shape: two red/black pairing tests instead of one 4-colour distinctness test. CBM/HC compose tests retuned to the 2-colour world (red suits compose, dark suits compose; no separate diamonds-immune or clubs-immune cases). 1191 passing / 0 failing — net 0 from the prior commit (3 tests removed: the 4-colour distinctness test + the diamonds/clubs-immune test; 2 tests added back: the red-pairing + dark-pairing tests; existing tests amended to new colour assumptions). ### `card_face_svg_pin` All 52 face hashes drift (every suit's colour shifted); 5 back hashes unchanged. Surgical rebaseline. ### `design-system.md` §Suit Colors retitled "Two-color traditional pairing", table updated with the new hex values, CBM section text simplified back to red→lime swap on both red suits. Workspace clippy clean. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
62b61cc786 |
feat(engine): switch card fronts to 4-colour deck
Hearts pink (`#fb9fb1`), Diamonds gold (`#ddb26f`), Clubs lime (`#acc267`), Spades gray (`#d0d0d0`) — each suit picks up its own base16-eighties accent so a player scanning the table can distinguish the suit by hue alone (faster recognition than the 2-colour traditional red/black scheme; common in poker decks). All four colours already exist in the palette as semantic state-token accents, so this is a pure remapping at the suit- glyph site, not a palette extension. The outlined-glyph differentiation (♦ ♣ outlined, ♥ ♠ filled) is preserved on top of the colour split — it stays the always- on colour-blind fallback per `design-system.md` §Accessibility, and matters more than ever now that CBM hearts (lime) and default clubs (lime) share a hue. ### Changes - `card_face_svg.rs`: split `SUIT_RED` / `SUIT_DARK` into four per-suit constants (`SUIT_HEART` / `SUIT_DIAMOND` / `SUIT_CLUB` / `SUIT_SPADE`). `suit_paint()` returns each suit's own colour. Card border picks up the suit colour automatically via the existing `(colour, paint)` destructure. - `card_plugin.rs`: new `DIAMOND_SUIT_COLOUR` + `CLUB_SUIT_COLOUR` constants; `text_colour()` rewritten as a per-suit match (was red/black bifurcation). Both rendering paths (PNG production + constant fallback under MinimalPlugins) stay in lockstep. - CBM behaviour clarified: only hearts swap to lime now; diamonds + clubs + spades are already hue-distinct from the heart pink and stay unchanged. Under CBM the heart (lime) and club (lime) share a hue but stay distinguishable via the always-on filled-vs-outlined glyph differentiation. - HC behaviour: only hearts (→ HC red) and spades (→ HC white) have defined boosts. Diamonds (gold) and clubs (lime) are already mid-luminance accents and stay at their default. New test `text_colour_diamonds_and_clubs_are_immune_to_accessibility_flags` pins all four flag combinations as no-ops for the gold + lime suits. - `design-system.md` §Suit Colors retitled "Four-color deck" with the 4-colour table; CBM section text updated to describe the hearts-only swap and the hearts/clubs hue collision under CBM. - `card_face_svg_pin.rs` rebaselined: 26 hashes drift (13 clubs + 13 diamonds — the two suits whose colours changed). Hearts, spades, and the 5 backs all keep their prior hashes. Surgical scope, exactly what the pin test was designed to surface. ### Tests 1191 passing / 0 failing — net 0 from the prior baseline: two old 2-colour tests removed (`text_colour_is_red_for_hearts_and_diamonds`, `text_colour_is_black_for_clubs_and_spades`), one consolidated 4-colour test added (`text_colour_4_colour_deck_assigns_each_suit_its_own_hue`) plus a pairwise-distinct invariant guard, and one new test covering the gold/lime suits' immunity to CBM/HC flags. Six existing CBM/HC tests rewritten to use only the suits each flag actually affects under the new scheme (hearts for CBM, hearts + spades for HC). Workspace clippy clean. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
3eb3a26789 |
feat(app): wire desktop window icon — Terminal ▌RS mark at runtime
Closes Resume-prompt Option A (the post-v0.21.0 first option). Half-day desktop work, no cert dependency. Three deliverables: 1. **SVG-authored icon** (`solitaire_engine/src/assets/icon_svg.rs`) — square Terminal mark: `#151515` background, brick-red `#a54242` 1 px border, brick-red ▌ cursor block centered, "RS" monogram in `#d0d0d0` foreground gray beneath. Same shape that already lives on the splash boot screen and card-back monogram, reused as the project's signature visual mark. Authored in a 64-unit logical box so it scales cleanly at every rasterisation target. 2. **9-size PNG hierarchy** (16, 24, 32, 48, 64, 128, 256, 512, 1024 px) regenerated by `solitaire_engine/examples/icon_generator.rs` into `assets/icon/icon_<size>.png`. Sizes cover Linux hicolor (16, 24, 32, 48, 64, 128, 256, 512), Windows .ico targets (16, 32, 48, 256), and macOS .icns targets (16, 32, 64, 128, 256, 512, 1024). The runtime path uses just the 256 px slot; the smaller sizes are pre-rendered for downstream packaging. 3. **Runtime `Window::icon` wiring** (`solitaire_app/src/lib.rs`). Bevy 0.18 has no `Window::icon` field — the icon is set through the underlying `winit::window::Window` via the `WinitWindows` resource. `set_window_icon` runs each Update tick, retries silently until `WinitWindows` is populated (typically frame 1 or 2), decodes the embedded 256 px PNG via `tiny_skia`, builds a `winit::window::Icon`, and self-disables via `Local<bool>`. Same one-shot pattern as `apply_smart_default_window_size`. Desktop-only — Android draws its launcher icon from the APK manifest, so the system is target-gated to `cfg(not(target_os = "android"))`. Dep changes (CLAUDE.md §8 user-confirmed): - `winit = "0.30"` promoted from a transitive Bevy dep to a direct dep on `solitaire_app` so `winit::window::Icon` is in scope — bevy_winit 0.18 doesn't re-export it. Version pinned to whatever Bevy uses; if Bevy bumps winit, this line bumps in lockstep. - `tiny-skia` added as a direct dep on `solitaire_app` for PNG → RGBA decode. Already in workspace deps for `solitaire_engine`; no version drift risk. - Both new deps target-gated to non-Android only. Test infrastructure: `solitaire_engine/tests/icon_svg_pin.rs` hashes the rasterised RGBA bytes at all 9 sizes via FNV-1a (same shape as `card_face_svg_pin`). Bootstrap pattern (empty EXPECTED → panic with hashes formatted as Rust source → paste back in) handles future intentional builder edits cleanly. Workspace clippy + cargo test --workspace clean. 1185 passing (+1 from v0.21.0's 1184 baseline — the icon pin's `rasterised_icon_bytes_match_pinned_hashes`). Out of scope for this commit: `.icns` / `.ico` bundling for macOS / Windows app packaging. Both are packaging-time concerns (set via bundle manifests, not runtime calls) and would need new deps (`ico` and `icns` crates) — separate followup if/when the project ships as a packaged macOS / Windows app rather than just `cargo run`. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
a292a7ead0 |
feat(engine): swap ACCENT_PRIMARY from cyan #6fc2ef to brick red #a54242
Project-wide palette shift at user request. Replaces the cyan primary accent everywhere it surfaces — splash boot screen, home menu glyphs, action chevrons, replay overlay banner + scrub fill + chip border, achievement checkmarks, leaderboard #1 indicator, radial menu fill, focus ring, card-back canonical badge, etc. — with `#a54242` from the same base16-eighties family as the existing pink suit colour. Knock-on changes that all land in this commit per the lockstep rule: - ui_theme.rs: ACCENT_PRIMARY (#a54242), ACCENT_PRIMARY_HOVER (#c25e5e brightened companion), FOCUS_RING (same hue, 0.85 alpha). Module-level palette comment + STOCK_BADGE_FG + CARD_SHADOW_ALPHA_DRAG doc strings updated to match. - card_plugin.rs: card_back_colour(0) now returns the brick-red ACCENT_PRIMARY (was cyan). RED_SUIT_COLOUR_CBM swapped from cyan to lime #acc267 — the CBM alternative needs to stay hue-distinct from the new red-family primary, lime is the next-best non-red base16-eighties accent. text_colour doc + CBM tests renamed cyan→lime in lockstep (text_colour_color_blind_mode_swaps_red_suits_to_lime). - card_face_svg.rs: BACK_ACCENTS[0] now "#a54242" (canonical Terminal back). - splash_plugin.rs / ui_modal.rs / replay_overlay.rs / selection_plugin.rs: descriptive "cyan" comments swapped to "accent" / "primary-accent" wording so the doc strings stay decoupled from any specific hue. Future palette tweaks won't require comment churn. - design-system.md: YAML token frontmatter updated (primary, surface-tint, suit-red-cb, primary-container, on-primary-container, inverse-primary). Palette table gains a project-specific `base08` slot for the new red. CTA / Selection / Card-back badge / Primary button / Bottom-bar active-icon / glow / CBM swap text all retuned. Historical references preserved (e.g. "Was cyan #6fc2ef before the 2026-05-08 swap") so the audit trail stays in the spec. - card_face_svg_pin.rs: rebaselined. Exactly one hash drift (back_0 — the canonical Terminal back's badge changed colour). Other 56 hashes identical (face SVGs don't reference the accent; back_1..4 use unchanged accents). The one-hash-drift signal confirms the change scope was surgical. Workspace clippy + cargo test --workspace clean, 1184 passing. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
dd101b3d54 |
fix(engine): render bottom-right card glyph upright (no 180° rotation)
The user noticed the bottom-right large suit glyphs were rendering upside-down — point-up hearts, stem-up spades — because the SVG transform pipeline applied a `rotate(180)` to match the traditional playing-card inverted-corner convention. That convention exists so a card reads correctly when flipped or read from the opposite side of the table. Single-orientation digital play doesn't benefit from it; most modern digital decks have abandoned it. User preference is upright. Drops the rotate from face_svg's bottom-right `<g transform>` and adjusts the translate so the visible glyph still lands at (178, 286)–(242, 350) — same screen footprint, same scale, just no flip. design-system.md § Game Cards updated in lockstep — line 220 no longer says "rotated 180°", instead documents the deliberate deviation from the traditional convention. Knock-on lockstep changes in this commit: - EXPECTED in tests/card_face_svg_pin.rs rebaselined: 52 face hashes shift, 5 back hashes unchanged. - assets/cards/faces/*.png regenerated (52 face PNGs). - solitaire_engine/assets/themes/default/*_*.svg regenerated (52 theme face SVGs that production rasterises at startup). Workspace clippy + cargo test --workspace clean. Pin test passes against the new hashes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
af414b6aed |
fix(engine): render card suit glyphs as SVG paths instead of text
The user's first post-migration screenshot showed near-invisible suit glyphs on every card — the rank rendered at correct size but the ♠ ♥ ♦ ♣ marks were tiny dots regardless of the requested 20px / 64px font-size. Root cause: the bundled FiraMono in svg_loader::shared_fontdb doesn't carry usable Unicode suit glyphs (U+2660-2666). usvg silently fell back to a substitute rendering at default size, producing the "tofu" effect. Fixes by replacing the `<text>` glyph rendering with inline SVG paths. `suit_path_d(suit)` returns a single closed-perimeter path authored in a 32 × 32 logical box, then face_svg wraps it in two `<g transform>` blocks (top-left small + bottom-right rotated large). Path-based rendering bypasses the font system entirely — same bytes on every machine, no fontdb dependency, no substitution risk. Same path data renders correctly whether filled (♥ ♠) or outlined (♦ ♣ — the always-on color-blind glyph differentiation from the design system). Knock-on changes that must land in this commit per the migration plan's lockstep rule: - `EXPECTED` in tests/card_face_svg_pin.rs rebaselined: 52 face hashes change (text → path), 5 back hashes unchanged (back_svg untouched). The bootstrap pattern in the test handled the rebaseline cleanly — empty EXPECTED, re-run, paste, re-run. - assets/cards/faces/*.png regenerated (the 52 face PNGs). - solitaire_engine/assets/themes/default/*_*.svg regenerated (the 52 theme face SVGs that production rasterises at startup). Both rendering paths must agree. Workspace clippy + cargo test --workspace clean. Pin test passes against the new hashes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
a14200ac2f |
fix(engine): regenerate default theme SVGs to Terminal aesthetic
Step 4's PNG regeneration left the cards looking unchanged at
runtime because the PNGs at assets/cards/ are only the *fallback*
art — production renders the bundled-default theme's SVGs, which
get include_bytes!()-embedded into the binary by
solitaire_engine::assets::sources and applied to CardImageSet at
startup by theme::plugin::apply_theme_to_card_image_set. Those
SVGs were still the legacy vector-playing-cards art.
Extends card_face_generator to write SVGs into both runtime
paths in lockstep:
1. assets/cards/{faces,backs}/*.png — fallback art (unchanged
from step 4).
2. solitaire_engine/assets/themes/default/*.svg — what production
actually renders. 52 face SVGs + 1 back SVG, generated from
the same face_svg / back_svg builders as the PNGs so the two
paths can never visually diverge.
Adds two helper functions to card_face_svg:
- theme_suit_token (clubs/diamonds/hearts/spades — lowercase
full word, matching CardKey::manifest_name)
- theme_rank_token (ace/2..10/jack/queen/king — same)
The theme back uses BACK_ACCENTS[0] (canonical Terminal cyan).
The other four accents only live as PNG fallbacks because the
theme system carries one back per theme.
Net SVG diff: -14884 / +940 lines — the legacy vector-playing-
cards SVGs were ~300 lines each of Inkscape-authored paths;
the Terminal SVGs are ~10 lines of programmatic output.
Workspace clippy + cargo test --workspace clean. Pin test
unaffected (the SVG builders themselves did not change).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
48b28d29f8 |
test(engine): pin card-face SVG output against rasteriser drift
Step 3 of the migration plan in docs/ui-mockups/card-face-migration.md. Extracts face_svg / back_svg + palette constants from the card_face_generator example into a new solitaire_engine::assets::card_face_svg module so an integration test can call them. The example becomes a thin wrapper. The new tests/card_face_svg_pin.rs hashes the raw RGBA8 pixel bytes from rasterising every face × suit + every back accent and compares each FNV-1a fingerprint against an embedded constant. Catches silent rendering drift if usvg / resvg / tiny_skia / the bundled FiraMono ever change in a way that perturbs pixels. Hashing is FNV-1a inline (~5 lines) rather than adding sha2 or blake3 — cryptographic strength isn't load-bearing here, just stable byte fingerprints. When the SVG builders intentionally change, empty EXPECTED to `&[]` and re-run the test once; it panics with the new hashes formatted as Rust source ready to paste back in. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
4b51e50203 |
fix(data): route data_dir() through a per-platform shim so Android persists
dirs::data_dir() returns None on Android, which silently disabled every persistence path (settings, stats, achievements, replays, game-state, time-attack sessions, user themes). New solitaire_data::platform::data_dir() shim falls through to dirs::data_dir() on desktop and returns the per-app sandbox at /data/data/com.solitairequest.app/files on Android — no JNI needed, since the package id is pinned in [package.metadata.android]. CLAUDE.md §10 already flagged this as a known pitfall; the shim pays it down at the one chokepoint instead of per feature. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
aad8bb9c83 |
Revert "feat(engine): bundle Rusty Pixel as a built-in theme"
This reverts commit
|
||
|
|
21ec03b157 |
feat(engine): bundle Rusty Pixel as a built-in theme
The pixel-art card theme generated via Claude Design (53 PNGs at
256x384, ~340 KB total) now ships embedded in the binary alongside
the existing default SVG theme. Players see the new theme in the
picker out of the box without needing to drop files into
~/.local/share/solitaire_quest/themes/.
solitaire_engine/assets/themes/rusty-pixel/:
- 53 PNGs (52 face cards + 1 back) at 256x384
- theme.ron declaring meta.id = "rusty-pixel",
card_aspect = (2, 3), pixel_art = true
assets/sources.rs:
- New constants RUSTY_PIXEL_THEME_MANIFEST_URL,
RUSTY_PIXEL_THEME_MANIFEST_PATH,
RUSTY_PIXEL_THEME_MANIFEST_BYTES.
- New embed_rusty_pixel_png! macro mirroring embed_default_svg!.
- New RUSTY_PIXEL_THEME_PNGS table — 53 entries, one per file.
- New rusty_pixel_theme_png_bytes(filename) lookup helper
mirroring default_theme_svg_bytes for the thumbnail cache.
- New populate_embedded_rusty_pixel_theme(app) registers the
manifest + every PNG into Bevy's EmbeddedAssetRegistry.
- AssetSourcesPlugin::build now calls both populate functions
so the picker has both themes loadable from the binary alone.
theme/registry.rs:
- New rusty_pixel_entry() returns the bundled metadata.
- build_registry now inserts default + rusty-pixel ahead of the
user-dir scan, and filters user themes whose id collides with
a bundled built-in. Bundled wins on collision because it's
guaranteed complete; the user's overriding copy may be partial
or stale.
- Updated existing tests for the new len()=2-instead-of-1 baseline.
- New test user_theme_id_collision_with_bundled_is_dropped pins
the dedup contract.
theme/plugin.rs:
- load_initial_theme + react_to_settings_theme_change now both
consult a new manifest_url_for(theme_id) helper that routes
bundled built-ins through embedded:// and unknown ids through
themes://. Drops the previous hard-coded "default →
DEFAULT_THEME_MANIFEST_URL else themes://" branch.
- read_theme_preview_bytes also checks the rusty-pixel embed
table before falling through to the user-dir filesystem read,
so the picker chip's thumbnail works on a fresh install where
the user-dir doesn't exist.
Workspace: 1172 passing tests / 0 failing, was 1171 (+1 net from
the new collision test). cargo clippy --workspace --all-targets
-- -D warnings clean. Binary grows by ~340 KB (the 53 bundled
PNGs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
ba527de351 |
feat(engine): card-art thumbnails in the theme picker
Settings → Cosmetic's theme picker showed only the theme name. Now each chip carries a small Ace-of-Spades + back preview pair so the player can see what each theme looks like before switching. A new ThemeThumbnailCache resource keys per-theme by id and stores two Handle<Image>s (ace + back) rasterised at thumbnail resolution via the existing rasterize_svg path. Generation runs once per theme registration in theme_plugin; subsequent picker re-spawns just look up the cached handles. Themes that lack one of the preview SVGs (broken user theme) get a Handle::default() placeholder rather than crashing — the placeholder renders as a transparent rectangle the same size as the missing thumbnail. The picker chip spawn loop in settings_plugin reads the cache and renders the pair as two child sprites above the chip text. The selected-theme chip's existing STATE_SUCCESS tint sits behind the thumbnails; contrast stays readable. Asset-source plumbing in assets/sources.rs and assets/mod.rs picks up the new bytes-loading helper that the thumbnail generator uses for embedded:// theme assets at startup time (before AssetServer is fully initialised). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
17f9b518f1 |
fix(engine): bundle fonts only and drop system-font fallback
Code-review feedback: the SVG rasteriser mixed three font-resolution layers (load_system_fonts + bundled FiraMono + lenient resolver appending CSS generics), which made card text rendering depend on which fonts the host machine happened to have. The Bevy UI face loaded separately at runtime via AssetServer. Picking option (a) from the review and applying it consistently: bundle FiraMono via include_bytes!() in BOTH layers, no system fallback anywhere. solitaire_engine/src/font_plugin.rs now embeds main.ttf at compile time and registers it with Assets<Font>. A parse failure aborts with "bundled FiraMono failed to parse — binary is corrupt"; the MinimalPlugins early-return stays as a "this plugin doesn't apply in headless tests" check (consumers query Option<Res<FontResource>> and degrade cleanly), not a production fallback. solitaire_engine/src/assets/svg_loader.rs drops load_system_fonts entirely, drops the lenient_font_resolver, and drops the five set_*_family pins. The new bundled_font_resolver ignores the SVG's font-family request and always returns the single bundled face — the bundled card SVGs reference Arial / Bitstream Vera Sans by name and we deliberately don't ship those, so routing every query to FiraMono keeps rasterisation deterministic. shared_fontdb asserts the embedded bytes parsed. The two layers now embed the same path (assets/fonts/main.ttf) independently, so they can't drift. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
fdb6c2ecfe |
fix(engine): bundle FiraMono into SVG fontdb as last-resort fallback
The hayeah card SVGs reference Bitstream Vera Sans and Arial by name.
The lenient FontResolver from
|
||
|
|
efa063fb8f |
fix(engine): fall through to system default font on unmatched family
Replaces the previous LogPlugin-filter approach (which suppresses the
warn message) with a fix at the source: a custom usvg FontResolver
that appends `sans-serif` and `serif` to every family-lookup query.
usvg's default selector queries fontdb with [SVG-requested families,
Serif] and emits `log::warn!("No match for '{family}'")` when the
query returns None. On systems without the SVG's named family (Arial
on Linux, etc.), every text node logs a warn even though the system
has perfectly good fonts available — the warn is a false negative
because fontdb's named-family lookup is exact-match only.
The new resolver appends both `Family::SansSerif` and `Family::Serif`
to the query, both resolved by fontdb (via fontconfig on Linux or
built-in defaults elsewhere) to whatever the system has installed.
The query now finds *some* face on any reasonably configured machine,
so `id.is_none()` is never true and the warn branch never fires. The
visible behaviour: SVGs that request unavailable named families now
silently use the system's default sans-serif font.
Reverts the LogPlugin filter from main.rs — silencing warns at the
log level was the wrong layer; fixing the lookup is.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
78cf30e906 |
fix(engine): silence usvg font-substitution warn spam
The bundled hayeah card SVGs declare font-family="Arial" for rank/suit text. usvg matches family names exactly, so on systems without Arial installed (every Linux distro by default) every text node bridged a log::warn! into our tracing output — 50+ lines per launch. Two-part fix: - svg_loader now populates a process-wide fontdb with system fonts (lazy via OnceLock) so substitution actually has faces to fall through to. usvg::Options::default() ships an empty fontdb, which meant text glyphs had nothing to fall back on at all. - LogPlugin extends DEFAULT_FILTER with usvg::text=error so the residual "no match" warns drop. The substitution itself works; the message is purely informational because Arial truly isn't installed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
b98cb8a99f |
feat(assets): swap card art to hayeah/playing-cards-assets (MIT)
Replaces the previous xCards-derived card faces (LGPL-3.0) with
hayeah/playing-cards-assets, which itself derives from the
public-domain vector-playing-cards Google Code project. The whole
package is MIT now — see CREDITS.md for the new attribution table
and the simpler license summary.
solitaire_engine/assets/themes/default/
52 face SVGs (clubs/diamonds/hearts/spades × ace/2-10/jack/queen/
king) — copied from hayeah, renamed to the canonical
`{suit}_{rank}.svg` form `CardKey::manifest_name` produces. The
bundled default theme manifest references each by the same name.
back.svg — original midnight-purple-themed card back, hand-written
to match the project's design tokens (BG_BASE / BG_ELEVATED /
ACCENT_PRIMARY / ACCENT_SECONDARY). MIT, original work.
assets/cards/faces/{RANK}{SUIT}.png
52 PNGs regenerated from the new SVGs at 750-tall via resvg 0.47.
These remain the legacy backwards-compat path that
`card_plugin::load_card_images` reads at startup; once the runtime
theme system finishes loading the embedded default theme, the
CardImageSet's face handles are overwritten with the SVG-rendered
variants and these PNGs become moot. Keeping them in place avoids
a brief blank-card flash before the async theme load completes.
solitaire_engine/src/assets/sources.rs
embed_default_svg!() macro + DEFAULT_THEME_SVGS table that bundles
every face + the back into the binary at compile time via
include_bytes!. populate_embedded_default_theme now iterates the
table so the EmbeddedAssetRegistry is populated under the same
asset paths the manifest references.
CREDITS.md
License summary collapses from MIT + LGPL-3.0 + OFL-1.1 to MIT +
OFL-1.1 (the OFL still applies to FiraMono). The hayeah upstream
URL replaces the previously-blank xCards entry.
cargo build / clippy --workspace --all-targets -- -D warnings / test
--workspace all green (960 passed, 0 failed, 9 ignored).
|
||
|
|
172d7773f0 |
feat(engine): asset sources for embedded + user theme dirs (Card theme phase 3)
Implements Phase 3 of CARD_PLAN.md — the embedded:// + themes:// asset
sources the card-theme system loads from. The bundled default-theme
manifest ships in the binary via Bevy's EmbeddedAssetRegistry; user
themes load from user_theme_dir() through a FileAssetReader-backed
source registered as `themes://`.
Registration is split across:
register_theme_asset_sources(&mut App)
Called BEFORE DefaultPlugins. Registers `themes://` while
AssetSourceBuilders is still mutable.
AssetSourcesPlugin
Added AFTER DefaultPlugins. Populates the EmbeddedAssetRegistry
that AssetPlugin's build step would otherwise overwrite.
Constants exposed for downstream consumers:
USER_THEMES = "themes" (asset-source name)
DEFAULT_THEME_MANIFEST_URL = "embedded://solitaire_engine/assets/themes/default/theme.ron"
Includes a stub default theme.ron (52 face slots + back) so
`ThemeManifest::validate()` accepts it today; PROVENANCE.md documents
the plan to drop in real SVG art (hayeah/playing-cards-assets) in a
follow-up.
4 new tests covering source registration, embedded-registry
population, manifest validation against the embedded stub, and the
manifest-URL constant matching the embedded asset path.
cargo check --workspace --all-targets / clippy --workspace
--all-targets -- -D warnings clean. cargo test could not be run in
this turn because the C linker (cc) is unexpectedly absent from the
sandbox; the test bodies compile cleanly under cargo check --tests
and will run on a normal toolchain.
|
||
|
|
205ad6f646 |
feat(engine): per-platform user-theme directory (Card theme phase 5)
Implements Phase 5 of CARD_PLAN.md. Phase 3 (asset sources) and
Phase 7 (zip importer) both depend on this so it goes first.
solitaire_engine/src/assets/user_dir.rs
user_theme_dir() -> PathBuf
Desktop (Linux/macOS/Windows): joins dirs::data_dir() with
"solitaire_quest/themes" — same parent as the rest of the
project's per-user files (settings.json, stats.json, etc.)
Mobile (Android/iOS): reads a process-wide OnceLock populated
by set_user_theme_dir() at entry-point bootstrap. Panics with a
targeted message if the override is missing — there is no
platform default we can guess that won't be wrong inside iOS
sandboxing or the Android storage model.
set_user_theme_dir(PathBuf) -> Result<(), PathBuf>
First-write-wins. Mobile entry points call this before App::run().
The plan suggested the `directories` crate; reused the existing `dirs`
workspace dep instead to keep the dependency surface minimal — both
crates share an author and the platform behaviour we need is identical.
3 new tests covering pure path composition (desktop nesting + empty
root) and a desktop-target-gated check that the detected data dir is
absolute. The OnceLock override is intentionally not unit-tested
because asserting its semantics would pollute global state for any
sibling test that calls `user_theme_dir()`.
|
||
|
|
b8fb3fbd6e |
feat(engine): SVG → Image asset loader (Card theme phase 1)
Implements the runtime SVG rasterisation pipeline that the card-theme
system (CARD_PLAN.md) is built on. Bevy 0.18 has no native SVG support;
this loader bridges usvg (parser) + resvg (renderer) + tiny-skia (CPU
pixmap) so the rest of the engine consumes themes as plain
Handle<Image>. Rasterisation happens once per (asset, settings) pair at
load time — Bevy's asset cache absorbs the cost.
solitaire_engine/src/assets/
mod.rs — module entrypoint
svg_loader.rs — SvgLoader (AssetLoader for .svg → Image)
SvgLoaderSettings { target_size: UVec2 } default 512×768
SvgLoaderError (Io / Parse / PixmapAlloc) via thiserror
rasterize_svg() helper exposed for non-asset-graph
callers (the future zip-importer validation step)
The rasteriser scales-to-fit while preserving aspect ratio, centring
the SVG inside the target box so a non-2:3 source doesn't pin to the
top-left corner.
7 new unit tests — default + custom target size, zero-dimension reject,
malformed-input reject, RGBA byte-count, extension advertisement, and
a compile-time guard that SvgLoaderSettings still satisfies the
AssetLoader::Settings trait bounds.
Workspace deps added: usvg 0.47, resvg 0.47, tiny-skia 0.12 (latest
minor versions; CARD_PLAN.md called out the placeholder numbers
needed verification).
cargo build / cargo clippy --workspace --all-targets -- -D warnings
/ cargo test --workspace all green (913 passed, 0 failed, 9 ignored —
+7 from the new loader tests).
|