1// Shared sidebar rendering: prize pool, odds, weekly pot + high scores, 2// recent plays. Renders whichever panel elements exist on the page. 3 4// Cabinet display names, keyed by the internal game id used in routes, saved 5// scores and the API. The ids are deliberately stable â renaming them would 6// orphan every score already recorded against the old key â so this map is 7// the single place a player-facing name is decided. Order sets the order of 8// the high-score boards, and matches the cabinet order on the home page. 9window.GAME_NAMES = { 10 pacman: 'CHOMPER', 11 invaders: 'SWARM', 12 tetris: 'CASCADE', 13 snake: 'COIL', 14 breakout: 'RICOCHET', 15 frogger: 'HOPPER', 16 asteroids: 'DRIFT', 17 missiles: 'SALVO', 18 centipede: 'CRAWLER', 19 tempest: 'VORTEX', 20 qbert: 'ZIGGURAT', 21 lander: 'DESCENT', 22 climber: 'GIRDER', 23 fusion: 'FUSION', 24 roller: 'ROLLER', 25 pop: 'POP', 26 pool: 'BREAK', 27 tapcade: 'TAPCADE', 28 fizz: 'FIZZ', 29 // Not a roster cabinet â no daily board, no replay hook â but it is on the 30 // duel ledger, and the duel page, the arena and every duel row name their 31 // game from this map. gameicontest holds GAME_ICON to the same keys. 32 golf: 'GOLF', 33 // The same, and the second one: its runs are re-run on the server from 34 // their input recording (downhillreplay.js), so unlike golf it can rate. 35 downhillrush: 'DOWNHILL RUSH', 36}; 37 38// The id a cabinet is SAVED under -> the name its files and its URL are under. 39// 40// The two were the same string until the cabinet scripts were renamed off other 41// people's trademarks: public/cascade.js, never public/tetris.js. Anything that 42// builds a script URL out of a game id has to come through here â 43// homeattract.js loading an attract demo on the floor, arena.js loading a 44// cabinet into a duel â and both of them appended '.js' to the id directly, 45// which is how /tempest.js became a 404 on the arcade floor the moment the 46// files moved. 47// 48// Defined beside GAME_NAMES because it is derived from it, and this file is 49// already the single place a cabinet's player-facing name is decided. Every 50// page that needs it loads panels.js first; index.html and duel.html both do. 51window.GAME_SLUG = id => String((window.GAME_NAMES || {})[id] || id).toLowerCase(); 52 53// What each cabinet actually is, in words anyone recognises. The names above 54// are the arcade's own and say nothing about the game to someone who has not 55// played it yet â CASCADE could be anything. Shown where a cabinet is being 56// chosen rather than merely listed, so picking one is not guesswork. 57window.GAME_KINDS = { 58 pacman: 'maze muncher', 59 invaders: 'space shooter', 60 tetris: 'falling blocks', 61 snake: 'grow and dodge', 62 breakout: 'brick breaker', 63 frogger: 'cross the road', 64 asteroids: 'rock shooter', 65 missiles: 'city defence', 66 centipede: 'bug shooter', 67 tempest: 'tube shooter', 68 qbert: 'pyramid hopper', 69 lander: 'moon landing', 70 climber: 'girder climb', 71 fusion: 'tile merge', 72 pool: 'eight-ball pool', 73 // ROLLER and POP were missing, which the cabinet-page breadcrumb made 74 // visible: with no kind to show it fell back to the category, and the trail 75 // read ARCADE / ARCADE. The versus games are here for the same reason â they 76 // have cabinet pages too. 77 roller: 'lane bowler', 78 pop: 'bubble shooter', 79 tapcade: 'rhythm tapper', 80 fizz: 'soda fountain rush', 81 brawl: '1v1 fighter', 82 harbormaster: 'fishing tournament', 83 neonrivals: 'arcade racer', 84 dudasjump: 'endless climber', 85 ragingbull: 'endless runner', 86 golf: 'pixel golf', 87 downhillrush: 'endless skier', 88}; 89 90// What kind of game each cabinet is, in the words a player filters a library 91// by. GAME_KINDS above says what a cabinet IS in a sentence ("maze muncher"); 92// this says which shelf it sits on, and the two are different jobs â a filter 93// with fourteen categories is not a filter. 94// 95// Keyed by the same internal ids as everything else here, so the arcade floor 96// stamps a card by reading its own href rather than carrying a category typed 97// into the markup. A cabinet missing from this map is not hidden by any filter: 98// arcade.js treats "no category" as "shows under ALL", because a game vanishing 99// from the floor is a worse failure than a game appearing under the wrong tab. 100// 101// The versus games are keyed by route rather than by game id â they have no id 102// in GAME_LIST, because they have no daily board. 103window.GAME_CATS = {
104 pacman: 'arcade', 105 invaders: 'action', 106 tetris: 'puzzle', 107 snake: 'arcade', 108 breakout: 'arcade', 109 frogger: 'arcade', 110 asteroids: 'action', 111 missiles: 'action', 112 centipede: 'action', 113 tempest: 'action', 114 qbert: 'puzzle', 115 lander: 'arcade', 116 climber: 'arcade', 117 fusion: 'puzzle', 118 roller: 'arcade', 119 pop: 'puzzle', 120 pool: 'arcade', 121 tapcade: 'arcade', 122 fizz: 'arcade', 123 brawl: 'multiplayer', 124 // Its own shelf, and not for tidiness. The Harbormaster Cup is the one game 125 // on the floor whose results do NOT go to the daily stage board â it runs its 126 // own tournament with its own prizes. Filing it under ARCADE beside sixteen 127 // cabinets that all feed one board would say, in the one word a library card 128 // has, that it is the same kind of thing. It is not. 129 harbormaster: 'tournament', 130 neonrivals: 'multiplayer', 131 dudasjump: 'arcade', 132 ragingbull: 'arcade', 133 golf: 'arcade', 134 downhillrush: 'arcade', 135}; 136 137// The route each cabinet answers on, so a card can be matched back to its id. 138// The floor links to display-name routes (/chomper), not to ids (/pacman), and 139// the old ids still 301 to them â so a lookup that only knew ids matched 140// nothing on the page it was reading. 141window.GAME_ROUTES = { 142 chomper: 'pacman', swarm: 'invaders', cascade: 'tetris', coil: 'snake', 143 ricochet: 'breakout', hopper: 'frogger', drift: 'asteroids', salvo: 'missiles', 144 crawler: 'centipede', vortex: 'tempest', ziggurat: 'qbert', descent: 'lander', 145 girder: 'climber', fusion: 'fusion', roller: 'roller', pop: 'pop', pool: 'pool', 146 tapcade: 'tapcade', fizz: 'fizz', 147 brawl: 'brawl', harbormaster: 'harbormaster', neonrivals: 'neonrivals', 148 dudasjump: 'dudasjump', 149 ragingbull: 'ragingbull', 150 golf: 'golf', 151 downhillrush: 'downhillrush', 152}; 153 154// One glyph per cabinet, drawn rather than named. 155// 156// A duel row said CHOMPER, and CHOMPER is a word you have to already know. The 157// list is fourteen of those in a column, all set in the same weight, so telling 158// them apart was reading rather than seeing. A shape is recognised before it is 159// read, which is the entire job here. 160// 161// Inline SVG, in a map, the way tabbar.js does it: no files to fetch, no 162// sprite sheet to keep in step with this list, crisp at any size, and 163// currentColor means each one inherits whatever the row around it is doing â 164// dimmed on a settled duel, accented on your turn, without a second copy. 165// 166// Drawn for ~18px. That is small enough that a thin outline disappears, so 167// these are mostly solid: silhouettes, not line art. 168window.GAME_ICON = { 169 // A soda glass with a head of foam over the rim and a straw in it â the 170 // pour, which is the whole of FIZZ, drawn as a silhouette. 171 fizz: '<path d="M5.5 9.5h13l-1.8 13H7.3z"/>' 172 + '<path d="M4.8 9.8a3.2 3.2 0 0 1 3.5-3.5 3.7 3.7 0 0 1 7.4 0 3.2 3.2 0 0 1 3.5 3.5z" opacity=".6"/>' 173 + '<path d="M13.4 8.4 17.9 1l1.8 1.1-4.3 6.9z" opacity=".85"/>' 174 + '<circle cx="10.2" cy="17.5" r="1.3" fill="#fff" opacity=".9"/>' 175 + '<circle cx="13.6" cy="13.8" r="1" fill="#fff" opacity=".9"/>', 176 // A pine on the slope and two ski tracks carving past it. 177 downhillrush: '<path d="M16 1.5l5.5 7.5h-3.2l4.2 6.5h-13l4.2-6.5h-3.2z"/>' 178 + '<rect x="14.8" y="15" width="2.4" height="4"/>' 179 + '<path d="M1.5 12.5l8 8.5M5.5 11l7.2 7.6" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/>', 180 // three hit rings in a row with an orb dropping onto the middle one: the 181 // lanes and the tap, which is the whole game at a glance 182 // A flag on its pole, the ball at the foot of it. 183 golf: '<rect x="8" y="3" width="2" height="16" rx="1"/>' 184 + '<path d="M10 3.5h9l-3 3.5 3 3.5h-9z"/>' 185 + '<circle cx="5.5" cy="19.5" r="2.6"/>', 186 tapcade: '<circle cx="4.5" cy="18" r="3.4" opacity=".55"/>' 187 + '<circle cx="19.5" cy="18" r="3.4" opacity=".55"/>' 188 + '<circle cx="12" cy="18" r="3.6" fill="none" stroke="currentColor" stroke-width="2.2"/>' 189 + '<circle cx="12" cy="7.5" r="4"/>' 190 + '<circle cx="10.6" cy="6.1" r="1.2" fill="#fff" opacity=".9"/>', 191 // a cue laid across the corner of a table, with the black in the pocket's 192 // mouth â the two things that say "pool" without a single word 193 pool: '<path d="M3.2 20.1 15.4 7.9l.9.9L4.1 21z" opacity=".85"/>' 194 + '<path d="M16.8 6.5 19 4.3a1.4 1.4 0 0 1 2 2l-2.2 2.2z"/>' 195 + '<circle cx="7.5" cy="7.5" r="3.6"/>'
196 + '<circle cx="7.5" cy="7.5" r="1.3" fill="#fff" opacity=".9"/>', 197 // the ring board seen from the lane, over the lane itself 198 roller: '<circle cx="12" cy="8.5" r="7" opacity=".3"/>', 199 // three touching spheres: the smallest group that pops 200 pop: '<circle cx="8" cy="9" r="4.6"/><circle cx="16" cy="9" r="4.6" opacity=".55"/><circle cx="12" cy="16" r="4.6" opacity=".8"/>' 201 + '<circle cx="12" cy="8.5" r="4.4" opacity=".55"/>' 202 + '<circle cx="12" cy="8.5" r="1.9"/>' 203 + '<path d="M6.5 17.5h11l-2 4.5h-7z"/>', 204 // a wedge with a pellet in front of it 205 pacman: '<path d="M10 12 16.5 8.25A7.5 7.5 0 1 0 16.5 15.75Z"/>' 206 + '<circle cx="20.6" cy="12" r="1.7"/>', 207 // the descending silhouette: antennae, shoulders, two feet 208 invaders: '<path d="M7 4h2v2H7zM15 4h2v2h-2zM9 6h6v2H9zM6 8h12v3H6zM4 11h16v4H4z' 209 + 'M4 15h3v2H4zM9 15h2v2H9zM13 15h2v2h-2zM17 15h3v2h-3z"/>', 210 // an S-piece mid-fall, with the well's floor under it 211 tetris: '<path d="M12 3h9v8h-9zM3 11h9v8H3zM12 11h9v8h-9z"/>', 212 // a body that turns back on itself, with a head 213 snake: '<path d="M3 3h18v3.5H6.5V9H18v9.5H3V15h11.5v-2.5H3z"/>' 214 + '<path d="M17.5 20.5h3.5V9h-3.5z" opacity=".75"/>', 215 // bricks above, ball between, paddle below 216 breakout: '<path d="M4 4h5v3H4zM10 4h4v3h-4zM15 4h5v3h-5zM4 8h4v3H4zM9 8h6v3H9zM16 8h4v3h-4z"/>' 217 + '<circle cx="12" cy="14.5" r="1.8"/>' 218 + '<path d="M7 18h10v2.5H7z"/>', 219 // a frog head-on: two eyes over a wide mouth, legs out 220 frogger: '<path d="M2 6.5h5v2.6H2zM9.5 6.5h5v2.6h-5zM17 6.5h5v2.6h-5z' 221 + 'M2 14.5h5v2.6H2zM9.5 14.5h5v2.6h-5zM17 14.5h5v2.6h-5z" opacity=".55"/>' 222 + '<path d="M12 1.5l5 5.5h-3v10h3l-5 5.5-5-5.5h3V7H7z"/>', 223 // an irregular rock, and the ship that is out-numbered by it 224 asteroids: '<path d="M10 2.5l6.5 1.5 4 5-2 6-6 3.5-6.5-2.5-3.5-6 2-5.5z"/>' 225 + '<path d="M18.5 16.5l3.5 1 .5 3.5-3.5 1-2.5-2 .5-3z" opacity=".7"/>', 226 // two trails arcing up to their intercepts 227 // The burst, not the rocket. Three rockets with three trails was nine shapes 228 // in eighteen pixels and read as a row of little figures; one impact reads at 229 // any size and shares its silhouette with nothing else here. 230 missiles: '<path d="M2 22l7.5-8 2.6 2.4L4.6 22z" opacity=".8"/>' 231 + '<path d="M13.00 1.20L14.02 5.46L17.35 2.61L15.67 6.66L20.04 6.31L16.30 8.60L20.04 10.89L15.67 10.54L17.35 14.59L14.02 11.74L13.00 16.00L11.98 11.74L8.65 14.59L10.33 10.54L5.96 10.89L9.70 8.60L5.96 6.31L10.33 6.66L8.65 2.61L11.98 5.46Z"/>', 232 // a segmented body coming down the column 233 centipede: '<circle cx="5" cy="7" r="2.5"/><circle cx="10.5" cy="7" r="2.2"/>' 234 + '<circle cx="15.5" cy="8.5" r="2.2"/><circle cx="19" cy="12" r="2.2"/>' 235 + '<circle cx="16" cy="15.5" r="2.2"/><circle cx="10.5" cy="16.5" r="2.2"/>' 236 + '<circle cx="5" cy="16.5" r="2.5"/>', 237 // the tube, receding 238 tempest: '<path d="M12 1.5 22.5 7.7v12.6L12 22.5 1.5 20.3V7.7z" fill="none" stroke="currentColor" stroke-width="2.6"/>'
239 + '<path d="M12 8.5l4.6 2.7v3.6L12 17.5l-4.6-2.7v-3.6z"/>', 240 // stepped cubes, seen from the corner 241 qbert: '<path d="M9 3h6v4H9zM6 8h12v4H6zM3 13h18v4H3z"/>' 242 + '<path d="M3 18.5h18v2.5H3z" opacity=".6"/>', 243 // a lander on its legs, with the burn under it 244 lander: '<path d="M5.5 5.5a6.5 6.5 0 0 1 13 0V11h-13z"/>' 245 + '<path d="M4 11.5h16v2.6H4z"/>' 246 + '<path d="M5 14.5l-3 7h3l2.2-7zM19 14.5l3 7h-3l-2.2-7z"/>' 247 + '<path d="M10 15h4l-2 6.5z" opacity=".8"/>', 248 // girders and the ladder between them 249 climber: '<path d="M6.5 2h2.8v17H6.5zM14.7 2h2.8v17h-2.8z"/>' 250 + '<path d="M6.5 5h11v2.4h-11zM6.5 10.3h11v2.4h-11zM6.5 15.6h11v2.4h-11z"/>' 251 + '<path d="M1.5 19.5h21v2.6h-21z" opacity=".75"/>', 252 // two tiles becoming one 253 fusion: '<path d="M2 5h8v8H2z" opacity=".65"/><path d="M11 5h8v8h-8z" opacity=".65"/>' 254 + '<path d="M6 14h12v7H6z"/>', 255}; 256 257// One cabinet's glyph, sized. Returns markup rather than an element because 258// every caller here is building a template string, and aria-hidden because the 259// name is always right beside it â a screen reader announcing "image" before 260// "CHOMPER" is noise, not information. 261window.gameIcon = (game, px = 18) => { 262 const paths = window.GAME_ICON[game]; 263 if (!paths) return ''; 264 return `<svg class="gi" viewBox="0 0 24 24" width="${px}" height="${px}" ` 265 + `fill="currentColor" aria-hidden="true" focusable="false">${paths}</svg>`; 266}; 267 268// Profile names are the first attacker-controlled text this site displays, and 269// every panel here renders through innerHTML. Escaping is therefore not 270// optional: an unescaped nickname is script running on the arcade floor. 271window.esc = v => String(v == null ? '' : v) 272 .replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>') 273 .replace(/"/g, '"').replace(/'/g, '''); 274 275// every wallet shown on the site links to its player page, labelled with its 276// nickname when it has one 277window.playerLink = (addr, profile) => { 278 const label = profile && profile.name ? window.esc(profile.name) : window.esc(addr); 279 return `<a class="pl-link" href="/player?w=${encodeURIComponent(addr)}">${label}</a>`; 280}; 281 282window.loadArcadeConfig = async function () { 283 const $ = id => document.getElementById(id); 284 const fmt = n => Number(n).toLocaleString(undefined, { maximumFractionDigits: 2 }); 285 // What a play costs, said the way a player thinks about it. Pegged to a 286 // dollar the SOL figure is a moving six-decimal number â the true amount 287 // signed for, but a poor headline. Falls back to SOL when the price is 288 // pinned to a constant, and says so plainly when there is no price at all. 289 const costLabel = c => (c.freeToPlay ? 'FREE' 290 : c.coinPriceUsd != null 291 ? '$' + c.coinPriceUsd 292 : (c.coinPriceSol ? c.coinPriceSol + ' SOL' : 'unavailable')); 293 294 // Cabinet pages set PAGE_GAME; the arcade floor does not. On a cabinet the 295 // score board and the play feed narrow to that machine â the pool, odds and 296 // weekly pot stay arcade-wide, because they are arcade-wide. 297 const PAGE = (window.PAGE_GAME || document.documentElement.dataset.game || null); 298 const res = await fetch('/api/config' + (PAGE ? '?game=' + encodeURIComponent(PAGE) : '')); 299 const config = await res.json(); 300 301 // The cabinet cards used to be MARKED UP as "clear level 1 for the prize 302 // roll" and rewritten here when the arcade turned out to be free. That put 303 // the claim in the served HTML â which is what view-source, a reader, a 304 // crawler and anyone with JS off actually get â and left the real copy 305 // existing only if a script ran. The promise being briefly true on screen 306 // was the smaller half of that problem. 307 // 308 // So it is the other way round now. The markup says what is true, and the 309 // gate condition rides along in data-gate; if the arcade is ever put back on 310 // coins, THAT is the case this composes at runtime. A claim nobody is making 311 // cannot be found in a page nobody rendered. 312 // Under FREE PLAY nothing touches a card's caption here. The caption is the 313 // game's category â POP / ARCADE, CASCADE / PUZZLE â written by arcade.js 314 // from the same registry the filter chips read, and what clearing a stage is 315 // worth is stated once in the library's header instead of seventeen times in 316 // 11px grey. 317 // 318 // This block therefore only composes the COIN arcade's wording, from the gate 319 // condition each card still carries in data-gate. It runs after arcade.js 320 // (that one is synchronous, this waits on /api/config), so leaving it 321 // unconditional would have overwritten every category the moment the config 322 // landed â which is exactly what it used to do to the homepage rail. 323 if (!config.freeToPlay) { 324 document.querySelectorAll('.cab-card-gate').forEach(el => { 325 // A versus game has no gate and no points, so touching its caption would 326 // replace something true with something that is not. data-nogate says the 327 // same about a card that is neither: the Harbormaster derby is its own 328 // tournament with its own board, so a gate line is not a harmless generic
329 // caption there â it is a false statement about where its results go. 330 if (el.closest('.cab-card-vs') || 'nogate' in el.dataset) return; 331 if (!el.dataset.gate) return; 332 el.textContent = el.dataset.gate + ' for the prize roll'; 333 }); 334 } 335 336 // The overlay above the play button was the same thing on every cabinet 337 // page: "CLEAR WAVE 1 TO UNLOCK THE PRIZE ROLL", shipped in the HTML and 338 // overwritten here. Fifteen pages of it, which is fourteen more than the 339 // note above accounted for. Markup-first now, so this only has to build the 340 // paid line back if the arcade ever runs on coins again. 341 if (!config.freeToPlay) { 342 // The whole page is markup-first now â it says what the free arcade does, 343 // because that is the arcade that is running. This branch composes the coin 344 // arcade back if one is ever switched on again: the gate line, the offer 345 // above the button, the button itself, and the class that puts the coin 346 // slot back on it. 347 document.body.classList.add('paid-arcade'); 348 const os = $('overlay-sub'); 349 if (os && os.dataset.gate) os.textContent = os.dataset.gate + ' TO UNLOCK THE PRIZE ROLL'; 350 const ot = $('overlay-title'); 351 if (ot) ot.textContent = '1 COIN TO PLAY'; 352 const tag = $('cab-mode'); 353 if (tag) tag.textContent = costLabel(config) + ' a play'; 354 const play = $('play-btn'); 355 if (play) { 356 const slot = play.querySelector('.coin-slot'); 357 play.innerHTML = ''; 358 if (slot) play.appendChild(slot); 359 play.appendChild(document.createTextNode('INSERT COIN')); 360 } 361 } 362 363 if (config.freeToPlay) { 364 // Nothing to rewrite. The cabinet pages ship saying FREE PLAY / PLAY FREE 365 // in the markup, the same way overlay-sub and the floor's cards already 366 // did â so a crawler, a reader, and anybody with a slow script sees the 367 // arcade that is actually running rather than a coin arcade that briefly 368 // corrects itself. The paid wording is composed in the branch above. 369 // Featured cabinet of the week: badge its card on the floor and call it out 370 // under the "Pick your cabinet" line. Double board points, so it steers 371 // today's play toward it. No-ops on a cabinet page (no floor to mark). 372 if (config.featured) { 373 const card = document.querySelector(`.cab-card[href="/${config.featured}"]`); 374 if (card && !card.querySelector('.cab-featured-badge')) { 375 card.classList.add('cab-featured'); 376 const badge = document.createElement('span'); 377 badge.className = 'cab-featured-badge'; 378 badge.textContent = 'FEATURED'; 379 card.appendChild(badge); 380 } 381 // The note that used to hang off the ARCADE title â "this week POP pays 382 // double" â moved onto the badge's tooltip. A page title is a name, and 383 // a name with a sentence stapled to it stops being scannable; the badge 384 // is already on the card, in the one place somebody choosing a cabinet 385 // is looking, and it is the thing they would click. 386 if (card) { 387 const name = window.esc(window.GAME_NAMES[config.featured] || config.featured); 388 card.setAttribute('title', `Featured this week â ${name} pays double`); 389 } 390 } 391 } 392 393 if ($('hero-pool-n')) { 394 // THIS ELEMENT NO LONGER EXISTS ON ANY PAGE â the arcade floor's prize 395 // headline went with the wind-down, and the guard above means this block 396 // is already a no-op. Kept as a guard rather than deleted because the id 397 // is the only thing that would bring it back, and left EMPTY rather than 398 // computing a figure: the original note here was right that a pool total 399 // reads as a promotion's prize, and prize totals are what sweepstakes law 400 // measures. If the element ever returns, it returns blank rather than 401 // quietly republishing the treasury. 402 $('hero-pool-n').textContent = ''; 403 // Signed out this shows the allowance everyone gets; wallet.js replaces it 404 // with the live count as soon as it knows who is asking. "FREE" was true 405 // but told a returning player nothing they did not already know. 406 if (config.daily) { 407 $('hero-cost-n').textContent = config.daily.playsPerDay; 408 if ($('hero-cost-x')) $('hero-cost-x').textContent = 'FREE PLAYS A DAY';
409 } else { 410 $('hero-cost-n').textContent = costLabel(config); 411 if ($('hero-cost-x')) $('hero-cost-x').textContent = 'PER PLAY'; 412 } 413 } 414 415 // THE POOL BALANCE IS NOT SHOWN. This printed "N tokens today" from the 416 // day's budget, or the whole pool balance when there was no board â a 417 // treasury figure on the arcade floor. Nothing is paid out of it, so the 418 // panel says what the arcade has actually done instead. 419 if ($('pool-balance')) { 420 $('pool-balance').textContent = `${fmt(config.totals.plays)} plays all-time`; 421 $('burned-total').textContent = ''; 422 } 423 424 const hs = config.weekly || {}; 425 426 // The glory panel belongs to a cabinet, not to the arcade floor. Fourteen 427 // boards stacked on the home page was several screens of scrolling before a 428 // visitor reached anything else, so the floor no longer carries the panel at 429 // all and this only renders on a cabinet page. 430 if ($('hiscores') && PAGE) { 431 const list = hs[PAGE]; 432 const board = list && list.length 433 ? `<ol class="hs-list">${list.map(s => `<li><b>${window.playerLink(s.player, s.profile)}</b><span>${fmt(s.score)}</span></li>`).join('')}</ol>` 434 : '<p class="muted hs-empty">No scores yet on this cabinet</p>'; 435 $('hiscores').innerHTML = board 436 + '<p class="muted hs-reset">Bragging rights — the daily board ranks on stages cleared</p>'; 437 } 438 439 if ($('wins-list')) { 440 // the server already scopes this to PAGE; filter again so an older server 441 // that ignores ?game= degrades to a short list rather than a wrong one 442 let plays = (config.recentPlays || []).slice().reverse(); 443 if (PAGE) plays = plays.filter(p => p.game === PAGE); 444 445 // "won 0" on every row was the old model talking. Nothing is won per play 446 // any more, so the row now carries what a run is actually judged on â 447 // stages first, because that is what the board pays, then score. 448 const ago = ts => { 449 const m = Math.floor((Date.now() - ts) / 60000); 450 if (m < 1) return 'now'; 451 if (m < 60) return m + 'm'; 452 const h = Math.floor(m / 60); 453 return h < 24 ? h + 'h' : Math.floor(h / 24) + 'd'; 454 }; 455 456 // A run the boards refused shows a dash rather than its number. This feed 457 // is the most-read surface on the site, and it was the last one still 458 // printing a score at face value after the daily board, the week's rows 459 // and the all-time table had all declined to carry it â which is how a 460 // wallet banking nothing still appeared to have posted two million. 461 const rows = plays.map(p => ` 462 <tr> 463 <td class="rp-who">${window.playerLink(p.player, p.profile)}</td> 464 ${PAGE ? '' : `<td class="rp-game">${window.esc(window.GAME_NAMES[p.game] || p.game)}</td>`} 465 <td class="dl-num rp-stages">${p.depth > 0 ? p.depth : '—'}</td> 466 <td class="dl-num rp-score">${p.counted === false ? '—' : fmt(p.score)}</td> 467 <td class="dl-num rp-ago">${ago(p.ts)}</td> 468 </tr>`).join(''); 469 470 $('wins-list').innerHTML = plays.length 471 ? `<table class="dl-table rp-table"> 472 <tr><th>PLAYER</th>${PAGE ? '' : '<th>CABINET</th>'} 473 <th class="dl-num">STAGES</th><th class="dl-num">SCORE</th><th class="dl-num"></th></tr> 474 ${rows} 475 </table>` 476 : `<p class="muted">${PAGE ? 'No plays yet on this cabinet' : 'No plays yet'}</p>`; 477 } 478 479 return config; 480};
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.