add everything

This commit is contained in:
Koolant
2026-04-17 12:29:41 -04:00
commit 4c1cfe6847
437 changed files with 11939 additions and 0 deletions

309
js/pages/Leaderboard.js Normal file
View 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 0100
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;
}