diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 00000000..11700c7c --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +jobs = 2 diff --git a/Cargo.toml b/Cargo.toml index 62f95c30..b9b799d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,9 @@ rand = "0.8" serde_json = { version = "1.0" } tiled = { version = "0.14.0", default-features = false } thiserror = { version = "2.0" } +approx = { version = "0.5" } +proptest = "1.7" +glam = "0.30" [dev-dependencies.bevy] version = "0.16.0" diff --git a/examples/helpers/ldtk.rs b/examples/helpers/ldtk.rs index bea87496..85551cfe 100644 --- a/examples/helpers/ldtk.rs +++ b/examples/helpers/ldtk.rs @@ -4,7 +4,7 @@ use bevy_ecs_tilemap::{ map::{TilemapId, TilemapSize, TilemapTexture, TilemapTileSize}, tiles::{TileBundle, TilePos, TileStorage, TileTextureIndex}, }; -use std::{collections::HashMap, io::ErrorKind}; +use std::collections::HashMap; use thiserror::Error; use bevy::{asset::io::Reader, reflect::TypePath}; @@ -71,10 +71,7 @@ impl AssetLoader for LdtkLoader { reader.read_to_end(&mut bytes).await?; let project: ldtk_rust::Project = serde_json::from_slice(&bytes).map_err(|e| { - std::io::Error::new( - ErrorKind::Other, - format!("Could not read contents of Ldtk map: {e}"), - ) + std::io::Error::other(format!("Could not read contents of Ldtk map: {e}")) })?; let dependencies: Vec<(i64, AssetPath)> = project .defs diff --git a/examples/helpers/tiled.rs b/examples/helpers/tiled.rs index 50ee6f5d..0763652d 100644 --- a/examples/helpers/tiled.rs +++ b/examples/helpers/tiled.rs @@ -12,7 +12,7 @@ // * When the 'atlas' feature is enabled tilesets using a collection of images will be skipped. // * Only finite tile layers are loaded. Infinite tile layers and object layers will be skipped. -use std::io::{Cursor, ErrorKind}; +use std::io::Cursor; use std::path::Path; use std::sync::Arc; @@ -118,9 +118,9 @@ impl AssetLoader for TiledLoader { tiled::DefaultResourceCache::new(), BytesResourceReader::new(&bytes), ); - let map = loader.load_tmx_map(load_context.path()).map_err(|e| { - std::io::Error::new(ErrorKind::Other, format!("Could not load TMX map: {e}")) - })?; + let map = loader + .load_tmx_map(load_context.path()) + .map_err(|e| std::io::Error::other(format!("Could not load TMX map: {e}")))?; let mut tilemap_textures = HashMap::default(); #[cfg(not(feature = "atlas"))] diff --git a/mintty.exe.stackdump b/mintty.exe.stackdump new file mode 100644 index 00000000..4b351647 --- /dev/null +++ b/mintty.exe.stackdump @@ -0,0 +1,23 @@ +Exception: STATUS_ACCESS_VIOLATION at rip=0010042165E +rax=0000000000000001 rbx=00000000FFFFC1B0 rcx=0000000000000000 +rdx=00000000FFFFCE00 rsi=0000000000000000 rdi=000000000000008F +r8 =000000000000005A r9 =000000000000005E r10=0000000100000000 +r11=00000001004216A8 r12=0000000000000002 r13=00000000FFFFC1B0 +r14=000000080010E650 r15=0000000000000000 +rbp=00000000FFFFC270 rsp=00000000FFFFC150 +program=C:\Users\Alex Work\AppData\Local\Programs\Git\usr\bin\mintty.exe, pid 905, thread main +cs=0033 ds=002B es=002B fs=0053 gs=002B ss=002B +Stack trace: +Frame Function Args +000FFFFC270 0010042165E (000FFFFC1E4, 00000000000, 00100426F2C, 00000000000) +000FFFFC270 00100422355 (00100427609, 6D6178656D305B1B, 7865745C73656C70, 008000A52A0) +000FFFFC270 0010041A6C4 (001004031A3, FFFFFFFF00000000, 00000000000, 001004E8600) +00000022510 0010042967A (00000000000, 00000000CE6, 001004E8600, 001004E8600) +00000022510 0010042BD70 (001004E300A, 001004E8F40, 00000000018, 000FFFFC3C0) +00000022510 0010042E238 (001004E30E0, 00000000100, 7FF886D6E684, 00000000000) +00000001000 00100404C33 (000FFFFC550, 008000434A0, 7FF886D69680, 00800000001) +000FFFFC550 00100460CB1 (00000000014, 00800042DD8, 000FFFFCB80, 000FFFFCC93) +000FFFFCCE0 0018004B0FB (00000000000, 00000000000, 00000000000, 00000000000) +000FFFFCDA0 00180048A2A (00000000000, 00000000000, 00000000000, 00000000000) +000FFFFCE50 00180048AEC (00000000000, 00000000000, 00000000000, 00000000000) +End of stack trace diff --git a/src/anchor.rs b/src/anchor.rs index 65fef7a4..a4d7ef0c 100644 --- a/src/anchor.rs +++ b/src/anchor.rs @@ -73,3 +73,53 @@ impl TilemapAnchor { } } } + +#[cfg(test)] +mod tests { + use super::*; + + fn fixed_params() -> (TilemapSize, TilemapGridSize, TilemapTileSize, TilemapType) { + ( + TilemapSize { x: 4, y: 3 }, + TilemapGridSize { x: 1.0, y: 1.0 }, + TilemapTileSize { x: 1.0, y: 1.0 }, + TilemapType::Square, + ) + } + + #[test] + fn none_anchor_is_zero() { + let (map, grid, tile, ty) = fixed_params(); + assert_eq!( + TilemapAnchor::None.as_offset(&map, &grid, &tile, &ty), + Vec2::ZERO + ); + } + + #[test] + fn center_equals_custom_zero() { + let (map, grid, tile, ty) = fixed_params(); + assert_eq!( + TilemapAnchor::Center.as_offset(&map, &grid, &tile, &ty), + TilemapAnchor::Custom(Vec2::ZERO).as_offset(&map, &grid, &tile, &ty) + ); + } + + #[test] + fn top_left_equals_custom_top_left() { + let (map, grid, tile, ty) = fixed_params(); + assert_eq!( + TilemapAnchor::TopLeft.as_offset(&map, &grid, &tile, &ty), + TilemapAnchor::Custom(Vec2::new(-0.5, 0.5)).as_offset(&map, &grid, &tile, &ty) + ); + } + + #[test] + fn top_right_equals_custom_top_right() { + let (map, grid, tile, ty) = fixed_params(); + assert_eq!( + TilemapAnchor::TopRight.as_offset(&map, &grid, &tile, &ty), + TilemapAnchor::Custom(Vec2::new(0.5, 0.5)).as_offset(&map, &grid, &tile, &ty) + ); + } +} diff --git a/src/array_texture_preload.rs b/src/array_texture_preload.rs index ad0011ae..bfba7ef4 100644 --- a/src/array_texture_preload.rs +++ b/src/array_texture_preload.rs @@ -83,3 +83,31 @@ pub(crate) fn extract( } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn tilemap_array_texture_default_is_sane() { + // Act + let tex = TilemapArrayTexture::default(); + + assert!(tex.filter.is_none(), "Filter should start unset (None)"); + assert_eq!( + tex.texture, + TilemapTexture::default(), + "Texture default mismatch" + ); + assert_eq!( + tex.tile_size, + TilemapTileSize::default(), + "Tile size default mismatch" + ); + assert_eq!( + tex.tile_spacing, + TilemapSpacing::default(), + "Tile spacing default mismatch" + ); + } +} diff --git a/src/helpers/filling.rs b/src/helpers/filling.rs index f2c59415..deb6b218 100644 --- a/src/helpers/filling.rs +++ b/src/helpers/filling.rs @@ -36,7 +36,7 @@ pub fn fill_tilemap( /// Fills a rectangular region with the given tile. /// /// The rectangular region is defined by an `origin` in [`TilePos`], and a -/// `size` in tiles ([`TilemapSize`]). +/// `size` in tiles ([`TilemapSize`]). pub fn fill_tilemap_rect( texture_index: TileTextureIndex, origin: TilePos, @@ -70,7 +70,7 @@ pub fn fill_tilemap_rect( /// Fills a rectangular region with colored versions of the given tile. /// /// The rectangular region is defined by an `origin` in [`TilePos`], and a -/// `size` in tiles ([`TilemapSize`]). +/// `size` in tiles ([`TilemapSize`]). pub fn fill_tilemap_rect_color( texture_index: TileTextureIndex, origin: TilePos, @@ -134,8 +134,8 @@ pub fn generate_hex_ring(origin: AxialPos, radius: u32) -> Vec { /// Generates a vector of hex positions that form a hexagon of given `radius` around the specified /// `origin`. pub fn generate_hexagon(origin: AxialPos, radius: u32) -> Vec { - let mut hexagon = Vec::with_capacity(1 + (6 * radius * (radius + 1) / 2) as usize); - for r in 0..(radius + 1) { + let mut hexagon = Vec::with_capacity(1 + (((6 * radius * (radius + 1)) / 2) as usize)); + for r in 0..radius + 1 { hexagon.extend(generate_hex_ring(origin, r)); } hexagon @@ -174,7 +174,56 @@ pub fn fill_tilemap_hexagon( ..Default::default() }) .id(); - tile_storage.checked_set(&tile_pos, tile_entity) + tile_storage.checked_set(&tile_pos, tile_entity); } }); } + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::HashSet; + + fn axial_distance(a: AxialPos, b: AxialPos) -> u32 { + let dq = (a.q - b.q).abs() as u32; + let dr = (a.r - b.r).abs() as u32; + let ds = (a.q + a.r - (b.q + b.r)).abs() as u32; + (dq + dr + ds) / 2 + } + + #[test] + fn ring_radius_zero_is_origin_only() { + let origin = AxialPos::new(0, 0); + let ring = generate_hex_ring(origin, 0); + assert_eq!(ring, vec![origin]); + } + + #[test] + fn ring_has_correct_length_and_radius() { + let origin = AxialPos::new(0, 0); + for r in 1..=4 { + let ring = generate_hex_ring(origin, r); + assert_eq!(ring.len() as u32, r * 6, "radius {r}"); + // no duplicates & all exactly r away + let uniq: HashSet<_> = ring.iter().cloned().collect(); + assert_eq!(uniq.len(), ring.len(), "radius {r} contains duplicates"); + assert!(ring.iter().all(|p| axial_distance(*p, origin) == r)); + } + } + + #[test] + fn hexagon_area_matches_formula_and_contains_rings() { + let origin = AxialPos::new(0, 0); + for r in 0..=4 { + let hex = generate_hexagon(origin, r); + let expected = 1 + 3 * r * (r + 1); + assert_eq!(hex.len() as u32, expected, "radius {r}"); + // make sure every inner ring element is inside the hexagon + for r_inner in 0..=r { + for p in generate_hex_ring(origin, r_inner) { + assert!(hex.contains(&p), "ring {r_inner} not fully inside hex {r}"); + } + } + } + } +} diff --git a/src/helpers/geometry.rs b/src/helpers/geometry.rs index 5cde1b05..b86f4513 100644 --- a/src/helpers/geometry.rs +++ b/src/helpers/geometry.rs @@ -2,6 +2,8 @@ use crate::map::TilemapType; use crate::tiles::TilePos; use crate::{TilemapAnchor, TilemapGridSize, TilemapSize, TilemapTileSize, Transform}; +// Deprecated. Skipping tests. + /// Calculates a [`Transform`] for a tilemap that places it so that its center is at /// `(0.0, 0.0, z)` in world space. #[deprecated(since = "0.15.1", note = "please use `TilemapAnchor::Center` instead")] @@ -29,5 +31,5 @@ pub fn get_tilemap_center_transform( let diff = high - low; - Transform::from_xyz(-diff.x / 2., -diff.y / 2., z) + Transform::from_xyz(-diff.x / 2.0, -diff.y / 2.0, z) } diff --git a/src/helpers/hex_grid/axial.rs b/src/helpers/hex_grid/axial.rs index 241e76da..ee56d2a7 100644 --- a/src/helpers/hex_grid/axial.rs +++ b/src/helpers/hex_grid/axial.rs @@ -509,3 +509,81 @@ impl From for FractionalAxialPos { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::helpers::hex_grid::offset::{ColEvenPos, ColOddPos, RowEvenPos, RowOddPos}; + + // ---------- small private helper ----------------------------------------------------------- + #[test] + fn ceiled_div_by_2_behaves_as_expected() { + // negative inputs + assert_eq!(super::ceiled_division_by_2(-3), -2); + assert_eq!(super::ceiled_division_by_2(-2), -1); + assert_eq!(super::ceiled_division_by_2(-1), -1); + + // zero & positives + assert_eq!(super::ceiled_division_by_2(0), 0); + assert_eq!(super::ceiled_division_by_2(1), 1); + assert_eq!(super::ceiled_division_by_2(2), 1); + assert_eq!(super::ceiled_division_by_2(3), 2); + } + + // ---------- axial <-> offset round-trips --------------------------------------------------- + const SAMPLE_AXIALS: &[AxialPos] = &[ + AxialPos::new(0, 0), + AxialPos::new(2, -1), + AxialPos::new(-4, 5), + AxialPos::new(7, -3), + ]; + + #[test] + fn round_trip_row_odd() { + for &ax in SAMPLE_AXIALS { + let back: AxialPos = RowOddPos::from(ax).into(); + assert_eq!(ax, back, "failed on {ax:?}"); + } + } + + #[test] + fn round_trip_row_even() { + for &ax in SAMPLE_AXIALS { + let back: AxialPos = RowEvenPos::from(ax).into(); + assert_eq!(ax, back, "failed on {ax:?}"); + } + } + + #[test] + fn round_trip_col_odd() { + for &ax in SAMPLE_AXIALS { + let back: AxialPos = ColOddPos::from(ax).into(); + assert_eq!(ax, back, "failed on {ax:?}"); + } + } + + #[test] + fn round_trip_col_even() { + for &ax in SAMPLE_AXIALS { + let back: AxialPos = ColEvenPos::from(ax).into(); + assert_eq!(ax, back, "failed on {ax:?}"); + } + } + + // ---------- magnitude & distance ----------------------------------------------------------- + #[test] + fn magnitude_matches_cube_formula() { + assert_eq!(AxialPos::new(0, 0).magnitude(), 0); + assert_eq!(AxialPos::new(1, 0).magnitude(), 1); + assert_eq!(AxialPos::new(1, -1).magnitude(), 1); + assert_eq!(AxialPos::new(2, -1).magnitude(), 2); // (2,-1,-1) → (2+1+1)/2 = 2 + } + + #[test] + fn distance_is_symmetric_and_correct() { + let a = AxialPos::new(2, -1); + let b = AxialPos::new(-1, 3); + assert_eq!(a.distance_from(&b), b.distance_from(&a)); + assert_eq!(a.distance_from(&b), 4); + } +} diff --git a/src/helpers/hex_grid/cube.rs b/src/helpers/hex_grid/cube.rs index dbae7e31..2e3343f2 100644 --- a/src/helpers/hex_grid/cube.rs +++ b/src/helpers/hex_grid/cube.rs @@ -175,3 +175,45 @@ impl FractionalCubePos { CubePos { q, r, s } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::helpers::hex_grid::axial::AxialPos; + + #[test] + fn axial_to_cube_preserves_identity() { + let axial = AxialPos { q: 2, r: -3 }; + let cube: CubePos = axial.into(); + assert_eq!(cube.q + cube.r + cube.s, 0, "q + r + s must be 0"); + } + + #[test] + fn cube_add_sub_behave_like_vectors() { + let a = CubePos::new(1, -2, 1); + let b = CubePos::new(2, 1, -3); + + assert_eq!(a + b, CubePos::new(3, -1, -2)); + assert_eq!(a - b, CubePos::new(-1, -3, 4)); + // Add with reference + assert_eq!(a + &b, CubePos::new(3, -1, -2)); + } + + #[test] + fn scalar_multiplication_works_for_i32_and_u32() { + let v = CubePos::new(1, -2, 1); + assert_eq!(3_i32 * v, CubePos::new(3, -6, 3)); + assert_eq!(3_u32 * v, CubePos::new(3, -6, 3)); + } + + #[test] + fn fractional_rounding_gives_containing_hex() { + // Point very close to (1,-1,0) but with largest delta on q + let frac = FractionalCubePos::new(1.49, -1.1, -0.39); + assert_eq!(frac.round(), CubePos::new(1, -1, 0)); + + // Point inside origin hex + let frac2 = FractionalCubePos::new(0.2, -0.1, -0.1); + assert_eq!(frac2.round(), CubePos::new(0, 0, 0)); + } +} diff --git a/src/helpers/hex_grid/neighbors.rs b/src/helpers/hex_grid/neighbors.rs index 1a43fca2..0d9d241f 100644 --- a/src/helpers/hex_grid/neighbors.rs +++ b/src/helpers/hex_grid/neighbors.rs @@ -98,7 +98,7 @@ impl Add for HexDirection { type Output = HexDirection; fn add(self, rhs: u32) -> Self::Output { - ((self as usize) + rhs as usize).into() + ((self as usize) + (rhs as usize)).into() } } @@ -130,7 +130,7 @@ impl Sub for HexDirection { type Output = HexDirection; fn sub(self, rhs: u32) -> Self::Output { - ((self as usize) - rhs as usize).into() + ((self as usize) - (rhs as usize)).into() } } @@ -534,3 +534,36 @@ impl HexNeighbors { self.and_then_ref(f) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn axial_conversion_matches_const_offsets() { + for (idx, expected) in HEX_OFFSETS.iter().enumerate() { + let dir = HEX_DIRECTIONS[idx]; + assert_eq!(AxialPos::from(dir), *expected); + assert_eq!(AxialPos::from(&dir), *expected); + } + } + + #[test] + fn hex_neighbors_get_set_iter_work() { + let mut neigh: HexNeighbors = HexNeighbors::default(); + // Initially everything is None + assert!(neigh.iter().next().is_none()); + + // Set two directions + neigh.set(HexDirection::One, 10); + neigh.set(HexDirection::Four, 20); + + assert_eq!(neigh.get(HexDirection::One), Some(&10)); + assert_eq!(neigh.get(HexDirection::Four), Some(&20)); + assert_eq!(neigh.get(HexDirection::Zero), None); + + // Iter should yield exactly the two we inserted, order guaranteed by HEX_DIRECTIONS + let collected: Vec = neigh.iter().copied().collect(); + assert_eq!(collected, vec![10, 20]); + } +} diff --git a/src/helpers/hex_grid/offset.rs b/src/helpers/hex_grid/offset.rs index e8cb3f6b..d84e27db 100644 --- a/src/helpers/hex_grid/offset.rs +++ b/src/helpers/hex_grid/offset.rs @@ -369,3 +369,52 @@ impl From<&TilePos> for ColEvenPos { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::TilemapSize; + + // Helper: a 10 × 10 map that fits all positive test coordinates. + fn test_map_size() -> TilemapSize { + TilemapSize { x: 10, y: 10 } + } + + #[test] + fn tilepos_round_trip_row_odd() { + let tp = TilePos { x: 3, y: 7 }; + let pos = RowOddPos::from(&tp); + assert_eq!(pos.as_tile_pos_unchecked(), tp); + assert_eq!(pos.as_tile_pos_given_map_size(&test_map_size()), Some(tp)); + } + + #[test] + fn tilepos_round_trip_row_even() { + let tp = TilePos { x: 4, y: 1 }; + let pos = RowEvenPos::from(&tp); + assert_eq!(pos.as_tile_pos_unchecked(), tp); + assert_eq!(pos.as_tile_pos_given_map_size(&test_map_size()), Some(tp)); + } + + #[test] + fn tilepos_round_trip_col_odd() { + let tp = TilePos { x: 2, y: 6 }; + let pos = ColOddPos::from(&tp); + assert_eq!(pos.as_tile_pos_unchecked(), tp); + assert_eq!(pos.as_tile_pos_given_map_size(&test_map_size()), Some(tp)); + } + + #[test] + fn tilepos_round_trip_col_even() { + let tp = TilePos { x: 9, y: 0 }; + let pos = ColEvenPos::from(&tp); + assert_eq!(pos.as_tile_pos_unchecked(), tp); + assert_eq!(pos.as_tile_pos_given_map_size(&test_map_size()), Some(tp)); + } + + #[test] + fn negative_coords_are_rejected() { + let bad = RowOddPos::new(-1, 4); + assert!(bad.as_tile_pos_given_map_size(&test_map_size()).is_none()); + } +} diff --git a/src/helpers/projection.rs b/src/helpers/projection.rs index 1caf1550..966d9fc0 100644 --- a/src/helpers/projection.rs +++ b/src/helpers/projection.rs @@ -80,8 +80,8 @@ impl TilePos { let pos = world_pos - offset; match map_type { TilemapType::Square => { - let x = ((pos.x / grid_size.x) + 0.5).floor() as i32; - let y = ((pos.y / grid_size.y) + 0.5).floor() as i32; + let x = (pos.x / grid_size.x + 0.5).floor() as i32; + let y = (pos.y / grid_size.y + 0.5).floor() as i32; TilePos::from_i32_pair(x, y, map_size) } @@ -112,3 +112,42 @@ impl TilePos { } } } + +#[cfg(test)] +mod tests { + use super::*; + use bevy::math::Vec2; + + #[test] + fn from_i32_pair_negative_returns_none() { + let map_size = TilemapSize { x: 10, y: 10 }; + // negative coordinates are invalid + assert!(TilePos::from_i32_pair(-1, 0, &map_size).is_none()); + assert!(TilePos::from_i32_pair(0, -1, &map_size).is_none()); + } + + #[test] + fn from_i32_pair_out_of_bounds_returns_none() { + let map_size = TilemapSize { x: 10, y: 10 }; + // coordinates equal to or larger than map_size are out-of-bounds + assert!(TilePos::from_i32_pair(10, 0, &map_size).is_none()); + assert!(TilePos::from_i32_pair(0, 10, &map_size).is_none()); + } + + #[test] + fn from_i32_pair_valid_returns_some() { + let map_size = TilemapSize { x: 10, y: 10 }; + let pos = TilePos::from_i32_pair(3, 7, &map_size).unwrap(); + assert_eq!(pos, TilePos { x: 3, y: 7 }); + } + + #[test] + fn center_in_world_unanchored_square() { + // (3, 1) on a 32×32 square grid should be at (96, 32) + let tile_pos = TilePos { x: 3, y: 1 }; + let grid_size = TilemapGridSize { x: 32.0, y: 32.0 }; + let center = tile_pos.center_in_world_unanchored(&grid_size, &TilemapType::Square); + + assert_eq!(center, Vec2::new(96.0, 32.0)); + } +} diff --git a/src/helpers/square_grid/diamond.rs b/src/helpers/square_grid/diamond.rs index 51f67f4e..f1ac643e 100644 --- a/src/helpers/square_grid/diamond.rs +++ b/src/helpers/square_grid/diamond.rs @@ -215,3 +215,31 @@ impl TilePos { .as_tile_pos(map_size) } } + +#[cfg(test)] +mod tests { + use super::*; + + fn grid32() -> TilemapGridSize { + TilemapGridSize { x: 32.0, y: 32.0 } + } + + #[test] + fn arithmetic_operators_work() { + let a = DiamondPos::new(2, -3); + let b = DiamondPos::new(-5, 7); + + assert_eq!(a + b, DiamondPos::new(-3, 4)); + assert_eq!(a - b, DiamondPos::new(7, -10)); + assert_eq!(3 * b, DiamondPos::new(-15, 21)); + } + + #[test] + fn world_roundtrip_through_center() { + let grid = grid32(); + let tile = DiamondPos::new(7, 4); + let world = tile.center_in_world(&grid); + let tile_back = DiamondPos::from_world_pos(&world, &grid); + assert_eq!(tile_back, tile); + } +} diff --git a/src/helpers/square_grid/mod.rs b/src/helpers/square_grid/mod.rs index 3f53fddf..25ece6d3 100644 --- a/src/helpers/square_grid/mod.rs +++ b/src/helpers/square_grid/mod.rs @@ -192,3 +192,84 @@ impl TilePos { .as_tile_pos(map_size) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::helpers::square_grid::neighbors::SquareDirection; + use crate::{TilemapGridSize, TilemapSize, tiles::TilePos}; + + fn gs() -> TilemapGridSize { + TilemapGridSize { x: 32.0, y: 32.0 } + } + + #[test] + fn add_sub_mul_work() { + let a = SquarePos::new(2, -3); + let b = SquarePos::new(-4, 8); + + assert_eq!(a + b, SquarePos::new(-2, 5)); + assert_eq!(a - b, SquarePos::new(6, -11)); + assert_eq!(3 * a, SquarePos::new(6, -9)); + } + + #[test] + fn conversions_are_correct() { + let tp = TilePos::new(4, 7); + assert_eq!(SquarePos::from(&tp), SquarePos::new(4, 7)); + + let dp = DiamondPos { x: -2, y: 5 }; + assert_eq!(SquarePos::from(dp), SquarePos::new(-2, 5)); + + let sp = StaggeredPos { x: 3, y: 1 }; + assert_eq!(SquarePos::from(sp), SquarePos::new(3, 4)); + } + + #[test] + fn project_and_unproject_roundtrip() { + let pos = SquarePos::new(5, 9); + let world = pos.center_in_world(&gs()); + assert_eq!(SquarePos::from_world_pos(&world, &gs()), pos); + } + + #[test] + fn corners_in_world_match_offset_helpers() { + let pos = SquarePos::new(0, 0); + for dir in [ + SquareDirection::NorthWest, + SquareDirection::NorthEast, + SquareDirection::SouthWest, + SquareDirection::SouthEast, + ] { + let by_center = pos.corner_in_world(dir, &gs()); + let by_offset = + pos.center_in_world(&gs()) + SquarePos::corner_offset_in_world(dir, &gs()); + assert_eq!(by_center, by_offset); + } + } + + #[test] + fn as_tile_pos_respects_bounds() { + let map_size = TilemapSize { x: 10, y: 10 }; + let inside = SquarePos::new(3, 9); + assert_eq!(inside.as_tile_pos(&map_size), Some(TilePos::new(3, 9))); + + let neg = SquarePos::new(-1, 0); + let out = SquarePos::new(10, 0); + assert!(neg.as_tile_pos(&map_size).is_none()); + assert!(out.as_tile_pos(&map_size).is_none()); + } + + #[test] + fn offset_matches_square_offsets_table() { + let origin = SquarePos::new(0, 0); + for dir in [ + SquareDirection::North, + SquareDirection::East, + SquareDirection::South, + SquareDirection::West, + ] { + assert_eq!(origin.offset(&dir), SQUARE_OFFSETS[dir as usize]); + } + } +} diff --git a/src/helpers/square_grid/neighbors.rs b/src/helpers/square_grid/neighbors.rs index 5903d283..cef9f050 100644 --- a/src/helpers/square_grid/neighbors.rs +++ b/src/helpers/square_grid/neighbors.rs @@ -104,7 +104,7 @@ impl Add for SquareDirection { type Output = SquareDirection; fn add(self, rhs: u32) -> Self::Output { - ((self as usize) + rhs as usize).into() + ((self as usize) + (rhs as usize)).into() } } @@ -136,7 +136,7 @@ impl Sub for SquareDirection { type Output = SquareDirection; fn sub(self, rhs: u32) -> Self::Output { - ((self as usize) - rhs as usize).into() + ((self as usize) - (rhs as usize)).into() } } @@ -399,3 +399,91 @@ impl Neighbors { self.and_then_ref(f) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn cardinal_and_diagonal_flags() { + for d in CARDINAL_SQUARE_DIRECTIONS { + assert!(d.is_cardinal()); + assert!(!d.is_diagonal()); + } + + for d in SQUARE_DIRECTIONS { + if !d.is_cardinal() { + assert!(d.is_diagonal()); + } + } + } + + #[test] + fn squarepos_from_direction_is_offset_lookup() { + for d in SQUARE_DIRECTIONS { + assert_eq!(SquarePos::from(d), SQUARE_OFFSETS[d as usize]); + } + } + + #[test] + fn set_get_get_mut_iter_variants() { + let mut n = Neighbors::::default(); + + // nothing set yet + assert!(n.east.is_none()); + assert_eq!(n.iter().count(), 0); + + // set a couple of values + n.set(SquareDirection::East, 10); + n.set(SquareDirection::SouthWest, 42); + + // get / get_mut + assert_eq!(n.get(SquareDirection::East), Some(&10)); + if let Some(x) = n.get_inner_mut(SquareDirection::East) { + *x += 1; + } + assert_eq!(n.get(SquareDirection::East), Some(&11)); + + // iterator order obeys SQUARE_DIRECTIONS + let items: Vec<_> = n.iter().copied().collect(); + assert_eq!(items, vec![11, 42]); + + // iter_with_direction gives the same ordering and pairs + let pairs: Vec<_> = n.iter_with_direction().collect(); + assert_eq!( + pairs, + vec![ + (SquareDirection::East, &11), + (SquareDirection::SouthWest, &42) + ] + ); + } + + #[test] + fn map_and_and_then_variants() { + let n = Neighbors { + east: Some(1u8), + west: Some(2u8), + ..Default::default() + }; + + // map_ref + let doubled = n.map_ref(|x| x * 2); + assert_eq!(doubled.east, Some(2)); + assert_eq!(doubled.west, Some(4)); + assert!(doubled.north.is_none()); + + // and_then (by value) + let filtered = n.and_then(|x| if x % 2 == 0 { Some(x) } else { None }); + assert_eq!(filtered.east, None); + assert_eq!(filtered.west, Some(2)); + } + + #[test] + fn from_directional_closure_is_exhaustive() { + let n = Neighbors::::from_directional_closure(|d| Some(d as u8)); + for d in SQUARE_DIRECTIONS { + assert_eq!(n.get(d), Some(&(d as u8))); + } + } +} diff --git a/src/helpers/square_grid/staggered.rs b/src/helpers/square_grid/staggered.rs index 07d6cf79..ef352f96 100644 --- a/src/helpers/square_grid/staggered.rs +++ b/src/helpers/square_grid/staggered.rs @@ -164,3 +164,41 @@ impl TilePos { .as_tile_pos(map_size) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::helpers::square_grid::SquarePos; + use crate::helpers::square_grid::diamond::DiamondPos; + use crate::helpers::square_grid::neighbors::SquareDirection::*; + + #[test] + fn conversion_diamond_to_staggered_roundtrip() { + let d = DiamondPos { x: 3, y: 7 }; + let s = StaggeredPos::from(d); + assert_eq!(s, StaggeredPos { x: 3, y: 4 }); + + // `DiamondPos` implements From<&StaggeredPos> elsewhere in the crate. + let back: DiamondPos = (&s).into(); + assert_eq!(back, d); + } + + #[test] + fn arithmetic_ops() { + let a = StaggeredPos { x: 2, y: 5 }; + let b = StaggeredPos { x: -1, y: 3 }; + + assert_eq!(a + b, StaggeredPos { x: 1, y: 8 }); + assert_eq!(a - b, StaggeredPos { x: 3, y: 2 }); + assert_eq!(2 * a, StaggeredPos { x: 4, y: 10 }); + } + + #[test] + fn offset_cardinals() { + let origin = StaggeredPos::new(0, 0); + for dir in [North, East, South, West] { + let expected = StaggeredPos::from(SquarePos::from(dir)); + assert_eq!(origin.offset(&dir), expected); + } + } +} diff --git a/src/helpers/transform.rs b/src/helpers/transform.rs index 28937498..66dd1884 100644 --- a/src/helpers/transform.rs +++ b/src/helpers/transform.rs @@ -50,3 +50,65 @@ pub fn chunk_aabb( let maximum = Vec3::from((c0.max(c1).max(c2).max(c3) + border, 1.0)); Aabb::from_min_max(minimum, maximum) } + +#[cfg(test)] +mod tests { + use super::*; + use approx::assert_relative_eq; + use bevy::math::{UVec2, Vec3}; + + #[test] + fn chunk_index_to_world_space_origin_square() { + let chunk_size = UVec2::new(4, 4); + let grid_size = TilemapGridSize { x: 1.0, y: 1.0 }; + + // Bottom-left chunk (index 0,0) should have its anchor-tile centre at (0.0, 0.0) + let ws = + chunk_index_to_world_space(UVec2::ZERO, chunk_size, &grid_size, &TilemapType::Square); + assert_relative_eq!(ws.x, 0.0, epsilon = 1e-6); + assert_relative_eq!(ws.y, 0.0, epsilon = 1e-6); + } + + #[test] + fn chunk_aabb_square_unit_sizes() { + // 4 × 4 chunk, unit grid & tile sizes + let chunk_size = UVec2::new(4, 4); + let grid_size = TilemapGridSize { x: 1.0, y: 1.0 }; + let tile_size = TilemapTileSize { x: 1.0, y: 1.0 }; + + let aabb = chunk_aabb(chunk_size, &grid_size, &tile_size, &TilemapType::Square); + + let min = aabb.min(); + let max = aabb.max(); + + assert_relative_eq!(min.x, -0.5, epsilon = 1e-6); + assert_relative_eq!(min.y, -0.5, epsilon = 1e-6); + assert_relative_eq!(min.z, 0.0, epsilon = 1e-6); + + assert_relative_eq!(max.x, 4.5, epsilon = 1e-6); + assert_relative_eq!(max.y, 4.5, epsilon = 1e-6); + assert_relative_eq!(max.z, 1.0, epsilon = 1e-6); + } + + #[test] + fn chunk_aabb_tile_size_larger_than_grid_size() { + let chunk_size = UVec2::new(2, 2); + let grid_size = TilemapGridSize { x: 1.0, y: 1.0 }; + let tile_size = TilemapTileSize { x: 2.0, y: 2.0 }; + + let aabb = chunk_aabb(chunk_size, &grid_size, &tile_size, &TilemapType::Square); + + let expected_min = Vec3::new(-1.0, -1.0, 0.0); + let expected_max = Vec3::new(3.0, 3.0, 1.0); + + let min = aabb.min(); + let max = aabb.max(); + + assert_relative_eq!(min.x, expected_min.x, epsilon = 1e-6); + assert_relative_eq!(max.x, expected_max.x, epsilon = 1e-6); + assert_relative_eq!(min.y, expected_min.y, epsilon = 1e-6); + assert_relative_eq!(max.y, expected_max.y, epsilon = 1e-6); + assert_relative_eq!(min.z, expected_min.z, epsilon = 1e-6); + assert_relative_eq!(max.z, expected_max.z, epsilon = 1e-6); + } +} diff --git a/src/lib.rs b/src/lib.rs index 576c61a5..f1a06aa4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -200,3 +200,14 @@ fn update_changed_tile_positions(mut query: Query<(&TilePos, &mut TilePosOld), C tile_pos_old.0 = *tile_pos; } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn frustum_culling_default_is_true() { + let fc = FrustumCulling::default(); + assert!(fc.0, "FrustumCulling::default() should be true"); + } +} diff --git a/src/map.rs b/src/map.rs index fe15f42f..c74a5f8e 100644 --- a/src/map.rs +++ b/src/map.rs @@ -510,49 +510,89 @@ mod tests { } #[test] fn add_tilemap_tile_size() { - let a = TilemapTileSize { x: 2., y: 2. }; - let b = TilemapTileSize { x: 3., y: 3. }; - assert_eq!(a + b, TilemapTileSize { x: 5., y: 5. }); + let a = TilemapTileSize { x: 2.0, y: 2.0 }; + let b = TilemapTileSize { x: 3.0, y: 3.0 }; + assert_eq!(a + b, TilemapTileSize { x: 5.0, y: 5.0 }); } #[test] fn add_tilemap_tile_size_vec2() { - let a = TilemapTileSize { x: 2., y: 2. }; - let b = Vec2 { x: 3., y: 3. }; - assert_eq!(a + b, TilemapTileSize { x: 5., y: 5. }); + let a = TilemapTileSize { x: 2.0, y: 2.0 }; + let b = Vec2 { x: 3.0, y: 3.0 }; + assert_eq!(a + b, TilemapTileSize { x: 5.0, y: 5.0 }); } #[test] fn add_tilemap_grid_size() { - let a = TilemapGridSize { x: 2., y: 2. }; - let b = TilemapGridSize { x: 3., y: 3. }; - assert_eq!(a + b, TilemapGridSize { x: 5., y: 5. }); + let a = TilemapGridSize { x: 2.0, y: 2.0 }; + let b = TilemapGridSize { x: 3.0, y: 3.0 }; + assert_eq!(a + b, TilemapGridSize { x: 5.0, y: 5.0 }); } fn add_tilemap_grid_size_vec2() { - let a = TilemapGridSize { x: 2., y: 2. }; - let b = Vec2 { x: 3., y: 3. }; - assert_eq!(a + b, TilemapGridSize { x: 5., y: 5. }); + let a = TilemapGridSize { x: 2.0, y: 2.0 }; + let b = Vec2 { x: 3.0, y: 3.0 }; + assert_eq!(a + b, TilemapGridSize { x: 5.0, y: 5.0 }); } #[test] fn add_tilemap_spacing() { - let a = TilemapSpacing { x: 2., y: 2. }; - let b = TilemapSpacing { x: 3., y: 3. }; - assert_eq!(a + b, TilemapSpacing { x: 5., y: 5. }); + let a = TilemapSpacing { x: 2.0, y: 2.0 }; + let b = TilemapSpacing { x: 3.0, y: 3.0 }; + assert_eq!(a + b, TilemapSpacing { x: 5.0, y: 5.0 }); } #[test] fn add_tilemap_spacing_vec2() { - let a = TilemapSpacing { x: 2., y: 2. }; - let b = Vec2 { x: 3., y: 3. }; - assert_eq!(a + b, TilemapSpacing { x: 5., y: 5. }); + let a = TilemapSpacing { x: 2.0, y: 2.0 }; + let b = Vec2 { x: 3.0, y: 3.0 }; + assert_eq!(a + b, TilemapSpacing { x: 5.0, y: 5.0 }); } #[test] fn add_tilemap_texture_size() { - let a = TilemapTextureSize { x: 2., y: 2. }; - let b = TilemapTextureSize { x: 3., y: 3. }; - assert_eq!(a + b, TilemapTextureSize { x: 5., y: 5. }); + let a = TilemapTextureSize { x: 2.0, y: 2.0 }; + let b = TilemapTextureSize { x: 3.0, y: 3.0 }; + assert_eq!(a + b, TilemapTextureSize { x: 5.0, y: 5.0 }); } #[test] fn add_tilemap_texture_size_vec2() { - let a = TilemapTextureSize { x: 2., y: 2. }; - let b = Vec2 { x: 3., y: 3. }; - assert_eq!(a + b, TilemapTextureSize { x: 5., y: 5. }); + let a = TilemapTextureSize { x: 2.0, y: 2.0 }; + let b = Vec2 { x: 3.0, y: 3.0 }; + assert_eq!(a + b, TilemapTextureSize { x: 5.0, y: 5.0 }); + } + #[test] + fn tilemap_size_count_and_conversions() { + let size = TilemapSize::new(4, 5); + assert_eq!(size.count(), 20); + + // TilemapSize ➜ Vec2 + let v: Vec2 = size.into(); + assert_eq!(v, Vec2::new(4.0, 5.0)); + + // TilemapSize ➜ UVec2 ➜ TilemapSize + let uv: UVec2 = size.into(); + assert_eq!(uv, UVec2::new(4, 5)); + let size2: TilemapSize = uv.into(); + assert_eq!(size2, size); + } + + #[test] + fn tilemap_tile_size_conversions() { + let ts = TilemapTileSize::new(16.0, 32.0); + + // TilemapTileSize ➜ Vec2 + let v: Vec2 = ts.into(); + assert_eq!(v, Vec2::new(16.0, 32.0)); + + // TilemapTileSize ➜ TilemapGridSize + let gs: TilemapGridSize = ts.into(); + assert_eq!(gs, TilemapGridSize::new(16.0, 32.0)); + } + + #[test] + fn tilemap_spacing_zero_is_zero() { + assert_eq!(TilemapSpacing::zero(), TilemapSpacing::new(0.0, 0.0)); + } + + #[test] + fn defaults_match_contract() { + let settings = TilemapRenderSettings::default(); + assert_eq!(settings.render_chunk_size, CHUNK_SIZE_2D); + assert!(!settings.y_sort); } } diff --git a/src/render/chunk.rs b/src/render/chunk.rs index f4b9aa66..222410cf 100644 --- a/src/render/chunk.rs +++ b/src/render/chunk.rs @@ -369,12 +369,12 @@ impl RenderChunk2d { mesh_vertex_buffer_layouts: &mut MeshVertexBufferLayouts, ) { if self.dirty_mesh { - let size = ((self.size_in_tiles.x * self.size_in_tiles.y) * 4) as usize; + let size = (self.size_in_tiles.x * self.size_in_tiles.y * 4) as usize; let mut positions: Vec<[f32; 4]> = Vec::with_capacity(size); let mut textures: Vec<[f32; 4]> = Vec::with_capacity(size); let mut colors: Vec<[f32; 4]> = Vec::with_capacity(size); let mut indices: Vec = - Vec::with_capacity(((self.size_in_tiles.x * self.size_in_tiles.y) * 6) as usize); + Vec::with_capacity((self.size_in_tiles.x * self.size_in_tiles.y * 6) as usize); let mut i = 0; @@ -435,17 +435,21 @@ impl RenderChunk2d { self.mesh.insert_indices(Indices::U32(indices)); let vertex_buffer_data = self.mesh.create_packed_vertex_buffer_data(); - let vertex_buffer = device.create_buffer_with_data(&BufferInitDescriptor { - usage: BufferUsages::VERTEX, - label: Some("Mesh Vertex Buffer"), - contents: &vertex_buffer_data, - }); + let vertex_buffer = device.create_buffer_with_data( + &(BufferInitDescriptor { + usage: BufferUsages::VERTEX, + label: Some("Mesh Vertex Buffer"), + contents: &vertex_buffer_data, + }), + ); - let index_buffer = device.create_buffer_with_data(&BufferInitDescriptor { - usage: BufferUsages::INDEX, - contents: self.mesh.get_index_buffer_bytes().unwrap(), - label: Some("Mesh Index Buffer"), - }); + let index_buffer = device.create_buffer_with_data( + &(BufferInitDescriptor { + usage: BufferUsages::INDEX, + contents: self.mesh.get_index_buffer_bytes().unwrap(), + label: Some("Mesh Index Buffer"), + }), + ); let buffer_info = RenderMeshBufferInfo::Indexed { count: self.mesh.indices().unwrap().len() as u32, diff --git a/src/render/extract.rs b/src/render/extract.rs index 3e2bdd92..c2a8c026 100644 --- a/src/render/extract.rs +++ b/src/render/extract.rs @@ -88,8 +88,8 @@ impl ExtractedTilemapTexture { it is being extracted as a texture!", ); let texture_size: TilemapTextureSize = image.size_f32().into(); - let tile_count_x = ((texture_size.x) / (tile_size.x + tile_spacing.x)).floor(); - let tile_count_y = ((texture_size.y) / (tile_size.y + tile_spacing.y)).floor(); + let tile_count_x = (texture_size.x / (tile_size.x + tile_spacing.x)).floor(); + let tile_count_y = (texture_size.y / (tile_size.y + tile_spacing.y)).floor(); ( (tile_count_x * tile_count_y) as u32, texture_size, @@ -107,7 +107,7 @@ impl ExtractedTilemapTexture { if this_tile_size != tile_size { panic!( "Expected all provided image assets to have size {tile_size:?}, \ - but found image with size: {this_tile_size:?}", + but found image with size: {this_tile_size:?}" ); } } @@ -260,7 +260,7 @@ pub fn extract( // bit 0 : flip_x // bit 1 : flip_y // bit 2 : flip_d (anti diagonal) - let tile_flip_bits = flip.x as i32 | ((flip.y as i32) << 1) | ((flip.d as i32) << 2); + let tile_flip_bits = (flip.x as i32) | ((flip.y as i32) << 1) | ((flip.d as i32) << 2); let mut position = Vec4::new(tile_pos.x as f32, tile_pos.y as f32, 0.0, 0.0); let mut texture = Vec4::new(tile_texture.0 as f32, tile_flip_bits as f32, 0.0, 0.0); @@ -364,7 +364,7 @@ pub fn extract( ), changed: ChangedInMainWorld, }, - )) + )); } } diff --git a/src/render/material.rs b/src/render/material.rs index 38808e35..72b50b73 100644 --- a/src/render/material.rs +++ b/src/render/material.rs @@ -299,7 +299,9 @@ fn extract_materials_tilemap( changed_assets.remove(id); removed.push(*id); } - _ => continue, + _ => { + continue; + } } } @@ -496,8 +498,8 @@ pub fn queue_material_tilemap_meshes( let z = if chunk.y_sort { transform.translation.z + (1.0 - - (transform.translation.y - / (chunk.map_size.y as f32 * chunk.tile_size.y))) + - transform.translation.y + / ((chunk.map_size.y as f32) * chunk.tile_size.y)) } else { transform.translation.z }; @@ -586,7 +588,7 @@ pub fn bind_material_tilemap_meshes( }; if render_materials.get(&material_handle.id()).is_none() { continue; - }; + } if let Some(chunk) = chunk_storage.get(&UVec4::new( chunk_id.0.x, diff --git a/src/render/mod.rs b/src/render/mod.rs index 4ea203c6..c5762795 100644 --- a/src/render/mod.rs +++ b/src/render/mod.rs @@ -87,7 +87,7 @@ impl RenderChunkSize { #[inline] pub fn map_tile_to_chunk_tile(&self, tile_position: &TilePos, chunk_position: &UVec2) -> UVec2 { let tile_pos: UVec2 = tile_position.into(); - tile_pos - (*chunk_position * self.0) + tile_pos - *chunk_position * self.0 } } @@ -226,7 +226,9 @@ impl Plugin for TilemapRenderingPlugin { let render_app = match app.get_sub_app_mut(RenderApp) { Some(render_app) => render_app, - None => return, + None => { + return; + } }; render_app.init_resource::(); @@ -271,7 +273,7 @@ pub fn set_texture_to_copy_src( ) { // quick and dirty, run this for all textures anytime a texture component is created. for texture in texture_query.iter() { - texture.set_images_to_copy_src(&mut images) + texture.set_images_to_copy_src(&mut images); } } @@ -297,7 +299,6 @@ pub const ATTRIBUTE_COLOR: MeshVertexAttribute = MeshVertexAttribute::new("Color", 231497124, VertexFormat::Float32x4); #[derive(Component, ExtractComponent, Clone)] - pub struct RemovedTileEntity(pub RenderEntity); #[derive(Component, ExtractComponent, Clone)] @@ -377,7 +378,9 @@ pub fn collect_modified_image_asset_events( for asset_event in asset_events.read() { let id = match asset_event { AssetEvent::Modified { id } => id, - _ => continue, + _ => { + continue; + } }; modified_image_ids.0.insert(*id); } diff --git a/src/render/prepare.rs b/src/render/prepare.rs index 0d5a48d9..a32d009a 100644 --- a/src/render/prepare.rs +++ b/src/render/prepare.rs @@ -215,9 +215,11 @@ pub(crate) fn prepare( chunk.get_map_type(), TilemapId(Entity::from_bits(chunk.tilemap_id)), DynamicUniformIndex:: { - index: mesh_uniforms.0.push(&MeshUniform { - transform: chunk.get_transform_matrix(), - }), + index: mesh_uniforms.0.push( + &(MeshUniform { + transform: chunk.get_transform_matrix(), + }), + ), marker: PhantomData, }, DynamicUniformIndex:: { @@ -240,7 +242,7 @@ pub fn prepare_removal( removed_maps: Query<&RemovedMapEntity>, ) { for removed_tile in removed_tiles.iter() { - chunk_storage.remove_tile_with_entity(removed_tile.0.id()) + chunk_storage.remove_tile_with_entity(removed_tile.0.id()); } for removed_map in removed_maps.iter() { diff --git a/src/render/texture_array_cache.rs b/src/render/texture_array_cache.rs index ba2bffb6..c10d610f 100644 --- a/src/render/texture_array_cache.rs +++ b/src/render/texture_array_cache.rs @@ -79,8 +79,8 @@ impl TextureArrayCache { it is being extracted as a texture!", ); let texture_size: TilemapTextureSize = image.size_f32().into(); - let tile_count_x = ((texture_size.x) / (tile_size.x + tile_spacing.x)).floor(); - let tile_count_y = ((texture_size.y) / (tile_size.y + tile_spacing.y)).floor(); + let tile_count_x = (texture_size.x / (tile_size.x + tile_spacing.x)).floor(); + let tile_count_y = (texture_size.y / (tile_size.y + tile_spacing.y)).floor(); ((tile_count_x * tile_count_y) as u32, texture_size) } TilemapTexture::Vector(handles) => { @@ -93,7 +93,7 @@ impl TextureArrayCache { if this_tile_size != tile_size { panic!( "Expected all provided image assets to have size {tile_size:?}, \ - but found image with size: {this_tile_size:?}", + but found image with size: {this_tile_size:?}" ); } } @@ -164,47 +164,53 @@ impl TextureArrayCache { *count }; - let gpu_texture = render_device.create_texture(&TextureDescriptor { - label: Some("texture_array"), - size: Extent3d { - width: tile_size.x as u32, - height: tile_size.y as u32, - depth_or_array_layers: count, - }, - mip_level_count: 1, - sample_count: 1, - dimension: TextureDimension::D2, - format: *format, - usage: TextureUsages::COPY_DST | TextureUsages::TEXTURE_BINDING, - view_formats: &[], - }); + let gpu_texture = render_device.create_texture( + &(TextureDescriptor { + label: Some("texture_array"), + size: Extent3d { + width: tile_size.x as u32, + height: tile_size.y as u32, + depth_or_array_layers: count, + }, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: *format, + usage: TextureUsages::COPY_DST | TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }), + ); - let sampler = render_device.create_sampler(&SamplerDescriptor { - label: Some("texture_array_sampler"), - address_mode_u: AddressMode::ClampToEdge, - address_mode_v: AddressMode::ClampToEdge, - address_mode_w: AddressMode::ClampToEdge, - mag_filter: *filter, - min_filter: *filter, - mipmap_filter: *filter, - lod_min_clamp: 0.0, - lod_max_clamp: f32::MAX, - compare: None, - anisotropy_clamp: 1, - border_color: None, - }); + let sampler = render_device.create_sampler( + &(SamplerDescriptor { + label: Some("texture_array_sampler"), + address_mode_u: AddressMode::ClampToEdge, + address_mode_v: AddressMode::ClampToEdge, + address_mode_w: AddressMode::ClampToEdge, + mag_filter: *filter, + min_filter: *filter, + mipmap_filter: *filter, + lod_min_clamp: 0.0, + lod_max_clamp: f32::MAX, + compare: None, + anisotropy_clamp: 1, + border_color: None, + }), + ); - let texture_view = gpu_texture.create_view(&TextureViewDescriptor { - label: Some("texture_array_view"), - format: None, - dimension: Some(TextureViewDimension::D2Array), - aspect: TextureAspect::All, - base_mip_level: 0, - mip_level_count: None, - base_array_layer: 0, - array_layer_count: Some(count), - usage: Some(gpu_texture.usage()), - }); + let texture_view = gpu_texture.create_view( + &(TextureViewDescriptor { + label: Some("texture_array_view"), + format: None, + dimension: Some(TextureViewDimension::D2Array), + aspect: TextureAspect::All, + base_mip_level: 0, + mip_level_count: None, + base_array_layer: 0, + array_layer_count: Some(count), + usage: Some(gpu_texture.usage()), + }), + ); let mip_level_count = gpu_texture.mip_level_count(); @@ -259,17 +265,18 @@ impl TextureArrayCache { let array_gpu_image = self.textures.get(texture).unwrap(); let count = *count; - let mut command_encoder = - render_device.create_command_encoder(&CommandEncoderDescriptor { + let mut command_encoder = render_device.create_command_encoder( + &(CommandEncoderDescriptor { label: Some("create_texture_array_from_atlas"), - }); + }), + ); for i in 0..count { let columns = (texture_size.x / (tile_size.x + spacing.x)).floor(); let sprite_sheet_x: f32 = - (i as f32 % columns).floor() * (tile_size.x + spacing.x) + spacing.x; + ((i as f32) % columns).floor() * (tile_size.x + spacing.x) + spacing.x; let sprite_sheet_y: f32 = - (i as f32 / columns).floor() * (tile_size.y + spacing.y) + spacing.y; + ((i as f32) / columns).floor() * (tile_size.y + spacing.y) + spacing.y; command_encoder.copy_texture_to_texture( TexelCopyTextureInfo { @@ -303,7 +310,7 @@ impl TextureArrayCache { let mut gpu_images = Vec::with_capacity(handles.len()); for handle in handles { if let Some(gpu_image) = render_images.get(handle) { - gpu_images.push(gpu_image) + gpu_images.push(gpu_image); } else { self.prepare_queue.insert(texture.clone_weak()); continue; @@ -314,10 +321,11 @@ impl TextureArrayCache { let array_gpu_image = self.textures.get(texture).unwrap(); let count = *count; - let mut command_encoder = - render_device.create_command_encoder(&CommandEncoderDescriptor { + let mut command_encoder = render_device.create_command_encoder( + &(CommandEncoderDescriptor { label: Some("create_texture_array_from_handles_vec"), - }); + }), + ); for i in 0..count { command_encoder.copy_texture_to_texture( diff --git a/src/tiles/mod.rs b/src/tiles/mod.rs index 891c8fb1..35ce5359 100644 --- a/src/tiles/mod.rs +++ b/src/tiles/mod.rs @@ -27,7 +27,7 @@ impl TilePos { /// Converts a tile position (2D) into an index in a flattened vector (1D), assuming the /// tile position lies in a tilemap of the specified size. pub fn to_index(&self, tilemap_size: &TilemapSize) -> usize { - ((self.y * tilemap_size.x) + self.x) as usize + (self.y * tilemap_size.x + self.x) as usize } /// Checks to see if `self` lies within a tilemap of the specified size. @@ -141,3 +141,44 @@ pub struct AnimatedTile { /// The speed the animation plays back at. pub speed: f32, } + +#[cfg(test)] +mod tests { + use super::*; + use bevy::math::{UVec2, Vec2}; + + #[test] + fn tile_pos_to_index() { + let map_size = TilemapSize { x: 10, y: 10 }; + let pos = TilePos::new(3, 4); + assert_eq!(pos.to_index(&map_size), 4 * 10 + 3); + } + + #[test] + fn tile_pos_within_bounds() { + let map_size = TilemapSize { x: 8, y: 8 }; + assert!(TilePos::new(7, 7).within_map_bounds(&map_size)); + assert!(!TilePos::new(8, 0).within_map_bounds(&map_size)); + assert!(!TilePos::new(0, 8).within_map_bounds(&map_size)); + } + + #[test] + fn conversions_round_trip() { + let original = TilePos::new(5, 6); + + // TilePos → UVec2 → TilePos + let as_uvec: UVec2 = original.into(); + assert_eq!(as_uvec, UVec2::new(5, 6)); + let back: TilePos = as_uvec.into(); + assert_eq!(back, original); + + // TilePos → Vec2 + let as_vec: Vec2 = original.into(); + assert_eq!(as_vec, Vec2::new(5.0, 6.0)); + } + + #[test] + fn visible_default_is_true() { + assert!(TileVisible::default().0); + } +} diff --git a/src/tiles/storage.rs b/src/tiles/storage.rs index 378b17e2..9369f5bd 100644 --- a/src/tiles/storage.rs +++ b/src/tiles/storage.rs @@ -120,3 +120,98 @@ impl TileStorage { self.tiles.iter_mut().filter_map(|opt| opt.take()) } } + +#[cfg(test)] +mod tests { + use super::*; + use bevy::prelude::Entity; + + fn e(id: u32) -> Entity { + // Helper that makes a deterministic dummy `Entity` + Entity::from_raw(id) + } + + fn size_3x3() -> TilemapSize { + TilemapSize { x: 3, y: 3 } + } + + #[test] + fn empty_storage_is_filled_with_none() { + let storage = TileStorage::empty(size_3x3()); + assert_eq!(storage.size, size_3x3()); + assert!(storage.iter().all(|opt| opt.is_none())); + } + + #[test] + fn set_and_get_roundtrip() { + let mut storage = TileStorage::empty(size_3x3()); + let pos = TilePos { x: 1, y: 2 }; + storage.set(&pos, e(42)); + assert_eq!(storage.get(&pos), Some(e(42))); + } + + #[test] + fn checked_get_respects_bounds() { + let storage = TileStorage::empty(size_3x3()); + // In-bounds → None (nothing stored yet) + assert_eq!(storage.checked_get(&TilePos { x: 0, y: 0 }), None); + // Out-of-bounds → None, **not** panic + assert_eq!(storage.checked_get(&TilePos { x: 99, y: 99 }), None); + } + + #[test] + #[should_panic] + fn get_panics_when_out_of_bounds() { + let storage = TileStorage::empty(size_3x3()); + let _ = storage.get(&TilePos { x: 50, y: 50 }); + } + + #[test] + fn remove_returns_entity_and_leaves_none() { + let mut storage = TileStorage::empty(size_3x3()); + let pos = TilePos { x: 2, y: 1 }; + storage.set(&pos, e(7)); + assert_eq!(storage.remove(&pos), Some(e(7))); + assert_eq!(storage.get(&pos), None); + } + + #[test] + fn drain_yields_every_entity_and_empties_storage() { + let mut storage = TileStorage::empty(size_3x3()); + storage.set(&TilePos { x: 0, y: 0 }, e(1)); + storage.set(&TilePos { x: 1, y: 1 }, e(2)); + storage.set(&TilePos { x: 2, y: 2 }, e(3)); + + let mut drained: Vec<_> = storage.drain().collect(); + drained.sort_by_key(|e| e.index()); + assert_eq!(drained, vec![e(1), e(2), e(3)]); + assert!(storage.iter().all(|opt| opt.is_none())); + } + + // ─────────────────────────────── + // MapEntities implementation + // ─────────────────────────────── + use bevy::ecs::entity::EntityMapper; + + struct AddOneMapper; + impl EntityMapper for AddOneMapper { + fn get_mapped(&mut self, entity: Entity) -> Entity { + // Just bump the raw id for test purposes + e(entity.index() + 1) + } + + fn set_mapped(&mut self, _source: Entity, _target: Entity) {} + } + + #[test] + fn map_entities_transforms_every_entity() { + let mut storage = TileStorage::empty(size_3x3()); + storage.set(&TilePos { x: 0, y: 0 }, e(10)); + storage.set(&TilePos { x: 0, y: 1 }, e(11)); + + storage.map_entities(&mut AddOneMapper); + + assert_eq!(storage.get(&TilePos { x: 0, y: 0 }), Some(e(11))); + assert_eq!(storage.get(&TilePos { x: 0, y: 1 }), Some(e(12))); + } +} diff --git a/tests/anchor.rs b/tests/anchor.rs new file mode 100644 index 00000000..81d310c5 --- /dev/null +++ b/tests/anchor.rs @@ -0,0 +1,44 @@ +use bevy::prelude::*; +use bevy_ecs_tilemap::{anchor::*, map::*}; +use proptest::prelude::*; + +proptest! { + #[test] + fn custom_equivalences_hold_across_random_inputs( + map_x in 1u32..20, + map_y in 1u32..20, + grid_x in 0.5f32..4.0, + grid_y in 0.5f32..4.0, + tile_x in 0.5f32..4.0, + tile_y in 0.5f32..4.0, + ) { + let map_size = TilemapSize { x: map_x, y: map_y }; + let grid_size = TilemapGridSize { x: grid_x, y: grid_y }; + let tile_size = TilemapTileSize { x: tile_x, y: tile_y }; + let map_type = TilemapType::Square; + + // Had to do some trickery because proptest and approx weren't playing nice. + // Accurate to 3 digits, change as needed. + let precision = 10f32.powf(3f32); + + // Center + prop_assert_eq!( + (TilemapAnchor::Center.as_offset(&map_size, &grid_size, &tile_size, &map_type) * precision).round() / precision, + (TilemapAnchor::Custom(Vec2::ZERO).as_offset(&map_size, &grid_size, &tile_size, &map_type) * precision).round() / precision + ); + + // Top-left + prop_assert_eq!( + (TilemapAnchor::TopLeft.as_offset(&map_size, &grid_size, &tile_size, &map_type) * precision).round() / precision, + (TilemapAnchor::Custom(Vec2::new(-0.5, 0.5)) + .as_offset(&map_size, &grid_size, &tile_size, &map_type) * precision).round() / precision + ); + + // Bottom-right + prop_assert_eq!( + (TilemapAnchor::BottomRight.as_offset(&map_size, &grid_size, &tile_size, &map_type) * precision).round() / precision, + (TilemapAnchor::Custom(Vec2::new(0.5, -0.5)) + .as_offset(&map_size, &grid_size, &tile_size, &map_type) * precision).round() / precision + ); + } +} diff --git a/tests/helpers__filling.rs b/tests/helpers__filling.rs new file mode 100644 index 00000000..70edd7dc --- /dev/null +++ b/tests/helpers__filling.rs @@ -0,0 +1,47 @@ +use bevy::ecs::{ + system::Commands, + world::{CommandQueue, World}, +}; +use bevy_ecs_tilemap::{ + map::{TilemapId, TilemapSize}, + prelude::fill_tilemap, + tiles::{TilePos, TileStorage, TileTextureIndex}, +}; + +fn spawn_tilemap(world: &mut World) -> (TilemapId, TileStorage) { + let size = TilemapSize { x: 4, y: 3 }; + let id = TilemapId(world.spawn_empty().id()); + (id, TileStorage::empty(size)) +} + +#[test] +fn fill_tilemap_fills_every_cell() { + let mut world = World::default(); + let mut queue = CommandQueue::default(); + let (tilemap_id, mut storage) = spawn_tilemap(&mut world); + let mut commands = Commands::new(&mut queue, &mut world); + + let size = storage.size; + + fill_tilemap( + TileTextureIndex(7), + size, + tilemap_id, + &mut commands, + &mut storage, + ); + queue.apply(&mut world); + + // every position should have an entity and the world should own it + let mut filled = 0; + for x in 0..size.x { + for y in 0..size.y { + let pos = TilePos { x, y }; + let entity = storage.get(&pos).expect("position not filled"); + assert!(!world.get_entity(entity).is_err()); + + filled += 1; + } + } + assert_eq!(filled, (size.x * size.y) as usize); +} diff --git a/tests/helpers__hex_grid__axial.rs b/tests/helpers__hex_grid__axial.rs new file mode 100644 index 00000000..48f75292 --- /dev/null +++ b/tests/helpers__hex_grid__axial.rs @@ -0,0 +1,60 @@ +use bevy_ecs_tilemap::helpers::hex_grid::axial::AxialPos; +use bevy_ecs_tilemap::map::{TilemapGridSize, TilemapSize}; +use bevy_ecs_tilemap::prelude::*; + +const GRID: TilemapGridSize = TilemapGridSize { x: 32.0, y: 32.0 }; + +#[test] +fn row_projection_round_trip() { + let samples = [ + AxialPos::new(0, 0), + AxialPos::new(3, -2), + AxialPos::new(-4, 5), + ]; + + for ax in samples { + let world = ax.center_in_world_row(&GRID); + let back = AxialPos::from_world_pos_row(&world, &GRID); + assert_eq!(ax, back, "row-oriented round-trip failed for {ax:?}"); + } +} + +#[test] +fn col_projection_round_trip() { + let samples = [ + AxialPos::new(0, 0), + AxialPos::new(1, 4), + AxialPos::new(-3, -2), + ]; + + for ax in samples { + let world = ax.center_in_world_col(&GRID); + let back = AxialPos::from_world_pos_col(&world, &GRID); + assert_eq!(ax, back, "col-oriented round-trip failed for {ax:?}"); + } +} + +#[test] +fn tilepos_coord_system_helpers() { + let map_size = TilemapSize { x: 10, y: 10 }; + let ax = AxialPos::new(3, 2); + + for &sys in &[ + HexCoordSystem::Row, + HexCoordSystem::Column, + HexCoordSystem::RowEven, + HexCoordSystem::RowOdd, + HexCoordSystem::ColumnEven, + HexCoordSystem::ColumnOdd, + ] { + let tp = ax + .as_tile_pos_given_coord_system_and_map_size(sys, &map_size) + .expect("axial pos should be inside map"); + let back = AxialPos::from_tile_pos_given_coord_system(&tp, sys); + assert_eq!( + ax, back, + "coord-system conversion failed for {:?} (tile={tp:?})", + sys + ); + } +} diff --git a/tests/helpers__hex_grid__cube.rs b/tests/helpers__hex_grid__cube.rs new file mode 100644 index 00000000..91d78d88 --- /dev/null +++ b/tests/helpers__hex_grid__cube.rs @@ -0,0 +1,13 @@ +use bevy_ecs_tilemap::helpers::hex_grid::axial::AxialPos; +use bevy_ecs_tilemap::helpers::hex_grid::cube::CubePos; + +#[test] +fn axial_round_trip_is_lossless() { + let axial = AxialPos { q: -5, r: 2 }; + let cube: CubePos = axial.into(); + let back: AxialPos = AxialPos { + q: cube.q, + r: cube.r, + }; + assert_eq!(back, axial, "Axial → Cube → Axial should be identity"); +} diff --git a/tests/helpers__hex_grid__neighbors.rs b/tests/helpers__hex_grid__neighbors.rs new file mode 100644 index 00000000..d82e2671 --- /dev/null +++ b/tests/helpers__hex_grid__neighbors.rs @@ -0,0 +1,27 @@ +use bevy_ecs_tilemap::helpers::hex_grid::neighbors::{HexDirection, HexNeighbors}; +use bevy_ecs_tilemap::map::HexCoordSystem; +use bevy_ecs_tilemap::map::TilemapSize; +use bevy_ecs_tilemap::tiles::TilePos; + +fn pos(x: u32, y: u32) -> TilePos { + TilePos { x, y } +} + +/// A border tile should yield `None` for neighbors that would fall off the map. +#[test] +fn border_tiles_clamp_neighbors_out_of_bounds() { + let size = TilemapSize { x: 3, y: 3 }; + // South-west corner + let corner = pos(0, 2); + + let neighbors = + HexNeighbors::::get_neighboring_positions(&corner, &size, &HexCoordSystem::Row); + + for dir in [HexDirection::One, HexDirection::Two, HexDirection::Three] { + assert!(neighbors.get(dir).is_none(), "{dir:?} should be None"); + } + + for dir in [HexDirection::Zero, HexDirection::Four, HexDirection::Five] { + assert!(neighbors.get(dir).is_some(), "{dir:?} should be Some"); + } +} diff --git a/tests/helpers__projection.rs b/tests/helpers__projection.rs new file mode 100644 index 00000000..44c125a4 --- /dev/null +++ b/tests/helpers__projection.rs @@ -0,0 +1,57 @@ +use bevy_ecs_tilemap::{ + anchor::TilemapAnchor, + map::{ + HexCoordSystem, IsoCoordSystem, TilemapGridSize, TilemapSize, TilemapTileSize, TilemapType, + }, + tiles::TilePos, +}; + +fn roundtrip( + original: TilePos, + map_type: TilemapType, + grid_size: TilemapGridSize, + tile_size: TilemapTileSize, +) { + let map_size = TilemapSize { x: 10, y: 10 }; + let anchor = TilemapAnchor::BottomLeft; + + let world = original.center_in_world(&map_size, &grid_size, &tile_size, &map_type, &anchor); + let recon = TilePos::from_world_pos( + &world, &map_size, &grid_size, &tile_size, &map_type, &anchor, + ) + .expect("round-trip should succeed"); + + assert_eq!(original, recon, "round-trip failed for {map_type:?}"); +} + +#[test] +fn square_roundtrip() { + roundtrip( + TilePos { x: 4, y: 6 }, + TilemapType::Square, + TilemapGridSize { x: 32.0, y: 32.0 }, + TilemapTileSize { x: 32.0, y: 32.0 }, + ); +} + +#[test] +fn hex_row_even_roundtrip() { + use HexCoordSystem::*; + roundtrip( + TilePos { x: 2, y: 5 }, + TilemapType::Hexagon(RowEven), + TilemapGridSize { x: 32.0, y: 32.0 }, + TilemapTileSize { x: 32.0, y: 32.0 }, + ); +} + +#[test] +fn iso_diamond_roundtrip() { + use IsoCoordSystem::*; + roundtrip( + TilePos { x: 1, y: 8 }, + TilemapType::Isometric(Diamond), + TilemapGridSize { x: 32.0, y: 16.0 }, + TilemapTileSize { x: 32.0, y: 16.0 }, + ); +} diff --git a/tests/helpers__square_grid__diamond.rs b/tests/helpers__square_grid__diamond.rs new file mode 100644 index 00000000..badd3f87 --- /dev/null +++ b/tests/helpers__square_grid__diamond.rs @@ -0,0 +1,27 @@ +use bevy_ecs_tilemap::helpers::square_grid::diamond::DiamondPos; +use bevy_ecs_tilemap::helpers::square_grid::neighbors::SquareDirection; +use bevy_ecs_tilemap::map::TilemapSize; +use bevy_ecs_tilemap::tiles::TilePos; + +#[test] +fn as_tile_pos_respects_bounds() { + let map = TilemapSize { x: 5, y: 5 }; + let inside = DiamondPos::new(3, 4); + let outside = DiamondPos::new(-1, 0); + + assert_eq!(inside.as_tile_pos(&map), Some(TilePos { x: 3, y: 4 })); + assert_eq!(outside.as_tile_pos(&map), None); +} + +#[test] +fn diamond_offset_follows_square_direction() { + use SquareDirection::*; + let map = TilemapSize { x: 4, y: 4 }; + let origin = TilePos { x: 1, y: 1 }; + + let up = origin.diamond_offset(&North, &map).unwrap(); + assert_eq!(up, TilePos { x: 1, y: 2 }); + + let right = origin.diamond_offset(&East, &map).unwrap(); + assert_eq!(right, TilePos { x: 2, y: 1 }); +} diff --git a/tests/helpers__square_grid__mod.rs b/tests/helpers__square_grid__mod.rs new file mode 100644 index 00000000..772125f1 --- /dev/null +++ b/tests/helpers__square_grid__mod.rs @@ -0,0 +1,33 @@ +use bevy_ecs_tilemap::{map::TilemapSize, tiles::TilePos, *}; +use helpers::square_grid::neighbors::SquareDirection; + +#[test] +fn square_offset_roundtrip() { + let map_size = TilemapSize { x: 5, y: 5 }; + let origin = TilePos::new(2, 2); + + // Take a step north then south – we should land back on origin. + let north = origin + .square_offset(&SquareDirection::North, &map_size) + .expect("in-bounds north neighbour"); + let back = north + .square_offset(&SquareDirection::South, &map_size) + .expect("in-bounds south neighbour"); + + assert_eq!(back, origin); +} + +#[test] +fn square_offset_out_of_bounds_returns_none() { + let map_size = TilemapSize { x: 4, y: 4 }; + let edge = TilePos::new(0, 0); + + assert!( + edge.square_offset(&SquareDirection::South, &map_size) + .is_none() + ); + assert!( + edge.square_offset(&SquareDirection::West, &map_size) + .is_none() + ); +} diff --git a/tests/helpers__square_grid__neighbors.rs b/tests/helpers__square_grid__neighbors.rs new file mode 100644 index 00000000..1c04618c --- /dev/null +++ b/tests/helpers__square_grid__neighbors.rs @@ -0,0 +1,108 @@ +use bevy_ecs_tilemap::{ + helpers::square_grid::{ + neighbors::{Neighbors, SquareDirection}, + staggered::StaggeredPos, + }, + map::TilemapSize, + tiles::{TilePos, TileStorage}, +}; + +#[test] +fn square_neighboring_positions_centre_of_small_map() { + let map = TilemapSize { x: 3, y: 3 }; + let centre = TilePos::new(1, 1); + + let neighbors = Neighbors::get_square_neighboring_positions(¢re, &map, true); + + // we should get all eight neighbouring cells + for (dx, dy) in [ + (1, 0), + (1, 1), + (0, 1), + (-1, 1), + (-1, 0), + (-1, -1), + (0, -1), + (1, -1), + ] { + let expected = TilePos::new((1isize + dx) as u32, (1isize + dy) as u32); + assert!( + neighbors.iter().any(|p| *p == expected), + "expected to find neighbour at {expected:?}" + ); + } +} + +#[test] +fn square_neighboring_positions_edges_clamp_to_none() { + let map = TilemapSize { x: 2, y: 2 }; + let corner = TilePos::new(0, 0); + + let neighbors = Neighbors::get_square_neighboring_positions(&corner, &map, true); + + // (0,0) only has East, North, NorthEast inside the map + assert_eq!(neighbors.east, Some(TilePos::new(1, 0))); + assert_eq!(neighbors.north, Some(TilePos::new(0, 1))); + assert_eq!(neighbors.north_east, Some(TilePos::new(1, 1))); + + assert!(neighbors.north_west.is_none()); + assert!(neighbors.west.is_none()); + assert!(neighbors.south_west.is_none()); + assert!(neighbors.south.is_none()); + assert!(neighbors.south_east.is_none()); +} + +#[test] +fn staggered_neighboring_positions_respects_offset() { + let map: TilemapSize = TilemapSize { x: 3, y: 3 }; + let start: TilePos = TilePos::new(1, 1); + + let neighbors: Neighbors = + Neighbors::get_staggered_neighboring_positions(&start, &map, false); + + // only cardinals requested + assert!(neighbors.north_east.is_none()); + assert!(neighbors.south_west.is_none()); + assert_eq!( + neighbors.north, + Some( + StaggeredPos::from(&start) + .offset(&SquareDirection::North) + .as_tile_pos(&map) + .unwrap() + ) + ); + assert_eq!( + neighbors.east, + Some( + StaggeredPos::from(&start) + .offset(&SquareDirection::East) + .as_tile_pos(&map) + .unwrap() + ) + ); +} + +#[test] +fn tile_storage_entity_lookup() { + // Smoke-test the `entities` helper. + use bevy::prelude::Entity; + + let map = TilemapSize { x: 2, y: 2 }; + let mut storage = TileStorage::empty(map); + + let a = Entity::from_raw(1); + let b = Entity::from_raw(2); + storage.set(&TilePos::new(1, 0), a); + storage.set(&TilePos::new(0, 1), b); + + let pos = TilePos::new(0, 0); + let neighbors = Neighbors::get_square_neighboring_positions(&pos, &map, false); + let entity_neighbors = neighbors.entities(&storage); + + assert_eq!(entity_neighbors.east, Some(a)); + assert_eq!(entity_neighbors.north, Some(b)); + // other cardinals None, diagonals not requested + assert!(entity_neighbors.south.is_none()); + assert!(entity_neighbors.west.is_none()); +} diff --git a/tests/helpers__square_grid__staggered.rs b/tests/helpers__square_grid__staggered.rs new file mode 100644 index 00000000..0452fc2d --- /dev/null +++ b/tests/helpers__square_grid__staggered.rs @@ -0,0 +1,42 @@ +use bevy_ecs_tilemap::{ + helpers::square_grid::{neighbors::SquareDirection::*, staggered::StaggeredPos}, + map::{TilemapGridSize, TilemapSize}, + tiles::TilePos, +}; + +#[test] +fn as_tile_pos_bounds() { + let map = TilemapSize { x: 10, y: 10 }; + + let inside = StaggeredPos::new(5, 5); + let outside = StaggeredPos::new(-1, 0); + + assert_eq!(inside.as_tile_pos(&map).unwrap(), TilePos { x: 5, y: 5 }); + assert!(outside.as_tile_pos(&map).is_none()); +} + +#[test] +fn tilepos_staggered_offset() { + let map = TilemapSize { x: 3, y: 3 }; + + let origin = TilePos { x: 1, y: 1 }; + assert_eq!( + origin.staggered_offset(&North, &map).unwrap(), + TilePos { x: 1, y: 2 } + ); + + // Edge should return None + let edge = TilePos { x: 0, y: 0 }; + assert!(edge.staggered_offset(&West, &map).is_none()); +} + +#[test] +fn world_roundtrip() { + let grid = TilemapGridSize { x: 32.0, y: 32.0 }; + let original = StaggeredPos::new(4, 2); + + let world = original.center_in_world(&grid); + let round = StaggeredPos::from_world_pos(&world, &grid); + + assert_eq!(original, round); +} diff --git a/tests/map1.rs b/tests/map1.rs new file mode 100644 index 00000000..ed9bb114 --- /dev/null +++ b/tests/map1.rs @@ -0,0 +1,29 @@ +use bevy::ecs::entity::{EntityMapper, MapEntities}; +use bevy::prelude::Entity; +use bevy_ecs_tilemap::map::TilemapId; + +/// Tiny stub that maps a single entity to a different one. +struct DummyMapper { + from: Entity, + to: Entity, +} + +impl EntityMapper for DummyMapper { + fn get_mapped(&mut self, e: Entity) -> Entity { + if e == self.from { self.to } else { e } + } + + fn set_mapped(&mut self, _source: Entity, _target: Entity) {} +} + +#[test] +fn tilemap_id_is_remapped() { + let old = Entity::from_raw(1); + let new = Entity::from_raw(2); + + let mut id = TilemapId(old); + let mut mapper = DummyMapper { from: old, to: new }; + + id.map_entities(&mut mapper); + assert_eq!(id.0, new); +} diff --git a/tests/map2.rs b/tests/map2.rs new file mode 100644 index 00000000..cadcdcf8 --- /dev/null +++ b/tests/map2.rs @@ -0,0 +1,65 @@ +use bevy::{ + app::App, + asset::{Assets, RenderAssetUsages}, + ecs::system::{Res, ResMut, RunSystemOnce}, + image::Image, + render::render_resource::{Extent3d, TextureDimension, TextureFormat, TextureUsages}, +}; +use bevy_ecs_tilemap::map::TilemapTexture; + +fn make_image() -> Image { + let mut image = Image::new_fill( + Extent3d { + width: 1, + height: 1, + depth_or_array_layers: 1, + }, + TextureDimension::D2, + &[255, 255, 255, 255], + TextureFormat::Rgba8UnormSrgb, + RenderAssetUsages::default(), + ); + image.texture_descriptor.usage = TextureUsages::TEXTURE_BINDING; + image +} + +#[test] +fn verify_ready_and_set_copy_src_work() { + let mut app = App::new(); + app.init_resource::>(); + + let handle = { + let mut images = app.world_mut().resource_mut::>(); + images.add(make_image()) + }; + let tex = TilemapTexture::Single(handle.clone_weak()); + + // 1. `verify_ready` should fail (COPY_SRC not set yet) + { + let tex = tex.clone(); + let _ = app + .world_mut() + .run_system_once(move |images: Res>| { + assert!(!tex.verify_ready(&images)); + }); + } + + // 2. add the COPY_SRC usage flag + { + let tex = tex.clone(); + let _ = app + .world_mut() + .run_system_once(move |mut images: ResMut>| { + tex.set_images_to_copy_src(&mut images); + }); + } + + // 3. `verify_ready` should now succeed + { + let _ = app + .world_mut() + .run_system_once(move |images: Res>| { + assert!(tex.verify_ready(&images)); + }); + } +} diff --git a/tests/tiles__mod.rs b/tests/tiles__mod.rs new file mode 100644 index 00000000..b2e79fed --- /dev/null +++ b/tests/tiles__mod.rs @@ -0,0 +1,19 @@ +use bevy::color::Color; +use bevy_ecs_tilemap::tiles::{TileBundle, TileFlip, TilePos}; + +#[test] +fn tile_bundle_defaults_are_consistent() { + let bundle = TileBundle::default(); + + // Position and visibility come from their own tested defaults + assert_eq!(bundle.position, TilePos::default()); + assert_eq!(bundle.visible.0, true); + + // Old-position starts in sync with `position` + assert_eq!(bundle.old_position.0, bundle.position); + + // Flip, color and texture index should be zeroed / identity + assert_eq!(bundle.flip, TileFlip::default()); + assert_eq!(bundle.texture_index.0, 0); + assert_eq!(bundle.color.0, Color::WHITE); +} diff --git a/tests/tiles__storage.rs b/tests/tiles__storage.rs new file mode 100644 index 00000000..6929e770 --- /dev/null +++ b/tests/tiles__storage.rs @@ -0,0 +1,41 @@ +use bevy::{ecs::world::CommandQueue, prelude::*}; +use bevy_ecs_tilemap::prelude::{TilePos, TileStorage, TilemapSize}; + +#[test] +fn drain_can_be_used_to_despawn_entities() { + let mut app = App::new(); + // Spawn three entities and store their IDs + let e1 = app.world_mut().spawn_empty().id(); + let e2 = app.world_mut().spawn_empty().id(); + let e3 = app.world_mut().spawn_empty().id(); + + // Put them in a storage component that lives on its own entity + let mut storage = TileStorage::empty(TilemapSize { x: 2, y: 2 }); + storage.set(&(TilePos { x: 0, y: 0 }), e1); + storage.set(&(TilePos { x: 1, y: 0 }), e2); + storage.set(&(TilePos { x: 0, y: 1 }), e3); + + let storage_entity = app.world_mut().spawn(storage).id(); + + // Use Commands-style despawning exactly as the docs example shows + let mut queue = CommandQueue::default(); + { + let mut storage = app + .world_mut() + .entity_mut(storage_entity) + .take::() + .unwrap(); + let mut commands = Commands::new(&mut queue, &app.world()); + for entity in storage.drain() { + commands.entity(entity).despawn(); + } + // Put the (now empty) storage back on the entity + commands.entity(storage_entity).insert(storage); + } + queue.apply(&mut app.world_mut()); + + // Verify that the entities are really gone + assert!(!app.world().entities().contains(e1)); + assert!(!app.world().entities().contains(e2)); + assert!(!app.world().entities().contains(e3)); +}