1/* The Torchbearer â the Roll of Aspirants. 2 * 3 * A leaderboard with no server: the top runs live in localStorage, arcade 4 * style, with three initials each. Two boards are kept â ALL TIME, and TODAY, 5 * which only accepts Daily Trial runs so its scores are genuinely comparable 6 * (everyone that day played the identical crypt). 7 * 8 * Every write is wrapped: private mode must degrade to "no history", never to 9 * a thrown exception. 10 */ 11window.SK = window.SK || {}; 12 13SK.BOARD_KEY = 'shadowKingCrownfall.board'; 14SK.BOARD_MAX = 10; 15 16SK.loadBoard = function () { 17 try { 18 const raw = window.localStorage.getItem(SK.BOARD_KEY); 19 const b = raw ? JSON.parse(raw) : []; 20 return Array.isArray(b) ? b : []; 21 } catch (e) { return []; } 22}; 23 24SK.saveBoard = function (rows) { 25 try { window.localStorage.setItem(SK.BOARD_KEY, JSON.stringify(rows)); } 26 catch (e) { /* private mode */ } 27}; 28 29/* Would this run make the board? Used to decide whether to ask for initials. */ 30SK.boardQualifies = function (score) { 31 if (score <= 0) return false; 32 const rows = SK.loadBoard(); 33 if (rows.length < SK.BOARD_MAX) return true; 34 return rows.some(r => score > r.score); 35}; 36 37SK.submitScore = function (run, won, name) { 38 const rows = SK.loadBoard(); 39 rows.push({ 40 name: (name || 'AAA').toUpperCase().slice(0, 3), 41 score: run.score, 42 floor: run.floor + 1, 43 loop: run.loop || 0, 44 cls: run.classId || 'knight', 45 mode: run.mode || 'free', 46 day: run.seedKey || SK.todayKey(), 47 won: !!won, 48 at: Date.now() 49 }); 50 rows.sort((a, b) => b.score - a.score); 51 const trimmed = rows.slice(0, SK.BOARD_MAX * 3); // keep some history for TODAY 52 SK.saveBoard(trimmed); 53 return trimmed; 54}; 55 56SK.LeaderScene = class LeaderScene extends Phaser.Scene { 57 constructor() { super({ key: 'LeaderScene' }); } 58 59 init(data) { 60 this.from = (data && data.from) || 'TitleScene'; 61 this.tab = (data && data.tab) || 'all'; 62 this.highlightAt = (data && data.highlightAt) || 0; 63 } 64 65 create() { 66 const w = this.scale.width, h = this.scale.height, cx = w / 2; 67 this.cameras.main.setBackgroundColor(SK.CSS.void); 68 this.cameras.main.fadeIn(180, 14, 11, 8); 69 this.ambient = SK.makeAmbient(this, 14); 70 71 SK.ui.header(this, cx, 26, 'ROLL OF ASPIRANTS', null, 72 { size: 27, ruleW: 460 }); 73 74 this.tabStrip = SK.ui.tabs(this, cx, 84, 75 [['all', 'ALL TIME'], ['today', "TODAY'S TRIAL"]], 76 (id) => { this.tab = id; this._render(); }); 77 78 this.frameX = 60; this.frameY = 112; 79 this.frameW = w - 120; this.frameH = h - 112 - 56; 80 SK.ui.panel(this, this.frameX, this.frameY, this.frameW, this.frameH, 81 { band: false, alpha: 0.55 }); 82 83 this.rows = this.add.container(0, 0); 84 this._render(); 85 86 this.add.text(cx, h - 32, 'ESC or BACKSPACE to go back', { 87 fontFamily: SK.FONT_MONO, fontSize: '14px', 88 color: SK.CSS.gold 89 }).setOrigin(0.5); 90 91 this.onKey = (e) => { 92 if (e.key === 'Escape' || e.key === 'Backspace') { 93 this.cameras.main.fade(160, 14, 11, 8); 94 this.time.delayedCall(170, () => this.scene.start(this.from)); 95 } 96 }; 97 this.input.keyboard.on('keydown', this.onKey); 98 this.events.once('shutdown', this.cleanup, this); 99 } 100 101 _render() { 102 const w = this.scale.width, cx = w / 2; 103 this.rows.removeAll(true); 104 if (this.tabStrip) this.tabStrip.paint(this.tab); 105 106 let list = SK.loadBoard(); 107 if (this.tab === 'today') { 108 const today = SK.todayKey(); 109 list = list.filter(r => r.mode === 'daily' && r.day === today); 110 } 111 list = list.slice(0, SK.BOARD_MAX); 112 113 const head = this.add.text(cx, this.frameY + 18, 114 ' # NAME SCORE FLOOR CALLING', { 115 fontFamily: SK.FONT_MONO, fontSize: '13px', 116 color: '#9c8b73' 117 }).setOrigin(0.5); 118 this.rows.add(head); 119 120 if (!list.length) { 121 this.rows.add(this.add.text(cx, this.frameY + 110, 122 this.tab === 'today' 123 ? 'No Daily Trial runs yet today.\nPress D on the title to set the first.' 124 : 'The roll is empty. Go and die gloriously.', { 125 fontFamily: SK.FONT_MONO, fontSize: '14px', 126 color: '#b4a389', align: 'center', lineSpacing: 6 127 }).setOrigin(0.5)); 128 return; 129 } 130 131 list.forEach((r, i) => { 132 const fresh = r.at === this.highlightAt; 133 const y = this.frameY + 48 + i * 27; 134 const cls = (SK.CLASSES[r.cls] || {}).tag || ''; 135 const line = 136 String(i + 1).padStart(3, ' ') + ' ' + 137 r.name.padEnd(6, ' ') + ' ' + 138 String(r.score).padStart(6, ' ') + ' ' + 139 (r.won ? 'CROWN' : String(r.floor).padStart(2, ' ') + ' ') + ' ' + 140 cls; 141 const t = this.add.text(cx, y, line, { 142 fontFamily: SK.FONT_MONO, fontSize: '15px', 143 color: fresh ? SK.CSS.gold : (r.won ? SK.CSS.flame : SK.CSS.bone) 144 }).setOrigin(0.5); 145 if (fresh) { 146 this.tweens.add({ 147 targets: t, alpha: { from: 1, to: 0.45 }, 148 duration: 520, yoyo: true, repeat: -1 149 }); 150 } 151 this.rows.add(t); 152 }); 153 } 154 155 cleanup() {
156 this.input.keyboard.off('keydown', this.onKey); 157 this.tweens.killAll(); 158 if (this.ambient) this.ambient.destroy(); 159 } 160}; 161 162/* ---- initials entry -------------------------------------------------- */ 163 164SK.NameScene = class NameScene extends Phaser.Scene { 165 constructor() { super({ key: 'NameScene' }); } 166 167 init(data) { this.won = !!(data && data.won); } 168 169 create() { 170 const w = this.scale.width, h = this.scale.height, cx = w / 2; 171 this.cameras.main.setBackgroundColor(SK.CSS.void); 172 this.cameras.main.fadeIn(180, 14, 11, 8); 173 this.ambient = SK.makeAmbient(this, 12); 174 175 this.letters = ['A', 'A', 'A']; 176 this.slot = 0; 177 this.done = false; 178 179 this.add.text(cx, 120, 'A PLACE ON THE ROLL', { 180 fontFamily: SK.FONT_MONO, fontSize: '26px', 181 color: SK.CSS.gold, stroke: '#12100c', strokeThickness: 4 182 }).setOrigin(0.5); 183 184 this.add.text(cx, 158, 'SCORE ' + SK.run.score, { 185 fontFamily: SK.FONT_MONO, fontSize: '18px', 186 color: SK.CSS.bone 187 }).setOrigin(0.5); 188 189 this.slotTexts = this.letters.map((c, i) => 190 this.add.text(cx + (i - 1) * 62, 250, c, { 191 fontFamily: SK.FONT_MONO, fontSize: '54px', 192 color: SK.CSS.gold 193 }).setOrigin(0.5)); 194 195 this.caret = this.add.graphics().setDepth(5); 196 197 this.add.text(cx, 340, 198 'TYPE three letters · ARROWS to change · ENTER to sign', { 199 fontFamily: SK.FONT_MONO, fontSize: '13px', 200 color: '#b4a389' 201 }).setOrigin(0.5); 202 203 this._paint(); 204 205 this.onKey = (e) => this._key(e); 206 this.input.keyboard.on('keydown', this.onKey); 207 this.events.once('shutdown', this.cleanup, this); 208 } 209 210 _paint() { 211 this.caret.clear(); 212 const cx = this.scale.width / 2; 213 this.caret.fillStyle(0xffcc33, 1); 214 this.caret.fillRect(cx + (this.slot - 1) * 62 - 20, 288, 40, 3);
215 this.slotTexts.forEach((t, i) => { 216 t.setText(this.letters[i]); 217 t.setColor(i === this.slot ? SK.CSS.gold : SK.CSS.bone); 218 }); 219 } 220 221 _key(e) { 222 if (this.done) return; 223 const k = e.key; 224 if (/^[a-zA-Z]$/.test(k)) { 225 this.letters[this.slot] = k.toUpperCase(); 226 this.slot = Math.min(2, this.slot + 1); 227 SK.synth.blip(); 228 } else if (k === 'ArrowRight') { this.slot = Math.min(2, this.slot + 1); } 229 else if (k === 'ArrowLeft') { this.slot = Math.max(0, this.slot - 1); } 230 else if (k === 'ArrowUp' || k === 'ArrowDown') { 231 const d = k === 'ArrowUp' ? 1 : -1; 232 const c = ((this.letters[this.slot].charCodeAt(0) - 65 + d) + 26) % 26; 233 this.letters[this.slot] = String.fromCharCode(65 + c); 234 SK.synth.blip(); 235 } else if (k === 'Backspace') { 236 this.slot = Math.max(0, this.slot - 1); 237 this.letters[this.slot] = 'A'; 238 } else if (k === 'Enter') { 239 this._submit(); 240 return; 241 } 242 this._paint(); 243 } 244 245 _submit() { 246 this.done = true; 247 const rows = SK.submitScore(SK.run, this.won, this.letters.join('')); 248 const mine = rows.find(r => r.name === this.letters.join('') && 249 r.score === SK.run.score); 250 SK.synth.shard(); 251 this.cameras.main.fade(220, 14, 11, 8); 252 this.time.delayedCall(230, () => this.scene.start('LeaderScene', { 253 from: 'TitleScene', highlightAt: mine ? mine.at : 0, 254 tab: SK.run.mode === 'daily' ? 'today' : 'all' 255 })); 256 } 257 258 cleanup() { 259 this.input.keyboard.off('keydown', this.onKey); 260 if (this.ambient) this.ambient.destroy(); 261 } 262};
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.