176 lines
5.5 KiB
Rust
176 lines
5.5 KiB
Rust
use crate::{
|
|
db::Pool,
|
|
error::{AppError, AppResult},
|
|
models::objective::{
|
|
ClaimRewardResult, ObjectiveDefinition, ObjectiveProgress, ObjectiveWithProgress,
|
|
},
|
|
};
|
|
use anyhow::Context;
|
|
use std::path::Path;
|
|
use uuid::Uuid;
|
|
|
|
pub fn load_objective_definitions(data_dir: &str) -> anyhow::Result<Vec<ObjectiveDefinition>> {
|
|
let dir = Path::new(data_dir).join("objectives");
|
|
let mut defs = Vec::new();
|
|
if !dir.exists() {
|
|
return Ok(defs);
|
|
}
|
|
for entry in std::fs::read_dir(&dir)? {
|
|
let entry = entry?;
|
|
let path = entry.path();
|
|
if path.extension().map(|e| e == "json").unwrap_or(false) {
|
|
let content =
|
|
std::fs::read_to_string(&path).with_context(|| format!("reading {:?}", path))?;
|
|
let batch: Vec<ObjectiveDefinition> =
|
|
serde_json::from_str(&content).with_context(|| format!("parsing {:?}", path))?;
|
|
defs.extend(batch);
|
|
}
|
|
}
|
|
Ok(defs)
|
|
}
|
|
|
|
pub async fn get_objectives_with_progress(
|
|
pool: &Pool,
|
|
profile_id: &str,
|
|
defs: &[ObjectiveDefinition],
|
|
) -> AppResult<Vec<ObjectiveWithProgress>> {
|
|
let progress_rows = sqlx::query_as::<_, ObjectiveProgress>(
|
|
"SELECT id, profile_id, objective_id, current, completed, claimed, updated_at FROM objective_progress WHERE profile_id = ?"
|
|
)
|
|
.bind(profile_id)
|
|
.fetch_all(pool)
|
|
.await?;
|
|
|
|
let result = defs
|
|
.iter()
|
|
.map(|def| {
|
|
let prog = progress_rows.iter().find(|p| p.objective_id == def.id);
|
|
ObjectiveWithProgress {
|
|
definition: def.clone(),
|
|
current: prog.map(|p| p.current).unwrap_or(0),
|
|
completed: prog.map(|p| p.completed).unwrap_or(false),
|
|
claimed: prog.map(|p| p.claimed).unwrap_or(false),
|
|
}
|
|
})
|
|
.collect();
|
|
|
|
Ok(result)
|
|
}
|
|
|
|
/// Increment a metric for all objectives that track it.
|
|
pub async fn increment_metric(
|
|
pool: &Pool,
|
|
profile_id: &str,
|
|
defs: &[ObjectiveDefinition],
|
|
metric: &str,
|
|
amount: i64,
|
|
) -> AppResult<Vec<String>> {
|
|
let mut completed_ids = Vec::new();
|
|
|
|
for def in defs
|
|
.iter()
|
|
.filter(|d| d.metric.as_str() == metric)
|
|
{
|
|
let existing = sqlx::query_as::<_, ObjectiveProgress>(
|
|
"SELECT id, profile_id, objective_id, current, completed, claimed, updated_at FROM objective_progress WHERE profile_id = ? AND objective_id = ?"
|
|
)
|
|
.bind(profile_id)
|
|
.bind(&def.id)
|
|
.fetch_optional(pool)
|
|
.await?;
|
|
|
|
let now = chrono::Utc::now().to_rfc3339();
|
|
|
|
if let Some(prog) = existing {
|
|
if prog.completed {
|
|
continue;
|
|
}
|
|
let new_val = (prog.current + amount).min(def.target);
|
|
let now_complete = new_val >= def.target;
|
|
sqlx::query(
|
|
"UPDATE objective_progress SET current = ?, completed = ?, updated_at = ? WHERE id = ?"
|
|
)
|
|
.bind(new_val)
|
|
.bind(now_complete)
|
|
.bind(&now)
|
|
.bind(&prog.id)
|
|
.execute(pool)
|
|
.await?;
|
|
if now_complete {
|
|
completed_ids.push(def.id.clone());
|
|
}
|
|
} else {
|
|
let new_val = amount.min(def.target);
|
|
let now_complete = new_val >= def.target;
|
|
let id = Uuid::new_v4().to_string();
|
|
sqlx::query(
|
|
"INSERT INTO objective_progress (id, profile_id, objective_id, current, completed, claimed, updated_at) VALUES (?, ?, ?, ?, ?, 0, ?)"
|
|
)
|
|
.bind(&id)
|
|
.bind(profile_id)
|
|
.bind(&def.id)
|
|
.bind(new_val)
|
|
.bind(now_complete)
|
|
.bind(&now)
|
|
.execute(pool)
|
|
.await?;
|
|
if now_complete {
|
|
completed_ids.push(def.id.clone());
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(completed_ids)
|
|
}
|
|
|
|
pub async fn claim_objective(
|
|
pool: &Pool,
|
|
profile_id: &str,
|
|
club_id: &str,
|
|
defs: &[ObjectiveDefinition],
|
|
objective_id: &str,
|
|
) -> AppResult<ClaimRewardResult> {
|
|
let prog = sqlx::query_as::<_, ObjectiveProgress>(
|
|
"SELECT id, profile_id, objective_id, current, completed, claimed, updated_at FROM objective_progress WHERE profile_id = ? AND objective_id = ?"
|
|
)
|
|
.bind(profile_id)
|
|
.bind(objective_id)
|
|
.fetch_optional(pool)
|
|
.await?
|
|
.ok_or_else(|| AppError::NotFound("objective not started yet".into()))?;
|
|
|
|
if !prog.completed {
|
|
return Err(AppError::BadRequest("objective not completed yet".into()));
|
|
}
|
|
if prog.claimed {
|
|
return Err(AppError::Conflict("reward already claimed".into()));
|
|
}
|
|
|
|
let def = defs
|
|
.iter()
|
|
.find(|d| d.id == objective_id)
|
|
.ok_or_else(|| AppError::NotFound(format!("objective '{objective_id}' not found")))?;
|
|
|
|
sqlx::query("UPDATE objective_progress SET claimed = 1 WHERE id = ?")
|
|
.bind(&prog.id)
|
|
.execute(pool)
|
|
.await?;
|
|
|
|
if def.reward_coins > 0 {
|
|
crate::services::club::add_coins(pool, club_id, def.reward_coins).await?;
|
|
}
|
|
if def.reward_xp > 0 {
|
|
crate::services::profile::add_xp(pool, profile_id, def.reward_xp).await?;
|
|
}
|
|
if let Some(pack_id) = &def.reward_pack_id {
|
|
crate::services::pack::grant_pack(pool, club_id, pack_id).await?;
|
|
}
|
|
|
|
Ok(ClaimRewardResult {
|
|
objective_id: objective_id.to_string(),
|
|
coins_granted: def.reward_coins,
|
|
xp_granted: def.reward_xp,
|
|
pack_granted: def.reward_pack_id.clone(),
|
|
})
|
|
}
|