1import { round, score } from './score.js'; 2 3/** 4 * Path to directory containing `_list.json` and all levels 5 */ 6const dir = '/data'; 7 8export async function fetchList() { 9 // Load pack definitions once 10 const packs = await fetchPacks(); // returns null or array 11 const levelToPacks = {}; 12 13 if (packs) { 14 packs.forEach(pack => { 15 (pack.levels ?? []).forEach(levelId => { 16 (levelToPacks[levelId] ??= []).push({ 17 name: pack.name, 18 colour: pack.colour, 19 }); 20 }); 21 }); 22 } 23 24 const listResult = await fetch(`${dir}/_list.json`); 25 try { 26 const list = await listResult.json(); 27 return await Promise.all( 28 list.map(async (path, rank) => { 29 const levelResult = await fetch(`${dir}/${path}.json`); 30 try { 31 const level = await levelResult.json(); 32 return [ 33 { 34 ...level, 35 path, 36 // Inject pack membership for the level (empty if none / packs failed) 37 packs: levelToPacks[path] ?? [], 38 records: (level.records ?? []) 39 .map(({ hz, ...rest }) => rest) // keep EDL behavior 40 .sort((a, b) => b.percent - a.percent), 41 }, 42 null, 43 ]; 44 } catch { 45 console.error(`Failed to load level #${rank + 1} ${path}.`); 46 return [null, path]; 47 } 48 }), 49 ); 50 } catch { 51 console.error(`Failed to load list.`); 52 return null; 53 } 54} 55 56export async function fetchEditors() { 57 try { 58 const editorsResults = await fetch(`${dir}/_editors.json`); 59 const editors = await editorsResults.json(); 60 return editors; 61 } catch { 62 return null; 63 } 64} 65 66export async function fetchLeaderboard() { 67 const list = await fetchList(); 68 69 if (!list) return null; 70 71 const scoreMap = {}; 72 const errs = []; 73 74 list.forEach(([level, err], rank) => { 75 if (err) { 76 errs.push(err); 77 return; 78 } 79 80 // Verification 81 const verifier = 82 Object.keys(scoreMap).find((u) => u.toLowerCase() === level.verifier.toLowerCase()) || 83 level.verifier; 84 85 scoreMap[verifier] ??= { verified: [], completed: [], progressed: [] }; 86 87 scoreMap[verifier].verified.push({ 88 rank: rank + 1, 89 level: level.name, 90 score: score(rank + 1, 100, level.percentToQualify), 91 link: level.verification, 92 }); 93 94 // Records 95 level.records.forEach((record) => { 96 const user = 97 Object.keys(scoreMap).find((u) => u.toLowerCase() === record.user.toLowerCase()) || 98 record.user; 99 100 scoreMap[user] ??= { verified: [], completed: [], progressed: [] }; 101 102 if (record.percent === 100) { 103 scoreMap[user].completed.push({ 104 rank: rank + 1, 105 level: level.name, 106 levelPath: level.path, // IMPORTANT for pack completion 107 score: score(rank + 1, 100, level.percentToQualify), 108 link: record.link, 109 }); 110 return; 111 } 112 113 scoreMap[user].progressed.push({ 114 rank: rank + 1, 115 level: level.name, 116 levelPath: level.path, // optional, but keep consistent 117 percent: record.percent, 118 score: score(rank + 1, record.percent, level.percentToQualify), 119 link: record.link, 120 }); 121 }); 122 }); 123 124 // Wrap in extra Object containing the user and total score 125 const res = Object.entries(scoreMap).map(([user, scores]) => { 126 const { verified, completed, progressed } = scores; 127 const total = [verified, completed, progressed].flat().reduce((prev, cur) => prev + cur.score, 0); 128 129 return { 130 user, 131 total: round(total), 132 packs: [], 133 ...scores, 134 }; 135 }); 136 137 /* ================= PACK COMPLETION ================= */ 138 139 const packs = await fetchPacks(); // uses _packlist.json 140 141 if (packs) { 142 res.forEach((player) => { 143 const completedIds = new Set( 144 (player.completed ?? []).map((l) => l.levelPath).filter(Boolean) 145 ); 146 147 player.packs = packs.filter((pack) => { 148 const levels = pack.levels ?? []; 149 if (levels.length === 0) return false; 150 return levels.every((levelId) => completedIds.has(levelId)); 151 }); 152 }); 153 } else {
154 res.forEach((player) => (player.packs = [])); 155 } 156 157 /* =================================================== */ 158 159 // Sort by total score 160 return [res.sort((a, b) => b.total - a.total), errs]; 161} 162 163export async function fetchPacks() { 164 try { 165 const res = await fetch(`${dir}/_packlist.json`); 166 return await res.json(); // array of {name, levels, colour} 167 } catch { 168 return null; 169 } 170} 171 172export async function fetchPackLevels(packName) { 173 try { 174 const packs = await fetchPacks(); 175 if (!packs) return null; 176 177 const pack = packs.find((p) => p.name === packName); 178 if (!pack) return null; 179 180 return await Promise.all( 181 (pack.levels ?? []).map(async (path, idx) => { 182 try { 183 const levelRes = await fetch(`${dir}/${path}.json`); 184 const level = await levelRes.json(); 185 186 return [ 187 { 188 level: { 189 ...level, 190 path, 191 // optional: also inject pack membership for pack-page levels 192 records: (level.records ?? []) 193 .map(({ hz, ...rest }) => rest) // keep EDL behavior 194 .sort((a, b) => b.percent - a.percent), 195 }, 196 }, 197 null, 198 ]; 199 } catch { 200 console.error(`Failed to load pack level #${idx + 1}: ${path}.json`); 201 return [null, path]; 202 } 203 }), 204 ); 205 } catch { 206 return null; 207 } 208}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.