feat: prop moving.

This commit is contained in:
Evan Carroll 2026-01-23 17:11:12 -06:00
parent a2841c413d
commit 6e637a29cd
7 changed files with 688 additions and 56 deletions

View file

@ -898,6 +898,12 @@ pub struct LooseProp {
pub prop_name: String,
/// Asset path for rendering (JOINed from source prop).
pub prop_asset_path: String,
/// If true, only moderators can move/scale/pickup this prop.
#[serde(default)]
pub is_locked: bool,
/// User ID of the moderator who locked this prop.
#[serde(default)]
pub locked_by: Option<Uuid>,
}
/// A server-wide prop (global library).

View file

@ -51,7 +51,9 @@ pub async fn list_channel_loose_props<'e>(
lp.expires_at,
lp.created_at,
COALESCE(sp.name, rp.name) as prop_name,
COALESCE(sp.asset_path, rp.asset_path) as prop_asset_path
COALESCE(sp.asset_path, rp.asset_path) as prop_asset_path,
lp.is_locked,
lp.locked_by
FROM scene.loose_props lp
LEFT JOIN server.props sp ON lp.server_prop_id = sp.id
LEFT JOIN realm.props rp ON lp.realm_prop_id = rp.id
@ -236,6 +238,8 @@ pub async fn drop_prop_to_canvas<'e>(
created_at,
prop_name,
prop_asset_path,
is_locked: false,
locked_by: None,
})
}
_ => {
@ -364,7 +368,9 @@ pub async fn update_loose_prop_scale<'e>(
scale,
dropped_by,
expires_at,
created_at
created_at,
is_locked,
locked_by
)
SELECT
u.id,
@ -378,7 +384,9 @@ pub async fn update_loose_prop_scale<'e>(
u.expires_at,
u.created_at,
COALESCE(sp.name, rp.name) as prop_name,
COALESCE(sp.asset_path, rp.asset_path) as prop_asset_path
COALESCE(sp.asset_path, rp.asset_path) as prop_asset_path,
u.is_locked,
u.locked_by
FROM updated u
LEFT JOIN server.props sp ON u.server_prop_id = sp.id
LEFT JOIN realm.props rp ON u.realm_prop_id = rp.id
@ -412,7 +420,9 @@ pub async fn get_loose_prop_by_id<'e>(
lp.expires_at,
lp.created_at,
COALESCE(sp.name, rp.name) as prop_name,
COALESCE(sp.asset_path, rp.asset_path) as prop_asset_path
COALESCE(sp.asset_path, rp.asset_path) as prop_asset_path,
lp.is_locked,
lp.locked_by
FROM scene.loose_props lp
LEFT JOIN server.props sp ON lp.server_prop_id = sp.id
LEFT JOIN realm.props rp ON lp.realm_prop_id = rp.id
@ -427,6 +437,174 @@ pub async fn get_loose_prop_by_id<'e>(
Ok(prop)
}
/// Move a loose prop to a new position.
pub async fn move_loose_prop<'e>(
executor: impl PgExecutor<'e>,
loose_prop_id: Uuid,
x: f64,
y: f64,
) -> Result<LooseProp, AppError> {
let prop = sqlx::query_as::<_, LooseProp>(
r#"
WITH updated AS (
UPDATE scene.loose_props
SET position = public.make_virtual_point($2::real, $3::real)
WHERE id = $1
AND (expires_at IS NULL OR expires_at > now())
RETURNING
id,
instance_id as channel_id,
server_prop_id,
realm_prop_id,
ST_X(position) as position_x,
ST_Y(position) as position_y,
scale,
dropped_by,
expires_at,
created_at,
is_locked,
locked_by
)
SELECT
u.id,
u.channel_id,
u.server_prop_id,
u.realm_prop_id,
u.position_x,
u.position_y,
u.scale,
u.dropped_by,
u.expires_at,
u.created_at,
COALESCE(sp.name, rp.name) as prop_name,
COALESCE(sp.asset_path, rp.asset_path) as prop_asset_path,
u.is_locked,
u.locked_by
FROM updated u
LEFT JOIN server.props sp ON u.server_prop_id = sp.id
LEFT JOIN realm.props rp ON u.realm_prop_id = rp.id
"#,
)
.bind(loose_prop_id)
.bind(x as f32)
.bind(y as f32)
.fetch_optional(executor)
.await?
.ok_or_else(|| AppError::NotFound("Loose prop not found or has expired".to_string()))?;
Ok(prop)
}
/// Lock a loose prop (moderator only).
pub async fn lock_loose_prop<'e>(
executor: impl PgExecutor<'e>,
loose_prop_id: Uuid,
locked_by: Uuid,
) -> Result<LooseProp, AppError> {
let prop = sqlx::query_as::<_, LooseProp>(
r#"
WITH updated AS (
UPDATE scene.loose_props
SET is_locked = true, locked_by = $2
WHERE id = $1
AND (expires_at IS NULL OR expires_at > now())
RETURNING
id,
instance_id as channel_id,
server_prop_id,
realm_prop_id,
ST_X(position) as position_x,
ST_Y(position) as position_y,
scale,
dropped_by,
expires_at,
created_at,
is_locked,
locked_by
)
SELECT
u.id,
u.channel_id,
u.server_prop_id,
u.realm_prop_id,
u.position_x,
u.position_y,
u.scale,
u.dropped_by,
u.expires_at,
u.created_at,
COALESCE(sp.name, rp.name) as prop_name,
COALESCE(sp.asset_path, rp.asset_path) as prop_asset_path,
u.is_locked,
u.locked_by
FROM updated u
LEFT JOIN server.props sp ON u.server_prop_id = sp.id
LEFT JOIN realm.props rp ON u.realm_prop_id = rp.id
"#,
)
.bind(loose_prop_id)
.bind(locked_by)
.fetch_optional(executor)
.await?
.ok_or_else(|| AppError::NotFound("Loose prop not found or has expired".to_string()))?;
Ok(prop)
}
/// Unlock a loose prop (moderator only).
pub async fn unlock_loose_prop<'e>(
executor: impl PgExecutor<'e>,
loose_prop_id: Uuid,
) -> Result<LooseProp, AppError> {
let prop = sqlx::query_as::<_, LooseProp>(
r#"
WITH updated AS (
UPDATE scene.loose_props
SET is_locked = false, locked_by = NULL
WHERE id = $1
AND (expires_at IS NULL OR expires_at > now())
RETURNING
id,
instance_id as channel_id,
server_prop_id,
realm_prop_id,
ST_X(position) as position_x,
ST_Y(position) as position_y,
scale,
dropped_by,
expires_at,
created_at,
is_locked,
locked_by
)
SELECT
u.id,
u.channel_id,
u.server_prop_id,
u.realm_prop_id,
u.position_x,
u.position_y,
u.scale,
u.dropped_by,
u.expires_at,
u.created_at,
COALESCE(sp.name, rp.name) as prop_name,
COALESCE(sp.asset_path, rp.asset_path) as prop_asset_path,
u.is_locked,
u.locked_by
FROM updated u
LEFT JOIN server.props sp ON u.server_prop_id = sp.id
LEFT JOIN realm.props rp ON u.realm_prop_id = rp.id
"#,
)
.bind(loose_prop_id)
.fetch_optional(executor)
.await?
.ok_or_else(|| AppError::NotFound("Loose prop not found or has expired".to_string()))?;
Ok(prop)
}
/// Delete expired loose props.
///
/// Returns the number of props deleted.

View file

@ -112,6 +112,28 @@ pub enum ClientMessage {
/// New scale factor (0.1 - 10.0).
scale: f32,
},
/// Move a loose prop to a new position.
MoveProp {
/// The loose prop ID to move.
loose_prop_id: Uuid,
/// New X coordinate in scene space.
x: f64,
/// New Y coordinate in scene space.
y: f64,
},
/// Lock a loose prop (moderator only).
LockProp {
/// The loose prop ID to lock.
loose_prop_id: Uuid,
},
/// Unlock a loose prop (moderator only).
UnlockProp {
/// The loose prop ID to unlock.
loose_prop_id: Uuid,
},
}
/// Server-to-client WebSocket messages.