Skip to main content

goud_engine/assets/loaders/tiled_map/
layer.rs

1//! Tiled map layer types.
2//!
3//! These types represent the asset-level data for tile and object layers
4//! extracted from a Tiled map file. They are decoupled from the `tiled`
5//! crate's internal representations.
6
7use std::collections::HashMap;
8
9/// A tile layer containing a grid of tile GID values.
10///
11/// GID 0 represents an empty tile. Non-zero GIDs reference tiles
12/// from the map's tilesets.
13#[derive(Debug, Clone)]
14pub struct TileLayer {
15    /// The layer name as set in the Tiled editor.
16    pub name: String,
17    /// Width of the layer in tiles.
18    pub width: u32,
19    /// Height of the layer in tiles.
20    pub height: u32,
21    /// Flat array of GID values in row-major order. GID 0 = empty.
22    pub tiles: Vec<u32>,
23    /// Whether the layer is visible.
24    pub visible: bool,
25    /// Layer opacity from 0.0 (transparent) to 1.0 (opaque).
26    pub opacity: f32,
27}
28
29impl TileLayer {
30    /// Returns the GID at the given tile coordinate, or `None` if out of bounds.
31    pub fn get_gid(&self, col: u32, row: u32) -> Option<u32> {
32        if col < self.width && row < self.height {
33            let idx = (row * self.width + col) as usize;
34            self.tiles.get(idx).copied()
35        } else {
36            None
37        }
38    }
39
40    /// Returns `true` if the tile at the given coordinate is empty (GID 0).
41    pub fn is_empty_at(&self, col: u32, row: u32) -> bool {
42        self.get_gid(col, row).is_none_or(|gid| gid == 0)
43    }
44}
45
46/// An object layer containing positioned objects.
47#[derive(Debug, Clone)]
48pub struct ObjectLayer {
49    /// The layer name as set in the Tiled editor.
50    pub name: String,
51    /// The objects in this layer.
52    pub objects: Vec<MapObject>,
53    /// Whether the layer is visible.
54    pub visible: bool,
55}
56
57/// A single object in an object layer.
58///
59/// Objects represent positioned entities such as spawn points,
60/// collision regions, or trigger zones.
61#[derive(Debug, Clone)]
62pub struct MapObject {
63    /// Unique ID within the map.
64    pub id: u32,
65    /// Object name as set in the Tiled editor.
66    pub name: String,
67    /// Object type/class as set in the Tiled editor.
68    pub object_type: String,
69    /// X position in pixels.
70    pub x: f32,
71    /// Y position in pixels.
72    pub y: f32,
73    /// Width in pixels.
74    pub width: f32,
75    /// Height in pixels.
76    pub height: f32,
77    /// Custom properties as string key-value pairs.
78    pub properties: HashMap<String, String>,
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    #[test]
86    fn test_tile_layer_get_gid_valid() {
87        let layer = TileLayer {
88            name: "ground".to_string(),
89            width: 3,
90            height: 2,
91            tiles: vec![1, 2, 3, 4, 5, 6],
92            visible: true,
93            opacity: 1.0,
94        };
95        assert_eq!(layer.get_gid(0, 0), Some(1));
96        assert_eq!(layer.get_gid(2, 0), Some(3));
97        assert_eq!(layer.get_gid(0, 1), Some(4));
98        assert_eq!(layer.get_gid(2, 1), Some(6));
99    }
100
101    #[test]
102    fn test_tile_layer_get_gid_out_of_bounds() {
103        let layer = TileLayer {
104            name: "ground".to_string(),
105            width: 2,
106            height: 2,
107            tiles: vec![1, 2, 3, 4],
108            visible: true,
109            opacity: 1.0,
110        };
111        assert_eq!(layer.get_gid(2, 0), None);
112        assert_eq!(layer.get_gid(0, 2), None);
113        assert_eq!(layer.get_gid(5, 5), None);
114    }
115
116    #[test]
117    fn test_tile_layer_is_empty_at() {
118        let layer = TileLayer {
119            name: "sparse".to_string(),
120            width: 3,
121            height: 1,
122            tiles: vec![0, 5, 0],
123            visible: true,
124            opacity: 1.0,
125        };
126        assert!(layer.is_empty_at(0, 0));
127        assert!(!layer.is_empty_at(1, 0));
128        assert!(layer.is_empty_at(2, 0));
129        // Out-of-bounds treated as empty.
130        assert!(layer.is_empty_at(3, 0));
131    }
132
133    #[test]
134    fn test_object_layer_construction() {
135        let obj = MapObject {
136            id: 1,
137            name: "spawn".to_string(),
138            object_type: "player_spawn".to_string(),
139            x: 100.0,
140            y: 200.0,
141            width: 32.0,
142            height: 32.0,
143            properties: HashMap::from([("difficulty".to_string(), "hard".to_string())]),
144        };
145        let layer = ObjectLayer {
146            name: "objects".to_string(),
147            objects: vec![obj],
148            visible: true,
149        };
150        assert_eq!(layer.objects.len(), 1);
151        assert_eq!(layer.objects[0].name, "spawn");
152        assert_eq!(
153            layer.objects[0].properties.get("difficulty"),
154            Some(&"hard".to_string())
155        );
156    }
157
158    #[test]
159    fn test_map_object_default_properties() {
160        let obj = MapObject {
161            id: 42,
162            name: String::new(),
163            object_type: String::new(),
164            x: 0.0,
165            y: 0.0,
166            width: 0.0,
167            height: 0.0,
168            properties: HashMap::new(),
169        };
170        assert_eq!(obj.id, 42);
171        assert!(obj.name.is_empty());
172        assert!(obj.properties.is_empty());
173    }
174}