Sigmally Fixes V2

Easily 3X your FPS on Sigmally.com + many bug fixes + great for multiboxing + supports SigMod

当前为 2024-06-13 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Sigmally Fixes V2
  3. // @version 2.2.9
  4. // @description Easily 3X your FPS on Sigmally.com + many bug fixes + great for multiboxing + supports SigMod
  5. // @author 8y8x
  6. // @match https://*.sigmally.com/*
  7. // @license MIT
  8. // @grant none
  9. // @namespace https://8y8x.dev/sigmally-fixes
  10. // @compatible chrome Recommended for all users, works perfectly out of the box
  11. // ==/UserScript==
  12.  
  13. // @ts-check
  14. /* eslint
  15. camelcase: 'error',
  16. comma-dangle: ['error', 'always-multiline'],
  17. indent: ['error', 'tab', { SwitchCase: 1 }],
  18. max-len: ['error', { code: 120 }],
  19. no-console: ['error', { allow: ['warn', 'error'] }],
  20. no-trailing-spaces: 'error',
  21. quotes: ['error', 'single'],
  22. semi: 'error',
  23. */ // a light eslint configuration that doesn't compromise code quality
  24. 'use strict';
  25.  
  26. (async () => {
  27. ////////////////////////////////
  28. // Define Auxiliary Functions //
  29. ////////////////////////////////
  30. const aux = (() => {
  31. const aux = {};
  32.  
  33. /**
  34. * @template T
  35. * @param {T} x
  36. * @param {string} err should be readable and easily translatable
  37. * @returns {T extends (null | undefined | false | 0) ? never : T}
  38. */
  39. aux.require = (x, err) => {
  40. if (!x) {
  41. err = '[Sigmally Fixes]: ' + err;
  42. prompt(err, err); // use prompt, so people can paste the error message into google translate
  43. throw err;
  44. }
  45.  
  46. return /** @type {any} */ (x);
  47. };
  48.  
  49. /**
  50. * consistent exponential easing relative to 60fps.
  51. * for example, with a factor of 2, o=0, n=1:
  52. * - at 60fps, 0.5 is returned.
  53. * - at 30fps (after 2 frames), 0.75 is returned.
  54. * - at 15fps (after 4 frames), 0.875 is returned.
  55. * - at 120fps, 0.292893 is returned. if you called this again with o=0.292893, n=1, you would get 0.5.
  56. *
  57. * @param {number} o
  58. * @param {number} n
  59. * @param {number} factor
  60. * @param {number} dt in seconds
  61. */
  62. aux.exponentialEase = (o, n, factor, dt) => {
  63. return o + (n - o) * (1 - (1 - 1 / factor)**(60 * dt));
  64. };
  65.  
  66. /**
  67. * @param {string} hex
  68. * @returns {[number, number, number]}
  69. */
  70. aux.hex2rgb = hex => {
  71. if (hex.length === 4) {
  72. return [
  73. (parseInt(hex[1], 16) || 0) / 15,
  74. (parseInt(hex[2], 16) || 0) / 15,
  75. (parseInt(hex[3], 16) || 0) / 15,
  76. ];
  77. } else if (hex.length === 7) {
  78. return [
  79. (parseInt(hex.slice(1, 3), 16) || 0) / 255,
  80. (parseInt(hex.slice(3, 5), 16) || 0) / 255,
  81. (parseInt(hex.slice(5, 7), 16) || 0) / 255,
  82. ];
  83. } else {
  84. return [0, 0, 0];
  85. }
  86. };
  87.  
  88. /** @param {[number, number, number]} rgb */
  89. aux.rgb2hex = rgb => {
  90. return [
  91. '#',
  92. Math.floor(rgb[0] * 255).toString(16).padStart(2, '0'),
  93. Math.floor(rgb[1] * 255).toString(16).padStart(2, '0'),
  94. Math.floor(rgb[2] * 255).toString(16).padStart(2, '0'),
  95. ].join('');
  96. };
  97.  
  98. /** @param {string} name */
  99. aux.parseName = name => {
  100. const match = name.match(/^\{.*?\}(.*)$/);
  101. if (match)
  102. name = match[1];
  103.  
  104. return name || 'An unnamed cell';
  105. };
  106.  
  107. /** @param {string} skin */
  108. aux.parseSkin = skin => {
  109. if (!skin) return skin;
  110. skin = skin.replace('1%', '').replace('2%', '').replace('3%', '');
  111. return '/static/skins/' + skin + '.png';
  112. };
  113.  
  114. /** @type {object | undefined} */
  115. aux.sigmod = undefined;
  116. setInterval(() => {
  117. // @ts-expect-error
  118. aux.sigmod = window.sigmod?.settings;
  119. }, 50);
  120.  
  121. /**
  122. * @param {string} selector
  123. * @param {boolean} value
  124. */
  125. aux.setting = (selector, value) => {
  126. /** @type {HTMLInputElement | null} */
  127. const el = document.querySelector(selector);
  128. return el ? el.checked : value;
  129. };
  130.  
  131. /**
  132. * Only changes sometimes, like when your skin is updated
  133. * @type {object | undefined}
  134. */
  135. aux.settings = undefined;
  136. function settings() {
  137. try {
  138. aux.settings = JSON.parse(localStorage.getItem('settings') ?? '');
  139. } catch (_) {}
  140. }
  141.  
  142. settings();
  143. setInterval(settings, 50);
  144. // apply saved gamemode because sigmally fixes connects before the main game even loads
  145. if (aux.settings?.gamemode) {
  146. /** @type {HTMLSelectElement | null} */
  147. const gamemode = document.querySelector('select#gamemode');
  148. if (gamemode)
  149. gamemode.value = aux.settings.gamemode;
  150. }
  151.  
  152. const trimCtx = aux.require(
  153. document.createElement('canvas').getContext('2d'),
  154. 'Unable to get 2D context for text utilities. This is probably your browser being dumb, maybe reload ' +
  155. 'the page?',
  156. );
  157. trimCtx.font = '20px Ubuntu';
  158. /**
  159. * trims text to a max of 250px at 20px font, same as vanilla sigmally
  160. * @param {string} text
  161. */
  162. aux.trim = text => {
  163. while (trimCtx.measureText(text).width > 250)
  164. text = text.slice(0, -1);
  165.  
  166. return text;
  167. };
  168.  
  169. /*
  170. If you have Sigmally open in two tabs and you're playing with an account, one has an outdated token while
  171. the other has the latest one. This causes problems because the tab with the old token does not work properly
  172. during the game (skin, XP) To fix this, the latest token is sent to the previously opened tab. This way you
  173. can collect XP in both tabs and use your selected skin.
  174. @czrsd
  175. */
  176. /** @type {{ token: string, updated: number } | undefined} */
  177. aux.token = undefined;
  178. const tokenChannel = new BroadcastChannel('sigfix-token');
  179. tokenChannel.addEventListener('message', msg => {
  180. /** @type {{ token: string, updated: number }} */
  181. const token = msg.data;
  182. if (!aux.token || aux.token.updated < token.updated)
  183. aux.token = token;
  184. });
  185.  
  186. /** @type {object | undefined} */
  187. aux.userData = undefined;
  188. // this is the best method i've found to get the userData object, since game.js uses strict mode
  189. Object.defineProperty(window, 'fetch', {
  190. value: new Proxy(fetch, {
  191. apply: (target, thisArg, args) => {
  192. let url = args[0];
  193. const data = args[1];
  194. if (typeof url === 'string') {
  195. // game.js doesn't think we're connected to a server, we default to eu0 because that's the
  196. // default everywhere else
  197. if (url.includes('/userdata/')) url = url.replace('///', '//eu0.sigmally.com/server/');
  198.  
  199. // patch the current token in the url and body of the request
  200. if (aux.token) {
  201. // 128 hex characters surrounded by non-hex characters (lookahead and lookbehind)
  202. const tokenTest = /(?<![0-9a-fA-F])[0-9a-fA-F]{128}(?![0-9a-fA-F])/g;
  203. url = url.replaceAll(tokenTest, aux.token.token);
  204. if (typeof data?.body === 'string')
  205. data.body = data.body.replaceAll(tokenTest, aux.token.token);
  206. }
  207.  
  208. args[0] = url;
  209. args[1] = data;
  210. }
  211.  
  212. return target.apply(thisArg, args).then(res => new Proxy(res, {
  213. get: (target, prop, _receiver) => {
  214. if (prop !== 'json') {
  215. const val = target[prop];
  216. if (typeof val === 'function')
  217. return val.bind(target);
  218. else
  219. return val;
  220. }
  221.  
  222. return () => target.json().then(obj => {
  223. if (obj?.body?.user) {
  224. aux.userData = obj.body.user;
  225. // NaN if invalid / undefined
  226. let updated = Number(new Date(aux.userData.updateTime));
  227. if (Number.isNaN(updated))
  228. updated = Date.now();
  229.  
  230. if (!aux.token || updated >= aux.token.updated) {
  231. aux.token = { token: aux.userData.token, updated };
  232. tokenChannel.postMessage(aux.token);
  233. }
  234. }
  235.  
  236. return obj;
  237. });
  238. },
  239. }));
  240. },
  241. }),
  242. });
  243.  
  244. /** @param {number} ms */
  245. aux.wait = ms => new Promise(resolve => setTimeout(resolve, ms));
  246.  
  247. return aux;
  248. })();
  249.  
  250.  
  251.  
  252. ////////////////////////
  253. // Destroy Old Client //
  254. ////////////////////////
  255. const destructor = await (async () => {
  256. // #1 : kill the rendering process
  257. const oldRQA = requestAnimationFrame;
  258. window.requestAnimationFrame = function(fn) {
  259. try {
  260. throw new Error();
  261. } catch (err) {
  262. // prevent drawing the game, but do NOT prevent saving settings (which is called on RQA)
  263. if (!err.stack.includes('/game.js') || err.stack.includes('HTML'))
  264. return oldRQA(fn);
  265. }
  266.  
  267. return -1;
  268. };
  269.  
  270. // #2 : kill access to using a WebSocket
  271. const realWebSocket = WebSocket;
  272. Object.defineProperty(window, 'WebSocket', new Proxy(WebSocket, {
  273. construct(_target, argArray, _newTarget) {
  274. if (argArray[0]?.includes('sigmally.com')) {
  275. throw new Error('Nope :) - hooked by Sigmally Fixes');
  276. }
  277.  
  278. // @ts-expect-error
  279. return new oldWS(...argArray);
  280. },
  281. }));
  282.  
  283. /** @type {WeakSet<WebSocket>} */
  284. const safeWebSockets = new WeakSet();
  285. let realWsSend = WebSocket.prototype.send;
  286. WebSocket.prototype.send = function() {
  287. if (!safeWebSockets.has(this) && this.url.includes('sigmally.com')) {
  288. this.onclose = null;
  289. this.close();
  290. throw new Error('Nope :) - hooked by Sigmally Fixes');
  291. }
  292.  
  293. return realWsSend.apply(this, arguments);
  294. };
  295.  
  296. // #3 : prevent keys from being registered by the game
  297. setInterval(() => {
  298. onkeydown = null;
  299. onkeyup = null;
  300. }, 50);
  301.  
  302. return { realWebSocket, safeWebSockets };
  303. })();
  304.  
  305.  
  306.  
  307. /////////////////////
  308. // Prepare Game UI //
  309. /////////////////////
  310. const ui = (() => {
  311. const ui = {};
  312.  
  313. (() => {
  314. const title = document.querySelector('#title');
  315. if (!title) return;
  316.  
  317. const watermark = document.createElement('span');
  318. watermark.innerHTML = '<a href="https://greasyfork.org/scripts/483587/versions" target="_blank">Sigmally Fixes 2.2.9</a> by yx';
  319. title.insertAdjacentElement('afterend', watermark);
  320. })();
  321.  
  322. ui.game = (() => {
  323. const game = {};
  324.  
  325. const newCanvas = document.createElement('canvas');
  326. newCanvas.style.cssText = `background: #003; width: 100vw; height: 100vh; position: fixed; top: 0; left: 0;
  327. z-index: 1;`;
  328. game.canvas = newCanvas;
  329. (document.querySelector('body div') ?? document.body).appendChild(newCanvas);
  330.  
  331. /** @type {HTMLCanvasElement | null} */
  332. const oldCanvas = document.querySelector('canvas#canvas');
  333. if (oldCanvas) {
  334. // leave the old canvas so the old client can actually run
  335. oldCanvas.style.display = 'none';
  336.  
  337. // forward macro inputs from the canvas to the old one - this is for sigmod mouse button controls
  338. newCanvas.addEventListener('mousedown', e => oldCanvas.dispatchEvent(new MouseEvent('mousedown', e)));
  339. newCanvas.addEventListener('mouseup', e => oldCanvas.dispatchEvent(new MouseEvent('mouseup', e)));
  340. // forward mouse movements from the old canvas to the window - this is for sigmod keybinds that move
  341. // the mouse
  342. oldCanvas.addEventListener('mousemove', e => dispatchEvent(new MouseEvent('mousemove', e)));
  343. }
  344.  
  345. const gl = aux.require(
  346. newCanvas.getContext('webgl2'),
  347. 'Couldn\'t get WebGL2 context. This is probably your browser being dumb and it should resolve itself ' +
  348. 'after a while.',
  349. );
  350.  
  351. game.gl = gl;
  352.  
  353. // indicate that we will restore the context
  354. newCanvas.addEventListener('webglcontextlost', e => {
  355. e.preventDefault(); // signal that we want to restore the context
  356. // cleanup old caches (after render), as we can't do this within initWebGL()
  357. render.resetTextCache();
  358. render.resetTextureCache();
  359. });
  360. newCanvas.addEventListener('webglcontextrestored', () => initWebGL());
  361.  
  362. game.viewportScale = 1;
  363. function resize() {
  364. newCanvas.width = Math.floor(innerWidth * devicePixelRatio);
  365. newCanvas.height = Math.floor(innerHeight * devicePixelRatio);
  366. game.viewportScale = Math.max(innerWidth / 1920, innerHeight / 1080);
  367. game.gl.viewport(0, 0, newCanvas.width, newCanvas.height);
  368. }
  369.  
  370. addEventListener('resize', resize);
  371. resize();
  372.  
  373. return game;
  374. })();
  375.  
  376. ui.stats = (() => {
  377. const container = document.createElement('div');
  378. container.style.cssText = 'position: fixed; top: 10px; left: 10px; width: 400px; height: fit-content; \
  379. user-select: none; z-index: 2; transform-origin: top left;';
  380. document.body.appendChild(container);
  381.  
  382. const score = document.createElement('div');
  383. score.style.cssText = 'font-family: Ubuntu; font-size: 30px; color: #fff; line-height: 1.0;';
  384. container.appendChild(score);
  385.  
  386. const measures = document.createElement('div');
  387. measures.style.cssText = 'font-family: Ubuntu; font-size: 20px; color: #fff; line-height: 1.1;';
  388. container.appendChild(measures);
  389.  
  390. const misc = document.createElement('div');
  391. // white-space: pre; allows using \r\n to insert line breaks
  392. misc.style.cssText = 'font-family: Ubuntu; font-size: 14px; color: #fff; white-space: pre; \
  393. line-height: 1.1; opacity: 0.5;';
  394. container.appendChild(misc);
  395.  
  396. /** @param {object} statData */
  397. function update(statData) {
  398. let uptime;
  399. if (statData.uptime < 60) {
  400. uptime = '<1min';
  401. } else {
  402. uptime = Math.floor(statData.uptime / 60 % 60) + 'min';
  403. if (statData.uptime >= 60 * 60)
  404. uptime = Math.floor(statData.uptime / 60 / 60 % 24) + 'hr ' + uptime;
  405. if (statData.uptime >= 24 * 60 * 60)
  406. uptime = Math.floor(statData.uptime / 24 / 60 / 60 % 60) + 'd ' + uptime;
  407. }
  408.  
  409. misc.textContent = [
  410. `${statData.name} (${statData.mode})`,
  411. `${statData.playersTotal} / ${statData.playersLimit} players`,
  412. `${statData.playersAlive} playing`,
  413. `${statData.playersSpect} spectating`,
  414. `${(statData.update * 2.5).toFixed(1)}% load @ ${uptime}`,
  415. ].join('\r\n');
  416. }
  417.  
  418. function matchTheme() {
  419. let color = aux.setting('input#darkTheme', true) ? '#fff' : '#000';
  420. score.style.color = color;
  421. measures.style.color = color;
  422. misc.style.color = color;
  423. }
  424.  
  425. matchTheme();
  426.  
  427. return { container, score, measures, misc, update, matchTheme };
  428. })();
  429.  
  430. ui.leaderboard = (() => {
  431. const container = document.createElement('div');
  432. container.style.cssText = 'position: fixed; top: 10px; right: 10px; width: 200px; height: fit-content; \
  433. user-select: none; z-index: 2; background: #0006; padding: 15px 5px; transform-origin: top right; \
  434. display: none;';
  435. document.body.appendChild(container);
  436.  
  437. const title = document.createElement('div');
  438. title.style.cssText = 'font-family: Ubuntu; font-size: 30px; color: #fff; text-align: center; width: 100%;';
  439. title.textContent = 'Leaderboard';
  440. container.appendChild(title);
  441.  
  442. const linesContainer = document.createElement('div');
  443. linesContainer.style.cssText = 'font-family: Ubuntu; font-size: 20px; line-height: 1.2; width: 100%; \
  444. height: fit-content; text-align: center; white-space: pre; overflow: hidden;';
  445. container.appendChild(linesContainer);
  446.  
  447. const lines = [];
  448. for (let i = 0; i < 11; ++i) {
  449. const line = document.createElement('div');
  450. line.style.display = 'none';
  451. linesContainer.appendChild(line);
  452. lines.push(line);
  453. }
  454.  
  455. function update() {
  456. world.leaderboard.forEach((entry, i) => {
  457. const line = lines[i];
  458. if (!line) return;
  459.  
  460. line.style.display = 'block';
  461. line.textContent = `${entry.place ?? i + 1}. ${entry.name}`;
  462. if (entry.me)
  463. line.style.color = '#faa';
  464. else if (entry.sub)
  465. line.style.color = '#ffc826';
  466. else
  467. line.style.color = '#fff';
  468. });
  469.  
  470. for (let i = world.leaderboard.length; i < lines.length; ++i)
  471. lines[i].style.display = 'none';
  472. }
  473.  
  474. return { container, title, linesContainer, lines, update };
  475. })();
  476.  
  477. /** @type {HTMLElement} */
  478. const mainMenu = aux.require(
  479. document.querySelector('#__line1')?.parentElement,
  480. 'Can\'t find the main menu UI. Try reloading the page?',
  481. );
  482.  
  483. /** @type {HTMLElement} */
  484. const statsContainer = aux.require(
  485. document.querySelector('#__line2'),
  486. 'Can\'t find the death screen UI. Try reloading the page?',
  487. );
  488.  
  489. /** @type {HTMLElement} */
  490. const continueButton = aux.require(
  491. document.querySelector('#continue_button'),
  492. 'Can\'t find the continue button (on death). Try reloading the page?',
  493. );
  494.  
  495. /** @type {HTMLElement | null} */
  496. const menuLinks = document.querySelector('#menu-links');
  497. /** @type {HTMLElement | null} */
  498. const overlay = document.querySelector('#overlays');
  499.  
  500. // sigmod uses this to detect if the menu is closed or not, otherwise this is unnecessary
  501. /** @type {HTMLElement | null} */
  502. const menuWrapper = document.querySelector('#menu-wrapper');
  503.  
  504. let escOverlayVisible = true;
  505. /**
  506. * @param {boolean} [show]
  507. */
  508. ui.toggleEscOverlay = show => {
  509. escOverlayVisible = show ?? !escOverlayVisible;
  510. if (escOverlayVisible) {
  511. mainMenu.style.display = '';
  512. if (overlay) overlay.style.display = '';
  513. if (menuLinks) menuLinks.style.display = '';
  514. if (menuWrapper) menuWrapper.style.display = '';
  515.  
  516. ui.deathScreen.hide();
  517. } else {
  518. mainMenu.style.display = 'none';
  519. if (overlay) overlay.style.display = 'none';
  520. if (menuLinks) menuLinks.style.display = 'none';
  521. if (menuWrapper) menuWrapper.style.display = 'none';
  522. }
  523. };
  524.  
  525. ui.escOverlayVisible = () => escOverlayVisible;
  526.  
  527. ui.deathScreen = (() => {
  528. const deathScreen = {};
  529.  
  530. continueButton.addEventListener('click', () => {
  531. ui.toggleEscOverlay(true);
  532. });
  533.  
  534. // i'm not gonna buy a boost to try and figure out how this thing works
  535. /** @type {HTMLElement | null} */
  536. const bonus = document.querySelector('#menu__bonus');
  537. if (bonus) bonus.style.display = 'none';
  538.  
  539. /**
  540. * @param {{ foodEaten: number, highestScore: number, highestPosition: number,
  541. * spawnedAt: number | undefined }} stats
  542. */
  543. deathScreen.show = stats => {
  544. const foodEatenElement = document.querySelector('#food_eaten');
  545. if (foodEatenElement)
  546. foodEatenElement.textContent = stats.foodEaten.toString();
  547.  
  548. const highestMassElement = document.querySelector('#highest_mass');
  549. if (highestMassElement)
  550. highestMassElement.textContent = Math.round(stats.highestScore).toString();
  551.  
  552. const highestPositionElement = document.querySelector('#top_leaderboard_position');
  553. if (highestPositionElement)
  554. highestPositionElement.textContent = stats.highestPosition.toString();
  555.  
  556. const timeAliveElement = document.querySelector('#time_alive');
  557. if (timeAliveElement) {
  558. let time;
  559. if (stats.spawnedAt === undefined)
  560. time = 0;
  561. else
  562. time = (performance.now() - stats.spawnedAt) / 1000;
  563. const hours = Math.floor(time / 60 / 60);
  564. const mins = Math.floor(time / 60 % 60);
  565. const seconds = Math.floor(time % 60);
  566.  
  567. timeAliveElement.textContent = `${hours ? hours + ' h' : ''} ${mins ? mins + ' m' : ''} `
  568. + `${seconds ? seconds + ' s' : ''}`;
  569. }
  570.  
  571. statsContainer.classList.remove('line--hidden');
  572. ui.toggleEscOverlay(false);
  573. if (overlay) overlay.style.display = '';
  574.  
  575. stats.foodEaten = 0;
  576. stats.highestScore = 0;
  577. stats.highestPosition = 0;
  578. stats.spawnedAt = undefined;
  579.  
  580. // refresh ads... ...yep
  581. const { adSlot4, adSlot5, adSlot6, googletag } = /** @type {any} */ (window);
  582. if (googletag) {
  583. googletag.cmd.push(() => googletag.display(adSlot4));
  584. googletag.cmd.push(() => googletag.display(adSlot5));
  585. googletag.cmd.push(() => googletag.display(adSlot6));
  586. }
  587. };
  588.  
  589. deathScreen.hide = () => {
  590. const shown = !statsContainer?.classList.contains('line--hidden');
  591. statsContainer?.classList.add('line--hidden');
  592. const { googletag } = /** @type {any} */ (window);
  593. if (shown && googletag) {
  594. googletag.cmd.push(() => googletag.pubads().refresh());
  595. }
  596. };
  597.  
  598. return deathScreen;
  599. })();
  600.  
  601. ui.minimap = (() => {
  602. const canvas = document.createElement('canvas');
  603. canvas.style.cssText = 'position: absolute; bottom: 0; right: 0; background: #0006; width: 200px; \
  604. height: 200px; z-index: 2; user-select: none;';
  605. canvas.width = canvas.height = 200;
  606. document.body.appendChild(canvas);
  607.  
  608. const ctx = aux.require(
  609. canvas.getContext('2d'),
  610. 'Unable to get 2D context for the minimap. This is probably your browser being dumb, maybe reload ' +
  611. 'the page?',
  612. );
  613.  
  614. return { canvas, ctx };
  615. })();
  616.  
  617. ui.chat = (() => {
  618. const chat = {};
  619.  
  620. const block = aux.require(
  621. document.querySelector('#chat_block'),
  622. 'Can\'t find the chat UI. Try reloading the page?',
  623. );
  624.  
  625. /**
  626. * @param {ParentNode} root
  627. * @param {string} selector
  628. */
  629. function clone(root, selector) {
  630. /** @type {HTMLElement} */
  631. const old = aux.require(
  632. root.querySelector(selector),
  633. `Can't find this chat element: ${selector}. Try reloading the page?`,
  634. );
  635.  
  636. const el = /** @type {HTMLElement} */ (old.cloneNode(true));
  637. el.id = '';
  638. old.style.display = 'none';
  639. old.insertAdjacentElement('afterend', el);
  640.  
  641. return el;
  642. }
  643.  
  644. // can't just replace the chat box - otherwise sigmod can't hide it - so we make its children invisible
  645. // elements grabbed with clone() are only styled by their class, not id
  646. const toggle = clone(document, '#chat_vsbltyBtn');
  647. const scrollbar = clone(document, '#chat_scrollbar');
  648. const thumb = clone(scrollbar, '#chat_thumb');
  649.  
  650. const input = chat.input = /** @type {HTMLInputElement} */ (aux.require(
  651. document.querySelector('#chat_textbox'),
  652. 'Can\'t find the chat textbox. Try reloading the page?',
  653. ));
  654.  
  655. const list = document.createElement('div');
  656. list.style.cssText = 'width: 400px; height: 182px; position: absolute; bottom: 54px; left: 46px; \
  657. overflow: hidden; user-select: none; z-index: 301;';
  658. block.appendChild(list);
  659.  
  660. let toggled = true;
  661. toggle.style.borderBottomLeftRadius = '10px'; // a bug fix :p
  662. toggle.addEventListener('click', () => {
  663. toggled = !toggled;
  664. input.style.display = toggled ? '' : 'none';
  665. scrollbar.style.display = toggled ? 'block' : 'none';
  666. list.style.display = toggled ? '' : 'none';
  667.  
  668. if (toggled) {
  669. toggle.style.borderTopRightRadius = toggle.style.borderBottomRightRadius = '';
  670. toggle.style.opacity = '';
  671. } else {
  672. toggle.style.borderTopRightRadius = toggle.style.borderBottomRightRadius = '10px';
  673. toggle.style.opacity = '0.25';
  674. }
  675. });
  676.  
  677. scrollbar.style.display = 'block';
  678. let scrollTop = 0; // keep a float here, because list.scrollTop is always casted to an int
  679. let thumbHeight = 1;
  680. let lastY;
  681. thumb.style.height = '182px';
  682.  
  683. function updateThumb() {
  684. thumb.style.bottom = (1 - list.scrollTop / (list.scrollHeight - 182)) * (182 - thumbHeight) + 'px';
  685. }
  686.  
  687. function scroll() {
  688. if (scrollTop >= list.scrollHeight - 182 - 40) {
  689. // close to bottom, snap downwards
  690. list.scrollTop = scrollTop = list.scrollHeight - 182;
  691. }
  692.  
  693. thumbHeight = Math.min(Math.max(182 / list.scrollHeight, 0.1), 1) * 182;
  694. thumb.style.height = thumbHeight + 'px';
  695. updateThumb();
  696. }
  697.  
  698. let scrolling = false;
  699. thumb.addEventListener('mousedown', () => void (scrolling = true));
  700. addEventListener('mouseup', () => void (scrolling = false));
  701. addEventListener('mousemove', e => {
  702. const deltaY = e.clientY - lastY;
  703. lastY = e.clientY;
  704.  
  705. if (!scrolling) return;
  706. e.preventDefault();
  707.  
  708. if (lastY === undefined) {
  709. lastY = e.clientY;
  710. return;
  711. }
  712.  
  713. list.scrollTop = scrollTop = Math.min(Math.max(
  714. scrollTop + deltaY * list.scrollHeight / 182, 0), list.scrollHeight - 182);
  715. updateThumb();
  716. });
  717.  
  718. let lastWasBarrier = true; // init to true, so we don't print a barrier as the first ever message (ugly)
  719. /**
  720. * @param {string} authorName
  721. * @param {[number, number, number]} rgb
  722. * @param {string} text
  723. */
  724. chat.add = (authorName, rgb, text) => {
  725. lastWasBarrier = false;
  726.  
  727. const container = document.createElement('div');
  728. const author = document.createElement('span');
  729. author.style.cssText = `color: ${aux.rgb2hex(rgb)}; padding-right: 0.75em;`;
  730. author.textContent = aux.trim(authorName);
  731. container.appendChild(author);
  732.  
  733. const msg = document.createElement('span');
  734. msg.textContent = aux.trim(text);
  735. container.appendChild(msg);
  736.  
  737. while (list.children.length > 100)
  738. list.firstChild?.remove();
  739.  
  740. list.appendChild(container);
  741.  
  742. scroll();
  743. };
  744.  
  745. chat.barrier = () => {
  746. if (lastWasBarrier) return;
  747. lastWasBarrier = true;
  748.  
  749. const barrier = document.createElement('div');
  750. barrier.style.cssText = 'width: calc(100% - 20px); height: 1px; background: #8888; margin: 10px;';
  751. list.appendChild(barrier);
  752.  
  753. scroll();
  754. };
  755.  
  756. chat.matchTheme = () => {
  757. list.style.color = aux.setting('input#darkTheme', true) ? '#fffc' : '#000c';
  758. };
  759.  
  760. return chat;
  761. })();
  762.  
  763. /** @param {string} msg */
  764. ui.error = msg => {
  765. const modal = /** @type {HTMLElement | null} */ (document.querySelector('#errormodal'));
  766. const desc = document.querySelector('#errormodal p');
  767. if (desc)
  768. desc.innerHTML = msg;
  769.  
  770. if (modal)
  771. modal.style.display = 'block';
  772. };
  773.  
  774. // sigmod quick fix
  775. (() => {
  776. // the play timer is inserted below the top-left stats, but because we offset them, we need to offset this
  777. // too
  778. const style = document.createElement('style');
  779. style.textContent = '.playTimer { transform: translate(5px, 10px); }';
  780. document.head.appendChild(style);
  781. })();
  782.  
  783. return ui;
  784. })();
  785.  
  786.  
  787.  
  788. /////////////////////////
  789. // Create Options Menu //
  790. /////////////////////////
  791. const settings = (() => {
  792. let settings = {
  793. cellOpacity: 1,
  794. cellOutlines: true,
  795. drawDelay: 120,
  796. jellySkinLag: true,
  797. jellyWobble: true,
  798. massBold: false,
  799. massOpacity: 1,
  800. massScaleFactor: 1,
  801. nameBold: false,
  802. nameScaleFactor: 1,
  803. selfSkin: '',
  804. unsplittableOpacity: 1,
  805. };
  806.  
  807. try {
  808. Object.assign(settings, JSON.parse(localStorage.getItem('sigfix') ?? ''));
  809. } catch (_) {}
  810.  
  811. /** @type {Set<() => void>} */
  812. const onSaves = new Set();
  813.  
  814. const channel = new BroadcastChannel('sigfix-settings');
  815. channel.addEventListener('message', msg => {
  816. Object.assign(settings, msg.data);
  817. onSaves.forEach(fn => fn());
  818. });
  819.  
  820. // #1 : define helper functions
  821. /**
  822. * @param {string} html
  823. * @returns {HTMLElement}
  824. */
  825. function fromHTML(html) {
  826. const div = document.createElement('div');
  827. div.innerHTML = html;
  828. return /** @type {HTMLElement} */ (div.firstElementChild);
  829. }
  830.  
  831. function save() {
  832. localStorage.setItem('sigfix', JSON.stringify(settings));
  833. channel.postMessage(settings);
  834. }
  835.  
  836. /**
  837. * @template O, T
  838. * @typedef {{ [K in keyof O]: O[K] extends T ? K : never }[keyof O]} PropertyOfType
  839. */
  840.  
  841. /**
  842. * @param {string} sliderSelector
  843. * @param {string} displaySelector
  844. * @param {PropertyOfType<typeof settings, number>} property
  845. * @param {number} decimals
  846. */
  847. function registerSlider(sliderSelector, displaySelector, property, decimals) {
  848. const slider = /** @type {HTMLInputElement} */ (document.querySelector(sliderSelector));
  849. const display = /** @type {HTMLElement} */ (document.querySelector(displaySelector));
  850. slider.value = settings[property].toString();
  851. display.textContent = settings[property].toFixed(decimals);
  852.  
  853. slider.addEventListener('input', () => {
  854. settings[property] = parseFloat(slider.value);
  855. display.textContent = settings[property].toFixed(decimals);
  856. save();
  857. });
  858.  
  859. onSaves.add(() => {
  860. slider.value = settings[property].toString();
  861. display.textContent = settings[property].toFixed(decimals);
  862. });
  863. }
  864.  
  865. /**
  866. * @param {string} inputSelector
  867. * @param {PropertyOfType<typeof settings, string>} property
  868. * @param {boolean} sync
  869. */
  870. function registerInput(inputSelector, property, sync) {
  871. const input = /** @type {HTMLInputElement} */ (document.querySelector(inputSelector));
  872. let value = input.value = settings[property];
  873.  
  874. input.addEventListener('change', () => {
  875. value = settings[property] = input.value;
  876. save();
  877. });
  878.  
  879. onSaves.add(() => {
  880. if (sync)
  881. value = input.value = settings[property];
  882. else
  883. settings[property] = value;
  884. });
  885. }
  886.  
  887. /**
  888. * @param {string} inputSelector
  889. * @param {PropertyOfType<typeof settings, boolean>} property
  890. */
  891. function registerCheckbox(inputSelector, property) {
  892. const checkbox = /** @type {HTMLInputElement} */ (document.querySelector(inputSelector));
  893. checkbox.checked = settings[property];
  894.  
  895. checkbox.addEventListener('change', () => {
  896. settings[property] = checkbox.checked;
  897. save();
  898. });
  899.  
  900. onSaves.add(() => {
  901. checkbox.checked = settings[property];
  902. });
  903. }
  904.  
  905. // #2 : create options for the vanilla game
  906. (() => {
  907. /** @type {HTMLElement} */
  908. const content = aux.require(
  909. document.querySelector('#cm_modal__settings .ctrl-modal__content'),
  910. 'Can\'t find the default settings UI. Try reloading the page, or disabling other scripts?',
  911. );
  912.  
  913. const style = document.createElement('style');
  914. style.innerHTML = `
  915. .sf-setting {
  916. display: block;
  917. height: 25px;
  918. position: relative;
  919. }
  920.  
  921. .sf-setting .sf-title {
  922. position: absolute;
  923. left: 0;
  924. }
  925.  
  926. .sf-setting .sf-option {
  927. position: absolute;
  928. right: 0;
  929. }
  930.  
  931. .sf-setting.double {
  932. height: 50px;
  933. }
  934.  
  935. .sf-setting.double .sf-option {
  936. top: 25px;
  937. }
  938.  
  939. .sf-setting span, .sf-setting input {
  940. display: block;
  941. float: left;
  942. height: 25px;
  943. line-height: 25px;
  944. margin-left: 5px;
  945. }
  946.  
  947. .sf-separator {
  948. text-align: center;
  949. width: 100%;
  950. }
  951. `;
  952. document.head.appendChild(style);
  953.  
  954. content.appendChild(fromHTML(`
  955. <div class="menu__item">
  956. <div style="width: 100%; height: 1px; background: #bfbfbf;"></div>
  957. </div>`));
  958. content.appendChild(fromHTML(`<div class="menu__item">
  959. <div class="sf-setting">
  960. <span class="sf-title">Draw delay</span>
  961. <div class="sf-option">
  962. <input id="sf-draw-delay" style="width: 100px;" type="range"
  963. min="40" max="300" step="5" value="120" list="sf-draw-delay-markers" />
  964. <datalist id="sf-draw-delay-markers"> <option value="120"></option> </datalist>
  965. <span id="sf-draw-delay-display" style="width: 40px; text-align: right;"></span>
  966. </div>
  967. </div>
  968. <div class="sf-setting double">
  969. <span class="sf-title">Self skin URL (not synced)</span>
  970. <div class="sf-option">
  971. <input id="sf-self-skin" placeholder="https://i.imgur.com/..." type="text" />
  972. </div>
  973. </div>
  974. <div class="sf-setting double">
  975. <span class="sf-title">Unsplittable cell outline opacity</span>
  976. <div class="sf-option">
  977. <input id="sf-unsplittable-opacity" style="width: 100px;" type="range"
  978. min="0" max="1" step="0.01" value="1" list="sf-unsplittable-opacity-markers" />
  979. <datalist id="sf-unsplittable-opacity-markers"> <option value="1"></option> </datalist>
  980. <span id="sf-unsplittable-opacity-display" style="width: 40px; text-align: right;"></span>
  981. </div>
  982. </div>
  983. <div class="sf-setting">
  984. <span class="sf-title">Cell outlines</span>
  985. <div class="sf-option">
  986. <input id="sf-cell-outlines" type="checkbox" />
  987. </div>
  988. </div>
  989. <div class="sf-setting">
  990. <span class="sf-title">Cell opacity</span>
  991. <div class="sf-option">
  992. <input id="sf-cell-opacity" style="width: 100px;" type="range"
  993. min="0" max="1" step="0.01" value="1" list="sf-cell-opacity-markers" />
  994. <datalist id="sf-cell-opacity-markers"> <option value="1"></option> </datalist>
  995. <span id="sf-cell-opacity-display" style="width: 40px; text-align: right;"></span>
  996. </div>
  997. </div>
  998.  
  999. <div class="sf-separator">•</div>
  1000.  
  1001. <div class="sf-setting">
  1002. <span class="sf-title">Name scale factor</span>
  1003. <div class="sf-option">
  1004. <input id="sf-name-scale" style="width: 100px;" type="range"
  1005. min="0.5" max="2" step="0.01" value="1" list="sf-name-scale-markers" />
  1006. <datalist id="sf-name-scale-markers"> <option value="1"></option> </datalist>
  1007. <span id="sf-name-scale-display" style="width: 40px; text-align: right;"></span>
  1008. </div>
  1009. </div>
  1010. <div class="sf-setting">
  1011. <span class="sf-title">Mass scale factor</span>
  1012. <div class="sf-option">
  1013. <input id="sf-mass-scale" style="width: 100px;" type="range"
  1014. min="0.5" max="4" step="0.01" value="1" list="sf-mass-scale-markers" />
  1015. <datalist id="sf-mass-scale-markers"> <option value="1"></option> </datalist>
  1016. <span id="sf-mass-scale-display" style="width: 40px; text-align: right;"></span>
  1017. </div>
  1018. </div>
  1019. <div class="sf-setting">
  1020. <span class="sf-title">Mass opacity</span>
  1021. <div class="sf-option">
  1022. <input id="sf-mass-opacity" style="width: 100px;" type="range"
  1023. min="0" max="1" step="0.01" value="1" list="sf-mass-opacity-markers" />
  1024. <datalist id="sf-mass-opacity-markers"> <option value="1"></option> </datalist>
  1025. <span id="sf-mass-opacity-display" style="width: 40px; text-align: right;"></span>
  1026. </div>
  1027. </div>
  1028. <div class="sf-setting">
  1029. <span class="sf-title">Bold name text / mass text</span>
  1030. <div class="sf-option">
  1031. <input id="sf-name-bold" type="checkbox" />
  1032. <input id="sf-mass-bold" type="checkbox" />
  1033. </div>
  1034. </div>
  1035.  
  1036. <div class="sf-separator">•</div>
  1037.  
  1038. <div class="sf-setting">
  1039. <span class="sf-title">Jelly physics effects on skins</span>
  1040. <div class="sf-option">
  1041. <input id="sf-jelly-skin-lag" type="checkbox" />
  1042. </div>
  1043. </div>
  1044. <div class="sf-setting">
  1045. <span class="sf-title">Jelly physics wobble effect</span>
  1046. <div class="sf-option">
  1047. <input id="sf-jelly-wobble" type="checkbox" />
  1048. </div>
  1049. </div>
  1050. </div>`));
  1051.  
  1052. registerSlider('#sf-draw-delay', '#sf-draw-delay-display', 'drawDelay', 0);
  1053. registerInput('#sf-self-skin', 'selfSkin', false);
  1054. registerSlider('#sf-unsplittable-opacity', '#sf-unsplittable-opacity-display', 'unsplittableOpacity', 2);
  1055. registerCheckbox('#sf-cell-outlines', 'cellOutlines');
  1056. registerSlider('#sf-cell-opacity', '#sf-cell-opacity-display', 'cellOpacity', 2);
  1057. registerSlider('#sf-name-scale', '#sf-name-scale-display', 'nameScaleFactor', 2);
  1058. registerSlider('#sf-mass-scale', '#sf-mass-scale-display', 'massScaleFactor', 2);
  1059. registerSlider('#sf-mass-opacity', '#sf-mass-opacity-display', 'massOpacity', 2);
  1060. registerCheckbox('#sf-name-bold', 'nameBold');
  1061. registerCheckbox('#sf-mass-bold', 'massBold');
  1062. registerCheckbox('#sf-jelly-skin-lag', 'jellySkinLag');
  1063. registerCheckbox('#sf-jelly-wobble', 'jellyWobble');
  1064. })();
  1065.  
  1066. // #3 : create options for sigmod
  1067. let sigmodInjection;
  1068. sigmodInjection = setInterval(() => {
  1069. const nav = document.querySelector('.mod_menu_navbar');
  1070. const content = document.querySelector('.mod_menu_content');
  1071. if (!nav || !content) return;
  1072.  
  1073. clearInterval(sigmodInjection);
  1074.  
  1075. const page = fromHTML(`
  1076. <div class="mod_tab scroll" style="display: none;">
  1077. <div class="modRowItems justify-sb">
  1078. <span>Draw delay</span>
  1079. <span class="justify-sb">
  1080. <input id="sfsm-draw-delay" style="width: 200px;" type="range"
  1081. min="40" max="300" step="5" value="120" list="sfsm-draw-delay-markers" />
  1082. <datalist id="sfsm-draw-delay-markers">
  1083. <option value="120"></option>
  1084. </datalist>
  1085. <span id="sfsm-draw-delay-display" class="text-center" style="width: 75px;"></span>
  1086. </span>
  1087. </div>
  1088. <div class="modRowItems justify-sb">
  1089. <span>Self skin URL (not synced)</span>
  1090. <input class="modInput" id="sfsm-self-skin" placeholder="https://i.imgur.com/..." type="text" />
  1091. </div>
  1092. <div class="modRowItems justify-sb">
  1093. <span>Unsplittable cell outline opacity</span>
  1094. <span class="justify-sb">
  1095. <input id="sfsm-unsplittable-opacity" style="width: 200px;" type="range"
  1096. min="0" max="1" step="0.01" value="1" list="sfsm-unsplittable-opacity-markers" />
  1097. <datalist id="sfsm-unsplittable-opacity-markers">
  1098. <option value="1"></option>
  1099. </datalist>
  1100. <span id="sfsm-unsplittable-opacity-display" class="text-center" style="width: 75px;"></span>
  1101. </span>
  1102. </div>
  1103. <div class="modRowItems justify-sb">
  1104. <span>Cell outlines</span>
  1105. <div style="width: 75px; text-align: center;">
  1106. <div class="modCheckbox" style="display: inline-block;">
  1107. <input id="sfsm-cell-outlines" type="checkbox" />
  1108. <label class="cbx" for="sfsm-cell-outlines"></label>
  1109. </div>
  1110. </div>
  1111. </div>
  1112. <div class="modRowItems justify-sb">
  1113. <span>Cell opacity</span>
  1114. <span class="justify-sb">
  1115. <input id="sfsm-cell-opacity" style="width: 200px;" type="range"
  1116. min="0" max="1" step="0.01" value="1" list="sfsm-cell-opacity-markers" />
  1117. <datalist id="sfsm-cell-opacity-markers">
  1118. <option value="1"></option>
  1119. </datalist>
  1120. <span id="sfsm-cell-opacity-display" class="text-center" style="width: 75px;"></span>
  1121. </span>
  1122. </div>
  1123.  
  1124. <span class="text-center">•</span>
  1125.  
  1126. <div class="modRowItems justify-sb">
  1127. <span>Name scale factor</span>
  1128. <span class="justify-sb">
  1129. <input id="sfsm-name-scale-factor" style="width: 200px;" type="range"
  1130. min="0.5" max="2" step="0.01" value="1" list="sfsm-name-scale-factor-markers" />
  1131. <datalist id="sfsm-name-scale-factor-markers">
  1132. <option value="1"></option>
  1133. </datalist>
  1134. <span id="sfsm-name-scale-factor-display" class="text-center" style="width: 75px;"></span>
  1135. </span>
  1136. </div>
  1137. <div class="modRowItems justify-sb">
  1138. <span>Mass scale factor</span>
  1139. <span class="justify-sb">
  1140. <input id="sfsm-mass-scale-factor" style="width: 200px;" type="range"
  1141. min="0.5" max="4" step="0.01" value="1" list="sfsm-mass-scale-factor-markers" />
  1142. <datalist id="sfsm-mass-scale-factor-markers">
  1143. <option value="1"></option>
  1144. </datalist>
  1145. <span id="sfsm-mass-scale-factor-display" class="text-center" style="width: 75px;"></span>
  1146. </span>
  1147. </div>
  1148. <div class="modRowItems justify-sb">
  1149. <span>Mass opacity</span>
  1150. <span class="justify-sb">
  1151. <input id="sfsm-mass-opacity" style="width: 200px;" type="range"
  1152. min="0" max="1" step="0.01" value="1" list="sfsm-mass-opacity-markers" />
  1153. <datalist id="sfsm-mass-opacity-markers">
  1154. <option value="1"></option>
  1155. </datalist>
  1156. <span id="sfsm-mass-opacity-display" class="text-center" style="width: 75px;"></span>
  1157. </span>
  1158. </div>
  1159. <div class="modRowItems justify-sb">
  1160. <span>Bold name text / mass text</span>
  1161. <div style="width: 75px; text-align: center;">
  1162. <div class="modCheckbox" style="display: inline-block;">
  1163. <input id="sfsm-name-bold" type="checkbox" />
  1164. <label class="cbx" for="sfsm-name-bold"></label>
  1165. </div>
  1166. <div class="modCheckbox" style="display: inline-block;">
  1167. <input id="sfsm-mass-bold" type="checkbox" />
  1168. <label class="cbx" for="sfsm-mass-bold"></label>
  1169. </div>
  1170. </div>
  1171. </div>
  1172. <span class="text-center">•</span>
  1173.  
  1174. <div class="modRowItems justify-sb">
  1175. <span>Jelly physics effect on skins</span>
  1176. <div style="width: 75px; text-align: center;">
  1177. <div class="modCheckbox" style="display: inline-block;">
  1178. <input id="sfsm-jelly-skin-lag" type="checkbox" />
  1179. <label class="cbx" for="sfsm-jelly-skin-lag"></label>
  1180. </div>
  1181. </div>
  1182. </div>
  1183. <div class="modRowItems justify-sb">
  1184. <span>Jelly physics wobble effect</span>
  1185. <div style="width: 75px; text-align: center;">
  1186. <div class="modCheckbox" style="display: inline-block;">
  1187. <input id="sfsm-jelly-wobble" type="checkbox" />
  1188. <label class="cbx" for="sfsm-jelly-wobble"></label>
  1189. </div>
  1190. </div>
  1191. </div>
  1192. </div>
  1193. `);
  1194. content.appendChild(page);
  1195.  
  1196. registerSlider('#sfsm-draw-delay', '#sfsm-draw-delay-display', 'drawDelay', 0);
  1197. registerInput('#sfsm-self-skin', 'selfSkin', false);
  1198. registerSlider(
  1199. '#sfsm-unsplittable-opacity', '#sfsm-unsplittable-opacity-display', 'unsplittableOpacity', 2);
  1200. registerCheckbox('#sfsm-cell-outlines', 'cellOutlines');
  1201. registerSlider('#sfsm-cell-opacity', '#sfsm-cell-opacity-display', 'cellOpacity', 2);
  1202. registerSlider('#sfsm-name-scale-factor', '#sfsm-name-scale-factor-display', 'nameScaleFactor', 2);
  1203. registerSlider('#sfsm-mass-scale-factor', '#sfsm-mass-scale-factor-display', 'massScaleFactor', 2);
  1204. registerSlider('#sfsm-mass-opacity', '#sfsm-mass-opacity-display', 'massOpacity', 2);
  1205. registerCheckbox('#sfsm-name-bold', 'nameBold');
  1206. registerCheckbox('#sfsm-mass-bold', 'massBold');
  1207. registerCheckbox('#sfsm-jelly-skin-lag', 'jellySkinLag');
  1208. registerCheckbox('#sfsm-jelly-wobble', 'jellyWobble');
  1209.  
  1210. const navButton = fromHTML('<button class="mod_nav_btn">🔥 Sig Fixes</button>');
  1211. nav.appendChild(navButton);
  1212. navButton.addEventListener('click', () => {
  1213. // basically openModTab() from sigmod
  1214. (/** @type {NodeListOf<HTMLElement>} */ (document.querySelectorAll('.mod_tab'))).forEach(tab => {
  1215. tab.style.opacity = '0';
  1216. setTimeout(() => tab.style.display = 'none', 200);
  1217. });
  1218.  
  1219. (/** @type {NodeListOf<HTMLElement>} */ (document.querySelectorAll('.mod_nav_btn'))).forEach(tab => {
  1220. tab.classList.remove('mod_selected');
  1221. });
  1222.  
  1223. navButton.classList.add('mod_selected');
  1224. setTimeout(() => {
  1225. page.style.display = 'flex';
  1226. setTimeout(() => page.style.opacity = '1',10);
  1227. }, 200);
  1228. });
  1229. }, 100);
  1230.  
  1231. return settings;
  1232. })();
  1233.  
  1234.  
  1235.  
  1236. ////////////////////////////////
  1237. // Setup Multi-tab World Sync //
  1238. ////////////////////////////////
  1239. /** @typedef {{
  1240. * self: string,
  1241. * owned: Set<number>,
  1242. * skin: string,
  1243. * }} SyncData
  1244. */
  1245. const sync = (() => {
  1246. const sync = {};
  1247. /** @type {Map<string, SyncData>} */
  1248. sync.others = new Map();
  1249.  
  1250. const frame = new BroadcastChannel('sigfix-frame');
  1251. const worldsync = new BroadcastChannel('sigfix-worldsync');
  1252. const self = Date.now() + '-' + Math.random();
  1253.  
  1254. sync.broadcast = () => {
  1255. /** @type {Set<number>} */
  1256. const owned = new Set();
  1257. world.mine.forEach(id => owned.add(id));
  1258. world.mineDead.forEach(id => owned.add(id));
  1259.  
  1260. /** @type {SyncData} */
  1261. const syncData = {
  1262. self,
  1263. owned,
  1264. skin: settings.selfSkin,
  1265. };
  1266. worldsync.postMessage(syncData);
  1267. };
  1268.  
  1269. sync.frame = () => {
  1270. frame.postMessage(undefined);
  1271. };
  1272.  
  1273. frame.addEventListener('message', () => {
  1274. // only update the world if we aren't rendering ourselves (example case: games open on two monitors)
  1275. if (document.visibilityState === 'hidden')
  1276. world.update();
  1277. });
  1278.  
  1279. worldsync.addEventListener('message', m => {
  1280. /** @type {SyncData} */
  1281. const data = m.data;
  1282. sync.others.set(data.self, data);
  1283. });
  1284.  
  1285. return sync;
  1286. })();
  1287.  
  1288.  
  1289.  
  1290. ///////////////////////////
  1291. // Setup World Variables //
  1292. ///////////////////////////
  1293. /** @typedef {{
  1294. * id: number,
  1295. * x: number, ox: number, nx: number,
  1296. * y: number, oy: number, ny: number,
  1297. * r: number, or: number, nr: number,
  1298. * rgb: [number, number, number],
  1299. * updated: number, born: number, dead: { to: Cell | undefined, at: number } | undefined,
  1300. * jagged: boolean,
  1301. * name: string, skin: string, sub: boolean,
  1302. * jelly: { x: number, y: number, r: number, wobble: Float32Array },
  1303. * }} Cell */
  1304. const world = (() => {
  1305. const world = {};
  1306.  
  1307. // #1 : define cell variables and functions
  1308. /** @type {Map<number, Cell>} */
  1309. world.cells = new Map();
  1310. /** @type {Set<Cell>} */
  1311. world.clanmates = new Set();
  1312. /** @type {number[]} */
  1313. world.mine = []; // order matters, as the oldest cells split first
  1314. /** @type {Set<number>} */
  1315. world.mineDead = new Set();
  1316.  
  1317. /**
  1318. * @param {Cell} cell
  1319. * @param {number} now
  1320. * @param {number | undefined} dt
  1321. */
  1322. world.move = function(cell, now, dt) {
  1323. let nx = cell.nx;
  1324. let ny = cell.ny;
  1325. if (cell.dead?.to) {
  1326. nx = cell.dead.to.x;
  1327. ny = cell.dead.to.y;
  1328. } else if (cell.r <= 20) {
  1329. cell.x = nx;
  1330. cell.y = ny;
  1331. return;
  1332. }
  1333.  
  1334. const a = Math.min(Math.max((now - cell.updated) / settings.drawDelay, 0), 1);
  1335. cell.x = cell.ox + (nx - cell.ox) * a;
  1336. cell.y = cell.oy + (ny - cell.oy) * a;
  1337. cell.r = cell.or + (cell.nr - cell.or) * a;
  1338.  
  1339. if (dt !== undefined) {
  1340. cell.jelly.x = aux.exponentialEase(cell.jelly.x, cell.x, 2, dt);
  1341. cell.jelly.y = aux.exponentialEase(cell.jelly.y, cell.y, 2, dt);
  1342. cell.jelly.r = aux.exponentialEase(cell.jelly.r, cell.r, 5, dt);
  1343. }
  1344. };
  1345.  
  1346. let last = performance.now();
  1347. world.update = function() {
  1348. const now = performance.now();
  1349. const dt = (now - last) / 1000;
  1350. last = now;
  1351.  
  1352. world.cells.forEach(cell => world.move(cell, now, dt));
  1353.  
  1354. // move camera
  1355. let avgX = 0;
  1356. let avgY = 0;
  1357. let totalR = 0;
  1358. let totalCells = 0;
  1359.  
  1360. world.mine.forEach(id => {
  1361. const cell = world.cells.get(id);
  1362. if (!cell || cell.dead) return;
  1363.  
  1364. avgX += cell.x;
  1365. avgY += cell.y;
  1366. totalR += cell.r;
  1367. ++totalCells;
  1368. });
  1369.  
  1370. avgX /= totalCells;
  1371. avgY /= totalCells;
  1372.  
  1373. let xyEaseFactor;
  1374. if (totalCells > 0) {
  1375. world.camera.tx = avgX;
  1376. world.camera.ty = avgY;
  1377. world.camera.tscale = Math.min(64 / totalR) ** 0.4 * input.zoom;
  1378.  
  1379. xyEaseFactor = 2;
  1380. } else {
  1381. xyEaseFactor = 20;
  1382. }
  1383.  
  1384. world.camera.x = aux.exponentialEase(world.camera.x, world.camera.tx, xyEaseFactor, dt);
  1385. world.camera.y = aux.exponentialEase(world.camera.y, world.camera.ty, xyEaseFactor, dt);
  1386. world.camera.scale = aux.exponentialEase(world.camera.scale, world.camera.tscale, 9, dt);
  1387. };
  1388.  
  1389. /** @returns {Float32Array} */
  1390. world.wobble = function() {
  1391. const speed = [Math.random(), Math.random(), Math.random()];
  1392. const shift = [Math.random(), Math.random(), Math.random()];
  1393. return new Float32Array([
  1394. // speed should be between 1-4, can be positive or negative
  1395. speed[0] * 6 - 3 + Math.sign(speed[0] - 0.5), shift[0],
  1396. speed[1] * 6 - 3 + Math.sign(speed[1] - 0.5), shift[1],
  1397. speed[2] * 6 - 3 + Math.sign(speed[2] - 0.5), shift[2],
  1398. ]);
  1399. };
  1400.  
  1401. // clean up dead cells
  1402. setInterval(() => {
  1403. const now = performance.now();
  1404. world.cells.forEach((cell, id) => {
  1405. if (!cell.dead) return;
  1406. if (now - cell.dead.at >= 120) {
  1407. world.cells.delete(id);
  1408. world.mineDead.delete(id);
  1409. }
  1410. });
  1411. }, 100);
  1412.  
  1413.  
  1414.  
  1415. // #2 : define others, like camera and borders
  1416. world.camera = {
  1417. x: 0, tx: 0,
  1418. y: 0, ty: 0,
  1419. scale: 1, tscale: 1,
  1420. };
  1421.  
  1422. /** @type {{ l: number, r: number, t: number, b: number } | undefined} */
  1423. world.border = undefined;
  1424.  
  1425. /** @type {{ name: string, me: boolean, sub: boolean, place: number | undefined }[]} */
  1426. world.leaderboard = [];
  1427.  
  1428.  
  1429.  
  1430. // #3 : define stats
  1431. world.stats = {
  1432. foodEaten: 0,
  1433. highestPosition: 200,
  1434. highestScore: 0,
  1435. /** @type {number | undefined} */
  1436. spawnedAt: undefined,
  1437. };
  1438.  
  1439.  
  1440.  
  1441. return world;
  1442. })();
  1443.  
  1444.  
  1445.  
  1446. //////////////////////////
  1447. // Setup All Networking //
  1448. //////////////////////////
  1449. const net = (() => {
  1450. const net = {};
  1451.  
  1452. // #1 : define state
  1453. /** @type {{ shuffle: Map<number, number>, unshuffle: Map<number, number> } | undefined} */
  1454. let handshake;
  1455. /** @type {number | undefined} */
  1456. let pendingPingFrom;
  1457. let pingInterval;
  1458. let reconnectAttempts = 0;
  1459. /** @type {WebSocket} */
  1460. let ws;
  1461.  
  1462. /** -1 if ping reply took too long @type {number | undefined} */
  1463. net.latency = undefined;
  1464. net.ready = false;
  1465.  
  1466. // #2 : connecting/reconnecting the websocket
  1467. /** @type {HTMLSelectElement | null} */
  1468. const gamemode = document.querySelector('#gamemode');
  1469. /** @type {HTMLOptionElement | null} */
  1470. const firstGamemode = document.querySelector('#gamemode option');
  1471.  
  1472. function connect() {
  1473. let server = 'wss://' + (gamemode?.value || firstGamemode?.value || 'ca0.sigmally.com/ws/');
  1474. if (location.search.startsWith('?ip='))
  1475. server = location.search.slice('?ip='.length);
  1476.  
  1477. try {
  1478. ws = new destructor.realWebSocket(server);
  1479. } catch (err) {
  1480. console.error('can\'t make WebSocket:', err);
  1481. aux.require(null, 'The server is invalid. Try changing the server, reloading the page, or clearing ' +
  1482. 'your browser cache and cookies.');
  1483. }
  1484.  
  1485. destructor.safeWebSockets.add(ws);
  1486. ws.binaryType = 'arraybuffer';
  1487. ws.addEventListener('close', wsClose);
  1488. ws.addEventListener('error', wsError);
  1489. ws.addEventListener('message', wsMessage);
  1490. ws.addEventListener('open', wsOpen);
  1491. }
  1492.  
  1493. function wsClose() {
  1494. handshake = undefined;
  1495. pendingPingFrom = undefined;
  1496. if (pingInterval)
  1497. clearInterval(pingInterval);
  1498.  
  1499. net.latency = undefined;
  1500. net.ready = false;
  1501.  
  1502. // hide/clear UI
  1503. ui.stats.misc.textContent = '';
  1504. world.leaderboard = [];
  1505. ui.leaderboard.update();
  1506.  
  1507. // clear world
  1508. world.border = undefined;
  1509. world.cells.clear(); // make sure we won't see overlapping IDs from new cells from the new connection
  1510. world.clanmates.clear();
  1511. while (world.mine.length) world.mine.pop();
  1512. world.mineDead.clear();
  1513. sync.broadcast();
  1514.  
  1515. setTimeout(connect, 500 * Math.min(reconnectAttempts++ + 1, 10));
  1516. }
  1517.  
  1518. /** @param {Event} err */
  1519. function wsError(err) {
  1520. console.error('WebSocket error:', err);
  1521. }
  1522.  
  1523. function wsOpen() {
  1524. reconnectAttempts = 0;
  1525.  
  1526. ui.chat.barrier();
  1527.  
  1528. // reset camera location to the middle; this is implied but never sent by the server
  1529. world.camera.x = world.camera.tx = 0;
  1530. world.camera.y = world.camera.ty = 0;
  1531. world.camera.scale = world.camera.tscale = 1;
  1532.  
  1533. ws.send(new TextEncoder().encode('SIG 0.0.1\x00'));
  1534. }
  1535.  
  1536. // listen for when the gamemode changes
  1537. gamemode?.addEventListener('change', () => {
  1538. ws.close();
  1539. });
  1540.  
  1541.  
  1542.  
  1543. // #3 : set up auxiliary functions
  1544. /**
  1545. * @param {DataView} dat
  1546. * @param {number} off
  1547. * @returns {[string, number]}
  1548. */
  1549. function readZTString(dat, off) {
  1550. const startOff = off;
  1551. for (; off < dat.byteLength; ++off) {
  1552. if (dat.getUint8(off) === 0) break;
  1553. }
  1554.  
  1555. return [new TextDecoder('utf-8').decode(dat.buffer.slice(startOff, off)), off + 1];
  1556. }
  1557.  
  1558. /**
  1559. * @param {number} opcode
  1560. * @param {object} data
  1561. */
  1562. function sendJson(opcode, data) {
  1563. if (!handshake) return;
  1564. const dataBuf = new TextEncoder().encode(JSON.stringify(data));
  1565. const buf = new ArrayBuffer(dataBuf.byteLength + 2);
  1566. const dat = new DataView(buf);
  1567.  
  1568. dat.setUint8(0, Number(handshake.shuffle.get(opcode)));
  1569. for (let i = 0; i < dataBuf.byteLength; ++i) {
  1570. dat.setUint8(1 + i, dataBuf[i]);
  1571. }
  1572.  
  1573. ws.send(buf);
  1574. }
  1575.  
  1576. function createPingLoop() {
  1577. function ping() {
  1578. if (!handshake) return; // shouldn't ever happen
  1579.  
  1580. if (pendingPingFrom !== undefined) {
  1581. // ping was not replied to, tell the player the ping text might be wonky for a bit
  1582. net.latency = -1;
  1583. }
  1584.  
  1585. ws.send(new Uint8Array([ Number(handshake.shuffle.get(0xfe)) ]));
  1586. pendingPingFrom = performance.now();
  1587. }
  1588.  
  1589. pingInterval = setInterval(ping, 2_000);
  1590. }
  1591.  
  1592.  
  1593.  
  1594. // #4 : set up message handler
  1595. /** @param {MessageEvent} msg */
  1596. function wsMessage(msg) {
  1597. const dat = new DataView(msg.data);
  1598. if (!handshake) {
  1599. // unlikely to change as we're still on v0.0.1 but i'll check it anyway
  1600. let [version, off] = readZTString(dat, 0);
  1601. if (version !== 'SIG 0.0.1') {
  1602. alert(`got unsupported version "${version}", expected "SIG 0.0.1"`);
  1603. return ws.close();
  1604. }
  1605.  
  1606. handshake = { shuffle: new Map(), unshuffle: new Map() };
  1607. for (let i = 0; i < 256; ++i) {
  1608. const shuffled = dat.getUint8(off + i);
  1609. handshake.shuffle.set(i, shuffled);
  1610. handshake.unshuffle.set(shuffled, i);
  1611. }
  1612.  
  1613. createPingLoop();
  1614.  
  1615. return;
  1616. }
  1617.  
  1618. const now = performance.now();
  1619. let off = 1;
  1620. switch (handshake.unshuffle.get(dat.getUint8(0))) {
  1621. case 0x10: { // world update
  1622. // #a : kills / consumes
  1623. const killCount = dat.getUint16(off, true);
  1624. off += 2;
  1625.  
  1626. for (let i = 0; i < killCount; ++i) {
  1627. const killerId = dat.getUint32(off, true);
  1628. const killedId = dat.getUint32(off + 4, true);
  1629. off += 8;
  1630.  
  1631. const killer = world.cells.get(killerId);
  1632. const killed = world.cells.get(killedId);
  1633. if (killed) {
  1634. killed.dead = { to: killer, at: now };
  1635. killed.updated = now;
  1636.  
  1637. if (killed.r <= 20 && world.mine.includes(killerId))
  1638. ++world.stats.foodEaten;
  1639.  
  1640. const myIdx = world.mine.indexOf(killedId);
  1641. if (myIdx !== -1) {
  1642. world.mine.splice(myIdx, 1);
  1643. world.mineDead.add(killedId);
  1644. }
  1645.  
  1646. world.clanmates.delete(killed);
  1647. }
  1648. }
  1649.  
  1650. // #b : updates
  1651. while (true) {
  1652. const id = dat.getUint32(off, true);
  1653. off += 4;
  1654. if (id === 0) break;
  1655.  
  1656. const x = dat.getInt16(off, true);
  1657. const y = dat.getInt16(off + 2, true);
  1658. const r = dat.getInt16(off + 4, true);
  1659. const flags = dat.getUint8(off + 6);
  1660. // (void 1 byte, "isUpdate")
  1661. // (void 1 byte, "isPlayer")
  1662. const sub = !!dat.getUint8(off + 9);
  1663. off += 10;
  1664.  
  1665. let clan; [clan, off] = readZTString(dat, off);
  1666.  
  1667. /** @type {[number, number, number] | undefined} */
  1668. let rgb;
  1669. if (flags & 0x02) {
  1670. // update color
  1671. rgb = [dat.getUint8(off) / 255, dat.getUint8(off + 1) / 255, dat.getUint8(off + 2) / 255];
  1672. off += 3;
  1673. }
  1674.  
  1675. let skin = '';
  1676. if (flags & 0x04) {
  1677. // update skin
  1678. [skin, off] = readZTString(dat, off);
  1679. skin = aux.parseSkin(skin);
  1680. }
  1681.  
  1682. let name = '';
  1683. if (flags & 0x08) {
  1684. // update name
  1685. [name, off] = readZTString(dat, off);
  1686. name = aux.parseName(name);
  1687. }
  1688.  
  1689. const jagged = !!(flags & 0x11);
  1690.  
  1691. const cell = world.cells.get(id);
  1692. if (cell && !cell.dead) {
  1693. // update cell.x and cell.y, to prevent rubber banding effect when tabbing out for a bit
  1694. world.move(cell, now, undefined);
  1695.  
  1696. cell.ox = cell.x; cell.oy = cell.y; cell.or = cell.r;
  1697. cell.nx = x; cell.ny = y; cell.nr = r; cell.sub = sub;
  1698. cell.jagged = jagged;
  1699. cell.updated = now;
  1700.  
  1701. if (rgb) cell.rgb = rgb;
  1702. if (skin) cell.skin = skin;
  1703. if (name) cell.name = name;
  1704.  
  1705. if (clan && clan === aux.userData?.clan)
  1706. world.clanmates.add(cell);
  1707. } else {
  1708. /** @type {Cell} */
  1709. const cell = {
  1710. id,
  1711. x, ox: x, nx: x,
  1712. y, oy: y, ny: y,
  1713. r, or: r, nr: r,
  1714. rgb: rgb ?? [1, 1, 1],
  1715. updated: now, born: now, dead: undefined,
  1716. jagged,
  1717. name, skin, sub,
  1718. jelly: {
  1719. x, y, r,
  1720. wobble: world.wobble(),
  1721. },
  1722. };
  1723.  
  1724. world.cells.set(id, cell);
  1725.  
  1726. if (clan && clan === aux.userData?.clan)
  1727. world.clanmates.add(cell);
  1728. }
  1729. }
  1730.  
  1731. // #c : deletes
  1732. const deleteCount = dat.getUint16(off, true);
  1733. off += 2;
  1734.  
  1735. for (let i = 0; i < deleteCount; ++i) {
  1736. const deletedId = dat.getUint32(off, true);
  1737. off += 4;
  1738.  
  1739. const deleted = world.cells.get(deletedId);
  1740. if (deleted) {
  1741. deleted.dead = { to: undefined, at: now };
  1742. world.clanmates.delete(deleted);
  1743. }
  1744. }
  1745.  
  1746. if (world.mine.length === 0 && world.stats.spawnedAt !== undefined) {
  1747. ui.deathScreen.show(world.stats);
  1748. }
  1749.  
  1750. sync.broadcast();
  1751.  
  1752. break;
  1753. }
  1754.  
  1755. case 0x11: { // update camera pos
  1756. world.camera.tx = dat.getFloat32(off, true);
  1757. world.camera.ty = dat.getFloat32(off + 4, true);
  1758. world.camera.tscale = dat.getFloat32(off + 8, true) * ui.game.viewportScale * input.zoom;
  1759. break;
  1760. }
  1761.  
  1762. case 0x12: // delete all cells
  1763. world.cells.forEach(cell => {
  1764. cell.dead ??= { to: undefined, at: now };
  1765. });
  1766. world.clanmates.clear();
  1767. // passthrough
  1768. case 0x14: // delete my cells
  1769. while (world.mine.length) world.mine.pop();
  1770. break;
  1771.  
  1772. case 0x20: { // new owned cell
  1773. world.mine.push(dat.getUint32(off, true));
  1774. if (world.mine.length === 1)
  1775. world.stats.spawnedAt = now;
  1776. break;
  1777. }
  1778.  
  1779. // case 0x30 is a text list (not a numbered list), leave unsupported
  1780. case 0x31: { // ffa leaderboard list
  1781. const lb = [];
  1782. const count = dat.getUint32(off, true);
  1783. off += 4;
  1784.  
  1785. let myPosition;
  1786. for (let i = 0; i < count; ++i) {
  1787. const me = !!dat.getUint32(off, true);
  1788. off += 4;
  1789.  
  1790. let name;
  1791. [name, off] = readZTString(dat, off);
  1792. name = aux.parseName(name);
  1793.  
  1794. // why this is copied into every leaderboard entry is beyond my understanding
  1795. myPosition = dat.getUint32(off, true);
  1796. const sub = !!dat.getUint32(off + 4, true);
  1797. off += 8;
  1798.  
  1799. lb.push({ name, sub, me, place: undefined });
  1800. }
  1801.  
  1802. if (myPosition) {
  1803. if (myPosition - 1 >= lb.length) {
  1804. /** @type {HTMLInputElement | null} */
  1805. const inputName = document.querySelector('input#nick');
  1806. lb.push({
  1807. me: true,
  1808. name: aux.parseName(inputName?.value ?? ''),
  1809. place: myPosition,
  1810. sub: false,
  1811. });
  1812. }
  1813.  
  1814. if (myPosition < world.stats.highestPosition)
  1815. world.stats.highestPosition = myPosition;
  1816. }
  1817.  
  1818. world.leaderboard = lb;
  1819. ui.leaderboard.update();
  1820. break;
  1821. }
  1822.  
  1823. case 0x40: { // border update
  1824. world.border = {
  1825. l: dat.getFloat64(off, true),
  1826. t: dat.getFloat64(off + 8, true),
  1827. r: dat.getFloat64(off + 16, true),
  1828. b: dat.getFloat64(off + 24, true),
  1829. };
  1830. break;
  1831. }
  1832.  
  1833. case 0x63: { // chat message
  1834. const flags = dat.getUint8(off);
  1835. const rgb = /** @type {[number, number, number]} */
  1836. ([dat.getUint8(off + 1) / 255, dat.getUint8(off + 2) / 255, dat.getUint8(off + 3) / 255]);
  1837. off += 4;
  1838.  
  1839. let name;
  1840. [name, off] = readZTString(dat, off);
  1841. let msg;
  1842. [msg, off] = readZTString(dat, off);
  1843.  
  1844. ui.chat.add(name, rgb, msg);
  1845. break;
  1846. }
  1847.  
  1848. case 0xb4: { // incorrect password alert
  1849. ui.error('Password is incorrect');
  1850. break;
  1851. }
  1852.  
  1853. case 0xdd: {
  1854. net.howarewelosingmoney();
  1855. net.ready = true;
  1856. break;
  1857. }
  1858.  
  1859. case 0xfe: { // server stats, response to a ping
  1860. let statString;
  1861. [statString, off] = readZTString(dat, off);
  1862.  
  1863. const statData = JSON.parse(statString);
  1864. ui.stats.update(statData);
  1865.  
  1866. if (pendingPingFrom) {
  1867. net.latency = now - pendingPingFrom;
  1868. pendingPingFrom = undefined;
  1869. }
  1870. break;
  1871. }
  1872. }
  1873. }
  1874.  
  1875.  
  1876.  
  1877. // #5 : export input functions
  1878. /**
  1879. * @param {number} x
  1880. * @param {number} y
  1881. */
  1882. net.move = function(x, y) {
  1883. if (!handshake) return;
  1884. const buf = new ArrayBuffer(13);
  1885. const dat = new DataView(buf);
  1886.  
  1887. dat.setUint8(0, Number(handshake.shuffle.get(0x10)));
  1888. dat.setInt32(1, x, true);
  1889. dat.setInt32(5, y, true);
  1890.  
  1891. ws.send(buf);
  1892. };
  1893.  
  1894. net.w = function() {
  1895. if (!handshake) return;
  1896. ws.send(new Uint8Array([ Number(handshake.shuffle.get(21)) ]));
  1897. };
  1898.  
  1899. net.qdown = function() {
  1900. if (!handshake) return;
  1901. ws.send(new Uint8Array([ Number(handshake.shuffle.get(18)) ]));
  1902. };
  1903.  
  1904. net.qup = function() {
  1905. if (!handshake) return;
  1906. ws.send(new Uint8Array([ Number(handshake.shuffle.get(19)) ]));
  1907. };
  1908.  
  1909. net.split = function() {
  1910. if (!handshake) return;
  1911. ws.send(new Uint8Array([ Number(handshake.shuffle.get(17)) ]));
  1912. };
  1913.  
  1914. /**
  1915. * @param {string} msg
  1916. */
  1917. net.chat = function(msg) {
  1918. if (!handshake) return;
  1919. const msgBuf = new TextEncoder().encode(msg);
  1920.  
  1921. const buf = new ArrayBuffer(msgBuf.byteLength + 3);
  1922. const dat = new DataView(buf);
  1923.  
  1924. dat.setUint8(0, Number(handshake.shuffle.get(0x63)));
  1925. // skip byte #1, seems to require authentication + not implemented anyway
  1926. for (let i = 0; i < msgBuf.byteLength; ++i)
  1927. dat.setUint8(2 + i, msgBuf[i]);
  1928.  
  1929. ws.send(buf);
  1930. };
  1931.  
  1932. /**
  1933. * @param {string | undefined} v2
  1934. * @param {string | undefined} v3
  1935. */
  1936. net.captcha = function(v2, v3) {
  1937. sendJson(0xdc, { recaptchaV2Token: v2, recaptchaV3Token: v3 });
  1938. };
  1939.  
  1940. /**
  1941. * @param {{ name: string, skin: string, [x: string]: any }} data
  1942. */
  1943. net.play = function(data) {
  1944. sendJson(0x00, data);
  1945. };
  1946.  
  1947. net.howarewelosingmoney = function() {
  1948. if (!handshake) return;
  1949. // this is a new thing added with the rest of the recent source code obfuscation (2024/02/18)
  1950. // which collects and links to your sigmally account, seemingly just for light data analysis but probably
  1951. // just for the fun of it:
  1952. // - your IP and country
  1953. // - whether you are under a proxy
  1954. // - whether you are using sigmod (because it also blocks ads)
  1955. // - whether you are using a traditional adblocker
  1956. //
  1957. // so, no thank you
  1958. sendJson(0xd0, { ip: '', country: '', proxy: false, user: null, blocker: 'sigmally fixes @8y8x' });
  1959. };
  1960.  
  1961. net.connection = function() {
  1962. if (!ws) return undefined;
  1963. if (ws.readyState !== WebSocket.OPEN) return undefined;
  1964. if (!handshake) return undefined;
  1965. return ws;
  1966. };
  1967.  
  1968.  
  1969.  
  1970. connect();
  1971. return net;
  1972. })();
  1973.  
  1974.  
  1975.  
  1976. //////////////////////////
  1977. // Setup Input Handlers //
  1978. //////////////////////////
  1979. const input = (() => {
  1980. const input = {};
  1981.  
  1982. // #1 : general inputs
  1983. // sigmod's macro feed runs at a slower interval but presses many times in that interval. allowing queueing 2 w
  1984. // presses makes it faster (it would be better if people disabled that macro, but no one would do that)
  1985. let forceW = 0;
  1986. let mouseX = 960;
  1987. let mouseY = 540;
  1988. let w = false;
  1989.  
  1990. input.zoom = 1;
  1991.  
  1992. function mouse() {
  1993. net.move(
  1994. world.camera.x + (mouseX - innerWidth / 2) / ui.game.viewportScale / world.camera.scale,
  1995. world.camera.y + (mouseY - innerHeight / 2) / ui.game.viewportScale / world.camera.scale,
  1996. );
  1997. }
  1998.  
  1999. function unfocused() {
  2000. return ui.escOverlayVisible() || document.activeElement?.tagName === 'INPUT';
  2001. }
  2002.  
  2003. setInterval(() => {
  2004. if (document.visibilityState === 'hidden') return;
  2005. mouse();
  2006. if (forceW > 0) {
  2007. --forceW;
  2008. net.w();
  2009. } else if (w) net.w();
  2010. }, 40);
  2011.  
  2012. // sigmod freezes the player by overlaying an invisible div, so we just listen for canvas movements instead
  2013. addEventListener('mousemove', e => {
  2014. if (ui.escOverlayVisible()) return;
  2015. // sigmod freezes the player by overlaying an invisible div, so we respect it
  2016. if (e.target instanceof HTMLDivElement
  2017. && /** @type {CSSUnitValue | undefined} */ (e.target.attributeStyleMap.get('z-index'))?.value === 99)
  2018. return;
  2019. mouseX = e.clientX;
  2020. mouseY = e.clientY;
  2021. });
  2022.  
  2023. addEventListener('wheel', e => {
  2024. if (unfocused()) return;
  2025. input.zoom *= 0.8 ** (e.deltaY / 100);
  2026. input.zoom = Math.min(Math.max(input.zoom, 0.8 ** 10), 0.8 ** -11);
  2027. });
  2028.  
  2029. addEventListener('keydown', e => {
  2030. if (!document.hasFocus()) return;
  2031.  
  2032. if (e.code === 'Escape') {
  2033. if (document.activeElement === ui.chat.input)
  2034. ui.chat.input.blur();
  2035. else
  2036. ui.toggleEscOverlay();
  2037. return;
  2038. }
  2039.  
  2040. if (unfocused()) {
  2041. if (e.code === 'Enter' && document.activeElement === ui.chat.input && ui.chat.input.value.length > 0) {
  2042. net.chat(ui.chat.input.value.slice(0, 15));
  2043. ui.chat.input.value = '';
  2044. ui.chat.input.blur();
  2045. }
  2046.  
  2047. return;
  2048. }
  2049.  
  2050. switch (e.code) {
  2051. case 'KeyQ':
  2052. if (!e.repeat)
  2053. net.qdown();
  2054. break;
  2055. case 'KeyW':
  2056. w = true;
  2057. forceW = Math.min(forceW + 1, 2);
  2058. break;
  2059. case 'Space': {
  2060. if (!e.repeat) {
  2061. // send mouse position immediately, so the split will go in the correct direction.
  2062. // setTimeout is used to ensure that our mouse position is actually updated (it comes after
  2063. // keydown events)
  2064. setTimeout(() => {
  2065. mouse();
  2066. net.split();
  2067. });
  2068. }
  2069. break;
  2070. }
  2071. case 'Enter': {
  2072. ui.chat.input.focus();
  2073. break;
  2074. }
  2075. }
  2076.  
  2077. if (e.ctrlKey && e.code === 'KeyW') {
  2078. // prevent ctrl+w (only when in fullscreen!) - helps when multiboxing
  2079. e.preventDefault();
  2080. } else if (e.ctrlKey && e.code === 'Tab') {
  2081. e.returnValue = true; // undo e.preventDefault() by SigMod
  2082. e.stopImmediatePropagation(); // prevent SigMod from calling e.preventDefault() afterwards
  2083. } else if (e.code === 'Tab') {
  2084. // prevent tabbing to a UI element, which then lets you press ctrl+w
  2085. e.preventDefault();
  2086. }
  2087. });
  2088.  
  2089. addEventListener('keyup', e => {
  2090. // do not check if unfocused
  2091. if (e.code === 'KeyQ')
  2092. net.qup();
  2093. else if (e.code === 'KeyW')
  2094. w = false;
  2095. });
  2096.  
  2097. // when switching tabs, make sure W is not being held
  2098. addEventListener('blur', () => {
  2099. // dispatch event to make sure sigmod gets it too
  2100. document.dispatchEvent(new KeyboardEvent('keyup', { code: 'KeyW', key: 'w' }));
  2101. forceW = 0;
  2102. });
  2103.  
  2104. addEventListener('beforeunload', e => {
  2105. e.preventDefault();
  2106. });
  2107.  
  2108. // prevent right clicking on the game
  2109. ui.game.canvas.addEventListener('contextmenu', e => e.preventDefault());
  2110.  
  2111. // prevent dragging when some things are selected - i have a habit of unconsciously clicking all the time,
  2112. // making me regularly drag text, disabling my mouse inputs for a bit
  2113. addEventListener('dragstart', e => e.preventDefault());
  2114.  
  2115.  
  2116.  
  2117. // #2 : play and spectate buttons, and captcha
  2118. /** @param {boolean} spectating */
  2119. function playData(spectating) {
  2120. /** @type {HTMLInputElement | null} */
  2121. const nickElement = document.querySelector('input#nick');
  2122. /** @type {HTMLInputElement | null} */
  2123. const password = document.querySelector('input#password');
  2124.  
  2125. return {
  2126. state: spectating ? 2 : undefined,
  2127. name: nickElement?.value ?? '',
  2128. skin: aux.settings?.skin,
  2129. token: aux.token?.token,
  2130. sub: (aux.userData?.subscription ?? 0) > Date.now(),
  2131. clan: aux.userData?.clan,
  2132. showClanmates: aux.setting('input#showClanmates', true),
  2133. password: password?.value,
  2134. };
  2135. }
  2136.  
  2137. /** @type {HTMLButtonElement} */
  2138. const play = aux.require(
  2139. document.querySelector('button#play-btn'),
  2140. 'Can\'t find the play button. Try reloading the page?',
  2141. );
  2142. /** @type {HTMLButtonElement} */
  2143. const spectate = aux.require(
  2144. document.querySelector('button#spectate-btn'),
  2145. 'Can\'t find the spectate button. Try reloading the page?',
  2146. );
  2147.  
  2148. play.disabled = spectate.disabled = true;
  2149.  
  2150. (async () => {
  2151. let grecaptcha, CAPTCHA2, CAPTCHA3;
  2152. do {
  2153. grecaptcha = /** @type {any} */ (window).grecaptcha;
  2154. CAPTCHA2 = /** @type {any} */ (window).CAPTCHA2;
  2155. CAPTCHA3 = /** @type {any} */ (window).CAPTCHA3;
  2156.  
  2157. await aux.wait(50);
  2158. } while (!(CAPTCHA2 && CAPTCHA3 && grecaptcha && 'execute' in grecaptcha
  2159. && 'ready' in grecaptcha && 'render' in grecaptcha && 'reset' in grecaptcha));
  2160.  
  2161. // prevent game.js from invoking recaptcha, as this can cause a lot of lag whenever sigmod spams the play
  2162. // button (e.g. when using the respawn keybind)
  2163. // @ts-expect-error
  2164. window.grecaptcha = {
  2165. execute: () => new Promise(() => {}),
  2166. ready: () => {},
  2167. render: () => {},
  2168. reset: () => {},
  2169. };
  2170.  
  2171. const container = document.createElement('div');
  2172. container.id = 'g-recaptcha2';
  2173. container.style.display = 'none';
  2174. play.parentNode?.insertBefore(container, play);
  2175.  
  2176. /** @type {WebSocket | undefined} */
  2177. let acceptedConnection = undefined;
  2178. let processing = false;
  2179. let v2Handle;
  2180. /** @type {string | undefined} */
  2181. let v2Token = undefined;
  2182. /** @type {string | undefined} */
  2183. let v3Token = undefined;
  2184.  
  2185. // we need to give recaptcha enough time to analyze us, otherwise v3 will fail
  2186. grecaptcha.ready(() => setInterval(async function captchaFlow() {
  2187. let connection = net.connection();
  2188. if (!connection) {
  2189. play.disabled = spectate.disabled = true;
  2190. return;
  2191. }
  2192.  
  2193. if (v2Token || v3Token) {
  2194. net.captcha(v2Token, v3Token);
  2195. v2Token = v3Token = undefined;
  2196. processing = false;
  2197. acceptedConnection = connection;
  2198. play.disabled = spectate.disabled = false;
  2199. return;
  2200. }
  2201.  
  2202. if (processing || connection === acceptedConnection)
  2203. return;
  2204.  
  2205. // start the process of getting a new captcha token
  2206. acceptedConnection = undefined;
  2207. processing = true;
  2208. play.disabled = spectate.disabled = true;
  2209.  
  2210. // (a) : get a v3 token
  2211. /** @type {string | undefined} */
  2212. const v3 = await grecaptcha.execute(CAPTCHA3);
  2213.  
  2214. // (b) : send the v3 token to sigmally for validation (sometimes sigmally doesn't accept v3 tokens)
  2215. const v3Accepted = await fetch('https://' + new URL(connection.url).hostname + '/server/recaptcha/v3', {
  2216. method: 'POST',
  2217. body: JSON.stringify({ recaptchaV3Token: v3 }),
  2218. headers: { 'Content-Type': 'application/json' },
  2219. })
  2220. .then(res => res.json()).then(res => !!res?.body?.success)
  2221. .catch(err => {
  2222. console.error('Failed to validate v3 token:', err);
  2223. return false;
  2224. });
  2225.  
  2226. // (c) : if accepted by this server, send the v3 token to this game
  2227. if (v3Accepted) {
  2228. container.style.display = 'none';
  2229. connection = net.connection();
  2230. if (connection) {
  2231. net.captcha(undefined, v3);
  2232. processing = false;
  2233. acceptedConnection = connection;
  2234. play.disabled = spectate.disabled = false;
  2235. } else {
  2236. v3Token = v3;
  2237. }
  2238.  
  2239. return;
  2240. }
  2241.  
  2242. // (d) : otherwise, render a v2 captcha
  2243. container.style.display = 'block';
  2244. play.style.display = spectate.style.display = 'none';
  2245. if (v2Handle !== undefined) {
  2246. grecaptcha.reset(v2Handle);
  2247. } else {
  2248. v2Handle = grecaptcha.render('g-recaptcha2', {
  2249. sitekey: CAPTCHA2,
  2250. callback: v2 => {
  2251. container.style.display = 'none';
  2252. play.style.display = spectate.style.display = '';
  2253.  
  2254. if (acceptedConnection) return;
  2255.  
  2256. connection = net.connection();
  2257. if (connection) {
  2258. net.captcha(v2, undefined);
  2259. processing = false;
  2260. acceptedConnection = connection;
  2261. play.disabled = spectate.disabled = false;
  2262. } else {
  2263. v2Token = v2;
  2264. }
  2265. },
  2266. });
  2267. }
  2268. }, 500));
  2269.  
  2270. /** @param {MouseEvent} e */
  2271. async function clickHandler(e) {
  2272. if (!acceptedConnection) return;
  2273. ui.toggleEscOverlay(false);
  2274. net.play(playData(e.currentTarget === spectate));
  2275. }
  2276.  
  2277. play.addEventListener('click', clickHandler);
  2278. spectate.addEventListener('click', clickHandler);
  2279. })();
  2280.  
  2281. return input;
  2282. })();
  2283.  
  2284.  
  2285.  
  2286. //////////////////////////
  2287. // Configure WebGL Data //
  2288. //////////////////////////
  2289. const { init: initWebGL, programs, uniforms } = (() => {
  2290. // note: WebGL functions only really return null if the context is lost - in which case, data will be replaced
  2291. // anyway after it's restored. so, we cast everything to a non-null type.
  2292. const obj = {};
  2293. const programs = {};
  2294. const uniforms = {};
  2295.  
  2296. const gl = ui.game.gl;
  2297.  
  2298. // define helper functions
  2299. /**
  2300. * @param {string} name
  2301. * @param {WebGLShader} vShader
  2302. * @param {WebGLShader} fShader
  2303. */
  2304. function program(name, vShader, fShader) {
  2305. const p = /** @type {WebGLProgram} */ (gl.createProgram());
  2306.  
  2307. gl.attachShader(p, vShader);
  2308. gl.attachShader(p, fShader);
  2309. gl.linkProgram(p);
  2310.  
  2311. // note: linking errors should not happen in production
  2312. aux.require(
  2313. gl.getProgramParameter(p, gl.LINK_STATUS) || !gl.isContextLost(),
  2314. `Can\'t link WebGL2 program "${name}". You might be on a weird browser.\n\nFull error log:\n` +
  2315. gl.getProgramInfoLog(p),
  2316. );
  2317.  
  2318. return p;
  2319. }
  2320.  
  2321. /**
  2322. * @param {string} name
  2323. * @param {number} type
  2324. * @param {string} source
  2325. */
  2326. function shader(name, type, source) {
  2327. const s = /** @type {WebGLShader} */ (gl.createShader(type));
  2328. gl.shaderSource(s, source);
  2329. gl.compileShader(s);
  2330.  
  2331. // note: compilation errors should not happen in production
  2332. aux.require(
  2333. gl.getShaderParameter(s, gl.COMPILE_STATUS) || !gl.isContextLost(),
  2334. `Can\'t compile WebGL2 shader "${name}". You might be on a weird browser.\n\nFull error log:\n` +
  2335. gl.getShaderInfoLog(s),
  2336. );
  2337.  
  2338. return s;
  2339. }
  2340.  
  2341. /**
  2342. * @template {string} T
  2343. * @param {string} programName
  2344. * @param {WebGLProgram} program
  2345. * @param {T[]} names
  2346. * @returns {{ [x in T]: WebGLUniformLocation }}
  2347. */
  2348. function getUniforms(programName, program, names) {
  2349. /** @type {{ [x in T]?: WebGLUniformLocation }} */
  2350. const uniforms = {};
  2351. names.forEach(name => {
  2352. const loc = /** @type {WebGLUniformLocation} */ (gl.getUniformLocation(program, name));
  2353. aux.require(
  2354. loc || !gl.isContextLost(), `Can\'t find WebGL2 uniform "${name}" in program "${programName}".`);
  2355.  
  2356. uniforms[name] = loc;
  2357. });
  2358.  
  2359. return /** @type {any} */ (uniforms);
  2360. }
  2361.  
  2362.  
  2363.  
  2364. function init() {
  2365. gl.enable(gl.BLEND);
  2366. gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
  2367.  
  2368. // create programs and uniforms
  2369. programs.bg = program(
  2370. 'bg',
  2371. shader('bg.vShader', gl.VERTEX_SHADER, `#version 300 es
  2372. layout(location = 0) in vec2 a_pos;
  2373.  
  2374. uniform float u_aspect_ratio;
  2375. uniform vec2 u_camera_pos;
  2376. uniform float u_camera_scale;
  2377.  
  2378. out vec2 v_world_pos;
  2379.  
  2380. void main() {
  2381. gl_Position = vec4(a_pos, 0, 1);
  2382.  
  2383. v_world_pos = a_pos * vec2(u_aspect_ratio, 1.0) / u_camera_scale;
  2384. v_world_pos += u_camera_pos * vec2(1.0, -1.0);
  2385. }
  2386. `),
  2387. shader('bg.fShader', gl.FRAGMENT_SHADER, `#version 300 es
  2388. precision highp float;
  2389. in vec2 v_world_pos;
  2390.  
  2391. uniform float u_camera_scale;
  2392.  
  2393. uniform vec4 u_border_color;
  2394. uniform float[4] u_border_lrtb;
  2395. uniform bool u_dark_theme_enabled;
  2396. uniform bool u_grid_enabled;
  2397. uniform sampler2D u_texture;
  2398.  
  2399. out vec4 out_color;
  2400.  
  2401. void main() {
  2402. if (u_grid_enabled) {
  2403. vec2 t_coord = v_world_pos / 50.0;
  2404. out_color = texture(u_texture, t_coord);
  2405. // fade grid pixels so when they become <1px wide, they're invisible
  2406. float alpha = clamp(u_camera_scale * 540.0 * 50.0, 0.0, 1.0) * 0.1;
  2407. if (u_dark_theme_enabled) {
  2408. out_color *= vec4(1, 1, 1, alpha);
  2409. } else {
  2410. out_color *= vec4(0, 0, 0, alpha);
  2411. }
  2412. }
  2413.  
  2414. // force border to always be visible, otherwise it flickers
  2415. float thickness = max(3.0 / (u_camera_scale * 540.0), 25.0);
  2416.  
  2417. // make a larger inner rectangle and a normal inverted outer rectangle
  2418. float inner_alpha = min(
  2419. min((v_world_pos.x + thickness) - u_border_lrtb[0],
  2420. u_border_lrtb[1] - (v_world_pos.x - thickness)),
  2421. min((v_world_pos.y + thickness) - u_border_lrtb[2],
  2422. u_border_lrtb[3] - (v_world_pos.y - thickness))
  2423. );
  2424. float outer_alpha = max(
  2425. max(u_border_lrtb[0] - v_world_pos.x, v_world_pos.x - u_border_lrtb[1]),
  2426. max(u_border_lrtb[2] - v_world_pos.y, v_world_pos.y - u_border_lrtb[3])
  2427. );
  2428. float alpha = clamp(min(inner_alpha, outer_alpha), 0.0, 1.0);
  2429.  
  2430. out_color = out_color * (1.0 - alpha) + u_border_color * alpha;
  2431. }
  2432. `),
  2433. );
  2434. uniforms.bg = getUniforms('bg', programs.bg, [
  2435. 'u_aspect_ratio', 'u_camera_pos', 'u_camera_scale',
  2436. 'u_border_color', 'u_border_lrtb', 'u_dark_theme_enabled', 'u_grid_enabled',
  2437. ]);
  2438.  
  2439.  
  2440.  
  2441. programs.cell = program(
  2442. 'cell',
  2443. shader('cell.vShader', gl.VERTEX_SHADER, `#version 300 es
  2444. layout(location = 0) in vec2 a_pos;
  2445.  
  2446. uniform float u_aspect_ratio;
  2447. uniform vec2 u_camera_pos;
  2448. uniform float u_camera_scale;
  2449.  
  2450. uniform float u_inner_radius;
  2451. uniform float u_outer_radius;
  2452. uniform vec2 u_pos;
  2453.  
  2454. out vec2 v_pos;
  2455. out vec2 v_t_coord;
  2456.  
  2457. void main() {
  2458. v_pos = a_pos;
  2459. v_t_coord = a_pos / (u_inner_radius / u_outer_radius) * 0.5 + 0.5;
  2460.  
  2461. vec2 clip_pos = -u_camera_pos + u_pos + a_pos * u_outer_radius;
  2462. clip_pos *= u_camera_scale * vec2(1.0 / u_aspect_ratio, -1.0);
  2463. gl_Position = vec4(clip_pos, 0, 1);
  2464. }
  2465. `),
  2466. shader('cell.fShader', gl.FRAGMENT_SHADER, `#version 300 es
  2467. precision highp float;
  2468. in vec2 v_pos;
  2469. in vec2 v_t_coord;
  2470.  
  2471. uniform float u_camera_scale;
  2472.  
  2473. uniform float u_alpha;
  2474. uniform vec4 u_color;
  2475. uniform float u_outer_radius;
  2476. uniform vec4 u_outline_color;
  2477. uniform bool u_outline_thick;
  2478. uniform sampler2D u_texture;
  2479. uniform bool u_texture_enabled;
  2480. uniform float u_time;
  2481. uniform float u_wobble[6];
  2482. uniform bool u_wobble_enabled;
  2483.  
  2484. out vec4 out_color;
  2485.  
  2486. void main() {
  2487. float blur = 0.5 * u_outer_radius * (540.0 * u_camera_scale);
  2488. float d2 = v_pos.x * v_pos.x + v_pos.y * v_pos.y;
  2489. float wobble_delta = 0.0;
  2490. if (u_wobble_enabled && d2 > 0.9) {
  2491. float theta = atan(v_pos.y, v_pos.x);
  2492. wobble_delta = sin(2.0 * (theta + u_time * u_wobble[0] + u_wobble[1]))
  2493. + sin(3.0 * (theta + u_time * u_wobble[2] + u_wobble[3]))
  2494. + sin(5.0 * (theta + u_time * u_wobble[4] + u_wobble[5]));
  2495. wobble_delta *= 0.75 / u_outer_radius;
  2496. }
  2497.  
  2498. float a = clamp(-blur * (d2 - (1.0 + wobble_delta)), 0.0, 1.0);
  2499.  
  2500. if (u_texture_enabled) {
  2501. out_color = texture(u_texture, v_t_coord);
  2502. }
  2503. out_color = vec4(out_color.rgb, 1) * out_color.a + u_color * (1.0 - out_color.a);
  2504.  
  2505. // outline
  2506. // d > 0.98 => d2 > 0.9604 (default)
  2507. // d > 0.96 => d2 > 0.9216 (thick)
  2508. float outline_d = u_outline_thick ? 0.9216 : 0.9604;
  2509. float oa = clamp(blur * (d2 - (outline_d + wobble_delta)), 0.0, 1.0) * u_outline_color.a;
  2510. out_color.rgb = out_color.rgb * (1.0 - oa) + u_outline_color.rgb * oa;
  2511.  
  2512. out_color.a *= a * u_alpha;
  2513. }
  2514. `),
  2515. );
  2516. uniforms.cell = getUniforms('cell', programs.cell, [
  2517. 'u_aspect_ratio', 'u_camera_pos', 'u_camera_scale',
  2518. 'u_alpha', 'u_color', 'u_outline_color', 'u_outline_thick', 'u_inner_radius', 'u_outer_radius', 'u_pos',
  2519. 'u_texture_enabled', 'u_time', 'u_wobble', 'u_wobble_enabled',
  2520. ]);
  2521.  
  2522.  
  2523.  
  2524. programs.text = program(
  2525. 'text',
  2526. shader('text.vShader', gl.VERTEX_SHADER, `#version 300 es
  2527. layout(location = 0) in vec2 a_pos;
  2528.  
  2529. uniform float u_aspect_ratio;
  2530. uniform vec2 u_camera_pos;
  2531. uniform float u_camera_scale;
  2532.  
  2533. uniform vec2 u_pos;
  2534. uniform float u_radius;
  2535. uniform bool u_subtext_centered;
  2536. uniform bool u_subtext_enabled;
  2537. uniform float u_subtext_offset;
  2538. uniform float u_subtext_scale;
  2539. uniform float u_text_scale;
  2540. uniform float u_text_aspect_ratio;
  2541.  
  2542. out vec2 v_pos;
  2543.  
  2544. void main() {
  2545. v_pos = a_pos / 1.01; // correct for jelly physics wobble effect
  2546.  
  2547. vec2 clip_space;
  2548. if (u_subtext_enabled) {
  2549. clip_space = v_pos * 0.5 * u_subtext_scale;
  2550. clip_space.x += u_subtext_offset * u_subtext_scale;
  2551. if (!u_subtext_centered) {
  2552. clip_space.y += 0.5 * u_text_scale;
  2553. clip_space.y += 0.25 * (u_subtext_scale - 1.0);
  2554. }
  2555. } else {
  2556. clip_space = v_pos * u_text_scale;
  2557. }
  2558.  
  2559. clip_space *= u_radius * 0.45 * vec2(u_text_aspect_ratio, 1.0);
  2560. clip_space += -u_camera_pos + u_pos;
  2561. clip_space *= u_camera_scale * vec2(1.0 / u_aspect_ratio, -1.0);
  2562. gl_Position = vec4(clip_space, 0, 1);
  2563. }
  2564. `),
  2565. shader('text.fShader', gl.FRAGMENT_SHADER, `#version 300 es
  2566. precision highp float;
  2567. in vec2 v_pos;
  2568.  
  2569. uniform float u_alpha;
  2570. uniform vec3 u_color1;
  2571. uniform vec3 u_color2;
  2572. uniform bool u_silhouette_enabled;
  2573.  
  2574. uniform sampler2D u_texture;
  2575. uniform sampler2D u_silhouette;
  2576.  
  2577. out vec4 out_color;
  2578.  
  2579. void main() {
  2580. vec2 t_coord = v_pos * 0.5 + 0.5;
  2581.  
  2582. float c2_alpha = (t_coord.x + t_coord.y) / 2.0;
  2583. vec4 color = vec4(u_color1 * (1.0 - c2_alpha) + u_color2 * c2_alpha, 1);
  2584. vec4 normal = texture(u_texture, t_coord);
  2585.  
  2586. if (u_silhouette_enabled) {
  2587. vec4 silhouette = texture(u_silhouette, t_coord);
  2588.  
  2589. // #fff - #000 => color (text)
  2590. // #fff - #fff => #fff (respect emoji)
  2591. // #888 - #888 => #888 (respect emoji)
  2592. // #fff - #888 => #888 + color/2 (blur/antialias)
  2593. out_color = silhouette + (normal - silhouette) * color;
  2594. } else {
  2595. out_color = normal * color;
  2596. }
  2597.  
  2598. out_color.a *= u_alpha;
  2599. }
  2600. `),
  2601. );
  2602. uniforms.text = getUniforms('text', programs.text, [
  2603. 'u_aspect_ratio', 'u_camera_pos', 'u_camera_scale',
  2604. 'u_alpha', 'u_color1', 'u_color2', 'u_pos', 'u_radius', 'u_silhouette', 'u_silhouette_enabled',
  2605. 'u_subtext_centered', 'u_subtext_enabled', 'u_subtext_offset', 'u_subtext_scale', 'u_text_aspect_ratio',
  2606. 'u_text_scale', 'u_texture',
  2607. ]);
  2608.  
  2609. // create and bind objects
  2610. const square = gl.createBuffer();
  2611. gl.bindBuffer(gl.ARRAY_BUFFER, square);
  2612. gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
  2613. // 1.01, rather than 1, to account for jelly physics wobbles
  2614. -1.01, -1.01,
  2615. 1.01, -1.01,
  2616. -1.01, 1.01,
  2617. 1.01, 1.01,
  2618. ]), gl.STATIC_DRAW);
  2619.  
  2620. const vao = gl.createVertexArray();
  2621. gl.bindVertexArray(vao);
  2622. gl.enableVertexAttribArray(0);
  2623. gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
  2624. }
  2625.  
  2626. init();
  2627.  
  2628. return { init, programs, uniforms };
  2629. })();
  2630.  
  2631.  
  2632.  
  2633. ///////////////////////////////
  2634. // Define Rendering Routines //
  2635. ///////////////////////////////
  2636. const render = (() => {
  2637. const render = {};
  2638. const gl = ui.game.gl;
  2639.  
  2640. // #1 : define small misc objects
  2641. // no point in breaking this across multiple lines
  2642. // eslint-disable-next-line max-len
  2643. const gridSrc = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAAAXNSR0IArs4c6QAAAKVJREFUaEPtkkEKwlAUxBzw/jeWL4J4gECkSrqftC/pzjnn9gfP3ofcf/mWbY8OuVLBilypxutbKlIRyUC/liQWYyuC1UnDikhiMbYiWJ00rIgkFmMrgtVJw4pIYjG2IlidNKyIJBZjK4LVScOKSGIxtiJYnTSsiCQWYyuC1UnDikhiMbYiWJ00rIgkFmMrgtVJw4pIYjG2IlidNPwU2TbpHV/DPgFxJfgvliP9RQAAAABJRU5ErkJggg==';
  2644.  
  2645.  
  2646.  
  2647. // #2 : define helper functions
  2648. const { resetTextureCache, textureFromCache } = (() => {
  2649. /** @type {Map<string, WebGLTexture | null>} */
  2650. const cache = new Map();
  2651. render.textureCache = cache;
  2652.  
  2653. return {
  2654. resetTextureCache: () => cache.clear(),
  2655. /**
  2656. * @param {string} src
  2657. */
  2658. textureFromCache: src => {
  2659. const cached = cache.get(src);
  2660. if (cached !== undefined)
  2661. return cached ?? undefined;
  2662.  
  2663. cache.set(src, null);
  2664.  
  2665. const image = new Image();
  2666. image.crossOrigin = 'anonymous';
  2667. image.addEventListener('load', () => {
  2668. const texture = /** @type {WebGLTexture} */ (gl.createTexture());
  2669. if (!texture) return;
  2670.  
  2671. gl.bindTexture(gl.TEXTURE_2D, texture);
  2672. gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
  2673. gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_NEAREST);
  2674. gl.generateMipmap(gl.TEXTURE_2D);
  2675. cache.set(src, texture);
  2676. });
  2677. image.src = src;
  2678.  
  2679. return undefined;
  2680. },
  2681. };
  2682. })();
  2683. render.resetTextureCache = resetTextureCache;
  2684.  
  2685. const { massTextFromCache, resetTextCache, textFromCache } = (() => {
  2686. /**
  2687. * @template {boolean} T
  2688. * @typedef {{
  2689. * aspectRatio: number,
  2690. * text: WebGLTexture | null,
  2691. * silhouette: WebGLTexture | null | undefined,
  2692. * accessed: number
  2693. * }} CacheEntry
  2694. */
  2695. /** @type {Map<string, CacheEntry<boolean>>} */
  2696. const cache = new Map();
  2697. render.textCache = cache;
  2698.  
  2699. setInterval(() => {
  2700. // remove text after not being used for 1 minute
  2701. const now = performance.now();
  2702. cache.forEach((entry, text) => {
  2703. if (now - entry.accessed > 60_000) {
  2704. // immediately delete text instead of waiting for GC
  2705. gl.deleteTexture(entry.text);
  2706. if (entry.silhouette !== undefined) gl.deleteTexture(entry.silhouette);
  2707. cache.delete(text);
  2708. }
  2709. });
  2710. }, 60_000);
  2711.  
  2712. const canvas = document.createElement('canvas');
  2713. const ctx = aux.require(
  2714. canvas.getContext('2d', { willReadFrequently: true }),
  2715. 'Unable to get 2D context for text drawing. This is probably your browser being weird, maybe reload ' +
  2716. 'the page?',
  2717. );
  2718.  
  2719. const baseTextSize = 96;
  2720.  
  2721. /**
  2722. * @param {string} text
  2723. * @param {boolean} silhouette
  2724. * @param {boolean} mass
  2725. * @returns {WebGLTexture | null}
  2726. */
  2727. const texture = (text, silhouette, mass) => {
  2728. const texture = gl.createTexture();
  2729. if (!texture) return texture;
  2730.  
  2731. const textSize = baseTextSize * (mass ? 0.5 * settings.massScaleFactor : settings.nameScaleFactor);
  2732. const lineWidth = Math.ceil(textSize / 10);
  2733.  
  2734. let font = '';
  2735. if (mass ? settings.massBold : settings.nameBold)
  2736. font = 'bold';
  2737. font += ' ' + textSize + 'px Ubuntu';
  2738.  
  2739. ctx.font = font;
  2740. canvas.width = ctx.measureText(text).width + lineWidth * 2;
  2741. canvas.height = textSize * 3;
  2742. ctx.clearRect(0, 0, canvas.width, canvas.height);
  2743.  
  2744. // setting canvas.width resets the canvas state
  2745. ctx.font = font;
  2746. ctx.lineJoin = 'round';
  2747. ctx.lineWidth = lineWidth;
  2748. ctx.fillStyle = silhouette ? '#000' : '#fff';
  2749. ctx.strokeStyle = '#000';
  2750. ctx.textBaseline = 'middle';
  2751.  
  2752. // add a space, which is to prevent sigmod from detecting the name
  2753. ctx.strokeText(text + ' ', lineWidth, textSize * 1.5);
  2754. ctx.fillText(text + ' ', lineWidth, textSize * 1.5);
  2755.  
  2756. const data = ctx.getImageData(0, 0, canvas.width, canvas.height);
  2757.  
  2758. gl.bindTexture(gl.TEXTURE_2D, texture);
  2759. gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, data);
  2760. gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST_MIPMAP_LINEAR);
  2761. gl.generateMipmap(gl.TEXTURE_2D);
  2762. return texture;
  2763. };
  2764.  
  2765. let drawnMassBold = false;
  2766. let drawnMassScaleFactor = -1;
  2767. let massAspectRatio = 1; // assumption: all mass digits are the same aspect ratio - true in the Ubuntu font
  2768. /** @type {(WebGLTexture | undefined)[]} */
  2769. const massTextCache = [];
  2770.  
  2771. /**
  2772. * @param {string} digit
  2773. * @returns {{ aspectRatio: number, texture: WebGLTexture | null }}
  2774. */
  2775. const massTextFromCache = digit => {
  2776. if (settings.massScaleFactor !== drawnMassScaleFactor || settings.massBold !== drawnMassBold) {
  2777. while (massTextCache.pop());
  2778. drawnMassScaleFactor = settings.massScaleFactor;
  2779. drawnMassBold = settings.massBold;
  2780. }
  2781.  
  2782. let cached = massTextCache[digit];
  2783. if (!cached) {
  2784. cached = massTextCache[digit] = texture(digit, false, true);
  2785. massAspectRatio = canvas.width / canvas.height;
  2786. }
  2787.  
  2788. return { aspectRatio: massAspectRatio, texture: cached };
  2789. };
  2790.  
  2791. const resetTextCache = () => {
  2792. cache.clear();
  2793. while (massTextCache.pop());
  2794. };
  2795.  
  2796. let drawnNamesBold = false;
  2797. let drawnNamesScaleFactor = -1;
  2798. /**
  2799. * @template {boolean} T
  2800. * @param {string} text
  2801. * @param {T} silhouette
  2802. * @returns {CacheEntry<T>}
  2803. */
  2804. const textFromCache = (text, silhouette) => {
  2805. if (drawnNamesScaleFactor !== settings.nameScaleFactor || drawnNamesBold !== settings.nameBold) {
  2806. cache.clear();
  2807. drawnNamesScaleFactor = settings.nameScaleFactor;
  2808. drawnNamesBold = settings.nameBold;
  2809. }
  2810.  
  2811. let entry = cache.get(text);
  2812. if (!entry) {
  2813. const shortened = aux.trim(text);
  2814. entry = {
  2815. text: texture(shortened, false, false),
  2816. aspectRatio: canvas.width / canvas.height, // mind the execution order
  2817. silhouette: silhouette ? texture(shortened, true, false) : undefined,
  2818. accessed: performance.now(),
  2819. };
  2820. cache.set(text, entry);
  2821. } else {
  2822. entry.accessed = performance.now();
  2823. }
  2824.  
  2825. if (silhouette && entry.silhouette === undefined)
  2826. entry.silhouette = texture(aux.trim(text), true, false);
  2827.  
  2828. return entry;
  2829. };
  2830.  
  2831. return { massTextFromCache, resetTextCache, textFromCache };
  2832. })();
  2833. render.resetTextCache = resetTextCache;
  2834.  
  2835.  
  2836.  
  2837. // #3 : define the render function
  2838. let fps = 0;
  2839. let lastFrame = performance.now();
  2840. const start = lastFrame;
  2841. function renderGame() {
  2842. const now = performance.now();
  2843. const dt = Math.max(now - lastFrame, 0.1) / 1000; // there's a chance (now - lastFrame) can be 0
  2844. fps += (1 / dt - fps) / 10;
  2845. lastFrame = now;
  2846.  
  2847. if (gl.isContextLost()) {
  2848. requestAnimationFrame(renderGame);
  2849. return;
  2850. }
  2851.  
  2852. // get settings
  2853. const cellColor = aux.sigmod?.cellColor ? aux.hex2rgb(aux.sigmod.cellColor) : undefined;
  2854. const hidePellets = aux.sigmod?.fps?.hideFood;
  2855. const mapColor = aux.sigmod?.mapColor ? aux.hex2rgb(aux.sigmod.mapColor) : undefined;
  2856. const outlineColor = aux.sigmod?.borderColor ? aux.hex2rgb(aux.sigmod.borderColor) : undefined;
  2857. const pelletColor = aux.sigmod?.foodColor ? aux.hex2rgb(aux.sigmod.foodColor) : undefined;
  2858. /** @type {object | undefined} */
  2859. const skinReplacement = aux.sigmod?.skinImage;
  2860. /** @type {string} */
  2861. const virusSrc = aux.sigmod?.virusImage ?? '/assets/images/viruses/2.png';
  2862.  
  2863. /** @type {[number, number, number] | undefined} */
  2864. let nameColor1;
  2865. /** @type {[number, number, number] | undefined} */
  2866. let nameColor2;
  2867. if (aux.sigmod?.nameColor) {
  2868. nameColor1 = nameColor2 = aux.hex2rgb(aux.sigmod.nameColor);
  2869. } else if (aux.sigmod?.gradientName?.enabled) {
  2870. // color1 and color2 are optional to set
  2871. if (aux.sigmod.gradientName.color1)
  2872. nameColor1 = aux.hex2rgb(aux.sigmod.gradientName.color1);
  2873.  
  2874. if (aux.sigmod.gradientName.color2)
  2875. nameColor2 = aux.hex2rgb(aux.sigmod.gradientName.color2);
  2876. }
  2877.  
  2878. const darkTheme = aux.setting('input#darkTheme', true);
  2879. const jellyPhysics = aux.setting('input#jellyPhysics', false);
  2880. const showBorder = aux.setting('input#showBorder', true);
  2881. const showGrid = aux.setting('input#showGrid', true);
  2882. const showMass = aux.setting('input#showMass', false);
  2883. const showMinimap = aux.setting('input#showMinimap', true);
  2884. const showNames = aux.setting('input#showNames', true);
  2885. const showSkins = aux.setting('input#showSkins', true);
  2886.  
  2887. /** @type {HTMLInputElement | null} */
  2888. const nickElement = document.querySelector('input#nick');
  2889. const nick = nickElement?.value ?? '?';
  2890.  
  2891. // note: most routines are named, for benchmarking purposes
  2892. (function updateCells() {
  2893. world.update();
  2894. sync.frame();
  2895. })();
  2896.  
  2897. (function setGlobalUniforms() {
  2898. const aspectRatio = ui.game.canvas.width / ui.game.canvas.height;
  2899. const cameraPosX = world.camera.x;
  2900. const cameraPosY = world.camera.y;
  2901. const cameraScale = world.camera.scale / 540; // (height of 1920x1080 / 2 = 540)
  2902.  
  2903. gl.useProgram(programs.bg);
  2904. gl.uniform1f(uniforms.bg.u_aspect_ratio, aspectRatio);
  2905. gl.uniform2f(uniforms.bg.u_camera_pos, cameraPosX, cameraPosY);
  2906. gl.uniform1f(uniforms.bg.u_camera_scale, cameraScale);
  2907.  
  2908. gl.useProgram(programs.cell);
  2909. gl.uniform1f(uniforms.cell.u_aspect_ratio, aspectRatio);
  2910. gl.uniform2f(uniforms.cell.u_camera_pos, cameraPosX, cameraPosY);
  2911. gl.uniform1f(uniforms.cell.u_camera_scale, cameraScale);
  2912. gl.uniform1f(uniforms.cell.u_time, (now - start) / 1000);
  2913.  
  2914. gl.useProgram(programs.text);
  2915. gl.uniform1f(uniforms.text.u_aspect_ratio, aspectRatio);
  2916. gl.uniform2f(uniforms.text.u_camera_pos, cameraPosX, cameraPosY);
  2917. gl.uniform1f(uniforms.text.u_camera_scale, cameraScale);
  2918. gl.uniform1f(uniforms.text.u_subtext_scale, settings.massScaleFactor);
  2919. gl.uniform1f(uniforms.text.u_text_scale, settings.nameScaleFactor);
  2920. gl.uniform1i(uniforms.text.u_texture, 0);
  2921. gl.uniform1i(uniforms.text.u_silhouette, 1);
  2922. })();
  2923.  
  2924. (function background() {
  2925. if (mapColor) {
  2926. gl.clearColor(...mapColor, 1);
  2927. } else if (darkTheme) {
  2928. gl.clearColor(0x11 / 255, 0x11 / 255, 0x11 / 255, 1); // #111
  2929. } else {
  2930. gl.clearColor(0xf2 / 255, 0xfb / 255, 0xff / 255, 1); // #f2fbff
  2931. }
  2932. gl.clear(gl.COLOR_BUFFER_BIT);
  2933.  
  2934. const gridTexture = textureFromCache(gridSrc);
  2935. if (!gridTexture) return;
  2936. gl.bindTexture(gl.TEXTURE_2D, gridTexture);
  2937.  
  2938. gl.useProgram(programs.bg);
  2939.  
  2940. if (showBorder && world.border) {
  2941. gl.uniform4f(uniforms.bg.u_border_color, 0, 0, 1, 1); // #00f
  2942. gl.uniform1fv(uniforms.bg.u_border_lrtb,
  2943. new Float32Array([ world.border.l, world.border.r, world.border.t, world.border.b ]));
  2944. } else {
  2945. gl.uniform4f(uniforms.bg.u_border_color, 0, 0, 0, 0); // transparent
  2946. gl.uniform1fv(uniforms.bg.u_border_lrtb, new Float32Array([ 0, 0, 0, 0 ]));
  2947. }
  2948.  
  2949. gl.uniform1i(uniforms.bg.u_dark_theme_enabled, Number(darkTheme));
  2950. gl.uniform1i(uniforms.bg.u_grid_enabled, Number(showGrid));
  2951.  
  2952. gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
  2953. })();
  2954.  
  2955. (function cells() {
  2956. /** @param {Cell} cell */
  2957. function calcAlpha(cell) {
  2958. let alpha = Math.min((now - cell.born) / 100, 1);
  2959. if (cell.dead)
  2960. alpha = Math.min(alpha, Math.max(1 - (now - cell.dead.at) / 100, 0));
  2961.  
  2962. return alpha;
  2963. }
  2964.  
  2965. // for white cell outlines
  2966. let nextCellIdx = world.mine.length;
  2967. const canSplit = world.mine.map(id => {
  2968. const cell = world.cells.get(id);
  2969. if (!cell) {
  2970. --nextCellIdx;
  2971. return false;
  2972. }
  2973.  
  2974. if (cell.nr < 128)
  2975. return false;
  2976.  
  2977. return nextCellIdx++ < 16;
  2978. });
  2979.  
  2980. /**
  2981. * @param {Cell} cell
  2982. * @param {number} alpha
  2983. */
  2984. function drawCell(cell, alpha) {
  2985. gl.useProgram(programs.cell);
  2986.  
  2987. gl.uniform1f(uniforms.cell.u_alpha, alpha * settings.cellOpacity);
  2988.  
  2989. if (jellyPhysics && cell.r > 20 && !cell.jagged) {
  2990. gl.uniform2f(uniforms.cell.u_pos, cell.jelly.x, cell.jelly.y);
  2991. gl.uniform1f(uniforms.cell.u_inner_radius, settings.jellySkinLag ? cell.jelly.r : cell.r);
  2992. gl.uniform1f(uniforms.cell.u_outer_radius, cell.jelly.r);
  2993. } else {
  2994. gl.uniform2f(uniforms.cell.u_pos, cell.x, cell.y);
  2995. gl.uniform1f(uniforms.cell.u_inner_radius, cell.r);
  2996. gl.uniform1f(uniforms.cell.u_outer_radius, cell.r);
  2997. }
  2998.  
  2999. if (jellyPhysics && settings.jellyWobble) {
  3000. gl.uniform1i(uniforms.cell.u_wobble_enabled, 1);
  3001. gl.uniform1fv(uniforms.cell.u_wobble, cell.jelly.wobble);
  3002. } else {
  3003. gl.uniform1i(uniforms.cell.u_wobble_enabled, 0);
  3004. }
  3005.  
  3006. if (cell.jagged) {
  3007. const virusTexture = textureFromCache(virusSrc);
  3008. if (!virusTexture)
  3009. return;
  3010.  
  3011. gl.uniform4f(uniforms.cell.u_color, 0, 0, 0, 0);
  3012. gl.uniform4f(uniforms.cell.u_outline_color, 0, 0, 0, 0);
  3013. gl.uniform1i(uniforms.cell.u_outline_thick, 0);
  3014. gl.uniform1i(uniforms.cell.u_texture_enabled, 1);
  3015. gl.bindTexture(gl.TEXTURE_2D, virusTexture);
  3016.  
  3017. gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
  3018. return;
  3019. }
  3020.  
  3021. if (cell.r <= 20) {
  3022. gl.uniform1i(uniforms.cell.u_outline_thick, 0);
  3023. if (pelletColor) {
  3024. gl.uniform4f(uniforms.cell.u_color, ...pelletColor, 1);
  3025. } else {
  3026. gl.uniform4f(uniforms.cell.u_color, ...cell.rgb, 1);
  3027. }
  3028. } else {
  3029. if (cellColor)
  3030. gl.uniform4f(uniforms.cell.u_color, ...cellColor, 1);
  3031. else
  3032. gl.uniform4f(uniforms.cell.u_color, ...cell.rgb, 1);
  3033. }
  3034.  
  3035. gl.uniform1i(uniforms.cell.u_texture_enabled, 0);
  3036. if (cell.r <= 20) {
  3037. gl.uniform4f(uniforms.cell.u_outline_color, 0, 0, 0, 0);
  3038. } else {
  3039. const myIndex = world.mine.indexOf(cell.id);
  3040. if (myIndex !== -1 && !canSplit[myIndex] && settings.unsplittableOpacity > 0) {
  3041. gl.uniform1i(uniforms.cell.u_outline_thick, 1);
  3042. if (darkTheme)
  3043. gl.uniform4f(uniforms.cell.u_outline_color, 1, 1, 1, settings.unsplittableOpacity);
  3044. else
  3045. gl.uniform4f(uniforms.cell.u_outline_color, 0, 0, 0, settings.unsplittableOpacity);
  3046. } else if (settings.cellOutlines) {
  3047. gl.uniform1i(uniforms.cell.u_outline_thick, 0);
  3048. if (outlineColor) {
  3049. gl.uniform4f(uniforms.cell.u_outline_color, ...outlineColor, 1);
  3050. } else {
  3051. gl.uniform4f(uniforms.cell.u_outline_color,
  3052. cell.rgb[0] * 0.9, cell.rgb[1] * 0.9, cell.rgb[2] * 0.9, 1);
  3053. }
  3054. } else {
  3055. gl.uniform4f(uniforms.cell.u_outline_color, 0, 0, 0, 0);
  3056. }
  3057.  
  3058. let skin = '';
  3059. if (settings.selfSkin && (world.mine.includes(cell.id) || world.mineDead.has(cell.id))) {
  3060. skin = settings.selfSkin;
  3061. } else {
  3062. for (const [_, data] of sync.others) {
  3063. if (data.owned.has(cell.id)) {
  3064. skin = data.skin;
  3065. break;
  3066. }
  3067. }
  3068.  
  3069. if (!skin && showSkins && cell.skin) {
  3070. if (skinReplacement && cell.skin.includes(skinReplacement.original + '.png'))
  3071. skin = skinReplacement.replaceImg;
  3072. else
  3073. skin = cell.skin;
  3074. }
  3075. }
  3076.  
  3077. if (skin) {
  3078. const texture = textureFromCache(skin);
  3079. if (texture) {
  3080. gl.uniform1i(uniforms.cell.u_texture_enabled, 1);
  3081. gl.bindTexture(gl.TEXTURE_2D, texture);
  3082. }
  3083. }
  3084. }
  3085.  
  3086. gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
  3087. }
  3088.  
  3089. /**
  3090. * @param {Cell} cell
  3091. * @param {number} alpha
  3092. */
  3093. function drawText(cell, alpha) {
  3094. const showThisName = showNames && cell.r > 75 && cell.name;
  3095. const showThisMass = showMass && cell.r > 75;
  3096. if (!showThisName && !showThisMass) return;
  3097.  
  3098. gl.useProgram(programs.text);
  3099.  
  3100. gl.uniform1f(uniforms.text.u_alpha, alpha);
  3101. if (jellyPhysics)
  3102. gl.uniform2f(uniforms.text.u_pos, cell.jelly.x, cell.jelly.y);
  3103. else
  3104. gl.uniform2f(uniforms.text.u_pos, cell.x, cell.y);
  3105. gl.uniform1f(uniforms.text.u_radius, cell.r);
  3106.  
  3107. let useSilhouette = false;
  3108. if (cell.sub) {
  3109. gl.uniform3f(uniforms.text.u_color1, 0xeb / 255, 0x95 / 255, 0x00 / 255); // #eb9500
  3110. gl.uniform3f(uniforms.text.u_color2, 0xe4 / 255, 0xb1 / 255, 0x10 / 255); // #e4b110
  3111. useSilhouette = true;
  3112. } else {
  3113. gl.uniform3f(uniforms.text.u_color1, 1, 1, 1);
  3114. gl.uniform3f(uniforms.text.u_color2, 1, 1, 1);
  3115. }
  3116.  
  3117. if (cell.name === nick) {
  3118. if (nameColor1) {
  3119. gl.uniform3f(uniforms.text.u_color1, ...nameColor1);
  3120. useSilhouette = true;
  3121. }
  3122.  
  3123. if (nameColor2) {
  3124. gl.uniform3f(uniforms.text.u_color2, ...nameColor2);
  3125. useSilhouette = true;
  3126. }
  3127. }
  3128.  
  3129. if (showThisName) {
  3130. const { aspectRatio, text, silhouette } = textFromCache(cell.name, useSilhouette);
  3131. gl.uniform1f(uniforms.text.u_text_aspect_ratio, aspectRatio);
  3132. gl.uniform1i(uniforms.text.u_silhouette_enabled, silhouette ? 1 : 0);
  3133. gl.uniform1i(uniforms.text.u_subtext_enabled, 0);
  3134.  
  3135. gl.bindTexture(gl.TEXTURE_2D, text);
  3136. if (silhouette) {
  3137. gl.activeTexture(gl.TEXTURE1);
  3138. gl.bindTexture(gl.TEXTURE_2D, silhouette);
  3139. gl.activeTexture(gl.TEXTURE0);
  3140. }
  3141.  
  3142. gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
  3143. }
  3144.  
  3145. if (showThisMass) {
  3146. gl.uniform1f(uniforms.text.u_alpha, alpha * settings.massOpacity);
  3147. gl.uniform1i(uniforms.text.u_silhouette_enabled, 0);
  3148. gl.uniform1i(uniforms.text.u_subtext_enabled, 1);
  3149.  
  3150. if (showThisName) {
  3151. gl.uniform1i(uniforms.text.u_subtext_centered, 0);
  3152. } else {
  3153. gl.uniform1i(uniforms.text.u_subtext_centered, 1);
  3154. }
  3155.  
  3156. // draw each digit separately, as Ubuntu makes them all the same width.
  3157. // significantly reduces the size of the text cache
  3158. const mass = Math.floor(cell.nr * cell.nr / 100).toString();
  3159. for (let i = 0; i < mass.length; ++i) {
  3160. const { aspectRatio, texture } = massTextFromCache(mass[i]);
  3161. gl.uniform1f(uniforms.text.u_text_aspect_ratio, aspectRatio);
  3162. gl.uniform1f(uniforms.text.u_subtext_offset, (i - (mass.length - 1) / 2) * 0.75);
  3163.  
  3164. gl.bindTexture(gl.TEXTURE_2D, texture);
  3165. gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
  3166. }
  3167. }
  3168. }
  3169.  
  3170. /** @type {Cell[]} */
  3171. const sorted = [];
  3172. world.cells.forEach(cell => {
  3173. if (cell.r > 20) {
  3174. // cell is probably important, order should be sorted
  3175. sorted.push(cell);
  3176. return;
  3177. }
  3178.  
  3179. if (cell.r <= 20 && hidePellets) return;
  3180. const alpha = calcAlpha(cell);
  3181. drawCell(cell, alpha);
  3182. });
  3183.  
  3184. sorted.sort((a, b) => a.r - b.r);
  3185.  
  3186. sorted.forEach(cell => {
  3187. const alpha = calcAlpha(cell);
  3188. drawCell(cell, alpha);
  3189. if (!cell.jagged)
  3190. drawText(cell, alpha);
  3191. });
  3192. })();
  3193.  
  3194. (function updateStats() {
  3195. ui.stats.matchTheme(); // not sure how to listen to when the checkbox changes when the game loads
  3196. if (showNames && world.leaderboard.length > 0)
  3197. ui.leaderboard.container.style.display = '';
  3198. else
  3199. ui.leaderboard.container.style.display = 'none';
  3200.  
  3201. let score = 0;
  3202. world.mine.forEach(id => {
  3203. const cell = world.cells.get(id);
  3204. if (!cell || cell.dead) return;
  3205.  
  3206. score += cell.nr * cell.nr / 100;
  3207. });
  3208.  
  3209. if (typeof aux.userData?.boost === 'number' && aux.userData.boost > Date.now())
  3210. score *= 2;
  3211.  
  3212. if (score > 0)
  3213. ui.stats.score.textContent = 'Score: ' + Math.floor(score);
  3214. else
  3215. ui.stats.score.textContent = '';
  3216.  
  3217. let measures = `${Math.floor(fps)} FPS`;
  3218. if (net.latency !== undefined) {
  3219. if (net.latency === -1)
  3220. measures += ' ????ms ping';
  3221. else
  3222. measures += ` ${Math.floor(net.latency)}ms ping`;
  3223. }
  3224.  
  3225. ui.stats.measures.textContent = measures;
  3226.  
  3227. if (score > world.stats.highestScore) {
  3228. world.stats.highestScore = score;
  3229. }
  3230. })();
  3231.  
  3232. (function minimap() {
  3233. if (!showMinimap) {
  3234. ui.minimap.canvas.style.display = 'none';
  3235. return;
  3236. } else {
  3237. ui.minimap.canvas.style.display = '';
  3238. }
  3239.  
  3240. const { border } = world;
  3241. if (!border) return;
  3242.  
  3243. // text needs to be small and sharp, i don't trust webgl with that, so we use a 2d context
  3244. const { canvas, ctx } = ui.minimap;
  3245. canvas.width = canvas.height = 200 * devicePixelRatio;
  3246.  
  3247. // sigmod overlay resizes itself differently, so we correct it whenever we need to
  3248. /** @type {HTMLCanvasElement | null} */
  3249. const sigmodMinimap = document.querySelector('canvas.minimap');
  3250. if (sigmodMinimap) {
  3251. // we need to check before updating the canvas, otherwise we will clear it
  3252. if (sigmodMinimap.style.width !== '200px' || sigmodMinimap.style.height !== '200px')
  3253. sigmodMinimap.style.width = sigmodMinimap.style.height = '200px';
  3254.  
  3255. if (sigmodMinimap.width !== canvas.width || sigmodMinimap.height !== canvas.height)
  3256. sigmodMinimap.width = sigmodMinimap.height = 200 * devicePixelRatio;
  3257. }
  3258.  
  3259. ctx.clearRect(0, 0, canvas.width, canvas.height);
  3260.  
  3261. const gameWidth = (border.r - border.l);
  3262. const gameHeight = (border.b - border.t);
  3263.  
  3264. // highlight current section
  3265. ctx.fillStyle = '#ff0';
  3266. ctx.globalAlpha = 0.3;
  3267.  
  3268. const sectionX = Math.floor((world.camera.x - border.l) / gameWidth * 5);
  3269. const sectionY = Math.floor((world.camera.y - border.t) / gameHeight * 5);
  3270. const sectorSize = canvas.width / 5;
  3271. ctx.fillRect(sectionX * sectorSize, sectionY * sectorSize, sectorSize, sectorSize);
  3272.  
  3273. // draw section names
  3274. ctx.font = `${Math.floor(sectorSize / 3)}px Ubuntu`;
  3275. ctx.fillStyle = darkTheme ? '#fff' : '#000';
  3276. ctx.globalAlpha = 0.3;
  3277. ctx.textAlign = 'center';
  3278. ctx.textBaseline = 'middle';
  3279.  
  3280. const cols = ['1', '2', '3', '4', '5'];
  3281. const rows = ['A', 'B', 'C', 'D', 'E'];
  3282. cols.forEach((col, y) => {
  3283. rows.forEach((row, x) => {
  3284. ctx.fillText(row + col, (x + 0.5) * sectorSize, (y + 0.5) * sectorSize);
  3285. });
  3286. });
  3287.  
  3288. ctx.globalAlpha = 1;
  3289.  
  3290.  
  3291.  
  3292. // draw cells
  3293. /** @param {Cell} cell */
  3294. const drawCell = function drawCell(cell) {
  3295. const x = (cell.x - border.l) / gameWidth * canvas.width;
  3296. const y = (cell.y - border.t) / gameHeight * canvas.height;
  3297. const r = Math.max(cell.r / gameWidth * canvas.width, 2);
  3298.  
  3299. ctx.fillStyle = aux.rgb2hex(cell.rgb);
  3300. ctx.beginPath();
  3301. ctx.moveTo(x + r, y);
  3302. ctx.arc(x, y, r, 0, 2 * Math.PI);
  3303. ctx.fill();
  3304. };
  3305.  
  3306. /**
  3307. * @param {number} x
  3308. * @param {number} y
  3309. * @param {string} name
  3310. */
  3311. const drawName = function drawName(x, y, name) {
  3312. x = (x - border.l) / gameWidth * canvas.width;
  3313. y = (y - border.t) / gameHeight * canvas.height;
  3314.  
  3315. ctx.fillStyle = '#fff';
  3316. // add a space to prevent sigmod from detecting names
  3317. ctx.fillText(name + ' ', x, y - 7 * devicePixelRatio - sectorSize / 6);
  3318. };
  3319.  
  3320. // draw clanmates first, below yourself
  3321. // we sort clanmates by color AND name, to ensure clanmates stay separate
  3322. /** @type {Map<string, { name: string, n: number, x: number, y: number }>} */
  3323. const avgPos = new Map();
  3324. world.clanmates.forEach(cell => {
  3325. if (world.mine.includes(cell.id)) return;
  3326. drawCell(cell);
  3327.  
  3328. const id = cell.name + cell.rgb[0] + cell.rgb[1] + cell.rgb[2];
  3329. const entry = avgPos.get(id);
  3330. if (entry) {
  3331. ++entry.n;
  3332. entry.x += cell.x;
  3333. entry.y += cell.y;
  3334. } else {
  3335. avgPos.set(id, { name: cell.name, n: 1, x: cell.x, y: cell.y });
  3336. }
  3337. });
  3338.  
  3339. avgPos.forEach(entry => {
  3340. drawName(entry.x / entry.n, entry.y / entry.n, entry.name);
  3341. });
  3342.  
  3343. // draw my cells above everyone else
  3344. let myName = '';
  3345. let ownN = 0;
  3346. let ownX = 0;
  3347. let ownY = 0;
  3348. world.mine.forEach(id => {
  3349. const cell = world.cells.get(id);
  3350. if (!cell) return;
  3351.  
  3352. drawCell(cell);
  3353. myName = cell.name;
  3354. ++ownN;
  3355. ownX += cell.x;
  3356. ownY += cell.y;
  3357. });
  3358.  
  3359. if (ownN <= 0) {
  3360. // if no cells were drawn, draw our spectate pos instead
  3361. const x = (world.camera.x - border.l) / gameWidth * canvas.width;
  3362. const y = (world.camera.y - border.t) / gameHeight * canvas.height;
  3363.  
  3364. ctx.fillStyle = '#faa';
  3365. ctx.beginPath();
  3366. ctx.moveTo(x + 5, y);
  3367. ctx.arc(x, y, 5 * devicePixelRatio, 0, 2 * Math.PI);
  3368. ctx.fill();
  3369. } else {
  3370. ownX /= ownN;
  3371. ownY /= ownN;
  3372. // draw name above player's cells
  3373. drawName(ownX, ownY, myName);
  3374.  
  3375. // send a hint to sigmod
  3376. ctx.globalAlpha = 0;
  3377. ctx.fillText(`X: ${ownX}, Y: ${ownY}`, 0, -1000);
  3378. }
  3379. })();
  3380.  
  3381. ui.chat.matchTheme();
  3382.  
  3383. requestAnimationFrame(renderGame);
  3384. }
  3385.  
  3386. renderGame();
  3387. return render;
  3388. })();
  3389.  
  3390.  
  3391.  
  3392. // @ts-expect-error for debugging purposes. dm me on discord @8y8x to work out stability if you need something
  3393. window.sigfix = { destructor, aux, ui, settings, sync, world, net, render };
  3394. })();