1use super::SpriteCommand;
2
3#[derive(Clone)]
6pub struct Tilemap {
7 pub width: u32,
8 pub height: u32,
9 pub tile_size: f32,
10 pub texture_id: u32,
11 pub atlas_columns: u32,
12 pub atlas_rows: u32,
13 tiles: Vec<u16>, }
15
16impl Tilemap {
17 pub fn new(
18 texture_id: u32,
19 width: u32,
20 height: u32,
21 tile_size: f32,
22 atlas_columns: u32,
23 atlas_rows: u32,
24 ) -> Self {
25 Self {
26 width,
27 height,
28 tile_size,
29 texture_id,
30 atlas_columns,
31 atlas_rows,
32 tiles: vec![0; (width * height) as usize],
33 }
34 }
35
36 pub fn set_tile(&mut self, gx: u32, gy: u32, tile_id: u16) {
37 if gx < self.width && gy < self.height {
38 self.tiles[(gy * self.width + gx) as usize] = tile_id;
39 }
40 }
41
42 pub fn get_tile(&self, gx: u32, gy: u32) -> u16 {
43 if gx < self.width && gy < self.height {
44 self.tiles[(gy * self.width + gx) as usize]
45 } else {
46 0
47 }
48 }
49
50 pub fn bake_visible(
52 &self,
53 world_offset_x: f32,
54 world_offset_y: f32,
55 layer: i32,
56 camera_x: f32,
57 camera_y: f32,
58 camera_zoom: f32,
59 viewport_w: f32,
60 viewport_h: f32,
61 ) -> Vec<SpriteCommand> {
62 let half_w = viewport_w / (2.0 * camera_zoom);
63 let half_h = viewport_h / (2.0 * camera_zoom);
64
65 let view_left = camera_x - half_w;
67 let view_right = camera_x + half_w;
68 let view_top = camera_y - half_h;
69 let view_bottom = camera_y + half_h;
70
71 let min_gx = ((view_left - world_offset_x) / self.tile_size)
73 .floor()
74 .max(0.0) as u32;
75 let max_gx = ((view_right - world_offset_x) / self.tile_size)
76 .ceil()
77 .min(self.width as f32) as u32;
78 let min_gy = ((view_top - world_offset_y) / self.tile_size)
79 .floor()
80 .max(0.0) as u32;
81 let max_gy = ((view_bottom - world_offset_y) / self.tile_size)
82 .ceil()
83 .min(self.height as f32) as u32;
84
85 let uv_tile_w = 1.0 / self.atlas_columns as f32;
86 let uv_tile_h = 1.0 / self.atlas_rows as f32;
87
88 let mut commands = Vec::new();
89
90 for gy in min_gy..max_gy {
91 for gx in min_gx..max_gx {
92 let tile_id = self.tiles[(gy * self.width + gx) as usize];
93 if tile_id == 0 {
94 continue;
95 }
96
97 let atlas_x = (tile_id as u32 - 1) % self.atlas_columns;
98 let atlas_y = (tile_id as u32 - 1) / self.atlas_columns;
99
100 commands.push(SpriteCommand {
101 texture_id: self.texture_id,
102 x: world_offset_x + gx as f32 * self.tile_size,
103 y: world_offset_y + gy as f32 * self.tile_size,
104 w: self.tile_size,
105 h: self.tile_size,
106 layer,
107 uv_x: atlas_x as f32 * uv_tile_w,
108 uv_y: atlas_y as f32 * uv_tile_h,
109 uv_w: uv_tile_w,
110 uv_h: uv_tile_h,
111 tint_r: 1.0,
112 tint_g: 1.0,
113 tint_b: 1.0,
114 tint_a: 1.0,
115 });
116 }
117 }
118
119 commands
120 }
121}
122
123#[derive(Clone)]
125pub struct TilemapStore {
126 tilemaps: std::collections::HashMap<u32, Tilemap>,
127 next_id: u32,
128}
129
130impl TilemapStore {
131 pub fn new() -> Self {
132 Self {
133 tilemaps: std::collections::HashMap::new(),
134 next_id: 1,
135 }
136 }
137
138 pub fn create(
139 &mut self,
140 texture_id: u32,
141 width: u32,
142 height: u32,
143 tile_size: f32,
144 atlas_columns: u32,
145 atlas_rows: u32,
146 ) -> u32 {
147 let id = self.next_id;
148 self.next_id += 1;
149 self.tilemaps.insert(
150 id,
151 Tilemap::new(texture_id, width, height, tile_size, atlas_columns, atlas_rows),
152 );
153 id
154 }
155
156 pub fn get(&self, id: u32) -> Option<&Tilemap> {
157 self.tilemaps.get(&id)
158 }
159
160 pub fn get_mut(&mut self, id: u32) -> Option<&mut Tilemap> {
161 self.tilemaps.get_mut(&id)
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168
169 #[test]
170 fn test_set_get_tile() {
171 let mut tm = Tilemap::new(1, 4, 4, 16.0, 4, 4);
172 assert_eq!(tm.get_tile(0, 0), 0);
173 tm.set_tile(1, 2, 5);
174 assert_eq!(tm.get_tile(1, 2), 5);
175 tm.set_tile(3, 3, 10);
176 assert_eq!(tm.get_tile(3, 3), 10);
177 }
178
179 #[test]
180 fn test_out_of_bounds() {
181 let mut tm = Tilemap::new(1, 4, 4, 16.0, 4, 4);
182 tm.set_tile(10, 10, 5);
184 assert_eq!(tm.get_tile(10, 10), 0);
186 assert_eq!(tm.get_tile(4, 0), 0);
187 assert_eq!(tm.get_tile(0, 4), 0);
188 }
189
190 #[test]
191 fn test_uv_computation() {
192 let mut tm = Tilemap::new(1, 2, 2, 32.0, 4, 2);
194 tm.set_tile(0, 0, 1); tm.set_tile(1, 0, 2); tm.set_tile(0, 1, 5); let cmds = tm.bake_visible(0.0, 0.0, 0, 32.0, 32.0, 1.0, 200.0, 200.0);
200 assert_eq!(cmds.len(), 3);
201
202 let uv_w = 1.0 / 4.0; let uv_h = 1.0 / 2.0; let c0 = &cmds[0];
207 assert!((c0.uv_x - 0.0).abs() < 1e-5);
208 assert!((c0.uv_y - 0.0).abs() < 1e-5);
209 assert!((c0.uv_w - uv_w).abs() < 1e-5);
210 assert!((c0.uv_h - uv_h).abs() < 1e-5);
211
212 let c1 = &cmds[1];
214 assert!((c1.uv_x - uv_w).abs() < 1e-5);
215 assert!((c1.uv_y - 0.0).abs() < 1e-5);
216
217 let c2 = &cmds[2];
219 assert!((c2.uv_x - 0.0).abs() < 1e-5);
220 assert!((c2.uv_y - uv_h).abs() < 1e-5);
221 }
222
223 #[test]
224 fn test_camera_culling() {
225 let mut tm = Tilemap::new(1, 10, 10, 16.0, 4, 4);
227 for gy in 0..10 {
228 for gx in 0..10 {
229 tm.set_tile(gx, gy, 1);
230 }
231 }
232
233 let cmds = tm.bake_visible(0.0, 0.0, 0, 80.0, 80.0, 1.0, 64.0, 64.0);
236
237 assert!(cmds.len() < 100);
239 assert!(cmds.len() >= 9); assert!(cmds.len() <= 25); for cmd in &cmds {
245 assert!(cmd.x >= 32.0); assert!(cmd.x <= 112.0);
247 assert!(cmd.y >= 32.0);
248 assert!(cmd.y <= 112.0);
249 }
250 }
251
252 #[test]
253 fn test_tile_zero_skipped() {
254 let mut tm = Tilemap::new(1, 3, 3, 16.0, 4, 4);
255 tm.set_tile(1, 1, 3);
257
258 let cmds = tm.bake_visible(0.0, 0.0, 0, 24.0, 24.0, 1.0, 200.0, 200.0);
259 assert_eq!(cmds.len(), 1);
260 assert!((cmds[0].x - 16.0).abs() < 1e-5);
261 assert!((cmds[0].y - 16.0).abs() < 1e-5);
262 }
263
264 #[test]
265 fn test_tilemap_store() {
266 let mut store = TilemapStore::new();
267 let id1 = store.create(1, 4, 4, 16.0, 4, 4);
268 let id2 = store.create(2, 8, 8, 32.0, 8, 8);
269
270 assert_eq!(id1, 1);
271 assert_eq!(id2, 2);
272
273 store.get_mut(id1).unwrap().set_tile(0, 0, 5);
275 assert_eq!(store.get(id1).unwrap().get_tile(0, 0), 5);
276
277 assert!(store.get(99).is_none());
279 assert!(store.get_mut(99).is_none());
280 }
281
282 #[test]
283 fn test_world_offset() {
284 let mut tm = Tilemap::new(1, 2, 2, 16.0, 4, 4);
285 tm.set_tile(0, 0, 1);
286 tm.set_tile(1, 1, 2);
287
288 let cmds = tm.bake_visible(100.0, 200.0, 5, 116.0, 216.0, 1.0, 200.0, 200.0);
290 assert_eq!(cmds.len(), 2);
291
292 assert!((cmds[0].x - 100.0).abs() < 1e-5);
293 assert!((cmds[0].y - 200.0).abs() < 1e-5);
294 assert_eq!(cmds[0].layer, 5);
295
296 assert!((cmds[1].x - 116.0).abs() < 1e-5);
297 assert!((cmds[1].y - 216.0).abs() < 1e-5);
298 }
299
300 #[test]
301 fn test_bake_produces_correct_sprite_fields() {
302 let mut tm = Tilemap::new(42, 1, 1, 24.0, 2, 2);
303 tm.set_tile(0, 0, 3); let cmds = tm.bake_visible(10.0, 20.0, 7, 22.0, 32.0, 1.0, 100.0, 100.0);
306 assert_eq!(cmds.len(), 1);
307
308 let c = &cmds[0];
309 assert_eq!(c.texture_id, 42);
310 assert!((c.x - 10.0).abs() < 1e-5);
311 assert!((c.y - 20.0).abs() < 1e-5);
312 assert!((c.w - 24.0).abs() < 1e-5);
313 assert!((c.h - 24.0).abs() < 1e-5);
314 assert_eq!(c.layer, 7);
315 assert!((c.uv_x - 0.0).abs() < 1e-5);
317 assert!((c.uv_y - 0.5).abs() < 1e-5);
318 assert!((c.uv_w - 0.5).abs() < 1e-5);
319 assert!((c.uv_h - 0.5).abs() < 1e-5);
320 assert!((c.tint_r - 1.0).abs() < 1e-5);
321 assert!((c.tint_g - 1.0).abs() < 1e-5);
322 assert!((c.tint_b - 1.0).abs() < 1e-5);
323 assert!((c.tint_a - 1.0).abs() < 1e-5);
324 }
325}