feat: add global & server leveling, quests, achievements, guilds land-claiming, social & toggleable configs
This commit is contained in:
1 parent
4b79f69c20
commit
b4cef85b1f
40 files changed
+10261
-27
No files matched your search
@@ -0,0 +1,296 @@
|
||||
//! Minecraft-style Achievements system.
|
||||
|
||||
use crate::auth::{AdminUser, AuthUser};
|
||||
use crate::error::AppResult;
|
||||
use crate::state::AppState;
|
||||
use axum::extract::{Path, State};
|
||||
use axum::Json;
|
||||
use scopenet_shared::Achievement;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
/// Get all achievements for authenticated user with their unlock status.
|
||||
pub async fn get_my_achievements(
|
||||
auth: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
) -> AppResult<Json<Vec<Achievement>>> {
|
||||
get_achievements_for_user(&state, &auth.uuid, false).await.map(Json)
|
||||
}
|
||||
|
||||
/// Get public unlocked achievements for a specific player profile.
|
||||
pub async fn get_player_achievements(
|
||||
Path(uuid): Path<String>,
|
||||
State(state): State<AppState>,
|
||||
) -> AppResult<Json<Vec<Achievement>>> {
|
||||
get_achievements_for_user(&state, &uuid, true).await.map(Json)
|
||||
}
|
||||
|
||||
pub async fn get_achievements_for_user(
|
||||
state: &AppState,
|
||||
uuid: &str,
|
||||
unlocked_only: bool,
|
||||
) -> AppResult<Vec<Achievement>> {
|
||||
let rows: Vec<(
|
||||
String,
|
||||
String,
|
||||
String,
|
||||
String,
|
||||
String,
|
||||
String,
|
||||
String,
|
||||
String,
|
||||
i64,
|
||||
bool,
|
||||
Option<String>,
|
||||
)> = sqlx::query_as(
|
||||
"SELECT a.id, a.title, a.description, a.category, a.icon_frame, a.icon_item,
|
||||
a.icon_bg, a.icon_border, a.xp_reward, a.secret, ua.unlocked_at
|
||||
FROM achievements a
|
||||
LEFT JOIN user_achievements ua ON ua.achievement_id = a.id AND ua.user_uuid = ?
|
||||
ORDER BY a.category ASC, a.xp_reward ASC",
|
||||
)
|
||||
.bind(uuid)
|
||||
.fetch_all(&state.db)
|
||||
.await?;
|
||||
|
||||
let list = rows
|
||||
.into_iter()
|
||||
.filter_map(
|
||||
|(
|
||||
id,
|
||||
title,
|
||||
desc,
|
||||
cat,
|
||||
frame,
|
||||
item,
|
||||
bg,
|
||||
border,
|
||||
xp,
|
||||
secret,
|
||||
unlocked_at,
|
||||
)| {
|
||||
let unlocked = unlocked_at.is_some();
|
||||
if unlocked_only && !unlocked {
|
||||
return None;
|
||||
}
|
||||
Some(Achievement {
|
||||
id,
|
||||
title,
|
||||
description: desc,
|
||||
category: cat,
|
||||
icon_frame: frame,
|
||||
icon_item: item,
|
||||
icon_bg: bg,
|
||||
icon_border: border,
|
||||
xp_reward: xp,
|
||||
secret,
|
||||
unlocked,
|
||||
unlocked_at,
|
||||
})
|
||||
},
|
||||
)
|
||||
.collect();
|
||||
|
||||
Ok(list)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Admin Achievements API
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct AdminAchievementRow {
|
||||
#[serde(flatten)]
|
||||
pub achievement: Achievement,
|
||||
pub requirement_type: String,
|
||||
pub requirement_key: String,
|
||||
pub requirement_value: i64,
|
||||
pub total_unlocked: i64,
|
||||
}
|
||||
|
||||
/// Admin list all achievements.
|
||||
pub async fn admin_list_achievements(
|
||||
_admin: AdminUser,
|
||||
State(state): State<AppState>,
|
||||
) -> AppResult<Json<Vec<AdminAchievementRow>>> {
|
||||
let rows: Vec<(
|
||||
String,
|
||||
String,
|
||||
String,
|
||||
String,
|
||||
String,
|
||||
String,
|
||||
String,
|
||||
String,
|
||||
String,
|
||||
String,
|
||||
i64,
|
||||
i64,
|
||||
bool,
|
||||
i64,
|
||||
)> = sqlx::query_as(
|
||||
"SELECT a.id, a.title, a.description, a.category, a.icon_frame, a.icon_item,
|
||||
a.icon_bg, a.icon_border, a.requirement_type, a.requirement_key,
|
||||
a.requirement_value, a.xp_reward, a.secret,
|
||||
COUNT(ua.user_uuid) as total_unlocked
|
||||
FROM achievements a
|
||||
LEFT JOIN user_achievements ua ON ua.achievement_id = a.id
|
||||
GROUP BY a.id
|
||||
ORDER BY a.category ASC, a.created_at DESC",
|
||||
)
|
||||
.fetch_all(&state.db)
|
||||
.await?;
|
||||
|
||||
let list = rows
|
||||
.into_iter()
|
||||
.map(
|
||||
|(
|
||||
id,
|
||||
title,
|
||||
desc,
|
||||
cat,
|
||||
frame,
|
||||
item,
|
||||
bg,
|
||||
border,
|
||||
req_type,
|
||||
req_key,
|
||||
req_val,
|
||||
xp,
|
||||
secret,
|
||||
total_unlocked,
|
||||
)| {
|
||||
AdminAchievementRow {
|
||||
achievement: Achievement {
|
||||
id,
|
||||
title,
|
||||
description: desc,
|
||||
category: cat,
|
||||
icon_frame: frame,
|
||||
icon_item: item,
|
||||
icon_bg: bg,
|
||||
icon_border: border,
|
||||
xp_reward: xp,
|
||||
secret,
|
||||
unlocked: false,
|
||||
unlocked_at: None,
|
||||
},
|
||||
requirement_type: req_type,
|
||||
requirement_key: req_key,
|
||||
requirement_value: req_val,
|
||||
total_unlocked,
|
||||
}
|
||||
},
|
||||
)
|
||||
.collect();
|
||||
|
||||
Ok(Json(list))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct AchievementPayload {
|
||||
pub id: Option<String>,
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
pub category: String,
|
||||
pub icon_frame: String,
|
||||
pub icon_item: String,
|
||||
pub icon_bg: String,
|
||||
pub icon_border: String,
|
||||
pub requirement_type: String,
|
||||
pub requirement_key: String,
|
||||
pub requirement_value: i64,
|
||||
pub xp_reward: i64,
|
||||
pub secret: Option<bool>,
|
||||
}
|
||||
|
||||
/// Admin create achievement.
|
||||
pub async fn admin_create_achievement(
|
||||
_admin: AdminUser,
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<AchievementPayload>,
|
||||
) -> AppResult<Json<Value>> {
|
||||
let id = payload
|
||||
.id
|
||||
.filter(|s| !s.trim().is_empty())
|
||||
.unwrap_or_else(|| format!("ach_{}", uuid::Uuid::new_v4().simple()));
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO achievements (id, title, description, category, icon_frame, icon_item, icon_bg, icon_border, requirement_type, requirement_key, requirement_value, xp_reward, secret, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
)
|
||||
.bind(&id)
|
||||
.bind(payload.title)
|
||||
.bind(payload.description)
|
||||
.bind(payload.category)
|
||||
.bind(payload.icon_frame)
|
||||
.bind(payload.icon_item)
|
||||
.bind(payload.icon_bg)
|
||||
.bind(payload.icon_border)
|
||||
.bind(payload.requirement_type)
|
||||
.bind(payload.requirement_key)
|
||||
.bind(payload.requirement_value)
|
||||
.bind(payload.xp_reward)
|
||||
.bind(payload.secret.unwrap_or(false))
|
||||
.bind(&now)
|
||||
.execute(&state.db)
|
||||
.await?;
|
||||
|
||||
Ok(Json(serde_json::json!({ "id": id, "ok": true })))
|
||||
}
|
||||
|
||||
/// Admin update achievement.
|
||||
pub async fn admin_update_achievement(
|
||||
_admin: AdminUser,
|
||||
Path(id): Path<String>,
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<AchievementPayload>,
|
||||
) -> AppResult<Json<Value>> {
|
||||
sqlx::query(
|
||||
"UPDATE achievements SET
|
||||
title = ?,
|
||||
description = ?,
|
||||
category = ?,
|
||||
icon_frame = ?,
|
||||
icon_item = ?,
|
||||
icon_bg = ?,
|
||||
icon_border = ?,
|
||||
requirement_type = ?,
|
||||
requirement_key = ?,
|
||||
requirement_value = ?,
|
||||
xp_reward = ?,
|
||||
secret = COALESCE(?, secret)
|
||||
WHERE id = ?",
|
||||
)
|
||||
.bind(payload.title)
|
||||
.bind(payload.description)
|
||||
.bind(payload.category)
|
||||
.bind(payload.icon_frame)
|
||||
.bind(payload.icon_item)
|
||||
.bind(payload.icon_bg)
|
||||
.bind(payload.icon_border)
|
||||
.bind(payload.requirement_type)
|
||||
.bind(payload.requirement_key)
|
||||
.bind(payload.requirement_value)
|
||||
.bind(payload.xp_reward)
|
||||
.bind(payload.secret)
|
||||
.bind(&id)
|
||||
.execute(&state.db)
|
||||
.await?;
|
||||
|
||||
Ok(Json(serde_json::json!({ "ok": true })))
|
||||
}
|
||||
|
||||
/// Admin delete achievement.
|
||||
pub async fn admin_delete_achievement(
|
||||
_admin: AdminUser,
|
||||
Path(id): Path<String>,
|
||||
State(state): State<AppState>,
|
||||
) -> AppResult<Json<Value>> {
|
||||
sqlx::query("DELETE FROM achievements WHERE id = ?")
|
||||
.bind(&id)
|
||||
.execute(&state.db)
|
||||
.await?;
|
||||
Ok(Json(serde_json::json!({ "ok": true })))
|
||||
}
|
||||
Reference in new issue
Block a user