- Add avatar_plugin: AvatarPlugin, AvatarResource, AvatarFetchEvent
- After AvatarFetchEvent fires, spawns an async reqwest download task
- On completion, decodes image bytes via image::load_from_memory →
Image::from_dynamic and inserts into Assets<Image>
- Expand auth task to also call fetch_me_with_token immediately after
login/register so avatar_url is available without a second round-trip
- poll_auth_task fires AvatarFetchEvent when avatar_url is Some, building
the full URL from base_url + relative avatar path
- Profile modal shows 48px circular avatar ImageNode when AvatarResource
is populated, or an initials disc (first letter of username) as fallback
- Add image = "0.25" and reqwest to solitaire_engine deps
- Add fetch_me_with_token helper to SolitaireServerClient for use when
the access token hasn't been persisted to keychain yet
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds *.jks / *.jks.bak / *.keystore to .gitignore so the
release signing material can never be committed accidentally.
Cargo.lock drift catches up with 7c07f71 (bevy dep added to
solitaire_data for Android target) — the prior commit edited
solitaire_data/Cargo.toml but didn't regenerate the lockfile.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds DifficultyLevel (Easy/Medium/Hard/Expert/Grandmaster/Random) to
solitaire_core::game_state alongside GameMode::Difficulty(DifficultyLevel).
Five seed catalogs (40 seeds each) are pre-verified by the new
gen_difficulty_seeds binary using tiered solver budgets (1K–200K moves).
DifficultyPlugin resolves StartDifficultyRequestEvent → catalog seed →
NewGameRequestEvent; Random uses a system-time seed and bypasses the
winnable-only filter. The home overlay gets an expandable Difficulty section
between Draw Mode and the mode grid; last-played tier persists in Settings.
Difficulty wins pool into Classic stats. 5 unit tests in difficulty_plugin.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a gen_seeds binary to solitaire_assetgen that brute-searches seeds
for hands solvable in ≤250 moves, then writes the list. The 75 new
seeds (0xCAFEBABE prefix) are appended to CHALLENGE_SEEDS in
solitaire_data::challenge.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
Two related platform-fit fixes for desktop launch:
1. Wayland session compatibility. The workspace Cargo.toml's
Bevy feature list previously enabled only `x11`, leaving
winit-on-Wayland to fall through to XWayland — the game
rendered inside an X11 frame stitched into the Wayland
compositor instead of as a native Wayland client. Adding
the `wayland` feature lets winit prefer Wayland when
WAYLAND_DISPLAY is set on the session, falling back to X11
when it isn't. Costs a few hundred KB of binary for the
libwayland-client bindings; comment in Cargo.toml explains
the trade.
2. Smart default window sizing. The fallback window size for
first launches (no saved geometry) was a fixed 1280x800. On
a 4K monitor that's a comparatively tiny window in one
corner; the game's cards then occupy a small physical area
even though the screen has plenty of room. New
`apply_smart_default_window_size` Update system queries
`Monitor` (with the `PrimaryMonitor` marker) and resizes the
primary window to ~70% of the monitor's *logical* size on
the first frame. Logical size already factors in the OS's
HiDPI scale factor, so:
- 1920x1080 / 1.0 scale → 1344x756 target
- 2560x1440 / 1.0 scale → 1792x1008 target
- 3840x2160 / 1.0 scale → 2688x1512 target
- 2880x1800 / 2.0 scale (Retina) → 1008x630 target
(same physical size as 1080p)
Clamped to the existing 800x600 minimum so old systems
don't get sub-minimum windows. Skipped entirely when saved
geometry was applied — the player's chosen size always
wins. Uses `Local<bool>` for one-shot semantics; the early-
exit per tick costs nothing once `*applied` is true.
Workspace: 1170 passing tests / 0 failing. cargo clippy
--workspace --all-targets -- -D warnings clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The theme picker chip's thumbnail loader hardcoded `.svg`
filenames (`spades_ace.svg`, `back.svg`) — a holdover from when
every shipped theme was vector-art. Raster-art user themes (e.g.
the v0.19 pixel-art theme generated via Claude Design and dropped
into ~/.local/share/solitaire_quest/themes/rusty-pixel/) had real
PNGs in their directory but the picker rendered placeholders
because it never tried the PNG sibling.
The fix is scoped to the thumbnail-cache pipeline. In-game card
rendering already worked via Bevy's standard PNG asset loader on
manifest-declared face/back paths — only the picker's small
preview chip was affected.
Changes in solitaire_engine/src/theme/plugin.rs:
- PREVIEW_FACE_FILENAME / PREVIEW_BACK_FILENAME (with embedded
`.svg` suffix) replaced by PREVIEW_FACE_BASENAME /
PREVIEW_BACK_BASENAME ("spades_ace" / "back"). The function
appends the extension itself.
- read_theme_preview_svg_bytes -> read_theme_preview_bytes
returns ThemePreviewBytes::{Svg, Png}. For "default" the
embedded table stays SVG-only. For user themes the function
tries `<basename>.svg` first (matching the bundled
convention) and falls back to `<basename>.png` second.
- rasterize_preview_to_handle gains a Png branch that calls a
new decode_png_for_thumbnail helper (Bevy's
Image::from_buffer with ImageType::Format(ImageFormat::Png)).
PNGs decode at native dimensions; the picker chip's UI
layout scales them at draw time. SVGs continue to rasterise
at the fixed 100x140 thumbnail size as before.
- generate_thumbnail_pair_for is unchanged in shape; just
threads the new enum through.
Tests:
- read_default_theme_preview_returns_some_for_canonical_files
updated to match the new function signature and assert on
the Svg variant explicitly.
- New png_only_user_theme_generates_real_thumbnails creates a
temp theme dir, writes a 2x3 PNG (encoded at runtime via the
`image` dev-dep so the bytes are guaranteed valid), and
asserts both ace + back yield non-default Handle<Image>.
Cleans up the temp dir afterward.
solitaire_engine/Cargo.toml: image = "0.25" added as a
dev-dependency for the test's runtime PNG encoding. Already a
transitive Bevy dep so the build graph is unchanged.
Workspace: 1171 passing tests / 0 failing, was 1170 (+1 new).
cargo clippy --workspace --all-targets -- -D warnings clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Quat: replay sharing as the next punch-list item.
End-to-end:
1. Player wins a game on a server-backed sync backend.
2. `sync_plugin::push_replay_on_win` spawns the upload task on
`AsyncComputeTaskPool` and stores the handle in the new
`PendingReplayUpload` resource. The previous in-flight task (if
any) is dropped — the most recent win is the one whose share link
the player will care about.
3. `poll_replay_upload_result` harvests the task on the main thread
each frame; on success writes `<server>/replays/<id>` to
`LastSharedReplayUrl`. `UnsupportedPlatform` (LocalOnlyProvider)
is silently absorbed; real network/auth errors warn-log.
4. The Stats overlay's action bar gains a "Copy share link" button.
Click writes `LastSharedReplayUrl` to the OS clipboard via
`arboard` and surfaces a "Copied: <url>" toast.
Trait change: `SyncProvider::push_replay` now returns `Result<String,
SyncError>` (the share URL) instead of `Result<(), SyncError>`. The
default (`UnsupportedPlatform`) is unchanged for non-server backends;
`SolitaireServerClient` parses the response body's `id` field and
composes `<base_url>/replays/<id>`. Both call paths (initial + 401
retry) go through the new `share_url_from_response` helper so the
parse logic isn't duplicated.
New deps:
- `arboard` (~10 KB, cross-platform clipboard) added to workspace +
`solitaire_engine`. `default-features = false` keeps the X11/Wayland
binary-feature deps off the dependency graph; arboard handles the
fallback. Approved per the ASK BEFORE rule.
Persistence: the URL is in-memory only — the player must share within
the session of the win. A future revision can persist it alongside
the replay history file if cross-session sharing is needed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes Quat investigation #2. The project uses kira for audio
(cpal 0.17 + alsa 0.10), but Bevy's default feature set still pulled
bevy_audio → rodio → cpal 0.15 + alsa 0.9 + symphonia codecs — about
50 transitive crates the binary never executes.
Workspace Cargo.toml's bevy entry now declares default-features =
false plus an explicit allow-list of the features actually used
(default_app subset + default_platform desktop subset + common_api +
2D + UI rendering). The list is derived analytically from the leaves
of Bevy 0.18's 2d and ui meta-features; built cleanly on the first
try with no missing-symbol errors.
Features intentionally omitted vs Bevy default:
- bevy_audio (kira handles audio directly)
- bevy_animation (custom CardAnimation, not Bevy's)
- bevy_gilrs, bevy_gizmos, bevy_picking variants, bevy_post_process,
scene, hdr, sysinfo_plugin (none used)
- webgl2, web, android-* (desktop-only; solitaire_wasm is Bevy-free
and uses wasm-bindgen + solitaire_core directly)
- wayland (X11 chosen; Wayland can be added later if requested)
Dependency-tree size for solitaire_app drops from 628 unique crates
to 577 (-51). Verified gone: bevy_audio, rodio, cpal 0.15. The
remaining cpal 0.17 and symphonia 0.5 are pulled by kira, not Bevy.
solitaire_wasm needed no changes — it doesn't depend on bevy.
All 1134 tests pass; clippy clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires the WASM module from the previous commit into a minimal web
viewer served at <server>/replays/<id>. Two new server routes:
- `GET /replays/:id` — returns the same embedded HTML page for any
id; the page itself reads the path from window.location in JS and
fetches the replay JSON via /api/replays/:id.
- `/web/*` — ServeDir for the static assets (replay.css, replay.js,
and the wasm-bindgen-generated pkg/).
Web layer:
- index.html — header, board, controls, status. Module script.
- replay.css — midnight-purple palette matching the desktop client,
dark felt board, CSS-grid pile layout, tableau fan via per-card
inline `top` offset.
- replay.js — fetches the replay, instantiates the wasm
ReplayPlayer, drives state(), step(). Controls: Restart, Play/Pause
toggle, Step. Auto-tick at 600 ms.
- pkg/ — generated by wasm-bindgen (committed so deployers don't
have to install wasm-bindgen-cli + the wasm32 target).
`tower-http = "0.6"` added to solitaire_server with the `fs` feature
for ServeDir.
To regenerate pkg/ after a solitaire_wasm change:
RUSTFLAGS='--cfg getrandom_backend="wasm_js"' \
cargo build -p solitaire_wasm \
--target wasm32-unknown-unknown --release
wasm-bindgen --target web \
--out-dir solitaire_server/web/pkg --no-typescript \
target/wasm32-unknown-unknown/release/solitaire_wasm.wasm
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A new `cdylib + rlib` workspace member that wraps `solitaire_core::
GameState` for use from JavaScript. The web replay viewer fetches a
replay JSON, hands it to `ReplayPlayer::new`, and steps through
moves one at a time — same Rust rules engine the desktop client
uses, so the two implementations cannot drift.
The crate intentionally does NOT depend on `solitaire_data` (which
pulls dirs/keyring/reqwest, none wasm-friendly). Instead it defines
a minimal `Replay` mirror with the same serde shape; the JSON wire
format is the contract.
Public surface (#[wasm_bindgen]):
- `ReplayPlayer::new(json)` — parse + rebuild deal from seed/mode
- `state()` / `step()` — return JS-friendly StateSnapshot
- `total_steps()` / `step_idx()` / `is_finished()` — progress helpers
Native-callable mirror (`from_json`, `step_native`) lets unit tests
exercise the state machine without going through `serde_wasm_bindgen`,
which panics off-target. 3 tests cover construction, step advance,
and invalid-JSON handling.
`getrandom` needs the `wasm_js` feature on the wasm32 target;
configured via the cfg target dep table so non-wasm builds aren't
affected.
Build pipeline (executed from the repo root):
rustup target add wasm32-unknown-unknown
RUSTFLAGS='--cfg getrandom_backend="wasm_js"' \
cargo build -p solitaire_wasm \
--target wasm32-unknown-unknown --release
wasm-bindgen --target web \
--out-dir solitaire_server/web/pkg --no-typescript \
target/wasm32-unknown-unknown/release/solitaire_wasm.wasm
The generated bindings land in solitaire_server/web/pkg/ and are
committed alongside the web UI (next commit).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drops dead deps and stale doc content carried over from the pre-MIT
art swap.
Cargo.toml manifests:
- solitaire_core no longer depends on chrono (no source references it
since the original sync-payload timestamps moved to solitaire_data).
- solitaire_sync no longer depends on serde_json (the sync types use
serde-derive with whatever serializer the caller picks; the old
json-specific helpers were removed earlier).
Cargo.lock pruned by `cargo build` to drop the now-untransitively-
referenced versions.
CREDITS.md redistribution clause: "LGPL and OFL notices" tightened to
"MIT (project + hayeah card art) and OFL (FiraMono)" since the LGPL
art is gone.
SESSION_HANDOFF.md:
- HEAD bumped to 924a1e2; test count to 960; 9 ignored.
- Punch list rewritten — the xCards-URL line is obsolete (we did the
swap), v0.1.0 tag exists locally, and player smoke-test is the
current top item.
- New "Card-theme system (CARD_PLAN.md, fully shipped)" section
summarises the seven-phase end-to-end flow so a future session has
the integration map without re-reading the plan.
- Optional list gains the SVG-vs-layout aspect-ratio note as a
cosmetic-only follow-up.
Removed the locked worktree at .claude/worktrees/agent-aa55a94d18c669d70
left behind by a prior Claude session.
cargo build / clippy --workspace --all-targets -- -D warnings / test
--workspace all green (960 passed, 0 failed, 9 ignored).
Implements Phase 7 of CARD_PLAN.md — the entry point that takes a
user-supplied theme zip archive, validates it end-to-end, and
atomically unpacks it into the per-platform user themes directory.
Public API:
import_theme(zip_path) -> Result<ThemeId, ImportError>
Resolves user_theme_dir() and unpacks into <user>/<id>/.
import_theme_into(zip_path, target_root) -> Result<ThemeId, ImportError>
Test-friendly variant that takes the destination explicitly so
unit tests never touch the global OnceLock override.
Safety guarantees enforced:
- 20 MB hard cap on archive size (read from the central directory
before any extraction).
- Zip-slip path traversal rejected via ZipFile::enclosed_name plus a
Component::Normal-only belt-and-braces check.
- Manifest parsed via ron::de and validated via the existing
ThemeManifest::validate (Phase 2) — surfaces named diagnostics for
missing-of-52, unknown keys, duplicate keys, and meta errors.
- Every referenced face + back rasterised through rasterize_svg as a
structural validity check before any bytes hit the destination.
- Atomic install: writes to <root>/.<id>.tmp/ then std::fs::rename
into place, with a recursive copy + remove fallback for cross-
device renames. Failed extraction wipes the staging dir; the user
themes root is never touched on error.
- Id collision with an existing theme dir rejected up front.
7 new tests covering the happy path plus six failure modes (missing
manifest, missing face, oversized archive, zip-slip, missing-file,
id collision). Tests build zips in tempfile::TempDir so they never
touch the real user themes directory.
Workspace deps: zip 8.6 (default-features off + deflate only),
tempfile 3.27 (dev only).
cargo check --workspace --all-targets / clippy --workspace
--all-targets -- -D warnings clean. cargo test could not be run in
this turn because cc disappeared from the sandbox; tests compile
under cargo check --tests and will run on a normal toolchain.
Implements Phase 2 of CARD_PLAN.md — the data types and `.theme.ron`
asset loader that build on Phase 1's SVG rasteriser.
solitaire_engine/src/theme/
mod.rs — CardKey { suit, rank } as the HashMap lookup key
(distinct from solitaire_core::Card which carries
per-deal id + face_up state); CardKey::all() yields
the 52 keys in suit-major / rank-ascending order;
manifest_name() and parse_manifest_name() round-trip
via the canonical "{suit}_{rank}" form.
ThemeMeta with structural validation (id non-empty,
no path separators, non-zero aspect components).
CardTheme #[derive(Asset, TypePath)] storing the
53 image handles + meta.
manifest.rs — ThemeManifest { meta, back, faces } with serde for
RON round-trip. validate() returns a strongly-typed
HashMap<CardKey, PathBuf>, surfacing precise errors
for unknown face keys, missing-of-52 entries, and
duplicate keys (RON silently keeps the last; brittle
for a release).
loader.rs — AssetLoader for .theme.ron. Validates manifest, then
composes sibling SVG paths via AssetPath::resolve so
the same loader works for both embedded:// and
themes:// asset sources (Phase 3 territory).
Schedules every face + back load through SvgLoader
with target_size derived from meta.card_aspect.
24 new tests covering: 52-key enumeration uniqueness, manifest-name
round trip, garbage-name rejection, complete/missing/unknown/duplicate
manifest validation, RON round-trip integrity, target-size aspect
math (2:3 → 512x768; non-standard; degenerate 1:10000 clamps to 1px).
Workspace deps added: ron 0.12.
cargo build / clippy --workspace --all-targets -- -D warnings / test
all green (937 passed total — +24 from Phase 2 vs the +7 from
Phase 1's b8fb3fb baseline).
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).
Server-side endpoint tests already exist in solitaire_server. This
adds the client-side counterpart: five integration tests in
solitaire_data/tests/sync_round_trip.rs that drive
SolitaireServerClient against an in-process axum::serve harness with
an in-memory SQLite database, covering:
- register_login_push_pull_round_trip — happy path: register, push
non-default stats, pull from a fresh client, assert the merged
payload reflects the pushed values
- pull_after_concurrent_pushes_merges_correctly — two clients on one
user push different games_played values, verify the server-side
merge returns the max
- unauthenticated_pull_returns_authentication_error — pull without
tokens surfaces SyncError::Auth as expected
- jwt_refresh_on_401_succeeds — replace the access token with one
whose exp is two hours stale (same signing key), pull triggers
401 → /api/auth/refresh → retry, asserts the call ultimately
succeeds
- pull_after_account_deletion_returns_default_or_error — register,
push, delete via the trait, confirm the next push surfaces a
result rather than panicking
keyring_core's mock store is installed once per process via Once;
each test uses a unique username so the shared store doesn't
cross-contaminate. Production code in sync_client.rs needed no
changes — the Box<dyn SyncProvider> design plus the mock keyring
were sufficient to drive every flow from outside.
solitaire_server is added as a path dev-dependency along with the
direct crates the harness needs (axum, sqlx, jsonwebtoken, uuid,
chrono, solitaire_sync); no runtime deps changed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the single shared face.png placeholder with 52 individual card
face images (120×168 px each), generated by the updated gen_art tool:
- solitaire_assetgen: add ab_glyph dep; rewrite gen_art to render each
card with FiraMono rank characters, programmatic suit symbols (heart,
spade, diamond, club drawn via circles/triangles), and standard pip
layout for numbered cards (A–10) plus large face letter for J/Q/K.
- CardImageSet: replace single `face` handle with `faces: [[Handle; 13]; 4]`
indexed by [suit][rank].
- card_sprite(): select the per-card face image by suit/rank indices.
- spawn/update_card_entity: suppress Text2d overlay when PNG faces are
loaded (rank/suit baked into image); keep overlay in solid-colour
fallback for tests.
- gen_sfx.rs: rename `gen` variable to `make` (reserved keyword in 2024).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Art pass (Phase 4):
- Generate placeholder PNG assets: face.png, back_0–4.png, bg_0–4.png via
solitaire_assetgen gen_art binary (16×16 RGBA, embedded via include_bytes!)
- Add FiraMono-Medium font (assets/fonts/main.ttf) embedded at compile time
- Add FontPlugin: loads font at startup, exposes FontResource; gracefully
falls back to default handle when Assets<Font> absent (MinimalPlugins tests)
- Wire CardImageSet into card_plugin: face/back PNGs replace solid-colour
sprites when available; tests continue using colour fallback via MinimalPlugins
- Wire BackgroundImageSet into table_plugin: bg PNGs replace solid-colour
background; empty set inserted when Assets<Image> absent in tests
- Fix hint highlight system (input_plugin): tint sprite.color directly instead
of replacing the whole Sprite (which would discard the image handle)
- Export FontPlugin, FontResource, CardImageSet from solitaire_engine::lib
- Register FontPlugin in solitaire_app before other plugins
Dependency upgrades (latest releases):
- keyring "2" → keyring "4" + keyring-core "1" (v4 split architecture into
separate core library crate)
- auth_tokens.rs: Entry::new now returns Result; delete_password →
delete_credential; NoDefaultStore error variant handled
- solitaire_app: add keyring::use_native_store(true) at startup for Linux
Secret Service / macOS Keychain / Windows Credential Store selection
ARCHITECTURE.md: fix Edition 2025→2021, update asset pipeline section,
add FontPlugin/CardImageSet/BackgroundImageSet to plugin and resource tables,
update Section 14 to reflect actual include_bytes!() rendering approach,
add Decision Log entries for embedded PNG and font decisions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Deletes the solitaire_gpgs crate and all GPGS references from settings,
sync client, profile plugin, CLAUDE.md, and ARCHITECTURE.md. The
self-hosted server covers all sync needs without the Android-only backend.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Import paths simplified: manager/tween modules re-exported from kira root
(AudioManager, AudioManagerSettings, DefaultBackend, Tween all via kira::*)
- Volume::Amplitude removed; replaced with Value<Decibels> using a new
amplitude_to_decibels() helper (20*log10 conversion, clamps to SILENCE)
- output_destination field removed from StaticSoundSettings; sounds routed
to sub-tracks by calling TrackHandle::play() directly instead of
AudioManager::play()
- set_volume() now accepts f32 (Decibels) not f64
- start_ambient_loop signature updated to take &mut Option<TrackHandle>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- BorderRadius is no longer a Component; moved into Node.border_radius
field at all 15 spawn sites across 6 plugin files
- Events<T> renamed to Messages<T> in test code (12 files)
- KeyboardEvents SystemParam renamed to KeyboardMessages to match the
MessageWriter rename done in the 0.17 hop
- WindowResolution::from((f32,f32)) removed; use (u32,u32) tuple in main.rs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Event/EventReader/EventWriter renamed to Message/MessageReader/MessageWriter
- add_event → add_message for all 67 call sites
- ScrollPosition changed to tuple struct ScrollPosition(Vec2)
- CursorIcon import moved from bevy::winit::cursor to bevy::window
- WindowResolution::from((f32,f32)) removed — use (u32,u32) tuple
- World::send_event → World::write_message in test code
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Resolves 15 violations found by `cargo clippy --workspace --tests -D warnings`:
- Remove unused imports (Card, Rank) in cursor_plugin tests
- Replace absurd i32::MAX comparison with a meaningful >= 0 check
- Use range .contains() instead of manual >= && <= (manual_range_contains)
- Move impl FromRequestParts before test module in middleware.rs (items_after_test_module)
- Move _VEC3_REFERENCED const before test module in input_plugin.rs
- Convert runtime assert on constant to const { assert!(...) }
- Use .contains() instead of .iter().any() for slice membership
- Replace .get(...).is_none() with !.contains_key(...) in HashMap checks
- Collapse Default::default() + field assignment into struct literal initializers
across solitaire_sync, solitaire_data, and solitaire_engine test helpers
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- solitaire_server: Axum auth, sync push/pull, leaderboard, daily
challenge, account deletion, JWT middleware, rate limiting via
tower_governor, SQLite migrations, health endpoint
- solitaire_server: expose build_test_router (no rate limiting) so
integration tests work without a peer IP in oneshot requests
- solitaire_sync: SyncPayload, merge logic, shared API types
- solitaire_data: SyncProvider trait, LocalOnlyProvider,
SolitaireServerClient, auth_tokens keyring integration, blanket
Box<dyn SyncProvider> impl
- solitaire_data/settings: derive Default on SyncBackend (clippy fix)
- .sqlx/: offline query cache so server compiles without a live DB
- sqlx: removed non-existent "offline" feature flag
- keyring v2: fixed Entry::new() returning Result<Entry>
- sqlx 0.8: all SQLite TEXT columns wrapped in Option<T>
- Integration tests: max_connections(1) on in-memory pool so all
connections share the same schema
All 191 tests pass; cargo clippy -D warnings clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New solitaire_assetgen crate with gen_sfx binary: synthesizes
five 44.1kHz mono 16-bit PCM WAVs (flip/place/deal/invalid/fanfare)
from an LCG noise source + sine/square synths. Output committed
under assets/audio/.
- AudioPlugin (engine): embeds the WAVs via include_bytes!, decodes
once with kira::StaticSoundData, plays on Draw / Move / NewGame /
GameWon events. card_invalid is loaded but unused — wiring it
needs a MoveRejectedEvent.
- AudioManager kept on the main thread (NonSend) since cpal is !Send
on some platforms; degrades gracefully if no audio device present.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Introduces the plumbing layer for Phase 3: GameStateResource wraps
solitaire_core::GameState, DragState tracks in-progress drags, and
SyncStatusResource holds runtime sync status. GamePlugin routes
Draw/Move/Undo/NewGame request events into GameState and emits
StateChangedEvent and GameWonEvent for downstream systems.
Also adds the Phase 3 implementation plan under docs/superpowers/plans/.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>