add everything
This commit is contained in:
309
js/pages/Leaderboard.js
Normal file
309
js/pages/Leaderboard.js
Normal file
@@ -0,0 +1,309 @@
|
||||
import { fetchLeaderboard, fetchPackLevels } from "../content.js";
|
||||
import { localize, getFontColour, getLeaderboardLevelThumbnail } from "../util.js";
|
||||
import { packScore } from "../score.js";
|
||||
|
||||
import Spinner from "../components/Spinner.js";
|
||||
import { store } from '../main.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Spinner,
|
||||
},
|
||||
data: () => ({
|
||||
leaderboard: [],
|
||||
profiles: [],
|
||||
loading: true,
|
||||
selected: 0,
|
||||
err: [],
|
||||
}),
|
||||
template: `
|
||||
<main v-if="loading" class="surface">
|
||||
<Spinner></Spinner>
|
||||
</main>
|
||||
<main v-else class="page-leaderboard-container">
|
||||
<div class="page-leaderboard">
|
||||
<div class="error-container">
|
||||
<p class="error" v-if="err.length > 0">
|
||||
Leaderboard may be incorrect, as the following levels could not be loaded: {{ err.join(', ') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="board-container surface">
|
||||
<table class="board">
|
||||
<tr v-for="(ientry, i) in leaderboard">
|
||||
<td class="rank">
|
||||
<p class="type-label-lg">#{{ i + 1 }}</p>
|
||||
</td>
|
||||
<td class="rank-image">
|
||||
<img v-if="i + 1 == 1" class="trophy" src="assets/Top1Trophy.png" />
|
||||
<img v-if="i + 1 == 2" class="trophy" src="assets/Top2Trophy.png" />
|
||||
<img v-if="i + 1 == 3" class="trophy" src="assets/Top3Trophy.png" />
|
||||
</td>
|
||||
<td class="total">
|
||||
<p class="type-label-lg">{{ localize(ientry.total) }}</p>
|
||||
</td>
|
||||
<td class="userIcon">
|
||||
<img class="ico" :src="\`/assets/icons/\${ientry.user}.png\`" :alt="ientry.user">
|
||||
</td>
|
||||
<td class="user" :class="{ 'active': selected == i }">
|
||||
<button @click="selected = i">
|
||||
<span class="type-label-lg">{{ ientry.user }}</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="player-container surface">
|
||||
<div class="player">
|
||||
<h1>#{{ selected + 1 }} {{ entry.user }}</h1>
|
||||
<h3>{{ entry.total }} points</h3>
|
||||
<p class="extra_points_info" v-if="entry.packPoints > 0">+{{ entry.total - entry.packPoints }} from levels</p>
|
||||
<p class="extra_points_info" v-if="entry.packPoints > 0">+{{ entry.packPoints }} from packs</p>
|
||||
|
||||
<h3 v-if="entry.verified.length > 0 || entry.completed.length > 0">
|
||||
Hardest: {{ [...entry.verified, ...entry.completed].reduce((min, current) => current.rank < min.rank ? current : min).level }}
|
||||
</h3>
|
||||
|
||||
<!-- ── Score breakdown ── -->
|
||||
<div class="lb-breakdown" v-if="breakdown.total > 0">
|
||||
|
||||
<!-- Donut chart -->
|
||||
<div class="lb-donut-wrap">
|
||||
<svg class="lb-donut" viewBox="0 0 120 120">
|
||||
<!-- background track -->
|
||||
<circle
|
||||
cx="60" cy="60" r="48"
|
||||
fill="none"
|
||||
stroke="rgba(255,255,255,0.06)"
|
||||
stroke-width="14"
|
||||
/>
|
||||
<!-- verified segment -->
|
||||
<circle
|
||||
v-if="breakdown.verified > 0"
|
||||
cx="60" cy="60" r="48"
|
||||
fill="none"
|
||||
stroke="#e8b84b"
|
||||
stroke-width="14"
|
||||
stroke-linecap="butt"
|
||||
:stroke-dasharray="donutDash(breakdown.verified, breakdown.total)"
|
||||
:stroke-dashoffset="donutOffset(0, breakdown.total)"
|
||||
transform="rotate(-90 60 60)"
|
||||
/>
|
||||
<!-- completed segment -->
|
||||
<circle
|
||||
v-if="breakdown.completed > 0"
|
||||
cx="60" cy="60" r="48"
|
||||
fill="none"
|
||||
stroke="#4b9de8"
|
||||
stroke-width="14"
|
||||
stroke-linecap="butt"
|
||||
:stroke-dasharray="donutDash(breakdown.completed, breakdown.total)"
|
||||
:stroke-dashoffset="donutOffset(breakdown.verified, breakdown.total)"
|
||||
transform="rotate(-90 60 60)"
|
||||
/>
|
||||
<!-- progressed segment -->
|
||||
<circle
|
||||
v-if="breakdown.progressed > 0"
|
||||
cx="60" cy="60" r="48"
|
||||
fill="none"
|
||||
stroke="#4be88a"
|
||||
stroke-width="14"
|
||||
stroke-linecap="butt"
|
||||
:stroke-dasharray="donutDash(breakdown.progressed, breakdown.total)"
|
||||
:stroke-dashoffset="donutOffset(breakdown.verified + breakdown.completed, breakdown.total)"
|
||||
transform="rotate(-90 60 60)"
|
||||
/>
|
||||
<!-- pack segment -->
|
||||
<circle
|
||||
v-if="breakdown.packs > 0"
|
||||
cx="60" cy="60" r="48"
|
||||
fill="none"
|
||||
stroke="#e84b9d"
|
||||
stroke-width="14"
|
||||
stroke-linecap="butt"
|
||||
:stroke-dasharray="donutDash(breakdown.packs, breakdown.total)"
|
||||
:stroke-dashoffset="donutOffset(breakdown.verified + breakdown.completed + breakdown.progressed, breakdown.total)"
|
||||
transform="rotate(-90 60 60)"
|
||||
/>
|
||||
<!-- centre label -->
|
||||
<text x="60" y="55" text-anchor="middle" class="lb-donut-total">{{ localize(breakdown.total) }}</text>
|
||||
<text x="60" y="70" text-anchor="middle" class="lb-donut-label">pts</text>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- Bar breakdown -->
|
||||
<div class="lb-bars">
|
||||
<div class="lb-bar-row" v-if="breakdown.verified > 0">
|
||||
<span class="lb-bar-dot" style="background:#e8b84b"></span>
|
||||
<span class="lb-bar-cat">Verified</span>
|
||||
<div class="lb-bar-track">
|
||||
<div class="lb-bar-fill" style="background:#e8b84b" :style="{ width: pct(breakdown.verified, breakdown.total) + '%' }"></div>
|
||||
</div>
|
||||
<span class="lb-bar-val">{{ localize(breakdown.verified) }}</span>
|
||||
</div>
|
||||
<div class="lb-bar-row" v-if="breakdown.completed > 0">
|
||||
<span class="lb-bar-dot" style="background:#4b9de8"></span>
|
||||
<span class="lb-bar-cat">Completed</span>
|
||||
<div class="lb-bar-track">
|
||||
<div class="lb-bar-fill" style="background:#4b9de8" :style="{ width: pct(breakdown.completed, breakdown.total) + '%' }"></div>
|
||||
</div>
|
||||
<span class="lb-bar-val">{{ localize(breakdown.completed) }}</span>
|
||||
</div>
|
||||
<div class="lb-bar-row" v-if="breakdown.progressed > 0">
|
||||
<span class="lb-bar-dot" style="background:#4be88a"></span>
|
||||
<span class="lb-bar-cat">Progressed</span>
|
||||
<div class="lb-bar-track">
|
||||
<div class="lb-bar-fill" style="background:#4be88a" :style="{ width: pct(breakdown.progressed, breakdown.total) + '%' }"></div>
|
||||
</div>
|
||||
<span class="lb-bar-val">{{ localize(breakdown.progressed) }}</span>
|
||||
</div>
|
||||
<div class="lb-bar-row" v-if="breakdown.packs > 0">
|
||||
<span class="lb-bar-dot" style="background:#e84b9d"></span>
|
||||
<span class="lb-bar-cat">Packs</span>
|
||||
<div class="lb-bar-track">
|
||||
<div class="lb-bar-fill" style="background:#e84b9d" :style="{ width: pct(breakdown.packs, breakdown.total) + '%' }"></div>
|
||||
</div>
|
||||
<span class="lb-bar-val">{{ localize(breakdown.packs) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- ── End score breakdown ── -->
|
||||
|
||||
<div class="packs" v-if="entry.packs.length > 0">
|
||||
<div v-for="pack in entry.packs" class="tag" :style="{background:pack.colour, color:getFontColour(pack.colour)}">
|
||||
{{pack.name}}
|
||||
</div>
|
||||
</div>
|
||||
<h2 v-if="entry.verified.length > 0">Verified ({{ entry.verified.length}})</h2>
|
||||
<table v-if="entry.verified.length > 0" class="table">
|
||||
<tr v-for="score in entry.verified">
|
||||
<td class="rank">
|
||||
<p>#{{ score.rank }}</p>
|
||||
</td>
|
||||
<td class="level" :style="getLeaderboardLevelThumbnail(score.level)">
|
||||
<a class="type-label-lg" target="_blank" :href="score.link">{{ score.level }}</a>
|
||||
<p class="type-label-sm">+{{ localize(Math.round(score.score)) }}</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<h2 v-if="entry.completed.length > 0">Completed ({{ entry.completed.length }})</h2>
|
||||
<table v-if="entry.completed.length > 0" class="table">
|
||||
<tr v-for="score in entry.completed">
|
||||
<td class="rank">
|
||||
<p>#{{ score.rank }}</p>
|
||||
</td>
|
||||
<td class="level" :style="getLeaderboardLevelThumbnail(score.level)">
|
||||
<a class="type-label-lg" target="_blank" :href="score.link">{{ score.level }}</a>
|
||||
<p class="type-label-sm">+{{ localize(Math.round(score.score)) }}</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<h2 v-if="entry.progressed.length > 0">Progressed ({{entry.progressed.length}})</h2>
|
||||
<table v-if="entry.progressed.length > 0" class="table">
|
||||
<tr v-for="score in entry.progressed">
|
||||
<td class="rank">
|
||||
<p>#{{ score.rank }}</p>
|
||||
</td>
|
||||
<td class="level" :style="getLeaderboardLevelThumbnail(score.level)">
|
||||
<a class="type-label-lg" target="_blank" :href="score.link">{{ score.level }}</a>
|
||||
<p class="type-label-sm">+{{ localize(Math.round(score.score)) }}</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
`,
|
||||
computed: {
|
||||
entry() {
|
||||
return this.leaderboard[this.selected];
|
||||
},
|
||||
// Sums each category's scores from the raw arrays
|
||||
breakdown() {
|
||||
const e = this.entry;
|
||||
if (!e) return { verified: 0, completed: 0, progressed: 0, packs: 0, total: 0 };
|
||||
|
||||
const sum = arr => (arr || []).reduce((acc, s) => acc + Math.round(s.score || 0), 0);
|
||||
|
||||
const verified = Math.round(sum(e.verified));
|
||||
const completed = Math.round(sum(e.completed));
|
||||
const progressed = Math.round(sum(e.progressed));
|
||||
const packs = e.packPoints || 0;
|
||||
const total = verified + completed + progressed + packs;
|
||||
|
||||
return { verified, completed, progressed, packs, total };
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
localize,
|
||||
getFontColour,
|
||||
getLeaderboardLevelThumbnail,
|
||||
|
||||
// Returns stroke-dasharray string for a donut segment
|
||||
// circumference = 2 * π * r = 2 * π * 48 ≈ 301.59
|
||||
donutDash(value, total) {
|
||||
const circ = 2 * Math.PI * 48;
|
||||
const filled = (value / total) * circ;
|
||||
return `${filled} ${circ - filled}`;
|
||||
},
|
||||
|
||||
// Returns stroke-dashoffset to rotate a segment into position
|
||||
// offset starts at 0 (top of circle after the -90deg transform)
|
||||
// and advances by each preceding segment's arc length
|
||||
donutOffset(preceding, total) {
|
||||
const circ = 2 * Math.PI * 48;
|
||||
// dashoffset is negative of how far around the circle we've gone
|
||||
return -((preceding / total) * circ);
|
||||
},
|
||||
|
||||
// Percentage of total, clamped 0–100
|
||||
pct(value, total) {
|
||||
if (!total) return 0;
|
||||
return Math.min(100, Math.round((value / total) * 100));
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
store.leaderboard = this;
|
||||
await resetLeaderboard();
|
||||
},
|
||||
};
|
||||
|
||||
export async function resetLeaderboard() {
|
||||
store.leaderboard.loading = true;
|
||||
|
||||
const [leaderboard, err] = await fetchLeaderboard();
|
||||
|
||||
// calculate pack points
|
||||
for (const entry of leaderboard) {
|
||||
let packPoints = 0;
|
||||
|
||||
if (entry.packs && entry.packs.length) {
|
||||
for (const pack of entry.packs) {
|
||||
const packLevels = await fetchPackLevels(pack.name);
|
||||
const levels = packLevels
|
||||
.map(l => ({
|
||||
listRank: l[0]?.listRank,
|
||||
percentToQualify: l[0]?.level?.percentToQualify ?? 0
|
||||
}))
|
||||
.filter(l => l.listRank);
|
||||
packPoints += packScore(levels);
|
||||
}
|
||||
}
|
||||
|
||||
entry.total += Math.round(packPoints);
|
||||
entry.packPoints = Math.round(packPoints);
|
||||
}
|
||||
|
||||
// pack points resort
|
||||
leaderboard.sort((a, b) => b.total - a.total);
|
||||
|
||||
for (const entry of leaderboard) {
|
||||
entry.verified = entry.verified.map(s => ({ ...s, score: Math.round(s.score) }));
|
||||
entry.completed = entry.completed.map(s => ({ ...s, score: Math.round(s.score) }));
|
||||
entry.progressed = entry.progressed.map(s => ({ ...s, score: Math.round(s.score) }));
|
||||
}
|
||||
|
||||
store.leaderboard.leaderboard = leaderboard;
|
||||
store.leaderboard.err = err;
|
||||
store.leaderboard.loading = false;
|
||||
}
|
||||
Reference in New Issue
Block a user