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
15#[derive(Clone, Deserialize)]
17#[serde(rename_all = "camelCase")]
18pub struct DynamicObjectConfig {
19 pub position: [f32; 2],
20 pub angle: f32,
21 pub width: f32,
22 pub height: f32,
23 pub density: f32,
24 #[serde(default)]
25 pub linear_damping: Option<f32>,
26 #[serde(default)]
27 pub angular_damping: Option<f32>,
28}
29
30#[derive(Clone, Deserialize)]
33#[serde(rename_all = "camelCase")]
34pub struct MapConfig {
35 #[serde(default)]
36 pub set_id: Option<String>,
37 #[serde(default)]
38 pub scale: Option<f32>,
39 pub step: f32,
40 pub map: Vec<Vec<i32>>,
41 #[serde(default)]
42 pub physics_static: Vec<i32>,
43 #[serde(default)]
44 pub physics_dynamic: Vec<DynamicObjectConfig>,
45 #[serde(default)]
46 pub respawns: IndexMap<String, Vec<[f32; 3]>>,
47}
48
49#[derive(Serialize, Deserialize)]
52pub struct GameMap {
53 pub set_id: String,
54 pub step: f32,
56 pub grid: Vec<Vec<i32>>,
58 pub physics_static: Vec<i32>,
59 pub respawns: IndexMap<String, Vec<[f32; 3]>>,
61 static_bodies: Vec<RigidBodyHandle>,
62 dynamic_bodies: Vec<RigidBodyHandle>,
63}
64
65impl GameMap {
66 pub fn create(
69 world: &mut PhysicsWorld,
70 cfg: &MapConfig,
71 default_scale: f32,
72 default_set_id: &str,
73 ) -> Self {
74 let scale = cfg.scale.unwrap_or(default_scale);
75 let step = cfg.step * scale;
76
77 let mut map = Self {
78 set_id: cfg
79 .set_id
80 .clone()
81 .unwrap_or_else(|| default_set_id.to_string()),
82 step,
83 grid: cfg.map.clone(),
84 physics_static: cfg.physics_static.clone(),
85 respawns: cfg
86 .respawns
87 .iter()
88 .map(|(team, arr)| {
89 (
90 team.clone(),
91 arr.iter()
92 .map(|[x, y, angle]| [x * scale, y * scale, *angle])
93 .collect(),
94 )
95 })
96 .collect(),
97 static_bodies: Vec::new(),
98 dynamic_bodies: Vec::new(),
99 };
100
101 map.create_static(world);
102 map.create_dynamic(world, &cfg.physics_dynamic, scale);
103
104 map
105 }
106
107 fn search_static_block(&self, work: &mut [Vec<Option<i32>>], y0: usize, x0: usize) -> (f32, f32) {
110 let mut x = x0;
111 let mut w_counter = 0;
112 let mut h_counter = 1;
113
114 while x < work[y0].len()
116 && work[y0][x].is_some_and(|tile| self.physics_static.contains(&tile))
117 {
118 work[y0][x] = None;
119 x += 1;
120 w_counter += 1;
121 }
122
123 let len_x = x;
124 let len_y = work.len();
125
126 for y in (y0 + 1)..len_y {
128 let mut empty_tile = false;
129 let mut x = x0;
130
131 while x < len_x {
132 if x < work[y].len()
133 && work[y][x].is_some_and(|tile| self.physics_static.contains(&tile))
134 {
135 x += 1;
136 } else {
137 empty_tile = true;
138 break;
139 }
140 }
141
142 if empty_tile {
143 break;
144 }
145
146 h_counter += 1;
147
148 for cell in work[y][x0..len_x].iter_mut() {
149 *cell = None;
150 }
151 }
152
153 (w_counter as f32 * self.step, h_counter as f32 * self.step)
154 }
155
156 fn create_static(&mut self, world: &mut PhysicsWorld) {
158 let mut work: Vec<Vec<Option<i32>>> = self
159 .grid
160 .iter()
161 .map(|row| row.iter().map(|&tile| Some(tile)).collect())
162 .collect();
163
164 for y in 0..work.len() {
165 for x in 0..work[y].len() {
166 let is_static = work[y][x].is_some_and(|tile| self.physics_static.contains(&tile));
167
168 if is_static {
169 let (width, height) = self.search_static_block(&mut work, y, x);
170 let pos_x = x as f32 * self.step + width / 2.0;
171 let pos_y = y as f32 * self.step + height / 2.0;
172
173 let body = world
174 .insert_body(RigidBodyBuilder::fixed().translation(Vector::new(pos_x, pos_y)));
175
176 world.insert_collider(
177 ColliderBuilder::cuboid(width / 2.0, height / 2.0)
178 .friction(DEFAULT_FRICTION)
179 .restitution(DEFAULT_RESTITUTION),
180 Some(body),
181 );
182
183 self.static_bodies.push(body);
184 }
185 }
186 }
187 }
188
189 fn create_dynamic(
191 &mut self,
192 world: &mut PhysicsWorld,
193 dynamics: &[DynamicObjectConfig],
194 scale: f32,
195 ) {
196 for data in dynamics {
197 let pos_x = data.position[0] * scale;
198 let pos_y = data.position[1] * scale;
199 let width = data.width * scale;
200 let height = data.height * scale;
201
202 let body = world.insert_body(
203 RigidBodyBuilder::dynamic()
204 .translation(Vector::new(pos_x, pos_y))
205 .rotation(deg_to_rad(data.angle))
206 .linear_damping(data.linear_damping.unwrap_or(0.0))
207 .angular_damping(data.angular_damping.unwrap_or(0.01))
208 .soft_ccd_prediction(width.min(height))
212 .user_data(encode_map_object()),
213 );
214
215 world.insert_collider(
217 ColliderBuilder::cuboid(width / 2.0, height / 2.0)
218 .translation(Vector::new(width / 2.0, height / 2.0))
219 .density(data.density)
220 .friction(DEFAULT_FRICTION)
221 .restitution(DEFAULT_RESTITUTION),
222 Some(body),
223 );
224
225 self.dynamic_bodies.push(body);
226 }
227 }
228
229 pub fn static_body_count(&self) -> usize {
232 self.static_bodies.len()
233 }
234
235 pub fn dynamic_body_count(&self) -> usize {
236 self.dynamic_bodies.len()
237 }
238
239 pub fn destroy(&mut self, world: &mut PhysicsWorld) {
241 for handle in self.static_bodies.drain(..) {
242 world.remove_body(handle);
243 }
244
245 for handle in self.dynamic_bodies.drain(..) {
246 world.remove_body(handle);
247 }
248 }
249
250 pub fn dynamic_map_data(&self, world: &PhysicsWorld) -> Vec<(u8, Vec<FieldValue>)> {
254 self.dynamic_bodies
255 .iter()
256 .enumerate()
257 .filter_map(|(index, &handle)| {
258 world.bodies.get(handle).map(|body| {
259 let pos = body.translation();
260
261 (
262 index as u8,
263 vec![
264 FieldValue::F32(round2(pos.x)),
265 FieldValue::F32(round2(pos.y)),
266 FieldValue::F32(round2(body.rotation().angle())),
267 ],
268 )
269 })
270 })
271 .collect()
272 }
273}
274
275#[cfg(test)]
276mod tests {
277 use super::*;
278
279 const TIME_STEP: f32 = 1.0 / 120.0;
280
281 fn map_config() -> MapConfig {
283 serde_json::from_value(serde_json::json!({
284 "step": 20.0,
285 "map": [[1, 1, 1, 1, 1], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]],
286 "physicsStatic": [1],
287 "physicsDynamic": [{
288 "position": [40.0, 60.0],
289 "angle": 0.0,
290 "width": 20.0,
291 "height": 20.0,
292 "density": 1.0
293 }]
294 }))
295 .unwrap()
296 }
297
298 fn make_world() -> PhysicsWorld {
299 let mut world = PhysicsWorld::new();
300
301 world.gravity = Vector::ZERO;
302 world.integration_parameters.dt = TIME_STEP;
303
304 world
305 }
306
307 fn max_penetration(world: &mut PhysicsWorld, body: RigidBodyHandle) -> f32 {
309 let mut depth: f32 = 0.0;
310
311 world.bodies[body].set_linvel(Vector::new(0.0, -2000.0), true);
314
315 for _ in 0..120 {
316 world.step();
317
318 for pair in world.contact_pairs() {
319 for manifold in &pair.manifolds {
320 for point in &manifold.points {
321 depth = depth.max(-point.dist);
322 }
323 }
324 }
325 }
326
327 depth
328 }
329
330 #[test]
331 fn dynamic_body_gets_soft_ccd_prediction_of_thickness() {
332 let mut world = make_world();
333 let map = GameMap::create(&mut world, &map_config(), 1.0, "set");
334
335 let prediction = world.bodies[map.dynamic_bodies[0]].soft_ccd_prediction();
336
337 assert_eq!(prediction, 20.0);
340 }
341
342 #[test]
343 fn soft_ccd_prediction_keeps_penetration_shallow() {
344 let mut world = make_world();
345 let map = GameMap::create(&mut world, &map_config(), 1.0, "set");
346 let handle = map.dynamic_bodies[0];
347
348 let predicted = max_penetration(&mut world, handle);
349
350 let mut plain_world = make_world();
351 let plain_map = GameMap::create(&mut plain_world, &map_config(), 1.0, "set");
352 let plain_handle = plain_map.dynamic_bodies[0];
353
354 plain_world.bodies[plain_handle].set_soft_ccd_prediction(0.0);
355
356 let plain = max_penetration(&mut plain_world, plain_handle);
357
358 assert!(
359 plain > 5.0,
360 "без предсказания ожидалось глубокое перекрытие, получено {plain}"
361 );
362 assert!(predicted < 2.0, "с предсказанием перекрытие {predicted}");
363 }
364}