Skip to main content

dreamwell_runtime/
game_state.rs

1// Game state — physics world, entity state, camera position, tick accumulation.
2// Consumes dreamwell-engine for game logic and CPU-side physics.
3
4use dreamwell_engine::physics::simulation::PhysicsWorld;
5use dreamwell_engine::TopologyLayer;
6
7/// Runtime game state with fixed-timestep tick accumulation and CPU physics.
8pub struct GameState {
9    pub tick: u64,
10    pub paused: bool,
11    pub active_layer: TopologyLayer,
12    pub player_position: glam::Vec3,
13    accumulated_time: f32,
14    tick_rate_ms: f32,
15    /// CPU-deterministic rigid body physics world.
16    pub physics: PhysicsWorld,
17}
18
19impl Default for GameState {
20    fn default() -> Self {
21        Self {
22            tick: 0,
23            paused: false,
24            active_layer: TopologyLayer::Area,
25            player_position: glam::Vec3::ZERO,
26            accumulated_time: 0.0,
27            tick_rate_ms: 50.0,
28            physics: PhysicsWorld::new(),
29        }
30    }
31}
32
33impl GameState {
34    /// Update game state for the current frame. Steps physics and tick accumulation.
35    pub fn update(&mut self, delta_time: f32) {
36        if self.paused {
37            return;
38        }
39
40        // Step physics at frame rate (semi-implicit Euler is stable for game dt)
41        self.physics.step(delta_time);
42
43        self.accumulated_time += delta_time * 1000.0;
44        while self.accumulated_time >= self.tick_rate_ms {
45            self.accumulated_time -= self.tick_rate_ms;
46            self.tick += 1;
47        }
48    }
49
50    pub fn toggle_pause(&mut self) {
51        self.paused = !self.paused;
52    }
53
54    /// Current player position.
55    pub fn player_position(&self) -> glam::Vec3 {
56        self.player_position
57    }
58
59    /// Current topology layer.
60    pub fn active_layer(&self) -> TopologyLayer {
61        self.active_layer
62    }
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68
69    #[test]
70    fn default_state() {
71        let gs = GameState::default();
72        assert_eq!(gs.tick, 0);
73        assert!(!gs.paused);
74        assert_eq!(gs.active_layer, TopologyLayer::Area);
75    }
76
77    #[test]
78    fn tick_accumulation() {
79        let mut gs = GameState::default();
80        // 50ms tick rate, 100ms delta → 2 ticks
81        gs.update(0.1);
82        assert_eq!(gs.tick, 2);
83    }
84
85    #[test]
86    fn pause_stops_ticks() {
87        let mut gs = GameState::default();
88        gs.paused = true;
89        gs.update(1.0);
90        assert_eq!(gs.tick, 0);
91    }
92
93    #[test]
94    fn physics_steps_with_update() {
95        use dreamwell_engine::physics::simulation::{CollisionShape, RigidBody};
96        let mut gs = GameState::default();
97        let id = gs
98            .physics
99            .add_body(RigidBody::dynamic(1.0, CollisionShape::Sphere { radius: 0.5 }).with_position([0.0, 10.0, 0.0]));
100        gs.update(1.0 / 60.0);
101        let body = gs.physics.body(id).unwrap();
102        assert!(body.position[1] < 10.0, "Sphere should fall under gravity");
103    }
104
105    #[test]
106    fn physics_paused_no_step() {
107        use dreamwell_engine::physics::simulation::{CollisionShape, RigidBody};
108        let mut gs = GameState::default();
109        let id = gs
110            .physics
111            .add_body(RigidBody::dynamic(1.0, CollisionShape::Sphere { radius: 0.5 }).with_position([0.0, 10.0, 0.0]));
112        gs.paused = true;
113        gs.update(1.0);
114        let body = gs.physics.body(id).unwrap();
115        assert_eq!(body.position[1], 10.0, "Sphere should not move when paused");
116    }
117}