use dreamwell_engine::avatar::{AvatarSpec, FootstepConfig, PresentationMode};
use dreamwell_engine::dream_file::DreamSceneV1;
pub struct FootstepPlatform {
pub position: [f32; 3],
pub age: f32,
pub fade_time: f32,
}
impl FootstepPlatform {
pub fn opacity(&self) -> f32 {
(1.0 - self.age / self.fade_time).max(0.0)
}
pub fn is_expired(&self) -> bool {
self.age >= self.fade_time
}
}
pub struct AvatarDemoState {
pub avatar_spec: AvatarSpec,
pub presentation_mode: PresentationMode,
pub scene: Option<DreamSceneV1>,
pub footstep_config: FootstepConfig,
pub footsteps: Vec<FootstepPlatform>,
pub max_footsteps: usize,
pub elapsed: f32,
pub active: bool,
}
impl Default for AvatarDemoState {
fn default() -> Self {
Self {
avatar_spec: AvatarSpec::default(),
presentation_mode: PresentationMode::Particle,
scene: None,
footstep_config: FootstepConfig::default(),
footsteps: Vec::with_capacity(32),
max_footsteps: 32,
elapsed: 0.0,
active: false,
}
}
}
impl AvatarDemoState {
pub fn from_scene(scene: DreamSceneV1) -> Self {
let avatar_spec = scene.asset_refs.iter()
.find(|a| a.kind == "fbx")
.map(|a| AvatarSpec {
id: "demo_avatar".into(),
name: "Walking Avatar".into(),
fbx_path: a.path.clone(),
..Default::default()
})
.unwrap_or_default();
Self {
avatar_spec,
scene: Some(scene),
active: true,
..Default::default()
}
}
pub fn spawn_footstep(&mut self, position: [f32; 3]) {
if self.footsteps.len() >= self.max_footsteps {
self.footsteps.remove(0);
}
self.footsteps.push(FootstepPlatform {
position,
age: 0.0,
fade_time: self.footstep_config.fade_time,
});
}
pub fn update_footsteps(&mut self, dt: f32) {
for step in &mut self.footsteps {
step.age += dt;
}
self.footsteps.retain(|s| !s.is_expired());
}
pub fn cycle_presentation(&mut self) {
self.presentation_mode = self.presentation_mode.next();
}
pub fn footstep_dreamlets_into(&self, out: &mut Vec<([f32; 3], [f32; 4], f32)>) {
let cfg = &self.footstep_config;
let n = cfg.dreamlets_per_step as usize;
let hw = cfg.step_width * 0.5;
let hl = cfg.step_length * 0.5;
for step in &self.footsteps {
let opacity = step.opacity();
if opacity <= 0.0 { continue; }
let color = [0.5, 0.55, 0.6, opacity * 0.8];
let cols = (n as f32).sqrt().ceil() as usize;
let rows = (n + cols - 1) / cols;
for r in 0..rows {
for c in 0..cols {
if r * cols + c >= n { break; }
let fx = (c as f32 / cols.max(1) as f32) * 2.0 - 1.0;
let fz = (r as f32 / rows.max(1) as f32) * 2.0 - 1.0;
let pos = [
step.position[0] + fx * hw,
step.position[1] + cfg.step_height * 0.5,
step.position[2] + fz * hl,
];
out.push((pos, color, cfg.step_height));
}
}
}
}
pub fn render_ui(&mut self, ctx: &egui::Context) -> bool {
let mut close_requested = false;
egui::Window::new("Avatar Demo")
.collapsible(true)
.resizable(false)
.default_pos([10.0, 400.0])
.default_width(280.0)
.show(ctx, |ui| {
ui.heading("Presentation");
ui.horizontal(|ui| {
ui.label("Mode:");
if ui.button(self.presentation_mode.label()).clicked() {
self.cycle_presentation();
}
});
ui.separator();
ui.label(format!("FBX: {}", self.avatar_spec.fbx_path));
ui.label(format!("Active footsteps: {}", self.footsteps.len()));
ui.label(format!("Elapsed: {:.1}s", self.elapsed));
ui.separator();
ui.label("Controls:");
ui.label(" Tab — cycle presentation");
ui.label(" WASD — move avatar");
ui.label(" Right-click drag — orbit camera");
ui.label(" Scroll — zoom");
ui.separator();
if ui.button("Close Demo").clicked() {
close_requested = true;
}
});
close_requested
}
}
pub fn avatar_spec_from_scene(scene: &DreamSceneV1) -> AvatarSpec {
scene.asset_refs.iter()
.find(|a| a.kind == "fbx")
.map(|a| AvatarSpec {
id: scene.scene_id.clone(),
name: scene.name.clone(),
fbx_path: a.path.clone(),
..Default::default()
})
.unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn footstep_lifecycle() {
let mut state = AvatarDemoState::default();
state.footstep_config.fade_time = 1.0;
state.spawn_footstep([0.0, 0.0, 0.0]);
assert_eq!(state.footsteps.len(), 1);
assert!((state.footsteps[0].opacity() - 1.0).abs() < 0.001);
state.update_footsteps(0.5);
assert!((state.footsteps[0].opacity() - 0.5).abs() < 0.001);
state.update_footsteps(0.6);
assert_eq!(state.footsteps.len(), 0, "expired footstep should be removed");
}
#[test]
fn presentation_cycle_all() {
let mut state = AvatarDemoState::default();
assert_eq!(state.presentation_mode, PresentationMode::Particle);
state.cycle_presentation();
assert_eq!(state.presentation_mode, PresentationMode::PointCloud);
state.cycle_presentation();
assert_eq!(state.presentation_mode, PresentationMode::Skin);
state.cycle_presentation();
assert_eq!(state.presentation_mode, PresentationMode::Wireframe);
state.cycle_presentation();
assert_eq!(state.presentation_mode, PresentationMode::Voxel);
state.cycle_presentation();
assert_eq!(state.presentation_mode, PresentationMode::Particle);
}
#[test]
fn footstep_dreamlets_bounded() {
let mut state = AvatarDemoState::default();
state.footstep_config.dreamlets_per_step = 16;
state.spawn_footstep([0.0, 0.0, 0.0]);
state.spawn_footstep([1.0, 0.0, 0.0]);
let mut buf = Vec::new();
state.footstep_dreamlets_into(&mut buf);
assert_eq!(buf.len(), 32); }
#[test]
fn max_footsteps_eviction() {
let mut state = AvatarDemoState::default();
state.max_footsteps = 4;
for i in 0..6 {
state.spawn_footstep([i as f32, 0.0, 0.0]);
}
assert_eq!(state.footsteps.len(), 4);
assert!((state.footsteps[0].position[0] - 2.0).abs() < 0.001);
}
}