1use crate::prelude::*;
21
22use alloc::collections::{BTreeMap, BTreeSet};
23
24use shuyi_core::TerrainId;
25
26use crate::format::AssetKind;
27use crate::image::{ImageId, ImageRect};
28
29pub const MIXES: usize = 16;
32
33pub const MIX_IN: char = '#';
35
36pub const MIX_OUT: char = '.';
38
39pub fn mix(places: [bool; 4]) -> u8 {
44 places.iter().fold(0, |m, &p| 2 * m + u8::from(p))
45}
46
47pub 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
62pub 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
70impl AssetKind for TerrainTiles {
73 const NAME: &'static str = "tileset";
74}
75
76#[derive(Debug, Clone, PartialEq, Eq)]
79#[non_exhaustive]
80pub struct TerrainTiles {
81 pub image: ImageId,
83 pub tile_px: (u32, u32),
86 pub look: Look,
89 pub looks: BTreeMap<String, Look>,
92}
93
94impl TerrainTiles {
95 #[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 pub fn look(&self, name: &str) -> &Look {
110 self.looks.get(name).unwrap_or(&self.look)
111 }
112}
113
114#[derive(Debug, Clone, PartialEq, Eq, Default)]
116pub struct Look {
117 pub tiles: BTreeMap<TerrainId, ImageRect>,
119 pub layers: Vec<Layer>,
121}
122
123#[derive(Debug, Clone, PartialEq, Eq)]
125pub enum Layer {
126 Corners(CornerLayer),
128 Tiles(TileLayer),
130}
131
132#[derive(Debug, Clone, PartialEq, Eq)]
135pub struct CornerLayer {
136 pub of: BTreeSet<TerrainId>,
138 pub tiles: [Option<ImageRect>; MIXES],
141}
142
143#[derive(Debug, Clone, PartialEq, Eq)]
145pub struct TileLayer {
146 pub of: BTreeSet<TerrainId>,
148 pub tile: ImageRect,
150 pub beside: BTreeSet<TerrainId>,
152 pub sides: [Option<ImageRect>; MIXES],
156}
157
158impl TileLayer {
159 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 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}