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:
+133
-5
@@ -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
@@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user