dreamwell-matter 1.0.0

DreamMatter benchmark — GPU physics materialization demo and profiler
Documentation
// Avatar controller — WASD movement, animation evaluation, dreamlet output.
//
// Manages position, facing, velocity, and locomotion state for the
// player-controlled avatar in the benchmark demo.

use dreamwell_engine::physics::simulation::PhysicsWorld;
use dreamwell_gpu::dream_animations::{AnimationState, LocomotionController, LocomotionInput};

/// Player avatar controller.
pub struct AvatarController {
    /// World-space position.
    pub position: [f32; 3],
    /// Facing direction (yaw in radians).
    pub facing_yaw: f32,
    /// Current velocity (m/s).
    pub velocity: [f32; 3],
    /// Movement speed (m/s).
    pub move_speed: f32,
    /// Current locomotion input (derived from keyboard state).
    pub locomotion_input: LocomotionInput,
    /// Animation state machine.
    pub animation_state: AnimationState,
    /// Whether the avatar is on the ground.
    pub is_grounded: bool,
}

impl Default for AvatarController {
    fn default() -> Self {
        Self {
            position: [0.0, 0.0, 0.0],
            facing_yaw: 0.0,
            velocity: [0.0; 3],
            move_speed: 5.0,
            locomotion_input: LocomotionInput::default(),
            animation_state: AnimationState::default(),
            is_grounded: true,
        }
    }
}

/// Key state from the input system.
pub struct AvatarKeys {
    pub forward: bool,
    pub back: bool,
    pub left: bool,
    pub right: bool,
    pub jump: bool,
    pub crouch: bool,
    pub sprint: bool,
}

impl AvatarController {
    pub fn new(position: [f32; 3]) -> Self {
        Self {
            position,
            ..Default::default()
        }
    }

    /// Update avatar from keyboard input and camera yaw.
    ///
    /// `keys`: current keyboard state.
    /// `camera_yaw`: camera yaw in radians (movement is relative to camera).
    /// `dt`: time step in seconds.
    /// `_physics`: physics world for collision queries (placeholder).
    pub fn update_from_input(
        &mut self,
        keys: &AvatarKeys,
        camera_yaw: f32,
        dt: f32,
        _physics: &PhysicsWorld,
    ) {
        // Compute movement direction from WASD relative to camera.
        let mut dir_x = 0.0f32;
        let mut dir_z = 0.0f32;
        if keys.forward { dir_z -= 1.0; }
        if keys.back { dir_z += 1.0; }
        if keys.left { dir_x -= 1.0; }
        if keys.right { dir_x += 1.0; }

        let len = (dir_x * dir_x + dir_z * dir_z).sqrt();
        if len > 0.001 {
            dir_x /= len;
            dir_z /= len;
        }

        // Rotate movement direction by camera yaw.
        let cos_yaw = camera_yaw.cos();
        let sin_yaw = camera_yaw.sin();
        let world_x = dir_x * cos_yaw - dir_z * sin_yaw;
        let world_z = dir_x * sin_yaw + dir_z * cos_yaw;

        // Update velocity.
        let speed = if keys.sprint { self.move_speed * 2.0 } else { self.move_speed };
        self.velocity[0] = world_x * speed;
        self.velocity[2] = world_z * speed;

        // Simple gravity.
        if !self.is_grounded {
            self.velocity[1] -= 9.81 * dt;
        } else if keys.jump {
            self.velocity[1] = 5.0;
            self.is_grounded = false;
        }

        // Apply velocity.
        self.position[0] += self.velocity[0] * dt;
        self.position[1] += self.velocity[1] * dt;
        self.position[2] += self.velocity[2] * dt;

        // Ground plane at y=0.
        if self.position[1] <= 0.0 {
            self.position[1] = 0.0;
            self.velocity[1] = 0.0;
            self.is_grounded = true;
        }

        // Update facing direction.
        if len > 0.001 {
            self.facing_yaw = world_z.atan2(world_x);
        }

        // Update locomotion input.
        let move_speed_norm = if keys.sprint { 1.0 } else if len > 0.001 { 0.5 } else { 0.0 };
        self.locomotion_input = LocomotionInput {
            move_direction: [world_x, world_z],
            move_speed: move_speed_norm,
            is_jumping: keys.jump,
            is_crouching: keys.crouch,
            is_grounded: self.is_grounded,
            is_sprinting: keys.sprint,
            velocity_y: self.velocity[1],
            ..Default::default()
        };
    }

    /// Evaluate animation state using the locomotion controller.
    /// Returns the selected clip index (if any).
    pub fn evaluate_animation(&mut self, locomotion: &LocomotionController) -> Option<usize> {
        locomotion.evaluate(&self.locomotion_input, &mut self.animation_state);
        self.animation_state.current_clip
    }
}

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

    #[test]
    fn wasd_position_change() {
        let mut avatar = AvatarController::new([0.0, 0.0, 0.0]);
        let world = PhysicsWorld::new();
        let keys = AvatarKeys {
            forward: true,
            back: false,
            left: false,
            right: false,
            jump: false,
            crouch: false,
            sprint: false,
        };
        avatar.update_from_input(&keys, 0.0, 0.1, &world);
        // Moving forward with camera yaw=0 should change Z position.
        assert!(
            avatar.position[2].abs() > 0.001 || avatar.position[0].abs() > 0.001,
            "position should change: {:?}",
            avatar.position
        );
    }

    #[test]
    fn animation_clip_selection() {
        let mut avatar = AvatarController::new([0.0, 0.0, 0.0]);
        let locomotion = LocomotionController::new(0, 1, 2, 3, 4);
        let world = PhysicsWorld::new();

        // Idle.
        let keys = AvatarKeys {
            forward: false, back: false, left: false, right: false,
            jump: false, crouch: false, sprint: false,
        };
        avatar.update_from_input(&keys, 0.0, 0.016, &world);
        let clip = avatar.evaluate_animation(&locomotion);
        assert_eq!(clip, Some(0), "idle should select clip 0");

        // Walk forward.
        let keys = AvatarKeys {
            forward: true, back: false, left: false, right: false,
            jump: false, crouch: false, sprint: false,
        };
        avatar.update_from_input(&keys, 0.0, 0.016, &world);
        avatar.evaluate_animation(&locomotion);
        // Should transition to walk (clip 1) via blend.
        let current = avatar.animation_state.current_clip;
        let next = avatar.animation_state.next_clip;
        assert!(
            current == Some(1) || next == Some(1),
            "walk should target clip 1, got current={current:?} next={next:?}"
        );
    }
}