Initial commit: OpenFUT Core

Offline Ultimate Team backend — game-independent REST API.

- 19 API endpoints: auth, profiles, clubs, cards, packs, squads,
  objectives, SBCs, match rewards, NPC market, statistics
- Axum + SQLite + SQLx with full migrations
- Weighted pack generator, SBC validation engine
- JSON-driven mod data (cards, packs, objectives, SBCs)
- 5 integration tests passing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
funman300
2026-06-25 14:52:06 -07:00
commit 1ffe0ffa9f
60 changed files with 6152 additions and 0 deletions
+106
View File
@@ -0,0 +1,106 @@
use crate::{
db::Pool,
error::AppResult,
models::{
match_result::{Match, MatchRewardResult, SubmitMatchRequest},
objective::ObjectiveDefinition,
},
services::{club, objective, profile, statistics},
};
const COINS_WIN: i64 = 400;
const COINS_DRAW: i64 = 150;
const COINS_LOSS: i64 = 75;
const XP_WIN: i64 = 200;
const XP_DRAW: i64 = 75;
const XP_LOSS: i64 = 30;
pub async fn process_match(
pool: &Pool,
profile_id: &str,
club_id: &str,
req: &SubmitMatchRequest,
obj_defs: &[ObjectiveDefinition],
) -> AppResult<MatchRewardResult> {
let outcome = if req.goals_for > req.goals_against {
"win"
} else if req.goals_for == req.goals_against {
"draw"
} else {
"loss"
};
let (coins, xp) = match outcome {
"win" => (COINS_WIN, XP_WIN),
"draw" => (COINS_DRAW, XP_DRAW),
_ => (COINS_LOSS, XP_LOSS),
};
let match_record = Match::new(
profile_id,
&req.squad_id,
&req.opponent_name,
req.goals_for,
req.goals_against,
&req.mode,
coins,
xp,
);
sqlx::query(
"INSERT INTO matches (id, profile_id, squad_id, opponent_name, goals_for, goals_against, outcome, coins_awarded, xp_awarded, mode, played_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
)
.bind(&match_record.id)
.bind(&match_record.profile_id)
.bind(&match_record.squad_id)
.bind(&match_record.opponent_name)
.bind(match_record.goals_for)
.bind(match_record.goals_against)
.bind(&match_record.outcome)
.bind(match_record.coins_awarded)
.bind(match_record.xp_awarded)
.bind(&match_record.mode)
.bind(&match_record.played_at)
.execute(pool)
.await?;
club::add_coins(pool, club_id, coins).await?;
profile::add_xp(pool, profile_id, xp).await?;
statistics::record_match(
pool,
profile_id,
outcome,
req.goals_for,
req.goals_against,
coins,
)
.await?;
let mut objectives_updated = Vec::new();
let mut completed =
objective::increment_metric(pool, profile_id, obj_defs, "matchesplayed", 1).await?;
objectives_updated.append(&mut completed);
if outcome == "win" {
let mut c =
objective::increment_metric(pool, profile_id, obj_defs, "matcheswon", 1).await?;
objectives_updated.append(&mut c);
}
let mut c =
objective::increment_metric(pool, profile_id, obj_defs, "goalsscored", req.goals_for)
.await?;
objectives_updated.append(&mut c);
let mut c =
objective::increment_metric(pool, profile_id, obj_defs, "coinsearned", coins).await?;
objectives_updated.append(&mut c);
Ok(MatchRewardResult {
match_record,
coins_awarded: coins,
xp_awarded: xp,
objectives_updated,
})
}