1use crate::tile_meta::CollisionType;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub struct TilemapEntry {
18 pub tile_id: u16,
20 pub flip_h: bool,
22 pub flip_v: bool,
24 pub palette_group: u8,
26 pub priority: u8,
28 pub collision_override: Option<CollisionType>,
31 pub animation_group: Option<u8>,
36}
37
38impl Default for TilemapEntry {
39 fn default() -> Self {
40 Self {
41 tile_id: 0,
42 flip_h: false,
43 flip_v: false,
44 palette_group: 0,
45 priority: 0,
46 collision_override: None,
47 animation_group: None,
48 }
49 }
50}
51
52#[derive(Debug, Clone)]
59pub struct Tilemap {
60 pub width: u16,
62 pub height: u16,
64 pub entries: Vec<TilemapEntry>,
66}
67
68impl Tilemap {
69 pub fn new(width: u16, height: u16) -> Self {
76 assert!(width > 0, "Tilemap width must be > 0");
77 assert!(height > 0, "Tilemap height must be > 0");
78
79 let len = width as usize * height as usize;
80 Self {
81 width,
82 height,
83 entries: vec![TilemapEntry::default(); len],
84 }
85 }
86
87 pub fn from_gb_tilemap(data: &[u8], width: u16, height: u16) -> Self {
97 assert!(width > 0, "Tilemap width must be > 0");
98 assert!(height > 0, "Tilemap height must be > 0");
99
100 let expected = width as usize * height as usize;
101 assert!(
102 data.len() >= expected,
103 "GB tilemap data too short: expected at least {} bytes, got {}",
104 expected,
105 data.len(),
106 );
107
108 let entries: Vec<TilemapEntry> = data[..expected]
109 .iter()
110 .map(|&b| TilemapEntry {
111 tile_id: b as u16,
112 ..Default::default()
113 })
114 .collect();
115
116 Self {
117 width,
118 height,
119 entries,
120 }
121 }
122
123 #[inline]
124 fn index(&self, x: u16, y: u16) -> usize {
125 y as usize * self.width as usize + x as usize
126 }
127
128 #[inline]
130 pub fn in_bounds(&self, x: u16, y: u16) -> bool {
131 x < self.width && y < self.height
132 }
133
134 #[inline]
137 pub fn get(&self, x: u16, y: u16) -> Option<&TilemapEntry> {
138 if self.in_bounds(x, y) {
139 Some(&self.entries[self.index(x, y)])
140 } else {
141 None
142 }
143 }
144
145 #[inline]
148 pub fn get_mut(&mut self, x: u16, y: u16) -> Option<&mut TilemapEntry> {
149 if self.in_bounds(x, y) {
150 let idx = self.index(x, y);
151 Some(&mut self.entries[idx])
152 } else {
153 None
154 }
155 }
156
157 #[inline]
160 pub fn set(&mut self, x: u16, y: u16, entry: TilemapEntry) {
161 if self.in_bounds(x, y) {
162 let idx = self.index(x, y);
163 self.entries[idx] = entry;
164 }
165 }
166
167 pub fn fill_rect(&mut self, x: u16, y: u16, w: u16, h: u16, entry: TilemapEntry) {
172 let x_end = (x + w).min(self.width);
173 let y_end = (y + h).min(self.height);
174 for row in y..y_end {
175 for col in x..x_end {
176 self.set(col, row, entry);
177 }
178 }
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185
186 #[test]
191 fn entry_default_is_zero_tile() {
192 let e = TilemapEntry::default();
193 assert_eq!(e.tile_id, 0);
194 assert!(!e.flip_h);
195 assert!(!e.flip_v);
196 assert_eq!(e.palette_group, 0);
197 assert_eq!(e.priority, 0);
198 assert_eq!(e.collision_override, None);
199 assert_eq!(e.animation_group, None);
200 }
201
202 #[test]
203 fn entry_with_collision_override() {
204 let e = TilemapEntry {
205 collision_override: Some(CollisionType::Water),
206 ..Default::default()
207 };
208 assert_eq!(e.collision_override, Some(CollisionType::Water));
209 }
210
211 #[test]
212 fn entry_with_animation() {
213 let e = TilemapEntry {
214 animation_group: Some(3),
215 ..Default::default()
216 };
217 assert_eq!(e.animation_group, Some(3));
218 }
219
220 #[test]
225 fn new_creates_correct_dimensions() {
226 let tm = Tilemap::new(32, 32);
227 assert_eq!(tm.width, 32);
228 assert_eq!(tm.height, 32);
229 assert_eq!(tm.entries.len(), 1024);
230 }
231
232 #[test]
233 fn new_fills_with_default_entries() {
234 let tm = Tilemap::new(4, 3);
235 for entry in &tm.entries {
236 assert_eq!(*entry, TilemapEntry::default());
237 }
238 }
239
240 #[test]
241 #[should_panic]
242 fn new_panics_on_zero_width() {
243 Tilemap::new(0, 10);
244 }
245
246 #[test]
247 #[should_panic]
248 fn new_panics_on_zero_height() {
249 Tilemap::new(10, 0);
250 }
251
252 #[test]
257 fn from_gb_tilemap_32x32() {
258 let data = vec![42u8; 1024];
259 let tm = Tilemap::from_gb_tilemap(&data, 32, 32);
260 assert_eq!(tm.width, 32);
261 assert_eq!(tm.height, 32);
262 for e in &tm.entries {
263 assert_eq!(e.tile_id, 42);
264 assert!(!e.flip_h);
265 assert!(!e.flip_v);
266 }
267 }
268
269 #[test]
270 fn from_gb_tilemap_preserves_values() {
271 let data: Vec<u8> = (0..9).collect();
272 let tm = Tilemap::from_gb_tilemap(&data, 3, 3);
273 for (i, e) in tm.entries.iter().enumerate() {
274 assert_eq!(e.tile_id, i as u16);
275 }
276 }
277
278 #[test]
279 fn from_gb_tilemap_ignores_extra_bytes() {
280 let mut data = vec![7u8; 9];
281 data.push(99);
282 let tm = Tilemap::from_gb_tilemap(&data, 3, 3);
283 assert_eq!(tm.entries.len(), 9);
284 for e in &tm.entries {
285 assert_eq!(e.tile_id, 7);
286 }
287 }
288
289 #[test]
290 #[should_panic]
291 fn from_gb_tilemap_panics_on_short_data() {
292 Tilemap::from_gb_tilemap(&[1, 2, 3], 4, 4);
293 }
294
295 #[test]
300 fn in_bounds() {
301 let tm = Tilemap::new(10, 8);
302 assert!(tm.in_bounds(0, 0));
303 assert!(tm.in_bounds(9, 7));
304 assert!(!tm.in_bounds(10, 0));
305 assert!(!tm.in_bounds(0, 8));
306 }
307
308 #[test]
309 fn get_returns_entry() {
310 let mut tm = Tilemap::new(5, 5);
311 let e = TilemapEntry {
312 tile_id: 99,
313 ..Default::default()
314 };
315 tm.set(2, 3, e);
316 assert_eq!(tm.get(2, 3), Some(&e));
317 }
318
319 #[test]
320 fn get_returns_none_oob() {
321 let tm = Tilemap::new(5, 5);
322 assert_eq!(tm.get(5, 0), None);
323 assert_eq!(tm.get(0, 5), None);
324 }
325
326 #[test]
327 fn get_mut_modifies_entry() {
328 let mut tm = Tilemap::new(4, 4);
329 {
330 let e = tm.get_mut(1, 2).unwrap();
331 e.flip_h = true;
332 }
333 assert!(tm.get(1, 2).unwrap().flip_h);
334 }
335
336 #[test]
337 fn get_mut_returns_none_oob() {
338 let mut tm = Tilemap::new(3, 3);
339 assert!(tm.get_mut(3, 0).is_none());
340 }
341
342 #[test]
343 fn set_and_get_roundtrip() {
344 let mut tm = Tilemap::new(16, 16);
345 let e = TilemapEntry {
346 tile_id: 255,
347 flip_h: true,
348 flip_v: true,
349 palette_group: 3,
350 priority: 1,
351 collision_override: Some(CollisionType::Impassable),
352 animation_group: Some(2),
353 };
354 tm.set(10, 5, e);
355 assert_eq!(tm.get(10, 5), Some(&e));
356 }
357
358 #[test]
359 fn set_oob_is_noop() {
360 let mut tm = Tilemap::new(4, 4);
361 let e = TilemapEntry {
362 tile_id: 99,
363 ..Default::default()
364 };
365 tm.set(4, 0, e);
366
367 for i in 0..4 {
368 for j in 0..4 {
369 assert_eq!(tm.get(i, j), Some(&TilemapEntry::default()));
370 }
371 }
372 }
373
374 #[test]
379 fn fill_rect_partial() {
380 let mut tm = Tilemap::new(8, 8);
381 let e = TilemapEntry {
382 tile_id: 42,
383 ..Default::default()
384 };
385 tm.fill_rect(2, 3, 4, 2, e);
386 for y in 3..5 {
387 for x in 2..6 {
388 assert_eq!(tm.get(x, y).unwrap().tile_id, 42);
389 }
390 }
391
392 assert_eq!(tm.get(0, 0).unwrap().tile_id, 0);
393 assert_eq!(tm.get(7, 7).unwrap().tile_id, 0);
394 }
395
396 #[test]
397 fn fill_rect_clamped_to_bounds() {
398 let mut tm = Tilemap::new(4, 4);
399 let e = TilemapEntry {
400 tile_id: 99,
401 ..Default::default()
402 };
403 tm.fill_rect(2, 2, 10, 10, e);
404
405 for y in 0..4 {
406 for x in 0..4 {
407 if x >= 2 && y >= 2 {
408 assert_eq!(tm.get(x, y).unwrap().tile_id, 99);
409 } else {
410 assert_eq!(tm.get(x, y).unwrap().tile_id, 0);
411 }
412 }
413 }
414 }
415
416 #[test]
421 fn supports_tile_ids_above_255() {
422 let e = TilemapEntry {
423 tile_id: 1023,
424 ..Default::default()
425 };
426 let mut tm = Tilemap::new(1, 1);
427 tm.set(0, 0, e);
428 assert_eq!(tm.get(0, 0).unwrap().tile_id, 1023);
429 }
430
431 #[test]
432 fn non_square_tilemap() {
433 let tm = Tilemap::new(64, 16);
434 assert_eq!(tm.entries.len(), 1024);
435 assert!(tm.get(63, 15).is_some());
436 assert!(tm.get(64, 0).is_none());
437 }
438}