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 rotation: 0.0,
116 origin_x: 0.5,
117 origin_y: 0.5,
118 flip_x: false,
119 flip_y: false,
120 opacity: 1.0,
121 blend_mode: 0,
122 shader_id: 0,
123 });
124 }
125 }
126
127 commands
128 }
129}
130
131#[derive(Clone)]
133pub struct TilemapStore {
134 tilemaps: std::collections::HashMap<u32, Tilemap>,
135 next_id: u32,
136}
137
138impl TilemapStore {
139 pub fn new() -> Self {
140 Self {
141 tilemaps: std::collections::HashMap::new(),
142 next_id: 1,
143 }
144 }
145
146 pub fn create(
147 &mut self,
148 texture_id: u32,
149 width: u32,
150 height: u32,
151 tile_size: f32,
152 atlas_columns: u32,
153 atlas_rows: u32,
154 ) -> u32 {
155 let id = self.next_id;
156 self.next_id += 1;
157 self.tilemaps.insert(
158 id,
159 Tilemap::new(texture_id, width, height, tile_size, atlas_columns, atlas_rows),
160 );
161 id
162 }
163
164 pub fn get(&self, id: u32) -> Option<&Tilemap> {
165 self.tilemaps.get(&id)
166 }
167
168 pub fn get_mut(&mut self, id: u32) -> Option<&mut Tilemap> {
169 self.tilemaps.get_mut(&id)
170 }
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176
177 #[test]
178 fn test_set_get_tile() {
179 let mut tm = Tilemap::new(1, 4, 4, 16.0, 4, 4);
180 assert_eq!(tm.get_tile(0, 0), 0);
181 tm.set_tile(1, 2, 5);
182 assert_eq!(tm.get_tile(1, 2), 5);
183 tm.set_tile(3, 3, 10);
184 assert_eq!(tm.get_tile(3, 3), 10);
185 }
186
187 #[test]
188 fn test_out_of_bounds() {
189 let mut tm = Tilemap::new(1, 4, 4, 16.0, 4, 4);
190 tm.set_tile(10, 10, 5);
192 assert_eq!(tm.get_tile(10, 10), 0);
194 assert_eq!(tm.get_tile(4, 0), 0);
195 assert_eq!(tm.get_tile(0, 4), 0);
196 }
197
198 #[test]
199 fn test_uv_computation() {
200 let mut tm = Tilemap::new(1, 2, 2, 32.0, 4, 2);
202 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);
208 assert_eq!(cmds.len(), 3);
209
210 let uv_w = 1.0 / 4.0; let uv_h = 1.0 / 2.0; let c0 = &cmds[0];
215 assert!((c0.uv_x - 0.0).abs() < 1e-5);
216 assert!((c0.uv_y - 0.0).abs() < 1e-5);
217 assert!((c0.uv_w - uv_w).abs() < 1e-5);
218 assert!((c0.uv_h - uv_h).abs() < 1e-5);
219
220 let c1 = &cmds[1];
222 assert!((c1.uv_x - uv_w).abs() < 1e-5);
223 assert!((c1.uv_y - 0.0).abs() < 1e-5);
224
225 let c2 = &cmds[2];
227 assert!((c2.uv_x - 0.0).abs() < 1e-5);
228 assert!((c2.uv_y - uv_h).abs() < 1e-5);
229 }
230
231 #[test]
232 fn test_camera_culling() {
233 let mut tm = Tilemap::new(1, 10, 10, 16.0, 4, 4);
235 for gy in 0..10 {
236 for gx in 0..10 {
237 tm.set_tile(gx, gy, 1);
238 }
239 }
240
241 let cmds = tm.bake_visible(0.0, 0.0, 0, 80.0, 80.0, 1.0, 64.0, 64.0);
244
245 assert!(cmds.len() < 100);
247 assert!(cmds.len() >= 9); assert!(cmds.len() <= 25); for cmd in &cmds {
253 assert!(cmd.x >= 32.0); assert!(cmd.x <= 112.0);
255 assert!(cmd.y >= 32.0);
256 assert!(cmd.y <= 112.0);
257 }
258 }
259
260 #[test]
261 fn test_tile_zero_skipped() {
262 let mut tm = Tilemap::new(1, 3, 3, 16.0, 4, 4);
263 tm.set_tile(1, 1, 3);
265
266 let cmds = tm.bake_visible(0.0, 0.0, 0, 24.0, 24.0, 1.0, 200.0, 200.0);
267 assert_eq!(cmds.len(), 1);
268 assert!((cmds[0].x - 16.0).abs() < 1e-5);
269 assert!((cmds[0].y - 16.0).abs() < 1e-5);
270 }
271
272 #[test]
273 fn test_tilemap_store() {
274 let mut store = TilemapStore::new();
275 let id1 = store.create(1, 4, 4, 16.0, 4, 4);
276 let id2 = store.create(2, 8, 8, 32.0, 8, 8);
277
278 assert_eq!(id1, 1);
279 assert_eq!(id2, 2);
280
281 store.get_mut(id1).unwrap().set_tile(0, 0, 5);
283 assert_eq!(store.get(id1).unwrap().get_tile(0, 0), 5);
284
285 assert!(store.get(99).is_none());
287 assert!(store.get_mut(99).is_none());
288 }
289
290 #[test]
291 fn test_world_offset() {
292 let mut tm = Tilemap::new(1, 2, 2, 16.0, 4, 4);
293 tm.set_tile(0, 0, 1);
294 tm.set_tile(1, 1, 2);
295
296 let cmds = tm.bake_visible(100.0, 200.0, 5, 116.0, 216.0, 1.0, 200.0, 200.0);
298 assert_eq!(cmds.len(), 2);
299
300 assert!((cmds[0].x - 100.0).abs() < 1e-5);
301 assert!((cmds[0].y - 200.0).abs() < 1e-5);
302 assert_eq!(cmds[0].layer, 5);
303
304 assert!((cmds[1].x - 116.0).abs() < 1e-5);
305 assert!((cmds[1].y - 216.0).abs() < 1e-5);
306 }
307
308 #[test]
309 fn test_bake_produces_correct_sprite_fields() {
310 let mut tm = Tilemap::new(42, 1, 1, 24.0, 2, 2);
311 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);
314 assert_eq!(cmds.len(), 1);
315
316 let c = &cmds[0];
317 assert_eq!(c.texture_id, 42);
318 assert!((c.x - 10.0).abs() < 1e-5);
319 assert!((c.y - 20.0).abs() < 1e-5);
320 assert!((c.w - 24.0).abs() < 1e-5);
321 assert!((c.h - 24.0).abs() < 1e-5);
322 assert_eq!(c.layer, 7);
323 assert!((c.uv_x - 0.0).abs() < 1e-5);
325 assert!((c.uv_y - 0.5).abs() < 1e-5);
326 assert!((c.uv_w - 0.5).abs() < 1e-5);
327 assert!((c.uv_h - 0.5).abs() < 1e-5);
328 assert!((c.tint_r - 1.0).abs() < 1e-5);
329 assert!((c.tint_g - 1.0).abs() < 1e-5);
330 assert!((c.tint_b - 1.0).abs() < 1e-5);
331 assert!((c.tint_a - 1.0).abs() < 1e-5);
332 }
333}