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
136impl MapConfig {
137 pub fn validate(&self) -> Result<(), String> {
141 let rows = self.map.len();
142 let level_count = self.levels.len() + 1;
143
144 if !self.levels.is_empty() {
145 if level_count > MAX_LEVELS {
146 return Err(format!(
147 "map levels: {level_count} levels, at most {MAX_LEVELS} supported"
148 ));
149 }
150
151 let mut keys: Vec<u8> = Vec::with_capacity(self.levels.len());
152
153 for key in self.levels.keys() {
154 let level: u8 = key
155 .parse()
156 .map_err(|_| format!("map levels: key '{key}' is not a level number"))?;
157
158 if level == 0 {
159 return Err(
160 "map levels: level 0 lives in map/physicsStatic, not in levels".to_string(),
161 );
162 }
163
164 keys.push(level);
165 }
166
167 keys.sort_unstable();
168
169 for (index, &level) in keys.iter().enumerate() {
170 if level as usize != index + 1 {
171 return Err(format!(
172 "map levels: levels must run from 1 without gaps, got {level} at position {}",
173 index + 1
174 ));
175 }
176 }
177 }
178
179 for (key, level) in &self.levels {
180 if level.map.len() != rows {
181 return Err(format!(
182 "map levels: level {key} grid has {} rows, map has {rows}",
183 level.map.len()
184 ));
185 }
186
187 for (y, row) in level.map.iter().enumerate() {
188 let expected = self.map[y].len();
189
190 if row.len() != expected {
191 return Err(format!(
192 "map levels: level {key} row {y} has {} cells, map has {expected}",
193 row.len()
194 ));
195 }
196 }
197
198 for tile in &level.walls {
199 if !level.floor.contains(tile) {
200 return Err(format!(
201 "map levels: level {key} wall tile {tile} is not part of floor"
202 ));
203 }
204 }
205 }
206
207 for (index, ramp) in self.ramps.iter().enumerate() {
208 if ramp.from == ramp.to {
209 return Err(format!("map ramps: ramp {index} goes from level {} to itself", ramp.from));
210 }
211
212 if (ramp.from as usize) >= level_count || (ramp.to as usize) >= level_count {
213 return Err(format!(
214 "map ramps: ramp {index} references level out of range (levels: {level_count})"
215 ));
216 }
217
218 let grid = if ramp.from == 0 {
219 Some(&self.map)
220 } else {
221 self.levels.get(&ramp.from.to_string()).map(|level| &level.map)
222 };
223
224 let found = grid.is_some_and(|grid| {
225 grid.iter().any(|row| row.contains(&ramp.tile))
226 });
227
228 if !found {
229 return Err(format!(
230 "map ramps: ramp {index} tile {} is missing from level {} grid",
231 ramp.tile, ramp.from
232 ));
233 }
234 }
235
236 for (team, points) in &self.respawns {
237 for (index, point) in points.iter().enumerate() {
238 if point.len() != 3 && point.len() != 4 {
239 return Err(format!(
240 "map respawns: {team}[{index}] has {} numbers, expected 3 or 4",
241 point.len()
242 ));
243 }
244
245 if point.len() == 4 && (point[3] as usize) >= level_count {
246 return Err(format!(
247 "map respawns: {team}[{index}] level {} is out of range (levels: {level_count})",
248 point[3]
249 ));
250 }
251 }
252 }
253
254 for (index, object) in self.physics_dynamic.iter().enumerate() {
255 if (object.level as usize) >= level_count {
256 return Err(format!(
257 "map physicsDynamic: object {index} level {} is out of range (levels: {level_count})",
258 object.level
259 ));
260 }
261 }
262
263 Ok(())
264 }
265}
266
267pub fn level_group(level: u8) -> Group {
272 match level {
273 0 => Group::GROUP_1,
274 _ => Group::GROUP_2,
275 }
276}
277
278pub fn level_interaction(level: u8) -> InteractionGroups {
280 let group = level_group(level);
281
282 InteractionGroups::new(group, group, InteractionTestMode::And)
283}
284
285pub fn levels_interaction(mask: Group) -> InteractionGroups {
287 InteractionGroups::new(mask, mask, InteractionTestMode::And)
288}
289
290#[derive(Clone, Copy, Debug, PartialEq)]
292pub struct RampSample {
293 pub progress: f32,
295 pub from: u8,
296 pub to: u8,
297}
298
299#[derive(Clone, Serialize, Deserialize)]
302pub struct RampRun {
303 pub axis: u8,
305 pub sign: i8,
307 pub from: u8,
308 pub to: u8,
309 pub min: f32,
311 pub max: f32,
312 pub cross_min: f32,
315 pub cross_max: f32,
316}
317
318#[derive(Clone, Default, Serialize, Deserialize)]
323pub struct MapLevels {
324 grids: Vec<Vec<Vec<i32>>>,
326 solid: Vec<Vec<i32>>,
328 floor: Vec<Vec<i32>>,
330 runs: Vec<RampRun>,
331 run_cells: Vec<Vec<i16>>,
333 tile_size: f32,
335}
336
337impl MapLevels {
338 pub fn build(
342 grid0: &[Vec<i32>],
343 solid0: &[i32],
344 levels: &IndexMap<String, MapLevelConfig>,
345 ramps: &[RampConfig],
346 tile_size: f32,
347 ) -> Self {
348 let mut out = Self {
349 grids: vec![grid0.to_vec()],
350 solid: vec![solid0.to_vec()],
351 floor: vec![Vec::new()],
352 runs: Vec::new(),
353 run_cells: grid0.iter().map(|row| vec![-1i16; row.len()]).collect(),
354 tile_size,
355 };
356
357 let mut ordered: Vec<(u8, &MapLevelConfig)> = levels
360 .iter()
361 .filter_map(|(key, level)| key.parse::<u8>().ok().map(|index| (index, level)))
362 .collect();
363
364 ordered.sort_by_key(|(index, _)| *index);
365
366 for (index, level) in ordered {
367 if index as usize != out.grids.len() {
368 continue;
369 }
370
371 out.grids.push(level.map.clone());
372 out.solid.push(level.walls.clone());
373 out.floor.push(level.floor.clone());
374 }
375
376 out.build_runs(ramps);
377
378 out
379 }
380
381 fn build_runs(&mut self, ramps: &[RampConfig]) {
383 for ramp in ramps {
384 let (axis, sign) = ramp.dir.axis_sign();
385 let Some(grid) = self.grid(ramp.from) else {
386 continue;
387 };
388 let grid = grid.clone();
389 let rows = grid.len();
390
391 if axis == 1 {
392 let cols = grid.iter().map(|row| row.len()).max().unwrap_or(0);
393
394 for x in 0..cols {
395 let mut y = 0;
396
397 while y < rows {
398 if grid[y].get(x) != Some(&ramp.tile) {
399 y += 1;
400 continue;
401 }
402
403 let y0 = y;
404
405 while y < rows && grid[y].get(x) == Some(&ramp.tile) {
406 y += 1;
407 }
408
409 self.push_run(ramp, axis, sign, y0, y, x, x + 1);
410 }
411 }
412 } else {
413 for (y, row) in grid.iter().enumerate() {
414 let mut x = 0;
415
416 while x < row.len() {
417 if row[x] != ramp.tile {
418 x += 1;
419 continue;
420 }
421
422 let x0 = x;
423
424 while x < row.len() && row[x] == ramp.tile {
425 x += 1;
426 }
427
428 self.push_run(ramp, axis, sign, x0, x, y, y + 1);
429 }
430 }
431 }
432 }
433 }
434
435 #[allow(clippy::too_many_arguments)]
439 fn push_run(
440 &mut self,
441 ramp: &RampConfig,
442 axis: u8,
443 sign: i8,
444 from: usize,
445 to: usize,
446 cross0: usize,
447 cross1: usize,
448 ) {
449 if self.runs.len() >= i16::MAX as usize {
450 return;
451 }
452
453 let index = self.runs.len() as i16;
454 let mut claimed = false;
455
456 for main in from..to {
457 for cross in cross0..cross1 {
458 let (x, y) = if axis == 1 { (cross, main) } else { (main, cross) };
459
460 let Some(cell) = self.run_cells.get_mut(y).and_then(|row| row.get_mut(x)) else {
461 continue;
462 };
463
464 if *cell < 0 {
465 *cell = index;
466 claimed = true;
467 }
468 }
469 }
470
471 if !claimed {
472 return;
473 }
474
475 let size = self.tile_size;
476
477 self.runs.push(RampRun {
478 axis,
479 sign,
480 from: ramp.from,
481 to: ramp.to,
482 min: from as f32 * size,
483 max: to as f32 * size,
484 cross_min: cross0 as f32 * size,
485 cross_max: cross1 as f32 * size,
486 });
487 }
488
489 pub fn is_layered(&self) -> bool {
491 self.grids.len() > 1
492 }
493
494 pub fn level_count(&self) -> usize {
496 self.grids.len().max(1)
497 }
498
499 pub fn tile_size(&self) -> f32 {
500 self.tile_size
501 }
502
503 pub fn grid(&self, level: u8) -> Option<&Vec<Vec<i32>>> {
504 self.grids.get(level as usize)
505 }
506
507 pub fn solid(&self, level: u8) -> &[i32] {
508 self.solid.get(level as usize).map_or(&[], |list| list)
509 }
510
511 pub fn floor(&self, level: u8) -> &[i32] {
512 self.floor.get(level as usize).map_or(&[], |list| list)
513 }
514
515 pub fn runs(&self) -> &[RampRun] {
516 &self.runs
517 }
518
519 pub fn cell_at(&self, x: f32, y: f32) -> Option<(usize, usize)> {
521 if self.tile_size <= 0.0 || x < 0.0 || y < 0.0 {
522 return None;
523 }
524
525 let cx = (x / self.tile_size).floor() as usize;
526 let cy = (y / self.tile_size).floor() as usize;
527 let row = self.grids.first()?.get(cy)?;
528
529 if cx >= row.len() { None } else { Some((cx, cy)) }
530 }
531
532 pub fn has_floor(&self, level: u8, x: f32, y: f32) -> bool {
535 let Some((cx, cy)) = self.cell_at(x, y) else {
536 return false;
537 };
538
539 if level == 0 {
540 return true;
541 }
542
543 let Some(grid) = self.grid(level) else {
544 return false;
545 };
546 let Some(&tile) = grid.get(cy).and_then(|row| row.get(cx)) else {
547 return false;
548 };
549
550 self.floor(level).contains(&tile)
551 }
552
553 pub fn is_solid(&self, level: u8, x: f32, y: f32) -> bool {
555 let Some((cx, cy)) = self.cell_at(x, y) else {
556 return false;
557 };
558 let Some(grid) = self.grid(level) else {
559 return false;
560 };
561 let Some(&tile) = grid.get(cy).and_then(|row| row.get(cx)) else {
562 return false;
563 };
564
565 self.solid(level).contains(&tile)
566 }
567
568 pub fn level_at(&self, x: f32, y: f32) -> u8 {
571 let mut level = 0;
572
573 for candidate in (1..self.level_count() as u8).rev() {
574 if self.has_floor(candidate, x, y) {
575 level = candidate;
576 break;
577 }
578 }
579
580 level
581 }
582
583 pub fn ramp_at(&self, x: f32, y: f32) -> Option<RampSample> {
585 let (cx, cy) = self.cell_at(x, y)?;
586 let index = *self.run_cells.get(cy)?.get(cx)?;
587
588 if index < 0 {
589 return None;
590 }
591
592 let run = &self.runs[index as usize];
593 let value = if run.axis == 0 { x } else { y };
594 let span = run.max - run.min;
595 let raw = if span <= 0.0 { 0.0 } else { (value - run.min) / span };
596 let progress = if run.sign > 0 { raw } else { 1.0 - raw };
597
598 Some(RampSample {
599 progress: progress.clamp(0.0, 1.0),
600 from: run.from,
601 to: run.to,
602 })
603 }
604}
605
606#[derive(Serialize, Deserialize)]
609pub struct GameMap {
610 pub set_id: String,
611 pub step: f32,
613 pub grid: Vec<Vec<i32>>,
615 pub physics_static: Vec<i32>,
616 pub respawns: IndexMap<String, Vec<Vec<f32>>>,
619 levels: MapLevels,
621 static_bodies: Vec<RigidBodyHandle>,
622 static_levels: Vec<u8>,
624 dynamic_bodies: Vec<RigidBodyHandle>,
625 dynamic_levels: Vec<u8>,
627}
628
629impl GameMap {
630 pub fn create(
633 world: &mut PhysicsWorld,
634 cfg: &MapConfig,
635 default_scale: f32,
636 default_set_id: &str,
637 ) -> Self {
638 let scale = cfg.scale.unwrap_or(default_scale);
639 let step = cfg.step * scale;
640 let levels = MapLevels::build(
641 &cfg.map,
642 &cfg.physics_static,
643 &cfg.levels,
644 &cfg.ramps,
645 step,
646 );
647
648 let mut map = Self {
649 set_id: cfg
650 .set_id
651 .clone()
652 .unwrap_or_else(|| default_set_id.to_string()),
653 step,
654 grid: cfg.map.clone(),
655 physics_static: cfg.physics_static.clone(),
656 respawns: cfg
657 .respawns
658 .iter()
659 .map(|(team, arr)| {
660 (
661 team.clone(),
662 arr.iter()
663 .map(|point| {
664 let mut out = point.clone();
665
666 if out.len() >= 2 {
667 out[0] *= scale;
668 out[1] *= scale;
669 }
670
671 out
672 })
673 .collect(),
674 )
675 })
676 .collect(),
677 levels,
678 static_bodies: Vec::new(),
679 static_levels: Vec::new(),
680 dynamic_bodies: Vec::new(),
681 dynamic_levels: Vec::new(),
682 };
683
684 map.create_static(world);
685 map.create_dynamic(world, &cfg.physics_dynamic, scale);
686
687 map
688 }
689
690 fn create_static(&mut self, world: &mut PhysicsWorld) {
695 for level in 0..self.levels.level_count() as u8 {
696 let Some(grid) = self.levels.grid(level) else {
697 continue;
698 };
699 let solid = self.levels.solid(level).to_vec();
700
701 let mut work: Vec<Vec<Option<i32>>> = grid
702 .iter()
703 .map(|row| row.iter().map(|&tile| Some(tile)).collect())
704 .collect();
705
706 for y in 0..work.len() {
707 for x in 0..work[y].len() {
708 let is_static = work[y][x].is_some_and(|tile| solid.contains(&tile));
709
710 if is_static {
711 let (width, height) =
712 search_static_block(&mut work, &solid, self.step, y, x);
713 let pos_x = x as f32 * self.step + width / 2.0;
714 let pos_y = y as f32 * self.step + height / 2.0;
715
716 let body = world.insert_body(
717 RigidBodyBuilder::fixed().translation(Vector::new(pos_x, pos_y)),
718 );
719
720 world.insert_collider(
721 ColliderBuilder::cuboid(width / 2.0, height / 2.0)
722 .friction(DEFAULT_FRICTION)
723 .restitution(DEFAULT_RESTITUTION)
724 .collision_groups(level_interaction(level)),
725 Some(body),
726 );
727
728 self.static_bodies.push(body);
729 self.static_levels.push(level);
730 }
731 }
732 }
733 }
734 }
735
736 fn create_dynamic(
738 &mut self,
739 world: &mut PhysicsWorld,
740 dynamics: &[DynamicObjectConfig],
741 scale: f32,
742 ) {
743 for data in dynamics {
744 let pos_x = data.position[0] * scale;
745 let pos_y = data.position[1] * scale;
746 let width = data.width * scale;
747 let height = data.height * scale;
748
749 let body = world.insert_body(
750 RigidBodyBuilder::dynamic()
751 .translation(Vector::new(pos_x, pos_y))
752 .rotation(deg_to_rad(data.angle))
753 .linear_damping(data.linear_damping.unwrap_or(0.0))
754 .angular_damping(data.angular_damping.unwrap_or(0.01))
755 .soft_ccd_prediction(width.min(height))
759 .user_data(encode_map_object()),
760 );
761
762 world.insert_collider(
764 ColliderBuilder::cuboid(width / 2.0, height / 2.0)
765 .translation(Vector::new(width / 2.0, height / 2.0))
766 .density(data.density)
767 .friction(DEFAULT_FRICTION)
768 .restitution(DEFAULT_RESTITUTION)
769 .collision_groups(level_interaction(data.level)),
770 Some(body),
771 );
772
773 self.dynamic_bodies.push(body);
774 self.dynamic_levels.push(data.level);
775 }
776 }
777
778 pub fn levels(&self) -> &MapLevels {
781 &self.levels
782 }
783
784 pub fn is_layered(&self) -> bool {
785 self.levels.is_layered()
786 }
787
788 pub fn level_count(&self) -> usize {
789 self.levels.level_count()
790 }
791
792 pub fn level_at(&self, x: f32, y: f32) -> u8 {
793 self.levels.level_at(x, y)
794 }
795
796 pub fn has_floor(&self, level: u8, x: f32, y: f32) -> bool {
797 self.levels.has_floor(level, x, y)
798 }
799
800 pub fn ramp_at(&self, x: f32, y: f32) -> Option<RampSample> {
801 self.levels.ramp_at(x, y)
802 }
803
804 pub fn dynamic_level(&self, index: usize) -> u8 {
806 self.dynamic_levels.get(index).copied().unwrap_or(0)
807 }
808
809 pub fn static_levels(&self) -> &[u8] {
811 &self.static_levels
812 }
813
814 pub fn dynamic_levels(&self) -> &[u8] {
816 &self.dynamic_levels
817 }
818
819 pub fn static_body_count(&self) -> usize {
822 self.static_bodies.len()
823 }
824
825 pub fn dynamic_body_count(&self) -> usize {
826 self.dynamic_bodies.len()
827 }
828
829 pub fn destroy(&mut self, world: &mut PhysicsWorld) {
831 for handle in self.static_bodies.drain(..) {
832 world.remove_body(handle);
833 }
834
835 for handle in self.dynamic_bodies.drain(..) {
836 world.remove_body(handle);
837 }
838
839 self.static_levels.clear();
840 self.dynamic_levels.clear();
841 }
842
843 pub fn dynamic_map_data(
853 &self,
854 world: &PhysicsWorld,
855 with_velocities: bool,
856 ) -> Vec<(u8, Vec<FieldValue>)> {
857 self.dynamic_bodies
858 .iter()
859 .enumerate()
860 .filter_map(|(index, &handle)| {
861 world.bodies.get(handle).map(|body| {
862 let pos = body.translation();
863
864 let mut fields = vec![
865 FieldValue::F32(round2(pos.x)),
866 FieldValue::F32(round2(pos.y)),
867 FieldValue::F32(round2(body.rotation().angle())),
868 ];
869
870 if with_velocities {
871 let linvel = body.linvel();
872 let angvel = body.angvel();
873 let resting = body.is_sleeping()
874 || (linvel.x.hypot(linvel.y) < REST_VELOCITY_EPSILON
875 && angvel.abs() < REST_VELOCITY_EPSILON);
876
877 if !resting {
878 fields.push(FieldValue::F32(round2(linvel.x)));
879 fields.push(FieldValue::F32(round2(linvel.y)));
880 fields.push(FieldValue::F32(round2(angvel)));
881 }
882 }
883
884 (index as u8, fields)
885 })
886 })
887 .collect()
888 }
889}
890
891fn search_static_block(
895 work: &mut [Vec<Option<i32>>],
896 solid: &[i32],
897 step: f32,
898 y0: usize,
899 x0: usize,
900) -> (f32, f32) {
901 let mut x = x0;
902 let mut w_counter = 0;
903 let mut h_counter = 1;
904
905 while x < work[y0].len() && work[y0][x].is_some_and(|tile| solid.contains(&tile)) {
907 work[y0][x] = None;
908 x += 1;
909 w_counter += 1;
910 }
911
912 let len_x = x;
913 let len_y = work.len();
914
915 for y in (y0 + 1)..len_y {
917 let mut empty_tile = false;
918 let mut x = x0;
919
920 while x < len_x {
921 if x < work[y].len() && work[y][x].is_some_and(|tile| solid.contains(&tile)) {
922 x += 1;
923 } else {
924 empty_tile = true;
925 break;
926 }
927 }
928
929 if empty_tile {
930 break;
931 }
932
933 h_counter += 1;
934
935 for cell in work[y][x0..len_x].iter_mut() {
936 *cell = None;
937 }
938 }
939
940 (w_counter as f32 * step, h_counter as f32 * step)
941}
942
943#[cfg(test)]
944mod tests {
945 use super::*;
946
947 const TIME_STEP: f32 = 1.0 / 120.0;
948
949 fn map_config() -> MapConfig {
951 serde_json::from_value(serde_json::json!({
952 "step": 20.0,
953 "map": [[1, 1, 1, 1, 1], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]],
954 "physicsStatic": [1],
955 "physicsDynamic": [{
956 "position": [40.0, 60.0],
957 "angle": 0.0,
958 "width": 20.0,
959 "height": 20.0,
960 "density": 1.0
961 }]
962 }))
963 .unwrap()
964 }
965
966 fn make_world() -> PhysicsWorld {
967 let mut world = PhysicsWorld::new();
968
969 world.gravity = Vector::ZERO;
970 world.integration_parameters.dt = TIME_STEP;
971
972 world
973 }
974
975 fn max_penetration(world: &mut PhysicsWorld, body: RigidBodyHandle) -> f32 {
977 let mut depth: f32 = 0.0;
978
979 world.bodies[body].set_linvel(Vector::new(0.0, -2000.0), true);
982
983 for _ in 0..120 {
984 world.step();
985
986 for pair in world.contact_pairs() {
987 for manifold in &pair.manifolds {
988 for point in &manifold.points {
989 depth = depth.max(-point.dist);
990 }
991 }
992 }
993 }
994
995 depth
996 }
997
998 fn layered_config() -> MapConfig {
1001 serde_json::from_value(serde_json::json!({
1002 "step": 20.0,
1003 "map": [[1, 0, 0], [0, 0, 0], [0, 0, 0]],
1004 "physicsStatic": [1],
1005 "levels": {
1006 "1": {
1007 "map": [[0, 0, 0], [0, 5, 6], [0, 0, 0]],
1008 "floor": [5, 6],
1009 "walls": [6]
1010 }
1011 }
1012 }))
1013 .unwrap()
1014 }
1015
1016 fn ramp_config(dir: &str) -> MapConfig {
1018 serde_json::from_value(serde_json::json!({
1019 "step": 20.0,
1020 "map": [[7, 0, 0], [7, 0, 0], [7, 0, 0]],
1021 "physicsStatic": [],
1022 "levels": {
1023 "1": { "map": [[0, 0, 0], [0, 0, 0], [0, 0, 0]], "floor": [] }
1024 },
1025 "ramps": [{ "tile": 7, "dir": dir }]
1026 }))
1027 .unwrap()
1028 }
1029
1030 fn collision_groups(world: &PhysicsWorld, body: RigidBodyHandle) -> InteractionGroups {
1031 let handle = world.bodies[body].colliders()[0];
1032
1033 world.colliders[handle].collision_groups()
1034 }
1035
1036 #[test]
1037 fn legacy_map_has_no_levels() {
1038 let mut world = make_world();
1039 let map = GameMap::create(&mut world, &map_config(), 1.0, "set");
1040
1041 assert!(!map.is_layered());
1042 assert_eq!(map.level_count(), 1);
1043 assert_eq!(map.static_body_count(), 1);
1045 assert_eq!(
1046 world.bodies[map.static_bodies[0]].translation(),
1047 Vector::new(50.0, 10.0)
1048 );
1049 }
1050
1051 #[test]
1052 fn layered_map_builds_static_per_level() {
1053 let mut world = make_world();
1054 let map = GameMap::create(&mut world, &layered_config(), 1.0, "set");
1055
1056 assert!(map.is_layered());
1057 assert_eq!(map.static_body_count(), 2);
1058 assert_eq!(map.static_levels(), &[0, 1]);
1059 assert_eq!(
1060 collision_groups(&world, map.static_bodies[0]).memberships,
1061 Group::GROUP_1
1062 );
1063 assert_eq!(
1064 collision_groups(&world, map.static_bodies[1]).memberships,
1065 Group::GROUP_2
1066 );
1067 }
1068
1069 #[test]
1070 fn dynamic_body_carries_level_group() {
1071 let mut cfg = layered_config();
1072
1073 cfg.physics_dynamic = vec![DynamicObjectConfig {
1074 position: [20.0, 20.0],
1075 angle: 0.0,
1076 width: 20.0,
1077 height: 20.0,
1078 density: 1.0,
1079 linear_damping: None,
1080 angular_damping: None,
1081 level: 1,
1082 }];
1083
1084 let mut world = make_world();
1085 let map = GameMap::create(&mut world, &cfg, 1.0, "set");
1086
1087 assert_eq!(map.dynamic_level(0), 1);
1088 assert_eq!(
1089 collision_groups(&world, map.dynamic_bodies[0]).memberships,
1090 Group::GROUP_2
1091 );
1092 }
1093
1094 #[test]
1095 fn has_floor_reports_slab_and_ground() {
1096 let mut world = make_world();
1097 let map = GameMap::create(&mut world, &layered_config(), 1.0, "set");
1098
1099 assert!(map.has_floor(0, 10.0, 10.0));
1100 assert!(!map.has_floor(0, -5.0, 10.0));
1101 assert!(!map.has_floor(0, 70.0, 10.0));
1102
1103 assert!(map.has_floor(1, 30.0, 30.0));
1104 assert!(map.has_floor(1, 50.0, 30.0));
1105 assert!(!map.has_floor(1, 10.0, 10.0));
1106 }
1107
1108 #[test]
1109 fn level_at_picks_highest_floor() {
1110 let mut world = make_world();
1111 let map = GameMap::create(&mut world, &layered_config(), 1.0, "set");
1112
1113 assert_eq!(map.level_at(30.0, 30.0), 1);
1114 assert_eq!(map.level_at(10.0, 30.0), 0);
1115 }
1116
1117 #[test]
1118 fn ramp_progress_runs_from_bottom_to_top() {
1119 let mut world = make_world();
1120 let north = GameMap::create(&mut world, &ramp_config("north"), 1.0, "set");
1121
1122 assert!(north.ramp_at(10.0, 59.0).unwrap().progress < 0.05);
1124 assert!(north.ramp_at(10.0, 1.0).unwrap().progress > 0.95);
1125 assert!((north.ramp_at(10.0, 30.0).unwrap().progress - 0.5).abs() < 0.01);
1126 assert_eq!(north.ramp_at(10.0, 30.0).unwrap().to, 1);
1127 assert!(north.ramp_at(30.0, 30.0).is_none());
1128
1129 let mut world = make_world();
1130 let south = GameMap::create(&mut world, &ramp_config("south"), 1.0, "set");
1131
1132 assert!(south.ramp_at(10.0, 1.0).unwrap().progress < 0.05);
1133 assert!(south.ramp_at(10.0, 59.0).unwrap().progress > 0.95);
1134 }
1135
1136 #[test]
1137 fn ramp_runs_are_split_per_line() {
1138 let mut cfg = ramp_config("north");
1139
1140 for row in &mut cfg.map {
1142 row[1] = 7;
1143 }
1144
1145 let mut world = make_world();
1146 let map = GameMap::create(&mut world, &cfg, 1.0, "set");
1147 let runs = map.levels().runs();
1148
1149 assert_eq!(runs.len(), 2);
1150 assert_eq!((runs[0].min, runs[0].max), (0.0, 60.0));
1151 assert_eq!((runs[1].min, runs[1].max), (0.0, 60.0));
1152 assert_ne!(runs[0].cross_min, runs[1].cross_min);
1153 }
1154
1155 #[test]
1156 fn respawn_accepts_three_and_four_numbers() {
1157 let cfg: MapConfig = serde_json::from_value(serde_json::json!({
1158 "step": 20.0,
1159 "map": [[0, 0], [0, 0]],
1160 "levels": { "1": { "map": [[0, 0], [0, 0]], "floor": [] } },
1161 "respawns": { "team1": [[10.0, 20.0, 0.0], [30.0, 40.0, 90.0, 1.0]] }
1162 }))
1163 .unwrap();
1164
1165 cfg.validate().unwrap();
1166
1167 let mut world = make_world();
1168 let map = GameMap::create(&mut world, &cfg, 2.0, "set");
1169 let points = &map.respawns["team1"];
1170
1171 assert_eq!(points[0], vec![20.0, 40.0, 0.0]);
1172 assert_eq!(points[1], vec![60.0, 80.0, 90.0, 1.0]);
1173 }
1174
1175 #[test]
1176 fn validate_rejects_mismatched_level_grid() {
1177 let mut cfg = layered_config();
1178
1179 cfg.levels["1"].map.pop();
1180
1181 let error = cfg.validate().unwrap_err();
1182
1183 assert!(error.contains("rows"), "{error}");
1184 }
1185
1186 #[test]
1187 fn validate_rejects_railing_outside_floor() {
1188 let mut cfg = layered_config();
1189
1190 cfg.levels["1"].floor = vec![5];
1191
1192 let error = cfg.validate().unwrap_err();
1193
1194 assert!(error.contains("floor"), "{error}");
1195 }
1196
1197 #[test]
1198 fn validate_rejects_unknown_ramp_tile() {
1199 let mut cfg = ramp_config("north");
1200
1201 cfg.ramps[0].tile = 99;
1202
1203 let error = cfg.validate().unwrap_err();
1204
1205 assert!(error.contains("missing"), "{error}");
1206 }
1207
1208 #[test]
1209 fn validate_rejects_level_out_of_range() {
1210 let cfg: MapConfig = serde_json::from_value(serde_json::json!({
1211 "step": 20.0,
1212 "map": [[0, 0], [0, 0]],
1213 "respawns": { "team1": [[10.0, 20.0, 0.0, 5.0]] }
1214 }))
1215 .unwrap();
1216
1217 let error = cfg.validate().unwrap_err();
1218
1219 assert!(error.contains("out of range"), "{error}");
1220 }
1221
1222 #[test]
1223 fn dynamic_body_gets_soft_ccd_prediction_of_thickness() {
1224 let mut world = make_world();
1225 let map = GameMap::create(&mut world, &map_config(), 1.0, "set");
1226
1227 let prediction = world.bodies[map.dynamic_bodies[0]].soft_ccd_prediction();
1228
1229 assert_eq!(prediction, 20.0);
1232 }
1233
1234 #[test]
1235 fn soft_ccd_prediction_keeps_penetration_shallow() {
1236 let mut world = make_world();
1237 let map = GameMap::create(&mut world, &map_config(), 1.0, "set");
1238 let handle = map.dynamic_bodies[0];
1239
1240 let predicted = max_penetration(&mut world, handle);
1241
1242 let mut plain_world = make_world();
1243 let plain_map = GameMap::create(&mut plain_world, &map_config(), 1.0, "set");
1244 let plain_handle = plain_map.dynamic_bodies[0];
1245
1246 plain_world.bodies[plain_handle].set_soft_ccd_prediction(0.0);
1247
1248 let plain = max_penetration(&mut plain_world, plain_handle);
1249
1250 assert!(
1251 plain > 5.0,
1252 "без предсказания ожидалось глубокое перекрытие, получено {plain}"
1253 );
1254 assert!(predicted < 2.0, "с предсказанием перекрытие {predicted}");
1255 }
1256}