1use std::collections::HashSet;
8use std::path::Path;
9
10use anyhow::{Context, Result};
11use serde::{Deserialize, Serialize};
12
13fn default_true() -> bool {
14 true
15}
16
17fn default_one() -> u32 {
18 1
19}
20
21#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
23pub struct TileCell(pub i32, pub i32, pub String);
24
25impl TileCell {
26 pub fn new(col: i32, row: i32, frame: impl Into<String>) -> Self {
28 Self(col, row, frame.into())
29 }
30
31 pub fn col(&self) -> i32 {
33 self.0
34 }
35
36 pub fn row(&self) -> i32 {
38 self.1
39 }
40
41 pub fn frame(&self) -> &str {
43 &self.2
44 }
45}
46
47#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
49pub struct TileMapLayer {
50 pub name: String,
52 #[serde(default = "default_true")]
54 pub visible: bool,
55 #[serde(default)]
57 pub collision_enabled: bool,
58 #[serde(default = "default_one")]
60 pub collision_layer: u32,
61 #[serde(default = "default_one")]
63 pub collision_mask: u32,
64 #[serde(default, skip_serializing_if = "Option::is_none")]
66 pub solid_frames: Option<Vec<String>>,
67 #[serde(default)]
69 pub tiles: Vec<TileCell>,
70}
71
72impl TileMapLayer {
73 pub fn new(name: impl Into<String>) -> Self {
74 Self {
75 name: name.into(),
76 visible: true,
77 collision_enabled: false,
78 collision_layer: 1,
79 collision_mask: 1,
80 solid_frames: None,
81 tiles: Vec::new(),
82 }
83 }
84}
85
86#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
88pub struct TileMapDoc {
89 pub atlas: String,
90 pub tile_size: [u32; 2],
91 #[serde(default, rename = "layer")]
92 pub layers: Vec<TileMapLayer>,
93}
94
95impl TileMapDoc {
96 pub fn new(atlas: impl Into<String>, tile_w: u32, tile_h: u32) -> Self {
97 Self {
98 atlas: atlas.into(),
99 tile_size: [tile_w, tile_h],
100 layers: Vec::new(),
101 }
102 }
103
104 pub fn layer(&self, idx: usize) -> Option<&TileMapLayer> {
105 self.layers.get(idx)
106 }
107
108 pub fn layer_mut(&mut self, idx: usize) -> Option<&mut TileMapLayer> {
109 self.layers.get_mut(idx)
110 }
111
112 pub fn layer_names(&self) -> Vec<&str> {
113 self.layers.iter().map(|l| l.name.as_str()).collect()
114 }
115
116 pub fn get_cell(&self, layer_idx: usize, col: i32, row: i32) -> Option<&str> {
117 let layer = self.layers.get(layer_idx)?;
118 layer
119 .tiles
120 .iter()
121 .find(|t| t.0 == col && t.1 == row)
122 .map(|t| t.2.as_str())
123 }
124
125 pub fn set_cell(&mut self, layer_idx: usize, col: i32, row: i32, frame: impl Into<String>) {
126 if let Some(layer) = self.layers.get_mut(layer_idx) {
127 let frame = frame.into();
128 if let Some(cell) = layer.tiles.iter_mut().find(|t| t.0 == col && t.1 == row) {
129 cell.2 = frame;
130 } else {
131 layer.tiles.push(TileCell(col, row, frame));
132 }
133 }
134 }
135
136 pub fn erase_cell(&mut self, layer_idx: usize, col: i32, row: i32) -> bool {
137 if let Some(layer) = self.layers.get_mut(layer_idx) {
138 if let Some(pos) = layer.tiles.iter().position(|t| t.0 == col && t.1 == row) {
139 layer.tiles.remove(pos);
140 return true;
141 }
142 }
143 false
144 }
145
146 pub fn clear_layer(&mut self, layer_idx: usize) {
147 if let Some(layer) = self.layers.get_mut(layer_idx) {
148 layer.tiles.clear();
149 }
150 }
151
152 pub fn used_cells(&self, layer_idx: usize) -> Vec<(i32, i32)> {
153 let Some(layer) = self.layers.get(layer_idx) else {
154 return Vec::new();
155 };
156 layer.tiles.iter().map(|t| (t.0, t.1)).collect()
157 }
158
159 pub fn solid_cells(&self, layer_idx: usize) -> Vec<(i32, i32)> {
161 let Some(layer) = self.layers.get(layer_idx) else {
162 return Vec::new();
163 };
164 if !layer.collision_enabled {
165 return Vec::new();
166 }
167 match &layer.solid_frames {
168 Some(solids) => {
169 let solid_set: HashSet<&str> = solids.iter().map(String::as_str).collect();
170 layer
171 .tiles
172 .iter()
173 .filter(|t| solid_set.contains(t.2.as_str()))
174 .map(|t| (t.0, t.1))
175 .collect()
176 }
177 None => layer.tiles.iter().map(|t| (t.0, t.1)).collect(),
178 }
179 }
180}
181
182pub fn world_to_map(world: (f64, f64), origin: (f64, f64), tile_size: (f64, f64)) -> (i32, i32) {
184 let w = if tile_size.0 <= 0.0 {
185 16.0
186 } else {
187 tile_size.0
188 };
189 let h = if tile_size.1 <= 0.0 {
190 16.0
191 } else {
192 tile_size.1
193 };
194 let col = ((world.0 - origin.0) / w).floor() as i32;
195 let row = ((world.1 - origin.1) / h).floor() as i32;
196 (col, row)
197}
198
199pub fn map_to_world(cell: (i32, i32), origin: (f64, f64), tile_size: (f64, f64)) -> (f64, f64) {
201 let w = if tile_size.0 <= 0.0 {
202 16.0
203 } else {
204 tile_size.0
205 };
206 let h = if tile_size.1 <= 0.0 {
207 16.0
208 } else {
209 tile_size.1
210 };
211 (
212 origin.0 + (cell.0 as f64 + 0.5) * w,
213 origin.1 + (cell.1 as f64 + 0.5) * h,
214 )
215}
216
217pub fn parse_tilemap_str(text: &str) -> Result<TileMapDoc> {
219 toml::from_str(text).context("invalid tilemap TOML")
220}
221
222pub fn load_tilemap(path: &Path) -> Result<TileMapDoc> {
224 let text = std::fs::read_to_string(path)
225 .with_context(|| format!("cannot read tilemap {}", path.display()))?;
226 parse_tilemap_str(&text).with_context(|| format!("cannot parse tilemap {}", path.display()))
227}
228
229pub fn save_tilemap(path: &Path, doc: &TileMapDoc) -> Result<()> {
231 let text = toml::to_string(doc).context("cannot serialize tilemap")?;
232 std::fs::write(path, text).with_context(|| format!("cannot write tilemap {}", path.display()))
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238
239 #[test]
240 fn roundtrip_tilemap_toml() {
241 let toml_src = r#"atlas = "res://assets/tileset.atlas.json"
242tile_size = [16, 16]
243
244[[layer]]
245name = "ground"
246visible = true
247collision_enabled = false
248collision_layer = 1
249collision_mask = 1
250tiles = [
251 [0, 0, "grass"],
252 [1, 0, "grass"],
253 [2, 0, "path"],
254]
255
256[[layer]]
257name = "walls"
258visible = true
259collision_enabled = true
260collision_layer = 2
261collision_mask = 1
262solid_frames = ["wall_brick"]
263tiles = [
264 [0, 1, "wall_brick"],
265 [1, 1, "wall_window"],
266]
267"#;
268 let doc = parse_tilemap_str(toml_src).unwrap();
269 assert_eq!(doc.atlas, "res://assets/tileset.atlas.json");
270 assert_eq!(doc.tile_size, [16, 16]);
271 assert_eq!(doc.layers.len(), 2);
272 assert_eq!(doc.get_cell(0, 0, 0), Some("grass"));
273 assert_eq!(doc.get_cell(0, 2, 0), Some("path"));
274 assert_eq!(doc.get_cell(0, 3, 0), None);
275
276 let solids = doc.solid_cells(1);
278 assert_eq!(solids, vec![(0, 1)]);
279
280 let serialized = toml::to_string(&doc).unwrap();
282 let doc2 = parse_tilemap_str(&serialized).unwrap();
283 assert_eq!(doc, doc2);
284 }
285
286 #[test]
287 fn coordinate_conversion() {
288 let origin = (100.0, 200.0);
289 let size = (16.0, 16.0);
290
291 let cell = (2, 3);
292 let world = map_to_world(cell, origin, size);
293 assert_eq!(world, (100.0 + 2.5 * 16.0, 200.0 + 3.5 * 16.0));
294
295 let back_cell = world_to_map(world, origin, size);
296 assert_eq!(back_cell, cell);
297
298 let neg_cell = (-3, -5);
300 let neg_world = map_to_world(neg_cell, origin, size);
301 let back_neg = world_to_map(neg_world, origin, size);
302 assert_eq!(back_neg, neg_cell);
303 }
304
305 #[test]
306 fn mutations() {
307 let mut doc = TileMapDoc::new("res://tiles.atlas.json", 16, 16);
308 doc.layers.push(TileMapLayer::new("main"));
309 assert_eq!(doc.get_cell(0, 0, 0), None);
310
311 doc.set_cell(0, 0, 0, "stone");
312 assert_eq!(doc.get_cell(0, 0, 0), Some("stone"));
313
314 doc.set_cell(0, 0, 0, "wood");
316 assert_eq!(doc.get_cell(0, 0, 0), Some("wood"));
317
318 assert!(doc.erase_cell(0, 0, 0));
320 assert_eq!(doc.get_cell(0, 0, 0), None);
321 assert!(!doc.erase_cell(0, 0, 0));
322 }
323}