PageSourceSearch

https://the-torchbearer.netlify.app/src/scenes/LeaderScene.js?v=27Jul-TB8

js the-torchbearer.netlify.app collected 2026-10-03 11:30:41 UTC 8,465 bytes, 262 lines download raw bytes

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.