Skip to main content

vimp_engine_core/
game.rs

1use std::sync::mpsc::channel;
2
3use rapier2d::prelude::*;
4use serde::{Deserialize, Serialize};
5
6use crate::nav::navigation::NavigationSystem;
7use crate::nav::spatial::SpatialGrid;
8use crate::config::{EngineConfig, PLAYER_STATE_LEN};
9use crate::events::CoreEvent;
10use crate::map::{GameMap, MapConfig};
11use crate::rng::Rng;
12use crate::sim::{GameDef, GameSim, SimCtx};
13use crate::snapshot::Block;
14
15/// защита от «спирали смерти» (Game._maxAccumulatedTime)
16const MAX_ACCUMULATED_TIME: f32 = 0.1;
17
18/// Движковый каркас симуляции: физический мир, карта, нав-граф/сетка
19/// ботов, PRNG, аккумулятор фиксированного шага, очередь удаления тел,
20/// события. Игровая логика (участники/оружие/снапшот-блоки) вынесена в
21/// `G::Sim` (см. core/src/sim.rs) — движок зовёт её только через
22/// `on_fixed_step`/`on_contacts`/`on_before_destroy`/`on_ai_tick`, не зная
23/// деталей конкретной игры. Конкретная игра (тип `G`, дефолт для
24/// `GameState`-алиасов — забота game-crate) не импортируется движком.
25pub struct EngineSim<G: GameDef> {
26    pub cfg: EngineConfig,
27    pub world: PhysicsWorld,
28    pub map: Option<GameMap>,
29    pub nav: Option<NavigationSystem>,
30    pub spatial: SpatialGrid,
31    pub rng: Rng,
32
33    time_step: f32,
34    accumulator: f32,
35
36    // события для меты, дренируются в take_events
37    pub events: Vec<CoreEvent>,
38
39    // очередь тел на удаление после обработки контактов
40    bodies_to_destroy: Vec<RigidBodyHandle>,
41
42    // содержал ли последний собранный body событийные блоки — для
43    // классификации канала WebRTC (meta reliable / state unreliable)
44    last_body_has_events: bool,
45
46    pub sim: G::Sim,
47}
48
49impl<G: GameDef> EngineSim<G> {
50    pub fn new(cfg: EngineConfig, game_cfg: &G::Config) -> Self {
51        let time_step = cfg.time_step;
52
53        let mut world = PhysicsWorld::new();
54
55        world.gravity = Vector::ZERO;
56        world.integration_parameters.dt = time_step;
57
58        let seed = cfg.seed;
59        let sim = G::Sim::new(game_cfg, &cfg);
60
61        Self {
62            world,
63            map: None,
64            nav: None,
65            spatial: SpatialGrid::new(600.0),
66            rng: Rng::new(seed),
67            time_step,
68            accumulator: 0.0,
69            events: Vec::new(),
70            bodies_to_destroy: Vec::new(),
71            last_body_has_events: false,
72            sim,
73            cfg,
74        }
75    }
76
77    // ***** карта ***** //
78
79    /// Создаёт карту из JSON (масштабирование внутри) и нав-граф ботов.
80    pub fn load_map(&mut self, json: &str) -> Result<(), String> {
81        let map_cfg: MapConfig = serde_json::from_str(json).map_err(|e| format!("bad map json: {e}"))?;
82
83        map_cfg.validate()?;
84
85        if let Some(mut old) = self.map.take() {
86            old.destroy(&mut self.world);
87        }
88
89        let map = GameMap::create(&mut self.world, &map_cfg, self.cfg.map_scale, &self.cfg.map_set_id);
90
91        // одноуровневая карта идёт прежним путём бит-в-бит: слоёный
92        // генератор для неё дал бы тот же граф, но лишним кодом на пути
93        self.nav = Some(if map.is_layered() {
94            NavigationSystem::generate_layered(map.levels(), map.step)
95        } else {
96            NavigationSystem::generate(&map.grid, &map.physics_static, map.step)
97        });
98
99        self.map = Some(map);
100
101        Ok(())
102    }
103
104    /// Информация о загруженной карте: setId, масштабированные респауны,
105    /// размеры мира (JSON) — 'null', если карта не загружена.
106    pub fn map_info_json(&self) -> String {
107        let Some(map) = &self.map else {
108            return "null".to_string();
109        };
110
111        let width = map.grid.first().map(|row| row.len()).unwrap_or(0) as f32 * map.step;
112        let height = map.grid.len() as f32 * map.step;
113
114        serde_json::json!({
115            "setId": map.set_id,
116            "step": map.step,
117            "width": width,
118            "height": height,
119            "respawns": map.respawns,
120            "levels": map.level_count(),
121        })
122        .to_string()
123    }
124
125    // ***** участники ***** //
126
127    pub fn spawn_actor(&mut self, game_id: u32, model_name: &str, team_id: u8, x: f32, y: f32, angle_deg: f32) -> Result<(), String> {
128        self.sim
129            .spawn_actor(&mut self.world, &mut self.events, game_id, model_name, team_id, x, y, angle_deg)
130    }
131
132    pub fn remove_actor(&mut self, game_id: u32) {
133        self.sim.remove_actor(&mut self.world, game_id);
134    }
135
136    pub fn reset_actor(&mut self, game_id: u32, team_id: u8, x: f32, y: f32, angle_deg: f32) {
137        self.sim.reset_actor(&mut self.world, game_id, team_id, x, y, angle_deg);
138    }
139
140    pub fn reset_all_vitals(&mut self) {
141        self.sim.reset_all_vitals(&mut self.events);
142    }
143
144    /// Явный уровень участника на слоёной карте (`respawns[i][3]`).
145    pub fn set_actor_level(&mut self, game_id: u32, level: u8) {
146        self.sim.set_actor_level(&mut self.world, game_id, level);
147    }
148
149    pub fn spawn_scripted_actor(&mut self, game_id: u32, model_name: &str, team_id: u8, x: f32, y: f32, angle_deg: f32) -> Result<(), String> {
150        self.sim.spawn_scripted_actor(
151            &mut self.world,
152            &mut self.rng,
153            &mut self.events,
154            game_id,
155            model_name,
156            team_id,
157            x,
158            y,
159            angle_deg,
160        )
161    }
162
163    pub fn remove_scripted_actor(&mut self, game_id: u32) {
164        self.sim.remove_scripted_actor(&mut self.world, game_id);
165    }
166
167    // ***** ввод ***** //
168
169    pub fn apply_input(&mut self, game_id: u32, seq: u32, action: &str, key_name: &str) {
170        self.sim.apply_input(game_id, seq, action, key_name);
171    }
172
173    /// Ввод указателем: мировая точка + биты состояния (см. `GameSim::apply_aim`).
174    pub fn apply_aim(&mut self, game_id: u32, seq: u32, x: f32, y: f32, flags: u32) {
175        self.sim.apply_aim(game_id, seq, x, y, flags);
176    }
177
178    pub fn last_input_seq(&self, game_id: u32) -> u32 {
179        self.sim.last_input_seq(game_id)
180    }
181
182    /// События за тик (kill/health/ammo/weapon/shake) одной JSON-строкой;
183    /// буфер очищается.
184    pub fn take_events_json(&mut self) -> String {
185        let events: Vec<CoreEvent> = self.events.drain(..).collect();
186
187        serde_json::to_string(&events).unwrap_or_else(|_| "[]".to_string())
188    }
189
190    // ***** запросы состояния ***** //
191
192    pub fn is_alive(&self, game_id: u32) -> bool {
193        self.sim.is_alive(game_id)
194    }
195
196    pub fn actor_position(&self, game_id: u32) -> Option<[f32; 2]> {
197        self.sim.actor_position(&self.world, game_id)
198    }
199
200    pub fn prediction_state(&self, game_id: u32) -> Option<([f32; PLAYER_STATE_LEN], bool)> {
201        self.sim.prediction_state(&self.world, game_id)
202    }
203
204    pub fn alive_players_flat(&self) -> Vec<f32> {
205        self.sim.alive_players_flat(&self.world)
206    }
207
208    /// Полный снапшот игроков (Game.getPlayersData), не дренируя
209    /// накопители событий — для первого кадра (FIRST_SHOT_DATA).
210    pub fn players_json(&self) -> String {
211        self.sim.players_json()
212    }
213
214    // ***** игровой тик ***** //
215
216    /// Обновляет физику фиксированными шагами (Game.updateData),
217    /// затем ИИ скриптовых участников (VIMP._onShotTick).
218    pub fn step(&mut self, dt: f32) {
219        self.accumulator = (self.accumulator + dt).min(MAX_ACCUMULATED_TIME);
220
221        while self.accumulator >= self.time_step {
222            self.step_fixed();
223            self.accumulator -= self.time_step;
224        }
225
226        self.sim.refresh_cached(&self.world);
227
228        let mut ctx = SimCtx {
229            world: &mut self.world,
230            cfg: &self.cfg,
231            map: &self.map,
232            nav: &self.nav,
233            spatial: &mut self.spatial,
234            rng: &mut self.rng,
235            events: &mut self.events,
236            bodies_to_destroy: &mut self.bodies_to_destroy,
237        };
238
239        self.sim.on_ai_tick(&mut ctx, dt);
240    }
241
242    /// Один фиксированный шаг физики.
243    fn step_fixed(&mut self) {
244        let mut ctx = SimCtx {
245            world: &mut self.world,
246            cfg: &self.cfg,
247            map: &self.map,
248            nav: &self.nav,
249            spatial: &mut self.spatial,
250            rng: &mut self.rng,
251            events: &mut self.events,
252            bodies_to_destroy: &mut self.bodies_to_destroy,
253        };
254
255        self.sim.on_fixed_step(&mut ctx, self.time_step);
256
257        // шаг физического мира со сбором начавшихся контактов
258        let (collision_send, collision_recv) = channel();
259        let (force_send, _force_recv) = channel();
260        let collector = ChannelEventCollector::new(collision_send, force_send);
261
262        self.world.step_with_events(&(), &collector);
263
264        let mut contacts = Vec::new();
265
266        while let Ok(event) = collision_recv.try_recv() {
267            if let CollisionEvent::Started(h1, h2, _) = event {
268                contacts.push((h1, h2));
269            }
270        }
271
272        let mut ctx = SimCtx {
273            world: &mut self.world,
274            cfg: &self.cfg,
275            map: &self.map,
276            nav: &self.nav,
277            spatial: &mut self.spatial,
278            rng: &mut self.rng,
279            events: &mut self.events,
280            bodies_to_destroy: &mut self.bodies_to_destroy,
281        };
282
283        self.sim.on_contacts(&mut ctx, &contacts);
284        self.destroy_queued_bodies();
285    }
286
287    /// Уничтожает тела из очереди (Game._destroyQueuedBodies).
288    fn destroy_queued_bodies(&mut self) {
289        if self.bodies_to_destroy.is_empty() {
290            return;
291        }
292
293        let handles: Vec<RigidBodyHandle> = self.bodies_to_destroy.drain(..).collect();
294
295        for handle in handles {
296            self.sim.on_before_destroy(&self.world, handle);
297            self.world.remove_body(handle);
298        }
299    }
300
301    // ***** снапшот ***** //
302
303    /// Собирает блоки тела снапшота, дренируя накопители событий.
304    /// Вызывается на каждый отправляемый кадр (throttle — забота JS).
305    pub fn build_snapshot_blocks(&mut self) -> Vec<(String, Block)> {
306        let (mut blocks, has_events) = self.sim.build_snapshot_blocks();
307
308        // динамические элементы карты — каждый отправляемый кадр.
309        // Хвост со скоростями пишется, только если схема набора карт его
310        // объявила (`optionalFrom`): движок не навязывает игре ширину строки
311        if let Some(map) = &self.map {
312            let with_velocities = self
313                .cfg
314                .snapshot
315                .keys
316                .get(&map.set_id)
317                .is_some_and(|schema| schema.optional_from.is_some());
318
319            blocks.push((
320                map.set_id.clone(),
321                Block::IndexedNoNull8(map.dynamic_map_data(&self.world, with_velocities)),
322            ));
323        }
324
325        self.last_body_has_events = has_events;
326
327        blocks
328    }
329
330    /// Содержал ли последний собранный body событийные блоки — для
331    /// классификации канала WebRTC (meta reliable / state unreliable).
332    pub fn body_has_events(&self) -> bool {
333        self.last_body_has_events
334    }
335
336    // ***** очистка ***** //
337
338    /// Удаляет всех игроков и снаряды, возвращает имена для очистки
339    /// полотна клиентов (Game.removePlayersAndShots).
340    pub fn remove_players_and_shots(&mut self) -> Vec<String> {
341        self.sim.remove_players_and_shots(&mut self.world)
342    }
343
344    /// Полная очистка игрового мира (Game.clear + сброс ботов).
345    pub fn clear(&mut self) {
346        // карта удаляет свои тела первой: повторное удаление уже
347        // удалённого тела в Rapier недопустимо
348        if let Some(mut map) = self.map.take() {
349            map.destroy(&mut self.world);
350        }
351
352        let handles: Vec<RigidBodyHandle> = self.world.rigid_bodies().map(|(handle, _)| handle).collect();
353
354        for handle in handles {
355            self.world.remove_body(handle);
356        }
357
358        self.sim.clear();
359        self.nav = None;
360        self.spatial.clear();
361        self.bodies_to_destroy.clear();
362
363        self.accumulator = 0.0;
364    }
365
366    // ***** отладка ***** //
367
368    /// Курированный дамп мира (тела, коллайдеры, карта, нав-граф,
369    /// spatial-сетка, rng, аккумулятор фикс-шага) — читаемая
370    /// альтернатива сырому `serialize_state` (см. `crate::debug`).
371    pub fn debug_json(&self) -> String {
372        crate::debug::engine_json(
373            &self.world,
374            &self.map,
375            &self.nav,
376            &self.spatial,
377            &self.rng,
378            self.accumulator,
379            self.time_step,
380        )
381        .to_string()
382    }
383
384    /// Самоописание игрового ядра: версия формата, версия движкового
385    /// крейта, опкоды `dispatch` (движковые + объявленные игрой).
386    pub fn abi_describe(&self) -> String {
387        crate::abi::describe_json(crate::abi::ENGINE_GAME_OPS, self.sim.dispatch_ops())
388    }
389
390    /// Опкод движка, иначе игровой опкод, иначе «не обработан» (пустой
391    /// вектор). Единая точка роста ABI: новая возможность приезжает
392    /// опкодом, а не новым символом — в dist уже опубликованной игры
393    /// символ не появится никогда.
394    pub fn dispatch(&mut self, op: &str, payload: &[u8]) -> Vec<u8> {
395        let out = match op {
396            crate::abi::OP_DEBUG_JSON => Some(self.debug_json().into_bytes()),
397            _ => self.sim.dispatch_op(op, payload),
398        };
399
400        crate::abi::dispatch_result(out)
401    }
402
403    // ***** handoff (Spike B / Этап 5.2) ***** //
404
405    /// Сериализует состояние симуляции для эстафетной передачи между
406    /// инстансами WASM. Накопители снапшота должны быть дренированы
407    /// (pack_body) перед вызовом.
408    pub fn serialize_state(&self) -> Result<Vec<u8>, String> {
409        let dump = EngineDump {
410            world: WorldDump {
411                gravity: [self.world.gravity.x, self.world.gravity.y],
412                integration_parameters: self.world.integration_parameters,
413                islands: self.world.islands.clone(),
414                broad_phase: self.world.broad_phase.clone(),
415                narrow_phase: self.world.narrow_phase.clone(),
416                bodies: self.world.bodies.clone(),
417                colliders: self.world.colliders.clone(),
418                impulse_joints: self.world.impulse_joints.clone(),
419                multibody_joints: self.world.multibody_joints.clone(),
420            },
421            map: &self.map,
422            rng: &self.rng,
423            accumulator: self.accumulator,
424            sim: self.sim.serialize(),
425        };
426
427        serde_json::to_vec(&dump).map_err(|e| e.to_string())
428    }
429
430    /// Восстанавливает состояние из дампа. Конфиг ядра должен совпадать
431    /// с конфигом инстанса, создавшего дамп.
432    pub fn deserialize_state(&mut self, data: &[u8]) -> Result<(), String> {
433        let dump: EngineDumpOwned = serde_json::from_slice(data).map_err(|e| e.to_string())?;
434
435        let mut world = PhysicsWorld::new();
436
437        world.gravity = Vector::new(dump.world.gravity[0], dump.world.gravity[1]);
438        world.integration_parameters = dump.world.integration_parameters;
439        world.islands = dump.world.islands;
440        world.broad_phase = dump.world.broad_phase;
441        world.narrow_phase = dump.world.narrow_phase;
442        world.bodies = dump.world.bodies;
443        world.colliders = dump.world.colliders;
444        world.impulse_joints = dump.world.impulse_joints;
445        world.multibody_joints = dump.world.multibody_joints;
446
447        self.world = world;
448        self.map = dump.map;
449        self.rng = dump.rng;
450        self.accumulator = dump.accumulator;
451        self.sim.deserialize(dump.sim)?;
452
453        self.bodies_to_destroy.clear();
454        self.events.clear();
455
456        // производные структуры не входят в дамп: нав-граф регенерируется
457        // из карты, пространственная сетка — из живых участников
458        self.nav = self.map.as_ref().map(|map| {
459            if map.is_layered() {
460                NavigationSystem::generate_layered(map.levels(), map.step)
461            } else {
462                NavigationSystem::generate(&map.grid, &map.physics_static, map.step)
463            }
464        });
465
466        self.sim.rebuild_spatial_grid(&self.world, &mut self.spatial);
467        self.sim.refresh_cached(&self.world);
468
469        Ok(())
470    }
471}
472
473#[derive(Serialize)]
474struct WorldDump {
475    gravity: [f32; 2],
476    integration_parameters: IntegrationParameters,
477    islands: IslandManager,
478    broad_phase: BroadPhaseBvh,
479    narrow_phase: NarrowPhase,
480    bodies: RigidBodySet,
481    colliders: ColliderSet,
482    impulse_joints: ImpulseJointSet,
483    multibody_joints: MultibodyJointSet,
484}
485
486#[derive(Deserialize)]
487struct WorldDumpOwned {
488    gravity: [f32; 2],
489    integration_parameters: IntegrationParameters,
490    islands: IslandManager,
491    broad_phase: BroadPhaseBvh,
492    narrow_phase: NarrowPhase,
493    bodies: RigidBodySet,
494    colliders: ColliderSet,
495    impulse_joints: ImpulseJointSet,
496    multibody_joints: MultibodyJointSet,
497}
498
499#[derive(Serialize)]
500struct EngineDump<'a> {
501    world: WorldDump,
502    map: &'a Option<GameMap>,
503    rng: &'a Rng,
504    accumulator: f32,
505    sim: serde_json::Value,
506}
507
508#[derive(Deserialize)]
509struct EngineDumpOwned {
510    world: WorldDumpOwned,
511    map: Option<GameMap>,
512    rng: Rng,
513    accumulator: f32,
514    sim: serde_json::Value,
515}
516
517/// Фикстурная вторая игра (Этап 7 плана отделения движка, PLAN.md):
518/// доказывает форму `GameDef`/`GameSim<G>` без физических тел (актёры —
519/// плоские данные, не rapier-тела — `spawn_actor` и т.п. игнорируют `world`,
520/// трейт этого не требует). Тот же принцип, что у клиентской фикстуры
521/// `TestClient` (`crate::client::game`): форма ABI важнее физики. Используется
522/// только тестами этого модуля, не влияет на `TanksGame`.
523#[cfg(test)]
524mod fixture {
525    use super::*;
526    use crate::config::FieldValue;
527    use serde::Deserialize;
528    use std::collections::{BTreeMap, BTreeSet};
529
530    #[derive(Deserialize)]
531    pub struct TestConfig {}
532
533    pub struct TestGame;
534
535    impl GameDef for TestGame {
536        type Config = TestConfig;
537        type Sim = TestSim;
538    }
539
540    #[derive(Clone, Copy)]
541    struct TestActor {
542        x: f32,
543        y: f32,
544        vx: f32,
545        vy: f32,
546        team: u8,
547        alive: bool,
548    }
549
550    pub struct TestSim {
551        actors: BTreeMap<u32, TestActor>,
552        scripted: BTreeSet<u32>,
553    }
554
555    impl GameSim<TestGame> for TestSim {
556        fn new(_cfg: &TestConfig, _engine_cfg: &EngineConfig) -> Self {
557            Self {
558                actors: BTreeMap::new(),
559                scripted: BTreeSet::new(),
560            }
561        }
562
563        fn spawn_actor(
564            &mut self,
565            _world: &mut PhysicsWorld,
566            _events: &mut Vec<CoreEvent>,
567            game_id: u32,
568            _model_name: &str,
569            team_id: u8,
570            x: f32,
571            y: f32,
572            _angle_deg: f32,
573        ) -> Result<(), String> {
574            self.actors.insert(
575                game_id,
576                TestActor { x, y, vx: 0.0, vy: 0.0, team: team_id, alive: true },
577            );
578
579            Ok(())
580        }
581
582        fn remove_actor(&mut self, _world: &mut PhysicsWorld, game_id: u32) {
583            self.actors.remove(&game_id);
584            self.scripted.remove(&game_id);
585        }
586
587        fn reset_actor(&mut self, _world: &mut PhysicsWorld, game_id: u32, team_id: u8, x: f32, y: f32, _angle_deg: f32) {
588            if let Some(actor) = self.actors.get_mut(&game_id) {
589                actor.x = x;
590                actor.y = y;
591                actor.team = team_id;
592                actor.alive = true;
593            }
594        }
595
596        fn reset_all_vitals(&mut self, _events: &mut Vec<CoreEvent>) {
597            for actor in self.actors.values_mut() {
598                actor.alive = true;
599            }
600        }
601
602        fn spawn_scripted_actor(
603            &mut self,
604            world: &mut PhysicsWorld,
605            _rng: &mut Rng,
606            events: &mut Vec<CoreEvent>,
607            game_id: u32,
608            model_name: &str,
609            team_id: u8,
610            x: f32,
611            y: f32,
612            angle_deg: f32,
613        ) -> Result<(), String> {
614            self.spawn_actor(world, events, game_id, model_name, team_id, x, y, angle_deg)?;
615            self.scripted.insert(game_id);
616
617            Ok(())
618        }
619
620        fn remove_scripted_actor(&mut self, world: &mut PhysicsWorld, game_id: u32) {
621            self.remove_actor(world, game_id);
622        }
623
624        fn apply_input(&mut self, game_id: u32, _seq: u32, action: &str, key_name: &str) {
625            let Some(actor) = self.actors.get_mut(&game_id) else {
626                return;
627            };
628
629            let magnitude = if action == "down" { 40.0 } else { 0.0 };
630
631            match key_name {
632                "forward" => actor.vy = -magnitude,
633                "back" => actor.vy = magnitude,
634                _ => {}
635            }
636        }
637
638        fn last_input_seq(&self, _game_id: u32) -> u32 {
639            0
640        }
641
642        fn is_alive(&self, game_id: u32) -> bool {
643            self.actors.get(&game_id).is_some_and(|a| a.alive)
644        }
645
646        fn actor_position(&self, _world: &PhysicsWorld, game_id: u32) -> Option<[f32; 2]> {
647            self.actors.get(&game_id).map(|a| [a.x, a.y])
648        }
649
650        fn prediction_state(&self, _world: &PhysicsWorld, game_id: u32) -> Option<([f32; PLAYER_STATE_LEN], bool)> {
651            self.actors
652                .get(&game_id)
653                .map(|a| ([a.x, a.y, 0.0, a.vx, a.vy, 0.0, 0.0, 0.0], false))
654        }
655
656        fn alive_players_flat(&self, _world: &PhysicsWorld) -> Vec<f32> {
657            self.actors
658                .iter()
659                .filter(|(_, a)| a.alive)
660                .flat_map(|(id, a)| [*id as f32, a.x, a.y])
661                .collect()
662        }
663
664        fn players_json(&self) -> String {
665            let rows: Vec<serde_json::Value> = self
666                .actors
667                .iter()
668                .map(|(id, a)| serde_json::json!({ "id": id, "x": a.x, "y": a.y, "team": a.team }))
669                .collect();
670
671            serde_json::to_string(&rows).unwrap()
672        }
673
674        fn on_fixed_step(&mut self, _ctx: &mut SimCtx, dt: f32) {
675            for actor in self.actors.values_mut() {
676                actor.x += actor.vx * dt;
677                actor.y += actor.vy * dt;
678            }
679        }
680
681        fn on_contacts(&mut self, _ctx: &mut SimCtx, _pairs: &[(ColliderHandle, ColliderHandle)]) {}
682
683        fn on_before_destroy(&mut self, _world: &PhysicsWorld, _handle: RigidBodyHandle) {}
684
685        fn on_ai_tick(&mut self, _ctx: &mut SimCtx, _dt: f32) {
686            // тривиальный ИИ фикстуры: скриптовый актёр всегда едет вправо
687            for &id in &self.scripted {
688                if let Some(actor) = self.actors.get_mut(&id) {
689                    actor.vx = 1.0;
690                }
691            }
692        }
693
694        fn refresh_cached(&mut self, _world: &PhysicsWorld) {}
695
696        fn build_snapshot_blocks(&mut self) -> (Vec<(String, Block)>, bool) {
697            let rows: Vec<(u8, Option<Vec<FieldValue>>)> = self
698                .actors
699                .iter()
700                .map(|(id, a)| (*id as u8, Some(vec![FieldValue::F32(a.x), FieldValue::F32(a.y)])))
701                .collect();
702
703            (vec![("actor".to_string(), Block::Indexed8(rows))], false)
704        }
705
706        fn remove_players_and_shots(&mut self, _world: &mut PhysicsWorld) -> Vec<String> {
707            let names: Vec<String> = self.actors.keys().map(|id| id.to_string()).collect();
708
709            self.actors.clear();
710            self.scripted.clear();
711
712            names
713        }
714
715        fn clear(&mut self) {
716            self.actors.clear();
717            self.scripted.clear();
718        }
719
720        fn serialize(&self) -> serde_json::Value {
721            let rows: Vec<serde_json::Value> = self
722                .actors
723                .iter()
724                .map(|(id, a)| serde_json::json!({ "id": id, "x": a.x, "y": a.y, "team": a.team }))
725                .collect();
726
727            serde_json::json!({ "actors": rows })
728        }
729
730        fn deserialize(&mut self, value: serde_json::Value) -> Result<(), String> {
731            let rows = value["actors"].as_array().ok_or("missing actors")?;
732
733            self.actors.clear();
734
735            for row in rows {
736                let id = row["id"].as_u64().ok_or("bad id")? as u32;
737                let x = row["x"].as_f64().ok_or("bad x")? as f32;
738                let y = row["y"].as_f64().ok_or("bad y")? as f32;
739                let team = row["team"].as_u64().ok_or("bad team")? as u8;
740
741                self.actors.insert(id, TestActor { x, y, vx: 0.0, vy: 0.0, team, alive: true });
742            }
743
744            Ok(())
745        }
746
747        fn rebuild_spatial_grid(&self, _world: &PhysicsWorld, _spatial: &mut SpatialGrid) {}
748    }
749}
750
751#[cfg(test)]
752mod tests {
753    use super::fixture::{TestConfig, TestGame};
754    use super::*;
755    use crate::snapshot::SnapshotPacker;
756
757    fn engine_config() -> EngineConfig {
758        serde_json::from_value(serde_json::json!({
759            "timeStep": 1.0 / 120.0,
760            "snapshot": {
761                "version": 3,
762                "port": 5,
763                "keys": {
764                    "actor": { "id": 1, "kind": "indexed8", "class": "hot", "fields": [
765                        { "name": "x", "ty": "f32", "interp": "lerp" },
766                        { "name": "y", "ty": "f32", "interp": "lerp" }
767                    ] }
768                }
769            },
770            "seed": 42
771        }))
772        .unwrap()
773    }
774
775    fn make_sim() -> EngineSim<TestGame> {
776        EngineSim::new(engine_config(), &TestConfig {})
777    }
778
779    #[test]
780    fn spawn_actor_and_fixed_step_moves_position() {
781        let mut sim = make_sim();
782
783        sim.spawn_actor(1, "m", 1, 0.0, 0.0, 0.0).unwrap();
784        sim.apply_input(1, 1, "down", "forward");
785
786        for _ in 0..60 {
787            sim.step(1.0 / 120.0);
788        }
789
790        let pos = sim.actor_position(1).unwrap();
791
792        assert!(pos[1] < 0.0); // forward = -vy
793    }
794
795    #[test]
796    fn remove_actor_clears_state() {
797        let mut sim = make_sim();
798
799        sim.spawn_actor(1, "m", 1, 5.0, 5.0, 0.0).unwrap();
800        assert!(sim.is_alive(1));
801
802        sim.remove_actor(1);
803        assert!(!sim.is_alive(1));
804        assert!(sim.actor_position(1).is_none());
805    }
806
807    #[test]
808    fn scripted_actor_runs_ai_tick() {
809        let mut sim = make_sim();
810
811        sim.spawn_scripted_actor(9, "m", 1, 0.0, 0.0, 0.0).unwrap();
812
813        // on_ai_tick — после фикс-шага в том же step(); vx применяется к
814        // позиции только следующим вызовом
815        sim.step(1.0 / 120.0);
816        sim.step(1.0 / 120.0);
817
818        let after = sim.actor_position(9).unwrap();
819
820        assert!(after[0] > 0.0); // фикстурный ИИ едет вправо (vx = 1.0)
821    }
822
823    #[test]
824    fn build_snapshot_blocks_packs_through_generic_schema() {
825        let mut sim = make_sim();
826
827        sim.spawn_actor(1, "m", 1, 3.0, 4.0, 0.0).unwrap();
828        sim.spawn_actor(2, "m", 1, 7.0, 8.0, 0.0).unwrap();
829
830        let blocks = sim.build_snapshot_blocks();
831        let mut packer = SnapshotPacker::new(engine_config().snapshot);
832
833        assert!(packer.pack_body(&blocks).is_ok());
834    }
835
836    #[test]
837    fn clear_removes_all_actors() {
838        let mut sim = make_sim();
839
840        sim.spawn_actor(1, "m", 1, 0.0, 0.0, 0.0).unwrap();
841        sim.spawn_scripted_actor(2, "m", 1, 0.0, 0.0, 0.0).unwrap();
842
843        sim.clear();
844
845        assert!(!sim.is_alive(1));
846        assert!(!sim.is_alive(2));
847        assert_eq!(sim.alive_players_flat().len(), 0);
848    }
849
850    // карта 2×2 с одной статичной стеной: хватает, чтобы в дампе появились
851    // тело, коллайдер и нав-граф
852    fn tiny_map_json() -> &'static str {
853        r#"{
854            "setId": "tiny",
855            "scale": 1,
856            "step": 10,
857            "map": [[1, 0], [0, 0]],
858            "physicsStatic": [1],
859            "respawns": { "team1": [[5, 5, 0]] }
860        }"#
861    }
862
863    // слоёная карта 4×4: плита уровня 1 в правой половине
864    fn layered_map_json() -> &'static str {
865        r#"{
866            "setId": "layered",
867            "scale": 1,
868            "step": 10,
869            "map": [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]],
870            "physicsStatic": [],
871            "levels": {
872                "1": {
873                    "map": [[0, 0, 9, 9], [0, 0, 9, 9], [0, 0, 9, 9], [0, 0, 9, 9]],
874                    "floor": [9]
875                }
876            }
877        }"#
878    }
879
880    #[test]
881    fn load_map_rejects_invalid_layers() {
882        let mut sim = make_sim();
883
884        // грид уровня 1 другой размерности — карта не должна загрузиться
885        let broken = r#"{
886            "step": 10,
887            "map": [[0, 0], [0, 0]],
888            "levels": { "1": { "map": [[0, 0]], "floor": [] } }
889        }"#;
890
891        assert!(sim.load_map(broken).is_err());
892        assert!(sim.map.is_none());
893        assert!(sim.nav.is_none());
894    }
895
896    #[test]
897    fn load_map_picks_layered_nav() {
898        let mut sim = make_sim();
899
900        sim.load_map(layered_map_json()).unwrap();
901
902        let map = sim.map.as_ref().unwrap();
903
904        assert!(map.is_layered());
905        assert_eq!(map.level_count(), 2);
906
907        let counts = sim.nav.as_ref().unwrap().nodes_by_level();
908
909        assert!(counts[0] > 0 && counts[1] > 0, "{counts:?}");
910    }
911
912    #[test]
913    fn debug_json_dumps_world_map_and_rng() {
914        let mut sim = make_sim();
915
916        sim.load_map(tiny_map_json()).unwrap();
917
918        let dump: serde_json::Value = serde_json::from_str(&sim.debug_json()).unwrap();
919
920        let bodies = dump["bodies"].as_array().unwrap();
921        let colliders = dump["colliders"].as_array().unwrap();
922
923        assert_eq!(bodies.len(), 1);
924        assert_eq!(colliders.len(), 1);
925        assert_eq!(bodies[0]["bodyType"], "Fixed");
926        assert_eq!(bodies[0]["translation"], serde_json::json!([5.0, 5.0]));
927        assert_eq!(colliders[0]["shape"], "cuboid");
928        assert_eq!(colliders[0]["halfExtents"], serde_json::json!([5.0, 5.0]));
929        assert_eq!(colliders[0]["isSensor"], false);
930        assert_eq!(colliders[0]["parent"], bodies[0]["handle"]);
931
932        assert_eq!(dump["map"]["setId"], "tiny");
933        assert_eq!(dump["map"]["staticBodies"], 1);
934        assert_eq!(dump["map"]["grid"]["rows"], 2);
935        assert_eq!(dump["map"]["respawns"]["team1"], 1);
936
937        assert!(dump["nav"]["nodes"].as_u64().unwrap() > 0);
938        assert_eq!(dump["spatial"]["cells"], 0);
939        assert_eq!(dump["rng"]["state"], "42");
940        assert_eq!(dump["step"]["accumulator"], 0.0);
941    }
942
943    #[test]
944    fn debug_json_is_null_for_map_and_nav_without_map() {
945        let dump: serde_json::Value = serde_json::from_str(&make_sim().debug_json()).unwrap();
946
947        assert!(dump["map"].is_null());
948        assert!(dump["nav"].is_null());
949        assert_eq!(dump["bodies"].as_array().unwrap().len(), 0);
950    }
951
952    #[test]
953    fn debug_json_is_stable_between_identical_runs() {
954        let mut a = make_sim();
955        let mut b = make_sim();
956
957        for sim in [&mut a, &mut b] {
958            sim.load_map(tiny_map_json()).unwrap();
959            sim.spawn_actor(1, "m", 1, 3.0, 4.0, 0.0).unwrap();
960            sim.step(1.0 / 120.0);
961        }
962
963        assert_eq!(a.debug_json(), b.debug_json());
964    }
965
966    #[test]
967    fn serialize_deserialize_round_trips_actors() {
968        let mut sim = make_sim();
969
970        sim.spawn_actor(1, "m", 3, 11.0, 22.0, 0.0).unwrap();
971
972        let dump = sim.sim.serialize();
973        let mut restored = make_sim();
974
975        restored.sim.deserialize(dump).unwrap();
976
977        assert_eq!(restored.actor_position(1), Some([11.0, 22.0]));
978    }
979}