feat(engine,assetgen): generate 52 individual card face PNGs
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>
@@ -7514,6 +7514,7 @@ dependencies = [
|
|||||||
name = "solitaire_assetgen"
|
name = "solitaire_assetgen"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"ab_glyph",
|
||||||
"png 0.17.16",
|
"png 0.17.16",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 213 B |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
@@ -10,7 +10,8 @@ publish = false
|
|||||||
# Not depended on by any other workspace crate.
|
# Not depended on by any other workspace crate.
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
png = "0.17"
|
png = "0.17"
|
||||||
|
ab_glyph = "0.2"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "gen_sfx"
|
name = "gen_sfx"
|
||||||
|
|||||||
@@ -1,178 +1,418 @@
|
|||||||
//! Generates placeholder PNG assets for card faces, card backs, and table
|
//! Generates PNG assets for Solitaire Quest.
|
||||||
//! backgrounds. All images are 16×16 pixels — Bevy's Sprite scales them via
|
|
||||||
//! `custom_size`, so small files keep the repository lightweight.
|
|
||||||
//!
|
//!
|
||||||
//! Run with:
|
//! Produces:
|
||||||
//! ```
|
//! - 52 card face PNGs (120×168) — one per card, with rank, suit symbol, and
|
||||||
//! cargo run -p solitaire_assetgen --bin gen_art
|
//! pip or face-letter layout baked in.
|
||||||
//! ```
|
//! - 5 card back PNGs (16×16 placeholder patterns).
|
||||||
|
//! - 5 background PNGs (16×16 placeholder patterns).
|
||||||
|
//!
|
||||||
|
//! Run with: `cargo run -p solitaire_assetgen --bin gen_art`
|
||||||
|
|
||||||
|
use ab_glyph::{Font, FontRef, PxScale, ScaleFont};
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::BufWriter;
|
use std::io::BufWriter;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// PNG helper
|
// Card dimensions and palette
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
/// Write a 16×16 RGBA image to `path`. `pixels` is a flat `[R,G,B,A, ...]`
|
const W: u32 = 120;
|
||||||
/// byte array with exactly 16 * 16 * 4 = 1024 bytes.
|
const H: u32 = 168;
|
||||||
fn save_png(path: &Path, pixels: &[u8; 1024]) {
|
|
||||||
|
const BG: [u8; 4] = [0xFE, 0xFE, 0xF2, 0xFF];
|
||||||
|
const BORDER: [u8; 4] = [0x99, 0x99, 0x99, 0xFF];
|
||||||
|
const RED: [u8; 4] = [0xCC, 0x11, 0x11, 0xFF];
|
||||||
|
const DARK: [u8; 4] = [0x11, 0x11, 0x11, 0xFF];
|
||||||
|
|
||||||
|
fn suit_color(suit: u8) -> [u8; 4] {
|
||||||
|
if suit == 1 || suit == 2 { RED } else { DARK }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rank_str(rank: u8) -> &'static str {
|
||||||
|
["A","2","3","4","5","6","7","8","9","10","J","Q","K"][rank as usize]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Pixel canvas (120×168 RGBA)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
struct Canvas {
|
||||||
|
data: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Canvas {
|
||||||
|
fn new() -> Self {
|
||||||
|
let mut data = vec![0u8; (W * H * 4) as usize];
|
||||||
|
for i in 0..(W * H) as usize {
|
||||||
|
data[i * 4..i * 4 + 4].copy_from_slice(&BG);
|
||||||
|
}
|
||||||
|
Self { data }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set(&mut self, x: i32, y: i32, c: [u8; 4]) {
|
||||||
|
if x < 0 || y < 0 || x >= W as i32 || y >= H as i32 { return; }
|
||||||
|
let i = (y as u32 * W + x as u32) as usize * 4;
|
||||||
|
let a = c[3] as f32 / 255.0;
|
||||||
|
if a >= 0.99 {
|
||||||
|
self.data[i..i + 4].copy_from_slice(&c);
|
||||||
|
} else if a > 0.01 {
|
||||||
|
self.data[i] = (self.data[i] as f32 * (1.0 - a) + c[0] as f32 * a) as u8;
|
||||||
|
self.data[i + 1] = (self.data[i + 1] as f32 * (1.0 - a) + c[1] as f32 * a) as u8;
|
||||||
|
self.data[i + 2] = (self.data[i + 2] as f32 * (1.0 - a) + c[2] as f32 * a) as u8;
|
||||||
|
self.data[i + 3] = 255;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn circle(&mut self, cx: f32, cy: f32, r: f32, c: [u8; 4]) {
|
||||||
|
for y in (cy - r - 1.0) as i32..=(cy + r + 1.0) as i32 {
|
||||||
|
for x in (cx - r - 1.0) as i32..=(cx + r + 1.0) as i32 {
|
||||||
|
if (x as f32 - cx).powi(2) + (y as f32 - cy).powi(2) <= r * r {
|
||||||
|
self.set(x, y, c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fill_rect(&mut self, x: i32, y: i32, w: i32, h: i32, c: [u8; 4]) {
|
||||||
|
for ry in y..y + h {
|
||||||
|
for rx in x..x + w {
|
||||||
|
self.set(rx, ry, c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn triangle(&mut self, pts: [(f32, f32); 3], c: [u8; 4]) {
|
||||||
|
let min_x = pts.iter().map(|p| p.0).fold(f32::INFINITY, f32::min) as i32;
|
||||||
|
let max_x = pts.iter().map(|p| p.0).fold(f32::NEG_INFINITY, f32::max) as i32;
|
||||||
|
let min_y = pts.iter().map(|p| p.1).fold(f32::INFINITY, f32::min) as i32;
|
||||||
|
let max_y = pts.iter().map(|p| p.1).fold(f32::NEG_INFINITY, f32::max) as i32;
|
||||||
|
let (ax, ay) = pts[0];
|
||||||
|
let (bx, by) = pts[1];
|
||||||
|
let (ex, ey) = pts[2];
|
||||||
|
for y in min_y..=max_y {
|
||||||
|
for x in min_x..=max_x {
|
||||||
|
let px = x as f32 + 0.5;
|
||||||
|
let py = y as f32 + 0.5;
|
||||||
|
let d0 = (bx - ax) * (py - ay) - (by - ay) * (px - ax);
|
||||||
|
let d1 = (ex - bx) * (py - by) - (ey - by) * (px - bx);
|
||||||
|
let d2 = (ax - ex) * (py - ey) - (ay - ey) * (px - ex);
|
||||||
|
let neg = d0 < 0.0 || d1 < 0.0 || d2 < 0.0;
|
||||||
|
let pos = d0 > 0.0 || d1 > 0.0 || d2 > 0.0;
|
||||||
|
if !(neg && pos) {
|
||||||
|
self.set(x, y, c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn diamond(&mut self, cx: f32, cy: f32, rx: f32, ry: f32, c: [u8; 4]) {
|
||||||
|
for y in (cy - ry - 1.0) as i32..=(cy + ry + 1.0) as i32 {
|
||||||
|
for x in (cx - rx - 1.0) as i32..=(cx + rx + 1.0) as i32 {
|
||||||
|
let nx = (x as f32 - cx).abs() / rx;
|
||||||
|
let ny = (y as f32 - cy).abs() / ry;
|
||||||
|
if nx + ny <= 1.0 {
|
||||||
|
self.set(x, y, c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Suit symbol drawing
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
fn draw_suit(cv: &mut Canvas, cx: f32, cy: f32, sz: f32, suit: u8, c: [u8; 4]) {
|
||||||
|
match suit {
|
||||||
|
0 => draw_club(cv, cx, cy, sz, c),
|
||||||
|
1 => draw_diamond_sym(cv, cx, cy, sz, c),
|
||||||
|
2 => draw_heart(cv, cx, cy, sz, c),
|
||||||
|
_ => draw_spade(cv, cx, cy, sz, c),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_heart(cv: &mut Canvas, cx: f32, cy: f32, sz: f32, c: [u8; 4]) {
|
||||||
|
let r = sz * 0.33;
|
||||||
|
let oy = cy - sz * 0.04;
|
||||||
|
cv.circle(cx - sz * 0.22, oy, r, c);
|
||||||
|
cv.circle(cx + sz * 0.22, oy, r, c);
|
||||||
|
cv.triangle([
|
||||||
|
(cx - sz * 0.52, oy + r * 0.4),
|
||||||
|
(cx + sz * 0.52, oy + r * 0.4),
|
||||||
|
(cx, cy + sz * 0.52),
|
||||||
|
], c);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_spade(cv: &mut Canvas, cx: f32, cy: f32, sz: f32, c: [u8; 4]) {
|
||||||
|
cv.triangle([
|
||||||
|
(cx, cy - sz * 0.52),
|
||||||
|
(cx - sz * 0.52, cy + sz * 0.1),
|
||||||
|
(cx + sz * 0.52, cy + sz * 0.1),
|
||||||
|
], c);
|
||||||
|
cv.circle(cx - sz * 0.22, cy + sz * 0.06, sz * 0.3, c);
|
||||||
|
cv.circle(cx + sz * 0.22, cy + sz * 0.06, sz * 0.3, c);
|
||||||
|
// stem + base
|
||||||
|
cv.triangle([
|
||||||
|
(cx, cy + sz * 0.12),
|
||||||
|
(cx - sz * 0.13, cy + sz * 0.5),
|
||||||
|
(cx + sz * 0.13, cy + sz * 0.5),
|
||||||
|
], c);
|
||||||
|
cv.fill_rect(
|
||||||
|
(cx - sz * 0.26) as i32,
|
||||||
|
(cy + sz * 0.43) as i32,
|
||||||
|
(sz * 0.52) as i32,
|
||||||
|
(sz * 0.1) as i32,
|
||||||
|
c,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_diamond_sym(cv: &mut Canvas, cx: f32, cy: f32, sz: f32, c: [u8; 4]) {
|
||||||
|
cv.diamond(cx, cy, sz * 0.44, sz * 0.57, c);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_club(cv: &mut Canvas, cx: f32, cy: f32, sz: f32, c: [u8; 4]) {
|
||||||
|
let r = sz * 0.29;
|
||||||
|
cv.circle(cx, cy - sz * 0.22, r, c);
|
||||||
|
cv.circle(cx - sz * 0.28, cy + sz * 0.1, r, c);
|
||||||
|
cv.circle(cx + sz * 0.28, cy + sz * 0.1, r, c);
|
||||||
|
cv.fill_rect(
|
||||||
|
(cx - sz * 0.08) as i32,
|
||||||
|
(cy + sz * 0.22) as i32,
|
||||||
|
(sz * 0.16) as i32 + 1,
|
||||||
|
(sz * 0.27) as i32,
|
||||||
|
c,
|
||||||
|
);
|
||||||
|
cv.fill_rect(
|
||||||
|
(cx - sz * 0.26) as i32,
|
||||||
|
(cy + sz * 0.45) as i32,
|
||||||
|
(sz * 0.52) as i32,
|
||||||
|
(sz * 0.09) as i32,
|
||||||
|
c,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Text rendering via ab_glyph
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
fn draw_text(cv: &mut Canvas, font: &FontRef<'_>, text: &str, px: f32, left: f32, top: f32, c: [u8; 4]) {
|
||||||
|
let scale = PxScale::from(px);
|
||||||
|
let baseline = top + font.as_scaled(scale).ascent();
|
||||||
|
let mut x = left;
|
||||||
|
for ch in text.chars() {
|
||||||
|
let gid = font.glyph_id(ch);
|
||||||
|
let glyph = gid.with_scale_and_position(scale, ab_glyph::point(x, baseline));
|
||||||
|
let adv = font.as_scaled(scale).h_advance(gid);
|
||||||
|
if let Some(outlined) = font.outline_glyph(glyph) {
|
||||||
|
let bounds = outlined.px_bounds();
|
||||||
|
outlined.draw(|gx, gy, cov| {
|
||||||
|
if cov > 0.02 {
|
||||||
|
let alpha = (cov * c[3] as f32) as u8;
|
||||||
|
cv.set(
|
||||||
|
(bounds.min.x + gx as f32) as i32,
|
||||||
|
(bounds.min.y + gy as f32) as i32,
|
||||||
|
[c[0], c[1], c[2], alpha],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
x += adv;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn text_w(font: &FontRef<'_>, text: &str, px: f32) -> f32 {
|
||||||
|
let scale = PxScale::from(px);
|
||||||
|
let sf = font.as_scaled(scale);
|
||||||
|
text.chars().map(|c| sf.h_advance(font.glyph_id(c))).sum()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn text_h(font: &FontRef<'_>, px: f32) -> f32 {
|
||||||
|
let scale = PxScale::from(px);
|
||||||
|
let sf = font.as_scaled(scale);
|
||||||
|
sf.ascent() - sf.descent()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Pip layout (rank 0=Ace … 9=Ten; rank 10-12 are face cards)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
fn pip_positions(rank: u8) -> &'static [(f32, f32)] {
|
||||||
|
match rank {
|
||||||
|
0 => &[(0.5, 0.5)],
|
||||||
|
1 => &[(0.5, 0.2), (0.5, 0.8)],
|
||||||
|
2 => &[(0.5, 0.12), (0.5, 0.5), (0.5, 0.88)],
|
||||||
|
3 => &[(0.25, 0.18), (0.75, 0.18), (0.25, 0.82), (0.75, 0.82)],
|
||||||
|
4 => &[(0.25, 0.18), (0.75, 0.18), (0.5, 0.5), (0.25, 0.82), (0.75, 0.82)],
|
||||||
|
5 => &[(0.25, 0.12), (0.75, 0.12), (0.25, 0.5), (0.75, 0.5), (0.25, 0.88), (0.75, 0.88)],
|
||||||
|
6 => &[(0.25, 0.1), (0.75, 0.1), (0.5, 0.31), (0.25, 0.5), (0.75, 0.5), (0.25, 0.9), (0.75, 0.9)],
|
||||||
|
7 => &[(0.25, 0.1), (0.75, 0.1), (0.5, 0.28), (0.25, 0.48), (0.75, 0.48), (0.5, 0.70), (0.25, 0.9), (0.75, 0.9)],
|
||||||
|
8 => &[(0.25, 0.1), (0.75, 0.1), (0.25, 0.35), (0.75, 0.35), (0.5, 0.5), (0.25, 0.65), (0.75, 0.65), (0.25, 0.9), (0.75, 0.9)],
|
||||||
|
9 => &[(0.25, 0.09), (0.75, 0.09), (0.5, 0.27), (0.25, 0.44), (0.75, 0.44), (0.25, 0.56), (0.75, 0.56), (0.5, 0.73), (0.25, 0.91), (0.75, 0.91)],
|
||||||
|
_ => &[],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pip area within the card (avoids the corner labels).
|
||||||
|
const PIP_X: f32 = 22.0;
|
||||||
|
const PIP_Y: f32 = 46.0;
|
||||||
|
const PIP_W: f32 = 76.0;
|
||||||
|
const PIP_H: f32 = 80.0;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Card face generation
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
fn make_card_face(font: &FontRef<'_>, rank: u8, suit: u8) -> Canvas {
|
||||||
|
let mut cv = Canvas::new();
|
||||||
|
let sc = suit_color(suit);
|
||||||
|
|
||||||
|
// Border (2 px)
|
||||||
|
for x in 0..W as i32 {
|
||||||
|
cv.set(x, 0, BORDER);
|
||||||
|
cv.set(x, 1, BORDER);
|
||||||
|
cv.set(x, H as i32 - 2, BORDER);
|
||||||
|
cv.set(x, H as i32 - 1, BORDER);
|
||||||
|
}
|
||||||
|
for y in 0..H as i32 {
|
||||||
|
cv.set(0, y, BORDER);
|
||||||
|
cv.set(1, y, BORDER);
|
||||||
|
cv.set(W as i32 - 2, y, BORDER);
|
||||||
|
cv.set(W as i32 - 1, y, BORDER);
|
||||||
|
}
|
||||||
|
|
||||||
|
let rank_s = rank_str(rank);
|
||||||
|
let rank_px = 18.0f32;
|
||||||
|
let suit_sz = 11.0f32;
|
||||||
|
let rh = text_h(font, rank_px);
|
||||||
|
let rw = text_w(font, rank_s, rank_px);
|
||||||
|
let corner_h = rh + 2.0 + suit_sz * 1.5;
|
||||||
|
|
||||||
|
// Top-left corner
|
||||||
|
let tl_x = 6.0f32;
|
||||||
|
let tl_y = 5.0f32;
|
||||||
|
draw_text(&mut cv, font, rank_s, rank_px, tl_x, tl_y, sc);
|
||||||
|
draw_suit(&mut cv, tl_x + suit_sz * 0.62, tl_y + rh + 2.0 + suit_sz * 0.75, suit_sz, suit, sc);
|
||||||
|
|
||||||
|
// Bottom-right corner (right-aligned rank, suit above it)
|
||||||
|
let br_rx = W as f32 - 6.0;
|
||||||
|
let br_by = H as f32 - 5.0;
|
||||||
|
let br_ty = br_by - corner_h;
|
||||||
|
draw_text(&mut cv, font, rank_s, rank_px, br_rx - rw, br_ty, sc);
|
||||||
|
draw_suit(&mut cv, br_rx - suit_sz * 0.62, br_ty + rh + 2.0 + suit_sz * 0.75, suit_sz, suit, sc);
|
||||||
|
|
||||||
|
// Center content
|
||||||
|
if rank >= 10 {
|
||||||
|
// Face cards: large rank letter + suit symbol below
|
||||||
|
let big_px = 52.0f32;
|
||||||
|
let big_w = text_w(font, rank_s, big_px);
|
||||||
|
let big_h = text_h(font, big_px);
|
||||||
|
let big_x = (W as f32 - big_w) / 2.0;
|
||||||
|
let big_y = H as f32 * 0.28;
|
||||||
|
draw_text(&mut cv, font, rank_s, big_px, big_x, big_y, sc);
|
||||||
|
let sym_sz = 22.0f32;
|
||||||
|
draw_suit(&mut cv, W as f32 * 0.5, big_y + big_h + sym_sz * 1.0, sym_sz, suit, sc);
|
||||||
|
} else {
|
||||||
|
// Pip cards
|
||||||
|
let pip_sz = if rank == 0 {
|
||||||
|
24.0f32 // Ace: large single pip
|
||||||
|
} else if rank <= 5 {
|
||||||
|
14.0
|
||||||
|
} else {
|
||||||
|
12.0
|
||||||
|
};
|
||||||
|
for &(nx, ny) in pip_positions(rank) {
|
||||||
|
let cx = PIP_X + nx * PIP_W;
|
||||||
|
let cy = PIP_Y + ny * PIP_H;
|
||||||
|
draw_suit(&mut cv, cx, cy, pip_sz, suit, sc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cv
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// PNG encoding helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
fn save_card_png(path: &Path, cv: &Canvas) {
|
||||||
|
save_png_wh(path, &cv.data, W, H);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_png_wh(path: &Path, data: &[u8], w: u32, h: u32) {
|
||||||
let file = File::create(path)
|
let file = File::create(path)
|
||||||
.unwrap_or_else(|e| panic!("cannot create {}: {e}", path.display()));
|
.unwrap_or_else(|e| panic!("cannot create {}: {e}", path.display()));
|
||||||
let mut w = BufWriter::new(file);
|
let mut bw = BufWriter::new(file);
|
||||||
let mut encoder = png::Encoder::new(&mut w, 16, 16);
|
let mut enc = png::Encoder::new(&mut bw, w, h);
|
||||||
encoder.set_color(png::ColorType::Rgba);
|
enc.set_color(png::ColorType::Rgba);
|
||||||
encoder.set_depth(png::BitDepth::Eight);
|
enc.set_depth(png::BitDepth::Eight);
|
||||||
let mut writer = encoder
|
let mut writer = enc.write_header()
|
||||||
.write_header()
|
|
||||||
.unwrap_or_else(|e| panic!("png header error for {}: {e}", path.display()));
|
.unwrap_or_else(|e| panic!("png header error for {}: {e}", path.display()));
|
||||||
writer
|
writer.write_image_data(data)
|
||||||
.write_image_data(pixels)
|
|
||||||
.unwrap_or_else(|e| panic!("png data error for {}: {e}", path.display()));
|
.unwrap_or_else(|e| panic!("png data error for {}: {e}", path.display()));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build a flat 16×16 RGBA pixel array using a per-pixel closure.
|
fn save_small_png(path: &Path, pixels: &[u8; 1024]) {
|
||||||
fn make_image<F: Fn(u32, u32) -> [u8; 4]>(f: F) -> [u8; 1024] {
|
save_png_wh(path, pixels, 16, 16);
|
||||||
let mut pixels = [0u8; 1024];
|
}
|
||||||
|
|
||||||
|
fn make_small<F: Fn(u32, u32) -> [u8; 4]>(f: F) -> [u8; 1024] {
|
||||||
|
let mut out = [0u8; 1024];
|
||||||
for y in 0u32..16 {
|
for y in 0u32..16 {
|
||||||
for x in 0u32..16 {
|
for x in 0u32..16 {
|
||||||
let rgba = f(x, y);
|
let c = f(x, y);
|
||||||
let i = ((y * 16 + x) * 4) as usize;
|
let i = ((y * 16 + x) * 4) as usize;
|
||||||
pixels[i..i + 4].copy_from_slice(&rgba);
|
out[i..i + 4].copy_from_slice(&c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pixels
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Card face
|
// Card backs (16×16 placeholder patterns)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
/// Cream/ivory solid fill — represents a blank card face.
|
|
||||||
fn make_face() -> [u8; 1024] {
|
|
||||||
make_image(|_, _| [0xF8, 0xF8, 0xF0, 0xFF])
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Card backs (match the colours used in card_plugin.rs `card_back_colour()`)
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/// back_0 — blue base with semi-transparent white horizontal stripes every 4 px.
|
|
||||||
fn make_back_0() -> [u8; 1024] {
|
fn make_back_0() -> [u8; 1024] {
|
||||||
make_image(|_, y| {
|
make_small(|_, y| if y % 4 < 2 { [0xFF, 0xFF, 0xFF, 40] } else { [0x26, 0x4D, 0x8C, 0xFF] })
|
||||||
if y % 4 < 2 {
|
|
||||||
[0xFF, 0xFF, 0xFF, 40]
|
|
||||||
} else {
|
|
||||||
[0x26, 0x4D, 0x8C, 0xFF]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// back_1 — red base with semi-transparent white diagonal stripes.
|
|
||||||
fn make_back_1() -> [u8; 1024] {
|
fn make_back_1() -> [u8; 1024] {
|
||||||
make_image(|x, y| {
|
make_small(|x, y| if (x + y) % 4 < 2 { [0xFF, 0xFF, 0xFF, 40] } else { [0x8C, 0x1A, 0x1A, 0xFF] })
|
||||||
if (x + y) % 4 < 2 {
|
|
||||||
[0xFF, 0xFF, 0xFF, 40]
|
|
||||||
} else {
|
|
||||||
[0x8C, 0x1A, 0x1A, 0xFF]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// back_2 — green base with white dots at every 4-px grid intersection.
|
|
||||||
fn make_back_2() -> [u8; 1024] {
|
fn make_back_2() -> [u8; 1024] {
|
||||||
make_image(|x, y| {
|
make_small(|x, y| if x.is_multiple_of(4) && y.is_multiple_of(4) { [0xFF, 0xFF, 0xFF, 0xFF] } else { [0x0D, 0x66, 0x1A, 0xFF] })
|
||||||
if x % 4 == 0 && y % 4 == 0 {
|
|
||||||
[0xFF, 0xFF, 0xFF, 0xFF]
|
|
||||||
} else {
|
|
||||||
[0x0D, 0x66, 0x1A, 0xFF]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// back_3 — purple base with a white diamond centred at (8, 8).
|
|
||||||
fn make_back_3() -> [u8; 1024] {
|
fn make_back_3() -> [u8; 1024] {
|
||||||
make_image(|x, y| {
|
make_small(|x, y| {
|
||||||
let dx = (x as i32 - 8).unsigned_abs();
|
let dx = (x as i32 - 8).unsigned_abs();
|
||||||
let dy = (y as i32 - 8).unsigned_abs();
|
let dy = (y as i32 - 8).unsigned_abs();
|
||||||
if dx + dy <= 4 {
|
if dx + dy <= 4 { [0xFF, 0xFF, 0xFF, 0xFF] } else { [0x59, 0x14, 0x85, 0xFF] }
|
||||||
[0xFF, 0xFF, 0xFF, 0xFF]
|
|
||||||
} else {
|
|
||||||
[0x59, 0x14, 0x85, 0xFF]
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// back_4 — teal base with a 1-px white border.
|
|
||||||
fn make_back_4() -> [u8; 1024] {
|
fn make_back_4() -> [u8; 1024] {
|
||||||
make_image(|x, y| {
|
make_small(|x, y| if x == 0 || x == 15 || y == 0 || y == 15 { [0xFF, 0xFF, 0xFF, 0xFF] } else { [0x0D, 0x66, 0x6B, 0xFF] })
|
||||||
if x == 0 || x == 15 || y == 0 || y == 15 {
|
|
||||||
[0xFF, 0xFF, 0xFF, 0xFF]
|
|
||||||
} else {
|
|
||||||
[0x0D, 0x66, 0x6B, 0xFF]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Backgrounds
|
// Backgrounds (16×16 placeholder patterns)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
/// bg_0 — dark green felt with very faint lighter grid lines every 8 px.
|
|
||||||
fn make_bg_0() -> [u8; 1024] {
|
fn make_bg_0() -> [u8; 1024] {
|
||||||
make_image(|x, y| {
|
make_small(|x, y| if x.is_multiple_of(8) || y.is_multiple_of(8) { [0xFF, 0xFF, 0xFF, 30] } else { [0x1A, 0x4D, 0x1A, 0xFF] })
|
||||||
if x % 8 == 0 || y % 8 == 0 {
|
|
||||||
[0xFF, 0xFF, 0xFF, 30]
|
|
||||||
} else {
|
|
||||||
[0x1A, 0x4D, 0x1A, 0xFF]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// bg_1 — dark wood brown with faint horizontal grain lines every 2 px.
|
|
||||||
fn make_bg_1() -> [u8; 1024] {
|
fn make_bg_1() -> [u8; 1024] {
|
||||||
make_image(|_, y| {
|
make_small(|_, y| if y.is_multiple_of(2) { [0xFF, 0xFF, 0xFF, 20] } else { [0x40, 0x2D, 0x1A, 0xFF] })
|
||||||
if y % 2 == 0 {
|
|
||||||
[0xFF, 0xFF, 0xFF, 20]
|
|
||||||
} else {
|
|
||||||
[0x40, 0x2D, 0x1A, 0xFF]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// bg_2 — navy with faint star/dot pattern (offset rows) every 8 px.
|
|
||||||
fn make_bg_2() -> [u8; 1024] {
|
fn make_bg_2() -> [u8; 1024] {
|
||||||
make_image(|x, y| {
|
make_small(|x, y| {
|
||||||
let row_offset: u32 = if (y / 4) % 2 == 0 { 0 } else { 4 };
|
let off: u32 = if (y / 4).is_multiple_of(2) { 0 } else { 4 };
|
||||||
if (x + row_offset) % 8 == 0 && y % 8 == 0 {
|
if (x + off).is_multiple_of(8) && y.is_multiple_of(8) { [0xFF, 0xFF, 0xFF, 0xFF] } else { [0x0D, 0x14, 0x38, 0xFF] }
|
||||||
[0xFF, 0xFF, 0xFF, 0xFF]
|
|
||||||
} else {
|
|
||||||
[0x0D, 0x14, 0x38, 0xFF]
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// bg_3 — burgundy with a faint diamond-grid pattern.
|
|
||||||
fn make_bg_3() -> [u8; 1024] {
|
fn make_bg_3() -> [u8; 1024] {
|
||||||
make_image(|x, y| {
|
make_small(|x, y| if (x + y).is_multiple_of(8) { [0xFF, 0xFF, 0xFF, 30] } else { [0x4D, 0x0D, 0x14, 0xFF] })
|
||||||
if (x + y) % 8 == 0 {
|
|
||||||
[0xFF, 0xFF, 0xFF, 30]
|
|
||||||
} else {
|
|
||||||
[0x4D, 0x0D, 0x14, 0xFF]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// bg_4 — charcoal with faint pixel noise (alternating pixels every 3 columns).
|
|
||||||
fn make_bg_4() -> [u8; 1024] {
|
fn make_bg_4() -> [u8; 1024] {
|
||||||
make_image(|x, y| {
|
make_small(|x, y| if (x + y).is_multiple_of(2) && x.is_multiple_of(3) { [0xFF, 0xFF, 0xFF, 20] } else { [0x1F, 0x1F, 0x24, 0xFF] })
|
||||||
if (x + y) % 2 == 0 && x % 3 == 0 {
|
|
||||||
[0xFF, 0xFF, 0xFF, 20]
|
|
||||||
} else {
|
|
||||||
[0x1F, 0x1F, 0x24, 0xFF]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -186,44 +426,43 @@ fn workspace_root() -> std::path::PathBuf {
|
|||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let root = workspace_root();
|
let root = workspace_root();
|
||||||
|
|
||||||
// Ensure output directories exist.
|
|
||||||
std::fs::create_dir_all(root.join("assets/cards/faces")).unwrap();
|
std::fs::create_dir_all(root.join("assets/cards/faces")).unwrap();
|
||||||
std::fs::create_dir_all(root.join("assets/cards/backs")).unwrap();
|
std::fs::create_dir_all(root.join("assets/cards/backs")).unwrap();
|
||||||
std::fs::create_dir_all(root.join("assets/backgrounds")).unwrap();
|
std::fs::create_dir_all(root.join("assets/backgrounds")).unwrap();
|
||||||
|
|
||||||
// Card face.
|
// Load font from disk (dev tool — runtime load is fine here).
|
||||||
let path = root.join("assets/cards/faces/face.png");
|
let font_path = root.join("assets/fonts/main.ttf");
|
||||||
save_png(&path, &make_face());
|
let font_bytes = std::fs::read(&font_path)
|
||||||
println!("wrote {}", path.display());
|
.unwrap_or_else(|e| panic!("failed to read {}: {e}", font_path.display()));
|
||||||
|
let font = FontRef::try_from_slice(&font_bytes)
|
||||||
|
.expect("failed to parse assets/fonts/main.ttf");
|
||||||
|
|
||||||
// Card backs.
|
// 52 card faces
|
||||||
let backs = [
|
let suits = ["c", "d", "h", "s"];
|
||||||
make_back_0(),
|
let ranks = ["a","2","3","4","5","6","7","8","9","10","j","q","k"];
|
||||||
make_back_1(),
|
for suit in 0u8..4 {
|
||||||
make_back_2(),
|
for rank in 0u8..13 {
|
||||||
make_back_3(),
|
let cv = make_card_face(&font, rank, suit);
|
||||||
make_back_4(),
|
let name = format!("{}_{}.png", ranks[rank as usize], suits[suit as usize]);
|
||||||
];
|
let path = root.join("assets/cards/faces").join(&name);
|
||||||
for (i, pixels) in backs.iter().enumerate() {
|
save_card_png(&path, &cv);
|
||||||
|
println!("wrote {}", path.display());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Card backs
|
||||||
|
for (i, pixels) in [make_back_0(), make_back_1(), make_back_2(), make_back_3(), make_back_4()].iter().enumerate() {
|
||||||
let path = root.join(format!("assets/cards/backs/back_{i}.png"));
|
let path = root.join(format!("assets/cards/backs/back_{i}.png"));
|
||||||
save_png(&path, pixels);
|
save_small_png(&path, pixels);
|
||||||
println!("wrote {}", path.display());
|
println!("wrote {}", path.display());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Backgrounds.
|
// Backgrounds
|
||||||
let bgs = [
|
for (i, pixels) in [make_bg_0(), make_bg_1(), make_bg_2(), make_bg_3(), make_bg_4()].iter().enumerate() {
|
||||||
make_bg_0(),
|
|
||||||
make_bg_1(),
|
|
||||||
make_bg_2(),
|
|
||||||
make_bg_3(),
|
|
||||||
make_bg_4(),
|
|
||||||
];
|
|
||||||
for (i, pixels) in bgs.iter().enumerate() {
|
|
||||||
let path = root.join(format!("assets/backgrounds/bg_{i}.png"));
|
let path = root.join(format!("assets/backgrounds/bg_{i}.png"));
|
||||||
save_png(&path, pixels);
|
save_small_png(&path, pixels);
|
||||||
println!("wrote {}", path.display());
|
println!("wrote {}", path.display());
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("gen_art: all placeholder PNG assets generated successfully.");
|
println!("gen_art: all assets generated successfully.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ fn main() -> io::Result<()> {
|
|||||||
("ambient_loop.wav", ambient_loop),
|
("ambient_loop.wav", ambient_loop),
|
||||||
];
|
];
|
||||||
|
|
||||||
for (name, gen) in &effects {
|
for (name, make) in &effects {
|
||||||
let samples = gen();
|
let samples = make();
|
||||||
let path = out_dir.join(name);
|
let path = out_dir.join(name);
|
||||||
write_wav_mono_pcm16(&path, SAMPLE_RATE, &samples)?;
|
write_wav_mono_pcm16(&path, SAMPLE_RATE, &samples)?;
|
||||||
println!("wrote {} ({} samples)", path.display(), samples.len());
|
println!("wrote {} ({} samples)", path.display(), samples.len());
|
||||||
|
|||||||
@@ -54,8 +54,11 @@ pub const BLACK_SUIT_COLOUR: Color = Color::srgb(0.08, 0.08, 0.08);
|
|||||||
/// solid-colour sprites (used in tests with `MinimalPlugins`).
|
/// solid-colour sprites (used in tests with `MinimalPlugins`).
|
||||||
#[derive(Resource)]
|
#[derive(Resource)]
|
||||||
pub struct CardImageSet {
|
pub struct CardImageSet {
|
||||||
/// Shared face image used for all face-up cards.
|
/// Per-card face images indexed by `[suit][rank]`.
|
||||||
pub face: Handle<Image>,
|
///
|
||||||
|
/// Suit order: Clubs=0, Diamonds=1, Hearts=2, Spades=3.
|
||||||
|
/// Rank order: Ace=0, Two=1 … King=12.
|
||||||
|
pub faces: [[Handle<Image>; 13]; 4],
|
||||||
/// One handle per unlockable card-back design (indices 0–4).
|
/// One handle per unlockable card-back design (indices 0–4).
|
||||||
pub backs: [Handle<Image>; 5],
|
pub backs: [Handle<Image>; 5],
|
||||||
}
|
}
|
||||||
@@ -202,8 +205,6 @@ impl Plugin for CardPlugin {
|
|||||||
/// so it does nothing and the plugin falls back to solid-colour sprites.
|
/// 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) {
|
fn load_card_images(images: Option<ResMut<Assets<Image>>>, mut commands: Commands) {
|
||||||
let Some(mut images) = images else {
|
let Some(mut images) = images else {
|
||||||
// Assets<Image> is absent (e.g. MinimalPlugins in tests) — skip so
|
|
||||||
// tests can still run. The plugin falls back to solid-colour sprites.
|
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
use bevy::asset::RenderAssetUsages;
|
use bevy::asset::RenderAssetUsages;
|
||||||
@@ -221,7 +222,79 @@ fn load_card_images(images: Option<ResMut<Assets<Image>>>, mut commands: Command
|
|||||||
.expect("valid card PNG")
|
.expect("valid card PNG")
|
||||||
};
|
};
|
||||||
|
|
||||||
let face = images.add(load(include_bytes!("../../assets/cards/faces/face.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"),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
let faces: [[Handle<Image>; 13]; 4] = std::array::from_fn(|suit| {
|
||||||
|
std::array::from_fn(|rank| images.add(load(FACE_BYTES[suit][rank])))
|
||||||
|
});
|
||||||
let backs = [
|
let backs = [
|
||||||
images.add(load(include_bytes!("../../assets/cards/backs/back_0.png"))),
|
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_1.png"))),
|
||||||
@@ -229,7 +302,7 @@ fn load_card_images(images: Option<ResMut<Assets<Image>>>, mut commands: Command
|
|||||||
images.add(load(include_bytes!("../../assets/cards/backs/back_3.png"))),
|
images.add(load(include_bytes!("../../assets/cards/backs/back_3.png"))),
|
||||||
images.add(load(include_bytes!("../../assets/cards/backs/back_4.png"))),
|
images.add(load(include_bytes!("../../assets/cards/backs/back_4.png"))),
|
||||||
];
|
];
|
||||||
commands.insert_resource(CardImageSet { face, backs });
|
commands.insert_resource(CardImageSet { faces, backs });
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builds the [`Sprite`] for a card, using PNG artwork when [`CardImageSet`] is
|
/// Builds the [`Sprite`] for a card, using PNG artwork when [`CardImageSet`] is
|
||||||
@@ -244,7 +317,28 @@ fn card_sprite(
|
|||||||
) -> Sprite {
|
) -> Sprite {
|
||||||
if let Some(set) = card_images {
|
if let Some(set) = card_images {
|
||||||
let image = if card.face_up {
|
let image = if card.face_up {
|
||||||
set.face.clone()
|
let suit_idx = match card.suit {
|
||||||
|
Suit::Clubs => 0,
|
||||||
|
Suit::Diamonds => 1,
|
||||||
|
Suit::Hearts => 2,
|
||||||
|
Suit::Spades => 3,
|
||||||
|
};
|
||||||
|
let rank_idx = match card.rank {
|
||||||
|
Rank::Ace => 0,
|
||||||
|
Rank::Two => 1,
|
||||||
|
Rank::Three => 2,
|
||||||
|
Rank::Four => 3,
|
||||||
|
Rank::Five => 4,
|
||||||
|
Rank::Six => 5,
|
||||||
|
Rank::Seven => 6,
|
||||||
|
Rank::Eight => 7,
|
||||||
|
Rank::Nine => 8,
|
||||||
|
Rank::Ten => 9,
|
||||||
|
Rank::Jack => 10,
|
||||||
|
Rank::Queen => 11,
|
||||||
|
Rank::King => 12,
|
||||||
|
};
|
||||||
|
set.faces[suit_idx][rank_idx].clone()
|
||||||
} else {
|
} else {
|
||||||
let idx = selected_back.min(set.backs.len() - 1);
|
let idx = selected_back.min(set.backs.len() - 1);
|
||||||
set.backs[idx].clone()
|
set.backs[idx].clone()
|
||||||
@@ -462,14 +556,16 @@ fn spawn_card_entity(
|
|||||||
) {
|
) {
|
||||||
let sprite = card_sprite(card, layout.card_size, back_colour, color_blind, card_images, selected_back);
|
let sprite = card_sprite(card, layout.card_size, back_colour, color_blind, card_images, selected_back);
|
||||||
|
|
||||||
commands
|
let mut entity = commands.spawn((
|
||||||
.spawn((
|
CardEntity { card_id: card.id },
|
||||||
CardEntity { card_id: card.id },
|
sprite,
|
||||||
sprite,
|
Transform::from_xyz(pos.x, pos.y, z),
|
||||||
Transform::from_xyz(pos.x, pos.y, z),
|
Visibility::default(),
|
||||||
Visibility::default(),
|
));
|
||||||
))
|
// When PNG faces are loaded the rank/suit are baked into the image.
|
||||||
.with_children(|b| {
|
// Only spawn the Text2d overlay in the solid-colour fallback (tests).
|
||||||
|
if card_images.is_none() {
|
||||||
|
entity.with_children(|b| {
|
||||||
b.spawn((
|
b.spawn((
|
||||||
CardLabel,
|
CardLabel,
|
||||||
Text2d::new(label_for(card)),
|
Text2d::new(label_for(card)),
|
||||||
@@ -478,12 +574,11 @@ fn spawn_card_entity(
|
|||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
TextColor(text_colour(card)),
|
TextColor(text_colour(card)),
|
||||||
// Above the card body on z so it doesn't get occluded by the
|
|
||||||
// parent sprite in back-to-front rendering.
|
|
||||||
Transform::from_xyz(0.0, 0.0, 0.01),
|
Transform::from_xyz(0.0, 0.0, 0.01),
|
||||||
label_visibility(card),
|
label_visibility(card),
|
||||||
));
|
));
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
@@ -526,22 +621,25 @@ fn update_card_entity(
|
|||||||
.insert(Transform::from_xyz(pos.x, pos.y, z));
|
.insert(Transform::from_xyz(pos.x, pos.y, z));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Despawn the old label child and respawn a fresh one, so rank/suit/
|
// Despawn any stale children and re-add the label overlay only when
|
||||||
// colour/visibility all stay in sync with the card's current state.
|
// operating in solid-colour mode (no PNG faces). In image mode the
|
||||||
|
// rank/suit are baked into the PNG, so no Text2d overlay is needed.
|
||||||
commands.entity(entity).despawn_related::<Children>();
|
commands.entity(entity).despawn_related::<Children>();
|
||||||
commands.entity(entity).with_children(|b| {
|
if card_images.is_none() {
|
||||||
b.spawn((
|
commands.entity(entity).with_children(|b| {
|
||||||
CardLabel,
|
b.spawn((
|
||||||
Text2d::new(label_for(card)),
|
CardLabel,
|
||||||
TextFont {
|
Text2d::new(label_for(card)),
|
||||||
font_size: layout.card_size.x * FONT_SIZE_FRAC,
|
TextFont {
|
||||||
..default()
|
font_size: layout.card_size.x * FONT_SIZE_FRAC,
|
||||||
},
|
..default()
|
||||||
TextColor(text_colour(card)),
|
},
|
||||||
Transform::from_xyz(0.0, 0.0, 0.01),
|
TextColor(text_colour(card)),
|
||||||
label_visibility(card),
|
Transform::from_xyz(0.0, 0.0, 0.01),
|
||||||
));
|
label_visibility(card),
|
||||||
});
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn label_for(card: &Card) -> String {
|
fn label_for(card: &Card) -> String {
|
||||||
|
|||||||