diff --git a/index.html b/index.html
index c579a1d..506dc47 100644
--- a/index.html
+++ b/index.html
@@ -144,8 +144,8 @@
Player |
Faction |
Disposition |
- Checked In |
ITC Rank |
+ Career W% |
List |
diff --git a/src/main.js b/src/main.js
index 1e40e71..2c861bd 100644
--- a/src/main.js
+++ b/src/main.js
@@ -87,6 +87,36 @@ async function* streamPlacings(leagueId, regionId) {
} while (nextKey);
}
+// Career win/loss/tie record, summed across every event in a player's BCP
+// placings history (same source and shape the 40k-rankings site uses for
+// its own win% column).
+async function fetchCareerRecord(userId) {
+ const r = await fetch(
+ `${API}/placings?placingsType=player&userId=${userId}&limit=1000`,
+ { headers: HEADERS, credentials: "omit" }
+ );
+ if (!r.ok) return { w: 0, l: 0, t: 0 };
+ const p = await r.json();
+ const items = Array.isArray(p) ? p : (p.data || []);
+ let w = 0, l = 0, t = 0;
+ for (const item of items) {
+ const v = item.value || item;
+ w += +(v.wins || 0);
+ l += +(v.losses || 0);
+ t += +(v.ties || 0);
+ }
+ return { w, l, t };
+}
+
+async function loadCareerRecords(userIds) {
+ const records = new Map();
+ await Promise.allSettled([...userIds].map(async uid => {
+ try { records.set(uid, await fetchCareerRecord(uid)); }
+ catch { records.set(uid, { w: 0, l: 0, t: 0 }); }
+ }));
+ return records;
+}
+
async function loadItcRanks(userIds) {
const ranks = new Map();
if (!userIds.size) return ranks;
@@ -309,6 +339,7 @@ const $ = id => document.getElementById(id);
const esc = s => String(s).replace(/[&<>"]/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c]));
const fullName = (u = {}) => [u.firstName, u.lastName].filter(Boolean).join(" ").trim();
const pct = (n, d) => d ? Math.round(n / d * 100) + "%" : "—";
+const fmtWpct = (w, l, t) => { const g = (+w || 0) + (+l || 0) + (+t || 0); return g ? ((+w || 0) / g * 100).toFixed(1) + "%" : "—"; };
function parseEventId(input) {
const s = String(input || "").trim();
@@ -496,17 +527,17 @@ function renderFactionBreakdown(container, factionCounts, dispositionsByFaction)
}).join("");
}
-function playerRowHTML(r, itcRanks, itcLoading) {
+function playerRowHTML(r, itcRanks, itcLoading, careerRecords, careerLoading) {
const name = fullName(r.user || {}) || r.user?.nickname || "Unknown";
- const checkedBadge = r.checkedIn
- ? `Yes`
- : `No`;
const listCell = r.listUrl
? `View`
: `—`;
const uid = r.userId || r.user?.id;
const rank = itcRanks.get(uid);
const rankLabel = itcLoading ? "…" : (rank != null ? `#${rank}` : "—");
+ const record = careerRecords.get(uid);
+ const wpctLabel = careerLoading ? "…" : fmtWpct(record?.w, record?.l, record?.t);
+ const wpctTitle = record ? ` title="${record.w || 0}-${record.l || 0}-${record.t || 0} career"` : "";
const inferred = text => `${text}`;
const factionCell = r._backfilledFaction ? inferred(esc(r.faction?.name || "—")) : esc(r.faction?.name || "—");
const dispositionCell = r._backfilledDisposition ? inferred(esc(r.subFaction?.name || "—")) : esc(r.subFaction?.name || "—");
@@ -515,8 +546,8 @@ function playerRowHTML(r, itcRanks, itcLoading) {
${esc(name)} |
${factionCell} |
${dispositionCell} |
- ${checkedBadge} |
${rankLabel} |
+ ${wpctLabel} |
${listCell} |
`;
}
@@ -527,7 +558,7 @@ function memberMatches(m, teamName, q) {
return hay.includes(q);
}
-function renderTeamList(teams, itcRanks, itcLoading, filterText) {
+function renderTeamList(teams, itcRanks, itcLoading, careerRecords, careerLoading, filterText) {
const q = (filterText || "").trim().toLowerCase();
const blocks = teams.map(t => {
@@ -564,12 +595,12 @@ function renderTeamList(teams, itcRanks, itcLoading, filterText) {
Player |
Faction |
Disposition |
- Checked In |
ITC Rank |
+ Career W% |
List |
- ${shown.map(m => playerRowHTML(m, itcRanks, itcLoading)).join("")}
+ ${shown.map(m => playerRowHTML(m, itcRanks, itcLoading, careerRecords, careerLoading)).join("")}
@@ -580,7 +611,7 @@ function renderTeamList(teams, itcRanks, itcLoading, filterText) {
`No teams match${q ? ` "${esc(filterText)}"` : ""}.
`;
}
-function renderPlayers(roster, itcRanks, itcLoading, filterText) {
+function renderPlayers(roster, itcRanks, itcLoading, careerRecords, careerLoading, filterText) {
const q = (filterText || "").trim().toLowerCase();
const rows = roster.filter(r => {
if (!q) return true;
@@ -589,7 +620,7 @@ function renderPlayers(roster, itcRanks, itcLoading, filterText) {
return hay.includes(q);
}).sort((a, b) => fullName(a.user || {}).localeCompare(fullName(b.user || {})));
- $("player-rows").innerHTML = rows.map(r => playerRowHTML(r, itcRanks, itcLoading)).join("");
+ $("player-rows").innerHTML = rows.map(r => playerRowHTML(r, itcRanks, itcLoading, careerRecords, careerLoading)).join("");
$("player-count").textContent = q
? `Showing ${rows.length} of ${roster.length} players`
@@ -617,7 +648,7 @@ function clearBanner() {
function render() {
if (!state) return;
- const { event, teamplayers, isTeamEvent, itcRanks, itcLoading, backfill, backfillStatus } = state;
+ const { event, teamplayers, isTeamEvent, itcRanks, itcLoading, careerRecords, careerLoading, backfill, backfillStatus } = state;
const roster = applyBackfill(state.roster, backfill).map(r =>
r.subFaction?.name
? { ...r, subFaction: { ...r.subFaction, name: correctDisposition(r.subFaction.name) } }
@@ -670,13 +701,13 @@ function render() {
const filterText = $("player-filter").value;
if (isTeamEvent) {
- renderTeamList(teams, itcRanks, itcLoading, filterText);
+ renderTeamList(teams, itcRanks, itcLoading, careerRecords, careerLoading, filterText);
const q = filterText.trim();
$("player-count").textContent = q
? `Filtering ${teams.length} teams for "${q}"`
: `${roster.length} players across ${teams.length} teams`;
} else {
- renderPlayers(roster, itcRanks, itcLoading, filterText);
+ renderPlayers(roster, itcRanks, itcLoading, careerRecords, careerLoading, filterText);
}
$("loading").classList.add("hidden");
@@ -726,6 +757,7 @@ async function loadEvent(rawInput) {
state = {
event, roster, teamplayers, isTeamEvent,
itcRanks: new Map(), itcLoading: true,
+ careerRecords: new Map(), careerLoading: true,
backfill: new Map(), backfillStatus: "idle", backfillAttempted: 0,
};
render();
@@ -739,6 +771,14 @@ async function loadEvent(rawInput) {
}
});
+ loadCareerRecords(userIds).then(records => {
+ if (state && state.event === event) {
+ state.careerRecords = records;
+ state.careerLoading = false;
+ render();
+ }
+ });
+
startBackfill(event);
} catch (err) {
const msg = String(err.message || err);