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

+15
View File
@@ -108,3 +108,18 @@ The game copies `imp-32.png` and `imp-hack-32.png` to `assets/sprites/`.
The gold sack is not generated: it is a small hand-made pixel pattern (`SACK_HD`, `SACK_SMALL`
in `src/gfx.rs`) that the game places on every frame when the atlas is built.
## 64x64 (2026-11-07)
The high-res style shows the imp in 64x64 when close and in 32x32 at the normal zoom (both from
the same raw sheets; a 64x64 sprite shown smaller would lose pixels and flicker). The 64x64
sheets use the body scale 60 pixels, the feet row 63, 24 colours plus the iron greys:
```bash
python3 $T raw-1.png imp --views 5 --size 64 --colors 24 --crop 0,0.27,1,0.72 --step 9.217 --feet 63 \
--add-colors "150,152,168;104,106,122"
# hack rows: --size 64 --step <body height>/60 --feet 63 --palette imp-64.png --outline-weight 0.6
```
Results: `imp-64.png`, `imp-hack-64.png`, preview `imp-hack-64-preview.png` (idle views in the
right column). The game copies them to `assets/sprites/`.
Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

+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);
}
}
}
+16 -10
View File
@@ -1563,8 +1563,9 @@ impl R3d {
// sprites as billboards
// the world's pixel rows (detailed sprites only where they get enough pixels)
let world_rows = pix.map_or(screen_height() * screen_dpi_scale(), |(_, th, _)| th as f32);
if let Some(mesh) = self.billboards(g, gfx, cam, t, world_rows) {
draw_mesh(&mesh);
let (small, detailed) = self.billboards(g, gfx, cam, t, world_rows);
for mesh in [small, detailed].iter().flatten() {
draw_mesh(mesh);
}
// solid unlit objects (default material, depth tested)
gl_use_default_material();
@@ -1638,7 +1639,8 @@ impl R3d {
self.draw_2d(g, gfx, cam, &m, t);
}
fn billboards(&self, g: &Game, gfx: &Gfx, cam: &Cam, t: f32, world_rows: f32) -> Option<Mesh> {
// the units as billboards: the 10x10 sprites and the detailed ones (two textures)
fn billboards(&self, g: &Game, gfx: &Gfx, cam: &Cam, t: f32, world_rows: f32) -> (Option<Mesh>, Option<Mesh>) {
let (_, r, _) = cam.basis();
let toward = cam.eye_dir();
let amb = |x: f32, z: f32| {
@@ -1656,6 +1658,7 @@ impl R3d {
let rg = vec2(r.x, r.z).normalize_or_zero();
let tg = vec2(toward.x, toward.z).normalize_or_zero();
let mut mb = MB::new();
let mut mb_hd = MB::new();
let sw = SPRITE_TEX_W;
let sh = SPRITE_TEX_H;
// detailed sprites only in the high-res style, and only where a sprite's cell (1.2 sizes
@@ -1761,19 +1764,24 @@ impl R3d {
}
let white = e.flash > 0.07;
let v = if e.captive != 0 { 0 } else { view };
let detailed = hd && w * 1.2 * px_per_tile >= crate::gfx::ACELL as f32 * 0.85;
// screen pixels of the cell per texel of a detailed cell (64x64 level), the 32x32
// level has two texels per pixel
let texel = w * 1.2 * px_per_tile / crate::gfx::HD_CELL as f32;
let detailed = hd && texel * 2.0 >= 0.85;
let fine = texel >= 0.85;
// treasure in a sack on the back; digging as a drawn animation (detailed sprites),
// frame by frame with the same rhythm as the swing below
let sack = e.carry > 0 && Gfx::has_sack(e.kind);
let hacking = e.working && !e.moving && !sleeping && matches!(e.job, Job::Dig(_) | Job::TearBridge(_));
let frame = ((ph * 1.3 / std::f32::consts::TAU).rem_euclid(1.0) * crate::gfx::HACK_FRAMES as f32) as u32;
let hd = if detailed { Gfx::hd_cell_view(e.kind, white, v, hacking.then_some(frame), sack) } else { None };
let hd = if detailed { Gfx::hd_cell_view(e.kind, white, v, hacking.then_some(frame), sack, fine) } else { None };
if hd.is_some() && hacking {
// the frames swing the pickaxe: no tilting of the whole figure
rot = 0.0;
lunge = 0.0;
}
let src = hd.or_else(|| sack.then(|| Gfx::sprite_cell_sack(e.kind, white, e.level, v)).flatten()).unwrap_or_else(|| Gfx::sprite_cell_view(e.kind, white, e.level, v));
let (sw, sh) = if hd.is_some() { (crate::gfx::HD_TEX_W, crate::gfx::HD_TEX_H) } else { (sw, sh) };
let (mut u0, v0) = (src.x / sw, src.y / sh);
let (mut u1, v1) = ((src.x + src.w) / sw, (src.y + src.h) / sh);
if mirror {
@@ -1813,6 +1821,7 @@ impl R3d {
};
// the 12x12 cell (sprite with a one pixel border for the gear): the sprite's feet
// (bottom of its 11th pixel row) stay on the ground
let mb = if hd.is_some() { &mut mb_hd } else { &mut mb };
mb.quad(
[corner(-w * 0.6, h * 1.1), corner(w * 0.6, h * 1.1), corner(w * 0.6, -h * 0.1), corner(-w * 0.6, -h * 0.1)],
[vec2(u0, v0), vec2(u1, v0), vec2(u1, v1), vec2(u0, v1)],
@@ -1820,11 +1829,8 @@ impl R3d {
[toward; 4],
);
}
if mb.v.is_empty() {
None
} else {
Some(Mesh { vertices: mb.v, indices: mb.i, texture: Some(gfx.sprites.clone()) })
}
let mesh = |mb: MB, tex: &Texture2D| if mb.v.is_empty() { None } else { Some(Mesh { vertices: mb.v, indices: mb.i, texture: Some(tex.clone()) }) };
(mesh(mb, &gfx.sprites), mesh(mb_hd, &gfx.hd_sprites))
}
fn draw_2d(&self, g: &Game, gfx: &Gfx, cam: &Cam, m: &Mat4, t: f32) {