Add pb display

This commit is contained in:
pleb 2026-04-25 11:15:52 -07:00
parent 86830adc47
commit 3410e3324e
4 changed files with 173 additions and 11 deletions

View File

@ -317,6 +317,48 @@ span:empty {
max-width: 44rem; max-width: 44rem;
} }
.personal-best-row {
display: none;
align-items: center;
gap: 0.45rem;
font-size: 1.3rem;
font-weight: 700;
opacity: 0.92;
line-height: 1.2;
}
#friendScores.has-personal-best .personal-best-row {
display: flex;
}
.personal-best-label {
font-size: 1rem;
font-weight: 700;
opacity: 0.75;
letter-spacing: 0.04em;
}
.personal-best-acc {
font-feature-settings: "tnum";
font-variant-numeric: tabular-nums;
}
.personal-best-delta {
font-size: 1.15rem;
font-weight: 700;
font-feature-settings: "tnum";
font-variant-numeric: tabular-nums;
min-width: 4.2ch;
}
.personal-best-delta.personal-best-delta--ahead {
color: #3ddc97;
}
.personal-best-delta.personal-best-delta--behind {
color: #ff6b6b;
}
#friendScoresHeader { #friendScoresHeader {
font-size: 1.3rem; font-size: 1.3rem;
font-weight: 700; font-weight: 700;

View File

@ -39,6 +39,11 @@
</div> </div>
</div> </div>
<div id="friendScores" aria-live="polite"> <div id="friendScores" aria-live="polite">
<div id="personalBestRow" class="personal-best-row">
<span class="personal-best-label">PB</span>
<span id="personalBestAcc" class="personal-best-acc"></span>
<span id="personalBestDelta" class="personal-best-delta"></span>
</div>
<div id="friendScoresHeader"><img id="friendScoresPlayerAvatar" src="images/unknown.svg" alt=""> <span id="friendScoresHeaderText">frenz</span> <img id="friendScoresHeaderImg" src="assets/peepohigh.webp" alt=""></div> <div id="friendScoresHeader"><img id="friendScoresPlayerAvatar" src="images/unknown.svg" alt=""> <span id="friendScoresHeaderText">frenz</span> <img id="friendScoresHeaderImg" src="assets/peepohigh.webp" alt=""></div>
<ol id="friendScoresList"></ol> <ol id="friendScoresList"></ol>
<div id="friendScoresEmpty">No map loaded</div> <div id="friendScoresEmpty">No map loaded</div>

View File

@ -246,6 +246,9 @@ var beatSaberPlus = {
switch (data._event) { switch (data._event) {
case "gameState": case "gameState":
document.body.dataset.gameState = data.gameStateChanged; document.body.dataset.gameState = data.gameStateChanged;
if (data.gameStateChanged === "Menu") {
resetPersonalBestDeltaPlaceholder();
}
break; break;
case "mapInfo": case "mapInfo":
void updateMapInfo(data.mapInfoChanged); void updateMapInfo(data.mapInfoChanged);
@ -475,6 +478,9 @@ var friendScoresEmpty = must("friendScoresEmpty");
var friendScoresHeaderText = must("friendScoresHeaderText"); var friendScoresHeaderText = must("friendScoresHeaderText");
var friendScoresPlayerAvatar = must("friendScoresPlayerAvatar"); var friendScoresPlayerAvatar = must("friendScoresPlayerAvatar");
var friendScoresHeaderImg = must("friendScoresHeaderImg"); var friendScoresHeaderImg = must("friendScoresHeaderImg");
var personalBestAcc = must("personalBestAcc");
var personalBestDelta = must("personalBestDelta");
var currentPbAccuracyPercent = null;
var cachedConfiguredPlayerAvatarKey = ""; var cachedConfiguredPlayerAvatarKey = "";
var cachedConfiguredPlayerAvatarSrc = "images/unknown.svg"; var cachedConfiguredPlayerAvatarSrc = "images/unknown.svg";
async function refreshConfiguredPlayerAvatar() { async function refreshConfiguredPlayerAvatar() {
@ -497,11 +503,42 @@ async function refreshConfiguredPlayerAvatar() {
cachedConfiguredPlayerAvatarSrc = profile?.avatar?.trim() || "images/unknown.svg"; cachedConfiguredPlayerAvatarSrc = profile?.avatar?.trim() || "images/unknown.svg";
friendScoresPlayerAvatar.src = cachedConfiguredPlayerAvatarSrc; friendScoresPlayerAvatar.src = cachedConfiguredPlayerAvatarSrc;
} }
function applyPersonalBestRow(pbPercent) {
currentPbAccuracyPercent = pbPercent;
friendScoresPanel.classList.toggle("has-personal-best", pbPercent !== null);
if (pbPercent === null) {
personalBestAcc.textContent = "";
personalBestDelta.textContent = "";
personalBestDelta.classList.remove("personal-best-delta--ahead", "personal-best-delta--behind");
return;
}
personalBestAcc.textContent = `${pbPercent.toFixed(2)}%`;
resetPersonalBestDeltaPlaceholder();
}
function resetPersonalBestDeltaPlaceholder() {
if (currentPbAccuracyPercent === null) return;
personalBestDelta.textContent = "\u2014";
personalBestDelta.classList.remove("personal-best-delta--ahead", "personal-best-delta--behind");
}
function updatePersonalBestDelta(liveAccuracyPercent) {
if (currentPbAccuracyPercent === null) return;
const delta = liveAccuracyPercent - currentPbAccuracyPercent;
personalBestDelta.textContent = `${delta >= 0 ? "+" : ""}${delta.toFixed(2)}%`;
personalBestDelta.classList.toggle("personal-best-delta--ahead", delta > 5e-4);
personalBestDelta.classList.toggle("personal-best-delta--behind", delta < -5e-4);
if (Math.abs(delta) <= 5e-4) {
personalBestDelta.classList.remove("personal-best-delta--ahead", "personal-best-delta--behind");
}
}
function updateScore(score) { function updateScore(score) {
if (!settings.score) return; if (settings.score) {
accuracy.textContent = (score.accuracy * 100).toFixed(1); accuracy.textContent = (score.accuracy * 100).toFixed(1);
mistakes.textContent = score.missCount ? String(score.missCount) : ""; mistakes.textContent = score.missCount ? String(score.missCount) : "";
accuracy.classList.toggle("failed", score.currentHealth === 0); accuracy.classList.toggle("failed", score.currentHealth === 0);
}
if (settings.friends) {
updatePersonalBestDelta(score.accuracy * 100);
}
} }
function avatarFromScore(score) { function avatarFromScore(score) {
if (typeof score.player === "object" && score.player?.avatar) { if (typeof score.player === "object" && score.player?.avatar) {
@ -511,6 +548,7 @@ function avatarFromScore(score) {
return url || null; return url || null;
} }
function clearFriendScores(message) { function clearFriendScores(message) {
applyPersonalBestRow(null);
friendScoresList.replaceChildren(); friendScoresList.replaceChildren();
friendScoresEmpty.textContent = message; friendScoresEmpty.textContent = message;
friendScoresHeaderText.textContent = "frenz?"; friendScoresHeaderText.textContent = "frenz?";
@ -548,6 +586,7 @@ function friendsRelationListKey(playerId) {
} }
function beginFriendScoresForNewMapContext() { function beginFriendScoresForNewMapContext() {
friendScoreRequestId += 1; friendScoreRequestId += 1;
applyPersonalBestRow(null);
if (!settings.friends) return; if (!settings.friends) return;
if (!currentMapHash) { if (!currentMapHash) {
clearFriendScores("No map loaded"); clearFriendScores("No map loaded");
@ -578,6 +617,7 @@ async function refreshMapFriendScores() {
clearFriendScores("Waiting for BeatLeader player id"); clearFriendScores("Waiting for BeatLeader player id");
return; return;
} }
applyPersonalBestRow(null);
friendScoresList.replaceChildren(); friendScoresList.replaceChildren();
friendScoresPanel.classList.remove("has-items"); friendScoresPanel.classList.remove("has-items");
friendScoresPanel.classList.add("is-loading"); friendScoresPanel.classList.add("is-loading");
@ -594,10 +634,12 @@ async function refreshMapFriendScores() {
friendsRelationCache = fetched; friendsRelationCache = fetched;
return fetched; return fetched;
})(); })();
const [leaderboards, friends] = await Promise.all([ const [leaderboards, friends, selfProfile] = await Promise.all([
fetchBLLeaderboardsByHash(hash), fetchBLLeaderboardsByHash(hash),
friendsPromise friendsPromise,
fetchBeatLeaderPlayer(playerId)
]); ]);
const myBeatLeaderId = selfProfile?.id ?? playerId;
if (requestId !== friendScoreRequestId) return; if (requestId !== friendScoreRequestId) return;
if (leaderboards.length === 0) { if (leaderboards.length === 0) {
clearFriendScores("No BeatLeader leaderboards found"); clearFriendScores("No BeatLeader leaderboards found");
@ -620,6 +662,15 @@ async function refreshMapFriendScores() {
} }
const scores = await fetchAllMapScoresByHash(hash, forPlayMode); const scores = await fetchAllMapScoresByHash(hash, forPlayMode);
if (requestId !== friendScoreRequestId) return; if (requestId !== friendScoreRequestId) return;
let playerPbAcc = null;
for (const score of scores) {
const scorePlayerId = score.playerId ?? (typeof score.player === "object" ? score.player?.id : null);
const playerKey = scorePlayerId == null ? "" : String(scorePlayerId);
if (!playerKey || playerKey !== String(myBeatLeaderId)) continue;
const acc = normalizeAccuracy(score.accuracy ?? score.acc);
if (acc === null) continue;
if (playerPbAcc === null || acc > playerPbAcc) playerPbAcc = acc;
}
const bestByPlayer = /* @__PURE__ */ new Map(); const bestByPlayer = /* @__PURE__ */ new Map();
for (const score of scores) { for (const score of scores) {
const scorePlayerId = score.playerId ?? (typeof score.player === "object" ? score.player?.id : null); const scorePlayerId = score.playerId ?? (typeof score.player === "object" ? score.player?.id : null);
@ -640,6 +691,10 @@ async function refreshMapFriendScores() {
}); });
} }
} }
if (playerPbAcc !== null) {
bestByPlayer.delete(String(myBeatLeaderId));
}
applyPersonalBestRow(playerPbAcc);
const sorted = Array.from(bestByPlayer.values()).sort((a, b) => b.acc - a.acc); const sorted = Array.from(bestByPlayer.values()).sort((a, b) => b.acc - a.acc);
renderFriendScores(sorted); renderFriendScores(sorted);
} catch { } catch {

View File

@ -72,6 +72,9 @@ const beatSaberPlus = {
switch (data._event) { switch (data._event) {
case "gameState": case "gameState":
document.body.dataset.gameState = data.gameStateChanged; document.body.dataset.gameState = data.gameStateChanged;
if (data.gameStateChanged === "Menu") {
resetPersonalBestDeltaPlaceholder();
}
break; break;
case "mapInfo": case "mapInfo":
void updateMapInfo(data.mapInfoChanged); void updateMapInfo(data.mapInfoChanged);
@ -342,6 +345,11 @@ const friendScoresEmpty = must<HTMLElement>("friendScoresEmpty");
const friendScoresHeaderText = must<HTMLElement>("friendScoresHeaderText"); const friendScoresHeaderText = must<HTMLElement>("friendScoresHeaderText");
const friendScoresPlayerAvatar = must<HTMLImageElement>("friendScoresPlayerAvatar"); const friendScoresPlayerAvatar = must<HTMLImageElement>("friendScoresPlayerAvatar");
const friendScoresHeaderImg = must<HTMLImageElement>("friendScoresHeaderImg"); const friendScoresHeaderImg = must<HTMLImageElement>("friendScoresHeaderImg");
const personalBestAcc = must<HTMLElement>("personalBestAcc");
const personalBestDelta = must<HTMLElement>("personalBestDelta");
/** BeatLeader PB (percent) for the current map + difficulty; null if none or friends panel inactive. */
let currentPbAccuracyPercent: number | null = null;
let cachedConfiguredPlayerAvatarKey = ""; let cachedConfiguredPlayerAvatarKey = "";
let cachedConfiguredPlayerAvatarSrc = "images/unknown.svg"; let cachedConfiguredPlayerAvatarSrc = "images/unknown.svg";
@ -367,11 +375,45 @@ async function refreshConfiguredPlayerAvatar() {
friendScoresPlayerAvatar.src = cachedConfiguredPlayerAvatarSrc; friendScoresPlayerAvatar.src = cachedConfiguredPlayerAvatarSrc;
} }
function applyPersonalBestRow(pbPercent: number | null) {
currentPbAccuracyPercent = pbPercent;
friendScoresPanel.classList.toggle("has-personal-best", pbPercent !== null);
if (pbPercent === null) {
personalBestAcc.textContent = "";
personalBestDelta.textContent = "";
personalBestDelta.classList.remove("personal-best-delta--ahead", "personal-best-delta--behind");
return;
}
personalBestAcc.textContent = `${pbPercent.toFixed(2)}%`;
resetPersonalBestDeltaPlaceholder();
}
function resetPersonalBestDeltaPlaceholder() {
if (currentPbAccuracyPercent === null) return;
personalBestDelta.textContent = "—";
personalBestDelta.classList.remove("personal-best-delta--ahead", "personal-best-delta--behind");
}
function updatePersonalBestDelta(liveAccuracyPercent: number) {
if (currentPbAccuracyPercent === null) return;
const delta = liveAccuracyPercent - currentPbAccuracyPercent;
personalBestDelta.textContent = `${delta >= 0 ? "+" : ""}${delta.toFixed(2)}%`;
personalBestDelta.classList.toggle("personal-best-delta--ahead", delta > 0.0005);
personalBestDelta.classList.toggle("personal-best-delta--behind", delta < -0.0005);
if (Math.abs(delta) <= 0.0005) {
personalBestDelta.classList.remove("personal-best-delta--ahead", "personal-best-delta--behind");
}
}
function updateScore(score: Score) { function updateScore(score: Score) {
if (!settings.score) return; if (settings.score) {
accuracy.textContent = (score.accuracy * 100).toFixed(1); accuracy.textContent = (score.accuracy * 100).toFixed(1);
mistakes.textContent = score.missCount ? String(score.missCount) : ""; mistakes.textContent = score.missCount ? String(score.missCount) : "";
accuracy.classList.toggle("failed", score.currentHealth === 0); accuracy.classList.toggle("failed", score.currentHealth === 0);
}
if (settings.friends) {
updatePersonalBestDelta(score.accuracy * 100);
}
} }
function avatarFromScore(score: BeatLeaderScore): string | null { function avatarFromScore(score: BeatLeaderScore): string | null {
@ -383,6 +425,7 @@ function avatarFromScore(score: BeatLeaderScore): string | null {
} }
function clearFriendScores(message: string) { function clearFriendScores(message: string) {
applyPersonalBestRow(null);
friendScoresList.replaceChildren(); friendScoresList.replaceChildren();
friendScoresEmpty.textContent = message; friendScoresEmpty.textContent = message;
friendScoresHeaderText.textContent = "frenz?"; friendScoresHeaderText.textContent = "frenz?";
@ -427,6 +470,7 @@ function friendsRelationListKey(playerId: string): string {
*/ */
function beginFriendScoresForNewMapContext() { function beginFriendScoresForNewMapContext() {
friendScoreRequestId += 1; friendScoreRequestId += 1;
applyPersonalBestRow(null);
if (!settings.friends) return; if (!settings.friends) return;
if (!currentMapHash) { if (!currentMapHash) {
clearFriendScores("No map loaded"); clearFriendScores("No map loaded");
@ -458,6 +502,7 @@ async function refreshMapFriendScores() {
clearFriendScores("Waiting for BeatLeader player id"); clearFriendScores("Waiting for BeatLeader player id");
return; return;
} }
applyPersonalBestRow(null);
friendScoresList.replaceChildren(); friendScoresList.replaceChildren();
friendScoresPanel.classList.remove("has-items"); friendScoresPanel.classList.remove("has-items");
friendScoresPanel.classList.add("is-loading"); friendScoresPanel.classList.add("is-loading");
@ -474,10 +519,12 @@ async function refreshMapFriendScores() {
friendsRelationCache = fetched; friendsRelationCache = fetched;
return fetched; return fetched;
})(); })();
const [leaderboards, friends] = await Promise.all([ const [leaderboards, friends, selfProfile] = await Promise.all([
fetchBLLeaderboardsByHash(hash), fetchBLLeaderboardsByHash(hash),
friendsPromise, friendsPromise,
fetchBeatLeaderPlayer(playerId),
]); ]);
const myBeatLeaderId = selfProfile?.id ?? playerId;
if (requestId !== friendScoreRequestId) return; if (requestId !== friendScoreRequestId) return;
if (leaderboards.length === 0) { if (leaderboards.length === 0) {
clearFriendScores("No BeatLeader leaderboards found"); clearFriendScores("No BeatLeader leaderboards found");
@ -501,6 +548,15 @@ async function refreshMapFriendScores() {
} }
const scores = await fetchAllMapScoresByHash(hash, forPlayMode); const scores = await fetchAllMapScoresByHash(hash, forPlayMode);
if (requestId !== friendScoreRequestId) return; if (requestId !== friendScoreRequestId) return;
let playerPbAcc: number | null = null;
for (const score of scores) {
const scorePlayerId = score.playerId ?? (typeof score.player === "object" ? score.player?.id : null);
const playerKey = scorePlayerId == null ? "" : String(scorePlayerId);
if (!playerKey || playerKey !== String(myBeatLeaderId)) continue;
const acc = normalizeAccuracy(score.accuracy ?? score.acc);
if (acc === null) continue;
if (playerPbAcc === null || acc > playerPbAcc) playerPbAcc = acc;
}
const bestByPlayer = new Map<string, { name: string; acc: number; avatar: string | null }>(); const bestByPlayer = new Map<string, { name: string; acc: number; avatar: string | null }>();
for (const score of scores) { for (const score of scores) {
const scorePlayerId = score.playerId ?? (typeof score.player === "object" ? score.player?.id : null); const scorePlayerId = score.playerId ?? (typeof score.player === "object" ? score.player?.id : null);
@ -523,6 +579,10 @@ async function refreshMapFriendScores() {
}); });
} }
} }
if (playerPbAcc !== null) {
bestByPlayer.delete(String(myBeatLeaderId));
}
applyPersonalBestRow(playerPbAcc);
const sorted = Array.from(bestByPlayer.values()).sort((a, b) => b.acc - a.acc); const sorted = Array.from(bestByPlayer.values()).sort((a, b) => b.acc - a.acc);
renderFriendScores(sorted); renderFriendScores(sorted);
} catch { } catch {