use crate::palette::{GbColor, Palette};
use dotzuki_engine::render::Rgba;
pub const BYTES_PER_TILE_ROW: usize = 2;
pub const BYTES_PER_TILE: usize = 16;
pub const TILE_PIXELS: usize = 8;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TileFormat {
Gb2bpp,
Gba4bpp,
FullColor,
}
#[derive(Debug, Clone)]
pub struct Tile {
pub pixels: [[u8; TILE_PIXELS]; TILE_PIXELS],
}
impl Tile {
pub fn from_2bpp(data: &[u8]) -> Self {
assert!(
data.len() >= BYTES_PER_TILE,
"Need {} bytes for a tile, got {}",
BYTES_PER_TILE,
data.len()
);
let mut pixels = [[0u8; TILE_PIXELS]; TILE_PIXELS];
for row in 0..TILE_PIXELS {
let lo = data[row * 2];
let hi = data[row * 2 + 1];
for col in 0..TILE_PIXELS {
let bit = 7 - col;
let color_index = ((hi >> bit) & 1) << 1 | ((lo >> bit) & 1);
pixels[row][col] = color_index;
}
}
Self { pixels }
}
pub fn blank() -> Self {
Self {
pixels: [[0; TILE_PIXELS]; TILE_PIXELS],
}
}
#[inline]
pub fn get(&self, row: usize, col: usize) -> u8 {
self.pixels[row][col]
}
pub fn render_row(&self, row: usize, palette: &Palette) -> [Rgba; TILE_PIXELS] {
let mut out = [Rgba::TRANSPARENT; TILE_PIXELS];
for col in 0..TILE_PIXELS {
let color_idx = GbColor::from_u8(self.pixels[row][col]);
out[col] = palette.color(color_idx);
}
out
}
pub fn flip_y(&self) -> Tile {
let mut pixels = [[0u8; TILE_PIXELS]; TILE_PIXELS];
for row in 0..TILE_PIXELS {
pixels[row] = self.pixels[TILE_PIXELS - 1 - row];
}
Tile { pixels }
}
pub fn flip_x(&self) -> Tile {
let mut pixels = [[0u8; TILE_PIXELS]; TILE_PIXELS];
for row in 0..TILE_PIXELS {
for col in 0..TILE_PIXELS {
pixels[row][col] = self.pixels[row][TILE_PIXELS - 1 - col];
}
}
Tile { pixels }
}
}
#[derive(Debug, Clone)]
pub struct TileSet {
tiles: Vec<Tile>,
format: TileFormat,
rgba_tiles: Vec<RgbaTile>,
}
impl TileSet {
pub fn from_2bpp(data: &[u8]) -> Self {
assert!(
data.len() % BYTES_PER_TILE == 0,
"Tile data length {} is not a multiple of {}",
data.len(),
BYTES_PER_TILE,
);
let count = data.len() / BYTES_PER_TILE;
let mut tiles = Vec::with_capacity(count);
for i in 0..count {
let start = i * BYTES_PER_TILE;
tiles.push(Tile::from_2bpp(&data[start..start + BYTES_PER_TILE]));
}
Self {
tiles,
format: TileFormat::Gb2bpp,
rgba_tiles: Vec::new(),
}
}
pub fn blank(count: usize) -> Self {
Self {
tiles: vec![Tile::blank(); count],
format: TileFormat::Gb2bpp,
rgba_tiles: Vec::new(),
}
}
pub fn get(&self, index: usize) -> &Tile {
if index < self.tiles.len() {
&self.tiles[index]
} else {
static BLANK: Tile = Tile {
pixels: [[0; TILE_PIXELS]; TILE_PIXELS],
};
&BLANK
}
}
pub fn len(&self) -> usize {
self.tiles.len()
}
pub fn is_empty(&self) -> bool {
self.tiles.is_empty()
}
pub fn set(&mut self, index: usize, tile: Tile) {
if index < self.tiles.len() {
self.tiles[index] = tile;
}
}
pub fn load_2bpp_at(&mut self, start_tile: usize, data: &[u8]) {
let count = data.len() / BYTES_PER_TILE;
for i in 0..count {
let tile_idx = start_tile + i;
if tile_idx >= self.tiles.len() {
break;
}
let start = i * BYTES_PER_TILE;
self.tiles[tile_idx] = Tile::from_2bpp(&data[start..start + BYTES_PER_TILE]);
}
}
pub fn from_1bpp(data: &[u8]) -> Self {
let bytes_per_tile_1bpp = 8;
assert!(
data.len() % bytes_per_tile_1bpp == 0,
"1bpp tile data length {} is not a multiple of {}",
data.len(),
bytes_per_tile_1bpp,
);
let count = data.len() / bytes_per_tile_1bpp;
let mut tiles = Vec::with_capacity(count);
for i in 0..count {
let mut pixels = [[0u8; TILE_PIXELS]; TILE_PIXELS];
for row in 0..TILE_PIXELS {
let byte = data[i * bytes_per_tile_1bpp + row];
for col in 0..TILE_PIXELS {
let bit = 7 - col;
pixels[row][col] = if (byte >> bit) & 1 == 1 { 3 } else { 0 };
}
}
tiles.push(Tile { pixels });
}
Self {
tiles,
format: TileFormat::Gb2bpp,
rgba_tiles: Vec::new(),
}
}
pub fn from_4bpp(data: &[u8]) -> Self {
const BYTES_PER_TILE_4BPP: usize = 32;
assert!(
data.len() % BYTES_PER_TILE_4BPP == 0,
"4bpp tile data length {} is not a multiple of {}",
data.len(),
BYTES_PER_TILE_4BPP,
);
let count = data.len() / BYTES_PER_TILE_4BPP;
let mut tiles = Vec::with_capacity(count);
for ti in 0..count {
let base = ti * BYTES_PER_TILE_4BPP;
let mut pixels = [[0u8; TILE_PIXELS]; TILE_PIXELS];
for row in 0..TILE_PIXELS {
let p0_lo = data[base + row * 2];
let p0_hi = data[base + row * 2 + 1];
let p1_lo = data[base + 16 + row * 2];
let p1_hi = data[base + 16 + row * 2 + 1];
for col in 0..TILE_PIXELS {
let bit = 7 - col;
let p0_color = ((p0_hi >> bit) & 1) << 1 | ((p0_lo >> bit) & 1);
let p1_color = ((p1_hi >> bit) & 1) << 1 | ((p1_lo >> bit) & 1);
pixels[row][col] = (p1_color << 2) | p0_color;
}
}
tiles.push(Tile { pixels });
}
Self {
tiles,
format: TileFormat::Gba4bpp,
rgba_tiles: Vec::new(),
}
}
pub fn from_rgba(pixels: &[Rgba], tile_count: usize) -> Self {
assert!(
pixels.len() >= tile_count * TILE_PIXELS * TILE_PIXELS,
"RGBA pixel data length {} is insufficient for {} tiles (need {})",
pixels.len(),
tile_count,
tile_count * TILE_PIXELS * TILE_PIXELS,
);
let mut rgba_tiles = Vec::with_capacity(tile_count);
for ti in 0..tile_count {
let base = ti * TILE_PIXELS * TILE_PIXELS;
let mut rgba_pixels = [[Rgba::TRANSPARENT; TILE_PIXELS]; TILE_PIXELS];
for row in 0..TILE_PIXELS {
for col in 0..TILE_PIXELS {
rgba_pixels[row][col] = pixels[base + row * TILE_PIXELS + col];
}
}
rgba_tiles.push(RgbaTile { pixels: rgba_pixels });
}
Self {
tiles: vec![Tile::blank(); tile_count],
format: TileFormat::FullColor,
rgba_tiles,
}
}
pub fn tile_format(&self) -> TileFormat {
self.format
}
pub fn get_rgba(&self, index: usize) -> Option<&RgbaTile> {
if self.format != TileFormat::FullColor {
return None;
}
self.rgba_tiles.get(index)
}
}
pub fn decode_2bpp_row(lo: u8, hi: u8) -> [u8; TILE_PIXELS] {
let mut out = [0u8; TILE_PIXELS];
for col in 0..TILE_PIXELS {
let bit = 7 - col;
out[col] = ((hi >> bit) & 1) << 1 | ((lo >> bit) & 1);
}
out
}
#[derive(Debug, Clone)]
pub struct RgbaTile {
pub pixels: [[Rgba; TILE_PIXELS]; TILE_PIXELS],
}
impl RgbaTile {
pub fn blank() -> Self {
Self {
pixels: [[Rgba::TRANSPARENT; TILE_PIXELS]; TILE_PIXELS],
}
}
#[inline]
pub fn get(&self, row: usize, col: usize) -> Rgba {
self.pixels[row][col]
}
#[inline]
pub fn render_row(&self, row: usize) -> [Rgba; TILE_PIXELS] {
self.pixels[row]
}
}
#[derive(Debug, Clone)]
pub struct RgbaTileSet {
tiles: Vec<RgbaTile>,
}
static BLANK_RGBA_TILE: RgbaTile = RgbaTile {
pixels: [[Rgba::TRANSPARENT; TILE_PIXELS]; TILE_PIXELS],
};
impl RgbaTileSet {
#[cfg(feature = "gpu")]
pub fn from_rgba_png(png_data: &[u8]) -> Result<Self, String> {
use image::GenericImageView;
let img = image::load_from_memory(png_data)
.map_err(|e| format!("Failed to decode PNG: {}", e))?;
let (w, h) = img.dimensions();
if w % TILE_PIXELS as u32 != 0 || h % TILE_PIXELS as u32 != 0 {
return Err(format!(
"PNG dimensions {}×{} are not multiples of {}",
w, h, TILE_PIXELS
));
}
let rgba = img.to_rgba8();
let tiles_x = (w / TILE_PIXELS as u32) as usize;
let tiles_y = (h / TILE_PIXELS as u32) as usize;
let mut tiles = Vec::with_capacity(tiles_x * tiles_y);
for ty in 0..tiles_y {
for tx in 0..tiles_x {
let mut pixels = [[Rgba::TRANSPARENT; TILE_PIXELS]; TILE_PIXELS];
let base_x = (tx * TILE_PIXELS) as u32;
let base_y = (ty * TILE_PIXELS) as u32;
for row in 0..TILE_PIXELS {
for col in 0..TILE_PIXELS {
let px = rgba.get_pixel(base_x + col as u32, base_y + row as u32);
pixels[row][col] = Rgba::from([px[0], px[1], px[2], px[3]]);
}
}
tiles.push(RgbaTile { pixels });
}
}
Ok(Self { tiles })
}
pub fn get(&self, index: usize) -> &RgbaTile {
if index < self.tiles.len() {
&self.tiles[index]
} else {
&BLANK_RGBA_TILE
}
}
pub fn len(&self) -> usize {
self.tiles.len()
}
pub fn is_empty(&self) -> bool {
self.tiles.is_empty()
}
}