Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 23 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,29 @@ This repository is intentionally lightweight: no build step, no backend, and no
- Verification script for data shape and HTML/script consistency
- Architecture notes in [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md)

## What This Does Not Claim

- Not affiliated with EA Sports
- Not an official FC26 database
- Prices, promo cards, and meta notes must be checked against the live game/client
- Data should be treated as a working catalogue, not a canonical market source
## Data, sources & scope

- **What this is:** an **independent, personal technical demonstration** — a static, build-less
single-page app exploring squad-building, tactics, and a football card catalogue. Its purpose
is to show front-end engineering (no framework, `localStorage` state, data validation, XSS-safe
rendering), not to reproduce or compete with any commercial product.
- **Player attribute data** (names, ratings, clubs, leagues) bundled in `data/*.json` originates
from a **community-compiled public dataset** (`dataSource: "EAFC26-DataHub"`). It is included
here only as a **static working snapshot** for the demo and is **not** presented as an official,
canonical, or up-to-date source. Verify anything against the live game before relying on it.
- **Player portraits are NOT included or fetched.** The app does **not** store, download, or
hotlink any third-party images. Faces are rendered locally as a generated **initials SVG
placeholder**. No request is made to any external image host.
- **No scraping is performed** by this repository. It contains only the static app and its data
snapshot — no data-collection or image-fetching scripts.

## Independence & trademarks

- **Not affiliated with, endorsed by, or associated with EA Sports, Electronic Arts, SoFIFA, or
any football league, club, or player.** "EA Sports FC" and related names are trademarks of their
respective owners; any reference is nominative and descriptive only.
- Not an official FC26 database. Prices, promo cards, and meta notes must be checked against the
live game/client.

---

Expand Down
106 changes: 12 additions & 94 deletions assets/fc26-command-center.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,9 @@ function fc26NormalizeCatalogCard(raw){
club: String(r.club || '').trim(),
nation: String(r.nation || '').trim(),
league: String(r.league || '').trim(),
face: (() => {
const f = r.face ? String(r.face).trim() : '';
if (f && /^https?:\/\//i.test(f)) return f;
const id = String(r.id != null ? r.id : '').trim();
if (!/^\d+$/.test(id)) return '';
const p6 = id.padStart(6, '0');
return 'https://cdn.sofifa.net/players/' + p6.slice(0, 3) + '/' + p6.slice(3) + '/26_120.png';
})(),
// Public-compliance: no third-party portrait URL is derived or stored.
// Faces render as a local initials SVG placeholder (see fc26CatalogFaceImgHtml).
face: '',
pace: Math.round(Number(r.pace) || 0),
sho: Math.round(Number(r.sho) || 0),
pas: Math.round(Number(r.pas) || 0),
Expand Down Expand Up @@ -113,59 +108,9 @@ function ensureCatalogLoaded(){
return _fc26CatalogPromise;
}

/** SofIFA : années 26 / 25 / 24 + tailles 120 / 180 / 60 (certaines têtes n’existent qu’en FC25). */
function fc26SofifaFaceChainFromUrl(faceUrl){
const s = String(faceUrl || '').trim();
const m = s.match(/^(https:\/\/cdn\.sofifa\.net\/players\/\d+\/\d+\/)(\d+)_(\d+)\.png$/i);
if (!m) return s ? [s] : [];
const base = m[1];
const out = [];
const seen = new Set();
for (const yr of ['26', '25', '24']) {
for (const sz of ['120', '180', '60']) {
const u = `${base}${yr}_${sz}.png`;
if (!seen.has(u)) {
seen.add(u);
out.push(u);
}
}
}
return out;
}

/** URL portrait SofIFA standard à partir de l’id EA (6 chiffres → dossiers aaa/bbb). */
function fc26SofifaDefaultFaceUrlFromPlayerId(pid){
const id = String(pid != null ? pid : '').trim();
if (!/^\d+$/.test(id)) return '';
const p6 = id.padStart(6, '0');
return 'https://cdn.sofifa.net/players/' + p6.slice(0, 3) + '/' + p6.slice(3) + '/26_120.png';
}

function fc26FaceOnError(ev){
const el = ev && ev.target;
if (!el || el.tagName !== 'IMG') return;
let chain = [];
try {
chain = JSON.parse(decodeURIComponent(el.getAttribute('data-fc-face-chain') || '%5B%5D'));
} catch (_) {
chain = [];
}
let i = parseInt(el.getAttribute('data-fc-face-i') || '0', 10);
i += 1;
if (i < chain.length) {
el.setAttribute('data-fc-face-i', String(i));
el.src = chain[i];
return;
}
if (el.dataset.fc26fb === '1') return;
el.dataset.fc26fb = '1';
const enc = el.getAttribute('data-fcfallback');
if (!enc) return;
try {
el.removeAttribute('onerror');
el.src = decodeURIComponent(enc);
} catch (_) {}
}
// Public-compliance: the third-party (SoFIFA) portrait-URL builders and the remote
// image error-chain handler were removed. Portraits are rendered as a local initials
// SVG placeholder (see fc26CatalogFaceImgHtml) — no external image requests are made.

function fc26CatalogFaceSvgDataUrl(displayName, w, h){
const W = Math.max(16, Math.round(Number(w) || 48));
Expand All @@ -183,42 +128,15 @@ function fc26CatalogFaceSvgDataUrl(displayName, w, h){
}

function fc26CatalogFaceImgHtml(faceUrl, w, h, className, fallbackName, playerId){
// Public-compliance: player portraits are NOT fetched from any third-party CDN.
// A locally-generated initials SVG placeholder is rendered instead — no hotlinking,
// no Referer handling, no external image request. faceUrl/playerId are intentionally ignored.
const W = Math.round(Number(w) || 48);
const H = Math.round(Number(h) || 48);
const ph = fc26CatalogFaceSvgDataUrl(fallbackName, W, H);
const dataFb = encodeURIComponent(ph);
const cls = String(className || '')
.replace(/[<>"']/g, '')
.trim();
let fu = faceUrl && /^https?:\/\//i.test(String(faceUrl)) ? String(faceUrl).replace(/"/g, '%22').replace(/'/g, '%27') : '';
if (!fu) {
const derived = fc26SofifaDefaultFaceUrlFromPlayerId(playerId);
if (derived) fu = derived.replace(/"/g, '%22').replace(/'/g, '%27');
}
const cls = String(className || '').replace(/[<>"']/g, '').trim();
const clsAttr = cls ? ` class="${cls}"` : '';
if (!fu) {
return `<img${clsAttr} src="${ph}" alt="" width="${W}" height="${H}" decoding="async" referrerpolicy="no-referrer">`;
}
let chain = fc26SofifaFaceChainFromUrl(fu);
const sofifaIdUrl = fc26SofifaDefaultFaceUrlFromPlayerId(playerId);
if (
sofifaIdUrl &&
!/^https:\/\/cdn\.sofifa\.net\/players\/\d+\/\d+\/\d+_\d+\.png$/i.test(String(fu).trim())
) {
const extra = fc26SofifaFaceChainFromUrl(sofifaIdUrl);
const seen = new Set(chain);
for (const u of extra) {
if (u && !seen.has(u)) {
seen.add(u);
chain.push(u);
}
}
}
const primary = chain[0] || fu;
const chainEnc = encodeURIComponent(JSON.stringify(chain));
const primaryEsc = primary.replace(/"/g, '%22').replace(/'/g, '%27');
/** SofIFA CDN : 403 si Referer tiers — ne pas envoyer de référent pour afficher les portraits hors sofifa.com. */
return `<img${clsAttr} src="${primaryEsc}" alt="" width="${W}" height="${H}" loading="lazy" decoding="async" referrerpolicy="no-referrer" data-fc-face-chain="${chainEnc}" data-fc-face-i="0" data-fcfallback="${dataFb}" onerror="fc26FaceOnError(event)">`;
return `<img${clsAttr} src="${ph}" alt="" width="${W}" height="${H}" decoding="async">`;
}

function initSquadStateOnce(){
Expand Down Expand Up @@ -636,7 +554,7 @@ function initSquadPage(){
statusEl.textContent =
'Catalogue indisponible : ' +
(e && e.message) +
' — exécutez « node scripts/build-fc26-cards-catalog.mjs » dans le dépôt, puis rechargez.';
' — vérifiez que data/fc26-cards-catalog.json est bien présent, puis rechargez.';
}
if (countEl) countEl.textContent = '0';
renderSquadSlots();
Expand Down
46 changes: 0 additions & 46 deletions scripts/backfill-sofifa-faces.mjs

This file was deleted.

Loading
Loading