import { fetchPacks, fetchPackLevels } from "../content.js"; import { getFontColour, embed } from "../util.js"; import { score, packScore } from "../score.js"; import Spinner from "../components/Spinner.js"; import LevelAuthors from "../components/List/LevelAuthors.js"; import { store } from '../main.js'; export default { components: { Spinner, LevelAuthors, }, template: ` LEVEL PACK {{ pack?.name }} {{ packPoints }} pts {{ selectedPackLevels.length }} levels {{ playerProgress.completed }}/{{ playerProgress.total }} by {{ currentPlayerName }} ALL PACKS {{ p.name }} {{ p.levels.length }} levels Preview progress for — none — {{ entry.user }} ✓ #{{ level[0]?.listRank }} {{ level[0]?.level.name }} +{{ levelScore(level[0]) }} pts {{ contribPct(level[0]) }}% › `, data: () => ({ packs: [], errors: [], selected: 0, selectedLevel: 0, selectedPackLevels: [], loading: true, loadingPack: true, toggledShowcase: true, sortBy: 'rank', selectedPlayer: '', }), computed: { pack() { return this.packs[this.selected]; }, video() { if (!this.selectedPackLevels[this.selectedLevel][0].level.showcase) { return embed(this.selectedPackLevels[this.selectedLevel][0].level.verification); } return embed( this.toggledShowcase ? this.selectedPackLevels[this.selectedLevel][0].level.showcase : this.selectedPackLevels[this.selectedLevel][0].level.verification, ); }, packPoints() { if (!Array.isArray(this.selectedPackLevels)) return 0; const levels = this.selectedPackLevels .map(([data]) => ({ listRank: data?.listRank, percentToQualify: data?.level?.percentToQualify, })) .filter(l => l.listRank); return Math.round(packScore(levels)); }, heroStyle() { return { '--hero-colour': this.pack?.colour || '#222' }; }, // Sorted level list sortedPackLevels() { if (!this.selectedPackLevels.length) return []; const copy = [...this.selectedPackLevels]; if (this.sortBy === 'points') { copy.sort((a, b) => { const pa = this.levelScore(a[0]); const pb = this.levelScore(b[0]); return pb - pa; }); } else { copy.sort((a, b) => (a[0]?.listRank ?? Infinity) - (b[0]?.listRank ?? Infinity)); } return copy; }, // Leaderboard integration — reads from store.leaderboard if available // safe even if leaderboard page was never visited this session leaderboardLoaded() { try { return !!(store.leaderboard?.leaderboard?.length); } catch { return false; } }, leaderboardEntries() { try { return store.leaderboard?.leaderboard || []; } catch { return []; } }, currentPlayerName() { return this.selectedPlayer || null; }, // Set of level names the selected player has completed or verified playerCompletedLevels() { if (!this.selectedPlayer || !this.leaderboardLoaded) return new Set(); const entry = this.leaderboardEntries.find(e => e.user === this.selectedPlayer); if (!entry) return new Set(); const names = new Set(); for (const s of [...(entry.verified || []), ...(entry.completed || [])]) { names.add(s.level); } return names; }, // Progress stats for the hero bar playerProgress() { if (!this.selectedPlayer || !this.selectedPackLevels.length) return null; const total = this.selectedPackLevels.length; const completed = this.selectedPackLevels.filter( level => this.playerCompletedLevels.has(level[0]?.level?.name) ).length; return { completed, total }; }, }, async mounted() { store.pack = this; await resetPacks(); }, methods: { async switchLevels(i) { this.loadingPack = true; this.selected = i; this.selectedLevel = 0; this.sortBy = 'rank'; this.selectedPackLevels = await fetchPackLevels(this.packs[this.selected].name); this.errors.length = 0; if (!this.packs) { this.errors = ['Failed to load list. Retry in a few minutes or notify list staff.']; } else { this.errors.push( ...this.selectedPackLevels .filter(([_, __, err]) => err) .map(([_, __, err]) => `Failed to load level. (${err}.json)`) ); } this.loadingPack = false; }, score, embed, getFontColour, goToLevel(name) { window.location.href = `/#/list/?level=${encodeURIComponent(name)}`; }, // Points a single level contributes levelScore(data) { if (!data) return 0; return Math.round(score(data.listRank, 100, data.level?.percentToQualify)); }, // Percentage of pack total this level contributes contribPct(data) { if (!data || !this.packPoints) return 0; return Math.round((this.levelScore(data) / this.packPoints) * 100); }, // Whether the selected player has completed a level isCompleted(levelName) { if (!levelName || !this.selectedPlayer) return false; return this.playerCompletedLevels.has(levelName); }, }, }; export async function resetPacks() { // Guard: if the packs component isn't mounted yet, store.pack won't // have the right shape — bail out so the component's own mounted() // call handles initialisation instead // bail if packs component isn't mounted yet if (!store.pack || store.pack.loading === undefined) return; try { store.pack.packs = await fetchPacks(); if (!store.pack.packs) { store.pack.errors = ['Failed to load list. Retry in a few minutes or notify list staff.']; return; } store.pack.selectedPackLevels = await fetchPackLevels( store.pack.packs[store.pack.selected].name ); store.pack.errors.push( ...store.pack.selectedPackLevels .filter(([_, __, err]) => err) .map(([_, __, err]) => `Failed to load level. (${err}.json)`) ); } catch (e) { console.error('resetPacks failed:', e); if (store.pack) store.pack.errors = ['Failed to load packs. Check the console for details.']; } finally { if (store.pack) { store.pack.loading = false; store.pack.loadingPack = false; } } }
ALL PACKS