Phase 10: card upgrade system (chemistry styles, position change, training)
CI / Build, lint & test (push) Failing after 1m37s
CI / Build, lint & test (push) Failing after 1m37s
- GET /chemistry-styles — lists 18 FUT-style chemistry styles with stat boost breakdowns - POST /collection/:id/chemistry-style — apply a style (Shadow, Hunter, Anchor, etc.) - POST /collection/:id/position — override a player's position for 500 coins - POST /collection/:id/training — add +1/+2/+3 OVR training bonus (max +3 total) - GET /collection now includes chemistry_style, position_override, training_bonus, effective_overall and effective_position fields per owned card - Migration 0007 adds three columns to owned_cards with safe defaults - 10 new integration tests — all 55 pass, clippy clean Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+11
-2
@@ -98,7 +98,7 @@ pub async fn get_collection(State(state): State<AppState>) -> AppResult<Json<Val
|
||||
let club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?;
|
||||
|
||||
let owned = sqlx::query_as::<_, OwnedCard>(
|
||||
"SELECT id, club_id, card_id, is_loan, loan_matches_remaining, acquired_at FROM owned_cards WHERE club_id = ?"
|
||||
"SELECT id, club_id, card_id, is_loan, loan_matches_remaining, acquired_at, chemistry_style, position_override, training_bonus FROM owned_cards WHERE club_id = ?"
|
||||
)
|
||||
.bind(&club.id)
|
||||
.fetch_all(&state.pool)
|
||||
@@ -108,11 +108,19 @@ pub async fn get_collection(State(state): State<AppState>) -> AppResult<Json<Val
|
||||
.iter()
|
||||
.filter_map(|o| {
|
||||
state.card_db.get(&o.card_id).map(|def| {
|
||||
let effective_overall = def.overall as i64 + o.training_bonus;
|
||||
let effective_position =
|
||||
o.position_override.as_deref().unwrap_or(&def.position);
|
||||
json!({
|
||||
"owned_card_id": o.id,
|
||||
"is_loan": o.is_loan,
|
||||
"loan_matches_remaining": o.loan_matches_remaining,
|
||||
"acquired_at": o.acquired_at,
|
||||
"chemistry_style": o.chemistry_style,
|
||||
"position_override": o.position_override,
|
||||
"training_bonus": o.training_bonus,
|
||||
"effective_overall": effective_overall,
|
||||
"effective_position": effective_position,
|
||||
"card": def,
|
||||
})
|
||||
})
|
||||
@@ -133,7 +141,8 @@ pub async fn delete_owned_card(
|
||||
let club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?;
|
||||
|
||||
let owned = sqlx::query_as::<_, OwnedCard>(
|
||||
"SELECT id, club_id, card_id, is_loan, loan_matches_remaining, acquired_at \
|
||||
"SELECT id, club_id, card_id, is_loan, loan_matches_remaining, acquired_at, \
|
||||
chemistry_style, position_override, training_bonus \
|
||||
FROM owned_cards WHERE id = ? AND club_id = ?",
|
||||
)
|
||||
.bind(&owned_card_id)
|
||||
|
||||
@@ -15,3 +15,4 @@ pub mod sbc;
|
||||
pub mod settings;
|
||||
pub mod squad;
|
||||
pub mod statistics;
|
||||
pub mod upgrades;
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
Json,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use crate::{
|
||||
app::AppState,
|
||||
error::AppResult,
|
||||
services::{club as club_svc, profile as profile_svc, upgrades as upgrade_svc},
|
||||
};
|
||||
|
||||
/// GET /chemistry-styles — list all available styles with their stat boosts.
|
||||
pub async fn get_chemistry_styles(State(state): State<AppState>) -> AppResult<Json<Value>> {
|
||||
Ok(Json(json!({
|
||||
"chemistry_styles": *state.chem_styles,
|
||||
"total": state.chem_styles.len(),
|
||||
})))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ApplyChemStyleRequest {
|
||||
pub style_id: String,
|
||||
}
|
||||
|
||||
/// POST /collection/:owned_card_id/chemistry-style
|
||||
pub async fn post_apply_chemistry_style(
|
||||
State(state): State<AppState>,
|
||||
Path(owned_card_id): Path<String>,
|
||||
Json(req): Json<ApplyChemStyleRequest>,
|
||||
) -> AppResult<Json<Value>> {
|
||||
let profile = profile_svc::get_active_profile(&state.pool).await?;
|
||||
let club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?;
|
||||
|
||||
let updated = upgrade_svc::apply_chemistry_style(
|
||||
&state.pool,
|
||||
&club.id,
|
||||
&owned_card_id,
|
||||
&req.style_id,
|
||||
&state.chem_styles,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let style = state
|
||||
.chem_styles
|
||||
.iter()
|
||||
.find(|s| s.id == updated.chemistry_style);
|
||||
|
||||
Ok(Json(json!({
|
||||
"owned_card_id": updated.id,
|
||||
"card_id": updated.card_id,
|
||||
"chemistry_style": updated.chemistry_style,
|
||||
"style_details": style,
|
||||
})))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ChangePositionRequest {
|
||||
pub position: String,
|
||||
}
|
||||
|
||||
/// POST /collection/:owned_card_id/position — costs 500 coins.
|
||||
pub async fn post_change_position(
|
||||
State(state): State<AppState>,
|
||||
Path(owned_card_id): Path<String>,
|
||||
Json(req): Json<ChangePositionRequest>,
|
||||
) -> AppResult<Json<Value>> {
|
||||
let profile = profile_svc::get_active_profile(&state.pool).await?;
|
||||
let club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?;
|
||||
|
||||
let updated = upgrade_svc::change_position(
|
||||
&state.pool,
|
||||
&club.id,
|
||||
&owned_card_id,
|
||||
&req.position,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let card_def = state.card_db.get(&updated.card_id);
|
||||
|
||||
Ok(Json(json!({
|
||||
"owned_card_id": updated.id,
|
||||
"card_id": updated.card_id,
|
||||
"original_position": card_def.map(|c| &c.position),
|
||||
"position_override": updated.position_override,
|
||||
"cost_coins": upgrade_svc::POSITION_CHANGE_COST,
|
||||
})))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ApplyTrainingRequest {
|
||||
/// OVR points to add. Must be 1–3. Total capped at +3.
|
||||
pub boost: i64,
|
||||
}
|
||||
|
||||
/// POST /collection/:owned_card_id/training — applies a training boost (up to +3 OVR total).
|
||||
pub async fn post_apply_training(
|
||||
State(state): State<AppState>,
|
||||
Path(owned_card_id): Path<String>,
|
||||
Json(req): Json<ApplyTrainingRequest>,
|
||||
) -> AppResult<Json<Value>> {
|
||||
let profile = profile_svc::get_active_profile(&state.pool).await?;
|
||||
let club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?;
|
||||
|
||||
let updated =
|
||||
upgrade_svc::apply_training(&state.pool, &club.id, &owned_card_id, req.boost).await?;
|
||||
|
||||
let card_def = state.card_db.get(&updated.card_id);
|
||||
let base_overall = card_def.map(|c| c.overall as i64).unwrap_or(0);
|
||||
|
||||
Ok(Json(json!({
|
||||
"owned_card_id": updated.id,
|
||||
"card_id": updated.card_id,
|
||||
"base_overall": base_overall,
|
||||
"training_bonus": updated.training_bonus,
|
||||
"effective_overall": base_overall + updated.training_bonus,
|
||||
"max_bonus": upgrade_svc::MAX_TRAINING_BONUS,
|
||||
})))
|
||||
}
|
||||
Reference in New Issue
Block a user