Detailed imp-style sprites for all 30 creatures in the high-res style; sprite texture in bands

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-07 23:21:58 +01:00
1 parent 210f17773a
commit 01e5326721
132 files changed
+1295 -43

No files matched your search

+115 -29
View File
@@ -1735,35 +1735,98 @@ const SPR_CELL: u32 = 12;
const SPR_K: u32 = 3;
pub const ACELL: u32 = SPR_CELL * SPR_K;
// 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.
// Detailed sprites (generated, see art/generated/imp-sprite and art/generated/sprites) 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
// imp): 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],
hack: Option<(&'static [u8], &'static [u8])>, // 64x64 and 32x32
}
const HD_SPRITES: [HdSprite; 1] = [HdSprite {
kind: EK::Worker,
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"),
}];
macro_rules! hd_sprite {
($kind:expr, $name:literal) => {
HdSprite {
kind: $kind,
idle: include_bytes!(concat!("../assets/sprites/", $name, "-64.png")),
idle_32: include_bytes!(concat!("../assets/sprites/", $name, "-32.png")),
hack: None,
}
};
}
const HD_SPRITES: [HdSprite; 31] = [
HdSprite {
kind: EK::Worker,
idle: include_bytes!("../assets/sprites/imp-64.png"),
idle_32: include_bytes!("../assets/sprites/imp-32.png"),
hack: Some((include_bytes!("../assets/sprites/imp-hack-64.png"), include_bytes!("../assets/sprites/imp-hack-32.png"))),
},
hd_sprite!(EK::Kobold, "kobold"),
hd_sprite!(EK::Spider, "spider"),
hd_sprite!(EK::Mage, "mage"),
hd_sprite!(EK::Ogre, "ogre"),
hd_sprite!(EK::Knight, "knight"),
hd_sprite!(EK::Archer, "archer"),
hd_sprite!(EK::Priest, "priest"),
hd_sprite!(EK::Dwarf, "dwarf"),
hd_sprite!(EK::Paladin, "paladin"),
hd_sprite!(EK::Jailer, "jailer"),
hd_sprite!(EK::Priestess, "priestess"),
hd_sprite!(EK::Chainbreaker, "chainbreaker"),
hd_sprite!(EK::Bat, "bat"),
hd_sprite!(EK::Gargoyle, "gargoyle"),
hd_sprite!(EK::Owl, "owl"),
hd_sprite!(EK::Wyrm, "wyrm"),
hd_sprite!(EK::Cogling, "cogling"),
hd_sprite!(EK::Revenant, "revenant"),
hd_sprite!(EK::Giant, "giant"),
hd_sprite!(EK::Fairy, "fairy"),
hd_sprite!(EK::BattleMage, "battlemage"),
hd_sprite!(EK::Ram, "ram"),
hd_sprite!(EK::Catapult, "catapult"),
hd_sprite!(EK::CaveBear, "cavebear"),
hd_sprite!(EK::Beetle, "beetle"),
hd_sprite!(EK::Trader, "trader"),
hd_sprite!(EK::Thief, "thief"),
hd_sprite!(EK::Monk, "monk"),
hd_sprite!(EK::Ranger, "ranger"),
hd_sprite!(EK::Inquisitor, "inquisitor"),
];
pub const HACK_FRAMES: u32 = 4;
// 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
// columns of a sprite with the digging animation, per detail level: idle and the hack frames,
// each without and with the sack; a sprite without it has one column per 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;
// the texture: the sprites' columns one after another (a sprite's own columns: the 64x64 level,
// then the 32x32 one), in bands of HD_BAND columns (a sprite never straddles two bands), every
// band VIEWS * 2 rows (a view, its white silhouette)
const HD_BAND: u32 = 40;
const fn hd_width(j: usize) -> u32 {
if HD_SPRITES[j].hack.is_some() { HD_COLS * 2 } else { 2 }
}
const fn hd_start(j: usize) -> u32 {
let mut p = 0;
let mut i = 0;
while i < j {
p += hd_width(i);
i += 1;
}
p
}
const HD_BANDS: u32 = (hd_start(HD_SPRITES.len()) + HD_BAND - 1) / HD_BAND;
pub const HD_TEX_W: f32 = (HD_BAND * HD_CELL) as f32;
pub const HD_TEX_H: f32 = (HD_BANDS * VIEWS * 2 * HD_CELL) as f32;
// where column `c` of sprite j lies: (texture column, first row of its band)
fn hd_place(j: usize, c: u32) -> (u32, u32) {
let p = hd_start(j) + c;
(p % HD_BAND, p / HD_BAND * VIEWS * 2)
}
// 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
@@ -2583,18 +2646,23 @@ fn gen_hd_sprites() -> Image {
for (j, hd) in HD_SPRITES.iter().enumerate() {
// (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)),
(png(hd.idle), hd.hack.map(|h| png(h.0)), 64, HD_CELL, 1, &SACK_64[..], (0.7, -0.65)),
(png(hd.idle_32), hd.hack.map(|h| png(h.1)), 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 {
let Some(hack) = hack else {
let (col, row) = hd_place(j, l as u32);
put_cell(&mut img, col, row + view * 2, &hd_cell(idle, view * n, 0, *n, *size), *k, HD_CELL);
continue;
};
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 (col, row) = hd_place(j, l as u32 * HD_COLS + f * 2);
put_cell(&mut img, col, row + 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);
put_cell(&mut img, col + 1, row + view * 2, &s, *k, HD_CELL);
}
}
}
@@ -2766,10 +2834,16 @@ impl Gfx {
// (`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 = (j * 2 + !fine as u32) * HD_COLS + f * 2 + sack as u32;
let row = view.min(VIEWS - 1) * 2 + white as u32;
let j = HD_SPRITES.iter().position(|h| h.kind == k)?;
// sprites without the digging animation (and without a sack) show their idle picture
let c = if HD_SPRITES[j].hack.is_some() {
let f = hack.map_or(0, |f| 1 + f % HACK_FRAMES);
(!fine as u32) * HD_COLS + f * 2 + sack as u32
} else {
!fine as u32
};
let (col, band) = hd_place(j, c);
let row = band + view.min(VIEWS - 1) * 2 + white as u32;
Some(Rect::new((col * HD_CELL) as f32, (row * HD_CELL) as f32, HD_CELL as f32, HD_CELL as f32))
}
@@ -3190,7 +3264,19 @@ mod sprite_tests {
}
}
}
assert!(Gfx::hd_cell_view(EK::Knight, false, 0, None, false, true).is_none());
// every kind has a detailed sprite, none straddles two bands of the texture
for &k in SPRITE_KINDS.iter() {
for fine in [true, false] {
for view in 0..VIEWS {
let r = Gfx::hd_cell_view(k, false, view, None, false, fine).expect("a detailed sprite for every kind");
assert!(opaque(&img, r) > 100, "{k:?} view {view} {fine}");
assert!(r.x + r.w <= HD_TEX_W && r.y + r.h <= HD_TEX_H);
}
}
}
for j in 0..HD_SPRITES.len() {
assert_eq!(hd_start(j) / HD_BAND, (hd_start(j) + hd_width(j) - 1) / HD_BAND, "sprite {j} straddles two bands");
}
}
#[test]