add initial crates and apps
This commit is contained in:
parent
5c87ba3519
commit
1ca300098f
113 changed files with 28169 additions and 0 deletions
201
crates/chattyness-db/src/queries/avatars.rs
Normal file
201
crates/chattyness-db/src/queries/avatars.rs
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
//! Avatar-related database queries.
|
||||
|
||||
use sqlx::PgExecutor;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::models::{ActiveAvatar, AvatarRenderData};
|
||||
use chattyness_error::AppError;
|
||||
|
||||
/// Get the active avatar for a user in a realm.
|
||||
pub async fn get_active_avatar<'e>(
|
||||
executor: impl PgExecutor<'e>,
|
||||
user_id: Uuid,
|
||||
realm_id: Uuid,
|
||||
) -> Result<Option<ActiveAvatar>, AppError> {
|
||||
let avatar = sqlx::query_as::<_, ActiveAvatar>(
|
||||
r#"
|
||||
SELECT user_id, realm_id, avatar_id, current_emotion, updated_at
|
||||
FROM props.active_avatars
|
||||
WHERE user_id = $1 AND realm_id = $2
|
||||
"#,
|
||||
)
|
||||
.bind(user_id)
|
||||
.bind(realm_id)
|
||||
.fetch_optional(executor)
|
||||
.await?;
|
||||
|
||||
Ok(avatar)
|
||||
}
|
||||
|
||||
/// Set the current emotion for a user in a realm.
|
||||
/// Returns the full emotion layer (9 asset paths) for the new emotion.
|
||||
pub async fn set_emotion<'e>(
|
||||
executor: impl PgExecutor<'e>,
|
||||
user_id: Uuid,
|
||||
realm_id: Uuid,
|
||||
emotion: i16,
|
||||
) -> Result<[Option<String>; 9], AppError> {
|
||||
if emotion < 0 || emotion > 9 {
|
||||
return Err(AppError::Validation("Emotion must be 0-9".to_string()));
|
||||
}
|
||||
|
||||
// Map emotion index to column prefix
|
||||
let emotion_prefix = match emotion {
|
||||
0 => "e_neutral",
|
||||
1 => "e_happy",
|
||||
2 => "e_sad",
|
||||
3 => "e_angry",
|
||||
4 => "e_surprised",
|
||||
5 => "e_thinking",
|
||||
6 => "e_laughing",
|
||||
7 => "e_crying",
|
||||
8 => "e_love",
|
||||
9 => "e_confused",
|
||||
_ => return Err(AppError::Validation("Emotion must be 0-9".to_string())),
|
||||
};
|
||||
|
||||
// Build dynamic query for the specific emotion's 9 positions
|
||||
let query = format!(
|
||||
r#"
|
||||
WITH updated AS (
|
||||
UPDATE props.active_avatars
|
||||
SET current_emotion = $3, updated_at = now()
|
||||
WHERE user_id = $1 AND realm_id = $2
|
||||
RETURNING avatar_id
|
||||
)
|
||||
SELECT
|
||||
(SELECT prop_asset_path FROM props.inventory WHERE id = a.{prefix}_0) as p0,
|
||||
(SELECT prop_asset_path FROM props.inventory WHERE id = a.{prefix}_1) as p1,
|
||||
(SELECT prop_asset_path FROM props.inventory WHERE id = a.{prefix}_2) as p2,
|
||||
(SELECT prop_asset_path FROM props.inventory WHERE id = a.{prefix}_3) as p3,
|
||||
(SELECT prop_asset_path FROM props.inventory WHERE id = a.{prefix}_4) as p4,
|
||||
(SELECT prop_asset_path FROM props.inventory WHERE id = a.{prefix}_5) as p5,
|
||||
(SELECT prop_asset_path FROM props.inventory WHERE id = a.{prefix}_6) as p6,
|
||||
(SELECT prop_asset_path FROM props.inventory WHERE id = a.{prefix}_7) as p7,
|
||||
(SELECT prop_asset_path FROM props.inventory WHERE id = a.{prefix}_8) as p8
|
||||
FROM updated u
|
||||
JOIN props.avatars a ON a.id = u.avatar_id
|
||||
"#,
|
||||
prefix = emotion_prefix
|
||||
);
|
||||
|
||||
let result = sqlx::query_as::<_, EmotionLayerRow>(&query)
|
||||
.bind(user_id)
|
||||
.bind(realm_id)
|
||||
.bind(emotion)
|
||||
.fetch_optional(executor)
|
||||
.await?;
|
||||
|
||||
match result {
|
||||
Some(row) => Ok([
|
||||
row.p0, row.p1, row.p2, row.p3, row.p4, row.p5, row.p6, row.p7, row.p8,
|
||||
]),
|
||||
None => Err(AppError::NotFound(
|
||||
"No active avatar for this user in this realm".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Row type for emotion layer query.
|
||||
#[derive(Debug, sqlx::FromRow)]
|
||||
struct EmotionLayerRow {
|
||||
p0: Option<String>,
|
||||
p1: Option<String>,
|
||||
p2: Option<String>,
|
||||
p3: Option<String>,
|
||||
p4: Option<String>,
|
||||
p5: Option<String>,
|
||||
p6: Option<String>,
|
||||
p7: Option<String>,
|
||||
p8: Option<String>,
|
||||
}
|
||||
|
||||
/// Get render data for a user's avatar in a realm.
|
||||
///
|
||||
/// Returns the asset paths for all equipped props in the avatar's current state.
|
||||
/// This is a simplified version that only returns the center position (position 4)
|
||||
/// props for skin, clothes, accessories, and current emotion layers.
|
||||
pub async fn get_avatar_render_data<'e>(
|
||||
executor: impl PgExecutor<'e>,
|
||||
user_id: Uuid,
|
||||
realm_id: Uuid,
|
||||
) -> Result<AvatarRenderData, AppError> {
|
||||
// Simplified query: just get position 4 (center) props for each layer
|
||||
// This covers the common case of simple face avatars
|
||||
let render_data = sqlx::query_as::<_, SimplifiedAvatarRow>(
|
||||
r#"
|
||||
SELECT
|
||||
a.id as avatar_id,
|
||||
aa.current_emotion,
|
||||
-- Skin layer center
|
||||
skin.prop_asset_path as skin_center,
|
||||
-- Clothes layer center
|
||||
clothes.prop_asset_path as clothes_center,
|
||||
-- Accessories layer center
|
||||
acc.prop_asset_path as accessories_center,
|
||||
-- Current emotion layer center (based on current_emotion)
|
||||
CASE aa.current_emotion
|
||||
WHEN 0 THEN (SELECT prop_asset_path FROM props.inventory WHERE id = a.e_neutral_4)
|
||||
WHEN 1 THEN (SELECT prop_asset_path FROM props.inventory WHERE id = a.e_happy_4)
|
||||
WHEN 2 THEN (SELECT prop_asset_path FROM props.inventory WHERE id = a.e_sad_4)
|
||||
WHEN 3 THEN (SELECT prop_asset_path FROM props.inventory WHERE id = a.e_angry_4)
|
||||
WHEN 4 THEN (SELECT prop_asset_path FROM props.inventory WHERE id = a.e_surprised_4)
|
||||
WHEN 5 THEN (SELECT prop_asset_path FROM props.inventory WHERE id = a.e_thinking_4)
|
||||
WHEN 6 THEN (SELECT prop_asset_path FROM props.inventory WHERE id = a.e_laughing_4)
|
||||
WHEN 7 THEN (SELECT prop_asset_path FROM props.inventory WHERE id = a.e_crying_4)
|
||||
WHEN 8 THEN (SELECT prop_asset_path FROM props.inventory WHERE id = a.e_love_4)
|
||||
WHEN 9 THEN (SELECT prop_asset_path FROM props.inventory WHERE id = a.e_confused_4)
|
||||
END as emotion_center
|
||||
FROM props.active_avatars aa
|
||||
JOIN props.avatars a ON aa.avatar_id = a.id
|
||||
LEFT JOIN props.inventory skin ON a.l_skin_4 = skin.id
|
||||
LEFT JOIN props.inventory clothes ON a.l_clothes_4 = clothes.id
|
||||
LEFT JOIN props.inventory acc ON a.l_accessories_4 = acc.id
|
||||
WHERE aa.user_id = $1 AND aa.realm_id = $2
|
||||
"#,
|
||||
)
|
||||
.bind(user_id)
|
||||
.bind(realm_id)
|
||||
.fetch_optional(executor)
|
||||
.await?;
|
||||
|
||||
match render_data {
|
||||
Some(row) => Ok(row.into()),
|
||||
None => Ok(AvatarRenderData::default()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Simplified avatar row for center-only rendering.
|
||||
#[derive(Debug, sqlx::FromRow)]
|
||||
struct SimplifiedAvatarRow {
|
||||
avatar_id: Uuid,
|
||||
current_emotion: i16,
|
||||
skin_center: Option<String>,
|
||||
clothes_center: Option<String>,
|
||||
accessories_center: Option<String>,
|
||||
emotion_center: Option<String>,
|
||||
}
|
||||
|
||||
impl From<SimplifiedAvatarRow> for AvatarRenderData {
|
||||
fn from(row: SimplifiedAvatarRow) -> Self {
|
||||
// For now, only populate position 4 (center)
|
||||
let mut skin_layer: [Option<String>; 9] = Default::default();
|
||||
let mut clothes_layer: [Option<String>; 9] = Default::default();
|
||||
let mut accessories_layer: [Option<String>; 9] = Default::default();
|
||||
let mut emotion_layer: [Option<String>; 9] = Default::default();
|
||||
|
||||
skin_layer[4] = row.skin_center;
|
||||
clothes_layer[4] = row.clothes_center;
|
||||
accessories_layer[4] = row.accessories_center;
|
||||
emotion_layer[4] = row.emotion_center;
|
||||
|
||||
Self {
|
||||
avatar_id: row.avatar_id,
|
||||
current_emotion: row.current_emotion,
|
||||
skin_layer,
|
||||
clothes_layer,
|
||||
accessories_layer,
|
||||
emotion_layer,
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue