Sigmally Fixes V2

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

目前为 2024-04-20 提交的版本。查看 最新版本

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