add initial crates and apps

This commit is contained in:
Evan Carroll 2026-01-12 15:34:40 -06:00
parent 5c87ba3519
commit 1ca300098f
113 changed files with 28169 additions and 0 deletions

View 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,
}
}
}