diff --git a/solitaire_engine/assets/themes/rusty-pixel/back.png b/solitaire_engine/assets/themes/rusty-pixel/back.png new file mode 100644 index 0000000..ef19f35 Binary files /dev/null and b/solitaire_engine/assets/themes/rusty-pixel/back.png differ diff --git a/solitaire_engine/assets/themes/rusty-pixel/clubs_10.png b/solitaire_engine/assets/themes/rusty-pixel/clubs_10.png new file mode 100644 index 0000000..ec5e928 Binary files /dev/null and b/solitaire_engine/assets/themes/rusty-pixel/clubs_10.png differ diff --git a/solitaire_engine/assets/themes/rusty-pixel/clubs_2.png b/solitaire_engine/assets/themes/rusty-pixel/clubs_2.png new file mode 100644 index 0000000..b6eda9d Binary files /dev/null and b/solitaire_engine/assets/themes/rusty-pixel/clubs_2.png differ diff --git a/solitaire_engine/assets/themes/rusty-pixel/clubs_3.png b/solitaire_engine/assets/themes/rusty-pixel/clubs_3.png new file mode 100644 index 0000000..09a1df6 Binary files /dev/null and b/solitaire_engine/assets/themes/rusty-pixel/clubs_3.png differ diff --git a/solitaire_engine/assets/themes/rusty-pixel/clubs_4.png b/solitaire_engine/assets/themes/rusty-pixel/clubs_4.png new file mode 100644 index 0000000..f4be5c7 Binary files /dev/null and b/solitaire_engine/assets/themes/rusty-pixel/clubs_4.png differ diff --git a/solitaire_engine/assets/themes/rusty-pixel/clubs_5.png b/solitaire_engine/assets/themes/rusty-pixel/clubs_5.png new file mode 100644 index 0000000..3526423 Binary files /dev/null and b/solitaire_engine/assets/themes/rusty-pixel/clubs_5.png differ diff --git a/solitaire_engine/assets/themes/rusty-pixel/clubs_6.png b/solitaire_engine/assets/themes/rusty-pixel/clubs_6.png new file mode 100644 index 0000000..ced4916 Binary files /dev/null and b/solitaire_engine/assets/themes/rusty-pixel/clubs_6.png differ diff --git a/solitaire_engine/assets/themes/rusty-pixel/clubs_7.png b/solitaire_engine/assets/themes/rusty-pixel/clubs_7.png new file mode 100644 index 0000000..974518e Binary files /dev/null and b/solitaire_engine/assets/themes/rusty-pixel/clubs_7.png differ diff --git a/solitaire_engine/assets/themes/rusty-pixel/clubs_8.png b/solitaire_engine/assets/themes/rusty-pixel/clubs_8.png new file mode 100644 index 0000000..534e1b1 Binary files /dev/null and b/solitaire_engine/assets/themes/rusty-pixel/clubs_8.png differ diff --git a/solitaire_engine/assets/themes/rusty-pixel/clubs_9.png b/solitaire_engine/assets/themes/rusty-pixel/clubs_9.png new file mode 100644 index 0000000..ebd8e24 Binary files /dev/null and b/solitaire_engine/assets/themes/rusty-pixel/clubs_9.png differ diff --git a/solitaire_engine/assets/themes/rusty-pixel/clubs_ace.png b/solitaire_engine/assets/themes/rusty-pixel/clubs_ace.png new file mode 100644 index 0000000..f2c6da9 Binary files /dev/null and b/solitaire_engine/assets/themes/rusty-pixel/clubs_ace.png differ diff --git a/solitaire_engine/assets/themes/rusty-pixel/clubs_jack.png b/solitaire_engine/assets/themes/rusty-pixel/clubs_jack.png new file mode 100644 index 0000000..fe7ada4 Binary files /dev/null and b/solitaire_engine/assets/themes/rusty-pixel/clubs_jack.png differ diff --git a/solitaire_engine/assets/themes/rusty-pixel/clubs_king.png b/solitaire_engine/assets/themes/rusty-pixel/clubs_king.png new file mode 100644 index 0000000..71772a6 Binary files /dev/null and b/solitaire_engine/assets/themes/rusty-pixel/clubs_king.png differ diff --git a/solitaire_engine/assets/themes/rusty-pixel/clubs_queen.png b/solitaire_engine/assets/themes/rusty-pixel/clubs_queen.png new file mode 100644 index 0000000..1ecdc87 Binary files /dev/null and b/solitaire_engine/assets/themes/rusty-pixel/clubs_queen.png differ diff --git a/solitaire_engine/assets/themes/rusty-pixel/diamonds_10.png b/solitaire_engine/assets/themes/rusty-pixel/diamonds_10.png new file mode 100644 index 0000000..fe9dd6b Binary files /dev/null and b/solitaire_engine/assets/themes/rusty-pixel/diamonds_10.png differ diff --git a/solitaire_engine/assets/themes/rusty-pixel/diamonds_2.png b/solitaire_engine/assets/themes/rusty-pixel/diamonds_2.png new file mode 100644 index 0000000..5d80d50 Binary files /dev/null and b/solitaire_engine/assets/themes/rusty-pixel/diamonds_2.png differ diff --git a/solitaire_engine/assets/themes/rusty-pixel/diamonds_3.png b/solitaire_engine/assets/themes/rusty-pixel/diamonds_3.png new file mode 100644 index 0000000..0834e1a Binary files /dev/null and b/solitaire_engine/assets/themes/rusty-pixel/diamonds_3.png differ diff --git a/solitaire_engine/assets/themes/rusty-pixel/diamonds_4.png b/solitaire_engine/assets/themes/rusty-pixel/diamonds_4.png new file mode 100644 index 0000000..921095f Binary files /dev/null and b/solitaire_engine/assets/themes/rusty-pixel/diamonds_4.png differ diff --git a/solitaire_engine/assets/themes/rusty-pixel/diamonds_5.png b/solitaire_engine/assets/themes/rusty-pixel/diamonds_5.png new file mode 100644 index 0000000..d64d5e8 Binary files /dev/null and b/solitaire_engine/assets/themes/rusty-pixel/diamonds_5.png differ diff --git a/solitaire_engine/assets/themes/rusty-pixel/diamonds_6.png b/solitaire_engine/assets/themes/rusty-pixel/diamonds_6.png new file mode 100644 index 0000000..7939893 Binary files /dev/null and b/solitaire_engine/assets/themes/rusty-pixel/diamonds_6.png differ diff --git a/solitaire_engine/assets/themes/rusty-pixel/diamonds_7.png b/solitaire_engine/assets/themes/rusty-pixel/diamonds_7.png new file mode 100644 index 0000000..1c70eef Binary files /dev/null and b/solitaire_engine/assets/themes/rusty-pixel/diamonds_7.png differ diff --git a/solitaire_engine/assets/themes/rusty-pixel/diamonds_8.png b/solitaire_engine/assets/themes/rusty-pixel/diamonds_8.png new file mode 100644 index 0000000..3ad4103 Binary files /dev/null and b/solitaire_engine/assets/themes/rusty-pixel/diamonds_8.png differ diff --git a/solitaire_engine/assets/themes/rusty-pixel/diamonds_9.png b/solitaire_engine/assets/themes/rusty-pixel/diamonds_9.png new file mode 100644 index 0000000..740bb8a Binary files /dev/null and b/solitaire_engine/assets/themes/rusty-pixel/diamonds_9.png differ diff --git a/solitaire_engine/assets/themes/rusty-pixel/diamonds_ace.png b/solitaire_engine/assets/themes/rusty-pixel/diamonds_ace.png new file mode 100644 index 0000000..c68b50c Binary files /dev/null and b/solitaire_engine/assets/themes/rusty-pixel/diamonds_ace.png differ diff --git a/solitaire_engine/assets/themes/rusty-pixel/diamonds_jack.png b/solitaire_engine/assets/themes/rusty-pixel/diamonds_jack.png new file mode 100644 index 0000000..aa979b1 Binary files /dev/null and b/solitaire_engine/assets/themes/rusty-pixel/diamonds_jack.png differ diff --git a/solitaire_engine/assets/themes/rusty-pixel/diamonds_king.png b/solitaire_engine/assets/themes/rusty-pixel/diamonds_king.png new file mode 100644 index 0000000..bfbc96f Binary files /dev/null and b/solitaire_engine/assets/themes/rusty-pixel/diamonds_king.png differ diff --git a/solitaire_engine/assets/themes/rusty-pixel/diamonds_queen.png b/solitaire_engine/assets/themes/rusty-pixel/diamonds_queen.png new file mode 100644 index 0000000..7b5b710 Binary files /dev/null and b/solitaire_engine/assets/themes/rusty-pixel/diamonds_queen.png differ diff --git a/solitaire_engine/assets/themes/rusty-pixel/hearts_10.png b/solitaire_engine/assets/themes/rusty-pixel/hearts_10.png new file mode 100644 index 0000000..d6fcc4b Binary files /dev/null and b/solitaire_engine/assets/themes/rusty-pixel/hearts_10.png differ diff --git a/solitaire_engine/assets/themes/rusty-pixel/hearts_2.png b/solitaire_engine/assets/themes/rusty-pixel/hearts_2.png new file mode 100644 index 0000000..bdc2e0d Binary files /dev/null and b/solitaire_engine/assets/themes/rusty-pixel/hearts_2.png differ diff --git a/solitaire_engine/assets/themes/rusty-pixel/hearts_3.png b/solitaire_engine/assets/themes/rusty-pixel/hearts_3.png new file mode 100644 index 0000000..8b96ca7 Binary files /dev/null and b/solitaire_engine/assets/themes/rusty-pixel/hearts_3.png differ diff --git a/solitaire_engine/assets/themes/rusty-pixel/hearts_4.png b/solitaire_engine/assets/themes/rusty-pixel/hearts_4.png new file mode 100644 index 0000000..f016ef1 Binary files /dev/null and b/solitaire_engine/assets/themes/rusty-pixel/hearts_4.png differ diff --git a/solitaire_engine/assets/themes/rusty-pixel/hearts_5.png b/solitaire_engine/assets/themes/rusty-pixel/hearts_5.png new file mode 100644 index 0000000..dd2f719 Binary files /dev/null and b/solitaire_engine/assets/themes/rusty-pixel/hearts_5.png differ diff --git a/solitaire_engine/assets/themes/rusty-pixel/hearts_6.png b/solitaire_engine/assets/themes/rusty-pixel/hearts_6.png new file mode 100644 index 0000000..05059c9 Binary files /dev/null and b/solitaire_engine/assets/themes/rusty-pixel/hearts_6.png differ diff --git a/solitaire_engine/assets/themes/rusty-pixel/hearts_7.png b/solitaire_engine/assets/themes/rusty-pixel/hearts_7.png new file mode 100644 index 0000000..c838716 Binary files /dev/null and b/solitaire_engine/assets/themes/rusty-pixel/hearts_7.png differ diff --git a/solitaire_engine/assets/themes/rusty-pixel/hearts_8.png b/solitaire_engine/assets/themes/rusty-pixel/hearts_8.png new file mode 100644 index 0000000..d15f453 Binary files /dev/null and b/solitaire_engine/assets/themes/rusty-pixel/hearts_8.png differ diff --git a/solitaire_engine/assets/themes/rusty-pixel/hearts_9.png b/solitaire_engine/assets/themes/rusty-pixel/hearts_9.png new file mode 100644 index 0000000..d444ecb Binary files /dev/null and b/solitaire_engine/assets/themes/rusty-pixel/hearts_9.png differ diff --git a/solitaire_engine/assets/themes/rusty-pixel/hearts_ace.png b/solitaire_engine/assets/themes/rusty-pixel/hearts_ace.png new file mode 100644 index 0000000..813fe94 Binary files /dev/null and b/solitaire_engine/assets/themes/rusty-pixel/hearts_ace.png differ diff --git a/solitaire_engine/assets/themes/rusty-pixel/hearts_jack.png b/solitaire_engine/assets/themes/rusty-pixel/hearts_jack.png new file mode 100644 index 0000000..8814184 Binary files /dev/null and b/solitaire_engine/assets/themes/rusty-pixel/hearts_jack.png differ diff --git a/solitaire_engine/assets/themes/rusty-pixel/hearts_king.png b/solitaire_engine/assets/themes/rusty-pixel/hearts_king.png new file mode 100644 index 0000000..346bb3c Binary files /dev/null and b/solitaire_engine/assets/themes/rusty-pixel/hearts_king.png differ diff --git a/solitaire_engine/assets/themes/rusty-pixel/hearts_queen.png b/solitaire_engine/assets/themes/rusty-pixel/hearts_queen.png new file mode 100644 index 0000000..de59198 Binary files /dev/null and b/solitaire_engine/assets/themes/rusty-pixel/hearts_queen.png differ diff --git a/solitaire_engine/assets/themes/rusty-pixel/spades_10.png b/solitaire_engine/assets/themes/rusty-pixel/spades_10.png new file mode 100644 index 0000000..6feda4e Binary files /dev/null and b/solitaire_engine/assets/themes/rusty-pixel/spades_10.png differ diff --git a/solitaire_engine/assets/themes/rusty-pixel/spades_2.png b/solitaire_engine/assets/themes/rusty-pixel/spades_2.png new file mode 100644 index 0000000..051f847 Binary files /dev/null and b/solitaire_engine/assets/themes/rusty-pixel/spades_2.png differ diff --git a/solitaire_engine/assets/themes/rusty-pixel/spades_3.png b/solitaire_engine/assets/themes/rusty-pixel/spades_3.png new file mode 100644 index 0000000..fab5b2c Binary files /dev/null and b/solitaire_engine/assets/themes/rusty-pixel/spades_3.png differ diff --git a/solitaire_engine/assets/themes/rusty-pixel/spades_4.png b/solitaire_engine/assets/themes/rusty-pixel/spades_4.png new file mode 100644 index 0000000..9947718 Binary files /dev/null and b/solitaire_engine/assets/themes/rusty-pixel/spades_4.png differ diff --git a/solitaire_engine/assets/themes/rusty-pixel/spades_5.png b/solitaire_engine/assets/themes/rusty-pixel/spades_5.png new file mode 100644 index 0000000..a0734c9 Binary files /dev/null and b/solitaire_engine/assets/themes/rusty-pixel/spades_5.png differ diff --git a/solitaire_engine/assets/themes/rusty-pixel/spades_6.png b/solitaire_engine/assets/themes/rusty-pixel/spades_6.png new file mode 100644 index 0000000..7f5cb1e Binary files /dev/null and b/solitaire_engine/assets/themes/rusty-pixel/spades_6.png differ diff --git a/solitaire_engine/assets/themes/rusty-pixel/spades_7.png b/solitaire_engine/assets/themes/rusty-pixel/spades_7.png new file mode 100644 index 0000000..33dc34a Binary files /dev/null and b/solitaire_engine/assets/themes/rusty-pixel/spades_7.png differ diff --git a/solitaire_engine/assets/themes/rusty-pixel/spades_8.png b/solitaire_engine/assets/themes/rusty-pixel/spades_8.png new file mode 100644 index 0000000..5f2bdba Binary files /dev/null and b/solitaire_engine/assets/themes/rusty-pixel/spades_8.png differ diff --git a/solitaire_engine/assets/themes/rusty-pixel/spades_9.png b/solitaire_engine/assets/themes/rusty-pixel/spades_9.png new file mode 100644 index 0000000..8d136a2 Binary files /dev/null and b/solitaire_engine/assets/themes/rusty-pixel/spades_9.png differ diff --git a/solitaire_engine/assets/themes/rusty-pixel/spades_ace.png b/solitaire_engine/assets/themes/rusty-pixel/spades_ace.png new file mode 100644 index 0000000..2316169 Binary files /dev/null and b/solitaire_engine/assets/themes/rusty-pixel/spades_ace.png differ diff --git a/solitaire_engine/assets/themes/rusty-pixel/spades_jack.png b/solitaire_engine/assets/themes/rusty-pixel/spades_jack.png new file mode 100644 index 0000000..2b22d54 Binary files /dev/null and b/solitaire_engine/assets/themes/rusty-pixel/spades_jack.png differ diff --git a/solitaire_engine/assets/themes/rusty-pixel/spades_king.png b/solitaire_engine/assets/themes/rusty-pixel/spades_king.png new file mode 100644 index 0000000..2b8aa99 Binary files /dev/null and b/solitaire_engine/assets/themes/rusty-pixel/spades_king.png differ diff --git a/solitaire_engine/assets/themes/rusty-pixel/spades_queen.png b/solitaire_engine/assets/themes/rusty-pixel/spades_queen.png new file mode 100644 index 0000000..a102105 Binary files /dev/null and b/solitaire_engine/assets/themes/rusty-pixel/spades_queen.png differ diff --git a/solitaire_engine/assets/themes/rusty-pixel/theme.ron b/solitaire_engine/assets/themes/rusty-pixel/theme.ron new file mode 100644 index 0000000..39e6af8 --- /dev/null +++ b/solitaire_engine/assets/themes/rusty-pixel/theme.ron @@ -0,0 +1,76 @@ +// Rusty Pixel — pixel-art card theme generated via Claude Design. +// +// 53 PNGs at 256×384 (4× nearest-neighbor upscale of the source +// 64×96 grid). Card aspect 2:3 matches the engine's layout +// assumption. Drop this directory under +// `/solitaire_quest/themes/rusty-pixel/` and the theme +// registry picks it up at next launch. +( + meta: ( + id: "rusty-pixel", + name: "Rusty Pixel", + author: "Claude Design", + version: "0.1.0", + card_aspect: (2, 3), + // Opt in to nearest-neighbor sampling so the pixel grid stays + // crisp at non-integer scales (Bevy's default bilinear filter + // mushes 256x384 pixel art when displayed at ~150x200 on a + // typical desktop window). + pixel_art: true, + ), + back: "back.png", + faces: { + "clubs_ace": "clubs_ace.png", + "clubs_2": "clubs_2.png", + "clubs_3": "clubs_3.png", + "clubs_4": "clubs_4.png", + "clubs_5": "clubs_5.png", + "clubs_6": "clubs_6.png", + "clubs_7": "clubs_7.png", + "clubs_8": "clubs_8.png", + "clubs_9": "clubs_9.png", + "clubs_10": "clubs_10.png", + "clubs_jack": "clubs_jack.png", + "clubs_queen": "clubs_queen.png", + "clubs_king": "clubs_king.png", + "diamonds_ace": "diamonds_ace.png", + "diamonds_2": "diamonds_2.png", + "diamonds_3": "diamonds_3.png", + "diamonds_4": "diamonds_4.png", + "diamonds_5": "diamonds_5.png", + "diamonds_6": "diamonds_6.png", + "diamonds_7": "diamonds_7.png", + "diamonds_8": "diamonds_8.png", + "diamonds_9": "diamonds_9.png", + "diamonds_10": "diamonds_10.png", + "diamonds_jack": "diamonds_jack.png", + "diamonds_queen": "diamonds_queen.png", + "diamonds_king": "diamonds_king.png", + "hearts_ace": "hearts_ace.png", + "hearts_2": "hearts_2.png", + "hearts_3": "hearts_3.png", + "hearts_4": "hearts_4.png", + "hearts_5": "hearts_5.png", + "hearts_6": "hearts_6.png", + "hearts_7": "hearts_7.png", + "hearts_8": "hearts_8.png", + "hearts_9": "hearts_9.png", + "hearts_10": "hearts_10.png", + "hearts_jack": "hearts_jack.png", + "hearts_queen": "hearts_queen.png", + "hearts_king": "hearts_king.png", + "spades_ace": "spades_ace.png", + "spades_2": "spades_2.png", + "spades_3": "spades_3.png", + "spades_4": "spades_4.png", + "spades_5": "spades_5.png", + "spades_6": "spades_6.png", + "spades_7": "spades_7.png", + "spades_8": "spades_8.png", + "spades_9": "spades_9.png", + "spades_10": "spades_10.png", + "spades_jack": "spades_jack.png", + "spades_queen": "spades_queen.png", + "spades_king": "spades_king.png", + }, +) diff --git a/solitaire_engine/src/assets/mod.rs b/solitaire_engine/src/assets/mod.rs index e235edf..99bb74e 100644 --- a/solitaire_engine/src/assets/mod.rs +++ b/solitaire_engine/src/assets/mod.rs @@ -11,8 +11,10 @@ pub mod svg_loader; pub mod user_dir; pub use sources::{ - default_theme_svg_bytes, populate_embedded_default_theme, register_theme_asset_sources, - AssetSourcesPlugin, DEFAULT_THEME_MANIFEST_URL, USER_THEMES, + default_theme_svg_bytes, populate_embedded_default_theme, + populate_embedded_rusty_pixel_theme, register_theme_asset_sources, + rusty_pixel_theme_png_bytes, AssetSourcesPlugin, DEFAULT_THEME_MANIFEST_URL, + RUSTY_PIXEL_THEME_MANIFEST_URL, USER_THEMES, }; pub use svg_loader::{rasterize_svg, SvgLoader, SvgLoaderError, SvgLoaderSettings}; pub use user_dir::{set_user_theme_dir, user_theme_dir}; diff --git a/solitaire_engine/src/assets/sources.rs b/solitaire_engine/src/assets/sources.rs index ab6275a..c06d9ed 100644 --- a/solitaire_engine/src/assets/sources.rs +++ b/solitaire_engine/src/assets/sources.rs @@ -155,6 +155,100 @@ const DEFAULT_THEME_SVGS: &[(&str, &[u8])] = &[ embed_default_svg!("spades_king.svg"), ]; +/// Stable embedded asset URL of the bundled rusty-pixel theme manifest. +/// +/// `theme/plugin.rs::manifest_url_for` uses this when the player +/// selects "Rusty Pixel" so the manifest loads from the binary's +/// embedded asset registry rather than `themes://` (which would +/// require a user-supplied copy on disk). +pub const RUSTY_PIXEL_THEME_MANIFEST_URL: &str = + "embedded://solitaire_engine/assets/themes/rusty-pixel/theme.ron"; + +/// Path the embedded rusty-pixel theme manifest registers under, +/// relative to the `embedded://` source root. Kept in lockstep with +/// [`RUSTY_PIXEL_THEME_MANIFEST_URL`] by the unit test +/// `rusty_pixel_theme_url_constant_matches_embedded_path`. +const RUSTY_PIXEL_THEME_MANIFEST_PATH: &str = + "solitaire_engine/assets/themes/rusty-pixel/theme.ron"; + +/// Bytes of the bundled rusty-pixel theme manifest. Mirrors the +/// default-theme embed pattern — `include_bytes!` resolves at compile +/// time so the binary ships the manifest even on machines whose +/// `solitaire_engine/assets/` directory is absent at runtime. +const RUSTY_PIXEL_THEME_MANIFEST_BYTES: &[u8] = + include_bytes!("../../assets/themes/rusty-pixel/theme.ron"); + +/// Generates a `(stable_path, bytes)` entry for one rusty-pixel +/// theme PNG. Mirrors [`embed_default_svg!`] for the second bundled +/// theme — the path matches what `theme.ron` references. +macro_rules! embed_rusty_pixel_png { + ($name:literal) => { + ( + concat!("solitaire_engine/assets/themes/rusty-pixel/", $name), + include_bytes!(concat!("../../assets/themes/rusty-pixel/", $name)) as &[u8], + ) + }; +} + +/// Every rusty-pixel theme PNG bundled into the binary. 53 entries: +/// 52 face cards + 1 back. The macro pulls each PNG via +/// `include_bytes!` so adding a new file is a one-line append. +const RUSTY_PIXEL_THEME_PNGS: &[(&str, &[u8])] = &[ + embed_rusty_pixel_png!("back.png"), + embed_rusty_pixel_png!("clubs_ace.png"), + embed_rusty_pixel_png!("clubs_2.png"), + embed_rusty_pixel_png!("clubs_3.png"), + embed_rusty_pixel_png!("clubs_4.png"), + embed_rusty_pixel_png!("clubs_5.png"), + embed_rusty_pixel_png!("clubs_6.png"), + embed_rusty_pixel_png!("clubs_7.png"), + embed_rusty_pixel_png!("clubs_8.png"), + embed_rusty_pixel_png!("clubs_9.png"), + embed_rusty_pixel_png!("clubs_10.png"), + embed_rusty_pixel_png!("clubs_jack.png"), + embed_rusty_pixel_png!("clubs_queen.png"), + embed_rusty_pixel_png!("clubs_king.png"), + embed_rusty_pixel_png!("diamonds_ace.png"), + embed_rusty_pixel_png!("diamonds_2.png"), + embed_rusty_pixel_png!("diamonds_3.png"), + embed_rusty_pixel_png!("diamonds_4.png"), + embed_rusty_pixel_png!("diamonds_5.png"), + embed_rusty_pixel_png!("diamonds_6.png"), + embed_rusty_pixel_png!("diamonds_7.png"), + embed_rusty_pixel_png!("diamonds_8.png"), + embed_rusty_pixel_png!("diamonds_9.png"), + embed_rusty_pixel_png!("diamonds_10.png"), + embed_rusty_pixel_png!("diamonds_jack.png"), + embed_rusty_pixel_png!("diamonds_queen.png"), + embed_rusty_pixel_png!("diamonds_king.png"), + embed_rusty_pixel_png!("hearts_ace.png"), + embed_rusty_pixel_png!("hearts_2.png"), + embed_rusty_pixel_png!("hearts_3.png"), + embed_rusty_pixel_png!("hearts_4.png"), + embed_rusty_pixel_png!("hearts_5.png"), + embed_rusty_pixel_png!("hearts_6.png"), + embed_rusty_pixel_png!("hearts_7.png"), + embed_rusty_pixel_png!("hearts_8.png"), + embed_rusty_pixel_png!("hearts_9.png"), + embed_rusty_pixel_png!("hearts_10.png"), + embed_rusty_pixel_png!("hearts_jack.png"), + embed_rusty_pixel_png!("hearts_queen.png"), + embed_rusty_pixel_png!("hearts_king.png"), + embed_rusty_pixel_png!("spades_ace.png"), + embed_rusty_pixel_png!("spades_2.png"), + embed_rusty_pixel_png!("spades_3.png"), + embed_rusty_pixel_png!("spades_4.png"), + embed_rusty_pixel_png!("spades_5.png"), + embed_rusty_pixel_png!("spades_6.png"), + embed_rusty_pixel_png!("spades_7.png"), + embed_rusty_pixel_png!("spades_8.png"), + embed_rusty_pixel_png!("spades_9.png"), + embed_rusty_pixel_png!("spades_10.png"), + embed_rusty_pixel_png!("spades_jack.png"), + embed_rusty_pixel_png!("spades_queen.png"), + embed_rusty_pixel_png!("spades_king.png"), +]; + /// Registers asset sources that must be in place *before* /// `AssetPlugin` is built. /// @@ -191,6 +285,7 @@ pub struct AssetSourcesPlugin; impl Plugin for AssetSourcesPlugin { fn build(&self, app: &mut App) { populate_embedded_default_theme(app); + populate_embedded_rusty_pixel_theme(app); } } @@ -254,6 +349,43 @@ pub fn populate_embedded_default_theme(app: &mut App) { } } +/// Returns the embedded PNG bytes for a single rusty-pixel theme file +/// (e.g. `"back.png"` or `"spades_ace.png"`), or `None` when the +/// filename is not bundled. Mirrors [`default_theme_svg_bytes`] for +/// the second bundled theme so the picker thumbnail cache can read +/// preview-sized art without going through the async asset graph. +pub fn rusty_pixel_theme_png_bytes(filename: &str) -> Option<&'static [u8]> { + let suffix = format!("/{filename}"); + RUSTY_PIXEL_THEME_PNGS + .iter() + .find(|(path, _)| path.ends_with(&suffix)) + .map(|(_, bytes)| *bytes) +} + +/// Pushes the bundled rusty-pixel theme manifest + every face/back +/// PNG into the [`EmbeddedAssetRegistry`]. Pairs with +/// [`populate_embedded_default_theme`] — both are called from +/// [`AssetSourcesPlugin::build`] after `AssetPlugin` has set up the +/// embedded source. +pub fn populate_embedded_rusty_pixel_theme(app: &mut App) { + let registry = app + .world_mut() + .get_resource_or_insert_with(EmbeddedAssetRegistry::default); + + registry.insert_asset( + std::path::PathBuf::from(RUSTY_PIXEL_THEME_MANIFEST_PATH), + std::path::Path::new(RUSTY_PIXEL_THEME_MANIFEST_PATH), + RUSTY_PIXEL_THEME_MANIFEST_BYTES, + ); + for (path, bytes) in RUSTY_PIXEL_THEME_PNGS { + registry.insert_asset( + std::path::PathBuf::from(*path), + std::path::Path::new(*path), + *bytes, + ); + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/solitaire_engine/src/theme/plugin.rs b/solitaire_engine/src/theme/plugin.rs index 3e75ab6..9292a4a 100644 --- a/solitaire_engine/src/theme/plugin.rs +++ b/solitaire_engine/src/theme/plugin.rs @@ -17,7 +17,8 @@ use bevy::prelude::*; use solitaire_core::card::{Rank, Suit}; use crate::assets::{ - default_theme_svg_bytes, rasterize_svg, user_theme_dir, DEFAULT_THEME_MANIFEST_URL, + default_theme_svg_bytes, rasterize_svg, rusty_pixel_theme_png_bytes, user_theme_dir, + DEFAULT_THEME_MANIFEST_URL, RUSTY_PIXEL_THEME_MANIFEST_URL, }; use crate::card_plugin::CardImageSet; use crate::events::StateChangedEvent; @@ -128,16 +129,32 @@ fn load_initial_theme( settings: Option>, mut commands: Commands, ) { - let url = match settings.as_deref() { - Some(s) if s.0.selected_theme_id != "default" => { - format!("themes://{}/theme.ron", s.0.selected_theme_id) - } - _ => DEFAULT_THEME_MANIFEST_URL.to_string(), - }; + let id = settings + .as_deref() + .map(|s| s.0.selected_theme_id.as_str()) + .unwrap_or("default"); + let url = manifest_url_for(id); let handle: Handle = asset_server.load(url); commands.insert_resource(ActiveTheme(handle)); } +/// Resolves a theme id to its manifest asset URL. +/// +/// Bundled built-ins (default, rusty-pixel) route to `embedded://` +/// so the binary's compile-time-baked manifest + face files load +/// without touching disk. Anything else routes to `themes://`, +/// which `register_theme_asset_sources` points at the user themes +/// directory. Callers (load_initial_theme, +/// react_to_settings_theme_change) consult this helper instead of +/// hard-coding the URL shape per id. +fn manifest_url_for(theme_id: &str) -> String { + match theme_id { + "default" => DEFAULT_THEME_MANIFEST_URL.to_string(), + "rusty-pixel" => RUSTY_PIXEL_THEME_MANIFEST_URL.to_string(), + _ => format!("themes://{theme_id}/theme.ron"), + } +} + /// Watches [`crate::settings_plugin::SettingsChangedEvent`] and /// triggers a fresh theme load whenever /// `Settings::selected_theme_id` changes. The settings panel's theme @@ -163,11 +180,7 @@ fn react_to_settings_theme_change( return; } - let url = if new_id == "default" { - DEFAULT_THEME_MANIFEST_URL.to_string() - } else { - format!("themes://{new_id}/theme.ron") - }; + let url = manifest_url_for(new_id); let handle: Handle = asset_server.load(url); commands.insert_resource(ActiveTheme(handle)); } @@ -362,11 +375,20 @@ enum ThemePreviewBytes { /// `.svg` then `.png`. Either branch returns /// `None` on I/O failure (file missing, permission denied, etc.). fn read_theme_preview_bytes(theme_id: &str, basename: &str) -> Option { + // Bundled built-ins consult their embed tables before any + // filesystem I/O so the thumbnail works on a fresh install where + // the user themes directory doesn't exist yet. if theme_id == "default" { let filename = format!("{basename}.svg"); return default_theme_svg_bytes(&filename) .map(|b| ThemePreviewBytes::Svg(b.to_vec())); } + if theme_id == "rusty-pixel" { + let filename = format!("{basename}.png"); + if let Some(bytes) = rusty_pixel_theme_png_bytes(&filename) { + return Some(ThemePreviewBytes::Png(bytes.to_vec())); + } + } let dir = user_theme_dir().join(theme_id); if let Ok(bytes) = std::fs::read(dir.join(format!("{basename}.svg"))) { return Some(ThemePreviewBytes::Svg(bytes)); diff --git a/solitaire_engine/src/theme/registry.rs b/solitaire_engine/src/theme/registry.rs index 6be6dad..e62bb28 100644 --- a/solitaire_engine/src/theme/registry.rs +++ b/solitaire_engine/src/theme/registry.rs @@ -25,7 +25,7 @@ use bevy::prelude::{App, Plugin, Resource, Startup}; use serde::Deserialize; use super::ThemeMeta; -use crate::assets::{user_theme_dir, DEFAULT_THEME_MANIFEST_URL}; +use crate::assets::{user_theme_dir, DEFAULT_THEME_MANIFEST_URL, RUSTY_PIXEL_THEME_MANIFEST_URL}; /// One entry in the [`ThemeRegistry`] — the data the picker UI needs /// to render a row and load the theme on selection. @@ -98,10 +98,24 @@ fn build_registry_on_startup(mut registry: bevy::ecs::system::ResMut ThemeRegistry { let mut entries = Vec::new(); entries.push(default_entry()); - entries.extend(discover_user_themes(user_dir)); + entries.push(rusty_pixel_entry()); + let bundled_ids: std::collections::HashSet = + entries.iter().map(|e| e.id.clone()).collect(); + let user = discover_user_themes(user_dir) + .into_iter() + .filter(|t| !bundled_ids.contains(&t.id)); + entries.extend(user); ThemeRegistry { entries } } @@ -123,6 +137,26 @@ fn default_entry() -> ThemeEntry { } } +/// The bundled rusty-pixel theme entry — pixel-art faces by Claude +/// Design, embedded under `embedded://solitaire_engine/assets/themes/rusty-pixel/`. +/// Inserted alongside the default so the picker offers both +/// out-of-the-box on a fresh install with no user themes directory. +fn rusty_pixel_entry() -> ThemeEntry { + ThemeEntry { + id: "rusty-pixel".to_string(), + display_name: "Rusty Pixel".to_string(), + manifest_url: RUSTY_PIXEL_THEME_MANIFEST_URL.to_string(), + meta: ThemeMeta { + id: "rusty-pixel".to_string(), + name: "Rusty Pixel".to_string(), + author: "Claude Design".to_string(), + version: "0.1.0".to_string(), + card_aspect: (2, 3), + pixel_art: true, + }, + } +} + /// Walks `user_dir`, treating every immediate subdirectory as a /// candidate theme. A subdirectory contributes one entry if and only /// if it contains a `theme.ron` whose `meta` block parses cleanly and @@ -240,20 +274,22 @@ mod tests { } #[test] - fn empty_user_dir_yields_only_the_default_entry() { + fn empty_user_dir_yields_only_the_bundled_built_ins() { let tmp = tempfile::tempdir().unwrap(); let registry = build_registry(tmp.path()); - assert_eq!(registry.len(), 1); + assert_eq!(registry.len(), 2, "default + rusty-pixel always present"); assert_eq!(registry.entries[0].id, "default"); + assert_eq!(registry.entries[1].id, "rusty-pixel"); } #[test] - fn nonexistent_user_dir_still_yields_default() { + fn nonexistent_user_dir_still_yields_bundled_built_ins() { let registry = build_registry(Path::new( "/definitely/not/a/real/path/should/not/panic", )); - assert_eq!(registry.len(), 1); + assert_eq!(registry.len(), 2); assert_eq!(registry.entries[0].id, "default"); + assert_eq!(registry.entries[1].id, "rusty-pixel"); } #[test] @@ -264,12 +300,40 @@ mod tests { write_manifest(&theme_dir, "midnight", "Midnight"); let registry = build_registry(tmp.path()); - assert_eq!(registry.len(), 2); + assert_eq!(registry.len(), 3, "default + rusty-pixel + midnight"); let entry = registry.find("midnight").expect("midnight registered"); assert_eq!(entry.display_name, "Midnight"); assert_eq!(entry.manifest_url, "themes://midnight/theme.ron"); } + #[test] + fn user_theme_id_collision_with_bundled_is_dropped() { + // A user-supplied directory whose `id` matches a bundled + // built-in (rusty-pixel) must not produce a duplicate + // registry entry. The bundled version wins because it's + // guaranteed complete; the user's overriding copy may be + // partial, stale, or otherwise broken. + let tmp = tempfile::tempdir().unwrap(); + let theme_dir = tmp.path().join("rusty-pixel"); + fs::create_dir_all(&theme_dir).unwrap(); + write_manifest(&theme_dir, "rusty-pixel", "User Override"); + + let registry = build_registry(tmp.path()); + assert_eq!( + registry.len(), 2, + "user override of bundled id must not appear as a duplicate", + ); + let entry = registry.find("rusty-pixel").expect("rusty-pixel registered"); + assert_eq!( + entry.display_name, "Rusty Pixel", + "bundled entry's display_name wins over the user override", + ); + assert_eq!( + entry.manifest_url, RUSTY_PIXEL_THEME_MANIFEST_URL, + "bundled embed:// URL wins over the user themes:// URL", + ); + } + #[test] fn full_manifest_also_works_via_meta_only_parser() { // The meta-only deserialiser must tolerate the full ThemeManifest @@ -310,8 +374,9 @@ mod tests { write_manifest(&theme_dir, "../etc/passwd", "Evil"); let registry = build_registry(tmp.path()); - assert_eq!(registry.len(), 1, "escape attempt must not register"); + assert_eq!(registry.len(), 2, "escape attempt must not register; built-ins remain"); assert_eq!(registry.entries[0].id, "default"); + assert_eq!(registry.entries[1].id, "rusty-pixel"); } #[test] @@ -322,7 +387,11 @@ mod tests { fs::write(lonely.join("readme.md"), "wrong filename").unwrap(); let registry = build_registry(tmp.path()); - assert_eq!(registry.len(), 1); + assert_eq!( + registry.len(), + 2, + "no user themes register; only the bundled built-ins remain", + ); } #[test] @@ -351,8 +420,9 @@ mod tests { refresh_registry(&mut registry, tmp.path()); - assert_eq!(registry.len(), 1); + assert_eq!(registry.len(), 2, "stale entry replaced; built-ins remain"); assert_eq!(registry.entries[0].id, "default"); + assert_eq!(registry.entries[1].id, "rusty-pixel"); assert!(registry.find("stale").is_none()); }