1use indexmap::IndexMap;
2use rapier2d::prelude::*;
3use serde::{Deserialize, Serialize};
4
5use crate::config::FieldValue;
6use crate::physics::{deg_to_rad, encode_map_object, round2};
7
8pub const DEFAULT_FRICTION: f32 = 0.2;
13pub const DEFAULT_RESTITUTION: f32 = 0.0;
14
15const REST_VELOCITY_EPSILON: f32 = 0.01;
18
19pub const MAX_LEVELS: usize = 2;
23
24#[derive(Clone, Deserialize)]
26#[serde(rename_all = "camelCase")]
27pub struct DynamicObjectConfig {
28 pub position: [f32; 2],
29 pub angle: f32,
30 pub width: f32,
31 pub height: f32,
32 pub density: f32,
33 #[serde(default)]
34 pub linear_damping: Option<f32>,
35 #[serde(default)]
36 pub angular_damping: Option<f32>,
37 #[serde(default)]
39 pub level: u8,
40}
41
42#[derive(Clone, Deserialize)]
46#[serde(rename_all = "camelCase")]
47pub struct MapLevelConfig {
48 pub map: Vec<Vec<i32>>,
51 #[serde(default)]
53 pub floor: Vec<i32>,
54 #[serde(default)]
58 pub walls: Vec<i32>,
59 #[serde(default)]
62 pub layers: IndexMap<String, Vec<i32>>,
63}
64
65#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
67#[serde(rename_all = "lowercase")]
68pub enum RampDir {
69 North,
71 South,
73 West,
75 East,
77}
78
79impl RampDir {
80 pub fn axis_sign(self) -> (u8, i8) {
82 match self {
83 RampDir::North => (1, -1),
84 RampDir::South => (1, 1),
85 RampDir::West => (0, -1),
86 RampDir::East => (0, 1),
87 }
88 }
89}
90
91#[derive(Clone, Deserialize)]
93#[serde(rename_all = "camelCase")]
94pub struct RampConfig {
95 pub tile: i32,
97 pub dir: RampDir,
98 #[serde(default)]
99 pub from: u8,
100 #[serde(default = "default_ramp_to")]
101 pub to: u8,
102}
103
104fn default_ramp_to() -> u8 {
105 1
106}
107
108#[derive(Clone, Deserialize)]
111#[serde(rename_all = "camelCase")]
112pub struct MapConfig {
113 #[serde(default)]
114 pub set_id: Option<String>,
115 #[serde(default)]
116 pub scale: Option<f32>,
117 pub step: f32,
118 pub map: Vec<Vec<i32>>,
119 #[serde(default)]
120 pub physics_static: Vec<i32>,
121 #[serde(default)]
122 pub physics_dynamic: Vec<DynamicObjectConfig>,
123 #[serde(default)]
127 pub respawns: IndexMap<String, Vec<Vec<f32>>>,
128 #[serde(default)]
131 pub levels: IndexMap<String, MapLevelConfig>,
132 #[serde(default)]
133 pub ramps: Vec<RampConfig>,
134}
135
136pub fn validate_levels(
144 map: &[Vec<i32>],
145 physics_static: &[i32],
146 levels: &IndexMap<String, MapLevelConfig>,
147 ramps: &[RampConfig],
148) -> Result<(), String> {
149 let rows = map.len();
150 let level_count = levels.len() + 1;
151
152 if !levels.is_empty() {
153 if level_count > MAX_LEVELS {
154 return Err(format!(
155 "map levels: {level_count} levels, at most {MAX_LEVELS} supported"
156 ));
157 }
158
159 let mut keys: Vec<u8> = Vec::with_capacity(levels.len());
160
161 for key in levels.keys() {
162 let level: u8 = key
163 .parse()
164 .map_err(|_| format!("map levels: key '{key}' is not a level number"))?;
165
166 if level == 0 {
167 return Err(
168 "map levels: level 0 lives in map/physicsStatic, not in levels".to_string(),
169 );
170 }
171
172 keys.push(level);
173 }
174
175 keys.sort_unstable();
176
177 for (index, &level) in keys.iter().enumerate() {
178 if level as usize != index + 1 {
179 return Err(format!(
180 "map levels: levels must run from 1 without gaps, got {level} at position {}",
181 index + 1
182 ));
183 }
184 }
185 }
186
187 for (key, level) in levels {
188 if level.map.len() != rows {
189 return Err(format!(
190 "map levels: level {key} grid has {} rows, map has {rows}",
191 level.map.len()
192 ));
193 }
194
195 for (y, row) in level.map.iter().enumerate() {
196 let expected = map[y].len();
197
198 if row.len() != expected {
199 return Err(format!(
200 "map levels: level {key} row {y} has {} cells, map has {expected}",
201 row.len()
202 ));
203 }
204 }
205
206 for tile in &level.walls {
207 if !level.floor.contains(tile) {
208 return Err(format!(
209 "map levels: level {key} wall tile {tile} is not part of floor"
210 ));
211 }
212 }
213 }
214
215 for (index, ramp) in ramps.iter().enumerate() {
216 if ramp.from == ramp.to {
217 return Err(format!("map ramps: ramp {index} goes from level {} to itself", ramp.from));
218 }
219
220 if (ramp.from as usize) >= level_count || (ramp.to as usize) >= level_count {
221 return Err(format!(
222 "map ramps: ramp {index} references level out of range (levels: {level_count})"
223 ));
224 }
225
226 let grid = if ramp.from == 0 {
227 Some(map)
228 } else {
229 levels.get(&ramp.from.to_string()).map(|level| level.map.as_slice())
230 };
231
232 let found = grid.is_some_and(|grid| {
233 grid.iter().any(|row| row.contains(&ramp.tile))
234 });
235
236 if !found {
237 return Err(format!(
238 "map ramps: ramp {index} tile {} is missing from level {} grid",
239 ramp.tile, ramp.from
240 ));
241 }
242
243 let grid = grid.unwrap_or(map);
248
249 for (x, y) in ramp_run_exits(grid, ramp.tile, ramp.dir) {
250 if !walkable(map, physics_static, levels, ramp.to, x, y) {
251 return Err(format!(
252 "map ramps: ramp {index} run ends at ({x}, {y}), which is not \
253 walkable ground of level {}",
254 ramp.to
255 ));
256 }
257 }
258 }
259
260 validate_level_edges(map, physics_static, levels)?;
261
262 Ok(())
263}
264
265fn validate_level_edges(
271 map: &[Vec<i32>],
272 physics_static: &[i32],
273 levels: &IndexMap<String, MapLevelConfig>,
274) -> Result<(), String> {
275 for (key, level) in levels {
276 for (y, row) in level.map.iter().enumerate() {
277 for (x, tile) in row.iter().enumerate() {
278 if !level.floor.contains(tile) || level.walls.contains(tile) {
280 continue;
281 }
282
283 for (dx, dy) in [(1_i64, 0_i64), (-1, 0), (0, 1), (0, -1)] {
284 let nx = x as i64 + dx;
285 let ny = y as i64 + dy;
286
287 let neighbour = cell_at(&level.map, nx, ny);
288
289 if neighbour.is_some_and(|tile| level.floor.contains(&tile)) {
290 continue;
291 }
292
293 if ground_walkable(map, physics_static, nx, ny) {
295 continue;
296 }
297
298 return Err(format!(
299 "map levels: level {key} floor cell ({x}, {y}) has an open \
300 edge at ({nx}, {ny}) with no walkable ground below — close \
301 it with a wall tile"
302 ));
303 }
304 }
305 }
306 }
307
308 Ok(())
309}
310
311fn cell_at(grid: &[Vec<i32>], x: i64, y: i64) -> Option<i32> {
312 if x < 0 || y < 0 {
313 return None;
314 }
315
316 grid.get(y as usize)
317 .and_then(|row| row.get(x as usize))
318 .copied()
319}
320
321fn ground_walkable(map: &[Vec<i32>], physics_static: &[i32], x: i64, y: i64) -> bool {
324 cell_at(map, x, y).is_some_and(|tile| !physics_static.contains(&tile))
325}
326
327fn walkable(
330 map: &[Vec<i32>],
331 physics_static: &[i32],
332 levels: &IndexMap<String, MapLevelConfig>,
333 level: u8,
334 x: i64,
335 y: i64,
336) -> bool {
337 if level == 0 {
338 return ground_walkable(map, physics_static, x, y);
339 }
340
341 let Some(cfg) = levels.get(&level.to_string()) else {
342 return false;
343 };
344
345 cell_at(&cfg.map, x, y)
346 .is_some_and(|tile| cfg.floor.contains(&tile) && !cfg.walls.contains(&tile))
347}
348
349fn ramp_run_exits(grid: &[Vec<i32>], tile: i32, dir: RampDir) -> Vec<(i64, i64)> {
352 let (axis, sign) = dir.axis_sign();
353 let mut out = Vec::new();
354
355 if axis == 1 {
356 let cols = grid.iter().map(|row| row.len()).max().unwrap_or(0);
357
358 for x in 0..cols {
359 let mut y = 0;
360
361 while y < grid.len() {
362 if grid[y].get(x) != Some(&tile) {
363 y += 1;
364 continue;
365 }
366
367 let y0 = y;
368
369 while y < grid.len() && grid[y].get(x) == Some(&tile) {
370 y += 1;
371 }
372
373 let exit = if sign > 0 { y as i64 } else { y0 as i64 - 1 };
374
375 out.push((x as i64, exit));
376 }
377 }
378 } else {
379 for (y, row) in grid.iter().enumerate() {
380 let mut x = 0;
381
382 while x < row.len() {
383 if row[x] != tile {
384 x += 1;
385 continue;
386 }
387
388 let x0 = x;
389
390 while x < row.len() && row[x] == tile {
391 x += 1;
392 }
393
394 let exit = if sign > 0 { x as i64 } else { x0 as i64 - 1 };
395
396 out.push((exit, y as i64));
397 }
398 }
399 }
400
401 out
402}
403
404impl MapConfig {
405 pub fn validate(&self) -> Result<(), String> {
409 let level_count = self.levels.len() + 1;
410
411 validate_levels(&self.map, &self.physics_static, &self.levels, &self.ramps)?;
412
413 for (team, points) in &self.respawns {
414 for (index, point) in points.iter().enumerate() {
415 if point.len() != 3 && point.len() != 4 {
416 return Err(format!(
417 "map respawns: {team}[{index}] has {} numbers, expected 3 or 4",
418 point.len()
419 ));
420 }
421
422 if point.len() == 4 && (point[3] as usize) >= level_count {
423 return Err(format!(
424 "map respawns: {team}[{index}] level {} is out of range (levels: {level_count})",
425 point[3]
426 ));
427 }
428 }
429 }
430
431 for (index, object) in self.physics_dynamic.iter().enumerate() {
432 if (object.level as usize) >= level_count {
433 return Err(format!(
434 "map physicsDynamic: object {index} level {} is out of range (levels: {level_count})",
435 object.level
436 ));
437 }
438 }
439
440 Ok(())
441 }
442}
443
444pub fn level_group(level: u8) -> Group {
449 debug_assert!(
453 (level as usize) < MAX_LEVELS,
454 "level_group: level {level} is out of range (MAX_LEVELS: {MAX_LEVELS})"
455 );
456
457 match level {
458 0 => Group::GROUP_1,
459 _ => Group::GROUP_2,
460 }
461}
462
463pub const STATIC_LEVEL_GROUP: Group = Group::GROUP_9;
470
471pub fn level_interaction(level: u8) -> InteractionGroups {
473 let group = level_group(level);
474
475 InteractionGroups::new(group, group, InteractionTestMode::And)
476}
477
478pub fn static_level_interaction(level: u8) -> InteractionGroups {
480 let group = level_group(level) | STATIC_LEVEL_GROUP;
481
482 InteractionGroups::new(group, group, InteractionTestMode::And)
483}
484
485pub fn levels_interaction(mask: Group) -> InteractionGroups {
487 InteractionGroups::new(mask, mask, InteractionTestMode::And)
488}
489
490#[derive(Clone, Copy, Debug, PartialEq)]
492pub struct RampSample {
493 pub progress: f32,
495 pub from: u8,
496 pub to: u8,
497}
498
499#[derive(Clone, Serialize, Deserialize)]
502pub struct RampRun {
503 pub axis: u8,
505 pub sign: i8,
507 pub from: u8,
508 pub to: u8,
509 pub min: f32,
511 pub max: f32,
512 pub cross_min: f32,
515 pub cross_max: f32,
516}
517
518#[derive(Clone, Default, Serialize, Deserialize)]
523pub struct MapLevels {
524 grids: Vec<Vec<Vec<i32>>>,
526 solid: Vec<Vec<i32>>,
528 floor: Vec<Vec<i32>>,
530 runs: Vec<RampRun>,
531 run_cells: Vec<Vec<i16>>,
533 tile_size: f32,
535}
536
537#[allow(clippy::too_many_arguments)]
543fn push_run(
544 runs: &mut Vec<RampRun>,
545 run_cells: &mut [Vec<i16>],
546 tile_size: f32,
547 ramp: &RampConfig,
548 axis: u8,
549 sign: i8,
550 from: usize,
551 to: usize,
552 cross0: usize,
553 cross1: usize,
554) {
555 if runs.len() >= i16::MAX as usize {
556 return;
557 }
558
559 let index = runs.len() as i16;
560 let mut claimed = false;
561
562 for main in from..to {
563 for cross in cross0..cross1 {
564 let (x, y) = if axis == 1 { (cross, main) } else { (main, cross) };
565
566 let Some(cell) = run_cells.get_mut(y).and_then(|row| row.get_mut(x)) else {
567 continue;
568 };
569
570 if *cell < 0 {
571 *cell = index;
572 claimed = true;
573 }
574 }
575 }
576
577 if !claimed {
578 return;
579 }
580
581 let size = tile_size;
582
583 runs.push(RampRun {
584 axis,
585 sign,
586 from: ramp.from,
587 to: ramp.to,
588 min: from as f32 * size,
589 max: to as f32 * size,
590 cross_min: cross0 as f32 * size,
591 cross_max: cross1 as f32 * size,
592 });
593}
594
595impl MapLevels {
596 pub fn build(
600 grid0: &[Vec<i32>],
601 solid0: &[i32],
602 levels: &IndexMap<String, MapLevelConfig>,
603 ramps: &[RampConfig],
604 tile_size: f32,
605 ) -> Self {
606 let mut out = Self {
607 grids: vec![grid0.to_vec()],
608 solid: vec![solid0.to_vec()],
609 floor: vec![Vec::new()],
610 runs: Vec::new(),
611 run_cells: grid0.iter().map(|row| vec![-1i16; row.len()]).collect(),
612 tile_size,
613 };
614
615 let mut ordered: Vec<(u8, &MapLevelConfig)> = levels
618 .iter()
619 .filter_map(|(key, level)| key.parse::<u8>().ok().map(|index| (index, level)))
620 .collect();
621
622 ordered.sort_by_key(|(index, _)| *index);
623
624 for (index, level) in ordered {
625 if index as usize != out.grids.len() {
626 continue;
627 }
628
629 out.grids.push(level.map.clone());
630 out.solid.push(level.walls.clone());
631 out.floor.push(level.floor.clone());
632 }
633
634 out.build_runs(ramps);
635
636 out
637 }
638
639 fn build_runs(&mut self, ramps: &[RampConfig]) {
641 let Self {
645 grids,
646 runs,
647 run_cells,
648 tile_size,
649 ..
650 } = self;
651
652 for ramp in ramps {
653 let (axis, sign) = ramp.dir.axis_sign();
654 let Some(grid) = grids.get(ramp.from as usize) else {
655 continue;
656 };
657 let rows = grid.len();
658
659 if axis == 1 {
660 let cols = grid.iter().map(|row| row.len()).max().unwrap_or(0);
661
662 for x in 0..cols {
663 let mut y = 0;
664
665 while y < rows {
666 if grid[y].get(x) != Some(&ramp.tile) {
667 y += 1;
668 continue;
669 }
670
671 let y0 = y;
672
673 while y < rows && grid[y].get(x) == Some(&ramp.tile) {
674 y += 1;
675 }
676
677 push_run(
678 runs,
679 run_cells,
680 *tile_size,
681 ramp,
682 axis,
683 sign,
684 y0,
685 y,
686 x,
687 x + 1,
688 );
689 }
690 }
691 } else {
692 for (y, row) in grid.iter().enumerate() {
693 let mut x = 0;
694
695 while x < row.len() {
696 if row[x] != ramp.tile {
697 x += 1;
698 continue;
699 }
700
701 let x0 = x;
702
703 while x < row.len() && row[x] == ramp.tile {
704 x += 1;
705 }
706
707 push_run(
708 runs,
709 run_cells,
710 *tile_size,
711 ramp,
712 axis,
713 sign,
714 x0,
715 x,
716 y,
717 y + 1,
718 );
719 }
720 }
721 }
722 }
723 }
724
725 pub fn is_layered(&self) -> bool {
727 self.grids.len() > 1
728 }
729
730 pub fn level_count(&self) -> usize {
732 self.grids.len().max(1)
733 }
734
735 pub fn tile_size(&self) -> f32 {
736 self.tile_size
737 }
738
739 pub fn grid(&self, level: u8) -> Option<&Vec<Vec<i32>>> {
740 self.grids.get(level as usize)
741 }
742
743 pub fn solid(&self, level: u8) -> &[i32] {
744 self.solid.get(level as usize).map_or(&[], |list| list)
745 }
746
747 pub fn floor(&self, level: u8) -> &[i32] {
748 self.floor.get(level as usize).map_or(&[], |list| list)
749 }
750
751 pub fn runs(&self) -> &[RampRun] {
752 &self.runs
753 }
754
755 pub fn cell_at(&self, x: f32, y: f32) -> Option<(usize, usize)> {
757 if self.tile_size <= 0.0 || x < 0.0 || y < 0.0 {
758 return None;
759 }
760
761 let cx = (x / self.tile_size).floor() as usize;
762 let cy = (y / self.tile_size).floor() as usize;
763 let row = self.grids.first()?.get(cy)?;
764
765 if cx >= row.len() { None } else { Some((cx, cy)) }
766 }
767
768 pub fn has_floor(&self, level: u8, x: f32, y: f32) -> bool {
771 let Some((cx, cy)) = self.cell_at(x, y) else {
772 return false;
773 };
774
775 if level == 0 {
776 return true;
777 }
778
779 let Some(grid) = self.grid(level) else {
780 return false;
781 };
782 let Some(&tile) = grid.get(cy).and_then(|row| row.get(cx)) else {
783 return false;
784 };
785
786 self.floor(level).contains(&tile)
787 }
788
789 pub fn is_solid(&self, level: u8, x: f32, y: f32) -> bool {
791 let Some((cx, cy)) = self.cell_at(x, y) else {
792 return false;
793 };
794 let Some(grid) = self.grid(level) else {
795 return false;
796 };
797 let Some(&tile) = grid.get(cy).and_then(|row| row.get(cx)) else {
798 return false;
799 };
800
801 self.solid(level).contains(&tile)
802 }
803
804 pub fn level_at(&self, x: f32, y: f32) -> u8 {
807 let mut level = 0;
808
809 for candidate in (1..self.level_count() as u8).rev() {
810 if self.has_floor(candidate, x, y) {
811 level = candidate;
812 break;
813 }
814 }
815
816 level
817 }
818
819 pub fn ramp_at(&self, x: f32, y: f32) -> Option<RampSample> {
821 let (cx, cy) = self.cell_at(x, y)?;
822 let index = *self.run_cells.get(cy)?.get(cx)?;
823
824 if index < 0 {
825 return None;
826 }
827
828 let run = &self.runs[index as usize];
829 let value = if run.axis == 0 { x } else { y };
830 let span = run.max - run.min;
831 let raw = if span <= 0.0 { 0.0 } else { (value - run.min) / span };
832 let progress = if run.sign > 0 { raw } else { 1.0 - raw };
833
834 Some(RampSample {
835 progress: progress.clamp(0.0, 1.0),
836 from: run.from,
837 to: run.to,
838 })
839 }
840}
841
842#[derive(Serialize, Deserialize)]
845pub struct GameMap {
846 pub set_id: String,
847 pub step: f32,
849 pub grid: Vec<Vec<i32>>,
851 pub physics_static: Vec<i32>,
852 pub respawns: IndexMap<String, Vec<Vec<f32>>>,
855 levels: MapLevels,
857 static_bodies: Vec<RigidBodyHandle>,
858 static_levels: Vec<u8>,
860 dynamic_bodies: Vec<RigidBodyHandle>,
861 dynamic_levels: Vec<u8>,
863}
864
865impl GameMap {
866 pub fn create(
869 world: &mut PhysicsWorld,
870 cfg: &MapConfig,
871 default_scale: f32,
872 default_set_id: &str,
873 ) -> Self {
874 let scale = cfg.scale.unwrap_or(default_scale);
875 let step = cfg.step * scale;
876 let levels = MapLevels::build(
877 &cfg.map,
878 &cfg.physics_static,
879 &cfg.levels,
880 &cfg.ramps,
881 step,
882 );
883
884 let mut map = Self {
885 set_id: cfg
886 .set_id
887 .clone()
888 .unwrap_or_else(|| default_set_id.to_string()),
889 step,
890 grid: cfg.map.clone(),
891 physics_static: cfg.physics_static.clone(),
892 respawns: cfg
893 .respawns
894 .iter()
895 .map(|(team, arr)| {
896 (
897 team.clone(),
898 arr.iter()
899 .map(|point| {
900 let mut out = point.clone();
901
902 if out.len() >= 2 {
903 out[0] *= scale;
904 out[1] *= scale;
905 }
906
907 out
908 })
909 .collect(),
910 )
911 })
912 .collect(),
913 levels,
914 static_bodies: Vec::new(),
915 static_levels: Vec::new(),
916 dynamic_bodies: Vec::new(),
917 dynamic_levels: Vec::new(),
918 };
919
920 map.create_static(world);
921 map.create_dynamic(world, &cfg.physics_dynamic, scale);
922
923 map
924 }
925
926 fn create_static(&mut self, world: &mut PhysicsWorld) {
931 for level in 0..self.levels.level_count() as u8 {
932 let Some(grid) = self.levels.grid(level) else {
933 continue;
934 };
935 let solid = self.levels.solid(level).to_vec();
936
937 let mut work: Vec<Vec<Option<i32>>> = grid
938 .iter()
939 .map(|row| row.iter().map(|&tile| Some(tile)).collect())
940 .collect();
941
942 for y in 0..work.len() {
943 for x in 0..work[y].len() {
944 let is_static = work[y][x].is_some_and(|tile| solid.contains(&tile));
945
946 if is_static {
947 let (width, height) =
948 search_static_block(&mut work, &solid, self.step, y, x);
949 let pos_x = x as f32 * self.step + width / 2.0;
950 let pos_y = y as f32 * self.step + height / 2.0;
951
952 let body = world.insert_body(
953 RigidBodyBuilder::fixed().translation(Vector::new(pos_x, pos_y)),
954 );
955
956 world.insert_collider(
957 ColliderBuilder::cuboid(width / 2.0, height / 2.0)
958 .friction(DEFAULT_FRICTION)
959 .restitution(DEFAULT_RESTITUTION)
960 .collision_groups(static_level_interaction(level)),
961 Some(body),
962 );
963
964 self.static_bodies.push(body);
965 self.static_levels.push(level);
966 }
967 }
968 }
969 }
970 }
971
972 fn create_dynamic(
974 &mut self,
975 world: &mut PhysicsWorld,
976 dynamics: &[DynamicObjectConfig],
977 scale: f32,
978 ) {
979 for data in dynamics {
980 let pos_x = data.position[0] * scale;
981 let pos_y = data.position[1] * scale;
982 let width = data.width * scale;
983 let height = data.height * scale;
984
985 let body = world.insert_body(
986 RigidBodyBuilder::dynamic()
987 .translation(Vector::new(pos_x, pos_y))
988 .rotation(deg_to_rad(data.angle))
989 .linear_damping(data.linear_damping.unwrap_or(0.0))
990 .angular_damping(data.angular_damping.unwrap_or(0.01))
991 .soft_ccd_prediction(width.min(height))
995 .user_data(encode_map_object()),
996 );
997
998 world.insert_collider(
1000 ColliderBuilder::cuboid(width / 2.0, height / 2.0)
1001 .translation(Vector::new(width / 2.0, height / 2.0))
1002 .density(data.density)
1003 .friction(DEFAULT_FRICTION)
1004 .restitution(DEFAULT_RESTITUTION)
1005 .collision_groups(level_interaction(data.level)),
1006 Some(body),
1007 );
1008
1009 self.dynamic_bodies.push(body);
1010 self.dynamic_levels.push(data.level);
1011 }
1012 }
1013
1014 pub fn levels(&self) -> &MapLevels {
1017 &self.levels
1018 }
1019
1020 pub fn is_layered(&self) -> bool {
1021 self.levels.is_layered()
1022 }
1023
1024 pub fn level_count(&self) -> usize {
1025 self.levels.level_count()
1026 }
1027
1028 pub fn level_at(&self, x: f32, y: f32) -> u8 {
1029 self.levels.level_at(x, y)
1030 }
1031
1032 pub fn has_floor(&self, level: u8, x: f32, y: f32) -> bool {
1033 self.levels.has_floor(level, x, y)
1034 }
1035
1036 pub fn ramp_at(&self, x: f32, y: f32) -> Option<RampSample> {
1037 self.levels.ramp_at(x, y)
1038 }
1039
1040 pub fn dynamic_level(&self, index: usize) -> u8 {
1042 self.dynamic_levels.get(index).copied().unwrap_or(0)
1043 }
1044
1045 pub fn static_levels(&self) -> &[u8] {
1047 &self.static_levels
1048 }
1049
1050 pub fn dynamic_levels(&self) -> &[u8] {
1052 &self.dynamic_levels
1053 }
1054
1055 pub fn static_body_count(&self) -> usize {
1058 self.static_bodies.len()
1059 }
1060
1061 pub fn dynamic_body_count(&self) -> usize {
1062 self.dynamic_bodies.len()
1063 }
1064
1065 pub fn destroy(&mut self, world: &mut PhysicsWorld) {
1067 for handle in self.static_bodies.drain(..) {
1068 world.remove_body(handle);
1069 }
1070
1071 for handle in self.dynamic_bodies.drain(..) {
1072 world.remove_body(handle);
1073 }
1074
1075 self.static_levels.clear();
1076 self.dynamic_levels.clear();
1077 }
1078
1079 pub fn dynamic_map_data(
1089 &self,
1090 world: &PhysicsWorld,
1091 with_velocities: bool,
1092 ) -> Vec<(u8, Vec<FieldValue>)> {
1093 self.dynamic_bodies
1094 .iter()
1095 .enumerate()
1096 .filter_map(|(index, &handle)| {
1097 world.bodies.get(handle).map(|body| {
1098 let pos = body.translation();
1099
1100 let mut fields = vec![
1101 FieldValue::F32(round2(pos.x)),
1102 FieldValue::F32(round2(pos.y)),
1103 FieldValue::F32(round2(body.rotation().angle())),
1104 ];
1105
1106 if with_velocities {
1107 let linvel = body.linvel();
1108 let angvel = body.angvel();
1109 let resting = body.is_sleeping()
1110 || (linvel.x.hypot(linvel.y) < REST_VELOCITY_EPSILON
1111 && angvel.abs() < REST_VELOCITY_EPSILON);
1112
1113 if !resting {
1114 fields.push(FieldValue::F32(round2(linvel.x)));
1115 fields.push(FieldValue::F32(round2(linvel.y)));
1116 fields.push(FieldValue::F32(round2(angvel)));
1117 }
1118 }
1119
1120 (index as u8, fields)
1121 })
1122 })
1123 .collect()
1124 }
1125}
1126
1127fn search_static_block(
1131 work: &mut [Vec<Option<i32>>],
1132 solid: &[i32],
1133 step: f32,
1134 y0: usize,
1135 x0: usize,
1136) -> (f32, f32) {
1137 let mut x = x0;
1138 let mut w_counter = 0;
1139 let mut h_counter = 1;
1140
1141 while x < work[y0].len() && work[y0][x].is_some_and(|tile| solid.contains(&tile)) {
1143 work[y0][x] = None;
1144 x += 1;
1145 w_counter += 1;
1146 }
1147
1148 let len_x = x;
1149 let len_y = work.len();
1150
1151 for y in (y0 + 1)..len_y {
1153 let mut empty_tile = false;
1154 let mut x = x0;
1155
1156 while x < len_x {
1157 if x < work[y].len() && work[y][x].is_some_and(|tile| solid.contains(&tile)) {
1158 x += 1;
1159 } else {
1160 empty_tile = true;
1161 break;
1162 }
1163 }
1164
1165 if empty_tile {
1166 break;
1167 }
1168
1169 h_counter += 1;
1170
1171 for cell in work[y][x0..len_x].iter_mut() {
1172 *cell = None;
1173 }
1174 }
1175
1176 (w_counter as f32 * step, h_counter as f32 * step)
1177}
1178
1179#[cfg(test)]
1180mod tests {
1181 use super::*;
1182
1183 const TIME_STEP: f32 = 1.0 / 120.0;
1184
1185 fn map_config() -> MapConfig {
1187 serde_json::from_value(serde_json::json!({
1188 "step": 20.0,
1189 "map": [[1, 1, 1, 1, 1], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]],
1190 "physicsStatic": [1],
1191 "physicsDynamic": [{
1192 "position": [40.0, 60.0],
1193 "angle": 0.0,
1194 "width": 20.0,
1195 "height": 20.0,
1196 "density": 1.0
1197 }]
1198 }))
1199 .unwrap()
1200 }
1201
1202 fn make_world() -> PhysicsWorld {
1203 let mut world = PhysicsWorld::new();
1204
1205 world.gravity = Vector::ZERO;
1206 world.integration_parameters.dt = TIME_STEP;
1207
1208 world
1209 }
1210
1211 fn max_penetration(world: &mut PhysicsWorld, body: RigidBodyHandle) -> f32 {
1213 let mut depth: f32 = 0.0;
1214
1215 world.bodies[body].set_linvel(Vector::new(0.0, -2000.0), true);
1218
1219 for _ in 0..120 {
1220 world.step();
1221
1222 for pair in world.contact_pairs() {
1223 for manifold in &pair.manifolds {
1224 for point in &manifold.points {
1225 depth = depth.max(-point.dist);
1226 }
1227 }
1228 }
1229 }
1230
1231 depth
1232 }
1233
1234 fn layered_config() -> MapConfig {
1237 serde_json::from_value(serde_json::json!({
1238 "step": 20.0,
1239 "map": [[1, 0, 0], [0, 0, 0], [0, 0, 0]],
1240 "physicsStatic": [1],
1241 "levels": {
1242 "1": {
1243 "map": [[0, 0, 0], [0, 5, 6], [0, 0, 0]],
1244 "floor": [5, 6],
1245 "walls": [6]
1246 }
1247 }
1248 }))
1249 .unwrap()
1250 }
1251
1252 fn ramp_config(dir: &str) -> MapConfig {
1254 serde_json::from_value(serde_json::json!({
1255 "step": 20.0,
1256 "map": [[7, 0, 0], [7, 0, 0], [7, 0, 0]],
1257 "physicsStatic": [],
1258 "levels": {
1259 "1": { "map": [[0, 0, 0], [0, 0, 0], [0, 0, 0]], "floor": [] }
1260 },
1261 "ramps": [{ "tile": 7, "dir": dir }]
1262 }))
1263 .unwrap()
1264 }
1265
1266 fn collision_groups(world: &PhysicsWorld, body: RigidBodyHandle) -> InteractionGroups {
1267 let handle = world.bodies[body].colliders()[0];
1268
1269 world.colliders[handle].collision_groups()
1270 }
1271
1272 #[test]
1273 fn legacy_map_has_no_levels() {
1274 let mut world = make_world();
1275 let map = GameMap::create(&mut world, &map_config(), 1.0, "set");
1276
1277 assert!(!map.is_layered());
1278 assert_eq!(map.level_count(), 1);
1279 assert_eq!(map.static_body_count(), 1);
1281 assert_eq!(
1282 world.bodies[map.static_bodies[0]].translation(),
1283 Vector::new(50.0, 10.0)
1284 );
1285 }
1286
1287 #[test]
1288 fn layered_map_builds_static_per_level() {
1289 let mut world = make_world();
1290 let map = GameMap::create(&mut world, &layered_config(), 1.0, "set");
1291
1292 assert!(map.is_layered());
1293 assert_eq!(map.static_body_count(), 2);
1294 assert_eq!(map.static_levels(), &[0, 1]);
1295 assert_eq!(
1298 collision_groups(&world, map.static_bodies[0]).memberships,
1299 Group::GROUP_1 | STATIC_LEVEL_GROUP
1300 );
1301 assert_eq!(
1302 collision_groups(&world, map.static_bodies[1]).memberships,
1303 Group::GROUP_2 | STATIC_LEVEL_GROUP
1304 );
1305 }
1306
1307 #[test]
1308 fn static_group_sees_walls_but_no_bodies() {
1309 let falling = InteractionGroups::new(
1310 STATIC_LEVEL_GROUP,
1311 STATIC_LEVEL_GROUP,
1312 InteractionTestMode::And,
1313 );
1314
1315 assert!(falling.test(static_level_interaction(0)));
1317 assert!(falling.test(static_level_interaction(1)));
1318 assert!(!falling.test(level_interaction(0)));
1320 assert!(!falling.test(level_interaction(1)));
1321 }
1322
1323 #[test]
1324 fn dynamic_body_carries_level_group() {
1325 let mut cfg = layered_config();
1326
1327 cfg.physics_dynamic = vec![DynamicObjectConfig {
1328 position: [20.0, 20.0],
1329 angle: 0.0,
1330 width: 20.0,
1331 height: 20.0,
1332 density: 1.0,
1333 linear_damping: None,
1334 angular_damping: None,
1335 level: 1,
1336 }];
1337
1338 let mut world = make_world();
1339 let map = GameMap::create(&mut world, &cfg, 1.0, "set");
1340
1341 assert_eq!(map.dynamic_level(0), 1);
1342 assert_eq!(
1343 collision_groups(&world, map.dynamic_bodies[0]).memberships,
1344 Group::GROUP_2
1345 );
1346 }
1347
1348 #[test]
1349 fn has_floor_reports_slab_and_ground() {
1350 let mut world = make_world();
1351 let map = GameMap::create(&mut world, &layered_config(), 1.0, "set");
1352
1353 assert!(map.has_floor(0, 10.0, 10.0));
1354 assert!(!map.has_floor(0, -5.0, 10.0));
1355 assert!(!map.has_floor(0, 70.0, 10.0));
1356
1357 assert!(map.has_floor(1, 30.0, 30.0));
1358 assert!(map.has_floor(1, 50.0, 30.0));
1359 assert!(!map.has_floor(1, 10.0, 10.0));
1360 }
1361
1362 #[test]
1363 fn level_at_picks_highest_floor() {
1364 let mut world = make_world();
1365 let map = GameMap::create(&mut world, &layered_config(), 1.0, "set");
1366
1367 assert_eq!(map.level_at(30.0, 30.0), 1);
1368 assert_eq!(map.level_at(10.0, 30.0), 0);
1369 }
1370
1371 #[test]
1372 fn ramp_progress_runs_from_bottom_to_top() {
1373 let mut world = make_world();
1374 let north = GameMap::create(&mut world, &ramp_config("north"), 1.0, "set");
1375
1376 assert!(north.ramp_at(10.0, 59.0).unwrap().progress < 0.05);
1378 assert!(north.ramp_at(10.0, 1.0).unwrap().progress > 0.95);
1379 assert!((north.ramp_at(10.0, 30.0).unwrap().progress - 0.5).abs() < 0.01);
1380 assert_eq!(north.ramp_at(10.0, 30.0).unwrap().to, 1);
1381 assert!(north.ramp_at(30.0, 30.0).is_none());
1382
1383 let mut world = make_world();
1384 let south = GameMap::create(&mut world, &ramp_config("south"), 1.0, "set");
1385
1386 assert!(south.ramp_at(10.0, 1.0).unwrap().progress < 0.05);
1387 assert!(south.ramp_at(10.0, 59.0).unwrap().progress > 0.95);
1388 }
1389
1390 #[test]
1391 fn ramp_runs_are_split_per_line() {
1392 let mut cfg = ramp_config("north");
1393
1394 for row in &mut cfg.map {
1396 row[1] = 7;
1397 }
1398
1399 let mut world = make_world();
1400 let map = GameMap::create(&mut world, &cfg, 1.0, "set");
1401 let runs = map.levels().runs();
1402
1403 assert_eq!(runs.len(), 2);
1404 assert_eq!((runs[0].min, runs[0].max), (0.0, 60.0));
1405 assert_eq!((runs[1].min, runs[1].max), (0.0, 60.0));
1406 assert_ne!(runs[0].cross_min, runs[1].cross_min);
1407 }
1408
1409 #[test]
1410 fn respawn_accepts_three_and_four_numbers() {
1411 let cfg: MapConfig = serde_json::from_value(serde_json::json!({
1412 "step": 20.0,
1413 "map": [[0, 0], [0, 0]],
1414 "levels": { "1": { "map": [[0, 0], [0, 0]], "floor": [] } },
1415 "respawns": { "team1": [[10.0, 20.0, 0.0], [30.0, 40.0, 90.0, 1.0]] }
1416 }))
1417 .unwrap();
1418
1419 cfg.validate().unwrap();
1420
1421 let mut world = make_world();
1422 let map = GameMap::create(&mut world, &cfg, 2.0, "set");
1423 let points = &map.respawns["team1"];
1424
1425 assert_eq!(points[0], vec![20.0, 40.0, 0.0]);
1426 assert_eq!(points[1], vec![60.0, 80.0, 90.0, 1.0]);
1427 }
1428
1429 #[test]
1430 fn validate_rejects_mismatched_level_grid() {
1431 let mut cfg = layered_config();
1432
1433 cfg.levels["1"].map.pop();
1434
1435 let error = cfg.validate().unwrap_err();
1436
1437 assert!(error.contains("rows"), "{error}");
1438 }
1439
1440 #[test]
1441 fn validate_rejects_railing_outside_floor() {
1442 let mut cfg = layered_config();
1443
1444 cfg.levels["1"].floor = vec![5];
1445
1446 let error = cfg.validate().unwrap_err();
1447
1448 assert!(error.contains("floor"), "{error}");
1449 }
1450
1451 #[test]
1452 fn validate_rejects_unknown_ramp_tile() {
1453 let mut cfg = ramp_config("north");
1454
1455 cfg.ramps[0].tile = 99;
1456
1457 let error = cfg.validate().unwrap_err();
1458
1459 assert!(error.contains("missing"), "{error}");
1460 }
1461
1462 #[test]
1467 fn shared_layered_fixtures() {
1468 let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
1469 .join("../contract/fixtures/layered");
1470
1471 let mut checked = 0;
1472
1473 for entry in std::fs::read_dir(&dir).expect("fixtures dir") {
1474 let path = entry.unwrap().path();
1475
1476 if path.extension().and_then(|e| e.to_str()) != Some("json") {
1477 continue;
1478 }
1479
1480 let name = path.file_stem().unwrap().to_string_lossy().to_string();
1481 let doc: serde_json::Value =
1482 serde_json::from_str(&std::fs::read_to_string(&path).unwrap())
1483 .unwrap_or_else(|e| panic!("{name}: {e}"));
1484
1485 let cfg: MapConfig = serde_json::from_value(doc["map"].clone())
1486 .unwrap_or_else(|e| panic!("{name}: {e}"));
1487
1488 let result = cfg.validate();
1489
1490 match doc.get("expect").and_then(|v| v.as_str()) {
1491 None => assert!(result.is_ok(), "{name}: {result:?}"),
1492 Some(fragment) => {
1493 let error = result.expect_err(&format!("{name}: expected an error"));
1494
1495 assert!(error.contains(fragment), "{name}: got {error}");
1496 }
1497 }
1498
1499 checked += 1;
1500 }
1501
1502 assert!(checked > 1, "корпус фикстур не прочитан: {checked}");
1503 }
1504
1505 #[test]
1506 fn validate_rejects_level_out_of_range() {
1507 let cfg: MapConfig = serde_json::from_value(serde_json::json!({
1508 "step": 20.0,
1509 "map": [[0, 0], [0, 0]],
1510 "respawns": { "team1": [[10.0, 20.0, 0.0, 5.0]] }
1511 }))
1512 .unwrap();
1513
1514 let error = cfg.validate().unwrap_err();
1515
1516 assert!(error.contains("out of range"), "{error}");
1517 }
1518
1519 #[test]
1520 fn dynamic_body_gets_soft_ccd_prediction_of_thickness() {
1521 let mut world = make_world();
1522 let map = GameMap::create(&mut world, &map_config(), 1.0, "set");
1523
1524 let prediction = world.bodies[map.dynamic_bodies[0]].soft_ccd_prediction();
1525
1526 assert_eq!(prediction, 20.0);
1529 }
1530
1531 #[test]
1532 fn soft_ccd_prediction_keeps_penetration_shallow() {
1533 let mut world = make_world();
1534 let map = GameMap::create(&mut world, &map_config(), 1.0, "set");
1535 let handle = map.dynamic_bodies[0];
1536
1537 let predicted = max_penetration(&mut world, handle);
1538
1539 let mut plain_world = make_world();
1540 let plain_map = GameMap::create(&mut plain_world, &map_config(), 1.0, "set");
1541 let plain_handle = plain_map.dynamic_bodies[0];
1542
1543 plain_world.bodies[plain_handle].set_soft_ccd_prediction(0.0);
1544
1545 let plain = max_penetration(&mut plain_world, plain_handle);
1546
1547 assert!(
1548 plain > 5.0,
1549 "без предсказания ожидалось глубокое перекрытие, получено {plain}"
1550 );
1551 assert!(predicted < 2.0, "с предсказанием перекрытие {predicted}");
1552 }
1553}