Skip to main content

vimp_engine_core/
map.rs

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
8// параметры поверхности по умолчанию (дефолты planck/Box2D,
9// с которыми сбалансировано ощущение управления)
10const DEFAULT_FRICTION: f32 = 0.2;
11const DEFAULT_RESTITUTION: f32 = 0.0;
12
13/// Динамический объект карты (physicsDynamic из src/data/maps/*).
14#[derive(Clone, Deserialize)]
15#[serde(rename_all = "camelCase")]
16pub struct DynamicObjectConfig {
17    pub position: [f32; 2],
18    pub angle: f32,
19    pub width: f32,
20    pub height: f32,
21    pub density: f32,
22    #[serde(default)]
23    pub linear_damping: Option<f32>,
24    #[serde(default)]
25    pub angular_damping: Option<f32>,
26}
27
28/// JSON карты (src/data/maps/*.js, экспортированный в .json).
29/// Поля рендера (spriteSheet, layers) ядру не нужны и игнорируются.
30#[derive(Clone, Deserialize)]
31#[serde(rename_all = "camelCase")]
32pub struct MapConfig {
33    #[serde(default)]
34    pub set_id: Option<String>,
35    #[serde(default)]
36    pub scale: Option<f32>,
37    pub step: f32,
38    pub map: Vec<Vec<i32>>,
39    #[serde(default)]
40    pub physics_static: Vec<i32>,
41    #[serde(default)]
42    pub physics_dynamic: Vec<DynamicObjectConfig>,
43    #[serde(default)]
44    pub respawns: IndexMap<String, Vec<[f32; 3]>>,
45}
46
47/// Карта в мире: порт физической части src/server/parts/Map.js +
48/// масштабирование из RoundManager.createMap (scaleMapData).
49#[derive(Serialize, Deserialize)]
50pub struct GameMap {
51    pub set_id: String,
52    /// Размер тайла после масштабирования.
53    pub step: f32,
54    /// Сетка тайлов (немасштабируемая) — источник нав-сетки ботов.
55    pub grid: Vec<Vec<i32>>,
56    pub physics_static: Vec<i32>,
57    /// Респауны по командам (масштабированные) — [x, y, angle°].
58    pub respawns: IndexMap<String, Vec<[f32; 3]>>,
59    static_bodies: Vec<RigidBodyHandle>,
60    dynamic_bodies: Vec<RigidBodyHandle>,
61}
62
63impl GameMap {
64    /// Создаёт тела карты в мире. `default_scale`/`default_set_id` —
65    /// значения конфига, перекрываемые полями самой карты.
66    pub fn create(
67        world: &mut PhysicsWorld,
68        cfg: &MapConfig,
69        default_scale: f32,
70        default_set_id: &str,
71    ) -> Self {
72        let scale = cfg.scale.unwrap_or(default_scale);
73        let step = cfg.step * scale;
74
75        let mut map = Self {
76            set_id: cfg
77                .set_id
78                .clone()
79                .unwrap_or_else(|| default_set_id.to_string()),
80            step,
81            grid: cfg.map.clone(),
82            physics_static: cfg.physics_static.clone(),
83            respawns: cfg
84                .respawns
85                .iter()
86                .map(|(team, arr)| {
87                    (
88                        team.clone(),
89                        arr.iter()
90                            .map(|[x, y, angle]| [x * scale, y * scale, *angle])
91                            .collect(),
92                    )
93                })
94                .collect(),
95            static_bodies: Vec::new(),
96            dynamic_bodies: Vec::new(),
97        };
98
99        map.create_static(world);
100        map.create_dynamic(world, &cfg.physics_dynamic, scale);
101
102        map
103    }
104
105    /// Жадный поиск прямоугольного блока стен (Map.searchStaticBlock).
106    /// Обработанные ячейки затираются в рабочей копии сетки.
107    fn search_static_block(&self, work: &mut [Vec<Option<i32>>], y0: usize, x0: usize) -> (f32, f32) {
108        let mut x = x0;
109        let mut w_counter = 0;
110        let mut h_counter = 1;
111
112        // ширина блока
113        while x < work[y0].len()
114            && work[y0][x].is_some_and(|tile| self.physics_static.contains(&tile))
115        {
116            work[y0][x] = None;
117            x += 1;
118            w_counter += 1;
119        }
120
121        let len_x = x;
122        let len_y = work.len();
123
124        // высота блока
125        for y in (y0 + 1)..len_y {
126            let mut empty_tile = false;
127            let mut x = x0;
128
129            while x < len_x {
130                if x < work[y].len()
131                    && work[y][x].is_some_and(|tile| self.physics_static.contains(&tile))
132                {
133                    x += 1;
134                } else {
135                    empty_tile = true;
136                    break;
137                }
138            }
139
140            if empty_tile {
141                break;
142            }
143
144            h_counter += 1;
145
146            for cell in work[y][x0..len_x].iter_mut() {
147                *cell = None;
148            }
149        }
150
151        (w_counter as f32 * self.step, h_counter as f32 * self.step)
152    }
153
154    /// Статические стены (Map.createStatic).
155    fn create_static(&mut self, world: &mut PhysicsWorld) {
156        let mut work: Vec<Vec<Option<i32>>> = self
157            .grid
158            .iter()
159            .map(|row| row.iter().map(|&tile| Some(tile)).collect())
160            .collect();
161
162        for y in 0..work.len() {
163            for x in 0..work[y].len() {
164                let is_static = work[y][x].is_some_and(|tile| self.physics_static.contains(&tile));
165
166                if is_static {
167                    let (width, height) = self.search_static_block(&mut work, y, x);
168                    let pos_x = x as f32 * self.step + width / 2.0;
169                    let pos_y = y as f32 * self.step + height / 2.0;
170
171                    let body = world
172                        .insert_body(RigidBodyBuilder::fixed().translation(Vector::new(pos_x, pos_y)));
173
174                    world.insert_collider(
175                        ColliderBuilder::cuboid(width / 2.0, height / 2.0)
176                            .friction(DEFAULT_FRICTION)
177                            .restitution(DEFAULT_RESTITUTION),
178                        Some(body),
179                    );
180
181                    self.static_bodies.push(body);
182                }
183            }
184        }
185    }
186
187    /// Динамические элементы (Map.createDynamic).
188    fn create_dynamic(
189        &mut self,
190        world: &mut PhysicsWorld,
191        dynamics: &[DynamicObjectConfig],
192        scale: f32,
193    ) {
194        for data in dynamics {
195            let pos_x = data.position[0] * scale;
196            let pos_y = data.position[1] * scale;
197            let width = data.width * scale;
198            let height = data.height * scale;
199
200            let body = world.insert_body(
201                RigidBodyBuilder::dynamic()
202                    .translation(Vector::new(pos_x, pos_y))
203                    .rotation(deg_to_rad(data.angle))
204                    .linear_damping(data.linear_damping.unwrap_or(0.0))
205                    .angular_damping(data.angular_damping.unwrap_or(0.01))
206                    .user_data(encode_map_object()),
207            );
208
209            // коллайдер со смещённым центром (позиция тела — угол объекта)
210            world.insert_collider(
211                ColliderBuilder::cuboid(width / 2.0, height / 2.0)
212                    .translation(Vector::new(width / 2.0, height / 2.0))
213                    .density(data.density)
214                    .friction(DEFAULT_FRICTION)
215                    .restitution(DEFAULT_RESTITUTION),
216                Some(body),
217            );
218
219            self.dynamic_bodies.push(body);
220        }
221    }
222
223    // ***** отладочный дамп (crate::debug) ***** //
224
225    pub fn static_body_count(&self) -> usize {
226        self.static_bodies.len()
227    }
228
229    pub fn dynamic_body_count(&self) -> usize {
230        self.dynamic_bodies.len()
231    }
232
233    /// Удаляет все тела карты из мира (Map.destroyMap).
234    pub fn destroy(&mut self, world: &mut PhysicsWorld) {
235        for handle in self.static_bodies.drain(..) {
236            world.remove_body(handle);
237        }
238
239        for handle in self.dynamic_bodies.drain(..) {
240            world.remove_body(handle);
241        }
242    }
243
244    /// Краткие данные динамических элементов (Map.getDynamicMapData):
245    /// индекс → [x, y, angle] (как поля строки блока), значения скруглены
246    /// до 2 знаков.
247    pub fn dynamic_map_data(&self, world: &PhysicsWorld) -> Vec<(u8, Vec<FieldValue>)> {
248        self.dynamic_bodies
249            .iter()
250            .enumerate()
251            .filter_map(|(index, &handle)| {
252                world.bodies.get(handle).map(|body| {
253                    let pos = body.translation();
254
255                    (
256                        index as u8,
257                        vec![
258                            FieldValue::F32(round2(pos.x)),
259                            FieldValue::F32(round2(pos.y)),
260                            FieldValue::F32(round2(body.rotation().angle())),
261                        ],
262                    )
263                })
264            })
265            .collect()
266    }
267}