feat(engine): switch asset loading to AssetServer with xCards artwork
CI / Test & Lint (push) Failing after 19s
CI / Release Build (push) Has been skipped

Replace compile-time include_bytes!() embedding for card faces, backgrounds,
and font with runtime AssetServer::load() calls. Swap in 52 xCards @2x PNGs
(LGPL-3.0) as card face assets and xCards bicycle_blue as back_0.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
funman300
2026-04-29 20:06:02 +00:00
parent efec6f22d5
commit fbe984cf64
108 changed files with 42 additions and 144 deletions
+23 -98
View File
@@ -196,111 +196,36 @@ impl Plugin for CardPlugin {
}
}
/// Loads card face and back PNGs at startup and inserts [`CardImageSet`].
/// Loads card face and back PNGs at startup via [`AssetServer`] and inserts
/// [`CardImageSet`].
///
/// The PNGs are embedded at compile time via `include_bytes!()`. Missing
/// files are compile errors, not runtime panics. Under `MinimalPlugins`
/// (tests) this system is still registered but `Assets<Image>` is unavailable,
/// so it does nothing and the plugin falls back to solid-colour sprites.
fn load_card_images(images: Option<ResMut<Assets<Image>>>, mut commands: Commands) {
let Some(mut images) = images else {
/// Faces: `assets/cards/faces/{RANK}{SUIT}.png` (e.g. `AC.png`, `10H.png`)
/// Backs: `assets/cards/backs/back_{0..4}.png`
///
/// Under `MinimalPlugins` (tests) `AssetServer` is absent, so the system
/// returns without inserting `CardImageSet` and the plugin falls back to
/// solid-colour sprites.
fn load_card_images(asset_server: Option<Res<AssetServer>>, mut commands: Commands) {
let Some(asset_server) = asset_server else {
return;
};
use bevy::asset::RenderAssetUsages;
use bevy::image::{CompressedImageFormats, ImageSampler, ImageType};
let load = |bytes: &[u8]| {
Image::from_buffer(
bytes,
ImageType::Extension("png"),
CompressedImageFormats::NONE,
true,
ImageSampler::default(),
RenderAssetUsages::RENDER_WORLD,
)
.expect("valid card PNG")
};
// 52 face images: faces[suit][rank]
// Suit: Clubs=0, Diamonds=1, Hearts=2, Spades=3
// Rank: Ace=0 … King=12
const FACE_BYTES: [[&[u8]; 13]; 4] = [
// Clubs
[
include_bytes!("../../assets/cards/faces/a_c.png"),
include_bytes!("../../assets/cards/faces/2_c.png"),
include_bytes!("../../assets/cards/faces/3_c.png"),
include_bytes!("../../assets/cards/faces/4_c.png"),
include_bytes!("../../assets/cards/faces/5_c.png"),
include_bytes!("../../assets/cards/faces/6_c.png"),
include_bytes!("../../assets/cards/faces/7_c.png"),
include_bytes!("../../assets/cards/faces/8_c.png"),
include_bytes!("../../assets/cards/faces/9_c.png"),
include_bytes!("../../assets/cards/faces/10_c.png"),
include_bytes!("../../assets/cards/faces/j_c.png"),
include_bytes!("../../assets/cards/faces/q_c.png"),
include_bytes!("../../assets/cards/faces/k_c.png"),
],
// Diamonds
[
include_bytes!("../../assets/cards/faces/a_d.png"),
include_bytes!("../../assets/cards/faces/2_d.png"),
include_bytes!("../../assets/cards/faces/3_d.png"),
include_bytes!("../../assets/cards/faces/4_d.png"),
include_bytes!("../../assets/cards/faces/5_d.png"),
include_bytes!("../../assets/cards/faces/6_d.png"),
include_bytes!("../../assets/cards/faces/7_d.png"),
include_bytes!("../../assets/cards/faces/8_d.png"),
include_bytes!("../../assets/cards/faces/9_d.png"),
include_bytes!("../../assets/cards/faces/10_d.png"),
include_bytes!("../../assets/cards/faces/j_d.png"),
include_bytes!("../../assets/cards/faces/q_d.png"),
include_bytes!("../../assets/cards/faces/k_d.png"),
],
// Hearts
[
include_bytes!("../../assets/cards/faces/a_h.png"),
include_bytes!("../../assets/cards/faces/2_h.png"),
include_bytes!("../../assets/cards/faces/3_h.png"),
include_bytes!("../../assets/cards/faces/4_h.png"),
include_bytes!("../../assets/cards/faces/5_h.png"),
include_bytes!("../../assets/cards/faces/6_h.png"),
include_bytes!("../../assets/cards/faces/7_h.png"),
include_bytes!("../../assets/cards/faces/8_h.png"),
include_bytes!("../../assets/cards/faces/9_h.png"),
include_bytes!("../../assets/cards/faces/10_h.png"),
include_bytes!("../../assets/cards/faces/j_h.png"),
include_bytes!("../../assets/cards/faces/q_h.png"),
include_bytes!("../../assets/cards/faces/k_h.png"),
],
// Spades
[
include_bytes!("../../assets/cards/faces/a_s.png"),
include_bytes!("../../assets/cards/faces/2_s.png"),
include_bytes!("../../assets/cards/faces/3_s.png"),
include_bytes!("../../assets/cards/faces/4_s.png"),
include_bytes!("../../assets/cards/faces/5_s.png"),
include_bytes!("../../assets/cards/faces/6_s.png"),
include_bytes!("../../assets/cards/faces/7_s.png"),
include_bytes!("../../assets/cards/faces/8_s.png"),
include_bytes!("../../assets/cards/faces/9_s.png"),
include_bytes!("../../assets/cards/faces/10_s.png"),
include_bytes!("../../assets/cards/faces/j_s.png"),
include_bytes!("../../assets/cards/faces/q_s.png"),
include_bytes!("../../assets/cards/faces/k_s.png"),
],
];
// Suit index: Clubs=0, Diamonds=1, Hearts=2, Spades=3
const SUIT_CHARS: [&str; 4] = ["C", "D", "H", "S"];
// Rank index: Ace=0 … King=12
const RANK_STRS: [&str; 13] = ["A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"];
let faces: [[Handle<Image>; 13]; 4] = std::array::from_fn(|suit| {
std::array::from_fn(|rank| images.add(load(FACE_BYTES[suit][rank])))
std::array::from_fn(|rank| {
asset_server.load(format!(
"cards/faces/{}{}.png",
RANK_STRS[rank], SUIT_CHARS[suit]
))
})
});
let backs = std::array::from_fn(|i| {
asset_server.load(format!("cards/backs/back_{i}.png"))
});
let backs = [
images.add(load(include_bytes!("../../assets/cards/backs/back_0.png"))),
images.add(load(include_bytes!("../../assets/cards/backs/back_1.png"))),
images.add(load(include_bytes!("../../assets/cards/backs/back_2.png"))),
images.add(load(include_bytes!("../../assets/cards/backs/back_3.png"))),
images.add(load(include_bytes!("../../assets/cards/backs/back_4.png"))),
];
commands.insert_resource(CardImageSet { faces, backs });
}
+5 -14
View File
@@ -1,6 +1,6 @@
// Register FontPlugin in solitaire_engine/src/lib.rs before use.
//! Embeds FiraMono-Medium as the project font and exposes it via [`FontResource`].
//! Loads FiraMono-Medium via the Bevy `AssetServer` and exposes it via [`FontResource`].
use bevy::prelude::*;
@@ -17,20 +17,11 @@ impl Plugin for FontPlugin {
}
}
fn load_font(fonts: Option<ResMut<Assets<Font>>>, mut commands: Commands) {
let Some(mut fonts) = fonts else {
// Assets<Font> absent (e.g. MinimalPlugins in tests) — insert default.
fn load_font(asset_server: Option<Res<AssetServer>>, mut commands: Commands) {
let Some(asset_server) = asset_server else {
// AssetServer absent (e.g. MinimalPlugins in tests) — insert default.
commands.insert_resource(FontResource(Handle::default()));
return;
};
let bytes: &'static [u8] = include_bytes!("../../assets/fonts/main.ttf");
match Font::try_from_bytes(bytes.to_vec()) {
Ok(font) => {
commands.insert_resource(FontResource(fonts.add(font)));
}
Err(e) => {
warn!("failed to load main.ttf: {e}; falling back to Bevy default font");
commands.insert_resource(FontResource(Handle::default()));
}
}
commands.insert_resource(FontResource(asset_server.load("fonts/main.ttf")));
}
+14 -32
View File
@@ -9,7 +9,7 @@ use bevy::window::WindowResized;
use solitaire_core::card::Suit;
use solitaire_core::pile::PileType;
use crate::events::HintVisualEvent;
use crate::events::{HintVisualEvent, StateChangedEvent};
use crate::layout::{compute_layout, Layout, LayoutResource};
#[cfg(test)]
use crate::layout::TABLE_COLOUR;
@@ -63,6 +63,7 @@ impl Plugin for TablePlugin {
app.add_message::<WindowResized>()
.add_message::<SettingsChangedEvent>()
.add_message::<HintVisualEvent>()
.add_message::<StateChangedEvent>()
.add_systems(Startup, load_background_images.before(setup_table))
.add_systems(Startup, setup_table)
.add_systems(
@@ -77,41 +78,17 @@ impl Plugin for TablePlugin {
}
}
/// Loads the 5 background PNG files at startup and stores their
/// [`Handle<Image>`]s in [`BackgroundImageSet`].
///
/// The PNGs are embedded at compile time via `include_bytes!()`. If a file
/// is missing the build will fail with a clear error rather than a runtime
/// panic.
fn load_background_images(images: Option<ResMut<Assets<Image>>>, mut commands: Commands) {
let Some(mut images) = images else {
// Assets<Image> is absent (e.g. MinimalPlugins in tests) — insert an
/// Loads the 5 background PNG files at startup via the Bevy `AssetServer` and
/// stores their [`Handle<Image>`]s in [`BackgroundImageSet`].
fn load_background_images(asset_server: Option<Res<AssetServer>>, mut commands: Commands) {
let Some(asset_server) = asset_server else {
// AssetServer absent (e.g. MinimalPlugins in tests) — insert an
// empty set so setup_table can proceed using a default handle.
commands.insert_resource(BackgroundImageSet { handles: Vec::new() });
return;
};
const BG_BYTES: [&[u8]; 5] = [
include_bytes!("../../assets/backgrounds/bg_0.png"),
include_bytes!("../../assets/backgrounds/bg_1.png"),
include_bytes!("../../assets/backgrounds/bg_2.png"),
include_bytes!("../../assets/backgrounds/bg_3.png"),
include_bytes!("../../assets/backgrounds/bg_4.png"),
];
let handles = BG_BYTES
.iter()
.map(|bytes| {
use bevy::image::{CompressedImageFormats, ImageSampler, ImageType};
let image = Image::from_buffer(
bytes,
ImageType::Extension("png"),
CompressedImageFormats::NONE,
true,
ImageSampler::default(),
bevy::asset::RenderAssetUsages::RENDER_WORLD,
)
.expect("valid background PNG");
images.add(image)
})
let handles = (0..5)
.map(|i| asset_server.load(format!("backgrounds/bg_{i}.png")))
.collect();
commands.insert_resource(BackgroundImageSet { handles });
}
@@ -298,10 +275,12 @@ fn spawn_pile_markers(commands: &mut Commands, layout: &Layout) {
}
}
#[allow(clippy::too_many_arguments)]
#[allow(clippy::type_complexity)]
fn on_window_resized(
mut events: MessageReader<WindowResized>,
mut layout_res: Option<ResMut<LayoutResource>>,
mut state_changed: MessageWriter<StateChangedEvent>,
mut backgrounds: Query<
(&mut Sprite, &mut Transform),
(With<TableBackground>, Without<PileMarker>),
@@ -331,6 +310,9 @@ fn on_window_resized(
transform.translation.y = pos.y;
}
}
// Reposition card sprites to the new layout.
state_changed.write(StateChangedEvent);
}
// ---------------------------------------------------------------------------