Skip to main content

shuyi_assets/
tileset.rs

1//! A tileset's terrain tiles (game ADR-0052): the pictures a grid map's
2//! sprite skin paints its ground with. For each look a map may ask for
3//! (outdoors, indoors), a tile for every terrain and the layers painted
4//! over those tiles. A layer's picture is chosen from the tiles around it,
5//! so a terrain grid alone says what the map looks like: a map file only
6//! ever names terrain.
7//!
8//! Two kinds of layer:
9//!
10//! - **Corners**: pictures drawn *between* tiles, each centred on the
11//!   point where four tiles meet, one for each mix of those four being in
12//!   the layer or not. A shore, a road's edge and a forest's outline are
13//!   these: they end where their tiles end.
14//! - **Tiles**: one picture on each tile of the layer (a tower, a bridge),
15//!   which may depend on which of its four neighbours are certain terrains
16//!   (a bridge turns to cross the water).
17//!
18//! These are the tiles as a skin paints them; how a game writes them in a
19//! file, and which terrains it has, are the game's.
20use crate::prelude::*;
21
22use alloc::collections::{BTreeMap, BTreeSet};
23
24use shuyi_core::TerrainId;
25
26use crate::format::AssetKind;
27use crate::image::{ImageId, ImageRect};
28
29/// How many mixes four yes-or-no places have: a layer has a picture for
30/// each (or none).
31pub const MIXES: usize = 16;
32
33/// The character of a place that is in, in a mix as written (`"##.."`).
34pub const MIX_IN: char = '#';
35
36/// The character of a place that is out.
37pub const MIX_OUT: char = '.';
38
39/// The number of a mix of four places, the first the highest bit: for a
40/// corners layer the tiles at the top-left, top-right, bottom-left and
41/// bottom-right of a corner point; for a tile's sides its neighbours up,
42/// right, down and left.
43pub fn mix(places: [bool; 4]) -> u8 {
44    places.iter().fold(0, |m, &p| 2 * m + u8::from(p))
45}
46
47/// The mix written as four of [`MIX_IN`] and [`MIX_OUT`], or `None` if
48/// `text` isn't that.
49pub fn parse_mix(text: &str) -> Option<u8> {
50    let mut places = [false; 4];
51    let mut chars = text.chars();
52    for place in &mut places {
53        *place = match chars.next()? {
54            MIX_IN => true,
55            MIX_OUT => false,
56            _ => return None,
57        };
58    }
59    chars.next().is_none().then(|| mix(places))
60}
61
62/// Mix `m` as written: four of [`MIX_IN`] and [`MIX_OUT`].
63pub fn mix_name(m: u8) -> String {
64    [8, 4, 2, 1]
65        .map(|bit| if m & bit == 0 { MIX_OUT } else { MIX_IN })
66        .iter()
67        .collect()
68}
69
70/// The tileset kind (ADR-0007), with no format of the engine's: how a
71/// game writes its tiles is its own, and its format reads them into this.
72impl AssetKind for TerrainTiles {
73    const NAME: &'static str = "tileset";
74}
75
76/// A tileset's terrain tiles. Build it with [`TerrainTiles::new`]
77/// (ADR-0039).
78#[derive(Debug, Clone, PartialEq, Eq)]
79#[non_exhaustive]
80pub struct TerrainTiles {
81    /// The image the tiles are in.
82    pub image: ImageId,
83    /// A map tile's size, across × down, in pixels: in the image, and on
84    /// screen.
85    pub tile_px: (u32, u32),
86    /// The tileset's own look: what a map is painted as unless
87    /// [`looks`](Self::looks) has the look it names.
88    pub look: Look,
89    /// Its other looks, by the name a map's look gives
90    /// ([`MapLook::tiles`](crate::grid_map::MapLook::tiles)).
91    pub looks: BTreeMap<String, Look>,
92}
93
94impl TerrainTiles {
95    /// The tiles of `image`, `tile_px` each, in the look `look` and no
96    /// other (`looks` starts empty).
97    #[must_use]
98    pub const fn new(image: ImageId, tile_px: (u32, u32), look: Look) -> Self {
99        Self {
100            image,
101            tile_px,
102            look,
103            looks: BTreeMap::new(),
104        }
105    }
106
107    /// The look a map naming `name` is painted as: that look if the
108    /// tileset has it, else its own.
109    pub fn look(&self, name: &str) -> &Look {
110        self.looks.get(name).unwrap_or(&self.look)
111    }
112}
113
114/// A terrain look: how one kind of place (outdoors, indoors) is drawn.
115#[derive(Debug, Clone, PartialEq, Eq, Default)]
116pub struct Look {
117    /// Each terrain's own tile, painted first.
118    pub tiles: BTreeMap<TerrainId, ImageRect>,
119    /// What is painted over the tiles, the first undermost.
120    pub layers: Vec<Layer>,
121}
122
123/// One layer of a [`Look`].
124#[derive(Debug, Clone, PartialEq, Eq)]
125pub enum Layer {
126    /// Pictures between tiles.
127    Corners(CornerLayer),
128    /// A picture on each tile.
129    Tiles(TileLayer),
130}
131
132/// Pictures drawn between tiles: each is one tile big and centred on a
133/// point where four tiles meet.
134#[derive(Debug, Clone, PartialEq, Eq)]
135pub struct CornerLayer {
136    /// The terrains that are in the layer.
137    pub of: BTreeSet<TerrainId>,
138    /// The picture for each [`mix`] of the four tiles round a point being
139    /// in the layer. A mix with none draws nothing; mix 0 never has one.
140    pub tiles: [Option<ImageRect>; MIXES],
141}
142
143/// A picture on each tile of some terrains.
144#[derive(Debug, Clone, PartialEq, Eq)]
145pub struct TileLayer {
146    /// The terrains that get the picture.
147    pub of: BTreeSet<TerrainId>,
148    /// The picture, unless [`sides`](Self::sides) has one.
149    pub tile: ImageRect,
150    /// The terrains a neighbour is looked at for.
151    pub beside: BTreeSet<TerrainId>,
152    /// The picture for a [`mix`] of the tile's four neighbours being one
153    /// of [`beside`](Self::beside), where it differs from
154    /// [`tile`](Self::tile).
155    pub sides: [Option<ImageRect>; MIXES],
156}
157
158impl TileLayer {
159    /// The picture of a tile whose neighbours mix as `sides`.
160    pub fn picture(&self, sides: u8) -> ImageRect {
161        let at = usize::from(sides);
162        self.sides.get(at).copied().flatten().unwrap_or(self.tile)
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169    use crate::image::ImageTable;
170
171    #[test]
172    fn a_mix_is_four_places_the_first_the_highest_bit() {
173        assert_eq!(mix([false; 4]), 0);
174        assert_eq!(mix([true, false, false, false]), 8);
175        assert_eq!(mix([false, true, false, false]), 4);
176        assert_eq!(mix([false, false, true, false]), 2);
177        assert_eq!(mix([false, false, false, true]), 1);
178        assert_eq!(mix([true; 4]), 15);
179        assert_eq!(mix([true, false, true, true]), 11);
180    }
181
182    #[test]
183    fn a_mix_is_written_as_four_of_hash_and_dot() {
184        for m in 0..16 {
185            assert_eq!(parse_mix(&mix_name(m)), Some(m), "{m}");
186        }
187        assert_eq!(mix_name(0), "....");
188        assert_eq!(mix_name(8), "#...");
189        assert_eq!(mix_name(5), ".#.#");
190        assert_eq!(mix_name(15), "####");
191        assert_eq!(parse_mix("#..#"), Some(9));
192        for bad in ["", "#", "###", "#####", "##.x", "x#..", "##. "] {
193            assert_eq!(parse_mix(bad), None, "{bad:?}");
194        }
195    }
196
197    fn rect(x: u32) -> ImageRect {
198        ImageRect {
199            x,
200            y: 0,
201            w: 16,
202            h: 16,
203        }
204    }
205
206    #[test]
207    fn a_tile_layer_shows_its_sides_picture_where_it_has_one() {
208        let mut sides = [None; MIXES];
209        sides[5] = Some(rect(32));
210        let layer = TileLayer {
211            of: BTreeSet::new(),
212            tile: rect(16),
213            beside: BTreeSet::new(),
214            sides,
215        };
216        assert_eq!(layer.picture(5), rect(32));
217        assert_eq!(layer.picture(0), rect(16));
218        assert_eq!(layer.picture(15), rect(16));
219        // No such mix: the plain picture.
220        assert_eq!(layer.picture(16), rect(16));
221        assert_eq!(layer.picture(u8::MAX), rect(16));
222    }
223
224    #[test]
225    fn a_map_is_painted_in_the_look_it_names_if_the_tileset_has_it() {
226        let table = ImageTable {
227            images: [(
228                "tiles.png",
229                crate::image::ImageInfo {
230                    width: 64,
231                    height: 64,
232                },
233            )]
234            .into(),
235        };
236        let own = Look {
237            tiles: [(TerrainId(0), rect(0))].into(),
238            layers: Vec::new(),
239        };
240        let indoor = Look {
241            tiles: [(TerrainId(0), rect(16))].into(),
242            layers: Vec::new(),
243        };
244        let tiles = TerrainTiles {
245            image: table.id("tiles.png").unwrap(),
246            tile_px: (16, 16),
247            look: own.clone(),
248            looks: [("indoor".to_owned(), indoor.clone())].into(),
249        };
250        assert_eq!(tiles.look("indoor"), &indoor);
251        assert_eq!(tiles.look("outdoor"), &own);
252        assert_eq!(tiles.look("cave"), &own);
253    }
254
255    #[test]
256    fn new_tiles_are_the_literal_with_no_other_look() {
257        let look = Look {
258            tiles: BTreeMap::new(),
259            layers: alloc::vec::Vec::new(),
260        };
261        let images = ImageTable::new([("tiles.png", crate::image::ImageInfo::new(48, 24))].into());
262        let image = images.id("tiles.png").unwrap();
263        assert_eq!(
264            TerrainTiles::new(image, (24, 24), look.clone()),
265            TerrainTiles {
266                image,
267                tile_px: (24, 24),
268                look,
269                looks: BTreeMap::new(),
270            }
271        );
272    }
273}