feat(engine): switch asset loading to AssetServer with xCards artwork
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:
@@ -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 });
|
||||
}
|
||||
|
||||
|
||||
@@ -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")));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user