simple_game_utils/tiles/
tilemap.rs

1use crate::prelude::*;
2use log::error;
3use std::collections::HashMap;
4use std::fmt::Debug;
5use std::rc::Rc;
6
7#[derive(Debug, Clone)]
8pub struct Tilemap<Image: Debug + Clone> {
9    ///index to `image`
10    tiles: Vec<usize>,
11    ///flags per tile, same size as `tiles`
12    flags: Vec<u32>,
13    size: MapSize,
14    ///number of tiles visible on screen
15    visible_size: MapSize,
16    ///top left offset for rendering (in tiles)
17    offset: MapPosition,
18    images: Vec<Rc<Image>>,
19    tile_size: (u32, u32),
20    subtile_offset: (i16, i16),
21    default_start: MapPosition,
22    exits: Vec<MapExit>,
23}
24
25impl<Image: Debug + Clone> Tilemap<Image> {
26    #[allow(clippy::too_many_arguments)]
27    pub fn new(
28        tiles: Vec<usize>,
29        flags: Vec<u32>,
30        size: MapSize,
31        tile_idx_image_name: Vec<String>,
32        tileset: Tileset<Image>,
33        render_size: (u32, u32),
34        default_start: MapPosition,
35        exits: Vec<MapExit>,
36    ) -> Result<Self, GameUtilError> {
37        let mut images = vec![];
38
39        let mut missing = vec![];
40        for name in tile_idx_image_name {
41            if let Some(img) = tileset.find_by_name(&name) {
42                images.push(Rc::new(img.clone()));
43            } else {
44                missing.push(name);
45            }
46        }
47        if !missing.is_empty() {
48            return Err(GameUtilError::InvalidTileset(
49                String::from("from code"),
50                missing,
51            ));
52        }
53
54        let mut visible_size = MapSize::new(
55            render_size.0 / tileset.tilesize().0,
56            render_size.1 / tileset.tilesize().1,
57        );
58        visible_size.w = visible_size.w.min(size.w);
59        visible_size.h = visible_size.h.min(size.h);
60
61        Ok(Self {
62            tiles,
63            flags,
64            size,
65            tile_size: tileset.tilesize(),
66            visible_size,
67            offset: MapPosition::new(0, 0),
68            images,
69            subtile_offset: (0, 0),
70            default_start,
71            exits,
72        })
73    }
74}
75
76impl<Image: Debug + Clone> Tilemap<Image> {
77    /// Pixel coord for tile
78    /// Result may be offscreen, before or after
79    pub fn px_for_tile<P: Into<MapPosition>>(&self, tile: P) -> (isize, isize) {
80        let tile = tile.into();
81        (
82            (self.tile_size.0 * tile.x) as isize - (self.tile_size.0 * self.offset.x) as isize
83                + self.subtile_offset.0 as isize,
84            (self.tile_size.1 * tile.y) as isize - (self.tile_size.1 * self.offset.y) as isize
85                + self.subtile_offset.1 as isize,
86        )
87    }
88
89    /// Pixel coord for tile, ignoring subtile offset
90    /// Result may be offscreen, before or after
91    pub fn orig_px_for_tile<P: Into<MapPosition>>(&self, tile: P) -> (isize, isize) {
92        let tile = tile.into();
93        (
94            (self.tile_size.0 * tile.x) as isize - (self.tile_size.0 * self.offset.x) as isize,
95            (self.tile_size.1 * tile.y) as isize - (self.tile_size.1 * self.offset.y) as isize,
96        )
97    }
98
99    /// Returns the pos of the first tile with at least one pixel visible
100    ///
101    /// Will match [Tilemap::first_visible_tile] unless a subtile offset is set
102    pub fn first_visible_tile(&self) -> MapPosition {
103        if self.subtile_offset == (0, 0) {
104            self.offset
105        } else {
106            let (x_offset, y_offset) = self.tiles_visible_from_subtile_offset();
107            let mut offset = self.offset;
108            offset.x = offset.x.saturating_add_signed(x_offset);
109            offset.y = offset.y.saturating_add_signed(y_offset);
110            offset
111        }
112    }
113
114    fn tiles_visible_from_subtile_offset(&self) -> (i32, i32) {
115        let x_offset = self.subtile_offset.0 as f64 / self.tile_size.0 as f64;
116        let x_offset = if x_offset.is_sign_positive() {
117            x_offset.ceil()
118        } else {
119            x_offset.floor()
120        } as i32;
121        let y_offset = self.subtile_offset.1 as f64 / self.tile_size.1 as f64;
122        let y_offset = if y_offset.is_sign_positive() {
123            y_offset.ceil()
124        } else {
125            y_offset.floor()
126        } as i32;
127        (x_offset, y_offset)
128    }
129
130    /// Returns the pos of the first fully visible tile
131    ///
132    /// See [Tilemap::first_visible_tile]
133    pub fn first_fully_visible_tile(&self) -> MapPosition {
134        self.offset
135    }
136
137    /// Returns the pos of the last tile with at least one pixel visible
138    ///
139    /// Will match [Tilemap::last_visible_tile] unless a subtile offset is set
140    pub fn last_visible_tile(&self) -> MapPosition {
141        let mut offset = self.offset;
142        offset.x += self.visible_size.w;
143        offset.y += self.visible_size.h;
144        if self.subtile_offset == (0, 0) {
145            offset
146        } else {
147            let (x_offset, y_offset) = self.tiles_visible_from_subtile_offset();
148            offset.x = offset.x.saturating_add_signed(x_offset);
149            offset.y = offset.y.saturating_add_signed(y_offset);
150            offset
151        }
152    }
153
154    /// Returns the pos of the last fully visible tile
155    ///
156    /// See [Tilemap::last_visible_tile]
157    pub fn last_fully_visible_tile(&self) -> MapPosition {
158        let mut offset = self.offset;
159        offset.x += self.visible_size.w;
160        offset.y += self.visible_size.h;
161        offset
162    }
163
164    /// Returns true if `tile` is inside the map
165    pub fn is_inside<P: Into<MapPosition>>(&self, tile: P) -> bool {
166        let tile = tile.into();
167        tile.x < self.size.w && tile.y < self.size.h
168    }
169
170    fn tile_idx<P: Into<MapPosition>>(&self, tile: P) -> Option<usize> {
171        let tile = tile.into();
172        let i = tile.to_idx(self.size);
173        if i < self.tiles.len() {
174            Some(i)
175        } else {
176            None
177        }
178    }
179
180    fn tile_pos(&self, tile: usize) -> Option<MapPosition> {
181        if tile < self.tiles.len() {
182            Some(MapPosition::from_idx(tile, self.size))
183        } else {
184            None
185        }
186    }
187
188    /// Loops through all visible tiles
189    /// calling `render` with the image and px coord
190    pub fn draw<F: FnMut(&Image, (isize, isize))>(&self, mut render: F) {
191        for x in 0..self.visible_size.w {
192            for y in 0..self.visible_size.h {
193                let x = x.saturating_add(self.offset.x);
194                let y = y.saturating_add(self.offset.y);
195                let i = (x + y * self.size.w) as usize;
196                if i < self.tiles.len() {
197                    render(&self.images[self.tiles[i]], self.px_for_tile((x, y)))
198                }
199            }
200        }
201    }
202
203    #[inline]
204    pub fn update_pos_with_offset(&self, pos: (isize, isize)) -> (isize, isize) {
205        (
206            pos.0 - self.subtile_offset.0 as isize,
207            pos.1 - self.subtile_offset.1 as isize,
208        )
209    }
210
211    /// Moves center of visible map to `pos`
212    pub fn center_on<P: Into<MapPosition>>(&mut self, pos: P) {
213        let pos = pos.into();
214        self.offset.x = pos.x.saturating_sub(self.visible_size.w / 2);
215        self.offset.y = pos.y.saturating_sub(self.visible_size.h / 2);
216        self.offset.x = self.offset.x.min(self.size.w - self.visible_size.w);
217        self.offset.y = self.offset.y.min(self.size.h - self.visible_size.h);
218    }
219
220    /// Returns a list of tiles matching `flag`
221    pub fn all_tiles_with_flag(&self, flag: u32) -> Vec<MapPosition> {
222        self.flags
223            .iter()
224            .enumerate()
225            .filter_map(|(i, tile_flag)| {
226                if *tile_flag & flag == flag {
227                    Some(self.tile_pos(i))
228                } else {
229                    None
230                }
231            })
232            .flatten()
233            .collect()
234    }
235
236    /// Returns true if `tile` has a flag of `value`
237    pub fn tile_has_flag<P: Into<MapPosition>>(&self, tile: P, value: u32) -> bool {
238        let tile = tile.into();
239        if let Some(i) = self.tile_idx(tile) {
240            value & self.flags[i] == value
241        } else {
242            false
243        }
244    }
245
246    /// Returns flag value for `tile`
247    pub fn flags_for_tile<P: Into<MapPosition>>(&self, tile: P) -> u32 {
248        let tile = tile.into();
249        if let Some(i) = self.tile_idx(tile) {
250            self.flags[i]
251        } else {
252            0
253        }
254    }
255
256    /// Sets the flag value for `tile`
257    pub fn set_flag<P: Into<MapPosition>>(&mut self, tile: P, value: u32) {
258        let tile = tile.into();
259        if let Some(i) = self.tile_idx(tile) {
260            self.flags[i] |= value;
261        } else {
262            error!("set_flag({tile:?}, {value}) outside of map")
263        }
264    }
265
266    /// Removes specified flags for `tile`
267    pub fn clear_flag<P: Into<MapPosition>>(&mut self, tile: P, value: u32) {
268        let tile = tile.into();
269        if let Some(i) = self.tile_idx(tile) {
270            self.flags[i] -= value;
271        } else {
272            error!("clear_flag({tile:?}, {value}) outside of map")
273        }
274    }
275
276    /// The default, safe start position on the map
277    pub fn default_start(&self) -> MapPosition {
278        self.default_start
279    }
280
281    /// All exits to other maps
282    pub fn exits(&self) -> &Vec<MapExit> {
283        &self.exits
284    }
285
286    pub fn tile_size(&self) -> (u32, u32) {
287        self.tile_size
288    }
289
290    pub fn size(&self) -> MapSize {
291        self.size
292    }
293
294    /// Sets a pixel offset for drawing
295    /// Primarily designed for smoothing animation the map when a character or camera is moving
296    pub fn set_subtile_offset(&mut self, subtile_offset: (i16, i16)) {
297        self.subtile_offset = subtile_offset;
298    }
299
300    /// Returns a pixel offset for drawing
301    pub fn subtile_offset(&self) -> (i16, i16) {
302        self.subtile_offset
303    }
304}
305
306impl TilemapFile {
307    pub fn into_tilemap<Image: Debug + Clone>(
308        self,
309        tileset: &Tileset<Image>,
310        visible_area_px: (u32, u32),
311    ) -> Result<Tilemap<Image>, GameUtilError> {
312        let mut images = vec![];
313        let mut flag_map = HashMap::new();
314        let mut missing = vec![];
315        for (i, tile) in self.tiles.iter().enumerate() {
316            if let Some(img) = tileset.find_by_name(&tile.image) {
317                flag_map.insert(i, tile.flags);
318                images.push(Rc::new(img.clone()));
319            } else {
320                missing.push(tile.image.clone());
321            }
322        }
323        if !missing.is_empty() {
324            return Err(GameUtilError::InvalidTileset(self.name.clone(), missing));
325        }
326        let size: MapSize = (self.map[0].len() as u32, self.map.len() as u32).into();
327        let mut visible_size: MapSize = (
328            visible_area_px.0 / tileset.tilesize().0,
329            visible_area_px.1 / tileset.tilesize().1,
330        )
331            .into();
332        visible_size.w = visible_size.w.min(size.w);
333        visible_size.h = visible_size.h.min(size.h);
334        let mut flags = vec![];
335        let mut tiles = vec![];
336        for row in &self.map {
337            for tile_idx in row {
338                let idx = *tile_idx as usize;
339                tiles.push(idx);
340                flags.push(flag_map[&idx]);
341            }
342        }
343        Ok(Tilemap {
344            tiles,
345            flags,
346            size,
347            visible_size,
348            offset: MapPosition::new(0, 0),
349            images,
350            tile_size: tileset.tilesize(),
351            subtile_offset: (0, 0),
352            default_start: self.data.start.into(),
353            exits: self
354                .data
355                .exits
356                .into_iter()
357                .map(MapExit::from_file)
358                .collect(),
359        })
360    }
361}
362
363#[cfg(test)]
364mod test {
365    use super::*;
366
367    const SAMPLE_RON: &str = r#"(
368        name: "Desert Temple",
369        tileset: "desert",
370        flags: {
371            1: "wall",
372            2: "trap"
373        },
374        tiles: [
375            (
376                image: "sand",
377                flags: 0
378            ),
379            (
380                image: "temple_floor",
381                flags: 0
382            ),
383            (
384                image: "temple_wall",
385                flags: 1
386            ),
387        ],
388        map: [
389            [2,2,2,2],
390            [2,0,0,2],
391            [2,0,0,2],
392            [2,1,2,2],
393        ],
394        data: (
395            start: (1,2),
396            exits: [
397                (1,3,"desert",4,5),
398            ]
399        )
400    )"#;
401
402    #[test]
403    fn test_loading() {
404        let tilemap_file: TilemapFile = ron::from_str(SAMPLE_RON).unwrap();
405        assert_eq!(tilemap_file.name, String::from("Desert Temple"));
406        assert_eq!(tilemap_file.tileset, String::from("desert"));
407        assert_eq!(
408            tilemap_file.data,
409            TilemapDataDescriptor {
410                start: (1, 2),
411                exits: vec![(1, 3, "desert".to_string(), 4, 5)],
412            }
413        );
414        assert_eq!(
415            tilemap_file.flags,
416            HashMap::from([(1, "wall".to_string()), (2, "trap".to_string())])
417        );
418        assert_eq!(
419            tilemap_file.map,
420            vec![
421                vec![2, 2, 2, 2],
422                vec![2, 0, 0, 2],
423                vec![2, 0, 0, 2],
424                vec![2, 1, 2, 2]
425            ]
426        );
427        assert_eq!(
428            tilemap_file.tiles,
429            vec![
430                TileDescriptor {
431                    image: "sand".to_string(),
432                    flags: 0,
433                },
434                TileDescriptor {
435                    image: "temple_floor".to_string(),
436                    flags: 0,
437                },
438                TileDescriptor {
439                    image: "temple_wall".to_string(),
440                    flags: 1,
441                }
442            ]
443        );
444    }
445
446    #[test]
447    fn init() {
448        let tileset =
449            Tileset::<&'static str>::new(vec![Rc::new("img")], vec!["img".to_string()], (16, 16));
450        let idx_map = vec!["img".to_string()];
451        let tilemap = Tilemap::new(
452            vec![0; 400],
453            vec![0; 400],
454            MapSize::new(20, 20),
455            idx_map,
456            tileset,
457            (300, 200),
458            MapPosition::new(0, 0),
459            vec![],
460        )
461        .unwrap();
462
463        assert_eq!(tilemap.offset, MapPosition::new(0, 0));
464        assert_eq!(tilemap.visible_size, MapSize::new(18, 12));
465        assert_eq!(tilemap.orig_px_for_tile((0_u32, 0)), (0, 0));
466        assert_eq!(tilemap.orig_px_for_tile((4_u32, 4)), (64, 64));
467        assert_eq!(tilemap.first_visible_tile(), MapPosition::new(0, 0));
468    }
469
470    #[test]
471    fn offset() {
472        let tileset =
473            Tileset::<&'static str>::new(vec![Rc::new("img")], vec!["img".to_string()], (16, 16));
474        let idx_map = vec!["img".to_string()];
475        let mut tilemap = Tilemap::new(
476            vec![0; 400],
477            vec![0; 400],
478            MapSize::new(20, 20),
479            idx_map,
480            tileset,
481            (300, 200),
482            MapPosition::new(0, 0),
483            vec![],
484        )
485        .unwrap();
486        tilemap.center_on(MapPosition::new(10, 6));
487
488        assert_eq!(tilemap.offset, MapPosition::new(1, 0));
489        assert_eq!(tilemap.visible_size, MapSize::new(18, 12));
490        assert_eq!(tilemap.orig_px_for_tile((0_u32, 0)), (-16, 0));
491        assert_eq!(tilemap.orig_px_for_tile((4_u32, 4)), (48, 64));
492        assert_eq!(tilemap.first_visible_tile(), MapPosition::new(1, 0));
493    }
494
495    #[allow(non_snake_case)]
496    #[test]
497    fn flags() {
498        let tileset =
499            Tileset::<&'static str>::new(vec![Rc::new("img")], vec!["img".to_string()], (16, 16));
500        let idx_map = vec!["img".to_string()];
501        let FLAG_WALL = 0b0001;
502        let FLAG_TRAP = 0b0010;
503        let mut flags = vec![0; 20];
504        flags[1] = FLAG_WALL;
505        flags[2] = FLAG_WALL;
506        flags[5] = FLAG_TRAP;
507        let mut tilemap = Tilemap::new(
508            vec![0; 20],
509            flags,
510            MapSize::new(4, 5),
511            idx_map,
512            tileset,
513            (300, 200),
514            MapPosition::new(0, 0),
515            vec![],
516        )
517        .unwrap();
518        tilemap.set_flag((3_u32, 3), FLAG_WALL);
519        tilemap.clear_flag((2_u32, 0), FLAG_WALL);
520        assert!(tilemap.tile_has_flag((1_u32, 0), FLAG_WALL));
521        assert!(!tilemap.tile_has_flag((1_u32, 0), FLAG_TRAP));
522        assert_eq!(tilemap.flags_for_tile((1_u32, 0)), FLAG_WALL);
523        assert_eq!(tilemap.flags_for_tile((3_u32, 0)), 0);
524        assert_eq!(
525            tilemap.all_tiles_with_flag(FLAG_WALL),
526            vec![MapPosition::new(1, 0), MapPosition::new(3, 3)]
527        );
528    }
529}