Skip to main content

dotzuki_renderer/
walk_sprite.rs

1//! Full-color overworld walk sprite — the render backend for the engine's generic
2//! [`dotzuki_engine::overworld::actor::OverworldActor`].
3//!
4//! An RGBA sheet sliced into a `rows × cols` grid (**row = facing** down/up/left/right,
5//! **col = walk frame**), produced by the `character-sprite-gen` skill. The actor picks
6//! `(row, col)`; this blits that cell bottom-centred on the foot tile (a 24×32 sprite
7//! stands on a 16×16 tile with its head above). pokered's overworld uses GB OAM sprites
8//! ([`crate::sprite`]) instead — same actor, different painter.
9
10use dotzuki_engine::render::{FrameBuffer, Rgba};
11
12/// A walk-cycle sprite sheet sliced into uniform `frame_w × frame_h` cells.
13pub struct WalkSprite {
14    /// Whole sheet, row-major RGBA, `sheet_w * sheet_h` pixels.
15    pixels: Vec<Rgba>,
16    sheet_w: u32,
17    pub frame_w: u32,
18    pub frame_h: u32,
19    pub rows: u32,
20    pub cols: u32,
21}
22
23impl WalkSprite {
24    /// Build from decoded RGBA pixels (`sheet_w * sheet_h`), sliced into cells.
25    pub fn from_rgba(
26        pixels: Vec<Rgba>,
27        sheet_w: u32,
28        sheet_h: u32,
29        frame_w: u32,
30        frame_h: u32,
31    ) -> Result<Self, String> {
32        if frame_w == 0
33            || frame_h == 0
34            || sheet_w < frame_w
35            || sheet_h < frame_h
36            || pixels.len() != (sheet_w * sheet_h) as usize
37        {
38            return Err(format!(
39                "bad sheet {sheet_w}x{sheet_h} / frame {frame_w}x{frame_h} / {} px",
40                pixels.len()
41            ));
42        }
43        Ok(Self {
44            pixels,
45            sheet_w,
46            frame_w,
47            frame_h,
48            rows: sheet_h / frame_h,
49            cols: sheet_w / frame_w,
50        })
51    }
52
53    /// Load + slice a PNG sprite sheet (`data/gfx/overworld/<id>/sheet.png`).
54    #[cfg(feature = "image-assets")]
55    pub fn load(path: &std::path::Path, frame_w: u32, frame_h: u32) -> Result<Self, String> {
56        let bytes = std::fs::read(path).map_err(|e| format!("read {}: {e}", path.display()))?;
57        let img = image::load_from_memory(&bytes)
58            .map_err(|e| format!("decode {}: {e}", path.display()))?
59            .to_rgba8();
60        let (w, h) = img.dimensions();
61        let pixels = img
62            .pixels()
63            .map(|p| Rgba::new(p.0[0], p.0[1], p.0[2], p.0[3]))
64            .collect();
65        Self::from_rgba(pixels, w, h, frame_w, frame_h)
66    }
67
68    /// Pixel of frame `(row, col)` at frame-local `(fx, fy)`; transparent if out of range.
69    #[inline]
70    pub fn pixel(&self, row: u32, col: u32, fx: u32, fy: u32) -> Rgba {
71        if row >= self.rows || col >= self.cols || fx >= self.frame_w || fy >= self.frame_h {
72            return Rgba::TRANSPARENT;
73        }
74        let x = col * self.frame_w + fx;
75        let y = row * self.frame_h + fy;
76        self.pixels
77            .get((y * self.sheet_w + x) as usize)
78            .copied()
79            .unwrap_or(Rgba::TRANSPARENT)
80    }
81
82    /// Blit frame `(row, col)` bottom-centred on the foot tile whose top-left is
83    /// `(foot_x, foot_y)` in screen pixels (tile size `tile`); the head extends above.
84    /// Transparent pixels are skipped; drawing is clipped to the framebuffer.
85    pub fn draw_on_tile(
86        &self,
87        fb: &mut FrameBuffer,
88        row: u32,
89        col: u32,
90        foot_x: i32,
91        foot_y: i32,
92        tile: i32,
93    ) {
94        let sx = foot_x - (self.frame_w as i32 - tile) / 2;
95        let sy = foot_y - (self.frame_h as i32 - tile);
96        let (w, h) = (fb.width() as i32, fb.height() as i32);
97        for fy in 0..self.frame_h {
98            let py = sy + fy as i32;
99            if py < 0 || py >= h {
100                continue;
101            }
102            for fx in 0..self.frame_w {
103                let c = self.pixel(row, col, fx, fy);
104                if c.a == 0 {
105                    continue;
106                }
107                let px = sx + fx as i32;
108                if px < 0 || px >= w {
109                    continue;
110                }
111                fb.set_pixel(px as u32, py as u32, c);
112            }
113        }
114    }
115}