use dreamwell_engine::physics::simulation::PhysicsWorld;
use dreamwell_gpu::dream_animations::{AnimationState, LocomotionController, LocomotionInput};
pub struct AvatarController {
pub position: [f32; 3],
pub facing_yaw: f32,
pub velocity: [f32; 3],
pub move_speed: f32,
pub locomotion_input: LocomotionInput,
pub animation_state: AnimationState,
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,
}
}
}
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()
}
}
pub fn update_from_input(
&mut self,
keys: &AvatarKeys,
camera_yaw: f32,
dt: f32,
_physics: &PhysicsWorld,
) {
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;
}
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;
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;
if !self.is_grounded {
self.velocity[1] -= 9.81 * dt;
} else if keys.jump {
self.velocity[1] = 5.0;
self.is_grounded = false;
}
self.position[0] += self.velocity[0] * dt;
self.position[1] += self.velocity[1] * dt;
self.position[2] += self.velocity[2] * dt;
if self.position[1] <= 0.0 {
self.position[1] = 0.0;
self.velocity[1] = 0.0;
self.is_grounded = true;
}
if len > 0.001 {
self.facing_yaw = world_z.atan2(world_x);
}
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()
};
}
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);
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();
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");
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);
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:?}"
);
}
}