${(c.rarity||'').toUpperCase()}
${ovr}
${pos}
@@ -899,10 +1017,12 @@ function filterCards() {
const club = el('f-club').value.toLowerCase();
const nation = el('f-nation').value.toLowerCase();
const sort = el('f-sort').value;
+ const loanOnly = el('f-loan-only')?.checked;
let cards = allCards.filter(item => {
const c = item.card;
if (!c) return false;
+ if (loanOnly && !item.is_loan) return false;
if (name && !c.name.toLowerCase().includes(name)) return false;
if (pos && (item.effective_position || c.position) !== pos) return false;
if (rarity && (c.rarity || '').toLowerCase() !== rarity) return false;
@@ -2010,9 +2130,17 @@ async function loadMarketListings() {
function filterMarket() {
const q = (el('m-search')?.value || '').toLowerCase();
- const filtered = q
- ? allMarketListings.filter(l => l.card && l.card.name.toLowerCase().includes(q))
- : allMarketListings;
+ const pos = el('m-pos')?.value || '';
+ const minP = parseInt(el('m-min-price')?.value) || 0;
+ const maxP = parseInt(el('m-max-price')?.value) || Infinity;
+ const filtered = allMarketListings.filter(l => {
+ if (!l.card) return false;
+ if (q && !l.card.name.toLowerCase().includes(q)) return false;
+ if (pos && l.card.position !== pos) return false;
+ const price = l.listing?.buy_now_price || l.buy_now_price || 0;
+ if (price < minP || price > maxP) return false;
+ return true;
+ });
if (!filtered.length) {
el('market-grid').innerHTML = '
No listings found. Click "Refresh Listings" to populate the market.';
diff --git a/src/mapper.rs b/src/mapper.rs
index 7e01c38..51298d2 100644
--- a/src/mapper.rs
+++ b/src/mapper.rs
@@ -236,6 +236,116 @@ const EXACT: &[ExactRoute] = &[
core_method: "GET", core_path: "/notifications",
notes: "FUT notifications (plural form) → Core notifications",
},
+ // ── Squad list ────────────────────────────────────────────────────────────
+ ExactRoute {
+ ea_method: "GET", ea_path: "/ut/game/fut/squad/list",
+ core_method: "GET", core_path: "/squads",
+ notes: "FUT squad list → Core all squads",
+ },
+ ExactRoute {
+ ea_method: "DELETE", ea_path: "/ut/game/fut/squad/active",
+ core_method: "DELETE", core_path: "/squad",
+ notes: "FUT delete active squad → Core delete squad (best-effort)",
+ },
+ // ── Achievements / Trophies ───────────────────────────────────────────────
+ ExactRoute {
+ ea_method: "GET", ea_path: "/ut/game/fut/trophies",
+ core_method: "GET", core_path: "/achievements",
+ notes: "FUT trophies → Core achievements",
+ },
+ ExactRoute {
+ ea_method: "GET", ea_path: "/ut/game/fut/trophy",
+ core_method: "GET", core_path: "/achievements",
+ notes: "FUT trophy (singular) → Core achievements",
+ },
+ // ── Rivals extra endpoints ────────────────────────────────────────────────
+ ExactRoute {
+ ea_method: "GET", ea_path: "/ut/game/fut/rivals/rank",
+ core_method: "GET", core_path: "/division",
+ notes: "FUT rivals rank → Core division",
+ },
+ ExactRoute {
+ ea_method: "POST", ea_path: "/ut/game/fut/rivals/result",
+ core_method: "POST", core_path: "/matches/result",
+ notes: "FUT rivals match result → Core match result",
+ },
+ ExactRoute {
+ ea_method: "GET", ea_path: "/ut/game/fut/rivals/leaderboard",
+ core_method: "GET", core_path: "/statistics",
+ notes: "FUT rivals leaderboard → Core statistics (offline approximation)",
+ },
+ // ── Objectives sub-groups ─────────────────────────────────────────────────
+ ExactRoute {
+ ea_method: "GET", ea_path: "/ut/game/fut/objectives/group",
+ core_method: "GET", core_path: "/objectives",
+ notes: "FUT objectives group → Core objectives",
+ },
+ ExactRoute {
+ ea_method: "GET", ea_path: "/ut/game/fut/objectives/daily",
+ core_method: "GET", core_path: "/objectives",
+ notes: "FUT daily objectives → Core objectives",
+ },
+ ExactRoute {
+ ea_method: "GET", ea_path: "/ut/game/fut/objectives/weekly",
+ core_method: "GET", core_path: "/objectives",
+ notes: "FUT weekly objectives → Core objectives",
+ },
+ ExactRoute {
+ ea_method: "POST", ea_path: "/ut/game/fut/objectives/group/claim",
+ core_method: "POST", core_path: "/objectives/claim",
+ notes: "FUT objectives group claim → Core objectives claim",
+ },
+ // ── Catalogue / Card search ───────────────────────────────────────────────
+ ExactRoute {
+ ea_method: "GET", ea_path: "/ut/game/fut/catalogue/item",
+ core_method: "GET", core_path: "/cards",
+ notes: "FUT catalogue items → Core card catalogue",
+ },
+ ExactRoute {
+ ea_method: "GET", ea_path: "/ut/game/fut/catalogue",
+ core_method: "GET", core_path: "/cards",
+ notes: "FUT catalogue → Core card catalogue",
+ },
+ // ── Store / Pricing ───────────────────────────────────────────────────────
+ ExactRoute {
+ ea_method: "GET", ea_path: "/ut/game/fut/store/pricetiers",
+ core_method: "GET", core_path: "/packs/store",
+ notes: "FUT price tiers → Core pack store definitions",
+ },
+ // ── Consumables / Chemistry / Fitness ─────────────────────────────────────
+ ExactRoute {
+ ea_method: "GET", ea_path: "/ut/game/fut/consumables",
+ core_method: "GET", core_path: "/chemistry-styles",
+ notes: "FUT consumables → Core chemistry styles (approximation)",
+ },
+ ExactRoute {
+ ea_method: "POST", ea_path: "/ut/game/fut/fitness",
+ core_method: "GET", core_path: "/club",
+ notes: "FUT fitness apply → Core club (placeholder, fitness not tracked)",
+ },
+ // ── Loan items ────────────────────────────────────────────────────────────
+ ExactRoute {
+ ea_method: "GET", ea_path: "/ut/game/fut/loanitems",
+ core_method: "GET", core_path: "/collection",
+ notes: "FUT loan items → Core collection (client filters is_loan)",
+ },
+ // ── Customization / Kit ───────────────────────────────────────────────────
+ ExactRoute {
+ ea_method: "GET", ea_path: "/ut/game/fut/customization",
+ core_method: "GET", core_path: "/settings",
+ notes: "FUT customization → Core settings",
+ },
+ ExactRoute {
+ ea_method: "PUT", ea_path: "/ut/game/fut/customization",
+ core_method: "PUT", core_path: "/settings",
+ notes: "FUT save customization → Core save settings",
+ },
+ // ── Active messages / MOTD ────────────────────────────────────────────────
+ ExactRoute {
+ ea_method: "GET", ea_path: "/ut/game/fut/activeMessage",
+ core_method: "GET", core_path: "/notifications",
+ notes: "FUT active messages → Core notifications (mapped to nearest equivalent)",
+ },
];
// ── Prefix mappings (dynamic path segments after a known prefix) ───────────────
@@ -300,6 +410,44 @@ fn prefix_routes() -> &'static [PrefixRoute] {
core_path_fn: |suffix| format!("/collection/{suffix}"),
notes: "FUT quick-sell item → Core delete owned card",
},
+ // /ut/game/fut/item/{owned_id} (PUT) → PUT /collection/{owned_id}/chemistry
+ PrefixRoute {
+ ea_method: "PUT",
+ ea_prefix: "/ut/game/fut/item/",
+ core_path_fn: |suffix| {
+ // suffix is just the owned_card_id; game sends chemistry style in body
+ format!("/collection/{suffix}/chemistry")
+ },
+ notes: "FUT apply chemistry style → Core update card chemistry",
+ },
+ // /ut/game/fut/squad/{id} (GET) → GET /squads/{id}
+ // Note: must come after exact matches for /squad/active, /squad/0, etc.
+ PrefixRoute {
+ ea_method: "GET",
+ ea_prefix: "/ut/game/fut/squad/",
+ core_path_fn: |suffix| {
+ // Skip known non-ID suffixes already handled as exact routes
+ match suffix {
+ "active" | "chemistry" | "list" => "/squad".to_string(),
+ _ => format!("/squads/{suffix}"),
+ }
+ },
+ notes: "FUT squad by ID → Core squad by ID",
+ },
+ // /ut/game/fut/squad/{id} (DELETE) → DELETE /squads/{id}
+ PrefixRoute {
+ ea_method: "DELETE",
+ ea_prefix: "/ut/game/fut/squad/",
+ core_path_fn: |suffix| format!("/squads/{suffix}"),
+ notes: "FUT delete squad by ID → Core delete squad",
+ },
+ // /ut/game/fut/sbc/set/{set_id} (GET) → GET /sbc/{set_id}
+ PrefixRoute {
+ ea_method: "GET",
+ ea_prefix: "/ut/game/fut/sbc/set/",
+ core_path_fn: |suffix| format!("/sbc/{suffix}"),
+ notes: "FUT SBC set details → Core individual SBC",
+ },
// /ut/game/fut/store/pack/{pack_id}/open → POST /packs/open/{pack_id}
PrefixRoute {
ea_method: "POST",
@@ -549,7 +697,77 @@ mod tests {
#[test]
fn test_total_exact_routes_count() {
- // Ensure we have a meaningful number of exact mappings
- assert!(EXACT.len() >= 35, "expected at least 35 exact mappings, got {}", EXACT.len());
+ assert!(EXACT.len() >= 55, "expected at least 55 exact mappings, got {}", EXACT.len());
+ }
+
+ // ── Phase 22 new mappings ─────────────────────────────────────────────────
+
+ #[test]
+ fn test_squad_list_maps() {
+ let m = map_to_core("GET", "/ut/game/fut/squad/list");
+ assert!(m.is_some());
+ assert_eq!(m.unwrap().core_path, "/squads");
+ }
+
+ #[test]
+ fn test_squad_by_id_get_maps() {
+ let m = map_to_core_with_method("GET", "/ut/game/fut/squad/abc-squad-id");
+ assert!(m.is_some());
+ assert_eq!(m.unwrap().core_path, "/squads/abc-squad-id");
+ }
+
+ #[test]
+ fn test_squad_by_id_delete_maps() {
+ let m = map_to_core_with_method("DELETE", "/ut/game/fut/squad/abc-squad-id");
+ assert!(m.is_some());
+ assert_eq!(m.unwrap().core_path, "/squads/abc-squad-id");
+ }
+
+ #[test]
+ fn test_item_put_chemistry_maps() {
+ let m = map_to_core_with_method("PUT", "/ut/game/fut/item/owned-999");
+ assert!(m.is_some());
+ assert_eq!(m.unwrap().core_path, "/collection/owned-999/chemistry");
+ }
+
+ #[test]
+ fn test_sbc_set_get_maps() {
+ let m = map_to_core_with_method("GET", "/ut/game/fut/sbc/set/sbc-123");
+ assert!(m.is_some());
+ assert_eq!(m.unwrap().core_path, "/sbc/sbc-123");
+ }
+
+ #[test]
+ fn test_trophies_maps() {
+ let m = map_to_core("GET", "/ut/game/fut/trophies");
+ assert!(m.is_some());
+ assert_eq!(m.unwrap().core_path, "/achievements");
+ }
+
+ #[test]
+ fn test_rivals_result_maps() {
+ let m = map_to_core("POST", "/ut/game/fut/rivals/result");
+ assert!(m.is_some());
+ assert_eq!(m.unwrap().core_path, "/matches/result");
+ }
+
+ #[test]
+ fn test_catalogue_item_maps() {
+ let m = map_to_core("GET", "/ut/game/fut/catalogue/item");
+ assert!(m.is_some());
+ assert_eq!(m.unwrap().core_path, "/cards");
+ }
+
+ #[test]
+ fn test_objectives_daily_maps() {
+ let m = map_to_core("GET", "/ut/game/fut/objectives/daily");
+ assert!(m.is_some());
+ assert_eq!(m.unwrap().core_path, "/objectives");
+ }
+
+ #[test]
+ fn test_active_message_maps() {
+ let m = map_to_core("GET", "/ut/game/fut/activeMessage");
+ assert!(m.is_some());
}
}
diff --git a/src/shaper.rs b/src/shaper.rs
index afd38b3..7d4c21b 100644
--- a/src/shaper.rs
+++ b/src/shaper.rs
@@ -28,6 +28,9 @@ pub fn shape_response(core_path: &str, core_response: Value) -> Value {
"/sbc" => return shape_sbc_response(core_response),
"/fut-champs" => return shape_fut_champs_response(core_response),
"/chemistry-styles" => return shape_chemistry_styles_response(core_response),
+ "/achievements" => return shape_achievements_response(core_response),
+ "/squads" => return shape_squads_list_response(core_response),
+ "/packs/store" => return shape_pack_store_response(core_response),
_ => {}
}
@@ -41,6 +44,9 @@ pub fn shape_response(core_path: &str, core_response: Value) -> Value {
if core_path.starts_with("/fut-champs/") {
return shape_fut_champs_response(core_response);
}
+ if core_path.starts_with("/squads/") {
+ return shape_squads_list_response(core_response);
+ }
// Unknown — pass through unchanged
core_response
@@ -290,6 +296,40 @@ fn shape_chemistry_styles_response(core: Value) -> Value {
})
}
+fn shape_achievements_response(core: Value) -> Value {
+ // FIFA 23 uses a "trophies" envelope with "trophyData" array
+ let achievements = core.get("achievements").cloned().unwrap_or(json!([]));
+ let earned = core.get("earned").and_then(|v| v.as_i64()).unwrap_or(0);
+ let total = core.get("total").and_then(|v| v.as_i64()).unwrap_or(0);
+ json!({
+ "trophyData": achievements,
+ "totalEarned": earned,
+ "totalAvailable": total,
+ "openfut": true,
+ })
+}
+
+fn shape_squads_list_response(core: Value) -> Value {
+ // Wrap squads in EA's squad list envelope
+ let squads = if core.is_array() {
+ core
+ } else {
+ core.get("squads").cloned().unwrap_or(json!([]))
+ };
+ json!({
+ "squads": squads,
+ "openfut": true,
+ })
+}
+
+fn shape_pack_store_response(core: Value) -> Value {
+ let packs = core.get("packs").cloned().unwrap_or(json!([]));
+ json!({
+ "purchasablePacks": packs,
+ "openfut": true,
+ })
+}
+
#[cfg(test)]
mod tests {
use super::*;
@@ -328,4 +368,28 @@ mod tests {
assert!(shaped["auctionInfo"].as_array().is_some());
assert_eq!(shaped["total"], 1);
}
+
+ #[test]
+ fn test_shape_achievements_response() {
+ let core = json!({ "achievements": [{ "id": "first_match" }], "earned": 1, "total": 18 });
+ let shaped = shape_response("/achievements", core);
+ assert!(shaped["trophyData"].is_array());
+ assert_eq!(shaped["totalEarned"], 1);
+ assert_eq!(shaped["totalAvailable"], 18);
+ }
+
+ #[test]
+ fn test_shape_squads_list_response() {
+ let core = json!({ "squads": [{ "id": "sq-1", "name": "My Squad" }] });
+ let shaped = shape_response("/squads", core);
+ assert!(shaped["squads"].is_array());
+ assert_eq!(shaped["squads"].as_array().unwrap().len(), 1);
+ }
+
+ #[test]
+ fn test_shape_pack_store_response() {
+ let core = json!({ "packs": [{ "id": "gold-pack" }] });
+ let shaped = shape_response("/packs/store", core);
+ assert!(shaped["purchasablePacks"].is_array());
+ }
}