18 data-driven achievements (achievements.json) across 8 trigger categories: matches_played, matches_won, goals_scored, packs_opened, sbcs_completed, cards_owned, level, objectives_completed, drafts_completed. Rarities span common → epic. Coin rewards range from 500 (first_match) to 6000 (win_50). check_and_unlock() queries the relevant metric from existing tables, skips already-earned achievements via INSERT OR IGNORE, grants coin rewards, and fires a persistent notification per unlock. Trigger values are cached per call to avoid redundant DB round-trips for same-trigger achievements. Checks run automatically after every match result (all triggers), every pack open (packs_opened), and every successful SBC submission (sbcs_completed). GET /achievements returns all definitions annotated with unlocked/unlocked_at, plus earned and total counts. POST /matches/result response gains an achievements_unlocked array (empty when nothing new unlocked). AppState gains achievement_defs (Arc<Vec<AchievementDefinition>>) loaded from data/achievements/**/*.json at startup — same pattern as obj_defs. 5 new tests: list endpoint, first_match unlock, first_win unlock, no-dup guard, coin reward verification. Core now at 82 tests. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+9
-3
@@ -2,6 +2,7 @@ use crate::middleware::{limit_concurrency, MakeRequestUuid};
|
||||
use crate::{
|
||||
config::Config,
|
||||
db::Pool,
|
||||
models::achievement::AchievementDefinition,
|
||||
models::chemistry_style::{load_chemistry_styles, ChemistryStyle},
|
||||
models::event::EventDefinition,
|
||||
models::objective::{ObjectiveDefinition, ObjectiveType},
|
||||
@@ -9,8 +10,8 @@ use crate::{
|
||||
models::sbc::SbcDefinition,
|
||||
routes,
|
||||
services::{
|
||||
card_db::CardDb, event::load_event_definitions, market,
|
||||
objective::load_objective_definitions, pack::load_pack_definitions,
|
||||
achievement::load_achievement_definitions, card_db::CardDb, event::load_event_definitions,
|
||||
market, objective::load_objective_definitions, pack::load_pack_definitions,
|
||||
sbc::load_sbc_definitions,
|
||||
},
|
||||
};
|
||||
@@ -37,6 +38,7 @@ pub struct AppState {
|
||||
pub sbc_defs: Arc<Vec<SbcDefinition>>,
|
||||
pub event_defs: Arc<Vec<EventDefinition>>,
|
||||
pub chem_styles: Arc<Vec<ChemistryStyle>>,
|
||||
pub achievement_defs: Arc<Vec<AchievementDefinition>>,
|
||||
}
|
||||
|
||||
pub async fn build(pool: Pool, cfg: Config) -> Result<Router> {
|
||||
@@ -46,15 +48,17 @@ pub async fn build(pool: Pool, cfg: Config) -> Result<Router> {
|
||||
let sbc_defs = Arc::new(load_sbc_definitions(&cfg.data_dir)?);
|
||||
let event_defs = Arc::new(load_event_definitions(&cfg.data_dir)?);
|
||||
let chem_styles = Arc::new(load_chemistry_styles(&cfg.data_dir)?);
|
||||
let achievement_defs = Arc::new(load_achievement_definitions(&cfg.data_dir)?);
|
||||
|
||||
tracing::info!(
|
||||
"Loaded {} cards, {} packs, {} objectives, {} SBCs, {} events, {} chemistry styles",
|
||||
"Loaded {} cards, {} packs, {} objectives, {} SBCs, {} events, {} chemistry styles, {} achievements",
|
||||
card_db.cards.len(),
|
||||
pack_defs.len(),
|
||||
obj_defs.len(),
|
||||
sbc_defs.len(),
|
||||
event_defs.len(),
|
||||
chem_styles.len(),
|
||||
achievement_defs.len(),
|
||||
);
|
||||
|
||||
let state = AppState {
|
||||
@@ -65,6 +69,7 @@ pub async fn build(pool: Pool, cfg: Config) -> Result<Router> {
|
||||
sbc_defs,
|
||||
event_defs: event_defs.clone(),
|
||||
chem_styles,
|
||||
achievement_defs,
|
||||
};
|
||||
|
||||
// Background task: refresh NPC market at startup and every 24 hours
|
||||
@@ -194,6 +199,7 @@ pub async fn build(pool: Pool, cfg: Config) -> Result<Router> {
|
||||
.route("/settings", get(routes::settings::get_settings))
|
||||
.route("/settings", put(routes::settings::put_settings))
|
||||
.route("/division", get(routes::division::get_division))
|
||||
.route("/achievements", get(routes::achievements::get_achievements))
|
||||
.route("/notifications", get(routes::notifications::get_notifications))
|
||||
.route("/notifications/read-all", post(routes::notifications::mark_all_notifications_read))
|
||||
.route("/notifications/:id/read", patch(routes::notifications::mark_notification_read))
|
||||
|
||||
Reference in New Issue
Block a user