notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

commit db11b1a8d364adcb6a5e3c314cf5be62c12d7368
parent e296d6ca0614fee017f0fcf8e068ae961c2cb8c5
Author: William Casarin <jb55@jb55.com>
Date:   Fri, 20 Feb 2026 15:59:20 -0800

nostrverse: add third-person avatar with water bottle placeholder

Users now appear as water bottle models in the scene. The self-user
is controlled via WASD with a third-person camera orbiting behind.
Avatar positions sync between the camera controller and scene graph.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Diffstat:
Mcrates/notedeck_nostrverse/src/lib.rs | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Mcrates/notedeck_nostrverse/src/room_state.rs | 6++++++
2 files changed, 62 insertions(+), 4 deletions(-)

diff --git a/crates/notedeck_nostrverse/src/lib.rs b/crates/notedeck_nostrverse/src/lib.rs @@ -193,16 +193,37 @@ impl NostrverseApp { .with_agent(true), ]; + // Assign the bottle model as avatar placeholder for all users + if let Some(model) = bottle { + for user in &mut self.state.users { + user.model_handle = Some(model); + } + } + + // Switch to third-person camera mode centered on the self-user + if let Some(renderer) = &self.renderer { + let self_pos = self + .state + .users + .iter() + .find(|u| u.is_self) + .map(|u| u.position) + .unwrap_or(Vec3::ZERO); + let mut r = renderer.renderer.lock().unwrap(); + r.set_third_person_mode(self_pos); + } + self.initialized = true; } - /// Sync room objects to the renderbud scene + /// Sync room objects and user avatars to the renderbud scene fn sync_scene(&mut self) { let Some(renderer) = &self.renderer else { return; }; let mut r = renderer.renderer.lock().unwrap(); + // Sync room objects for obj in &mut self.state.objects { let transform = Transform { translation: obj.position, @@ -211,14 +232,45 @@ impl NostrverseApp { }; if let Some(scene_id) = obj.scene_object_id { - // Update existing object's transform r.update_object_transform(scene_id, transform); } else if let Some(model) = obj.model_handle { - // Place new object in scene let scene_id = r.place_object(model, transform); obj.scene_object_id = Some(scene_id); } - // If model_handle is None, model hasn't loaded yet (Phase 3) + } + + // Read avatar position/yaw from the third-person controller + let avatar_pos = r.avatar_position(); + let avatar_yaw = r.avatar_yaw(); + + // Update self-user's position from the controller + if let Some(pos) = avatar_pos { + if let Some(self_user) = self.state.users.iter_mut().find(|u| u.is_self) { + self_user.position = pos; + } + } + + // Sync all user avatars to the scene + let bottle_scale = 5.0_f32; + for user in &mut self.state.users { + let yaw = if user.is_self { + avatar_yaw.unwrap_or(0.0) + } else { + 0.0 + }; + + let transform = Transform { + translation: user.position, + rotation: glam::Quat::from_rotation_y(yaw), + scale: Vec3::splat(bottle_scale), + }; + + if let Some(scene_id) = user.scene_object_id { + r.update_object_transform(scene_id, transform); + } else if let Some(model) = user.model_handle { + let scene_id = r.place_object(model, transform); + user.scene_object_id = Some(scene_id); + } } } diff --git a/crates/notedeck_nostrverse/src/room_state.rs b/crates/notedeck_nostrverse/src/room_state.rs @@ -128,6 +128,10 @@ pub struct RoomUser { pub is_self: bool, /// Whether this user is an AI agent pub is_agent: bool, + /// Runtime: renderbud scene object handle for avatar + pub scene_object_id: Option<ObjectId>, + /// Runtime: loaded model handle for avatar + pub model_handle: Option<Model>, } impl RoomUser { @@ -138,6 +142,8 @@ impl RoomUser { position, is_self: false, is_agent: false, + scene_object_id: None, + model_handle: None, } }