Imp in 64x64 when close, 32x32 at normal zoom; detailed sprites in their own texture

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-07 21:17:17 +01:00
1 parent 2d1ed177a7
commit 69a6b13006
9 files changed
+173 -89

No files matched your search

+142 -79
View File
@@ -98,6 +98,7 @@ pub const T_MOSS: u32 = 216; // 4 variants: mushroom grove ground, moss with glo
pub struct Gfx {
pub atlas: Texture2D,
pub sprites: Texture2D,
pub hd_sprites: Texture2D, // detailed sprites (see HD_SPRITES, hd_cell_view)
pub cursors: Texture2D, // mouse pointer frames, 16x16 each (see CURSOR_*)
pub badges: Texture2D, // achievement badges, 32x32 each (see badges.rs)
pub font: Texture2D, // Dinowyde (glyphs of 6x8 px ordered by Unicode, see font_slot) and DINOBYTE
@@ -1368,36 +1369,46 @@ fn gen_atlas() -> Image {
const SPR: usize = 10;
const SPR_CELL: u32 = 12;
// the atlas holds every sprite pixel as SPR_K x SPR_K texels, so detailed sprites (HD_SPRITES)
// fit into the same cells: a cell is SPR_CELL * SPR_K = 36 texels
// the atlas holds every sprite pixel as SPR_K x SPR_K texels: a cell is SPR_CELL * SPR_K texels
const SPR_K: u32 = 3;
pub const ACELL: u32 = SPR_CELL * SPR_K;
// Detailed 32x32 sprites (generated, see art/generated/), used instead of the 10x10 ones in the
// high-res graphics style when close enough (render3d::billboards). Transparent background, the
// feet on the bottom row. `idle`: the five views side by side (VIEWS); `hack`: the digging
// animation, HACK_FRAMES frames side by side, one row per view. No gear yet.
// Detailed sprites (generated, see art/generated/) in their own texture, used instead of the
// 10x10 ones in the high-res graphics style (render3d::billboards). Two levels of detail from
// the same pictures: 64x64 when the sprite gets enough screen pixels (close), 32x32 otherwise,
// so neither loses pixels nor flickers. Sheets: transparent, the feet on the bottom row;
// `idle*`: the five views side by side (VIEWS); `hack*`: the digging animation, HACK_FRAMES
// frames side by side, one row per view. No gear yet.
struct HdSprite {
kind: EK,
idle: &'static [u8],
hack: &'static [u8],
idle_32: &'static [u8],
hack_32: &'static [u8],
}
const HD_SPRITES: [HdSprite; 1] = [HdSprite {
kind: EK::Worker,
idle: include_bytes!("../assets/sprites/imp-32.png"),
hack: include_bytes!("../assets/sprites/imp-hack-32.png"),
idle: include_bytes!("../assets/sprites/imp-64.png"),
hack: include_bytes!("../assets/sprites/imp-hack-64.png"),
idle_32: include_bytes!("../assets/sprites/imp-32.png"),
hack_32: include_bytes!("../assets/sprites/imp-hack-32.png"),
}];
const HD: u32 = 32;
pub const HACK_FRAMES: u32 = 4;
// atlas columns of a detailed sprite: idle and the hack frames, each without and with the sack
// a detailed sprite's cell: the same shape as a 10x10 sprite's 12x12 cell (6 texels per pixel),
// so the billboard is the same; the 64x64 picture sits where the 10x10 sprite would
pub const HD_CELL: u32 = SPR_CELL * 6;
// columns per detailed sprite and detail level: idle and the hack frames, each without and with
// the sack; then the same for the 32x32 level
const HD_COLS: u32 = (1 + HACK_FRAMES) * 2;
pub const HD_TEX_W: f32 = (HD_SPRITES.len() as u32 * HD_COLS * 2 * HD_CELL) as f32;
pub const HD_TEX_H: f32 = (VIEWS * 2 * HD_CELL) as f32;
// kinds that carry treasure in a sack on the back (instead of the coin sign over the head);
// their 10x10 sprite with the sack gets an own atlas column after SPRITE_KINDS
const SACK_KINDS: [EK; 1] = [EK::Worker];
// the sack (k outline, B burlap, b its shade and the rope, y/Y gold coins at the opening)
const SACK_SMALL: [&str; 5] = [".kk.", "kyYk", "kBbk", "kBBk", ".kk."];
const SACK_HD: [&str; 12] = [
const SACK_32: [&str; 12] = [
"....kkkk....",
"...kyYYyk...",
"..kyYyyYyk..",
@@ -1411,6 +1422,31 @@ const SACK_HD: [&str; 12] = [
"..kBBBBBBk..",
"...kkkkkk...",
];
// the same, finer for the 64x64 sprites (d: the shadow side)
const SACK_64: [&str; 22] = [
".......kkkkkk.......",
".....kkyYYYyykk.....",
"....kyYYyYYyyYyk....",
"...kyYyyYyYYyyYyk...",
"...kkyYYyyyYYyykk...",
"....kkbbbbbbbbkk....",
".....kbBbbbbBbk.....",
"......kBBBBBBk......",
".....kBBBBBBBBk.....",
"....kBBBBBBBBBBk....",
"...kBBBBbBBBBBBBk...",
"..kBBBBBBBBBbBBBBk..",
"..kBBbBBBBBBBBBBBk..",
".kBBBBBBBBBBBBbBBBk.",
".kBBBBBBBbBBBBBBBBk.",
".kBBBBBBBBBBBBBBBdk.",
".kBBbBBBBBBBBBBBddk.",
".kBBBBBBBBBBBBBdddk.",
"..kBBBBBBBBBBBdddk..",
"..kdBBBBBBBBBddddk..",
"...kkddddddddddkk...",
".....kkkkkkkkkk.....",
];
fn sprite_rows(k: EK) -> [&'static str; 10] {
match k {
@@ -1868,7 +1904,7 @@ pub const SPRITE_KINDS: [EK; 31] = [
EK::Inquisitor,
];
// width of the sprite texture in pixels
pub const SPRITE_TEX_W: f32 = ((SPRITE_KINDS.len() + SACK_KINDS.len()) as u32 * ACELL + HD_SPRITES.len() as u32 * HD_COLS * ACELL) as f32;
pub const SPRITE_TEX_W: f32 = ((SPRITE_KINDS.len() + SACK_KINDS.len()) as u32 * ACELL) as f32;
pub const SPRITE_TEX_H: f32 = (2 * VIEWS * GEAR_TIERS * ACELL) as f32;
pub fn sprite_index(k: EK) -> u32 {
@@ -2105,6 +2141,7 @@ fn add_sack(g: &mut Cell, view: u32, pat: &[&str], front_dx: (f32, f32)) {
'b' => c(108, 70, 40),
'y' => c(232, 176, 48),
'Y' => c(255, 232, 120),
'd' => c(112, 74, 42),
_ => continue,
};
let (x, y) = (x0 + px as i32, y0 + py as i32);
@@ -2119,30 +2156,32 @@ fn add_sack(g: &mut Cell, view: u32, pat: &[&str], front_dx: (f32, f32)) {
}
}
// writes a cell into the atlas: column, cell row (the white silhouette goes into the row below),
// every cell pixel as k x k texels
fn put_cell(img: &mut Image, col: u32, row: u32, g: &Cell, k: u32) {
// writes a cell into an atlas of `cell` x `cell` texel cells: column, cell row (the white
// silhouette goes into the row below), every cell pixel as k x k texels
fn put_cell(img: &mut Image, col: u32, row: u32, g: &Cell, k: u32, cell: u32) {
for (y, line) in g.iter().enumerate() {
for (x, p) in line.iter().enumerate() {
let Some(c) = p else { continue };
for d in 0..k * k {
let (px, py) = (col * ACELL + x as u32 * k + d % k, row * ACELL + y as u32 * k + d / k);
let (px, py) = (col * cell + x as u32 * k + d % k, row * cell + y as u32 * k + d / k);
img.set_pixel(px, py, *c);
img.set_pixel(px, py + ACELL, WHITE);
img.set_pixel(px, py + cell, WHITE);
}
}
}
}
// a 32x32 picture from a sheet into a 36x36 cell (the feet on the 10x10 sprite's bottom row)
fn hd_cell(src: &Image, sx: u32, sy: u32) -> Cell {
let mut g = vec![vec![None; ACELL as usize]; ACELL as usize];
for y in 0..HD {
for x in 0..HD {
// an n x n picture from a sheet into a cell grid of `size` (centred, the feet on the bottom row
// of a 10x10 sprite: 11/12 of the cell)
fn hd_cell(src: &Image, sx: u32, sy: u32, n: u32, size: u32) -> Cell {
let mut g = vec![vec![None; size as usize]; size as usize];
let (ox, oy) = ((size - n) / 2, size * 11 / 12 - n);
for y in 0..n {
for x in 0..n {
if sx + x < src.width as u32 && sy + y < src.height as u32 {
let p = src.get_pixel(sx + x, sy + y);
if p.a > 0.5 {
g[1 + y as usize][2 + x as usize] = Some(Color::new(p.r, p.g, p.b, 1.0));
g[(oy + y) as usize][(ox + x) as usize] = Some(Color::new(p.r, p.g, p.b, 1.0));
}
}
}
@@ -2150,9 +2189,9 @@ fn hd_cell(src: &Image, sx: u32, sy: u32) -> Cell {
g
}
// The sprite atlas. Columns: SPRITE_KINDS, SACK_KINDS (with the sack), then HD_COLS per
// detailed sprite. 10x10 sprites: row (view * GEAR_TIERS + tier) * 2 with the gear of the tier,
// the row after it the white silhouette (hit flash). Detailed sprites: row view * 2 (+1 white).
// The sprite atlas of the 10x10 sprites. Columns: SPRITE_KINDS, then SACK_KINDS with the sack.
// Row (view * GEAR_TIERS + tier) * 2 with the gear of the tier, the row after it the white
// silhouette (hit flash).
fn gen_sprites() -> Image {
let n = SPRITE_KINDS.len() as u32;
let mut img = Image::gen_image_color(SPRITE_TEX_W as u16, SPRITE_TEX_H as u16, Color::new(0.0, 0.0, 0.0, 0.0));
@@ -2161,27 +2200,40 @@ fn gen_sprites() -> Image {
for tier in 0..GEAR_TIERS {
let row = (view * GEAR_TIERS + tier) * 2;
for (i, &k) in SPRITE_KINDS.iter().enumerate() {
put_cell(&mut img, i as u32, row, &small(k, tier, view), SPR_K);
put_cell(&mut img, i as u32, row, &small(k, tier, view), SPR_K, ACELL);
}
for (i, &k) in SACK_KINDS.iter().enumerate() {
let mut g = small(k, tier, view);
add_sack(&mut g, view, &SACK_SMALL, (-0.5, -0.5));
put_cell(&mut img, n + i as u32, row, &g, SPR_K);
put_cell(&mut img, n + i as u32, row, &g, SPR_K, ACELL);
}
}
}
let base = n + SACK_KINDS.len() as u32;
img
}
// The texture of the detailed sprites (HD_SPRITES): per sprite HD_COLS columns of 64x64
// pictures, then HD_COLS of 32x32 ones (two texels per pixel); row view * 2, the row after it
// the white silhouette.
fn gen_hd_sprites() -> Image {
let mut img = Image::gen_image_color(HD_TEX_W as u16, HD_TEX_H as u16, Color::new(0.0, 0.0, 0.0, 0.0));
let png = |b: &[u8]| Image::from_file_with_format(b, Some(ImageFormat::Png)).expect("hd sprite");
for (j, hd) in HD_SPRITES.iter().enumerate() {
let idle = Image::from_file_with_format(hd.idle, Some(ImageFormat::Png)).expect("hd sprite");
let hack = Image::from_file_with_format(hd.hack, Some(ImageFormat::Png)).expect("hd sprite");
for view in 0..VIEWS {
for f in 0..1 + HACK_FRAMES {
let g = if f == 0 { hd_cell(&idle, view * HD, 0) } else { hd_cell(&hack, (f - 1) * HD, view * HD) };
let col = base + j as u32 * HD_COLS + f * 2;
put_cell(&mut img, col, view * 2, &g, 1);
let mut s = g;
add_sack(&mut s, view, &SACK_HD, (0.7, -0.65));
put_cell(&mut img, col + 1, view * 2, &s, 1);
// (idle, hack, picture size, cell grid, texels per pixel, sack, where the sack shows from the front)
let levels = [
(png(hd.idle), png(hd.hack), 64, HD_CELL, 1, &SACK_64[..], (0.7, -0.65)),
(png(hd.idle_32), png(hd.hack_32), 32, HD_CELL / 2, 2, &SACK_32[..], (0.7, -0.65)),
];
for (l, (idle, hack, n, size, k, sack, dx)) in levels.iter().enumerate() {
for view in 0..VIEWS {
for f in 0..1 + HACK_FRAMES {
let g = if f == 0 { hd_cell(idle, view * n, 0, *n, *size) } else { hd_cell(hack, (f - 1) * n, view * n, *n, *size) };
let col = (j as u32 * 2 + l as u32) * HD_COLS + f * 2;
put_cell(&mut img, col, view * 2, &g, *k, HD_CELL);
let mut s = g;
add_sack(&mut s, view, sack, *dx);
put_cell(&mut img, col + 1, view * 2, &s, *k, HD_CELL);
}
}
}
}
@@ -2308,7 +2360,9 @@ impl Gfx {
cursors.set_filter(FilterMode::Nearest);
let badges = Texture2D::from_image(&crate::badges::gen_badges());
badges.set_filter(FilterMode::Nearest);
Gfx { atlas, sprites, cursors, badges, font, font_map }
let hd_sprites = Texture2D::from_image(&gen_hd_sprites());
hd_sprites.set_filter(FilterMode::Nearest);
Gfx { atlas, sprites, hd_sprites, cursors, badges, font, font_map }
}
pub fn cell(id: u32) -> Rect {
@@ -2335,14 +2389,15 @@ impl Gfx {
}
// the cell of a detailed sprite's view, if the kind has one (same layout as sprite_cell_view)
// the cell of a detailed sprite, if the kind has one: a view, standing (`hack` None) or a
// frame of the digging animation, with or without the sack
pub fn hd_cell_view(k: EK, white: bool, view: u32, hack: Option<u32>, sack: bool) -> Option<Rect> {
// the cell of a detailed sprite in `hd_sprites`, if the kind has one: a view, standing
// (`hack` None) or a frame of the digging animation, with or without the sack; `fine`: the
// 64x64 level, else the 32x32 one
pub fn hd_cell_view(k: EK, white: bool, view: u32, hack: Option<u32>, sack: bool, fine: bool) -> Option<Rect> {
let j = HD_SPRITES.iter().position(|h| h.kind == k)? as u32;
let f = hack.map_or(0, |f| 1 + f % HACK_FRAMES);
let col = (SPRITE_KINDS.len() + SACK_KINDS.len()) as u32 + j * HD_COLS + f * 2 + sack as u32;
let col = (j * 2 + !fine as u32) * HD_COLS + f * 2 + sack as u32;
let row = view.min(VIEWS - 1) * 2 + white as u32;
Some(Rect::new((col * ACELL) as f32, (row * ACELL) as f32, ACELL as f32, ACELL as f32))
Some(Rect::new((col * HD_CELL) as f32, (row * HD_CELL) as f32, HD_CELL as f32, HD_CELL as f32))
}
// whether a kind carries its treasure in a sack on the back
@@ -2582,36 +2637,46 @@ mod sprite_tests {
use super::*;
fn opaque(img: &Image, r: Rect) -> usize {
(0..ACELL * ACELL).filter(|i| img.get_pixel(r.x as u32 + i % ACELL, r.y as u32 + i / ACELL).a > 0.5).count()
let n = r.w as u32;
(0..n * n).filter(|i| img.get_pixel(r.x as u32 + i % n, r.y as u32 + i / n).a > 0.5).count()
}
// texels that differ between two cells
fn differ(img: &Image, a: Rect, b: Rect) -> usize {
(0..ACELL * ACELL)
let n = a.w as u32;
(0..n * n)
.filter(|i| {
let p = img.get_pixel(a.x as u32 + i % ACELL, a.y as u32 + i / ACELL);
let q = img.get_pixel(b.x as u32 + i % ACELL, b.y as u32 + i / ACELL);
let p = img.get_pixel(a.x as u32 + i % n, a.y as u32 + i / n);
let q = img.get_pixel(b.x as u32 + i % n, b.y as u32 + i / n);
(p.r, p.g, p.b, p.a) != (q.r, q.g, q.b, q.a)
})
.count()
}
#[test]
fn detailed_sprites_in_the_atlas() {
let img = gen_sprites();
assert_eq!((img.width as f32, img.height as f32), (SPRITE_TEX_W, SPRITE_TEX_H));
for view in 0..VIEWS {
for hack in [None, Some(0), Some(1), Some(2), Some(3)] {
let r = Gfx::hd_cell_view(EK::Worker, false, view, hack, false).expect("the imp has a detailed sprite");
let w = Gfx::hd_cell_view(EK::Worker, true, view, hack, false).unwrap();
let s = Gfx::hd_cell_view(EK::Worker, false, view, hack, true).unwrap();
assert!(opaque(&img, r) > 150, "view {view} {hack:?} drawn");
assert_eq!(opaque(&img, r), opaque(&img, w), "white silhouette matches");
assert!(differ(&img, s, r) > 15, "view {view} {hack:?}: the sack shows");
fn detailed_sprites() {
let img = gen_hd_sprites();
assert_eq!((img.width as f32, img.height as f32), (HD_TEX_W, HD_TEX_H));
for fine in [true, false] {
for view in 0..VIEWS {
for hack in [None, Some(0), Some(1), Some(2), Some(3)] {
let r = Gfx::hd_cell_view(EK::Worker, false, view, hack, false, fine).expect("the imp has a detailed sprite");
let w = Gfx::hd_cell_view(EK::Worker, true, view, hack, false, fine).unwrap();
let s = Gfx::hd_cell_view(EK::Worker, false, view, hack, true, fine).unwrap();
assert!(opaque(&img, r) > 600, "view {view} {hack:?} {fine} drawn");
assert_eq!(opaque(&img, r), opaque(&img, w), "white silhouette matches");
assert!(differ(&img, s, r) > 60, "view {view} {hack:?} {fine}: the sack shows");
}
}
}
assert!(Gfx::hd_cell_view(EK::Knight, false, 0, None, false).is_none());
// the 10x10 sprites: every pixel a 3x3 block; the imp's sack variant
assert!(Gfx::hd_cell_view(EK::Knight, false, 0, None, false, true).is_none());
}
#[test]
fn small_sprites_and_sack() {
let img = gen_sprites();
assert_eq!((img.width as f32, img.height as f32), (SPRITE_TEX_W, SPRITE_TEX_H));
// every pixel a 3x3 block
let r = Gfx::sprite_cell_view(EK::Knight, false, 1, 0);
for y in 0..SPR_CELL {
for x in 0..SPR_CELL {
@@ -2628,32 +2693,30 @@ mod sprite_tests {
}
}
// `cargo test --bins dump_imp_cells -- --ignored` writes the imp's cells to /tmp for a look:
// rows: 10x10, 10x10 with sack, then idle and the hack frames without and with sack
// `cargo test --bins dump_imp_cells -- --ignored` writes the imp's detailed cells to /tmp for
// a look: rows idle and the hack frames, without and with sack; 64x64 left, 32x32 right
#[test]
#[ignore]
fn dump_imp_cells() {
let img = gen_sprites();
let mut cells: Vec<Vec<Rect>> = Vec::new();
for sack in [false, true] {
let r: Vec<Rect> = (0..VIEWS).map(|v| if sack { Gfx::sprite_cell_sack(EK::Worker, false, 1, v).unwrap() } else { Gfx::sprite_cell_view(EK::Worker, false, 1, v) }).collect();
cells.push(r);
}
let img = gen_hd_sprites();
let mut rows: Vec<Vec<Rect>> = Vec::new();
for sack in [false, true] {
for hack in [None, Some(0), Some(1), Some(2), Some(3)] {
cells.push((0..VIEWS).map(|v| Gfx::hd_cell_view(EK::Worker, false, v, hack, sack).unwrap()).collect());
let mut r: Vec<Rect> = (0..VIEWS).map(|v| Gfx::hd_cell_view(EK::Worker, false, v, hack, sack, true).unwrap()).collect();
r.extend((0..VIEWS).map(|v| Gfx::hd_cell_view(EK::Worker, false, v, hack, sack, false).unwrap()));
rows.push(r);
}
}
let z = 3;
let (w, h) = (ACELL * z * VIEWS, ACELL * z * cells.len() as u32);
let c = HD_CELL;
let (w, h) = (c * VIEWS * 2, c * rows.len() as u32);
let mut out = Image::gen_image_color(w as u16, h as u16, Color::new(0.35, 0.33, 0.38, 1.0));
for (row, list) in cells.iter().enumerate() {
for (view, r) in list.iter().enumerate() {
for y in 0..ACELL * z {
for x in 0..ACELL * z {
let p = img.get_pixel(r.x as u32 + x / z, r.y as u32 + y / z);
for (row, list) in rows.iter().enumerate() {
for (i, r) in list.iter().enumerate() {
for y in 0..c {
for x in 0..c {
let p = img.get_pixel(r.x as u32 + x, r.y as u32 + y);
if p.a > 0.5 {
out.set_pixel(view as u32 * ACELL * z + x, h - 1 - (row as u32 * ACELL * z + y), p);
out.set_pixel(i as u32 * c + x, h - 1 - (row as u32 * c + y), p);
}
}
}