Phase 22: mapper round 2, response shaper, card detail modal, market filters

- mapper.rs: 22 new exact routes (squad list, trophies, rivals, objectives
  sub-groups, catalogue, consumables, loan items, active messages, price
  tiers, fitness, customization) + 5 new prefix routes (squad GET/DELETE by
  ID, item chemistry PUT, SBC set GET); total exact routes now ≥55
- shaper.rs: shape /achievements → trophyData envelope; /squads → squads
  list envelope; /packs/store → purchasablePacks envelope; also shapes
  parameterised /squads/{id} responses
- dashboard: card detail modal (click any collection card → full stat view,
  chemistry/training/loan info, quick-sell action); loan-only checkbox in
  collection filters; position + min/max price filters in transfer market

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
funman300
2026-06-25 18:28:43 -07:00
parent 72ffe9a806
commit 893ee369c2
3 changed files with 417 additions and 7 deletions
+133 -5
View File
@@ -243,6 +243,28 @@ h2 { font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.05em;
.event-status { font-size: 0.75rem; font-weight: 600; } .event-status { font-size: 0.75rem; font-weight: 600; }
.event-status.on { color: #3fb950; } .event-status.on { color: #3fb950; }
.event-status.off { color: #8b949e; } .event-status.off { color: #8b949e; }
/* ── Card Detail Modal ── */
.cd-overlay { display:none; position:fixed; inset:0; background:rgba(0,0,0,0.82); z-index:300; align-items:center; justify-content:center; }
.cd-overlay.open { display:flex; }
.cd-box { background:#161b22; border:1px solid #30363d; border-radius:12px; padding:24px; max-width:520px; width:90%; max-height:88vh; overflow-y:auto; }
.cd-header { display:flex; align-items:flex-start; gap:16px; margin-bottom:16px; }
.cd-ovr { font-size:3rem; font-weight:800; color:#f5a623; line-height:1; min-width:64px; text-align:center; }
.cd-info { flex:1; }
.cd-name { font-size:1.2rem; font-weight:700; margin-bottom:2px; }
.cd-pos-badge { font-size:0.72rem; text-transform:uppercase; letter-spacing:0.06em; color:#8b949e; }
.cd-meta { display:grid; grid-template-columns:1fr 1fr; gap:4px 16px; font-size:0.8rem; margin:10px 0; }
.cd-meta-row { display:flex; gap:4px; }
.cd-meta-lbl { color:#8b949e; }
.cd-stats { display:grid; grid-template-columns:1fr 1fr; gap:5px 20px; margin:12px 0; }
.cd-stat { display:flex; align-items:center; gap:6px; font-size:0.82rem; }
.cd-stat-lbl { width:30px; color:#8b949e; font-size:0.72rem; text-align:right; }
.cd-stat-val { width:26px; font-weight:700; font-size:0.88rem; }
.cd-stat-bar { flex:1; height:5px; background:#21262d; border-radius:3px; overflow:hidden; }
.cd-stat-fill { height:100%; border-radius:3px; background:#3fb950; }
.cd-stat-fill.med { background:#d29922; }
.cd-stat-fill.low { background:#f85149; }
.cd-actions { display:flex; gap:8px; margin-top:16px; flex-wrap:wrap; }
</style> </style>
</head> </head>
<body> <body>
@@ -272,6 +294,29 @@ h2 { font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.05em;
</div> </div>
</div> </div>
<!-- ── Card detail modal ──────────────────────────────────────────────────── -->
<div id="card-detail-overlay" class="cd-overlay" onclick="if(event.target===this)closeCardDetail()">
<div class="cd-box">
<div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:4px">
<span id="cd-rarity" class="card-rarity" style="position:static;display:inline-block"></span>
<button class="btn btn-secondary btn-sm" onclick="closeCardDetail()">✕ Close</button>
</div>
<div class="cd-header">
<div>
<div class="cd-ovr" id="cd-ovr"></div>
<div id="cd-pos-badge" class="cd-pos-badge"></div>
</div>
<div class="cd-info">
<div class="cd-name" id="cd-name"></div>
<div class="cd-meta" id="cd-meta"></div>
</div>
</div>
<div class="cd-stats" id="cd-stats"></div>
<div id="cd-upgrades" style="display:flex;gap:6px;flex-wrap:wrap;margin-top:8px"></div>
<div class="cd-actions" id="cd-actions"></div>
</div>
</div>
<!-- ── Reset confirmation modal ──────────────────────────────────────────── --> <!-- ── Reset confirmation modal ──────────────────────────────────────────── -->
<div id="reset-modal" style="display:none;position:fixed;inset:0;z-index:900; <div id="reset-modal" style="display:none;position:fixed;inset:0;z-index:900;
background:rgba(0,0,0,0.75);align-items:center;justify-content:center"> background:rgba(0,0,0,0.75);align-items:center;justify-content:center">
@@ -371,6 +416,9 @@ h2 { font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.05em;
<option value="name">Sort: Name</option> <option value="name">Sort: Name</option>
<option value="pos">Sort: Position</option> <option value="pos">Sort: Position</option>
</select> </select>
<label style="display:flex;align-items:center;gap:5px;font-size:0.82rem;color:#8b949e;cursor:pointer">
<input type="checkbox" id="f-loan-only" onchange="filterCards()"> Loans only
</label>
</div> </div>
<div id="collection-count" style="font-size:0.8rem;color:#8b949e;margin-bottom:8px"></div> <div id="collection-count" style="font-size:0.8rem;color:#8b949e;margin-bottom:8px"></div>
<div id="collection-grid" class="card-grid"><span class="loading">Loading…</span></div> <div id="collection-grid" class="card-grid"><span class="loading">Loading…</span></div>
@@ -667,6 +715,18 @@ h2 { font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.05em;
<button class="btn btn-secondary btn-sm" onclick="refreshMarket()">Refresh Listings</button> <button class="btn btn-secondary btn-sm" onclick="refreshMarket()">Refresh Listings</button>
<input id="m-search" placeholder="Search player…" oninput="filterMarket()" <input id="m-search" placeholder="Search player…" oninput="filterMarket()"
style="background:#21262d;border:1px solid #30363d;color:#c9d1d9;padding:5px 10px;border-radius:6px;font-size:0.8rem"> style="background:#21262d;border:1px solid #30363d;color:#c9d1d9;padding:5px 10px;border-radius:6px;font-size:0.8rem">
<select id="m-pos" onchange="filterMarket()"
style="background:#21262d;border:1px solid #30363d;color:#c9d1d9;padding:5px 8px;border-radius:6px;font-size:0.8rem">
<option value="">All positions</option>
<option>GK</option><option>CB</option><option>RB</option><option>LB</option>
<option>CDM</option><option>CM</option><option>CAM</option>
<option>RM</option><option>LM</option><option>RW</option><option>LW</option>
<option>CF</option><option>ST</option>
</select>
<input id="m-min-price" type="number" placeholder="Min price" min="0" step="100" oninput="filterMarket()"
style="background:#21262d;border:1px solid #30363d;color:#c9d1d9;padding:5px 8px;border-radius:6px;font-size:0.8rem;width:90px">
<input id="m-max-price" type="number" placeholder="Max price" min="0" step="100" oninput="filterMarket()"
style="background:#21262d;border:1px solid #30363d;color:#c9d1d9;padding:5px 8px;border-radius:6px;font-size:0.8rem;width:90px">
</div> </div>
<div id="market-grid" class="market-grid"><span class="loading">Loading…</span></div> <div id="market-grid" class="market-grid"><span class="loading">Loading…</span></div>
</div> </div>
@@ -789,6 +849,64 @@ function quickSellValue(overall) {
return 150; return 150;
} }
function showCardDetailById(ownedCardId) {
const item = allCards.find(c => c.owned_card_id === ownedCardId);
if (item) showCardDetail(item);
}
function showCardDetail(item) {
const c = item.card;
const ovr = item.effective_overall ?? c.overall;
const pos = item.effective_position ?? c.position;
const sellCoins = quickSellValue(ovr);
el('cd-rarity').className = `card-rarity ${rarityClass(c.rarity)}`;
el('cd-rarity').textContent = (c.rarity || '').toUpperCase();
el('cd-ovr').textContent = ovr;
el('cd-pos-badge').textContent = pos;
el('cd-name').textContent = c.name;
el('cd-meta').innerHTML = `
<div class="cd-meta-row"><span class="cd-meta-lbl">Club:</span> ${c.club || '—'}</div>
<div class="cd-meta-row"><span class="cd-meta-lbl">Nation:</span> ${c.nation || '—'}</div>
<div class="cd-meta-row"><span class="cd-meta-lbl">League:</span> ${c.league || '—'}</div>
<div class="cd-meta-row"><span class="cd-meta-lbl">Quick-sell:</span> <strong style="color:#f5a623">${sellCoins.toLocaleString()}c</strong></div>
`;
function cdBar(label, val) {
const v = val || 0;
const cls = v >= 75 ? '' : v >= 60 ? 'med' : 'low';
return `<div class="cd-stat">
<span class="cd-stat-lbl">${label}</span>
<span class="cd-stat-val">${v}</span>
<div class="cd-stat-bar"><div class="cd-stat-fill ${cls}" style="width:${v}%"></div></div>
</div>`;
}
el('cd-stats').innerHTML =
cdBar('PAC', c.pace) + cdBar('SHO', c.shooting) + cdBar('PAS', c.passing) +
cdBar('DRI', c.dribbling) + cdBar('DEF', c.defending) + cdBar('PHY', c.physical);
const upgrades = [];
if (item.chemistry_style && item.chemistry_style !== 'basic')
upgrades.push(`<span class="upgrade-pill chem-pill">${item.chemistry_style}</span>`);
if (item.training_bonus > 0)
upgrades.push(`<span class="upgrade-pill train-pill">Training +${item.training_bonus}</span>`);
if (item.is_loan)
upgrades.push(`<span class="upgrade-pill loan-pill">LOAN · ${item.loan_matches_remaining ?? '?'} left</span>`);
el('cd-upgrades').innerHTML = upgrades.join('');
el('cd-actions').innerHTML = item.is_loan
? `<button class="btn btn-secondary btn-sm" onclick="closeCardDetail()">Close</button>`
: `<button class="btn btn-danger btn-sm" onclick="closeCardDetail();quickSellCard('${item.owned_card_id}', ${sellCoins}, document.createElement('button'))">Quick-sell ${sellCoins.toLocaleString()}c</button>
<button class="btn btn-secondary btn-sm" onclick="closeCardDetail()">Close</button>`;
el('card-detail-overlay').classList.add('open');
}
function closeCardDetail() {
el('card-detail-overlay').classList.remove('open');
}
async function quickSellCard(ownedCardId, coinValue, btn) { async function quickSellCard(ownedCardId, coinValue, btn) {
if (!confirm(`Quick-sell for ${coinValue.toLocaleString()} coins? This cannot be undone.`)) return; if (!confirm(`Quick-sell for ${coinValue.toLocaleString()} coins? This cannot be undone.`)) return;
btn.disabled = true; btn.disabled = true;
@@ -817,9 +935,9 @@ function renderPlayerCard(item) {
const sellCoins = quickSellValue(ovr); const sellCoins = quickSellValue(ovr);
const sellBtn = item.is_loan const sellBtn = item.is_loan
? `<button class="btn btn-danger btn-sm" disabled title="Loan cards cannot be sold" style="width:100%;margin-top:6px">LOAN</button>` ? `<button class="btn btn-danger btn-sm" disabled title="Loan cards cannot be sold" style="width:100%;margin-top:6px">LOAN</button>`
: `<button class="btn btn-danger btn-sm" onclick="quickSellCard('${item.owned_card_id}', ${sellCoins}, this)" style="width:100%;margin-top:6px">Sell ${sellCoins.toLocaleString()}c</button>`; : `<button class="btn btn-danger btn-sm" onclick="event.stopPropagation();quickSellCard('${item.owned_card_id}', ${sellCoins}, this)" style="width:100%;margin-top:6px">Sell ${sellCoins.toLocaleString()}c</button>`;
return `<div class="player-card" title="${c.club} · ${c.league} · ${c.nation}"> return `<div class="player-card" title="Click for details · ${c.club} · ${c.league}" onclick="showCardDetailById('${item.owned_card_id}')" style="cursor:pointer">
<span class="card-rarity ${rarityClass(c.rarity)}">${(c.rarity||'').toUpperCase()}</span> <span class="card-rarity ${rarityClass(c.rarity)}">${(c.rarity||'').toUpperCase()}</span>
<div class="card-ovr">${ovr}</div> <div class="card-ovr">${ovr}</div>
<div class="card-pos">${pos}</div> <div class="card-pos">${pos}</div>
@@ -899,10 +1017,12 @@ function filterCards() {
const club = el('f-club').value.toLowerCase(); const club = el('f-club').value.toLowerCase();
const nation = el('f-nation').value.toLowerCase(); const nation = el('f-nation').value.toLowerCase();
const sort = el('f-sort').value; const sort = el('f-sort').value;
const loanOnly = el('f-loan-only')?.checked;
let cards = allCards.filter(item => { let cards = allCards.filter(item => {
const c = item.card; const c = item.card;
if (!c) return false; if (!c) return false;
if (loanOnly && !item.is_loan) return false;
if (name && !c.name.toLowerCase().includes(name)) return false; if (name && !c.name.toLowerCase().includes(name)) return false;
if (pos && (item.effective_position || c.position) !== pos) return false; if (pos && (item.effective_position || c.position) !== pos) return false;
if (rarity && (c.rarity || '').toLowerCase() !== rarity) return false; if (rarity && (c.rarity || '').toLowerCase() !== rarity) return false;
@@ -2010,9 +2130,17 @@ async function loadMarketListings() {
function filterMarket() { function filterMarket() {
const q = (el('m-search')?.value || '').toLowerCase(); const q = (el('m-search')?.value || '').toLowerCase();
const filtered = q const pos = el('m-pos')?.value || '';
? allMarketListings.filter(l => l.card && l.card.name.toLowerCase().includes(q)) const minP = parseInt(el('m-min-price')?.value) || 0;
: allMarketListings; 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) { if (!filtered.length) {
el('market-grid').innerHTML = '<span class="empty">No listings found. Click "Refresh Listings" to populate the market.</span>'; el('market-grid').innerHTML = '<span class="empty">No listings found. Click "Refresh Listings" to populate the market.</span>';
+220 -2
View File
@@ -236,6 +236,116 @@ const EXACT: &[ExactRoute] = &[
core_method: "GET", core_path: "/notifications", core_method: "GET", core_path: "/notifications",
notes: "FUT notifications (plural form) → Core 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) ─────────────── // ── 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}"), core_path_fn: |suffix| format!("/collection/{suffix}"),
notes: "FUT quick-sell item → Core delete owned card", 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} // /ut/game/fut/store/pack/{pack_id}/open → POST /packs/open/{pack_id}
PrefixRoute { PrefixRoute {
ea_method: "POST", ea_method: "POST",
@@ -549,7 +697,77 @@ mod tests {
#[test] #[test]
fn test_total_exact_routes_count() { fn test_total_exact_routes_count() {
// Ensure we have a meaningful number of exact mappings assert!(EXACT.len() >= 55, "expected at least 55 exact mappings, got {}", EXACT.len());
assert!(EXACT.len() >= 35, "expected at least 35 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());
} }
} }
+64
View File
@@ -28,6 +28,9 @@ pub fn shape_response(core_path: &str, core_response: Value) -> Value {
"/sbc" => return shape_sbc_response(core_response), "/sbc" => return shape_sbc_response(core_response),
"/fut-champs" => return shape_fut_champs_response(core_response), "/fut-champs" => return shape_fut_champs_response(core_response),
"/chemistry-styles" => return shape_chemistry_styles_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/") { if core_path.starts_with("/fut-champs/") {
return shape_fut_champs_response(core_response); return shape_fut_champs_response(core_response);
} }
if core_path.starts_with("/squads/") {
return shape_squads_list_response(core_response);
}
// Unknown — pass through unchanged // Unknown — pass through unchanged
core_response 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@@ -328,4 +368,28 @@ mod tests {
assert!(shaped["auctionInfo"].as_array().is_some()); assert!(shaped["auctionInfo"].as_array().is_some());
assert_eq!(shaped["total"], 1); 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());
}
} }