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