diff --git a/art/generated/imp-sprite/PROMPTS.md b/art/generated/imp-sprite/PROMPTS.md index 596eccf..80efe4f 100644 --- a/art/generated/imp-sprite/PROMPTS.md +++ b/art/generated/imp-sprite/PROMPTS.md @@ -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
/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/`. diff --git a/art/generated/imp-sprite/imp-64-preview.png b/art/generated/imp-sprite/imp-64-preview.png new file mode 100644 index 0000000..6606170 Binary files /dev/null and b/art/generated/imp-sprite/imp-64-preview.png differ diff --git a/art/generated/imp-sprite/imp-64.png b/art/generated/imp-sprite/imp-64.png new file mode 100644 index 0000000..6fbb9b3 Binary files /dev/null and b/art/generated/imp-sprite/imp-64.png differ diff --git a/art/generated/imp-sprite/imp-hack-64-preview.png b/art/generated/imp-sprite/imp-hack-64-preview.png new file mode 100644 index 0000000..62727ad Binary files /dev/null and b/art/generated/imp-sprite/imp-hack-64-preview.png differ diff --git a/art/generated/imp-sprite/imp-hack-64.png b/art/generated/imp-sprite/imp-hack-64.png new file mode 100644 index 0000000..a8b65bd Binary files /dev/null and b/art/generated/imp-sprite/imp-hack-64.png differ diff --git a/assets/sprites/imp-64.png b/assets/sprites/imp-64.png new file mode 100644 index 0000000..6fbb9b3 Binary files /dev/null and b/assets/sprites/imp-64.png differ diff --git a/assets/sprites/imp-hack-64.png b/assets/sprites/imp-hack-64.png new file mode 100644 index 0000000..a8b65bd Binary files /dev/null and b/assets/sprites/imp-hack-64.png differ diff --git a/src/gfx.rs b/src/gfx.rs index 02b923d..97d9e9c 100644 --- a/src/gfx.rs +++ b/src/gfx.rs @@ -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