dreamwell-runtime 1.0.0

Dreamwell Runtime — cross-platform GPU-accelerated game client
Documentation
// Game state — physics world, entity state, camera position, tick accumulation.
// Consumes dreamwell-engine for game logic and CPU-side physics.

use dreamwell_engine::physics::simulation::PhysicsWorld;
use dreamwell_engine::TopologyLayer;

/// Runtime game state with fixed-timestep tick accumulation and CPU physics.
pub struct GameState {
    pub tick: u64,
    pub paused: bool,
    pub active_layer: TopologyLayer,
    pub player_position: glam::Vec3,
    accumulated_time: f32,
    tick_rate_ms: f32,
    /// CPU-deterministic rigid body physics world.
    pub physics: PhysicsWorld,
}

impl Default for GameState {
    fn default() -> Self {
        Self {
            tick: 0,
            paused: false,
            active_layer: TopologyLayer::Area,
            player_position: glam::Vec3::ZERO,
            accumulated_time: 0.0,
            tick_rate_ms: 50.0,
            physics: PhysicsWorld::new(),
        }
    }
}

impl GameState {
    /// Update game state for the current frame. Steps physics and tick accumulation.
    pub fn update(&mut self, delta_time: f32) {
        if self.paused {
            return;
        }

        // Step physics at frame rate (semi-implicit Euler is stable for game dt)
        self.physics.step(delta_time);

        self.accumulated_time += delta_time * 1000.0;
        while self.accumulated_time >= self.tick_rate_ms {
            self.accumulated_time -= self.tick_rate_ms;
            self.tick += 1;
        }
    }

    pub fn toggle_pause(&mut self) {
        self.paused = !self.paused;
    }

    /// Current player position.
    pub fn player_position(&self) -> glam::Vec3 {
        self.player_position
    }

    /// Current topology layer.
    pub fn active_layer(&self) -> TopologyLayer {
        self.active_layer
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn default_state() {
        let gs = GameState::default();
        assert_eq!(gs.tick, 0);
        assert!(!gs.paused);
        assert_eq!(gs.active_layer, TopologyLayer::Area);
    }

    #[test]
    fn tick_accumulation() {
        let mut gs = GameState::default();
        // 50ms tick rate, 100ms delta → 2 ticks
        gs.update(0.1);
        assert_eq!(gs.tick, 2);
    }

    #[test]
    fn pause_stops_ticks() {
        let mut gs = GameState::default();
        gs.paused = true;
        gs.update(1.0);
        assert_eq!(gs.tick, 0);
    }

    #[test]
    fn physics_steps_with_update() {
        use dreamwell_engine::physics::simulation::{CollisionShape, RigidBody};
        let mut gs = GameState::default();
        let id = gs
            .physics
            .add_body(RigidBody::dynamic(1.0, CollisionShape::Sphere { radius: 0.5 }).with_position([0.0, 10.0, 0.0]));
        gs.update(1.0 / 60.0);
        let body = gs.physics.body(id).unwrap();
        assert!(body.position[1] < 10.0, "Sphere should fall under gravity");
    }

    #[test]
    fn physics_paused_no_step() {
        use dreamwell_engine::physics::simulation::{CollisionShape, RigidBody};
        let mut gs = GameState::default();
        let id = gs
            .physics
            .add_body(RigidBody::dynamic(1.0, CollisionShape::Sphere { radius: 0.5 }).with_position([0.0, 10.0, 0.0]));
        gs.paused = true;
        gs.update(1.0);
        let body = gs.physics.body(id).unwrap();
        assert_eq!(body.position[1], 10.0, "Sphere should not move when paused");
    }
}