use crate::tile_meta::CollisionType;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MetatileCell {
pub tile_id: u8,
pub flip_x: bool,
pub flip_y: bool,
pub palette_bank: u8,
}
impl Default for MetatileCell {
fn default() -> Self {
Self {
tile_id: 0,
flip_x: false,
flip_y: false,
palette_bank: 0,
}
}
}
impl MetatileCell {
pub fn from_tile_id(tile_id: u8) -> Self {
Self {
tile_id,
..Default::default()
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CollisionCell {
pub collision: CollisionType,
}
impl CollisionCell {
pub fn new(collision: CollisionType) -> Self {
Self { collision }
}
pub fn passable() -> Self {
Self {
collision: CollisionType::Passable,
}
}
pub fn impassable() -> Self {
Self {
collision: CollisionType::Impassable,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TriggerType {
OnStep,
OnEnter,
OnInteract,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TriggerDef {
pub script_name: String,
pub trigger_type: TriggerType,
}
#[derive(Debug, Clone)]
pub struct MetatileDef {
pub size: (u8, u8),
pub tiles: Vec<MetatileCell>,
pub collision: Vec<CollisionCell>,
pub trigger: Option<TriggerDef>,
pub animation_group: Option<u8>,
pub z_offset: i8,
}
impl MetatileDef {
pub fn new(size: (u8, u8)) -> Self {
assert!(size.0 > 0, "metatile width must be > 0");
assert!(size.1 > 0, "metatile height must be > 0");
let count = (size.0 as usize) * (size.1 as usize);
MetatileDef {
size,
tiles: vec![MetatileCell::default(); count],
collision: vec![CollisionCell::passable(); count],
trigger: None,
animation_group: None,
z_offset: 0,
}
}
pub fn tile_count(&self) -> usize {
(self.size.0 as usize) * (self.size.1 as usize)
}
pub fn from_gb_block(block_data: &[u8]) -> Self {
assert_eq!(
block_data.len(),
16,
"GB block data must be exactly 16 bytes, got {}",
block_data.len()
);
let tiles: Vec<MetatileCell> = block_data
.iter()
.map(|&id| MetatileCell::from_tile_id(id))
.collect();
MetatileDef {
size: (4, 4),
tiles,
collision: vec![CollisionCell::passable(); 16],
trigger: None,
animation_group: None,
z_offset: 0,
}
}
}
#[derive(Debug, Clone)]
pub struct MetatileRegistry {
pub defs: Vec<MetatileDef>,
}
impl MetatileRegistry {
pub fn new() -> Self {
MetatileRegistry { defs: Vec::new() }
}
pub fn add_def(&mut self, def: MetatileDef) -> usize {
let index = self.defs.len();
self.defs.push(def);
index
}
pub fn get(&self, index: usize) -> Option<&MetatileDef> {
self.defs.get(index)
}
pub fn from_gb_blocksets(bst_data: &[u8]) -> Self {
assert!(
bst_data.len() % 16 == 0,
"GB blockset data length must be a multiple of 16, got {}",
bst_data.len()
);
let mut registry = MetatileRegistry::new();
for chunk in bst_data.chunks_exact(16) {
registry.add_def(MetatileDef::from_gb_block(chunk));
}
registry
}
}
impl Default for MetatileRegistry {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_creates_empty_metatile() {
let m = MetatileDef::new((2, 3));
assert_eq!(m.size, (2, 3));
assert_eq!(m.tile_count(), 6);
assert_eq!(m.tiles.len(), 6);
assert_eq!(m.collision.len(), 6);
assert!(m.trigger.is_none());
assert!(m.animation_group.is_none());
assert_eq!(m.z_offset, 0);
for tile in &m.tiles {
assert_eq!(tile.tile_id, 0);
assert!(!tile.flip_x);
assert!(!tile.flip_y);
assert_eq!(tile.palette_bank, 0);
}
for cell in &m.collision {
assert_eq!(cell.collision, CollisionType::Passable);
}
}
#[test]
fn test_new_2x2() {
let m = MetatileDef::new((2, 2));
assert_eq!(m.size, (2, 2));
assert_eq!(m.tile_count(), 4);
}
#[test]
fn test_new_4x4() {
let m = MetatileDef::new((4, 4));
assert_eq!(m.size, (4, 4));
assert_eq!(m.tile_count(), 16);
}
#[test]
#[should_panic(expected = "metatile width must be > 0")]
fn test_new_zero_width_panics() {
MetatileDef::new((0, 4));
}
#[test]
#[should_panic(expected = "metatile height must be > 0")]
fn test_new_zero_height_panics() {
MetatileDef::new((4, 0));
}
#[test]
fn test_from_gb_block_full_16_bytes() {
let data: [u8; 16] = [
0x00, 0x01, 0x02, 0x03, 0x10, 0x11, 0x12, 0x13, 0x20, 0x21, 0x22, 0x23, 0x30, 0x31, 0x32, 0x33, ];
let m = MetatileDef::from_gb_block(&data);
assert_eq!(m.size, (4, 4));
assert_eq!(m.tile_count(), 16);
assert_eq!(m.z_offset, 0);
assert!(m.trigger.is_none());
assert_eq!(m.tiles[0].tile_id, 0x00);
assert_eq!(m.tiles[1].tile_id, 0x01);
assert_eq!(m.tiles[2].tile_id, 0x02);
assert_eq!(m.tiles[3].tile_id, 0x03);
assert_eq!(m.tiles[4].tile_id, 0x10);
assert_eq!(m.tiles[15].tile_id, 0x33);
for tile in &m.tiles {
assert!(!tile.flip_x);
assert!(!tile.flip_y);
assert_eq!(tile.palette_bank, 0);
}
}
#[test]
fn test_from_gb_block_all_zeros() {
let data = [0u8; 16];
let m = MetatileDef::from_gb_block(&data);
assert_eq!(m.size, (4, 4));
for tile in &m.tiles {
assert_eq!(tile.tile_id, 0);
}
}
#[test]
fn test_from_gb_block_with_high_tile_ids() {
let mut data = [0u8; 16];
for (i, b) in data.iter_mut().enumerate() {
*b = 0xA0 + i as u8;
}
let m = MetatileDef::from_gb_block(&data);
assert_eq!(m.tiles[0].tile_id, 0xA0);
assert_eq!(m.tiles[15].tile_id, 0xAF);
}
#[test]
#[should_panic(expected = "GB block data must be exactly 16 bytes")]
fn test_from_gb_block_too_short_panics() {
MetatileDef::from_gb_block(&[0u8; 8]);
}
#[test]
#[should_panic(expected = "GB block data must be exactly 16 bytes")]
fn test_from_gb_block_too_long_panics() {
MetatileDef::from_gb_block(&[0u8; 32]);
}
#[test]
#[should_panic(expected = "GB block data must be exactly 16 bytes")]
fn test_from_gb_block_empty_panics() {
MetatileDef::from_gb_block(&[]);
}
#[test]
fn test_tile_count_various_sizes() {
assert_eq!(MetatileDef::new((1, 1)).tile_count(), 1);
assert_eq!(MetatileDef::new((2, 2)).tile_count(), 4);
assert_eq!(MetatileDef::new((3, 3)).tile_count(), 9);
assert_eq!(MetatileDef::new((4, 4)).tile_count(), 16);
assert_eq!(MetatileDef::new((2, 5)).tile_count(), 10);
}
#[test]
fn test_metatile_with_trigger() {
let mut m = MetatileDef::new((2, 2));
m.trigger = Some(TriggerDef {
script_name: "heal_party".into(),
trigger_type: TriggerType::OnStep,
});
let t = m.trigger.as_ref().unwrap();
assert_eq!(t.script_name, "heal_party");
assert_eq!(t.trigger_type, TriggerType::OnStep);
}
#[test]
fn test_metatile_with_animation_group() {
let mut m = MetatileDef::new((2, 2));
m.animation_group = Some(7);
assert_eq!(m.animation_group, Some(7));
}
#[test]
fn test_metatile_z_offset() {
let mut m = MetatileDef::new((2, 2));
m.z_offset = -4;
assert_eq!(m.z_offset, -4);
m.z_offset = 8;
assert_eq!(m.z_offset, 8);
}
#[test]
fn test_registry_new_is_empty() {
let r = MetatileRegistry::new();
assert!(r.defs.is_empty());
assert!(r.get(0).is_none());
}
#[test]
fn test_registry_add_and_get() {
let mut r = MetatileRegistry::new();
let m1 = MetatileDef::new((2, 2));
let m2 = MetatileDef::new((3, 3));
let idx1 = r.add_def(m1);
let idx2 = r.add_def(m2);
assert_eq!(idx1, 0);
assert_eq!(idx2, 1);
assert!(r.get(0).is_some());
assert!(r.get(1).is_some());
assert!(r.get(2).is_none());
assert_eq!(r.get(0).unwrap().size, (2, 2));
assert_eq!(r.get(1).unwrap().size, (3, 3));
}
#[test]
fn test_registry_get_out_of_bounds() {
let r = MetatileRegistry::new();
assert!(r.get(0).is_none());
assert!(r.get(100).is_none());
}
#[test]
fn test_registry_default() {
let r = MetatileRegistry::default();
assert!(r.defs.is_empty());
}
#[test]
fn test_from_gb_blocksets_single_block() {
let data: Vec<u8> = (0..16).collect(); let registry = MetatileRegistry::from_gb_blocksets(&data);
assert_eq!(registry.defs.len(), 1);
let m = registry.get(0).unwrap();
assert_eq!(m.size, (4, 4));
assert_eq!(m.tiles[0].tile_id, 0x00);
assert_eq!(m.tiles[15].tile_id, 0x0F);
}
#[test]
fn test_from_gb_blocksets_multiple_blocks() {
let data: Vec<u8> = (0..32).collect();
let registry = MetatileRegistry::from_gb_blocksets(&data);
assert_eq!(registry.defs.len(), 2);
let m0 = registry.get(0).unwrap();
assert_eq!(m0.tiles[0].tile_id, 0x00);
assert_eq!(m0.tiles[15].tile_id, 0x0F);
let m1 = registry.get(1).unwrap();
assert_eq!(m1.tiles[0].tile_id, 0x10);
assert_eq!(m1.tiles[15].tile_id, 0x1F);
}
#[test]
fn test_from_gb_blocksets_many_blocks() {
let data = vec![0u8; 256 * 16];
let registry = MetatileRegistry::from_gb_blocksets(&data);
assert_eq!(registry.defs.len(), 256);
for def in ®istry.defs {
assert_eq!(def.size, (4, 4));
assert_eq!(def.tile_count(), 16);
}
}
#[test]
fn test_from_gb_blocksets_empty() {
let registry = MetatileRegistry::from_gb_blocksets(&[]);
assert!(registry.defs.is_empty());
}
#[test]
#[should_panic(expected = "GB blockset data length must be a multiple of 16")]
fn test_from_gb_blocksets_unaligned_panics() {
MetatileRegistry::from_gb_blocksets(&[0u8; 10]);
}
#[test]
#[should_panic(expected = "GB blockset data length must be a multiple of 16")]
fn test_from_gb_blocksets_odd_length_panics() {
MetatileRegistry::from_gb_blocksets(&[0u8; 17]);
}
#[test]
fn test_collision_cell_passable() {
let c = CollisionCell::passable();
assert_eq!(c.collision, CollisionType::Passable);
}
#[test]
fn test_collision_cell_impassable() {
let c = CollisionCell::impassable();
assert_eq!(c.collision, CollisionType::Impassable);
}
#[test]
fn test_collision_cell_custom() {
let c = CollisionCell::new(CollisionType::Water);
assert_eq!(c.collision, CollisionType::Water);
}
#[test]
fn test_tilemap_entry_default() {
let t = MetatileCell::default();
assert_eq!(t.tile_id, 0);
assert!(!t.flip_x);
assert!(!t.flip_y);
assert_eq!(t.palette_bank, 0);
}
#[test]
fn test_tilemap_entry_from_tile_id() {
let t = MetatileCell::from_tile_id(42);
assert_eq!(t.tile_id, 42);
assert!(!t.flip_x);
assert!(!t.flip_y);
}
#[test]
fn test_tilemap_entry_with_flips() {
let t = MetatileCell {
tile_id: 0x80,
flip_x: true,
flip_y: false,
palette_bank: 3,
};
assert!(t.flip_x);
assert!(!t.flip_y);
assert_eq!(t.palette_bank, 3);
}
#[test]
fn test_trigger_type_equality() {
assert_eq!(TriggerType::OnStep, TriggerType::OnStep);
assert_ne!(TriggerType::OnStep, TriggerType::OnInteract);
}
#[test]
fn test_trigger_def() {
let td = TriggerDef {
script_name: "warp_to_start_town".into(),
trigger_type: TriggerType::OnEnter,
};
assert_eq!(td.script_name, "warp_to_start_town");
assert_eq!(td.trigger_type, TriggerType::OnEnter);
}
}