Sigmally Fixes V2

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

当前为 2024-12-17 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Sigmally Fixes V2
  3. // @version 2.4.1
  4. // @description Easily 3X your FPS on Sigmally.com + many bug fixes + great for multiboxing + supports SigMod
  5. // @author 8y8x
  6. // @match https://*.sigmally.com/*
  7. // @license MIT
  8. // @grant none
  9. // @namespace https://8y8x.dev/sigmally-fixes
  10. // @icon https://raw.githubusercontent.com/8y8x/sigmally-fixes/refs/heads/main/icon.png
  11. // @compatible chrome Recommended for all users, works perfectly out of the box
  12. // @compatible opera Works fine, multiboxers may need to change some browser keybinds
  13. // @compatible edge Works fine, multiboxers may need to change some browser keybinds
  14. // ==/UserScript==
  15.  
  16. // @ts-check
  17. /* eslint
  18. camelcase: 'error',
  19. comma-dangle: ['error', 'always-multiline'],
  20. indent: ['error', 'tab', { SwitchCase: 1 }],
  21. max-len: ['error', { code: 120 }],
  22. no-console: ['error', { allow: ['warn', 'error'] }],
  23. no-trailing-spaces: 'error',
  24. quotes: ['error', 'single'],
  25. semi: 'error',
  26. */ // a light eslint configuration that doesn't compromise code quality
  27. 'use strict';
  28.  
  29. (async () => {
  30. const sfVersion = '2.4.1';
  31. // yes, this actually makes a significant difference
  32. const undefined = window.undefined;
  33.  
  34. ////////////////////////////////
  35. // Define Auxiliary Functions //
  36. ////////////////////////////////
  37. const aux = (() => {
  38. const aux = {};
  39.  
  40. /** @type {Map<string, string>} */
  41. aux.clans = new Map();
  42. function fetchClans() {
  43. fetch('https://sigmally.com/api/clans').then(r => r.json()).then(r => {
  44. if (r.status !== 'success') {
  45. setTimeout(() => fetchClans(), 10_000);
  46. return;
  47. }
  48.  
  49. aux.clans.clear();
  50. r.data.forEach(clan => {
  51. if (typeof clan._id !== 'string' || typeof clan.name !== 'string') return;
  52. aux.clans.set(clan._id, clan.name);
  53. });
  54.  
  55. // does not need to be updated often, but just enough so people who leave their tab open don't miss out
  56. setTimeout(() => fetchClans(), 600_000);
  57. }).catch(err => {
  58. console.warn('Error while fetching clans:', err);
  59. setTimeout(() => fetchClans(), 10_000);
  60. });
  61. }
  62. fetchClans();
  63.  
  64. /**
  65. * @template T
  66. * @param {T} x
  67. * @param {string} err should be readable and easily translatable
  68. * @returns {T extends (null | undefined | false | 0) ? never : T}
  69. */
  70. aux.require = (x, err) => {
  71. if (!x) {
  72. err = '[Sigmally Fixes]: ' + err;
  73. prompt(err, err); // use prompt, so people can paste the error message into google translate
  74. throw err;
  75. }
  76.  
  77. return /** @type {any} */ (x);
  78. };
  79.  
  80. /**
  81. * consistent exponential easing relative to 60fps.
  82. * for example, with a factor of 2, o=0, n=1:
  83. * - at 60fps, 0.5 is returned.
  84. * - at 30fps (after 2 frames), 0.75 is returned.
  85. * - at 15fps (after 4 frames), 0.875 is returned.
  86. * - at 120fps, 0.292893 is returned. if you called this again with o=0.292893, n=1, you would get 0.5.
  87. *
  88. * @param {number} o
  89. * @param {number} n
  90. * @param {number} factor
  91. * @param {number} dt in seconds
  92. */
  93. aux.exponentialEase = (o, n, factor, dt) => {
  94. return o + (n - o) * (1 - (1 - 1 / factor) ** (60 * dt));
  95. };
  96.  
  97. /**
  98. * @param {string} hex
  99. * @returns {[number, number, number, number]}
  100. */
  101. aux.hex2rgba = hex => {
  102. switch (hex.length) {
  103. case 4: // #rgb
  104. case 5: // #rgba
  105. return [
  106. (parseInt(hex[1], 16) || 0) / 15,
  107. (parseInt(hex[2], 16) || 0) / 15,
  108. (parseInt(hex[3], 16) || 0) / 15,
  109. hex.length === 5 ? (parseInt(hex[4], 16) || 0) / 15 : 1,
  110. ];
  111. case 7: // #rrggbb
  112. case 9: // #rrggbbaa
  113. return [
  114. (parseInt(hex.slice(1, 3), 16) || 0) / 255,
  115. (parseInt(hex.slice(3, 5), 16) || 0) / 255,
  116. (parseInt(hex.slice(5, 7), 16) || 0) / 255,
  117. hex.length === 9 ? (parseInt(hex.slice(7, 9), 16) || 0) / 255 : 1,
  118. ];
  119. default:
  120. return [1, 1, 1, 1];
  121. }
  122. };
  123.  
  124. /**
  125. * @param {number} r
  126. * @param {number} g
  127. * @param {number} b
  128. * @param {number} a
  129. */
  130. aux.rgba2hex = (r, g, b, a) => {
  131. return [
  132. '#',
  133. Math.floor(r * 255).toString(16).padStart(2, '0'),
  134. Math.floor(g * 255).toString(16).padStart(2, '0'),
  135. Math.floor(b * 255).toString(16).padStart(2, '0'),
  136. Math.floor(a * 255).toString(16).padStart(2, '0'),
  137. ].join('');
  138. };
  139.  
  140. // i don't feel like making an awkward adjustment to aux.rgba2hex
  141. /**
  142. * @param {number} r
  143. * @param {number} g
  144. * @param {number} b
  145. * @param {any} _a
  146. */
  147. aux.rgba2hex6 = (r, g, b, _a) => {
  148. return [
  149. '#',
  150. Math.floor(r * 255).toString(16).padStart(2, '0'),
  151. Math.floor(g * 255).toString(16).padStart(2, '0'),
  152. Math.floor(b * 255).toString(16).padStart(2, '0'),
  153. ].join('');
  154. };
  155.  
  156. /** @param {string} name */
  157. aux.parseName = name => name.match(/^\{.*?\}(.*)$/)?.[1] ?? name;
  158.  
  159. /** @param {string} skin */
  160. aux.parseSkin = skin => {
  161. if (!skin) return skin;
  162. skin = skin.replace('1%', '').replace('2%', '').replace('3%', '');
  163. return '/static/skins/' + skin + '.png';
  164. };
  165.  
  166. /**
  167. * @param {DataView} dat
  168. * @param {number} off
  169. * @returns {[string, number]}
  170. */
  171. aux.readZTString = (dat, off) => {
  172. const startOff = off;
  173. for (; off < dat.byteLength; ++off) {
  174. if (dat.getUint8(off) === 0) break;
  175. }
  176.  
  177. return [aux.textDecoder.decode(new DataView(dat.buffer, startOff, off - startOff)), off + 1];
  178. };
  179.  
  180. /** @type {{
  181. * cellColor?: [number, number, number, number],
  182. * foodColor?: [number, number, number, number],
  183. * mapColor?: [number, number, number, number],
  184. * outlineColor?: [number, number, number, number],
  185. * nameColor1?: [number, number, number, number],
  186. * nameColor2?: [number, number, number, number],
  187. * hidePellets?: boolean,
  188. * rapidFeedKey?: string,
  189. * removeOutlines?: boolean,
  190. * showNames?: boolean,
  191. * skinReplacement?: { original: string | null, replacement?: string | null, replaceImg?: string | null },
  192. * virusImage?: string,
  193. * } | undefined} */
  194. aux.sigmodSettings = undefined;
  195. setInterval(() => {
  196. // @ts-expect-error
  197. const sigmod = window.sigmod?.settings;
  198. if (sigmod) {
  199. let sigmodSettings = aux.sigmodSettings = {};
  200. /**
  201. * @param {'cellColor' | 'foodColor' | 'mapColor' | 'outlineColor' | 'nameColor1' | 'nameColor2'} prop
  202. * @param {any[]} lookups
  203. */
  204. const applyColor = (prop, lookups) => {
  205. for (const lookup of lookups) {
  206. if (lookup) {
  207. sigmodSettings[prop] = aux.hex2rgba(lookup);
  208. return;
  209. }
  210. }
  211. };
  212. applyColor('cellColor', [sigmod.game?.cellColor]);
  213. applyColor('foodColor', [sigmod.game?.foodColor]);
  214. applyColor('mapColor', [sigmod.game?.map?.color, sigmod.mapColor]);
  215. // sigmod treats the map border as cell borders for some reason
  216. if (!['#00f', '#00f0', '#0000ff', '#000000ffff'].includes(sigmod.game?.borderColor))
  217. applyColor('outlineColor', [sigmod.game?.borderColor]);
  218. // note: singular nameColor takes priority
  219. applyColor('nameColor1', [
  220. sigmod.game?.name?.color,
  221. sigmod.game?.name?.gradient?.enabled && sigmod.game.name.gradient.left,
  222. ]);
  223. applyColor('nameColor2', [
  224. sigmod.game?.name?.color,
  225. sigmod.game?.name?.gradient?.enabled && sigmod.game.name.gradient.right,
  226. ]);
  227. // v10 does not have a 'hide food' setting; check food's transparency
  228. aux.sigmodSettings.hidePellets = aux.sigmodSettings.foodColor?.[3] === 0;
  229. aux.sigmodSettings.removeOutlines = sigmod.game?.removeOutlines;
  230. aux.sigmodSettings.skinReplacement = sigmod.game?.skins;
  231. aux.sigmodSettings.virusImage = sigmod.game?.virusImage;
  232. aux.sigmodSettings.rapidFeedKey = sigmod.macros?.keys?.rapidFeed;
  233. // sigmod's showNames setting is always "true" interally (i think??)
  234. aux.sigmodSettings.showNames = aux.setting('input#showNames', true);
  235. }
  236. }, 200);
  237.  
  238. // patch some sigmod bugs
  239. let patchSigmodInterval;
  240. patchSigmodInterval = setInterval(() => {
  241. const sigmod = /** @type {any} */ (window).sigmod;
  242. if (!sigmod) return;
  243.  
  244. clearInterval(patchSigmodInterval);
  245.  
  246. // anchor chat and minimap to the screen, so scrolling to zoom doesn't move them
  247. // it's possible that cursed will change something at any time so i'm being safe here
  248. const minimapContainer = /** @type {HTMLElement | null} */ (document.querySelector('.minimapContainer'));
  249. if (minimapContainer) minimapContainer.style.position = 'fixed';
  250.  
  251. const modChat = /** @type {HTMLElement | null} */ (document.querySelector('.modChat'));
  252. if (modChat) modChat.style.position = 'fixed';
  253. }, 500);
  254.  
  255. /**
  256. * @param {string} selector
  257. * @param {boolean} value
  258. */
  259. aux.setting = (selector, value) => {
  260. /** @type {HTMLInputElement | null} */
  261. const el = document.querySelector(selector);
  262. return el ? el.checked : value;
  263. };
  264.  
  265. const settings = () => {
  266. try {
  267. // current skin is saved in localStorage
  268. aux.settings = JSON.parse(localStorage.getItem('settings') ?? '');
  269. } catch (_) {
  270. aux.settings = /** @type {any} */ ({});
  271. }
  272.  
  273. // sigmod forces dark theme to be enabled
  274. if (aux.sigmodSettings) {
  275. // sigmod doesn't have a checkbox for dark theme, so we infer it from the custom map color
  276. const { mapColor } = aux.sigmodSettings;
  277. aux.settings.darkTheme
  278. = mapColor ? (mapColor[0] < 0.6 && mapColor[1] < 0.6 && mapColor[2] < 0.6) : true;
  279. } else {
  280. aux.settings.darkTheme = aux.setting('input#darkTheme', true);
  281. }
  282. aux.settings.jellyPhysics = aux.setting('input#jellyPhysics', false);
  283. aux.settings.showBorder = aux.setting('input#showBorder', true);
  284. aux.settings.showClanmates = aux.setting('input#showClanmates', true);
  285. aux.settings.showGrid = aux.setting('input#showGrid', true);
  286. aux.settings.showMass = aux.setting('input#showMass', false);
  287. aux.settings.showMinimap = aux.setting('input#showMinimap', true);
  288. aux.settings.showSkins = aux.setting('input#showSkins', true);
  289. aux.settings.zoomout = aux.setting('input#moreZoom', true);
  290. return aux.settings;
  291. };
  292.  
  293. /** @type {{ darkTheme: boolean, jellyPhysics: boolean, showBorder: boolean, showClanmates: boolean,
  294. showGrid: boolean, showMass: boolean, showMinimap: boolean, showSkins: boolean, zoomout: boolean,
  295. gamemode: any, skin: any }} */
  296. aux.settings = settings();
  297. setInterval(settings, 250);
  298. // apply saved gamemode because sigmally fixes connects before the main game even loads
  299. if (aux.settings?.gamemode) {
  300. /** @type {HTMLSelectElement | null} */
  301. const gamemode = document.querySelector('select#gamemode');
  302. if (gamemode)
  303. gamemode.value = aux.settings.gamemode;
  304. }
  305.  
  306. aux.textEncoder = new TextEncoder();
  307. aux.textDecoder = new TextDecoder();
  308.  
  309. const trimCtx = aux.require(
  310. document.createElement('canvas').getContext('2d'),
  311. 'Unable to get 2D context for text utilities. This is probably your browser being dumb, maybe reload ' +
  312. 'the page?',
  313. );
  314. trimCtx.font = '20px Ubuntu';
  315. /**
  316. * trims text to a max of 250px at 20px font, same as vanilla sigmally
  317. * @param {string} text
  318. */
  319. aux.trim = text => {
  320. while (trimCtx.measureText(text).width > 250)
  321. text = text.slice(0, -1);
  322.  
  323. return text;
  324. };
  325.  
  326. /*
  327. If you have Sigmally open in two tabs and you're playing with an account, one has an outdated token while
  328. the other has the latest one. This causes problems because the tab with the old token does not work properly
  329. during the game (skin, XP) To fix this, the latest token is sent to the previously opened tab. This way you
  330. can collect XP in both tabs and use your selected skin.
  331. @czrsd
  332. */
  333. /** @type {{ token: string, updated: number } | undefined} */
  334. aux.token = undefined;
  335. const tokenChannel = new BroadcastChannel('sigfix-token');
  336. tokenChannel.addEventListener('message', msg => {
  337. /** @type {{ token: string, updated: number }} */
  338. const token = msg.data;
  339. if (!aux.token || aux.token.updated < token.updated)
  340. aux.token = token;
  341. });
  342.  
  343. /** @type {object | undefined} */
  344. aux.userData = undefined;
  345. aux.oldFetch = fetch.bind(window);
  346. // this is the best method i've found to get the userData object, since game.js uses strict mode
  347. Object.defineProperty(window, 'fetch', {
  348. value: new Proxy(fetch, {
  349. apply: (target, thisArg, args) => {
  350. let url = args[0];
  351. const data = args[1];
  352. if (typeof url === 'string') {
  353. if (url.includes('/server/recaptcha/v3'))
  354. return new Promise(() => { }); // block game.js from attempting to go through captcha flow
  355.  
  356. // game.js doesn't think we're connected to a server, we default to eu0 because that's the
  357. // default everywhere else
  358. if (url.includes('/userdata/')) url = url.replace('///', '//eu0.sigmally.com/server/');
  359.  
  360. // patch the current token in the url and body of the request
  361. if (aux.token) {
  362. // 128 hex characters surrounded by non-hex characters (lookahead and lookbehind)
  363. const tokenTest = /(?<![0-9a-fA-F])[0-9a-fA-F]{128}(?![0-9a-fA-F])/g;
  364. url = url.replaceAll(tokenTest, aux.token.token);
  365. if (typeof data?.body === 'string')
  366. data.body = data.body.replaceAll(tokenTest, aux.token.token);
  367. }
  368.  
  369. args[0] = url;
  370. args[1] = data;
  371. }
  372.  
  373. return target.apply(thisArg, args).then(res => new Proxy(res, {
  374. get: (target, prop, _receiver) => {
  375. if (prop !== 'json') {
  376. const val = target[prop];
  377. if (typeof val === 'function')
  378. return val.bind(target);
  379. else
  380. return val;
  381. }
  382.  
  383. return () => target.json().then(obj => {
  384. if (obj?.body?.user) {
  385. aux.userData = obj.body.user;
  386. // NaN if invalid / undefined
  387. let updated = Number(new Date(aux.userData.updateTime));
  388. if (Number.isNaN(updated))
  389. updated = Date.now();
  390.  
  391. if (!aux.token || updated >= aux.token.updated) {
  392. aux.token = { token: aux.userData.token, updated };
  393. tokenChannel.postMessage(aux.token);
  394. }
  395. }
  396.  
  397. return obj;
  398. });
  399. },
  400. }));
  401. },
  402. }),
  403. });
  404.  
  405. /** @param {number} ms */
  406. aux.wait = ms => new Promise(resolve => setTimeout(resolve, ms));
  407.  
  408. return aux;
  409. })();
  410.  
  411.  
  412.  
  413. ////////////////////////
  414. // Destroy Old Client //
  415. ////////////////////////
  416. const destructor = (() => {
  417. const destructor = {};
  418. // #1 : kill the rendering process
  419. const oldRQA = requestAnimationFrame;
  420. window.requestAnimationFrame = function (fn) {
  421. try {
  422. throw new Error();
  423. } catch (err) {
  424. // prevent drawing the game, but do NOT prevent saving settings (which is called on RQA)
  425. if (!err.stack.includes('/game.js') || err.stack.includes('HTML'))
  426. return oldRQA(fn);
  427. }
  428.  
  429. return -1;
  430. };
  431.  
  432. // #2 : kill access to using a WebSocket
  433. destructor.realWebSocket = WebSocket;
  434. Object.defineProperty(window, 'WebSocket', {
  435. value: new Proxy(WebSocket, {
  436. construct(_target, argArray, _newTarget) {
  437. if (argArray[0]?.includes('sigmally.com')) {
  438. throw new Error('Nope :) - hooked by Sigmally Fixes');
  439. }
  440.  
  441. // @ts-expect-error
  442. return new destructor.realWebSocket(...argArray);
  443. },
  444. }),
  445. });
  446.  
  447. /** @type {{ status: 'left' | 'pending', started: number } | undefined} */
  448. destructor.respawnBlock = undefined;
  449.  
  450. const cmdRepresentation = new TextEncoder().encode('/leaveworld').toString();
  451. /** @type {WeakSet<WebSocket>} */
  452. destructor.safeWebSockets = new WeakSet();
  453. destructor.realWsSend = WebSocket.prototype.send;
  454. WebSocket.prototype.send = function (x) {
  455. if (!destructor.safeWebSockets.has(this) && this.url.includes('sigmally.com')) {
  456. this.onclose = null;
  457. this.close();
  458. throw new Error('Nope :) - hooked by Sigmally Fixes');
  459. }
  460.  
  461. if (settings.blockNearbyRespawns) {
  462. let buf;
  463. if (x instanceof ArrayBuffer) buf = x;
  464. else if (x instanceof DataView) buf = x.buffer;
  465. else if (x instanceof Uint8Array) buf = x.buffer;
  466.  
  467. if (buf && buf.byteLength === '/leaveworld'.length + 3
  468. && new Uint8Array(buf).toString().includes(cmdRepresentation)) {
  469. // block respawns if we haven't actually respawned yet (with a 500ms max in case something fails)
  470. if (performance.now() - (destructor.respawnBlock?.started ?? -Infinity) < 500) return;
  471. destructor.respawnBlock = undefined;
  472. // trying to respawn; see if we are nearby an alive multi-tab
  473. if (world.mine.length > 0) {
  474. world.moveCamera();
  475. for (const data of sync.others.values()) {
  476. const d = Math.hypot(data.camera.tx - world.camera.tx, data.camera.ty - world.camera.ty);
  477. if (data.owned.size > 0 && d <= 7500)
  478. return;
  479. }
  480. }
  481.  
  482. // we are allowing a respawn, take note
  483. destructor.respawnBlock = { status: 'pending', started: performance.now() };
  484. }
  485. }
  486.  
  487. return destructor.realWsSend.apply(this, arguments);
  488. };
  489.  
  490. // #3 : prevent keys from being registered by the game
  491. setInterval(() => {
  492. onkeydown = null;
  493. onkeyup = null;
  494. }, 50);
  495.  
  496. return destructor;
  497. })();
  498.  
  499.  
  500.  
  501. /////////////////////
  502. // Prepare Game UI //
  503. /////////////////////
  504. const ui = (() => {
  505. const ui = {};
  506.  
  507. (() => {
  508. const title = document.querySelector('#title');
  509. if (!title) return;
  510.  
  511. const watermark = document.createElement('span');
  512. watermark.innerHTML = `<a href="https://greasyfork.org/scripts/483587/versions" \
  513. target="_blank">Sigmally Fixes ${sfVersion}</a> by yx`;
  514. if (sfVersion.includes('BETA')) {
  515. watermark.innerHTML += ' <br><a \
  516. href="https://raw.githubusercontent.com/8y8x/sigmally-fixes/refs/heads/main/sigmally-fixes.user.js"\
  517. target="_blank">[Update beta here]</a>';
  518. }
  519. title.insertAdjacentElement('afterend', watermark);
  520.  
  521. // check if this version is problematic, don't do anything if this version is too new to be in versions.json
  522. // take care to ensure users can't be logged
  523. fetch('https://raw.githubusercontent.com/8y8x/sigmally-fixes/main/versions.json')
  524. .then(res => res.json())
  525. .then(res => {
  526. if (sfVersion in res && !res[sfVersion].ok && res[sfVersion].alert) {
  527. const color = res[sfVersion].color || '#f00';
  528. const box = document.createElement('div');
  529. box.style.cssText = `background: ${color}3; border: 1px solid ${color}; width: 100%; \
  530. height: fit-content; font-size: 1em; padding: 5px; margin: 5px 0; border-radius: 3px; \
  531. color: ${color}`;
  532. box.innerHTML = String(res[sfVersion].alert)
  533. .replace(/\<|\>/g, '') // never allow html tag injection
  534. .replace(/\{link\}/g, '<a href="https://greasyfork.org/scripts/483587">[click here]</a>')
  535. .replace(/\{autolink\}/g, '<a href="\
  536. https://update.greasyfork.org/scripts/483587/Sigmally%20Fixes%20V2.user.js">\
  537. [click here]</a>');
  538.  
  539. watermark.insertAdjacentElement('afterend', box);
  540. }
  541. })
  542. .catch(err => console.warn('Failed to check Sigmally Fixes version:', err));
  543. })();
  544.  
  545. ui.game = (() => {
  546. const game = {};
  547.  
  548. /** @type {HTMLCanvasElement | null} */
  549. const oldCanvas = document.querySelector('canvas#canvas');
  550. if (!oldCanvas) {
  551. throw 'exiting script - no canvas found';
  552. }
  553.  
  554. const newCanvas = document.createElement('canvas');
  555. newCanvas.id = 'sf-canvas';
  556. newCanvas.style.cssText = `background: #003; width: 100vw; height: 100vh; position: fixed; top: 0; left: 0;
  557. z-index: 1;`;
  558. game.canvas = newCanvas;
  559. (document.querySelector('body div') ?? document.body).appendChild(newCanvas);
  560.  
  561. // leave the old canvas so the old client can actually run
  562. oldCanvas.style.display = 'none';
  563.  
  564. // forward macro inputs from the canvas to the old one - this is for sigmod mouse button controls
  565. newCanvas.addEventListener('mousedown', e => oldCanvas.dispatchEvent(new MouseEvent('mousedown', e)));
  566. newCanvas.addEventListener('mouseup', e => oldCanvas.dispatchEvent(new MouseEvent('mouseup', e)));
  567. // forward mouse movements from the old canvas to the window - this is for sigmod keybinds that move
  568. // the mouse
  569. oldCanvas.addEventListener('mousemove', e => dispatchEvent(new MouseEvent('mousemove', e)));
  570.  
  571. const gl = aux.require(
  572. newCanvas.getContext('webgl2', { alpha: false, depth: false }),
  573. 'Couldn\'t get WebGL2 context. Possible causes:\r\n' +
  574. '- Maybe GPU/Hardware acceleration needs to be enabled in your browser settings; \r\n' +
  575. '- Maybe your browser is just acting weird and it might fix itself after a restart; \r\n' +
  576. '- Maybe your GPU drivers are exceptionally old.',
  577. );
  578.  
  579. game.gl = gl;
  580.  
  581. // indicate that we will restore the context
  582. newCanvas.addEventListener('webglcontextlost', e => {
  583. e.preventDefault(); // signal that we want to restore the context
  584. // cleanup old caches (after render), as we can't do this within initWebGL()
  585. render.resetTextCache();
  586. render.resetTextureCache();
  587. });
  588. newCanvas.addEventListener('webglcontextrestored', () => glconf.init());
  589.  
  590. function resize() {
  591. newCanvas.width = Math.ceil(innerWidth * devicePixelRatio);
  592. newCanvas.height = Math.ceil(innerHeight * devicePixelRatio);
  593. game.gl.viewport(0, 0, newCanvas.width, newCanvas.height);
  594. }
  595.  
  596. addEventListener('resize', resize);
  597. resize();
  598.  
  599. return game;
  600. })();
  601.  
  602. ui.stats = (() => {
  603. const container = document.createElement('div');
  604. container.style.cssText = 'position: fixed; top: 10px; left: 10px; width: 400px; height: fit-content; \
  605. user-select: none; z-index: 2; transform-origin: top left;';
  606. document.body.appendChild(container);
  607.  
  608. const score = document.createElement('div');
  609. score.style.cssText = 'font-family: Ubuntu; font-size: 30px; color: #fff; line-height: 1.0;';
  610. container.appendChild(score);
  611.  
  612. const measures = document.createElement('div');
  613. measures.style.cssText = 'font-family: Ubuntu; font-size: 20px; color: #fff; line-height: 1.1;';
  614. container.appendChild(measures);
  615.  
  616. const misc = document.createElement('div');
  617. // white-space: pre; allows using \r\n to insert line breaks
  618. misc.style.cssText = 'font-family: Ubuntu; font-size: 14px; color: #fff; white-space: pre; \
  619. line-height: 1.1; opacity: 0.5;';
  620. container.appendChild(misc);
  621.  
  622. let statsLastUpdated = performance.now();
  623. const update = () => {
  624. let color = aux.settings.darkTheme ? '#fff' : '#000';
  625. score.style.color = color;
  626. measures.style.color = color;
  627. misc.style.color = color;
  628.  
  629. score.style.fontWeight = measures.style.fontWeight = settings.boldUi ? 'bold' : 'normal';
  630. measures.style.opacity = settings.showStats ? '1' : '0.5';
  631. misc.style.opacity = settings.showStats ? '0.5' : '0';
  632.  
  633. statsLastUpdated = performance.now();
  634. if ((aux.sigmodSettings?.showNames ?? true) && world.leaderboard.length > 0)
  635. ui.leaderboard.container.style.display = '';
  636. else {
  637. ui.leaderboard.container.style.display = 'none';
  638. }
  639.  
  640. let scoreVal = 0;
  641. for (const id of world.mine) {
  642. const cell = world.cells.get(id);
  643. if (cell) {
  644. // we use nr because this is what the server sees; interpolated mass is irrelevant
  645. // we also floor every cell individually, so the score matches what you could count yourself
  646. scoreVal += Math.floor(cell.nr * cell.nr / 100);
  647. }
  648. }
  649. if (typeof aux.userData?.boost === 'number' && aux.userData.boost > Date.now())
  650. scoreVal *= 2;
  651. if (scoreVal > world.stats.highestScore) {
  652. world.stats.highestScore = scoreVal;
  653. }
  654.  
  655. score.textContent = scoreVal > 0 ? ('Score: ' + Math.floor(scoreVal)) : '';
  656.  
  657. let measuresText = `${Math.floor(render.fps)} FPS`;
  658. if (net.latency !== undefined) {
  659. if (net.latency === -1)
  660. measuresText += ' ????ms ping';
  661. else
  662. measuresText += ` ${Math.floor(net.latency)}ms ping`;
  663. }
  664.  
  665. measures.textContent = measuresText;
  666. };
  667. // if the player starts lagging, we still need to update the stats
  668. setInterval(() => {
  669. if (performance.now() - statsLastUpdated > 250)
  670. update();
  671. }, 250);
  672.  
  673. /** @param {object} statData */
  674. function updateMisc(statData) {
  675. let uptime;
  676. if (statData.uptime < 60) {
  677. uptime = '<1min';
  678. } else {
  679. uptime = Math.floor(statData.uptime / 60 % 60) + 'min';
  680. if (statData.uptime >= 60 * 60)
  681. uptime = Math.floor(statData.uptime / 60 / 60 % 24) + 'hr ' + uptime;
  682. if (statData.uptime >= 24 * 60 * 60)
  683. uptime = Math.floor(statData.uptime / 24 / 60 / 60 % 60) + 'd ' + uptime;
  684. }
  685.  
  686. misc.textContent = [
  687. `${statData.name} (${statData.mode})`,
  688. `${statData.playersTotal} / ${statData.playersLimit} players`,
  689. `${statData.playersAlive} playing`,
  690. `${statData.playersSpect} spectating`,
  691. `${(statData.update * 2.5).toFixed(1)}% load @ ${uptime}`,
  692. ].join('\r\n');
  693. }
  694.  
  695. function matchTheme() {
  696. let color = aux.settings.darkTheme ? '#fff' : '#000';
  697. score.style.color = color;
  698. measures.style.color = color;
  699. misc.style.color = color;
  700. }
  701.  
  702. matchTheme();
  703.  
  704. return { container, score, measures, misc, update, updateMisc, matchTheme };
  705. })();
  706.  
  707. ui.leaderboard = (() => {
  708. const container = document.createElement('div');
  709. container.style.cssText = 'position: fixed; top: 10px; right: 10px; width: 200px; height: fit-content; \
  710. user-select: none; z-index: 2; background: #0006; padding: 15px 5px; transform-origin: top right; \
  711. display: none;';
  712. document.body.appendChild(container);
  713.  
  714. const title = document.createElement('div');
  715. title.style.cssText = 'font-family: Ubuntu; font-size: 30px; color: #fff; text-align: center; width: 100%;';
  716. title.textContent = 'Leaderboard';
  717. container.appendChild(title);
  718.  
  719. const linesContainer = document.createElement('div');
  720. linesContainer.style.cssText = 'font-family: Ubuntu; font-size: 20px; line-height: 1.2; width: 100%; \
  721. height: fit-content; text-align: center; white-space: pre; overflow: hidden;';
  722. container.appendChild(linesContainer);
  723.  
  724. const lines = [];
  725. for (let i = 0; i < 11; ++i) {
  726. const line = document.createElement('div');
  727. line.style.display = 'none';
  728. linesContainer.appendChild(line);
  729. lines.push(line);
  730. }
  731.  
  732. function update() {
  733. const friends = /** @type {any} */ (window).sigmod?.friend_names;
  734. const friendSettings = /** @type {any} */ (window).sigmod?.friends_settings;
  735. world.leaderboard.forEach((entry, i) => {
  736. const line = lines[i];
  737. if (!line) return;
  738.  
  739. line.style.display = 'block';
  740. line.textContent = `${entry.place ?? i + 1}. ${entry.name || 'An unnamed cell'}`;
  741. if (entry.me)
  742. line.style.color = '#faa';
  743. else if (friends instanceof Set && friends.has(entry.name) && friendSettings?.highlight_friends)
  744. line.style.color = friendSettings.highlight_color;
  745. else if (entry.sub)
  746. line.style.color = '#ffc826';
  747. else
  748. line.style.color = '#fff';
  749. });
  750.  
  751. for (let i = world.leaderboard.length; i < lines.length; ++i)
  752. lines[i].style.display = 'none';
  753.  
  754. container.style.fontWeight = settings.boldUi ? 'bold' : 'normal';
  755. }
  756.  
  757. return { container, title, linesContainer, lines, update };
  758. })();
  759.  
  760. /** @type {HTMLElement} */
  761. const mainMenu = aux.require(
  762. document.querySelector('#__line1')?.parentElement,
  763. 'Can\'t find the main menu UI. Try reloading the page?',
  764. );
  765.  
  766. /** @type {HTMLElement} */
  767. const statsContainer = aux.require(
  768. document.querySelector('#__line2'),
  769. 'Can\'t find the death screen UI. Try reloading the page?',
  770. );
  771.  
  772. /** @type {HTMLElement} */
  773. const continueButton = aux.require(
  774. document.querySelector('#continue_button'),
  775. 'Can\'t find the continue button (on death). Try reloading the page?',
  776. );
  777.  
  778. /** @type {HTMLElement | null} */
  779. const menuLinks = document.querySelector('#menu-links');
  780. /** @type {HTMLElement | null} */
  781. const overlay = document.querySelector('#overlays');
  782.  
  783. // sigmod uses this to detect if the menu is closed or not, otherwise this is unnecessary
  784. /** @type {HTMLElement | null} */
  785. const menuWrapper = document.querySelector('#menu-wrapper');
  786.  
  787. let escOverlayVisible = true;
  788. /**
  789. * @param {boolean} [show]
  790. */
  791. ui.toggleEscOverlay = show => {
  792. escOverlayVisible = show ?? !escOverlayVisible;
  793. if (escOverlayVisible) {
  794. mainMenu.style.display = '';
  795. if (overlay) overlay.style.display = '';
  796. if (menuLinks) menuLinks.style.display = '';
  797. if (menuWrapper) menuWrapper.style.display = '';
  798.  
  799. ui.deathScreen.hide();
  800. } else {
  801. mainMenu.style.display = 'none';
  802. if (overlay) overlay.style.display = 'none';
  803. if (menuLinks) menuLinks.style.display = 'none';
  804. if (menuWrapper) menuWrapper.style.display = 'none';
  805. }
  806. };
  807.  
  808. ui.escOverlayVisible = () => escOverlayVisible;
  809.  
  810. ui.deathScreen = (() => {
  811. const deathScreen = {};
  812.  
  813. continueButton.addEventListener('click', () => {
  814. ui.toggleEscOverlay(true);
  815. });
  816.  
  817. // i'm not gonna buy a boost to try and figure out how this thing works
  818. /** @type {HTMLElement | null} */
  819. const bonus = document.querySelector('#menu__bonus');
  820. if (bonus) bonus.style.display = 'none';
  821.  
  822. /**
  823. * @param {{ foodEaten: number, highestScore: number, highestPosition: number,
  824. * spawnedAt: number | undefined }} stats
  825. */
  826. deathScreen.show = stats => {
  827. const foodEatenElement = document.querySelector('#food_eaten');
  828. if (foodEatenElement)
  829. foodEatenElement.textContent = stats.foodEaten.toString();
  830.  
  831. const highestMassElement = document.querySelector('#highest_mass');
  832. if (highestMassElement)
  833. highestMassElement.textContent = Math.round(stats.highestScore).toString();
  834.  
  835. const highestPositionElement = document.querySelector('#top_leaderboard_position');
  836. if (highestPositionElement)
  837. highestPositionElement.textContent = stats.highestPosition.toString();
  838.  
  839. const timeAliveElement = document.querySelector('#time_alive');
  840. if (timeAliveElement) {
  841. let time;
  842. if (stats.spawnedAt === undefined)
  843. time = 0;
  844. else
  845. time = (performance.now() - stats.spawnedAt) / 1000;
  846. const hours = Math.floor(time / 60 / 60);
  847. const mins = Math.floor(time / 60 % 60);
  848. const seconds = Math.floor(time % 60);
  849.  
  850. timeAliveElement.textContent = `${hours ? hours + ' h' : ''} ${mins ? mins + ' m' : ''} `
  851. + `${seconds ? seconds + ' s' : ''}`;
  852. }
  853.  
  854. statsContainer.classList.remove('line--hidden');
  855. ui.toggleEscOverlay(false);
  856. if (overlay) overlay.style.display = '';
  857.  
  858. stats.foodEaten = 0;
  859. stats.highestScore = 0;
  860. stats.highestPosition = 0;
  861. stats.spawnedAt = undefined;
  862.  
  863. // refresh ads... ...yep
  864. const { adSlot4, adSlot5, adSlot6, googletag } = /** @type {any} */ (window);
  865. if (googletag) {
  866. googletag.cmd.push(() => googletag.display(adSlot4));
  867. googletag.cmd.push(() => googletag.display(adSlot5));
  868. googletag.cmd.push(() => googletag.display(adSlot6));
  869. }
  870. };
  871.  
  872. deathScreen.hide = () => {
  873. const shown = !statsContainer?.classList.contains('line--hidden');
  874. statsContainer?.classList.add('line--hidden');
  875. const { googletag } = /** @type {any} */ (window);
  876. if (shown && googletag) {
  877. googletag.cmd.push(() => googletag.pubads().refresh());
  878. }
  879. };
  880.  
  881. return deathScreen;
  882. })();
  883.  
  884. ui.minimap = (() => {
  885. const canvas = document.createElement('canvas');
  886. canvas.style.cssText = 'position: fixed; bottom: 0; right: 0; background: #0006; width: 200px; \
  887. height: 200px; z-index: 2; user-select: none;';
  888. canvas.width = canvas.height = 200;
  889. document.body.appendChild(canvas);
  890.  
  891. const ctx = aux.require(
  892. canvas.getContext('2d', { willReadFrequently: false }),
  893. 'Unable to get 2D context for the minimap. This is probably your browser being dumb, maybe reload ' +
  894. 'the page?',
  895. );
  896.  
  897. return { canvas, ctx };
  898. })();
  899.  
  900. ui.chat = (() => {
  901. const chat = {};
  902.  
  903. const block = aux.require(
  904. document.querySelector('#chat_block'),
  905. 'Can\'t find the chat UI. Try reloading the page?',
  906. );
  907.  
  908. /**
  909. * @param {ParentNode} root
  910. * @param {string} selector
  911. */
  912. function clone(root, selector) {
  913. /** @type {HTMLElement} */
  914. const old = aux.require(
  915. root.querySelector(selector),
  916. `Can't find this chat element: ${selector}. Try reloading the page?`,
  917. );
  918.  
  919. const el = /** @type {HTMLElement} */ (old.cloneNode(true));
  920. el.id = '';
  921. old.style.display = 'none';
  922. old.insertAdjacentElement('afterend', el);
  923.  
  924. return el;
  925. }
  926.  
  927. // can't just replace the chat box - otherwise sigmod can't hide it - so we make its children invisible
  928. // elements grabbed with clone() are only styled by their class, not id
  929. const toggle = clone(document, '#chat_vsbltyBtn');
  930. const scrollbar = clone(document, '#chat_scrollbar');
  931. const thumb = clone(scrollbar, '#chat_thumb');
  932.  
  933. const input = chat.input = /** @type {HTMLInputElement} */ (aux.require(
  934. document.querySelector('#chat_textbox'),
  935. 'Can\'t find the chat textbox. Try reloading the page?',
  936. ));
  937.  
  938. // allow zooming in/out on trackpad without moving the UI
  939. input.style.position = 'fixed';
  940. toggle.style.position = 'fixed';
  941. scrollbar.style.position = 'fixed';
  942.  
  943. const list = document.createElement('div');
  944. list.style.cssText = 'width: 400px; height: 182px; position: fixed; bottom: 54px; left: 46px; \
  945. overflow: hidden; user-select: none; z-index: 301;';
  946. block.appendChild(list);
  947.  
  948. let toggled = true;
  949. toggle.style.borderBottomLeftRadius = '10px'; // a bug fix :p
  950. toggle.addEventListener('click', () => {
  951. toggled = !toggled;
  952. input.style.display = toggled ? '' : 'none';
  953. scrollbar.style.display = toggled ? 'block' : 'none';
  954. list.style.display = toggled ? '' : 'none';
  955.  
  956. if (toggled) {
  957. toggle.style.borderTopRightRadius = toggle.style.borderBottomRightRadius = '';
  958. toggle.style.opacity = '';
  959. } else {
  960. toggle.style.borderTopRightRadius = toggle.style.borderBottomRightRadius = '10px';
  961. toggle.style.opacity = '0.25';
  962. }
  963. });
  964.  
  965. scrollbar.style.display = 'block';
  966. let scrollTop = 0; // keep a float here, because list.scrollTop is always casted to an int
  967. let thumbHeight = 1;
  968. let lastY;
  969. thumb.style.height = '182px';
  970.  
  971. function updateThumb() {
  972. thumb.style.bottom = (1 - list.scrollTop / (list.scrollHeight - 182)) * (182 - thumbHeight) + 'px';
  973. }
  974.  
  975. function scroll() {
  976. if (scrollTop >= list.scrollHeight - 182 - 40) {
  977. // close to bottom, snap downwards
  978. list.scrollTop = scrollTop = list.scrollHeight - 182;
  979. }
  980.  
  981. thumbHeight = Math.min(Math.max(182 / list.scrollHeight, 0.1), 1) * 182;
  982. thumb.style.height = thumbHeight + 'px';
  983. updateThumb();
  984. }
  985.  
  986. let scrolling = false;
  987. thumb.addEventListener('mousedown', () => void (scrolling = true));
  988. addEventListener('mouseup', () => void (scrolling = false));
  989. addEventListener('mousemove', e => {
  990. const deltaY = e.clientY - lastY;
  991. lastY = e.clientY;
  992.  
  993. if (!scrolling) return;
  994. e.preventDefault();
  995.  
  996. if (lastY === undefined) {
  997. lastY = e.clientY;
  998. return;
  999. }
  1000.  
  1001. list.scrollTop = scrollTop = Math.min(Math.max(
  1002. scrollTop + deltaY * list.scrollHeight / 182, 0), list.scrollHeight - 182);
  1003. updateThumb();
  1004. });
  1005.  
  1006. let lastWasBarrier = true; // init to true, so we don't print a barrier as the first ever message (ugly)
  1007. /**
  1008. * @param {string} authorName
  1009. * @param {[number, number, number, number]} rgb
  1010. * @param {string} text
  1011. * @param {boolean} server
  1012. */
  1013. chat.add = (authorName, rgb, text, server) => {
  1014. lastWasBarrier = false;
  1015.  
  1016. const container = document.createElement('div');
  1017. const author = document.createElement('span');
  1018. author.style.cssText = `color: ${aux.rgba2hex(...rgb)}; padding-right: 0.75em;`;
  1019. author.textContent = aux.trim(authorName);
  1020. container.appendChild(author);
  1021.  
  1022. const msg = document.createElement('span');
  1023. if (server) msg.style.cssText = `color: ${aux.rgba2hex(...rgb)}`;
  1024. msg.textContent = aux.trim(text);
  1025. container.appendChild(msg);
  1026.  
  1027. while (list.children.length > 100)
  1028. list.firstChild?.remove();
  1029.  
  1030. list.appendChild(container);
  1031.  
  1032. scroll();
  1033. };
  1034.  
  1035. chat.barrier = () => {
  1036. if (lastWasBarrier) return;
  1037. lastWasBarrier = true;
  1038.  
  1039. const barrier = document.createElement('div');
  1040. barrier.style.cssText = 'width: calc(100% - 20px); height: 1px; background: #8888; margin: 10px;';
  1041. list.appendChild(barrier);
  1042.  
  1043. scroll();
  1044. };
  1045.  
  1046. chat.matchTheme = () => {
  1047. list.style.color = aux.settings.darkTheme ? '#fffc' : '#000c';
  1048. // make author names darker in light theme
  1049. list.style.filter = aux.settings.darkTheme ? '' : 'brightness(75%)';
  1050. };
  1051.  
  1052. return chat;
  1053. })();
  1054.  
  1055. /** @param {string} msg */
  1056. ui.error = msg => {
  1057. const modal = /** @type {HTMLElement | null} */ (document.querySelector('#errormodal'));
  1058. const desc = document.querySelector('#errormodal p');
  1059. if (desc)
  1060. desc.innerHTML = msg;
  1061.  
  1062. if (modal)
  1063. modal.style.display = 'block';
  1064. };
  1065.  
  1066. // sigmod quick fix
  1067. (() => {
  1068. // the play timer is inserted below the top-left stats, but because we offset them, we need to offset this
  1069. // too
  1070. const style = document.createElement('style');
  1071. style.textContent = '.playTimer { transform: translate(5px, 10px); }';
  1072. document.head.appendChild(style);
  1073. })();
  1074.  
  1075. return ui;
  1076. })();
  1077.  
  1078.  
  1079.  
  1080. /////////////////////////
  1081. // Create Options Menu //
  1082. /////////////////////////
  1083. const settings = (() => {
  1084. const settings = {
  1085. /** @type {'auto' | 'never'} */
  1086. autoZoom: 'auto',
  1087. background: '',
  1088. blockBrowserKeybinds: false,
  1089. blockNearbyRespawns: false,
  1090. boldUi: false,
  1091. cellGlow: false,
  1092. cellOpacity: 1,
  1093. cellOutlines: true,
  1094. clans: false,
  1095. clanScaleFactor: 1,
  1096. drawDelay: 120,
  1097. jellySkinLag: true,
  1098. massBold: false,
  1099. massOpacity: 1,
  1100. massScaleFactor: 1,
  1101. mergeCamera: false,
  1102. mergeViewArea: false,
  1103. nameBold: false,
  1104. nameScaleFactor: 1,
  1105. outlineMulti: 0.2,
  1106. // delta's default colors, #ff00aa and #ffffff
  1107. outlineMultiColor: /** @type {[number, number, number, number]} */ ([1, 0, 2/3, 1]),
  1108. outlineMultiInactiveColor: /** @type {[number, number, number, number]} */ ([1, 1, 1, 1]),
  1109. pelletGlow: false,
  1110. scrollFactor: 1,
  1111. selfSkin: '',
  1112. showStats: true,
  1113. syncSkin: true,
  1114. textOutlinesFactor: 1,
  1115. tracer: false,
  1116. unsplittableOpacity: 1,
  1117. };
  1118.  
  1119. try {
  1120. Object.assign(settings, JSON.parse(localStorage.getItem('sigfix') ?? ''));
  1121. } catch (_) { }
  1122.  
  1123. /** @type {Set<() => void>} */
  1124. const onSaves = new Set();
  1125.  
  1126. const channel = new BroadcastChannel('sigfix-settings');
  1127. channel.addEventListener('message', msg => {
  1128. Object.assign(settings, msg.data);
  1129. onSaves.forEach(fn => fn());
  1130. });
  1131.  
  1132. // #1 : define helper functions
  1133. /**
  1134. * @param {string} html
  1135. * @returns {HTMLElement}
  1136. */
  1137. function fromHTML(html) {
  1138. const div = document.createElement('div');
  1139. div.innerHTML = html;
  1140. return /** @type {HTMLElement} */ (div.firstElementChild);
  1141. }
  1142.  
  1143. function save() {
  1144. localStorage.setItem('sigfix', JSON.stringify(settings));
  1145. /** @type {any} */
  1146. const replicated = { ...settings };
  1147. delete replicated.selfSkin;
  1148. channel.postMessage(replicated);
  1149. }
  1150.  
  1151. /**
  1152. * @template O, T
  1153. * @typedef {{ [K in keyof O]: O[K] extends T ? K : never }[keyof O]} PropertyOfType
  1154. */
  1155.  
  1156. const vanillaMenu = document.querySelector('#cm_modal__settings .ctrl-modal__content');
  1157. vanillaMenu?.appendChild(fromHTML(`
  1158. <div class="menu__item">
  1159. <div style="width: 100%; height: 1px; background: #bfbfbf;"></div>
  1160. </div>
  1161. `));
  1162.  
  1163. const vanillaContainer = document.createElement('div');
  1164. vanillaContainer.className = 'menu__item';
  1165. vanillaMenu?.appendChild(vanillaContainer);
  1166.  
  1167. const sigmodContainer = document.createElement('div');
  1168. sigmodContainer.className = 'mod_tab scroll';
  1169. sigmodContainer.style.display = 'none';
  1170.  
  1171. /**
  1172. * @param {PropertyOfType<typeof settings, number>} property
  1173. * @param {string} title
  1174. * @param {number | undefined} initial
  1175. * @param {number} min
  1176. * @param {number} max
  1177. * @param {number} step
  1178. * @param {number} decimals
  1179. * @param {boolean} double
  1180. * @param {string} help
  1181. */
  1182. function slider(property, title, initial, min, max, step, decimals, double, help) {
  1183. /**
  1184. * @param {HTMLInputElement} slider
  1185. * @param {HTMLInputElement} display
  1186. */
  1187. const listen = (slider, display) => {
  1188. slider.value = settings[property].toString();
  1189. display.value = settings[property].toFixed(decimals);
  1190.  
  1191. slider.addEventListener('input', () => {
  1192. settings[property] = Number(slider.value);
  1193. display.value = settings[property].toFixed(decimals);
  1194. save();
  1195. });
  1196.  
  1197. display.addEventListener('change', () => {
  1198. const value = Number(display.value);
  1199. if (!Number.isNaN(value))
  1200. settings[property] = value;
  1201.  
  1202. display.value = slider.value = settings[property].toFixed(decimals);
  1203. save();
  1204. });
  1205.  
  1206. onSaves.add(() => {
  1207. slider.value = settings[property].toString();
  1208. display.value = settings[property].toFixed(decimals);
  1209. });
  1210. };
  1211.  
  1212. const datalist = `<datalist id="sf-${property}-markers"> <option value="${initial}"></option> </datalist>`;
  1213. const vanilla = fromHTML(`
  1214. <div style="height: ${double ? '50' : '25'}px; position: relative;" title="${help}">
  1215. <div style="height: 25px; line-height: 25px; position: absolute; top: 0; left: 0;">${title}</div>
  1216. <div style="height: 25px; margin-left: 5px; position: absolute; right: 0; bottom: 0;">
  1217. <input id="sf-${property}" style="display: block; float: left; height: 25px; line-height: 25px;\
  1218. margin-left: 5px;" min="${min}" max="${max}" step="${step}" value="${initial}"
  1219. list="sf-${property}-markers" type="range" />
  1220. ${initial !== undefined ? datalist : ''}
  1221. <input id="sf-${property}-display" style="display: block; float: left; height: 25px; \
  1222. line-height: 25px; width: 50px; text-align: right;" />
  1223. </div>
  1224. </div>
  1225. `);
  1226. listen(
  1227. /** @type {HTMLInputElement} */(vanilla.querySelector(`input#sf-${property}`)),
  1228. /** @type {HTMLInputElement} */(vanilla.querySelector(`input#sf-${property}-display`)));
  1229. vanillaContainer.appendChild(vanilla);
  1230.  
  1231. const datalistSm
  1232. = `<datalist id="sfsm-${property}-markers"> <option value="${initial}"></option> </datalist>`;
  1233. const sigmod = fromHTML(`
  1234. <div class="modRowItems justify-sb" style="padding: 5px 10px;" title="${help}">
  1235. <span>${title}</span>
  1236. <span class="justify-sb">
  1237. <input id="sfsm-${property}" style="width: 200px;" type="range" min="${min}" max="${max}"
  1238. step="${step}" value="${initial}" list="sfsm-${property}-markers" />
  1239. ${initial !== undefined ? datalistSm : ''}
  1240. <input id="sfsm-${property}-display" class="text-center form-control" style="border: none; \
  1241. width: 50px; margin: 0 15px;" />
  1242. </span>
  1243. </div>
  1244. `);
  1245. listen(
  1246. /** @type {HTMLInputElement} */(sigmod.querySelector(`input#sfsm-${property}`)),
  1247. /** @type {HTMLInputElement} */(sigmod.querySelector(`input#sfsm-${property}-display`)));
  1248. sigmodContainer.appendChild(sigmod);
  1249. }
  1250.  
  1251. /**
  1252. * @param {PropertyOfType<typeof settings, string>} property
  1253. * @param {string} title
  1254. * @param {string} placeholder
  1255. * @param {boolean} sync
  1256. * @param {string} help
  1257. */
  1258. function input(property, title, placeholder, sync, help) {
  1259. /**
  1260. * @param {HTMLInputElement} input
  1261. */
  1262. const listen = input => {
  1263. let oldValue = input.value = settings[property];
  1264.  
  1265. input.addEventListener('input', () => {
  1266. oldValue = settings[property] = /** @type {any} */ (input.value);
  1267. save();
  1268. });
  1269.  
  1270. onSaves.add(() => {
  1271. if (sync) input.value = settings[property];
  1272. else input.value = settings[property] = /** @type {any} */ (oldValue);
  1273. });
  1274. };
  1275.  
  1276. const vanilla = fromHTML(`
  1277. <div style="height: 50px; position: relative;" title="${help}">
  1278. <div style="height: 25px; line-height: 25px; position: absolute; top: 0; left: 0;">${title}</div>
  1279. <div style="height: 25px; margin-left: 5px; position: absolute; right: 0; bottom: 0;">
  1280. <input id="sf-${property}" placeholder="${placeholder}" type="text" />
  1281. </div>
  1282. </div>
  1283. `);
  1284. listen(/** @type {HTMLInputElement} */(vanilla.querySelector(`input#sf-${property}`)));
  1285. vanillaContainer.appendChild(vanilla);
  1286.  
  1287. const sigmod = fromHTML(`
  1288. <div class="modRowItems justify-sb" style="padding: 5px 10px;" title="${help}">
  1289. <span>${title}</span>
  1290. <input class="modInput" id="sfsm-${property}" placeholder="${placeholder}" \
  1291. style="width: 250px;" type="text" />
  1292. </div>
  1293. `);
  1294. listen(/** @type {HTMLInputElement} */(sigmod.querySelector(`input#sfsm-${property}`)));
  1295. sigmodContainer.appendChild(sigmod);
  1296. }
  1297.  
  1298. /**
  1299. * @param {PropertyOfType<typeof settings, boolean>} property
  1300. * @param {string} title
  1301. * @param {string} help
  1302. */
  1303. function checkbox(property, title, help) {
  1304. /**
  1305. * @param {HTMLInputElement} input
  1306. */
  1307. const listen = input => {
  1308. input.checked = settings[property];
  1309.  
  1310. input.addEventListener('input', () => {
  1311. settings[property] = input.checked;
  1312. save();
  1313. });
  1314.  
  1315. onSaves.add(() => input.checked = settings[property]);
  1316. };
  1317.  
  1318. const vanilla = fromHTML(`
  1319. <div style="height: 25px; position: relative;" title="${help}">
  1320. <div style="height: 25px; line-height: 25px; position: absolute; top: 0; left: 0;">${title}</div>
  1321. <div style="height: 25px; margin-left: 5px; position: absolute; right: 0; bottom: 0;">
  1322. <input id="sf-${property}" type="checkbox" />
  1323. </div>
  1324. </div>
  1325. `);
  1326. listen(/** @type {HTMLInputElement} */(vanilla.querySelector(`input#sf-${property}`)));
  1327. vanillaContainer.appendChild(vanilla);
  1328.  
  1329. const sigmod = fromHTML(`
  1330. <div class="modRowItems justify-sb" style="padding: 5px 10px;" title="${help}">
  1331. <span>${title}</span>
  1332. <div style="width: 75px; text-align: center;">
  1333. <div class="modCheckbox" style="display: inline-block;">
  1334. <input id="sfsm-${property}" type="checkbox" />
  1335. <label class="cbx" for="sfsm-${property}"></label>
  1336. </div>
  1337. </div>
  1338. </div>
  1339. `);
  1340. listen(/** @type {HTMLInputElement} */(sigmod.querySelector(`input#sfsm-${property}`)));
  1341. sigmodContainer.appendChild(sigmod);
  1342. }
  1343.  
  1344. /**
  1345. * @param {PropertyOfType<typeof settings, [number, number, number, number]>} property
  1346. * @param {string} title
  1347. * @param {string} help
  1348. */
  1349. function color(property, title, help) {
  1350. /**
  1351. * @param {HTMLInputElement} input
  1352. * @param {HTMLInputElement} visible
  1353. */
  1354. const listen = (input, visible) => {
  1355. input.value = aux.rgba2hex6(...settings[property]);
  1356. visible.checked = settings[property][3] > 0;
  1357.  
  1358. const changed = () => {
  1359. settings[property] = aux.hex2rgba(input.value);
  1360. settings[property][3] = visible.checked ? 1 : 0;
  1361. save();
  1362. };
  1363. input.addEventListener('input', changed);
  1364. visible.addEventListener('input', changed);
  1365.  
  1366. onSaves.add(() => {
  1367. input.value = aux.rgba2hex6(...settings[property]);
  1368. visible.checked = settings[property][3] > 0;
  1369. });
  1370. };
  1371.  
  1372. const vanilla = fromHTML(`
  1373. <div style="height: 25px; position: relative;" title="${help}">
  1374. <div style="height: 25px; line-height: 25px; position: absolute; top: 0; left: 0;">${title}</div>
  1375. <div style="height: 25px; margin-left: 5px; position: absolute; right: 0; bottom: 0;">
  1376. <input id="sf-${property}-visible" type="checkbox" />
  1377. <input id="sf-${property}" type="color" />
  1378. </div>
  1379. </div>
  1380. `);
  1381. listen(/** @type {HTMLInputElement} */(vanilla.querySelector(`input#sf-${property}`)),
  1382. /** @type {HTMLInputElement} */(vanilla.querySelector(`input#sf-${property}-visible`)));
  1383. vanillaContainer.appendChild(vanilla);
  1384.  
  1385. const sigmod = fromHTML(`
  1386. <div class="modRowItems justify-sb" style="padding: 5px 10px;" title="${help}">
  1387. <span>${title}</span>
  1388. <div style="width: 75px; text-align: center;">
  1389. <div class="modCheckbox" style="display: inline-block;">
  1390. <input id="sfsm-${property}-visible" type="checkbox" />
  1391. <label class="cbx" for="sfsm-${property}-visible"></label>
  1392. </div>
  1393. <input id="sfsm-${property}" type="color" />
  1394. </div>
  1395. </div>
  1396. `);
  1397. listen(/** @type {HTMLInputElement} */(sigmod.querySelector(`input#sfsm-${property}`)),
  1398. /** @type {HTMLInputElement} */(sigmod.querySelector(`input#sfsm-${property}-visible`)));
  1399. sigmodContainer.appendChild(sigmod);
  1400. }
  1401.  
  1402. /**
  1403. * @param {PropertyOfType<typeof settings, string>} property
  1404. * @param {string} title
  1405. * @param {[string, string][]} options
  1406. * @param {string} help
  1407. */
  1408. function dropdown(property, title, options, help) {
  1409. /**
  1410. * @param {HTMLSelectElement} input
  1411. */
  1412. const listen = input => {
  1413. input.value = settings[property];
  1414.  
  1415. const changed = () => {
  1416. settings[property] = /** @type {any} */ (input.value);
  1417. save();
  1418. };
  1419. input.addEventListener('input', changed);
  1420.  
  1421. onSaves.add(() => {
  1422. input.value = settings[property];
  1423. });
  1424. };
  1425.  
  1426. const vanilla = fromHTML(`
  1427. <div style="height: 25px; position: relative;" title="${help}">
  1428. <div style="height: 25px; line-height: 25px; position: absolute; top: 0; left: 0;">${title}</div>
  1429. <div style="height: 25px; margin-left: 5px; position: absolute; right: 0; bottom: 0;">
  1430. <select id="sf-${property}">
  1431. ${options.map(([value, name]) => `<option value="${value}">${name}</option>`).join('\n')}
  1432. </select>
  1433. </div>
  1434. </div>
  1435. `);
  1436. listen(/** @type {HTMLSelectElement} */(vanilla.querySelector(`select#sf-${property}`)));
  1437. vanillaContainer.appendChild(vanilla);
  1438.  
  1439. const sigmod = fromHTML(`
  1440. <div class="modRowItems justify-sb" style="padding: 5px 10px;" title="${help}">
  1441. <span>${title}</span>
  1442. <select class="form-control" id="sfsm-${property}" style="width: 250px;">
  1443. ${options.map(([value, name]) => `<option value="${value}">${name}</option>`).join('\n')}
  1444. </select>
  1445. </div>
  1446. `);
  1447. listen(/** @type {HTMLSelectElement} */(sigmod.querySelector(`select#sfsm-${property}`)));
  1448. sigmodContainer.appendChild(sigmod);
  1449. }
  1450.  
  1451. function separator(text = '•') {
  1452. vanillaContainer.appendChild(fromHTML(`<div style="text-align: center; width: 100%;">${text}</div>`));
  1453. sigmodContainer.appendChild(fromHTML(`<span class="text-center">${text}</span>`));
  1454. }
  1455.  
  1456. // #2 : generate ui for settings
  1457. separator('Hover over a setting for more info');
  1458. slider('drawDelay', 'Draw delay', 120, 40, 300, 1, 0, false,
  1459. 'How long (in ms) cells will lag behind for. Lower values mean cells will very quickly catch up to where ' +
  1460. 'they actually are.');
  1461. checkbox('cellOutlines', 'Cell outlines', 'Whether the subtle dark outlines around cells (including skins) ' +
  1462. 'should draw.');
  1463. slider('cellOpacity', 'Cell opacity', undefined, 0.5, 1, 0.005, 3, false,
  1464. 'How opaque cells should be. 1 = fully visible, 0 = invisible. It can be helpful to see the size of a ' +
  1465. 'smaller cell under a big cell.');
  1466. input('selfSkin', 'Self skin URL (not synced)', 'https://i.imgur.com/...', false,
  1467. 'Direct URL to a custom skin for yourself. Not visible to others. You are able to use different skins ' +
  1468. 'for different tabs.');
  1469. input('background', 'Map background image', 'https://i.imgur.com/...', true,
  1470. 'A square background image to use within the entire map border. Images under 1024x1024 will be treated ' +
  1471. 'as a repeating pattern, where 50 pixels = 1 grid square.');
  1472. checkbox('tracer', 'Lines between cells and mouse', 'If enabled, draws a line between all of the cells you ' +
  1473. 'control and your mouse. Useful as a hint to your subconscious about which tab you\'re currently on.');
  1474. separator('• multibox •');
  1475. checkbox('mergeCamera', 'Merge camera between tabs',
  1476. 'Whether to place the camera in between your nearby tabs. This makes tab changes while multiboxing ' +
  1477. 'completely seamless (a sort of \'one-tab\'). This setting uses a weighted camera, which focuses the ' +
  1478. 'camera at your center of mass (i.e. your tiny cells won\'t mess up your aim).');
  1479. checkbox('mergeViewArea', 'Combine visible cells between tabs',
  1480. 'When enabled, *all* tabs will share what cells they see between each other. Sigmally Fixes puts a lot ' +
  1481. 'of effort into making this as seamless as possible, so it can be laggy on lower-end devices.');
  1482. slider('outlineMulti', 'Current tab cell outline thickness', 0.2, 0, 1, 0.01, 2, true,
  1483. 'Draws an inverse outline on your cells, the thickness being a % of your cell radius. This only shows ' +
  1484. 'when \'merge camera between tabs\' is enabled and when you\'re near one of your tabs.');
  1485. color('outlineMultiColor', 'Current tab outline color',
  1486. 'The outline color of your current multibox tab.');
  1487. color('outlineMultiInactiveColor', 'Other tab outline color',
  1488. 'The outline color for the cells of your other unfocused multibox tabs. Turn off the checkbox to disable.');
  1489. separator('• inputs •');
  1490. slider('scrollFactor', 'Zoom speed', 1, 0.05, 1, 0.05, 2, false,
  1491. 'A smaller zoom speed lets you fine-tune your zoom.');
  1492. dropdown('autoZoom', 'Auto-zoom', [['auto', 'When not multiboxing'], ['never', 'Never']],
  1493. 'When enabled, automatically zooms in/out for you based on how big you are. ');
  1494. checkbox('blockBrowserKeybinds', 'Block all browser keybinds',
  1495. 'When enabled, only Ctrl+Tab and F11 are allowed to be pressed. You must be in fullscreen, and ' +
  1496. 'non-Chrome browsers probably won\'t respect this setting. Doesn\'t work for Ctrl+W anymore: get a ' +
  1497. 'browser extension to block it for you.');
  1498. checkbox('blockNearbyRespawns', 'Block respawns near other tabs',
  1499. 'Disables the respawn keybind (SigMod-only) when near one of your bigger tabs.');
  1500. separator('• text •');
  1501. slider('nameScaleFactor', 'Name scale factor', 1, 0.5, 2, 0.01, 2, false, 'The size multiplier of names.');
  1502. slider('massScaleFactor', 'Mass scale factor', 1, 0.5, 4, 0.01, 2, false,
  1503. 'The size multiplier of mass (which is half the size of names)');
  1504. slider('massOpacity', 'Mass opacity', 1, 0, 1, 0.01, 2, false,
  1505. 'The opacity of the mass text. You might find it visually appealing to have mass be a little dimmer than ' +
  1506. 'names.');
  1507. checkbox('nameBold', 'Bold name text', 'Uses the bold Ubuntu font for names (like Agar.io).');
  1508. checkbox('massBold', 'Bold mass text', 'Uses a bold font for mass.');
  1509. checkbox('clans', 'Show clans', 'When enabled, shows the name of the clan a player is in above their name. ' +
  1510. 'If you turn off names (using SigMod), then player names will be replaced with their clan\'s.');
  1511. slider('clanScaleFactor', 'Clan scale factor', 1, 0.5, 4, 0.01, 2, false,
  1512. 'The size multiplier of a player\'s clan displayed above their name (only when \'Show clans\' is ' +
  1513. 'enabled). When names are off, names will be replaced with clans and use the name scale factor instead.');
  1514. slider('textOutlinesFactor', 'Text outline thickness factor', 1, 0, 2, 0.01, 2, false,
  1515. 'The multiplier of the thickness of the black stroke around names, mass, and clans on cells. You can set ' +
  1516. 'this to 0 to disable outlines AND text shadows.');
  1517. separator('• other •');
  1518. slider('unsplittableOpacity', 'Unsplittable cell outline opacity', 1, 0, 1, 0.01, 2, true,
  1519. 'How visible the white outline around cells that can\'t split should be. 0 = not visible, 1 = fully ' +
  1520. 'visible.');
  1521. checkbox('jellySkinLag', 'Jelly physics cell size lag',
  1522. 'Jelly physics causes cells to grow and shrink slower than text and skins, making the game more ' +
  1523. 'satisfying. If you have a skin that looks weird only with jelly physics, try turning this off.');
  1524. checkbox('cellGlow', 'Cell glow', 'When enabled, makes cells have a slight glow. This could slightly ' +
  1525. 'affect performance.');
  1526. checkbox('pelletGlow', 'Pellet glow', 'When enabled, gives pellets a slight glow. This should not affect ' +
  1527. 'performance.');
  1528. checkbox('boldUi', 'Top UI uses bold text', 'When enabled, the top-left score and stats UI and the ' +
  1529. 'leaderboard will use the bold Ubuntu font.');
  1530. checkbox('showStats', 'Show server stats', 'When disabled, hides the top-left server stats including the ' +
  1531. 'player count and server uptime.');
  1532. checkbox('syncSkin', 'Show self skin on other tabs',
  1533. 'Whether your custom skin should be shown on your other tabs too.');
  1534.  
  1535. // #3 : create options for sigmod
  1536. let sigmodInjection;
  1537. sigmodInjection = setInterval(() => {
  1538. const nav = document.querySelector('.mod_menu_navbar');
  1539. const content = document.querySelector('.mod_menu_content');
  1540. if (!nav || !content) return;
  1541.  
  1542. clearInterval(sigmodInjection);
  1543.  
  1544. content.appendChild(sigmodContainer);
  1545.  
  1546. const navButton = fromHTML('<button class="mod_nav_btn">🔥 Sig Fixes</button>');
  1547. nav.appendChild(navButton);
  1548. navButton.addEventListener('click', () => {
  1549. // basically openModTab() from sigmod
  1550. (/** @type {NodeListOf<HTMLElement>} */ (document.querySelectorAll('.mod_tab'))).forEach(tab => {
  1551. tab.style.opacity = '0';
  1552. setTimeout(() => tab.style.display = 'none', 200);
  1553. });
  1554.  
  1555. (/** @type {NodeListOf<HTMLElement>} */ (document.querySelectorAll('.mod_nav_btn'))).forEach(tab => {
  1556. tab.classList.remove('mod_selected');
  1557. });
  1558.  
  1559. navButton.classList.add('mod_selected');
  1560. setTimeout(() => {
  1561. sigmodContainer.style.display = 'flex';
  1562. setTimeout(() => sigmodContainer.style.opacity = '1', 10);
  1563. }, 200);
  1564. });
  1565. }, 100);
  1566.  
  1567. return settings;
  1568. })();
  1569.  
  1570.  
  1571.  
  1572. ////////////////////////////////
  1573. // Setup Multi-tab World Sync //
  1574. ////////////////////////////////
  1575. /** @typedef {{
  1576. * self: string,
  1577. * camera: { tx: number, ty: number },
  1578. * owned: Map<number, { x: number, y: number, r: number, jr: number } | false>,
  1579. * skin: string,
  1580. * updated: { now: number, timeOrigin: number },
  1581. * }} TabData
  1582. */
  1583. const sync = (() => {
  1584. const sync = {};
  1585. /** @type {Map<string, number>} */
  1586. sync.lastPacket = new Map();
  1587. /** @type {{
  1588. * cells: Map<number, { merged: Cell | undefined, model: Cell | undefined, tabs: Map<string, Cell> }>,
  1589. * pellets: Map<number, { merged: Cell | undefined, model: Cell | undefined, tabs: Map<string, Cell> }>,
  1590. * } | undefined} */
  1591. sync.merge = undefined;
  1592. /** @type {Map<string, TabData>} */
  1593. sync.others = new Map();
  1594.  
  1595. const frame = new BroadcastChannel('sigfix-frame');
  1596. const tabsync = new BroadcastChannel('sigfix-tabsync');
  1597. const worldsync = new BroadcastChannel('sigfix-worldsync');
  1598. const zoom = new BroadcastChannel('sigfix-zoom');
  1599. const self = sync.self = Date.now() + '-' + Math.random();
  1600.  
  1601. /**
  1602. * @param {TabData} data
  1603. * @param {number} foreignNow
  1604. */
  1605. const localized = (data, foreignNow) => (data.updated.timeOrigin - performance.timeOrigin) + foreignNow;
  1606. // foreignNow + data.updated.timeOrigin - performance.timeOrigin; different order so maybe better precision?
  1607.  
  1608. sync.frame = () => {
  1609. frame.postMessage(undefined);
  1610. };
  1611. frame.addEventListener('message', () => {
  1612. // only update the world if we aren't rendering ourselves (example case: games open on two monitors)
  1613. if (document.visibilityState === 'hidden') {
  1614. world.moveCamera();
  1615. input.move();
  1616. }
  1617.  
  1618. // might be preferable over document.visibilityState
  1619. if (!document.hasFocus())
  1620. input.antiAfk();
  1621. });
  1622.  
  1623. /** @param {number} now */
  1624. sync.tabsync = now => {
  1625. /** @type {TabData['owned']} */
  1626. const owned = new Map();
  1627. for (const id of world.mine) {
  1628. const cell = world.cells.get(id);
  1629. if (!cell) continue;
  1630.  
  1631. owned.set(id, world.xyr(cell, undefined, now));
  1632. }
  1633. for (const id of world.mineDead)
  1634. owned.set(id, false);
  1635.  
  1636. /** @type {TabData} */
  1637. const syncData = {
  1638. self,
  1639. camera: world.camera,
  1640. owned,
  1641. skin: settings.selfSkin,
  1642. updated: { now, timeOrigin: performance.timeOrigin },
  1643. };
  1644. tabsync.postMessage(syncData);
  1645. };
  1646. tabsync.addEventListener('message', e => {
  1647. /** @type {TabData} */
  1648. const data = e.data;
  1649. sync.others.set(data.self, data);
  1650. });
  1651.  
  1652. sync.worldsync = () => {
  1653. worldsync.postMessage({ type: 'sync-response', cells: world.cells, pellets: world.pellets, self });
  1654. };
  1655. /** @param {DataView} dat */
  1656. sync.worldupdate = dat => {
  1657. worldsync.postMessage({ type: 'update', self, dat });
  1658. };
  1659. let lastSyncResponse = 0;
  1660. worldsync.addEventListener('message', e => {
  1661. switch (e.data.type) {
  1662. case 'update': {
  1663. /** @type {{ self: string, dat: DataView }} */
  1664. const data = e.data;
  1665. const now = performance.now();
  1666. if (now - (sync.lastPacket.get(data.self) ?? -Infinity) > 3000) {
  1667. // if we don't exactly know what data to build from, request it
  1668. // there's a chance other tabs might not know either
  1669. worldsync.postMessage({ type: 'sync-request', self: data.self });
  1670. return;
  1671. }
  1672.  
  1673. sync.lastPacket.set(data.self, now);
  1674. sync.readWorldUpdate(data.self, data.dat);
  1675. sync.tryMerge();
  1676. break;
  1677. }
  1678.  
  1679. case 'sync-request': {
  1680. if (self !== /** @type {string} */ (e.data.self)) return;
  1681. // do NOT tolerate spamming worldsyncRequests. let the other tabs suffer for a second, rather than
  1682. // resending like 50 times on a lag spike
  1683. const now = performance.now();
  1684. if (now - lastSyncResponse < 1000) return;
  1685. lastSyncResponse = now;
  1686.  
  1687. sync.tabsync(now);
  1688. sync.worldsync();
  1689. break;
  1690. }
  1691.  
  1692. case 'sync-response': {
  1693. /** @type {{ cells: Map<number, Cell>, pellets: Map<number, Cell>, self: string }} */
  1694. const data = e.data;
  1695. const tab = sync.others.get(data.self);
  1696. if (!tab || !sync.merge) return;
  1697.  
  1698. // first, clear all previously known cells
  1699. for (const key of /** @type {const} */ (['cells', 'pellets'])) {
  1700. for (const [id, collection] of sync.merge[key]) {
  1701. collection.tabs.delete(data.self);
  1702. if (collection.tabs.size === 0) sync.merge[key].delete(id);
  1703. }
  1704. }
  1705.  
  1706. // then add new ones
  1707. for (const key of /** @type {const} */ (['cells', 'pellets'])) {
  1708. for (const cell of data[key].values()) {
  1709. cell.born = localized(tab, cell.born);
  1710. cell.updated = localized(tab, cell.updated);
  1711. if (cell.deadAt !== undefined) cell.deadAt = localized(tab, cell.deadAt);
  1712.  
  1713. let collection = sync.merge[key].get(cell.id);
  1714. if (!collection) {
  1715. collection = { merged: undefined, model: undefined, tabs: new Map() };
  1716. sync.merge[key].set(cell.id, collection);
  1717. }
  1718. collection.tabs.set(data.self, cell);
  1719. }
  1720. }
  1721.  
  1722. sync.lastPacket.set(data.self, performance.now());
  1723. break;
  1724. }
  1725. }
  1726. });
  1727.  
  1728. sync.zoom = () => zoom.postMessage(input.zoom);
  1729. zoom.addEventListener('message', e => void (input.zoom = e.data));
  1730.  
  1731. /**
  1732. * @param {string} tab
  1733. * @param {DataView} dat
  1734. */
  1735. sync.readWorldUpdate = (tab, dat) => {
  1736. let off = 0;
  1737. const now = performance.now();
  1738. switch (dat.getUint8(off++)) {
  1739. case 0x10: // world updates
  1740. // #a : kills / consumes
  1741. const killCount = dat.getUint16(off, true);
  1742. off += 2;
  1743.  
  1744. for (let i = 0; i < killCount; ++i) {
  1745. const killerId = dat.getUint32(off, true);
  1746. const killedId = dat.getUint32(off + 4, true);
  1747. off += 8;
  1748.  
  1749. let killed;
  1750. if (tab === self) {
  1751. killed = world.pellets.get(killedId) ?? world.cells.get(killedId);
  1752. } else {
  1753. killed = sync.merge?.pellets.get(killedId)?.tabs.get(tab)
  1754. ?? sync.merge?.cells.get(killedId)?.tabs.get(tab);
  1755. }
  1756. if (killed) {
  1757. killed.deadTo = killerId;
  1758. killed.deadAt = killed.updated = now;
  1759.  
  1760. if (tab === self) {
  1761. world.clanmates.delete(killed);
  1762.  
  1763. if (killed.pellet && world.mine.includes(killerId))
  1764. ++world.stats.foodEaten;
  1765.  
  1766. const myIdx = world.mine.indexOf(killedId);
  1767. if (myIdx !== -1) {
  1768. world.mine.splice(myIdx, 1);
  1769. world.mineDead.add(killedId);
  1770. }
  1771. }
  1772. }
  1773. }
  1774.  
  1775. // #b : updates
  1776. while (true) {
  1777. const id = dat.getUint32(off, true);
  1778. off += 4;
  1779. if (id === 0) break;
  1780.  
  1781. const x = dat.getInt16(off, true);
  1782. const y = dat.getInt16(off + 2, true);
  1783. const r = dat.getUint16(off + 4, true);
  1784. const flags = dat.getUint8(off + 6);
  1785. // (void 1 byte, "isUpdate")
  1786. // (void 1 byte, "isPlayer")
  1787. const sub = !!dat.getUint8(off + 9);
  1788. off += 10;
  1789.  
  1790. let clan;
  1791. [clan, off] = aux.readZTString(dat, off);
  1792.  
  1793. /** @type {number | undefined} */
  1794. let Rgb, rGb, rgB;
  1795. if (flags & 0x02) {
  1796. // update color
  1797. Rgb = dat.getUint8(off) / 255;
  1798. rGb = dat.getUint8(off + 1) / 255;
  1799. rgB = dat.getUint8(off + 2) / 255;
  1800. off += 3;
  1801. }
  1802.  
  1803. let skin = '';
  1804. if (flags & 0x04) {
  1805. // update skin
  1806. [skin, off] = aux.readZTString(dat, off);
  1807. skin = aux.parseSkin(skin);
  1808. }
  1809.  
  1810. let name = '';
  1811. if (flags & 0x08) {
  1812. // update name
  1813. [name, off] = aux.readZTString(dat, off);
  1814. name = aux.parseName(name);
  1815. if (name) render.textFromCache(name, sub); // make sure the texture is ready on render
  1816. }
  1817.  
  1818. const jagged = !!(flags & 0x11);
  1819. const eject = !!(flags & 0x20);
  1820.  
  1821. /** @type {Cell | undefined} */
  1822. let cell;
  1823. if (tab === self) {
  1824. // prefer accessing the local map
  1825. cell = world.cells.get(id) ?? world.pellets.get(id);
  1826. } else {
  1827. cell = sync.merge?.cells.get(id)?.tabs.get(tab)
  1828. ?? sync.merge?.pellets.get(id)?.tabs.get(tab);
  1829. }
  1830. if (cell && cell.deadAt === undefined) {
  1831. const { x: ix, y: iy, r: ir, jr } = world.xyr(cell, undefined, now);
  1832. cell.ox = ix; cell.oy = iy; cell.or = ir;
  1833. cell.jr = jr;
  1834. cell.nx = x; cell.ny = y; cell.nr = r;
  1835. cell.jagged = jagged;
  1836. cell.updated = now;
  1837.  
  1838. if (Rgb !== undefined) {
  1839. cell.Rgb = Rgb;
  1840. cell.rGb = /** @type {number} */ (rGb);
  1841. cell.rgB = /** @type {number} */ (rgB);
  1842. }
  1843. if (skin) cell.skin = skin;
  1844. if (name) cell.name = name;
  1845. cell.sub = sub;
  1846.  
  1847. cell.clan = clan;
  1848. if (tab === self && clan && clan === aux.userData?.clan)
  1849. world.clanmates.add(cell);
  1850. } else {
  1851. if (cell?.deadAt !== undefined) {
  1852. // when respawning, OgarII does not send the description of cells if you spawn in the
  1853. // same area, despite those cells being deleted from your view area
  1854. if (Rgb === undefined)
  1855. ({ Rgb, rGb, rgB } = cell);
  1856. name ||= cell.name;
  1857. skin ||= cell.skin;
  1858. }
  1859.  
  1860. /** @type {Cell} */
  1861. const ncell = {
  1862. id,
  1863. ox: x, nx: x,
  1864. oy: y, ny: y,
  1865. or: r, nr: r, jr: r,
  1866. Rgb: Rgb ?? 1, rGb: rGb ?? 1, rgB: rgB ?? 1,
  1867. jagged, pellet: r < 75 && !eject, // tourney servers have bigger pellets
  1868. updated: now, born: now,
  1869. deadAt: undefined, deadTo: -1,
  1870. name, skin, sub, clan,
  1871. };
  1872.  
  1873. const key = ncell.pellet ? 'pellets' : 'cells';
  1874. if (tab === self) world[key].set(id, ncell);
  1875. if (sync.merge) {
  1876. let collection = sync.merge[key].get(id);
  1877. if (!collection) {
  1878. collection = { merged: undefined, model: undefined, tabs: new Map() };
  1879. sync.merge[key].set(id, collection);
  1880. }
  1881. collection.tabs.set(tab, ncell);
  1882. }
  1883.  
  1884. if (tab === self && clan === aux.userData?.clan)
  1885. world.clanmates.add(ncell);
  1886. }
  1887. }
  1888.  
  1889. // #c : deletes
  1890. const deleteCount = dat.getUint16(off, true);
  1891. off += 2;
  1892.  
  1893. for (let i = 0; i < deleteCount; ++i) {
  1894. const deletedId = dat.getUint32(off, true);
  1895. off += 4;
  1896.  
  1897. let deleted;
  1898. if (tab === self) {
  1899. deleted = world.pellets.get(deletedId) ?? world.cells.get(deletedId);
  1900. } else {
  1901. deleted = sync.merge?.pellets.get(deletedId)?.tabs.get(tab)
  1902. ?? sync.merge?.cells.get(deletedId)?.tabs.get(tab);
  1903. }
  1904. if (deleted) {
  1905. if (deleted.deadAt === undefined) {
  1906. deleted.deadAt = now;
  1907. deleted.deadTo = -1;
  1908. }
  1909.  
  1910. if (tab === self) {
  1911. world.clanmates.delete(deleted);
  1912.  
  1913. const myIdx = world.mine.indexOf(deletedId);
  1914. if (myIdx !== -1) {
  1915. world.mine.splice(myIdx, 1);
  1916. world.mineDead.add(myIdx);
  1917. }
  1918. }
  1919. }
  1920. }
  1921.  
  1922. // #4 : clean all cells
  1923. sync.clean();
  1924. if (tab === self) {
  1925. for (const [id, cell] of world.cells) {
  1926. if (cell.deadAt === undefined) continue;
  1927. if (now - cell.deadAt >= 200) {
  1928. world.cells.delete(id);
  1929. world.mineDead.delete(id);
  1930. }
  1931. }
  1932.  
  1933. for (const [id, cell] of world.pellets) {
  1934. if (cell.deadAt === undefined) continue;
  1935. if (now - cell.deadAt >= 200) {
  1936. world.pellets.delete(id);
  1937. }
  1938. }
  1939. }
  1940. break;
  1941.  
  1942. case 0x12: // delete all cells
  1943. // DO NOT just clear the maps! when respawning, OgarII will not resend cell data if we spawn nearby.
  1944. if (tab === self) {
  1945. // self cells are linked to those in sync.merge, this should be faster
  1946. for (const map of [world.cells, world.pellets]) {
  1947. for (const cell of map.values()) {
  1948. if (cell.deadAt === undefined) cell.deadAt = now;
  1949. }
  1950. }
  1951. } else if (sync.merge) {
  1952. for (const map of [sync.merge.cells, sync.merge.pellets]) {
  1953. for (const collection of map.values()) {
  1954. const cell = collection.tabs.get(tab);
  1955. if (cell && cell.deadAt === undefined) cell.deadAt = now;
  1956. }
  1957. }
  1958. }
  1959. break;
  1960. }
  1961. };
  1962.  
  1963. sync.tryMerge = () => {
  1964. // for camera merging to look extremely smooth, we need to merge packets and apply them *ONLY* when all
  1965. // tabs are synchronized.
  1966. // if you simply fall back to what the other tabs see, you will get lots of flickering and warping (what
  1967. // delta suffers from).
  1968. // threfore, we make sure that all tabs that share visible cells see them in the same spots, to make sure
  1969. // they are all on the same tick
  1970. // it's also not sufficient to simply count how many update (0x10) packets we get, as /leaveworld (part of
  1971. // respawn functionality) stops those packets from coming in
  1972. // if the view areas are disjoint, then there's nothing we can do but this should never happen when
  1973. // splitrunning
  1974.  
  1975. if (!settings.mergeViewArea || performance.now() - render.lastFrame > 45_000 || sync.others.size === 0) {
  1976. // very performance-intensive; don't update if not rendering
  1977. sync.merge = undefined;
  1978. render.upload('pellets');
  1979. return;
  1980. }
  1981.  
  1982. const now = performance.now();
  1983. let inheritBorn = false; // without, when turning on sync.merge, cells will appear to fade in again
  1984.  
  1985. if (!sync.merge) {
  1986. sync.merge = { cells: new Map(), pellets: new Map() };
  1987. inheritBorn = true;
  1988.  
  1989. // copy all local cells into here
  1990. for (const [map, to]
  1991. of /** @type {const} */ ([[world.cells, sync.merge.cells], [world.pellets, sync.merge.pellets]])) {
  1992. for (const cell of map.values()) {
  1993. /** @type {Map<string, Cell>} */
  1994. const tabs = new Map();
  1995. tabs.set(self, cell);
  1996. to.set(cell.id, { merged: undefined, model: undefined, tabs });
  1997. }
  1998. }
  1999. }
  2000.  
  2001. // #2 : ensure all important cells are synced
  2002. for (const map of [sync.merge.cells, sync.merge.pellets]) {
  2003. for (const collection of map.values()) {
  2004. /** @type {Cell | undefined} */
  2005. let model;
  2006. for (const cell of collection.tabs.values()) {
  2007. if (!model) {
  2008. model = cell;
  2009. continue;
  2010. }
  2011.  
  2012. const modelDisappeared = model.deadAt !== undefined && model.deadTo === -1;
  2013. const cellDisappeared = cell.deadAt !== undefined && cell.deadTo === -1;
  2014.  
  2015. if (!modelDisappeared && !cellDisappeared) {
  2016. // both cells are visible; are they going to the same place?
  2017. if (model.nx !== cell.nx || model.ny !== cell.ny || model.nr !== cell.nr) {
  2018. return; // outta here
  2019. }
  2020. } else if (modelDisappeared && !cellDisappeared) {
  2021. // model went out of view; prefer the visible cell
  2022. model = cell;
  2023. } else if (!modelDisappeared && cellDisappeared) {
  2024. // cell went out of view; prefer model
  2025. } else {
  2026. // both cells went out of view; prefer the one that disappeared latest
  2027. if (/** @type {number} */ (cell.deadAt) > /** @type {number} */ (model.deadAt)) {
  2028. model = cell;
  2029. }
  2030. }
  2031. }
  2032.  
  2033. collection.model = model;
  2034. }
  2035. }
  2036.  
  2037. // #3 : tabs are all synced; merge changes
  2038. for (const map of [sync.merge.cells, sync.merge.pellets]) {
  2039. for (const collection of map.values()) {
  2040. const merged = collection.merged;
  2041. const model = collection.model;
  2042. if (!model) {
  2043. collection.merged = undefined;
  2044. continue;
  2045. }
  2046.  
  2047. if (!merged) {
  2048. if (model.deadAt === undefined) {
  2049. collection.merged = {
  2050. id: model.id,
  2051. ox: model.nx, nx: model.nx,
  2052. oy: model.ny, ny: model.ny,
  2053. or: model.nr, nr: model.nr, jr: model.nr,
  2054. Rgb: model.Rgb, rGb: model.rGb, rgB: model.rgB,
  2055. jagged: model.jagged, pellet: model.pellet,
  2056. name: model.name, skin: model.skin, sub: model.sub, clan: model.clan,
  2057. born: inheritBorn ? model.born : now, updated: now,
  2058. deadTo: -1,
  2059. deadAt: undefined,
  2060. };
  2061. }
  2062. } else {
  2063. if (merged.deadAt === undefined) {
  2064. const { x, y, r, jr } = world.xyr(merged, undefined, now);
  2065. merged.ox = x;
  2066. merged.oy = y;
  2067. merged.or = r;
  2068. merged.jr = jr;
  2069. merged.nx = model.nx;
  2070. merged.ny = model.ny;
  2071. merged.nr = model.nr;
  2072. merged.updated = now;
  2073. }
  2074.  
  2075. if (model.deadAt !== undefined) {
  2076. if (merged.deadAt === undefined) {
  2077. // merged is finally dying
  2078. merged.deadAt = now;
  2079. merged.deadTo = model.deadTo;
  2080. }
  2081. } else if (merged.deadAt !== undefined) {
  2082. // cell is no longer dead (probably came back into view)
  2083. merged.ox = model.nx;
  2084. merged.oy = model.ny;
  2085. merged.or = model.nr;
  2086. merged.deadAt = undefined;
  2087. merged.deadTo = -1;
  2088. merged.born = merged.updated = now;
  2089. }
  2090. }
  2091. }
  2092. }
  2093.  
  2094. sync.clean();
  2095. render.upload('pellets');
  2096. };
  2097.  
  2098. let lastClean = 0;
  2099. sync.clean = () => {
  2100. const now = performance.now();
  2101. if (now - lastClean < 500) return; // sync.clean is a huge bottleneck
  2102. lastClean = now;
  2103.  
  2104. if (sync.merge) {
  2105. // don't do array unpacking if not necessary
  2106. let idIterator = sync.merge.cells.keys();
  2107. for (const collection of sync.merge.cells.values()) {
  2108. const id = idIterator.next().value;
  2109. const cellIterator = collection.tabs.values();
  2110.  
  2111. for (const key of collection.tabs.keys()) {
  2112. if (key === self || sync.others.has(key)) {
  2113. const cell = /** @type {Cell} */ (cellIterator.next().value);
  2114. if (cell.deadAt === undefined) continue;
  2115. if (now - cell.deadAt < 500) continue;
  2116. }
  2117.  
  2118. collection.tabs.delete(key);
  2119. if (key === self) world.mineDead.delete(id);
  2120. }
  2121.  
  2122. if (collection.tabs.size === 0) sync.merge.cells.delete(id);
  2123. }
  2124.  
  2125. idIterator = sync.merge.pellets.keys();
  2126. for (const collection of sync.merge.pellets.values()) {
  2127. const id = idIterator.next().value;
  2128. const cellIterator = collection.tabs.values();
  2129.  
  2130. for (const key of collection.tabs.keys()) {
  2131. if (key === self || sync.others.has(key)) {
  2132. const cell = cellIterator.next().value;
  2133. if (cell.deadAt === undefined) continue;
  2134. if (now - cell.deadAt < 500) continue;
  2135. }
  2136.  
  2137. collection.tabs.delete(key);
  2138. }
  2139.  
  2140. if (collection.tabs.size === 0) sync.merge.pellets.delete(id);
  2141. }
  2142. }
  2143.  
  2144. sync.others.forEach((data, key) => {
  2145. // only get rid of a tab if it lags out alone
  2146. if (net.lastUpdate - localized(data, data.updated.now) > 500) {
  2147. sync.others.delete(key);
  2148. sync.lastPacket.delete(key);
  2149. }
  2150. });
  2151. };
  2152. setInterval(() => sync.clean(), 500);
  2153.  
  2154. return sync;
  2155. })();
  2156.  
  2157.  
  2158.  
  2159. ///////////////////////////
  2160. // Setup World Variables //
  2161. ///////////////////////////
  2162. /** @typedef {{
  2163. * id: number,
  2164. * ox: number, nx: number,
  2165. * oy: number, ny: number,
  2166. * or: number, nr: number, jr: number,
  2167. * Rgb: number, rGb: number, rgB: number,
  2168. * updated: number, born: number, deadTo: number, deadAt: number | undefined,
  2169. * jagged: boolean, pellet: boolean,
  2170. * name: string, skin: string, sub: boolean, clan: string,
  2171. * }} Cell */
  2172. const world = (() => {
  2173. const world = {};
  2174.  
  2175. // #1 : define cell variables and functions
  2176. /** @type {Map<number, Cell>} */
  2177. world.cells = new Map();
  2178. /** @type {Set<Cell>} */
  2179. world.clanmates = new Set();
  2180. /** @type {number[]} */
  2181. world.mine = []; // order matters, as the oldest cells split first
  2182. /** @type {Set<number>} */
  2183. world.mineDead = new Set();
  2184. /** @type {Map<number, Cell>} */
  2185. world.pellets = new Map();
  2186.  
  2187. /**
  2188. * @param {Cell} cell
  2189. * @param {Cell | undefined} killer
  2190. * @param {number} now
  2191. * @returns {{ x: number, y: number, r: number, jr: number }}
  2192. */
  2193. world.xyr = (cell, killer, now) => {
  2194. let a = (now - cell.updated) / settings.drawDelay;
  2195. a = a < 0 ? 0 : a > 1 ? 1 : a;
  2196. let nx = cell.nx;
  2197. let ny = cell.ny;
  2198. if (killer && cell.deadAt !== undefined && (killer.deadAt === undefined || cell.deadAt <= killer.deadAt)) {
  2199. // do not animate death towards a cell that died already (went offscreen)
  2200. nx = killer.nx;
  2201. ny = killer.ny;
  2202. }
  2203.  
  2204. const x = cell.ox + (nx - cell.ox) * a;
  2205. const y = cell.oy + (ny - cell.oy) * a;
  2206. const r = cell.or + (cell.nr - cell.or) * a;
  2207.  
  2208. const dt = (now - cell.updated) / 1000;
  2209. return {
  2210. x, y, r,
  2211. jr: aux.exponentialEase(cell.jr, r, 5, dt), // vanilla uses a factor of 10, but it's basically unusable
  2212. };
  2213. };
  2214.  
  2215. let last = performance.now();
  2216. world.moveCamera = () => {
  2217. const now = performance.now();
  2218. const dt = (now - last) / 1000;
  2219. last = now;
  2220.  
  2221. const weight = settings.mergeCamera ? 2 : 0;
  2222.  
  2223. /**
  2224. * @param {Iterable<number>} owned
  2225. * @param {Map<number, { x: number, y: number, r: number, jr: number } | false> | undefined} fallback
  2226. * @returns {{
  2227. * weightedX: number, weightedY: number, totalWeight: number,
  2228. * scale: number, width: number, height: number
  2229. * }}
  2230. */
  2231. const cameraDesc = (owned, fallback) => {
  2232. let weightedX = 0;
  2233. let weightedY = 0;
  2234. let totalWeight = 0;
  2235. let totalR = 0;
  2236.  
  2237. for (const id of owned) {
  2238. /** @type {{ x: number, y: number, r: number, jr: number }} */
  2239. let xyr;
  2240.  
  2241. let cell;
  2242. if (settings.mergeViewArea && sync.merge) {
  2243. cell = sync.merge.cells.get(id)?.merged;
  2244. } else {
  2245. cell = world.cells.get(id);
  2246. }
  2247. if (!cell) {
  2248. const partial = fallback?.get(id);
  2249. if (!partial) continue;
  2250. xyr = partial;
  2251. } else if (cell.deadAt !== undefined) continue;
  2252. else xyr = world.xyr(cell, undefined, now);
  2253.  
  2254. const weighted = xyr.r ** weight;
  2255. weightedX += xyr.x * weighted;
  2256. weightedY += xyr.y * weighted;
  2257. totalWeight += weighted;
  2258. totalR += xyr.r;
  2259. }
  2260.  
  2261. const scale = Math.min(64 / totalR, 1) ** 0.4;
  2262. const width = 1920 / 2 / scale;
  2263. const height = 1080 / 2 / scale;
  2264.  
  2265. return { weightedX, weightedY, totalWeight, scale, width, height };
  2266. };
  2267.  
  2268. const localDesc = cameraDesc(world.mine, undefined);
  2269. let { weightedX, weightedY, totalWeight } = localDesc;
  2270. const localX = weightedX / totalWeight;
  2271. const localY = weightedY / totalWeight;
  2272.  
  2273. world.camera.merged = false;
  2274. if (settings.mergeCamera && localDesc.totalWeight > 0) {
  2275. for (const data of sync.others.values()) {
  2276. const thisDesc = cameraDesc(data.owned.keys(), data.owned);
  2277. if (thisDesc.totalWeight <= 0) continue;
  2278. const thisX = thisDesc.weightedX / thisDesc.totalWeight;
  2279. const thisY = thisDesc.weightedY / thisDesc.totalWeight;
  2280.  
  2281. const threshold = 1000
  2282. + Math.min(localDesc.totalWeight / 100 / 100, thisDesc.totalWeight / 100 / 100);
  2283. if (Math.abs(thisX - localX) < localDesc.width + thisDesc.width + threshold
  2284. && Math.abs(thisY - localY) < localDesc.height + thisDesc.height + threshold) {
  2285. weightedX += thisDesc.weightedX;
  2286. weightedY += thisDesc.weightedY;
  2287. totalWeight += thisDesc.totalWeight;
  2288. world.camera.merged = true;
  2289. }
  2290. }
  2291. }
  2292.  
  2293. // auto + merge => 0.25
  2294. // auto + -merge => localDesc.scale
  2295. // never + merge => 0.25
  2296. // never + -merge => 0.25
  2297. /** @type {number} */
  2298. let zoomout;
  2299. if (settings.autoZoom === 'never' || (settings.autoZoom === 'auto' && settings.mergeCamera)) {
  2300. zoomout = 0.25;
  2301. } else {
  2302. zoomout = localDesc.scale;
  2303. }
  2304.  
  2305. let xyEaseFactor;
  2306. if (totalWeight > 0) {
  2307. world.camera.tx = weightedX / totalWeight;
  2308. world.camera.ty = weightedY / totalWeight;
  2309. world.camera.tscale = zoomout * input.zoom;
  2310.  
  2311. xyEaseFactor = 2;
  2312. } else {
  2313. xyEaseFactor = 20;
  2314. }
  2315.  
  2316. world.camera.x = aux.exponentialEase(world.camera.x, world.camera.tx, xyEaseFactor, dt);
  2317. world.camera.y = aux.exponentialEase(world.camera.y, world.camera.ty, xyEaseFactor, dt);
  2318. world.camera.scale = aux.exponentialEase(world.camera.scale, world.camera.tscale, 9, dt);
  2319. };
  2320.  
  2321.  
  2322.  
  2323. // #2 : define others, like camera and borders
  2324. world.camera = {
  2325. x: 0, y: 0, scale: 1,
  2326. tx: 0, ty: 0, tscale: 1,
  2327. merged: false,
  2328. };
  2329.  
  2330. /** @type {{ l: number, r: number, t: number, b: number } | undefined} */
  2331. world.border = undefined;
  2332.  
  2333. /** @type {{ name: string, me: boolean, sub: boolean, place: number | undefined }[]} */
  2334. world.leaderboard = [];
  2335.  
  2336.  
  2337.  
  2338. // #3 : define stats
  2339. world.stats = {
  2340. foodEaten: 0,
  2341. highestPosition: 200,
  2342. highestScore: 0,
  2343. /** @type {number | undefined} */
  2344. spawnedAt: undefined,
  2345. };
  2346.  
  2347.  
  2348.  
  2349. return world;
  2350. })();
  2351.  
  2352.  
  2353.  
  2354. //////////////////////////
  2355. // Setup All Networking //
  2356. //////////////////////////
  2357. const net = (() => {
  2358. const net = {};
  2359.  
  2360. // #1 : define state
  2361. /** @type {{ shuffle: Map<number, number>, unshuffle: Map<number, number> } | undefined} */
  2362. let handshake;
  2363. /** @type {number | undefined} */
  2364. let pendingPingFrom;
  2365. let pingInterval;
  2366. let wasOpen = false;
  2367. /** @type {WebSocket} */
  2368. let ws;
  2369.  
  2370. /** -1 if ping reply took too long @type {number | undefined} */
  2371. net.latency = undefined;
  2372. net.ready = false;
  2373. net.lastUpdate = -Infinity;
  2374. net.rejected = false;
  2375.  
  2376. // #2 : connecting/reconnecting the websocket
  2377. /** @type {HTMLSelectElement | null} */
  2378. const gamemode = document.querySelector('#gamemode');
  2379. /** @type {HTMLOptionElement | null} */
  2380. const firstGamemode = document.querySelector('#gamemode option');
  2381.  
  2382. net.url = () => {
  2383. let server = 'wss://' + (gamemode?.value || firstGamemode?.value || 'ca0.sigmally.com/ws/');
  2384. if (location.search.startsWith('?ip='))
  2385. server = location.search.slice('?ip='.length);
  2386.  
  2387. return server;
  2388. };
  2389.  
  2390. function connect() {
  2391. // you can connect to multiple servers easily while being ratelimited
  2392. if (ws?.readyState !== WebSocket.CLOSED && ws?.readyState !== WebSocket.CLOSING) ws?.close?.();
  2393. try {
  2394. ws = new destructor.realWebSocket(net.url());
  2395. } catch (err) {
  2396. console.error('can\'t make WebSocket:', err);
  2397. aux.require(null, 'The server is invalid. Try changing the server, reloading the page, or clearing ' +
  2398. 'your browser cache and cookies.');
  2399. }
  2400.  
  2401. destructor.safeWebSockets.add(ws);
  2402. ws.binaryType = 'arraybuffer';
  2403. ws.addEventListener('close', wsClose);
  2404. ws.addEventListener('error', wsError);
  2405. ws.addEventListener('message', wsMessage);
  2406. ws.addEventListener('open', wsOpen);
  2407. }
  2408.  
  2409. function wsClose() {
  2410. handshake = undefined;
  2411. pendingPingFrom = undefined;
  2412. if (pingInterval)
  2413. clearInterval(pingInterval);
  2414.  
  2415. net.latency = undefined;
  2416. net.lastUpdate = performance.now();
  2417. net.ready = false;
  2418. if (!wasOpen) net.rejected = true;
  2419. wasOpen = false;
  2420.  
  2421. // hide/clear UI and show death screen if necessary
  2422. ui.stats.misc.textContent = '';
  2423. world.leaderboard = [];
  2424. ui.leaderboard.update();
  2425. if (world.stats.spawnedAt !== undefined) {
  2426. ui.deathScreen.show(world.stats);
  2427. ui.stats.update();
  2428. } else {
  2429. ui.toggleEscOverlay(true);
  2430. }
  2431.  
  2432. // clear world
  2433. world.border = undefined;
  2434. world.cells.clear(); // make sure we won't see overlapping IDs from new cells from the new connection
  2435. world.pellets.clear();
  2436. world.clanmates.clear();
  2437. while (world.mine.length) world.mine.pop();
  2438. world.mineDead.clear();
  2439. sync.tabsync(performance.now());
  2440. sync.worldupdate(new DataView(new Uint8Array([ 0x12 ]).buffer)); // broadcast a "delete all cells" packet
  2441. sync.tryMerge();
  2442. }
  2443.  
  2444. let reconnectAttempts = 0;
  2445. /** @type {number | undefined} */
  2446. let willReconnectAt;
  2447. setInterval(() => {
  2448. // retry after 500, 1000, 1500, 3000ms of closing
  2449. // OR if a captcha was (very recently) accepted
  2450. if (ws.readyState !== WebSocket.CLOSING && ws.readyState !== WebSocket.CLOSED)
  2451. return;
  2452.  
  2453. const now = performance.now();
  2454. if (input.captchaAcceptedAt && now - input.captchaAcceptedAt <= 3000) {
  2455. willReconnectAt = undefined;
  2456. connect();
  2457. return;
  2458. }
  2459.  
  2460. if (willReconnectAt === undefined) {
  2461. willReconnectAt = now + Math.min(500 * ++reconnectAttempts, 3000);
  2462. } else if (now >= willReconnectAt) {
  2463. willReconnectAt = undefined;
  2464. connect();
  2465. }
  2466. }, 50);
  2467.  
  2468. /** @param {Event} err */
  2469. function wsError(err) {
  2470. console.error('WebSocket error:', err);
  2471. }
  2472.  
  2473. function wsOpen() {
  2474. net.rejected = false;
  2475. wasOpen = true;
  2476. reconnectAttempts = 0;
  2477.  
  2478. ui.chat.barrier();
  2479.  
  2480. // reset camera location to the middle; this is implied but never sent by the server
  2481. world.camera.x = world.camera.tx = 0;
  2482. world.camera.y = world.camera.ty = 0;
  2483. world.camera.scale = world.camera.tscale = 1;
  2484.  
  2485. ws.send(aux.textEncoder.encode('SIG 0.0.1\x00'));
  2486. }
  2487.  
  2488. // listen for when the gamemode changes
  2489. gamemode?.addEventListener('change', () => {
  2490. ws.close();
  2491. });
  2492.  
  2493.  
  2494.  
  2495. // #3 : set up auxiliary functions
  2496. /**
  2497. * @param {number} opcode
  2498. * @param {object} data
  2499. */
  2500. function sendJson(opcode, data) {
  2501. // must check readyState as a weboscket might be in the 'CLOSING' state (so annoying!)
  2502. if (!handshake || ws.readyState !== WebSocket.OPEN) return;
  2503. const dataBuf = aux.textEncoder.encode(JSON.stringify(data));
  2504. const dat = new DataView(new ArrayBuffer(dataBuf.byteLength + 2));
  2505.  
  2506. dat.setUint8(0, Number(handshake.shuffle.get(opcode)));
  2507. for (let i = 0; i < dataBuf.byteLength; ++i) {
  2508. dat.setUint8(1 + i, dataBuf[i]);
  2509. }
  2510.  
  2511. ws.send(dat);
  2512. }
  2513.  
  2514. function createPingLoop() {
  2515. function ping() {
  2516. if (!handshake || ws.readyState !== WebSocket.OPEN) return; // shouldn't ever happen
  2517.  
  2518. if (pendingPingFrom !== undefined) {
  2519. // ping was not replied to, tell the player the ping text might be wonky for a bit
  2520. net.latency = -1;
  2521. }
  2522.  
  2523. ws.send(new Uint8Array([Number(handshake.shuffle.get(0xfe))]));
  2524. pendingPingFrom = performance.now();
  2525. }
  2526.  
  2527. pingInterval = setInterval(ping, 2_000);
  2528. }
  2529.  
  2530.  
  2531.  
  2532. // #4 : set up message handler
  2533. /** @param {MessageEvent} msg */
  2534. function wsMessage(msg) {
  2535. const dat = new DataView(msg.data);
  2536. if (!handshake) {
  2537. // unlikely to change as we're still on v0.0.1 but i'll check it anyway
  2538. let [version, off] = aux.readZTString(dat, 0);
  2539. if (version !== 'SIG 0.0.1') {
  2540. alert(`got unsupported version "${version}", expected "SIG 0.0.1"`);
  2541. return ws.close();
  2542. }
  2543.  
  2544. handshake = { shuffle: new Map(), unshuffle: new Map() };
  2545. for (let i = 0; i < 256; ++i) {
  2546. const shuffled = dat.getUint8(off + i);
  2547. handshake.shuffle.set(i, shuffled);
  2548. handshake.unshuffle.set(shuffled, i);
  2549. }
  2550.  
  2551. createPingLoop();
  2552.  
  2553. return;
  2554. }
  2555.  
  2556. const now = performance.now();
  2557. const opcode = Number(handshake.unshuffle.get(dat.getUint8(0)));
  2558. dat.setUint8(0, opcode);
  2559. let off = 1;
  2560. switch (opcode) {
  2561. case 0x10: { // world update
  2562. net.lastUpdate = now;
  2563. if (destructor.respawnBlock?.status === 'left') {
  2564. destructor.respawnBlock = undefined;
  2565. }
  2566.  
  2567. // start ASAP!
  2568. sync.tabsync(now);
  2569. if (settings.mergeViewArea) sync.worldupdate(dat);
  2570.  
  2571. sync.readWorldUpdate(sync.self, dat);
  2572. sync.tryMerge();
  2573.  
  2574. if (world.mine.length === 0 && world.stats.spawnedAt !== undefined) {
  2575. ui.deathScreen.show(world.stats);
  2576. }
  2577.  
  2578. ui.stats.update();
  2579. break;
  2580. }
  2581.  
  2582. case 0x11: { // update camera pos
  2583. world.camera.tx = dat.getFloat32(off, true);
  2584. world.camera.ty = dat.getFloat32(off + 4, true);
  2585. world.camera.tscale = dat.getFloat32(off + 8, true) * input.zoom;
  2586. break;
  2587. }
  2588.  
  2589. case 0x12: // delete all cells
  2590. net.lastUpdate = now;
  2591. // happens every time you respawn
  2592. if (destructor.respawnBlock?.status === 'pending') {
  2593. destructor.respawnBlock.status = 'left';
  2594. }
  2595. sync.readWorldUpdate(sync.self, dat);
  2596. sync.tryMerge();
  2597. world.clanmates.clear();
  2598. if (settings.mergeViewArea) sync.worldupdate(dat);
  2599. // passthrough
  2600. case 0x14: // delete my cells
  2601. while (world.mine.length) world.mine.pop();
  2602. break;
  2603.  
  2604. case 0x20: { // new owned cell
  2605. world.mine.push(dat.getUint32(off, true));
  2606. if (world.mine.length === 1)
  2607. world.stats.spawnedAt = now;
  2608. break;
  2609. }
  2610.  
  2611. // case 0x30 is a text list (not a numbered list), leave unsupported
  2612. case 0x31: { // ffa leaderboard list
  2613. const lb = [];
  2614. const count = dat.getUint32(off, true);
  2615. off += 4;
  2616.  
  2617. let myPosition;
  2618. for (let i = 0; i < count; ++i) {
  2619. const me = !!dat.getUint32(off, true);
  2620. off += 4;
  2621.  
  2622. let name;
  2623. [name, off] = aux.readZTString(dat, off);
  2624. name = aux.parseName(name);
  2625.  
  2626. // why this is copied into every leaderboard entry is beyond my understanding
  2627. myPosition = dat.getUint32(off, true);
  2628. const sub = !!dat.getUint32(off + 4, true);
  2629. off += 8;
  2630.  
  2631. lb.push({ name, sub, me, place: undefined });
  2632. }
  2633.  
  2634. if (myPosition) {
  2635. if (myPosition - 1 >= lb.length) {
  2636. /** @type {HTMLInputElement | null} */
  2637. const inputName = document.querySelector('input#nick');
  2638. lb.push({
  2639. me: true,
  2640. name: aux.parseName(inputName?.value ?? ''),
  2641. place: myPosition,
  2642. sub: false,
  2643. });
  2644. }
  2645.  
  2646. if (myPosition < world.stats.highestPosition)
  2647. world.stats.highestPosition = myPosition;
  2648. }
  2649.  
  2650. world.leaderboard = lb;
  2651. ui.leaderboard.update();
  2652. break;
  2653. }
  2654.  
  2655. case 0x40: { // border update
  2656. world.border = {
  2657. l: dat.getFloat64(off, true),
  2658. t: dat.getFloat64(off + 8, true),
  2659. r: dat.getFloat64(off + 16, true),
  2660. b: dat.getFloat64(off + 24, true),
  2661. };
  2662. break;
  2663. }
  2664.  
  2665. case 0x63: { // chat message
  2666. const flags = dat.getUint8(off);
  2667. const rgb = /** @type {[number, number, number, number]} */
  2668. ([dat.getUint8(off + 1) / 255, dat.getUint8(off + 2) / 255, dat.getUint8(off + 3) / 255, 1]);
  2669. off += 4;
  2670.  
  2671. let name;
  2672. [name, off] = aux.readZTString(dat, off);
  2673. let msg;
  2674. [msg, off] = aux.readZTString(dat, off);
  2675.  
  2676. ui.chat.add(name, rgb, msg, !!(flags & 0x80));
  2677. break;
  2678. }
  2679.  
  2680. case 0xb4: { // incorrect password alert
  2681. ui.error('Password is incorrect');
  2682. break;
  2683. }
  2684.  
  2685. case 0xdd: {
  2686. net.howarewelosingmoney();
  2687. net.ready = true;
  2688. break;
  2689. }
  2690.  
  2691. case 0xfe: { // server stats, response to a ping
  2692. let statString;
  2693. [statString, off] = aux.readZTString(dat, off);
  2694.  
  2695. const statData = JSON.parse(statString);
  2696. ui.stats.updateMisc(statData);
  2697.  
  2698. if (pendingPingFrom) {
  2699. net.latency = now - pendingPingFrom;
  2700. pendingPingFrom = undefined;
  2701. }
  2702. break;
  2703. }
  2704. }
  2705. }
  2706.  
  2707.  
  2708.  
  2709. // #5 : export input functions
  2710. /**
  2711. * @param {number} x
  2712. * @param {number} y
  2713. */
  2714. net.move = function (x, y) {
  2715. if (!handshake || ws.readyState !== WebSocket.OPEN) return;
  2716. const dat = new DataView(new ArrayBuffer(13));
  2717.  
  2718. dat.setUint8(0, Number(handshake.shuffle.get(0x10)));
  2719. dat.setInt32(1, x, true);
  2720. dat.setInt32(5, y, true);
  2721.  
  2722. ws.send(dat);
  2723. };
  2724.  
  2725. net.w = function () {
  2726. if (!handshake || ws.readyState !== WebSocket.OPEN) return;
  2727. ws.send(new Uint8Array([Number(handshake.shuffle.get(21))]));
  2728. };
  2729.  
  2730. net.qdown = function () {
  2731. if (!handshake || ws.readyState !== WebSocket.OPEN) return;
  2732. ws.send(new Uint8Array([Number(handshake.shuffle.get(18))]));
  2733. };
  2734.  
  2735. net.qup = function () {
  2736. if (!handshake || ws.readyState !== WebSocket.OPEN) return;
  2737. ws.send(new Uint8Array([Number(handshake.shuffle.get(19))]));
  2738. };
  2739.  
  2740. net.split = function () {
  2741. if (!handshake || ws.readyState !== WebSocket.OPEN) return;
  2742. ws.send(new Uint8Array([Number(handshake.shuffle.get(17))]));
  2743. };
  2744.  
  2745. /**
  2746. * @param {string} msg
  2747. */
  2748. net.chat = function (msg) {
  2749. if (!handshake || ws.readyState !== WebSocket.OPEN) return;
  2750. const msgBuf = aux.textEncoder.encode(msg);
  2751. const dat = new DataView(new ArrayBuffer(msgBuf.byteLength + 3));
  2752.  
  2753. dat.setUint8(0, Number(handshake.shuffle.get(0x63)));
  2754. // skip flags, not implemented anyway
  2755. for (let i = 0; i < msgBuf.byteLength; ++i)
  2756. dat.setUint8(2 + i, msgBuf[i]);
  2757.  
  2758. ws.send(dat);
  2759. };
  2760.  
  2761. /**
  2762. * @param {{ name: string, skin: string, [x: string]: any }} data
  2763. */
  2764. net.play = function (data) {
  2765. sendJson(0x00, data);
  2766. };
  2767.  
  2768. net.howarewelosingmoney = function () {
  2769. if (!handshake || ws.readyState !== WebSocket.OPEN) return;
  2770. // this is a new thing added with the rest of the recent source code obfuscation (2024/02/18)
  2771. // which collects and links to your sigmally account, seemingly just for light data analysis but probably
  2772. // just for the fun of it:
  2773. // - your IP and country
  2774. // - whether you are under a proxy
  2775. // - whether you are using sigmod (because it also blocks ads)
  2776. // - whether you are using a traditional adblocker
  2777. //
  2778. // so, no thank you
  2779. sendJson(0xd0, { ip: '', country: '', proxy: false, user: null, blocker: 'sigmally fixes @8y8x' });
  2780. };
  2781.  
  2782. net.connection = function () {
  2783. if (!ws) return undefined;
  2784. if (!handshake || ws.readyState !== WebSocket.OPEN) return undefined;
  2785. return ws;
  2786. };
  2787.  
  2788.  
  2789.  
  2790. connect();
  2791. return net;
  2792. })();
  2793.  
  2794.  
  2795.  
  2796. //////////////////////////
  2797. // Setup Input Handlers //
  2798. //////////////////////////
  2799. const input = (() => {
  2800. const input = {};
  2801.  
  2802. // #1 : general inputs
  2803. /** @type {number | undefined} */
  2804. let lastMouseX = undefined;
  2805. /** @type {number | undefined} */
  2806. let lastMouseY = undefined;
  2807. let mouseX = 0; // -1 <= mouseX <= 1
  2808. let mouseY = 0; // -1 <= mouseY <= 1
  2809. let forceW = false;
  2810. let w = false;
  2811.  
  2812. input.zoom = 1;
  2813.  
  2814. /** @returns [number, number] */
  2815. input.mouse = () => {
  2816. return [
  2817. world.camera.x + mouseX * (innerWidth / innerHeight) * 540 / world.camera.scale,
  2818. world.camera.y + mouseY * 540 / world.camera.scale,
  2819. ];
  2820. };
  2821.  
  2822. function mouse() {
  2823. const [x, y] = input.mouse();
  2824. net.move(x, y);
  2825. lastMouseX = mouseX;
  2826. lastMouseY = mouseY;
  2827. }
  2828.  
  2829. function unfocused() {
  2830. return ui.escOverlayVisible() || document.activeElement?.tagName === 'INPUT';
  2831. }
  2832.  
  2833. let lastMovement = performance.now();
  2834. input.move = () => {
  2835. // called every frame because tabbing out reduces setInterval frequency, which messes up mouse flick fixes
  2836. const now = performance.now();
  2837. if (now - lastMovement < 40) return;
  2838. lastMovement = now;
  2839.  
  2840. // if holding w with sigmod, tabbing out, then tabbing in, avoid spitting out only one W
  2841. const consumedForceW = forceW;
  2842. forceW = false;
  2843.  
  2844. // allow flicking mouse then immediately switching tabs in the same tick
  2845. if (document.visibilityState === 'hidden' && lastMouseX === mouseX && lastMouseY === mouseY) return;
  2846. mouse();
  2847.  
  2848. if (consumedForceW || w) net.w();
  2849. };
  2850.  
  2851. // anti-afk when another tab is playing
  2852. let lastCheck = performance.now();
  2853. input.antiAfk = () => {
  2854. const now = performance.now();
  2855. // only check every 10s, don't want to spam packets but don't want to miss resetting the afk timer
  2856. if (now - lastCheck < 10_000) return;
  2857. lastCheck = now;
  2858.  
  2859. // check if any other tabs are *alive*
  2860. for (const tab of sync.others.values()) {
  2861. if (tab.owned.size > 0) {
  2862. net.qup(); // send literally any packet at all
  2863. break;
  2864. }
  2865. }
  2866. };
  2867.  
  2868. // sigmod freezes the player by overlaying an invisible div, so we just listen for canvas movements instead
  2869. addEventListener('mousemove', e => {
  2870. if (ui.escOverlayVisible()) return;
  2871. // sigmod freezes the player by overlaying an invisible div, so we respect it
  2872. if (e.target instanceof HTMLDivElement
  2873. && /** @type {CSSUnitValue | undefined} */ (e.target.attributeStyleMap.get('z-index'))?.value === 99)
  2874. return;
  2875. mouseX = (e.clientX / innerWidth * 2) - 1;
  2876. mouseY = (e.clientY / innerHeight * 2) - 1;
  2877. });
  2878.  
  2879. addEventListener('wheel', e => {
  2880. if (unfocused()) return;
  2881. let deltaY;
  2882. if (e.deltaMode === e.DOM_DELTA_PAGE) {
  2883. // support for the very obscure "scroll by page" setting in windows
  2884. deltaY = e.deltaY;
  2885. } else { // i don't think browsers support DOM_DELTA_LINE, so assume DOM_DELTA_PIXEL
  2886. deltaY = e.deltaY / 100;
  2887. }
  2888. input.zoom *= 0.8 ** (deltaY * settings.scrollFactor);
  2889. const minZoom = (!settings.mergeCamera && !aux.settings.zoomout) ? 1 : 0.8 ** 10;
  2890. input.zoom = Math.min(Math.max(input.zoom, minZoom), 0.8 ** -11);
  2891. sync.zoom();
  2892. });
  2893.  
  2894. addEventListener('keydown', e => {
  2895. if (e.code === 'Escape') {
  2896. if (document.activeElement === ui.chat.input)
  2897. ui.chat.input.blur();
  2898. else
  2899. ui.toggleEscOverlay();
  2900. return;
  2901. }
  2902.  
  2903. if (unfocused()) {
  2904. if (e.code === 'Enter' && document.activeElement === ui.chat.input && ui.chat.input.value.length > 0) {
  2905. net.chat(ui.chat.input.value.slice(0, 15));
  2906. ui.chat.input.value = '';
  2907. ui.chat.input.blur();
  2908. }
  2909.  
  2910. return;
  2911. }
  2912.  
  2913. switch (e.code) {
  2914. case 'KeyQ':
  2915. if (!e.repeat)
  2916. net.qdown();
  2917. break;
  2918. case 'KeyW':
  2919. forceW = true;
  2920. w = true;
  2921. break;
  2922. case 'Space': {
  2923. if (!e.repeat) {
  2924. // send mouse position immediately, so the split will go in the correct direction.
  2925. // setTimeout is used to ensure that our mouse position is actually updated (it comes after
  2926. // keydown events)
  2927. setTimeout(() => {
  2928. mouse();
  2929. net.split();
  2930. });
  2931. }
  2932. break;
  2933. }
  2934. case 'Enter': {
  2935. ui.chat.input.focus();
  2936. break;
  2937. }
  2938. }
  2939.  
  2940. if (e.ctrlKey && e.code === 'Tab') {
  2941. e.returnValue = true; // undo e.preventDefault() by SigMod
  2942. e.stopImmediatePropagation(); // prevent SigMod from calling e.preventDefault() afterwards
  2943. } else if (settings.blockBrowserKeybinds && e.code !== 'F11')
  2944. e.preventDefault();
  2945. else if ((e.ctrlKey && e.code === 'KeyW') || e.code === 'Tab')
  2946. e.preventDefault();
  2947. });
  2948.  
  2949. addEventListener('keyup', e => {
  2950. // do not check if unfocused
  2951. if (e.code === 'KeyQ')
  2952. net.qup();
  2953. else if (e.code === 'KeyW')
  2954. w = false;
  2955. });
  2956.  
  2957. // when switching tabs, make sure W is not being held
  2958. addEventListener('blur', () => {
  2959. // force sigmod to get the signal
  2960. if (aux.sigmodSettings?.rapidFeedKey)
  2961. document.dispatchEvent(new KeyboardEvent('keyup', { key: aux.sigmodSettings.rapidFeedKey }));
  2962.  
  2963. w = false;
  2964. });
  2965.  
  2966. addEventListener('beforeunload', e => {
  2967. e.preventDefault();
  2968. });
  2969.  
  2970. // prevent right clicking on the game
  2971. ui.game.canvas.addEventListener('contextmenu', e => e.preventDefault());
  2972.  
  2973. // prevent dragging when some things are selected - i have a habit of unconsciously clicking all the time,
  2974. // making me regularly drag text, disabling my mouse inputs for a bit
  2975. addEventListener('dragstart', e => e.preventDefault());
  2976.  
  2977.  
  2978.  
  2979. // #2 : play and spectate buttons, and captcha
  2980. /** @param {boolean} spectating */
  2981. function playData(spectating) {
  2982. /** @type {HTMLInputElement | null} */
  2983. const nickElement = document.querySelector('input#nick');
  2984. /** @type {HTMLInputElement | null} */
  2985. const password = document.querySelector('input#password');
  2986.  
  2987. return {
  2988. state: spectating ? 2 : undefined,
  2989. name: nickElement?.value ?? '',
  2990. skin: aux.settings.skin,
  2991. token: aux.token?.token,
  2992. sub: (aux.userData?.subscription ?? 0) > Date.now(),
  2993. clan: aux.userData?.clan,
  2994. showClanmates: aux.settings.showClanmates,
  2995. password: password?.value,
  2996. };
  2997. }
  2998.  
  2999. /** @type {HTMLButtonElement} */
  3000. const play = aux.require(
  3001. document.querySelector('button#play-btn'),
  3002. 'Can\'t find the play button. Try reloading the page?',
  3003. );
  3004. /** @type {HTMLButtonElement} */
  3005. const spectate = aux.require(
  3006. document.querySelector('button#spectate-btn'),
  3007. 'Can\'t find the spectate button. Try reloading the page?',
  3008. );
  3009.  
  3010. play.disabled = spectate.disabled = true;
  3011. const playText = play.textContent;
  3012.  
  3013. (async () => {
  3014. const mount = document.createElement('div');
  3015. mount.id = 'sf-captcha-mount';
  3016. mount.style.display = 'none';
  3017. play.parentNode?.insertBefore(mount, play);
  3018.  
  3019. /** @type {Set<() => void> | undefined} */
  3020. let onGrecaptchaReady = new Set();
  3021. /** @type {Set<() => void> | undefined} */
  3022. let onTurnstileReady = new Set();
  3023. let grecaptcha, turnstile, CAPTCHA2, CAPTCHA3, TURNSTILE;
  3024.  
  3025. let readyCheck;
  3026. readyCheck = setInterval(() => {
  3027. // it's possible that recaptcha or turnstile may be removed in the future, so we be redundant to stay
  3028. // safe
  3029. if (onGrecaptchaReady) {
  3030. ({ grecaptcha, CAPTCHA2, CAPTCHA3 } = /** @type {any} */ (window));
  3031. if (grecaptcha?.ready && CAPTCHA2 && CAPTCHA3) {
  3032. const handlers = onGrecaptchaReady;
  3033. onGrecaptchaReady = undefined;
  3034.  
  3035. grecaptcha.ready(() => {
  3036. handlers.forEach(cb => cb());
  3037. // prevent game.js from using grecaptcha and messing things up
  3038. ({ grecaptcha } = /** @type {any} */ (window));
  3039. /** @type {any} */ (window).grecaptcha = {
  3040. execute: () => { },
  3041. ready: () => { },
  3042. render: () => { },
  3043. reset: () => { },
  3044. };
  3045. });
  3046. }
  3047. }
  3048.  
  3049. if (onTurnstileReady) {
  3050. ({ turnstile, TURNSTILE } = /** @type {any} */ (window));
  3051. if (turnstile?.ready && TURNSTILE) {
  3052. const handlers = onTurnstileReady;
  3053. onTurnstileReady = undefined;
  3054. handlers.forEach(cb => cb());
  3055.  
  3056. // prevent game.js from using turnstile and messing things up
  3057. /** @type {any} */ (window).turnstile = {
  3058. execute: () => { },
  3059. ready: () => { },
  3060. render: () => { },
  3061. reset: () => { },
  3062. };
  3063. }
  3064. }
  3065.  
  3066. if (!onGrecaptchaReady && !onTurnstileReady)
  3067. clearInterval(readyCheck);
  3068. }, 50);
  3069.  
  3070. /**
  3071. * @param {string} url
  3072. * @returns {Promise<string>}
  3073. */
  3074. const tokenVariant = async url => {
  3075. const host = new URL(url).host;
  3076. if (host.includes('sigmally.com'))
  3077. return aux.oldFetch(`https://${host}/server/recaptcha/v3`)
  3078. .then(res => res.json())
  3079. .then(res => res.version ?? 'none');
  3080. else
  3081. return Promise.resolve('none');
  3082. };
  3083.  
  3084. /** @type {unique symbol} */
  3085. const used = Symbol();
  3086. /** @type {unique symbol} */
  3087. const waiting = Symbol();
  3088. let nextTryAt = 0;
  3089. /** @type {undefined | typeof waiting | typeof used
  3090. * | { variant: string, token: string | undefined }} */
  3091. let token = undefined;
  3092. /** @type {string | undefined} */
  3093. let turnstileHandle;
  3094. /** @type {number | undefined} */
  3095. let v2Handle;
  3096.  
  3097. input.captchaAcceptedAt = undefined;
  3098.  
  3099. /**
  3100. * @param {string} url
  3101. * @param {string} variant
  3102. * @param {string | undefined} captchaToken
  3103. */
  3104. const publishToken = (url, variant, captchaToken) => {
  3105. const url2 = net.url();
  3106. play.textContent = `${playText} (validating)`;
  3107. if (url === url2) {
  3108. const host = new URL(url).host;
  3109. aux.oldFetch(`https://${host}/server/recaptcha/v3`, {
  3110. method: 'POST',
  3111. headers: { 'content-type': 'application/json' },
  3112. body: JSON.stringify({ token: captchaToken }),
  3113. })
  3114. .then(res => res.json())
  3115. .then(res => {
  3116. if (res.status === 'complete') {
  3117. token = used;
  3118. play.disabled = spectate.disabled = false;
  3119. play.textContent = playText;
  3120. input.captchaAcceptedAt = performance.now();
  3121. net.rejected = false; // wait until we try connecting again
  3122. }
  3123. })
  3124. .catch(err => {
  3125. play.textContent = playText;
  3126. token = undefined;
  3127. nextTryAt = performance.now() + 400;
  3128. throw err;
  3129. });
  3130. } else {
  3131. token = { variant, token: captchaToken };
  3132. }
  3133. };
  3134.  
  3135. setInterval(() => {
  3136. const canPlay = !net.rejected && net.connection()?.readyState === WebSocket.OPEN;
  3137. if (play.disabled !== !canPlay) {
  3138. play.disabled = spectate.disabled = !canPlay;
  3139. play.textContent = playText;
  3140. }
  3141.  
  3142. if (token === waiting) return;
  3143. if (!net.rejected) return;
  3144.  
  3145. const url = net.url();
  3146.  
  3147. if (typeof token !== 'object') {
  3148. // get a new token if first time, or if we're on a new connection now
  3149. if (performance.now() < nextTryAt) return;
  3150.  
  3151. token = waiting;
  3152. play.disabled = spectate.disabled = true;
  3153. play.textContent = `${playText} (getting type)`;
  3154. tokenVariant(url)
  3155. .then(async variant => {
  3156. const url2 = net.url();
  3157. if (url !== url2) {
  3158. // server changed and may want a different variant; restart
  3159. token = undefined;
  3160. return;
  3161. }
  3162.  
  3163. if (variant === 'v2') {
  3164. mount.style.display = 'block';
  3165. play.style.display = spectate.style.display = 'none';
  3166. play.textContent = playText;
  3167. if (v2Handle !== undefined) {
  3168. grecaptcha.reset(v2Handle);
  3169. } else {
  3170. const cb = () => void (v2Handle = grecaptcha.render('sf-captcha-mount', {
  3171. sitekey: CAPTCHA2,
  3172. callback: v2 => {
  3173. mount.style.display = 'none';
  3174. play.style.display = spectate.style.display = '';
  3175. publishToken(url, variant, v2);
  3176. },
  3177. }));
  3178. if (onGrecaptchaReady)
  3179. onGrecaptchaReady.add(cb);
  3180. else
  3181. grecaptcha.ready(cb);
  3182. }
  3183. } else if (variant === 'v3') {
  3184. play.textContent = `${playText} (solving)`;
  3185. const cb = () => grecaptcha.execute(CAPTCHA3)
  3186. .then(v3 => publishToken(url, variant, v3));
  3187. if (onGrecaptchaReady)
  3188. onGrecaptchaReady.add(cb);
  3189. else
  3190. grecaptcha.ready(cb);
  3191. } else if (variant === 'turnstile') {
  3192. mount.style.display = 'block';
  3193. play.style.display = spectate.style.display = 'none';
  3194. play.textContent = playText;
  3195. if (turnstileHandle !== undefined) {
  3196. turnstile.reset(turnstileHandle);
  3197. } else {
  3198. const cb = () => void (turnstileHandle = turnstile.render('#sf-captcha-mount', {
  3199. sitekey: TURNSTILE,
  3200. callback: turnstileToken => {
  3201. mount.style.display = 'none';
  3202. play.style.display = spectate.style.display = '';
  3203. publishToken(url, variant, turnstileToken);
  3204. },
  3205. }));
  3206. if (onTurnstileReady)
  3207. onTurnstileReady.add(cb);
  3208. else
  3209. cb();
  3210. }
  3211. } else {
  3212. // server wants "none" or unknown token variant; don't show a captcha
  3213. publishToken(url, variant, undefined);
  3214. play.disabled = spectate.disabled = false;
  3215. play.textContent = playText;
  3216. }
  3217. }).catch(err => {
  3218. token = undefined;
  3219. nextTryAt = performance.now() + 400;
  3220. console.warn('Error while getting token variant:', err);
  3221. });
  3222. } else {
  3223. // token is ready to be used, check variant
  3224. const got = token;
  3225. token = waiting;
  3226. play.disabled = spectate.disabled = true;
  3227. play.textContent = `${playText} (getting type)`;
  3228. tokenVariant(url)
  3229. .then(variant2 => {
  3230. if (got.variant !== variant2) {
  3231. // server wants a different token variant
  3232. token = undefined;
  3233. } else
  3234. publishToken(url, got.variant, got.token);
  3235. }).catch(err => {
  3236. token = got;
  3237. nextTryAt = performance.now() + 400;
  3238. console.warn('Error while getting token variant:', err);
  3239. });
  3240. }
  3241. }, 100);
  3242.  
  3243. /** @param {MouseEvent} e */
  3244. async function clickHandler(e) {
  3245. if (!net.connection() || net.rejected) return;
  3246. ui.toggleEscOverlay(false);
  3247. if (e.currentTarget === spectate) {
  3248. // you should be able to escape sigmod auto-respawn and spectate as long as you don't have mass
  3249. let score = 0;
  3250. for (const id of world.mine) {
  3251. const cell = world.cells.get(id);
  3252. if (!cell) continue;
  3253. score += cell.nr * cell.nr / 100;
  3254. }
  3255.  
  3256. if (0 < score && score < 5500) {
  3257. world.stats.spawnedAt = undefined; // prevent death screen from appearing
  3258. net.chat('/leaveworld'); // instant respawn
  3259. net.play(playData(true)); // required, idk why
  3260. net.chat('/joinworld 1'); // spectating doesn't automatically put you back into the world
  3261. }
  3262. }
  3263.  
  3264. net.play(playData(e.currentTarget === spectate));
  3265. }
  3266.  
  3267. play.addEventListener('click', clickHandler);
  3268. spectate.addEventListener('click', clickHandler);
  3269. })();
  3270.  
  3271. return input;
  3272. })();
  3273.  
  3274.  
  3275.  
  3276. //////////////////////////
  3277. // Configure WebGL Data //
  3278. //////////////////////////
  3279. const glconf = (() => {
  3280. // note: WebGL functions only really return null if the context is lost - in which case, data will be replaced
  3281. // anyway after it's restored. so, we cast everything to a non-null type.
  3282. const glconf = {};
  3283. const programs = glconf.programs = {};
  3284. const uniforms = glconf.uniforms = {};
  3285. /** @type {WebGLBuffer} */
  3286. glconf.pelletAlphaBuffer = /** @type {never} */ (undefined);
  3287. /** @type {WebGLBuffer} */
  3288. glconf.pelletBuffer = /** @type {never} */ (undefined);
  3289. /** @type {{
  3290. * vao: WebGLVertexArrayObject,
  3291. * circleBuffer: WebGLBuffer,
  3292. * alphaBuffer: WebGLBuffer,
  3293. * alphaBufferSize: number }[]} */
  3294. glconf.vao = [];
  3295.  
  3296. const gl = ui.game.gl;
  3297. /** @type {Map<string, number>} */
  3298. const uboBindings = new Map();
  3299.  
  3300. /**
  3301. * @param {string} name
  3302. * @param {number} type
  3303. * @param {string} source
  3304. */
  3305. function shader(name, type, source) {
  3306. const s = /** @type {WebGLShader} */ (gl.createShader(type));
  3307. gl.shaderSource(s, source);
  3308. gl.compileShader(s);
  3309.  
  3310. // note: compilation errors should not happen in production
  3311. aux.require(
  3312. gl.getShaderParameter(s, gl.COMPILE_STATUS) || gl.isContextLost(),
  3313. `Can\'t compile WebGL2 shader "${name}". You might be on a weird browser.\n\nFull error log:\n` +
  3314. gl.getShaderInfoLog(s),
  3315. );
  3316.  
  3317. return s;
  3318. }
  3319.  
  3320. /**
  3321. * @param {string} name
  3322. * @param {string} vSource
  3323. * @param {string} fSource
  3324. * @param {string[]} ubos
  3325. * @param {string[]} textures
  3326. */
  3327. function program(name, vSource, fSource, ubos, textures) {
  3328. const vShader = shader(`${name}.vShader`, gl.VERTEX_SHADER, vSource.trim());
  3329. const fShader = shader(`${name}.fShader`, gl.FRAGMENT_SHADER, fSource.trim());
  3330. const p = /** @type {WebGLProgram} */ (gl.createProgram());
  3331.  
  3332. gl.attachShader(p, vShader);
  3333. gl.attachShader(p, fShader);
  3334. gl.linkProgram(p);
  3335.  
  3336. // note: linking errors should not happen in production
  3337. aux.require(
  3338. gl.getProgramParameter(p, gl.LINK_STATUS) || gl.isContextLost(),
  3339. `Can\'t link WebGL2 program "${name}". You might be on a weird browser.\n\nFull error log:\n` +
  3340. gl.getProgramInfoLog(p),
  3341. );
  3342.  
  3343. for (const tag of ubos) {
  3344. const index = gl.getUniformBlockIndex(p, tag); // returns 4294967295 if invalid... just don't make typos
  3345. let binding = uboBindings.get(tag);
  3346. if (binding === undefined)
  3347. uboBindings.set(tag, binding = uboBindings.size);
  3348. gl.uniformBlockBinding(p, index, binding);
  3349.  
  3350. const size = gl.getActiveUniformBlockParameter(p, index, gl.UNIFORM_BLOCK_DATA_SIZE);
  3351. const ubo = uniforms[tag] = gl.createBuffer();
  3352. gl.bindBuffer(gl.UNIFORM_BUFFER, ubo);
  3353. gl.bufferData(gl.UNIFORM_BUFFER, size, gl.DYNAMIC_DRAW);
  3354. gl.bindBufferBase(gl.UNIFORM_BUFFER, binding, ubo);
  3355. }
  3356.  
  3357. // bind texture uniforms to TEXTURE0, TEXTURE1, etc.
  3358. gl.useProgram(p);
  3359. for (let i = 0; i < textures.length; ++i) {
  3360. const loc = gl.getUniformLocation(p, textures[i]);
  3361. gl.uniform1i(loc, i);
  3362. }
  3363. gl.useProgram(null);
  3364.  
  3365. return p;
  3366. }
  3367.  
  3368. const parts = {
  3369. boilerplate: '#version 300 es\nprecision highp float; precision highp int;',
  3370. borderUbo: `layout(std140) uniform Border { // size = 0x24
  3371. vec4 u_border_color; // @ 0x00, i = 0
  3372. vec4 u_border_xyzw_lrtb; // @ 0x10, i = 4
  3373. int u_border_flags; // @ 0x20, i = 8
  3374. float u_background_width; // @ 0x24, i = 9
  3375. float u_background_height; // @ 0x28, i = 10
  3376. };`,
  3377. cameraUbo: `layout(std140) uniform Camera { // size = 0x10
  3378. float u_camera_ratio; // @ 0x00
  3379. float u_camera_scale; // @ 0x04
  3380. vec2 u_camera_pos; // @ 0x08
  3381. };`,
  3382. cellUbo: `layout(std140) uniform Cell { // size = 0x28
  3383. float u_cell_radius; // @ 0x00, i = 0
  3384. float u_cell_radius_skin; // @ 0x04, i = 1
  3385. vec2 u_cell_pos; // @ 0x08, i = 2
  3386. vec4 u_cell_color; // @ 0x10, i = 4
  3387. float u_cell_alpha; // @ 0x20, i = 8
  3388. int u_cell_flags; // @ 0x24, i = 9
  3389. };`,
  3390. cellSettingsUbo: `layout(std140) uniform CellSettings { // size = 0x40
  3391. vec4 u_cell_active_outline; // @ 0x00
  3392. vec4 u_cell_inactive_outline; // @ 0x10
  3393. vec4 u_cell_unsplittable_outline; // @ 0x20
  3394. vec4 u_cell_subtle_outline_override; // @ 0x30
  3395. float u_cell_active_outline_thickness; // @ 0x40
  3396. };`,
  3397. circleUbo: `layout(std140) uniform Circle { // size = 0x08
  3398. float u_circle_alpha; // @ 0x00
  3399. float u_circle_scale; // @ 0x04
  3400. };`,
  3401. textUbo: `layout(std140) uniform Text { // size = 0x38
  3402. vec4 u_text_color1; // @ 0x00, i = 0
  3403. vec4 u_text_color2; // @ 0x10, i = 4
  3404. float u_text_alpha; // @ 0x20, i = 8
  3405. float u_text_aspect_ratio; // @ 0x24, i = 9
  3406. float u_text_scale; // @ 0x28, i = 10
  3407. int u_text_silhouette_enabled; // @ 0x2c, i = 11
  3408. vec2 u_text_offset; // @ 0x30, i = 12
  3409. };`,
  3410. tracerUbo: `layout(std140) uniform Tracer { // size = 0x10
  3411. vec2 u_tracer_pos1; // @ 0x00, i = 0
  3412. vec2 u_tracer_pos2; // @ 0x08, i = 2
  3413. };`,
  3414. };
  3415.  
  3416.  
  3417.  
  3418. glconf.init = () => {
  3419. gl.enable(gl.BLEND);
  3420. gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
  3421.  
  3422. // create programs and uniforms
  3423. programs.bg = program('bg', `
  3424. ${parts.boilerplate}
  3425. layout(location = 0) in vec2 a_vertex;
  3426. ${parts.borderUbo}
  3427. ${parts.cameraUbo}
  3428. flat out float f_blur;
  3429. flat out float f_thickness;
  3430. out vec2 v_uv;
  3431. out vec2 v_world_pos;
  3432.  
  3433. void main() {
  3434. f_blur = 1.0 * (540.0 * u_camera_scale);
  3435. f_thickness = max(3.0 / f_blur, 25.0); // force border to always be visible, otherwise it flickers
  3436.  
  3437. v_world_pos = a_vertex * vec2(u_camera_ratio, 1.0) / u_camera_scale;
  3438. v_world_pos += u_camera_pos * vec2(1.0, -1.0);
  3439.  
  3440. if ((u_border_flags & 0x04) != 0) { // background repeating
  3441. v_uv = v_world_pos * 0.02 * (50.0 / u_background_width);
  3442. v_uv /= vec2(1.0, u_background_height / u_background_width);
  3443. } else {
  3444. v_uv = (v_world_pos - vec2(u_border_xyzw_lrtb.x, u_border_xyzw_lrtb.z))
  3445. / vec2(u_border_xyzw_lrtb.y - u_border_xyzw_lrtb.x,
  3446. u_border_xyzw_lrtb.w - u_border_xyzw_lrtb.z);
  3447. v_uv = vec2(v_uv.x, 1.0 - v_uv.y); // flip vertically
  3448. }
  3449.  
  3450. gl_Position = vec4(a_vertex, 0, 1); // span the whole screen
  3451. }
  3452. `, `
  3453. ${parts.boilerplate}
  3454. flat in float f_blur;
  3455. flat in float f_thickness;
  3456. in vec2 v_uv;
  3457. in vec2 v_world_pos;
  3458. ${parts.borderUbo}
  3459. ${parts.cameraUbo}
  3460. uniform sampler2D u_texture;
  3461. out vec4 out_color;
  3462.  
  3463. void main() {
  3464. if ((u_border_flags & 0x01) != 0) { // background enabled
  3465. if ((u_border_flags & 0x04) != 0 // repeating
  3466. || (0.0 <= min(v_uv.x, v_uv.y) && max(v_uv.x, v_uv.y) <= 1.0)) { // within border
  3467. out_color = texture(u_texture, v_uv);
  3468. }
  3469. }
  3470.  
  3471. // make a larger inner rectangle and a normal inverted outer rectangle
  3472. float inner_alpha = min(
  3473. min((v_world_pos.x + f_thickness) - u_border_xyzw_lrtb.x,
  3474. u_border_xyzw_lrtb.y - (v_world_pos.x - f_thickness)),
  3475. min((v_world_pos.y + f_thickness) - u_border_xyzw_lrtb.z,
  3476. u_border_xyzw_lrtb.w - (v_world_pos.y - f_thickness))
  3477. );
  3478. float outer_alpha = max(
  3479. max(u_border_xyzw_lrtb.x - v_world_pos.x, v_world_pos.x - u_border_xyzw_lrtb.y),
  3480. max(u_border_xyzw_lrtb.z - v_world_pos.y, v_world_pos.y - u_border_xyzw_lrtb.w)
  3481. );
  3482. float alpha = clamp(f_blur * min(inner_alpha, outer_alpha), 0.0, 1.0);
  3483.  
  3484. out_color = out_color * (1.0 - alpha) + u_border_color * alpha;
  3485. }
  3486. `, ['Border', 'Camera'], ['u_texture']);
  3487.  
  3488.  
  3489.  
  3490. programs.cell = program('cell', `
  3491. ${parts.boilerplate}
  3492. layout(location = 0) in vec2 a_vertex;
  3493. ${parts.cameraUbo}
  3494. ${parts.cellUbo}
  3495. ${parts.cellSettingsUbo}
  3496. flat out vec4 f_active_outline;
  3497. flat out float f_active_radius;
  3498. flat out float f_blur;
  3499. flat out int f_show_skin;
  3500. flat out vec4 f_subtle_outline;
  3501. flat out float f_subtle_radius;
  3502. flat out vec4 f_unsplittable_outline;
  3503. flat out float f_unsplittable_radius;
  3504. out vec2 v_vertex;
  3505. out vec2 v_uv;
  3506.  
  3507. void main() {
  3508. f_blur = 0.5 * u_cell_radius * (540.0 * u_camera_scale);
  3509. f_show_skin = u_cell_flags & 0x01;
  3510.  
  3511. // subtle outlines (at least 1px wide)
  3512. float subtle_thickness = max(max(u_cell_radius * 0.02, 2.0 / (540.0 * u_camera_scale)), 10.0);
  3513. f_subtle_radius = 1.0 - (subtle_thickness / u_cell_radius);
  3514. if ((u_cell_flags & 0x02) != 0) {
  3515. f_subtle_outline = u_cell_color * 0.9; // darker outline by default
  3516. f_subtle_outline.rgb += (u_cell_subtle_outline_override.rgb - f_subtle_outline.rgb)
  3517. * u_cell_subtle_outline_override.a;
  3518. } else {
  3519. f_subtle_outline = vec4(0, 0, 0, 0);
  3520. }
  3521.  
  3522. // active multibox outlines (thick, a % of the visible cell radius)
  3523. f_active_radius = 1.0 - u_cell_active_outline_thickness;
  3524. if ((u_cell_flags & 0x0c) != 0) {
  3525. f_active_outline = (u_cell_flags & 0x04) != 0 ? u_cell_active_outline : u_cell_inactive_outline;
  3526. } else {
  3527. f_active_outline = vec4(0, 0, 0, 0);
  3528. }
  3529.  
  3530. // unsplittable cell outline, 2x the subtle thickness
  3531. // (except at small sizes, it shouldn't look overly thick)
  3532. float unsplittable_thickness = max(max(u_cell_radius * 0.04, 4.0 / (540.0 * u_camera_scale)), 10.0);
  3533. f_unsplittable_radius = 1.0 - (unsplittable_thickness / u_cell_radius);
  3534. if ((u_cell_flags & 0x10) != 0) {
  3535. f_unsplittable_outline = u_cell_unsplittable_outline;
  3536. } else {
  3537. f_unsplittable_outline = vec4(0, 0, 0, 0);
  3538. }
  3539.  
  3540. v_vertex = a_vertex;
  3541. v_uv = a_vertex * (u_cell_radius / u_cell_radius_skin) * 0.5 + 0.5;
  3542.  
  3543. vec2 clip_pos = -u_camera_pos + u_cell_pos + v_vertex * u_cell_radius;
  3544. clip_pos *= u_camera_scale * vec2(1.0 / u_camera_ratio, -1.0);
  3545. gl_Position = vec4(clip_pos, 0, 1);
  3546. }
  3547. `, `
  3548. ${parts.boilerplate}
  3549. flat in vec4 f_active_outline;
  3550. flat in float f_active_radius;
  3551. flat in float f_blur;
  3552. flat in int f_show_skin;
  3553. flat in vec4 f_subtle_outline;
  3554. flat in float f_subtle_radius;
  3555. flat in vec4 f_unsplittable_outline;
  3556. flat in float f_unsplittable_radius;
  3557. in vec2 v_vertex;
  3558. in vec2 v_uv;
  3559. ${parts.cameraUbo}
  3560. ${parts.cellUbo}
  3561. ${parts.cellSettingsUbo}
  3562. uniform sampler2D u_skin;
  3563. out vec4 out_color;
  3564.  
  3565. void main() {
  3566. float d = length(v_vertex.xy);
  3567.  
  3568. // skin; square clipping, outskirts should use the cell color
  3569. if (f_show_skin != 0 && 0.0 <= min(v_uv.x, v_uv.y) && max(v_uv.x, v_uv.y) <= 1.0) {
  3570. vec4 tex = texture(u_skin, v_uv);
  3571. out_color = out_color * (1.0 - tex.a) + tex;
  3572. } else {
  3573. out_color = u_cell_color;
  3574. }
  3575.  
  3576. // subtle outline
  3577. float a = clamp(f_blur * (d - f_subtle_radius), 0.0, 1.0) * f_subtle_outline.a;
  3578. out_color.rgb += (f_subtle_outline.rgb - out_color.rgb) * a;
  3579.  
  3580. // active multibox outline
  3581. a = clamp(f_blur * (d - f_active_radius), 0.0, 1.0) * f_active_outline.a;
  3582. out_color.rgb += (f_active_outline.rgb - out_color.rgb) * a;
  3583.  
  3584. // unsplittable cell outline
  3585. a = clamp(f_blur * (d - f_unsplittable_radius), 0.0, 1.0) * f_unsplittable_outline.a;
  3586. out_color.rgb += (f_unsplittable_outline.rgb - out_color.rgb) * a;
  3587.  
  3588. // final circle mask
  3589. a = clamp(-f_blur * (d - 1.0), 0.0, 1.0);
  3590. out_color.a *= a * u_cell_alpha;
  3591. }
  3592. `, ['Camera', 'Cell', 'CellSettings'], ['u_skin']);
  3593.  
  3594.  
  3595.  
  3596. // also used to draw glow
  3597. programs.circle = program('circle', `
  3598. ${parts.boilerplate}
  3599. layout(location = 0) in vec2 a_vertex;
  3600. layout(location = 1) in vec2 a_cell_pos;
  3601. layout(location = 2) in float a_cell_radius;
  3602. layout(location = 3) in vec4 a_cell_color;
  3603. layout(location = 4) in float a_cell_alpha;
  3604. ${parts.cameraUbo}
  3605. ${parts.circleUbo}
  3606. out vec2 v_vertex;
  3607. flat out float f_blur;
  3608. flat out vec4 f_cell_color;
  3609.  
  3610. void main() {
  3611. float radius = a_cell_radius;
  3612. f_cell_color = a_cell_color * vec4(1, 1, 1, a_cell_alpha * u_circle_alpha);
  3613. if (u_circle_scale > 0.0) {
  3614. f_blur = 1.0;
  3615. radius *= u_circle_scale;
  3616. } else {
  3617. f_blur = 0.5 * a_cell_radius * (540.0 * u_camera_scale);
  3618. }
  3619. v_vertex = a_vertex;
  3620.  
  3621. vec2 clip_pos = -u_camera_pos + a_cell_pos + v_vertex * radius;
  3622. clip_pos *= u_camera_scale * vec2(1.0 / u_camera_ratio, -1.0);
  3623. gl_Position = vec4(clip_pos, 0, 1);
  3624. }
  3625. `, `
  3626. ${parts.boilerplate}
  3627. in vec2 v_vertex;
  3628. flat in float f_blur;
  3629. flat in vec4 f_cell_color;
  3630. out vec4 out_color;
  3631.  
  3632. void main() {
  3633. // use squared distance for more natural glow; shouldn't matter for pellets
  3634. float d = length(v_vertex.xy);
  3635. out_color = f_cell_color;
  3636. out_color.a *= clamp(f_blur * (1.0 - d), 0.0, 1.0);
  3637. }
  3638. `, ['Camera', 'Circle'], []);
  3639.  
  3640.  
  3641.  
  3642. programs.text = program('text', `
  3643. ${parts.boilerplate}
  3644. layout(location = 0) in vec2 a_vertex;
  3645. ${parts.cameraUbo}
  3646. ${parts.cellUbo}
  3647. ${parts.textUbo}
  3648. out vec4 v_color;
  3649. out vec2 v_uv;
  3650. out vec2 v_vertex;
  3651.  
  3652. void main() {
  3653. v_uv = a_vertex * 0.5 + 0.5;
  3654. float c2_alpha = (v_uv.x + v_uv.y) / 2.0;
  3655. v_color = u_text_color1 * (1.0 - c2_alpha) + u_text_color2 * c2_alpha;
  3656. v_vertex = a_vertex;
  3657.  
  3658. vec2 clip_space = v_vertex * u_text_scale + u_text_offset;
  3659. clip_space *= u_cell_radius_skin * 0.45 * vec2(u_text_aspect_ratio, 1.0);
  3660. clip_space += -u_camera_pos + u_cell_pos;
  3661. clip_space *= u_camera_scale * vec2(1.0 / u_camera_ratio, -1.0);
  3662. gl_Position = vec4(clip_space, 0, 1);
  3663. }
  3664. `, `
  3665. ${parts.boilerplate}
  3666. in vec4 v_color;
  3667. in vec2 v_uv;
  3668. in vec2 v_vertex;
  3669. ${parts.cameraUbo}
  3670. ${parts.cellUbo}
  3671. ${parts.textUbo}
  3672. uniform sampler2D u_texture;
  3673. uniform sampler2D u_silhouette;
  3674. out vec4 out_color;
  3675.  
  3676. void main() {
  3677. vec4 normal = texture(u_texture, v_uv);
  3678.  
  3679. if (u_text_silhouette_enabled != 0) {
  3680. vec4 silhouette = texture(u_silhouette, v_uv);
  3681.  
  3682. // #fff - #000 => color (text)
  3683. // #fff - #fff => #fff (respect emoji)
  3684. // #888 - #888 => #888 (respect emoji)
  3685. // #fff - #888 => #888 + color/2 (blur/antialias)
  3686. out_color = silhouette + (normal - silhouette) * v_color;
  3687. } else {
  3688. out_color = normal * v_color;
  3689. }
  3690.  
  3691. out_color.a *= u_text_alpha;
  3692. }
  3693. `, ['Camera', 'Cell', 'Text'], ['u_texture', 'u_silhouette']);
  3694.  
  3695. programs.tracer = program('tracer', `
  3696. ${parts.boilerplate}
  3697. layout(location = 0) in vec2 a_vertex;
  3698. ${parts.cameraUbo}
  3699. ${parts.tracerUbo}
  3700. out vec2 v_vertex;
  3701.  
  3702. void main() {
  3703. v_vertex = a_vertex;
  3704. float alpha = (a_vertex.x + 1.0) / 2.0;
  3705. float d = length(u_tracer_pos2 - u_tracer_pos1);
  3706. float thickness = 0.002 / u_camera_scale;
  3707. // black magic
  3708. vec2 world_pos = u_tracer_pos1 + (u_tracer_pos2 - u_tracer_pos1)
  3709. * mat2(alpha, a_vertex.y / d * thickness, a_vertex.y / d * -thickness, alpha);
  3710.  
  3711. vec2 clip_pos = -u_camera_pos + world_pos;
  3712. clip_pos *= u_camera_scale * vec2(1.0 / u_camera_ratio, -1.0);
  3713. gl_Position = vec4(clip_pos, 0, 1);
  3714. }
  3715. `, `
  3716. ${parts.boilerplate}
  3717. in vec2 v_pos;
  3718. out vec4 out_color;
  3719.  
  3720. void main() {
  3721. out_color = vec4(0.5, 0.5, 0.5, 0.25);
  3722. }
  3723. `, ['Camera', 'Tracer'], []);
  3724.  
  3725. // initialize two VAOs; one for pellets, one for cell glow only
  3726. glconf.vao = [];
  3727. for (let i = 0; i < 2; ++i) {
  3728. const vao = /** @type {WebGLVertexArrayObject} */ (gl.createVertexArray());
  3729. gl.bindVertexArray(vao);
  3730.  
  3731. // square (location = 0), used for all instances
  3732. gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
  3733. gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ -1, -1, 1, -1, -1, 1, 1, 1 ]), gl.STATIC_DRAW);
  3734. gl.enableVertexAttribArray(0);
  3735. gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
  3736.  
  3737. // pellet/circle buffer (each instance is 6 floats or 24 bytes)
  3738. const circleBuffer = /** @type {WebGLBuffer} */ (gl.createBuffer());
  3739. gl.bindBuffer(gl.ARRAY_BUFFER, circleBuffer);
  3740. // a_cell_pos, vec2 (location = 1)
  3741. gl.enableVertexAttribArray(1);
  3742. gl.vertexAttribPointer(1, 2, gl.FLOAT, false, 4 * 7, 0);
  3743. gl.vertexAttribDivisor(1, 1);
  3744. // a_cell_radius, float (location = 2)
  3745. gl.enableVertexAttribArray(2);
  3746. gl.vertexAttribPointer(2, 1, gl.FLOAT, false, 4 * 7, 4 * 2);
  3747. gl.vertexAttribDivisor(2, 1);
  3748. // a_cell_color, vec3 (location = 3)
  3749. gl.enableVertexAttribArray(3);
  3750. gl.vertexAttribPointer(3, 4, gl.FLOAT, false, 4 * 7, 4 * 3);
  3751. gl.vertexAttribDivisor(3, 1);
  3752.  
  3753. // pellet/circle alpha buffer, updated every frame
  3754. const alphaBuffer = /** @type {WebGLBuffer} */ (gl.createBuffer());
  3755. gl.bindBuffer(gl.ARRAY_BUFFER, alphaBuffer);
  3756. // a_cell_alpha, float (location = 4)
  3757. gl.enableVertexAttribArray(4);
  3758. gl.vertexAttribPointer(4, 1, gl.FLOAT, false, 0, 0);
  3759. gl.vertexAttribDivisor(4, 1);
  3760.  
  3761. glconf.vao.push({ vao, alphaBuffer, circleBuffer, alphaBufferSize: 0 });
  3762. }
  3763.  
  3764. gl.bindVertexArray(glconf.vao[0].vao);
  3765. };
  3766.  
  3767. glconf.init();
  3768. return glconf;
  3769. })();
  3770.  
  3771.  
  3772.  
  3773. ///////////////////////////////
  3774. // Define Rendering Routines //
  3775. ///////////////////////////////
  3776. const render = (() => {
  3777. const render = {};
  3778. const { gl } = ui.game;
  3779.  
  3780. // #1 : define small misc objects
  3781. // no point in breaking this across multiple lines
  3782. // eslint-disable-next-line max-len
  3783. const darkGridSrc = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAACXBIWXMAAA7DAAAOwwHHb6hkAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAGBJREFUaIHtz4EJwCAAwDA39oT/H+qeEAzSXNA+a61xgfmeLtilEU0jmkY0jWga0TSiaUTTiKYRTSOaRjSNaBrRNKJpRNOIphFNI5pGNI1oGtE0omlEc83IN8aYpyN2+AH6nwOVa0odrQAAAABJRU5ErkJggg==';
  3784. // eslint-disable-next-line max-len
  3785. const lightGridSrc = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAACXBIWXMAAA7DAAAOwwHHb6hkAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAGFJREFUaIHtzwENgDAQwMA9LvAvdJgg2UF6CtrZe6+vm5n7Oh3xlkY0jWga0TSiaUTTiKYRTSOaRjSNaBrRNKJpRNOIphFNI5pGNI1oGtE0omlE04imEc1vRmatdZ+OeMMDa8cDlf3ZAHkAAAAASUVORK5CYII=';
  3786.  
  3787. let lastMinimapDraw = performance.now();
  3788. /** @type {{ bg: ImageData, darkTheme: boolean } | undefined} */
  3789. let minimapCache;
  3790. document.fonts.ready.then(() => void (minimapCache = undefined)); // make sure minimap is drawn with Ubuntu font
  3791.  
  3792.  
  3793. // #2 : define helper functions
  3794. const { resetTextureCache, textureFromCache } = (() => {
  3795. /** @type {Map<string, { texture: WebGLTexture, width: number, height: number } | null>} */
  3796. const cache = new Map();
  3797. render.textureCache = cache;
  3798.  
  3799. return {
  3800. resetTextureCache: () => cache.clear(),
  3801. /**
  3802. * @param {string} src
  3803. * @returns {{ texture: WebGLTexture, width: number, height: number } | undefined}
  3804. */
  3805. textureFromCache: src => {
  3806. const cached = cache.get(src);
  3807. if (cached !== undefined)
  3808. return cached ?? undefined;
  3809.  
  3810. cache.set(src, null);
  3811.  
  3812. const image = new Image();
  3813. image.crossOrigin = 'anonymous';
  3814. image.addEventListener('load', () => {
  3815. const texture = /** @type {WebGLTexture} */ (gl.createTexture());
  3816. if (!texture) return;
  3817.  
  3818. gl.bindTexture(gl.TEXTURE_2D, texture);
  3819. gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
  3820. gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_NEAREST);
  3821. gl.generateMipmap(gl.TEXTURE_2D);
  3822. cache.set(src, { texture, width: image.width, height: image.height });
  3823. });
  3824. image.src = src;
  3825.  
  3826. return undefined;
  3827. },
  3828. };
  3829. })();
  3830. render.resetTextureCache = resetTextureCache;
  3831.  
  3832. const { refreshTextCache, massTextFromCache, resetTextCache, textFromCache } = (() => {
  3833. /**
  3834. * @template {boolean} T
  3835. * @typedef {{
  3836. * aspectRatio: number,
  3837. * text: WebGLTexture | null,
  3838. * silhouette: WebGLTexture | null | undefined,
  3839. * accessed: number
  3840. * }} CacheEntry
  3841. */
  3842. /** @type {Map<string, CacheEntry<boolean>>} */
  3843. const cache = new Map();
  3844. render.textCache = cache;
  3845.  
  3846. setInterval(() => {
  3847. // remove text after not being used for 1 minute
  3848. const now = performance.now();
  3849. cache.forEach((entry, text) => {
  3850. if (now - entry.accessed > 60_000) {
  3851. // immediately delete text instead of waiting for GC
  3852. if (entry.text !== undefined)
  3853. gl.deleteTexture(entry.text);
  3854. if (entry.silhouette !== undefined)
  3855. gl.deleteTexture(entry.silhouette);
  3856. cache.delete(text);
  3857. }
  3858. });
  3859. }, 60_000);
  3860.  
  3861. const canvas = document.createElement('canvas');
  3862. const ctx = aux.require(
  3863. canvas.getContext('2d', { willReadFrequently: true }),
  3864. 'Unable to get 2D context for text drawing. This is probably your browser being weird, maybe reload ' +
  3865. 'the page?',
  3866. );
  3867.  
  3868. // sigmod forces a *really* ugly shadow on ctx.fillText so we have to lock the property beforehand
  3869. const realProps = Object.getOwnPropertyDescriptors(Object.getPrototypeOf(ctx));
  3870. const realShadowBlurSet
  3871. = aux.require(realProps.shadowBlur.set, 'did CanvasRenderingContext2D spec change?').bind(ctx);
  3872. const realShadowColorSet
  3873. = aux.require(realProps.shadowColor.set, 'did CanvasRenderingContext2D spec change?').bind(ctx);
  3874. Object.defineProperties(ctx, {
  3875. shadowBlur: {
  3876. get: () => 0,
  3877. set: x => {
  3878. if (x === 0) realShadowBlurSet(0);
  3879. else realShadowBlurSet(8);
  3880. },
  3881. },
  3882. shadowColor: {
  3883. get: () => 'transparent',
  3884. set: x => {
  3885. if (x === 'transparent') realShadowColorSet('transparent');
  3886. else realShadowColorSet('#0003');
  3887. },
  3888. },
  3889. });
  3890.  
  3891. /**
  3892. * @param {string} text
  3893. * @param {boolean} silhouette
  3894. * @param {boolean} mass
  3895. * @returns {WebGLTexture | null}
  3896. */
  3897. const texture = (text, silhouette, mass) => {
  3898. const texture = gl.createTexture();
  3899. if (!texture) return texture;
  3900.  
  3901. const baseTextSize = 96;
  3902. const textSize = baseTextSize * (mass ? 0.5 * settings.massScaleFactor : settings.nameScaleFactor);
  3903. const lineWidth = Math.ceil(textSize / 10) * settings.textOutlinesFactor;
  3904.  
  3905. let font = '';
  3906. if (mass ? settings.massBold : settings.nameBold)
  3907. font = 'bold';
  3908. font += ' ' + textSize + 'px Ubuntu';
  3909.  
  3910. ctx.font = font;
  3911. // if rendering an empty string (somehow) then width can be 0 with no outlines
  3912. canvas.width = (ctx.measureText(text).width + lineWidth * 2) || 1;
  3913. canvas.height = textSize * 3;
  3914. ctx.clearRect(0, 0, canvas.width, canvas.height);
  3915.  
  3916. // setting canvas.width resets the canvas state
  3917. ctx.font = font;
  3918. ctx.lineJoin = 'round';
  3919. ctx.lineWidth = lineWidth;
  3920. ctx.fillStyle = silhouette ? '#000' : '#fff';
  3921. ctx.strokeStyle = '#000';
  3922. ctx.textBaseline = 'middle';
  3923.  
  3924. ctx.shadowBlur = lineWidth;
  3925. ctx.shadowColor = lineWidth > 0 ? '#0002' : 'transparent';
  3926.  
  3927. // add a space, which is to prevent sigmod from detecting the name
  3928. if (lineWidth > 0) ctx.strokeText(text + ' ', lineWidth, textSize * 1.5);
  3929. ctx.shadowColor = 'transparent';
  3930. ctx.fillText(text + ' ', lineWidth, textSize * 1.5);
  3931.  
  3932. const data = ctx.getImageData(0, 0, canvas.width, canvas.height);
  3933.  
  3934. gl.bindTexture(gl.TEXTURE_2D, texture);
  3935. gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, data);
  3936. gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST_MIPMAP_LINEAR);
  3937. gl.generateMipmap(gl.TEXTURE_2D);
  3938. return texture;
  3939. };
  3940.  
  3941. let massAspectRatio = 1; // assumption: all mass digits are the same aspect ratio - true in the Ubuntu font
  3942. /** @type {(WebGLTexture | undefined)[]} */
  3943. const massTextCache = [];
  3944.  
  3945. /**
  3946. * @param {string} digit
  3947. * @returns {{ aspectRatio: number, texture: WebGLTexture | null }}
  3948. */
  3949. const massTextFromCache = digit => {
  3950. let cached = massTextCache[digit];
  3951. if (!cached) {
  3952. cached = massTextCache[digit] = texture(digit, false, true);
  3953. massAspectRatio = canvas.width / canvas.height;
  3954. }
  3955.  
  3956. return { aspectRatio: massAspectRatio, texture: cached };
  3957. };
  3958.  
  3959. const resetTextCache = () => {
  3960. cache.clear();
  3961. while (massTextCache.pop());
  3962. };
  3963.  
  3964. let drawnMassBold = false;
  3965. let drawnMassScaleFactor = -1;
  3966. let drawnNamesBold = false;
  3967. let drawnNamesScaleFactor = -1;
  3968. let drawnOutlinesFactor = 1;
  3969.  
  3970. const refreshTextCache = () => {
  3971. if (drawnMassBold !== settings.massBold || drawnMassScaleFactor !== settings.massScaleFactor
  3972. || drawnNamesScaleFactor !== settings.nameScaleFactor || drawnNamesBold !== settings.nameBold
  3973. || drawnOutlinesFactor !== settings.textOutlinesFactor
  3974. ) {
  3975. resetTextCache();
  3976. drawnMassBold = settings.massBold;
  3977. drawnMassScaleFactor = settings.massScaleFactor;
  3978. drawnNamesBold = settings.nameBold;
  3979. drawnNamesScaleFactor = settings.nameScaleFactor;
  3980. drawnOutlinesFactor = settings.textOutlinesFactor;
  3981. }
  3982. };
  3983.  
  3984. /**
  3985. * @template {boolean} T
  3986. * @param {string} text
  3987. * @param {T} silhouette
  3988. * @returns {CacheEntry<T>}
  3989. */
  3990. const textFromCache = (text, silhouette) => {
  3991. let entry = cache.get(text);
  3992. if (!entry) {
  3993. const shortened = aux.trim(text);
  3994. /** @type {CacheEntry<T>} */
  3995. entry = {
  3996. text: texture(shortened, false, false),
  3997. aspectRatio: canvas.width / canvas.height, // mind the execution order
  3998. silhouette: silhouette ? texture(shortened, true, false) : undefined,
  3999. accessed: performance.now(),
  4000. };
  4001. cache.set(text, entry);
  4002. } else {
  4003. entry.accessed = performance.now();
  4004. }
  4005.  
  4006. if (silhouette && entry.silhouette === undefined) {
  4007. setTimeout(() => {
  4008. entry.silhouette = texture(aux.trim(text), true, false);
  4009. });
  4010. }
  4011.  
  4012. return entry;
  4013. };
  4014.  
  4015. // reload text once Ubuntu has loaded, prevents some serif fonts from being locked in
  4016. document.fonts.ready.then(() => resetTextCache());
  4017.  
  4018. return { refreshTextCache, massTextFromCache, resetTextCache, textFromCache };
  4019. })();
  4020. render.resetTextCache = resetTextCache;
  4021. render.textFromCache = textFromCache;
  4022.  
  4023. let cellAlpha = new Float32Array(0);
  4024. let cellBuffer = new Float32Array(0);
  4025. let pelletAlpha = new Float32Array(0);
  4026. let pelletBuffer = new Float32Array(0);
  4027. let uploadedPellets = 0;
  4028. /**
  4029. * @param {'cells' | 'pellets'} key
  4030. * @param {number=} now
  4031. */
  4032. render.upload = (key, now) => {
  4033. if ((key === 'pellets' && aux.sigmodSettings?.hidePellets)
  4034. || performance.now() - render.lastFrame > 45_000) {
  4035. // do not render pellets on inactive windows (very laggy!)
  4036. uploadedPellets = 0;
  4037. return;
  4038. }
  4039.  
  4040. now ??= performance.now(); // the result will never actually be used, just for type checking
  4041. const vao = glconf.vao[key === 'pellets' ? 0 : 1];
  4042.  
  4043. const map = (settings.mergeViewArea && sync.merge) ? sync.merge : world;
  4044.  
  4045. // find expected # of pellets (exclude any that are being *animated*)
  4046. let expected = 0;
  4047. if (key === 'pellets') {
  4048. if (sync.merge) {
  4049. for (const collection of sync.merge.pellets.values()) {
  4050. if (collection.merged?.deadTo === -1) ++expected;
  4051. }
  4052. } else {
  4053. for (const pellet of world.pellets.values()) {
  4054. if (pellet.deadTo === -1) ++expected;
  4055. }
  4056. }
  4057. } else {
  4058. expected = map.cells.size;
  4059. }
  4060.  
  4061. // grow the pellet buffer by 2x multiples if necessary
  4062. let alphaBuffer = key === 'cells' ? cellAlpha : pelletAlpha;
  4063. let objBuffer = key === 'cells' ? cellBuffer : pelletBuffer;
  4064. let instances = alphaBuffer.length || 1;
  4065. while (instances < expected) {
  4066. instances *= 2;
  4067. }
  4068. // when the webgl context is lost, the buffer sizes get reset to zero
  4069. const resizing = instances * 4 !== vao.alphaBufferSize;
  4070. if (resizing) {
  4071. if (key === 'pellets') {
  4072. alphaBuffer = pelletAlpha = new Float32Array(instances);
  4073. objBuffer = pelletBuffer = new Float32Array(instances * 7);
  4074. } else {
  4075. alphaBuffer = cellAlpha = new Float32Array(instances);
  4076. objBuffer = cellBuffer = new Float32Array(instances * 7);
  4077. }
  4078. }
  4079.  
  4080. const color = key === 'pellets' ? aux.sigmodSettings?.foodColor : aux.sigmodSettings?.cellColor;
  4081. const foodBlank = key === 'pellets' && color?.[0] === 0 && color?.[1] === 0 && color?.[2] === 0;
  4082.  
  4083. let i = 0;
  4084. /** @param {Cell} cell */
  4085. const iterate = cell => {
  4086. /** @type {number} */
  4087. let nx, ny, nr;
  4088. if (key !== 'cells') {
  4089. if (cell.deadTo !== -1) return;
  4090. nx = cell.nx; ny = cell.ny; nr = cell.nr;
  4091. } else {
  4092. let jr;
  4093. ({ x: nx, y: ny, r: nr, jr } = world.xyr(cell, undefined, now));
  4094. if (aux.settings.jellyPhysics) nr = jr;
  4095. }
  4096.  
  4097. objBuffer[i * 7] = nx;
  4098. objBuffer[i * 7 + 1] = ny;
  4099. objBuffer[i * 7 + 2] = nr;
  4100. if (color && !foodBlank) {
  4101. objBuffer[i * 7 + 3] = color[0]; objBuffer[i * 7 + 4] = color[1];
  4102. objBuffer[i * 7 + 5] = color[2]; objBuffer[i * 7 + 6] = color[3];
  4103. } else {
  4104. objBuffer[i * 7 + 3] = cell.Rgb; objBuffer[i * 7 + 4] = cell.rGb;
  4105. objBuffer[i * 7 + 5] = cell.rgB; objBuffer[i * 7 + 6] = foodBlank ? color[3] : 1;
  4106. }
  4107. ++i;
  4108. };
  4109. if (sync.merge) {
  4110. for (const collection of sync.merge[key].values()) {
  4111. if (collection.merged) {
  4112. iterate(collection.merged);
  4113. }
  4114. }
  4115. } else {
  4116. for (const cell of world[key].values()) {
  4117. iterate(cell);
  4118. }
  4119. }
  4120.  
  4121. // now, upload data
  4122. if (resizing) {
  4123. gl.bindBuffer(gl.ARRAY_BUFFER, vao.alphaBuffer);
  4124. gl.bufferData(gl.ARRAY_BUFFER, alphaBuffer.byteLength, gl.STATIC_DRAW);
  4125. gl.bindBuffer(gl.ARRAY_BUFFER, vao.circleBuffer);
  4126. gl.bufferData(gl.ARRAY_BUFFER, objBuffer, gl.STATIC_DRAW);
  4127. vao.alphaBufferSize = alphaBuffer.byteLength;
  4128. } else {
  4129. gl.bindBuffer(gl.ARRAY_BUFFER, vao.circleBuffer);
  4130. gl.bufferSubData(gl.ARRAY_BUFFER, 0, objBuffer);
  4131. }
  4132. gl.bindBuffer(gl.ARRAY_BUFFER, null);
  4133.  
  4134. if (key === 'pellets') uploadedPellets = expected;
  4135. };
  4136.  
  4137.  
  4138. // #3 : define ubo views
  4139. // firefox adds some padding to uniform buffer sizes, so best to check its size
  4140. gl.bindBuffer(gl.UNIFORM_BUFFER, glconf.uniforms.Border);
  4141. const borderUboBuffer = new ArrayBuffer(gl.getBufferParameter(gl.UNIFORM_BUFFER, gl.BUFFER_SIZE));
  4142. // must reference an arraybuffer for the memory to be shared between these views
  4143. const borderUboFloats = new Float32Array(borderUboBuffer);
  4144. const borderUboInts = new Int32Array(borderUboBuffer);
  4145.  
  4146. gl.bindBuffer(gl.UNIFORM_BUFFER, glconf.uniforms.Cell);
  4147. const cellUboBuffer = new ArrayBuffer(gl.getBufferParameter(gl.UNIFORM_BUFFER, gl.BUFFER_SIZE));
  4148. const cellUboFloats = new Float32Array(cellUboBuffer);
  4149. const cellUboInts = new Int32Array(cellUboBuffer);
  4150.  
  4151. gl.bindBuffer(gl.UNIFORM_BUFFER, glconf.uniforms.Text);
  4152. const textUboBuffer = new ArrayBuffer(gl.getBufferParameter(gl.UNIFORM_BUFFER, gl.BUFFER_SIZE));
  4153. const textUboFloats = new Float32Array(textUboBuffer);
  4154. const textUboInts = new Int32Array(textUboBuffer);
  4155.  
  4156. gl.bindBuffer(gl.UNIFORM_BUFFER, glconf.uniforms.Tracer);
  4157. const tracerUboBuffer = new ArrayBuffer(gl.getBufferParameter(gl.UNIFORM_BUFFER, gl.BUFFER_SIZE));
  4158. const tracerUboFloats = new Float32Array(tracerUboBuffer);
  4159.  
  4160. gl.bindBuffer(gl.UNIFORM_BUFFER, null); // leaving uniform buffer bound = scary!
  4161.  
  4162.  
  4163. // #4 : define the render function
  4164. render.fps = 0;
  4165. render.lastFrame = performance.now();
  4166. function renderGame() {
  4167. const now = performance.now();
  4168. const dt = Math.max(now - render.lastFrame, 0.1) / 1000; // there's a chance (now - lastFrame) can be 0
  4169. render.fps += (1 / dt - render.fps) / 10;
  4170. render.lastFrame = now;
  4171.  
  4172. if (gl.isContextLost()) {
  4173. requestAnimationFrame(renderGame);
  4174. return;
  4175. }
  4176.  
  4177. // get settings
  4178. const defaultVirusSrc = '/assets/images/viruses/2.png';
  4179. const virusSrc = aux.sigmodSettings?.virusImage ?? defaultVirusSrc;
  4180.  
  4181. const showNames = aux.sigmodSettings?.showNames ?? true;
  4182.  
  4183. const { cellColor, foodColor, outlineColor, skinReplacement } = aux.sigmodSettings ?? {};
  4184.  
  4185. /** @type {HTMLInputElement | null} */
  4186. const nickElement = document.querySelector('input#nick');
  4187. const nick = nickElement?.value ?? '?';
  4188.  
  4189. refreshTextCache();
  4190.  
  4191. // note: most routines are named, for benchmarking purposes
  4192. (function updateGame() {
  4193. world.moveCamera();
  4194. input.move();
  4195. sync.frame();
  4196. })();
  4197.  
  4198. (function setGlobalUniforms() {
  4199. // note that binding the same buffer to gl.UNIFORM_BUFFER twice in a row causes it to not update.
  4200. // why that happens is completely beyond me but oh well.
  4201. // for consistency, we always bind gl.UNIFORM_BUFFER to null directly after updating it.
  4202. gl.bindBuffer(gl.UNIFORM_BUFFER, glconf.uniforms.Camera);
  4203. gl.bufferSubData(gl.UNIFORM_BUFFER, 0, new Float32Array([
  4204. ui.game.canvas.width / ui.game.canvas.height, world.camera.scale / 540,
  4205. world.camera.x, world.camera.y,
  4206. ]));
  4207. gl.bindBuffer(gl.UNIFORM_BUFFER, null);
  4208.  
  4209. gl.bindBuffer(gl.UNIFORM_BUFFER, glconf.uniforms.CellSettings);
  4210. gl.bufferSubData(gl.UNIFORM_BUFFER, 0, new Float32Array([
  4211. ...settings.outlineMultiColor, // cell_active_outline
  4212. ...settings.outlineMultiInactiveColor, // cell_inactive_outline
  4213. ...(aux.settings.darkTheme ? [1, 1, 1, 1] : [0, 0, 0, 1]), // cell_unsplittable_outline
  4214. ...(outlineColor ?? [0, 0, 0, 0]), // cell_subtle_outline_override
  4215. settings.outlineMulti,
  4216. ]));
  4217. gl.bindBuffer(gl.UNIFORM_BUFFER, null);
  4218. })();
  4219.  
  4220. (function background() {
  4221. if (aux.sigmodSettings?.mapColor) {
  4222. gl.clearColor(...aux.sigmodSettings.mapColor);
  4223. } else if (aux.settings.darkTheme) {
  4224. gl.clearColor(0x11 / 255, 0x11 / 255, 0x11 / 255, 1); // #111
  4225. } else {
  4226. gl.clearColor(0xf2 / 255, 0xfb / 255, 0xff / 255, 1); // #f2fbff
  4227. }
  4228. gl.clear(gl.COLOR_BUFFER_BIT);
  4229.  
  4230. gl.useProgram(glconf.programs.bg);
  4231.  
  4232. let texture;
  4233. if (settings.background) {
  4234. texture = textureFromCache(settings.background);
  4235. } else if (aux.settings.showGrid) {
  4236. texture = textureFromCache(aux.settings.darkTheme ? darkGridSrc : lightGridSrc);
  4237. }
  4238. gl.bindTexture(gl.TEXTURE_2D, texture?.texture ?? null);
  4239. const repeating = texture && texture.width <= 1024 && texture.height <= 1024;
  4240.  
  4241. let borderColor;
  4242. let borderLrtb;
  4243. if (aux.settings.showBorder && world.border) {
  4244. borderColor = [0, 0, 1, 1]; // #00ff
  4245. borderLrtb = world.border;
  4246. } else {
  4247. borderColor = [0, 0, 0, 0]; // transparent
  4248. borderLrtb = { l: 0, r: 0, t: 0, b: 0 };
  4249. }
  4250.  
  4251. // u_border_color
  4252. borderUboFloats[0] = borderColor[0]; borderUboFloats[1] = borderColor[1];
  4253. borderUboFloats[2] = borderColor[2]; borderUboFloats[3] = borderColor[3];
  4254. // u_border_xyzw_lrtb
  4255. borderUboFloats[4] = borderLrtb.l;
  4256. borderUboFloats[5] = borderLrtb.r;
  4257. borderUboFloats[6] = borderLrtb.t;
  4258. borderUboFloats[7] = borderLrtb.b;
  4259.  
  4260. // flags
  4261. borderUboInts[8] = (texture ? 0x01 : 0) | (aux.settings.darkTheme ? 0x02 : 0) | (repeating ? 0x04 : 0);
  4262.  
  4263. // u_background_width and u_background_height
  4264. borderUboFloats[9] = texture?.width ?? 1;
  4265. borderUboFloats[10] = texture?.height ?? 1;
  4266.  
  4267. gl.bindBuffer(gl.UNIFORM_BUFFER, glconf.uniforms.Border);
  4268. gl.bufferSubData(gl.UNIFORM_BUFFER, 0, borderUboFloats);
  4269. gl.bindBuffer(gl.UNIFORM_BUFFER, null);
  4270. gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
  4271. })();
  4272.  
  4273. (function cells() {
  4274. // for white cell outlines
  4275. let nextCellIdx = world.mine.length;
  4276. const canSplit = world.mine.map(id => {
  4277. let cell;
  4278. if (sync.merge) {
  4279. cell = sync.merge.cells.get(id)?.merged;
  4280. } else {
  4281. cell = world.cells.get(id);
  4282. }
  4283. if (!cell) {
  4284. --nextCellIdx;
  4285. return false;
  4286. }
  4287.  
  4288. if (cell.nr < 128)
  4289. return false;
  4290.  
  4291. return nextCellIdx++ < 16;
  4292. });
  4293.  
  4294. /**
  4295. * @param {Cell} cell
  4296. * @returns {number}
  4297. */
  4298. const calcAlpha = cell => {
  4299. let alpha = (now - cell.born) / 100;
  4300. if (cell.deadAt !== undefined) {
  4301. const alpha2 = 1 - (now - cell.deadAt) / 100;
  4302. if (alpha2 < alpha) alpha = alpha2;
  4303. }
  4304. alpha = alpha > 1 ? 1 : alpha < 0 ? 0 : alpha;
  4305. return alpha;
  4306. };
  4307.  
  4308. /**
  4309. * @param {Cell} cell
  4310. */
  4311. function draw(cell) {
  4312. // #1 : draw cell
  4313. gl.useProgram(glconf.programs.cell);
  4314.  
  4315. const alpha = calcAlpha(cell);
  4316. cellUboFloats[8] = alpha * settings.cellOpacity;
  4317.  
  4318. /** @type {Cell | undefined} */
  4319. let killer;
  4320. if (cell.deadTo !== -1) {
  4321. if (sync.merge) {
  4322. killer = sync.merge.cells.get(cell.deadTo)?.merged;
  4323. } else {
  4324. killer = world.cells.get(cell.deadTo);
  4325. }
  4326. }
  4327. const { x, y, r, jr } = world.xyr(cell, killer, now);
  4328. // without jelly physics, the radius of cells is adjusted such that its subtle outline doesn't go
  4329. // past its original radius.
  4330. // jelly physics does not do this, so colliding cells need to look kinda 'joined' together,
  4331. // so we multiply the radius by 1.02 (approximately the size increase from the stroke thickness)
  4332. cellUboFloats[2] = x;
  4333. cellUboFloats[3] = y;
  4334. if (aux.settings.jellyPhysics && !cell.jagged && !cell.pellet) {
  4335. const strokeThickness = Math.max(jr * 0.01, 10);
  4336. cellUboFloats[0] = jr + strokeThickness;
  4337. cellUboFloats[1] = (settings.jellySkinLag ? r : jr) + strokeThickness;
  4338. } else {
  4339. cellUboFloats[0] = cellUboFloats[1] = r;
  4340. }
  4341.  
  4342. if (cell.jagged) {
  4343. const virusTexture = textureFromCache(virusSrc);
  4344. if (virusTexture) {
  4345. gl.bindTexture(gl.TEXTURE_2D, virusTexture.texture);
  4346. cellUboInts[9] = 0x01; // skin and nothing else
  4347. // draw a fully transparent cell
  4348. cellUboFloats[4] = cellUboFloats[5] = cellUboFloats[6] = cellUboFloats[7] = 0;
  4349. } else {
  4350. cellUboInts[9] = 0;
  4351. cellUboFloats[4] = 1;
  4352. cellUboFloats[5] = 0;
  4353. cellUboFloats[6] = 0;
  4354. cellUboFloats[7] = 0.5;
  4355. }
  4356.  
  4357. gl.bindBuffer(gl.UNIFORM_BUFFER, glconf.uniforms.Cell);
  4358. gl.bufferSubData(gl.UNIFORM_BUFFER, 0, cellUboBuffer);
  4359. gl.bindBuffer(gl.UNIFORM_BUFFER, null);
  4360. gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
  4361. // draw default viruses twice for better contrast against light theme
  4362. if (!aux.settings.darkTheme && virusSrc === defaultVirusSrc)
  4363. gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
  4364. return;
  4365. }
  4366.  
  4367. cellUboInts[9] = 0;
  4368. const color = (cell.pellet ? foodColor : cellColor) ?? [cell.Rgb, cell.rGb, cell.rgB, 1];
  4369. cellUboFloats[4] = color[0]; cellUboFloats[5] = color[1];
  4370. cellUboFloats[6] = color[2]; cellUboFloats[7] = color[3];
  4371.  
  4372. cellUboInts[9] |= settings.cellOutlines ? 0x02 : 0;
  4373.  
  4374. if (!cell.pellet) {
  4375. const myIndex = world.mine.indexOf(cell.id);
  4376. if (myIndex !== -1) {
  4377. if (world.camera.merged) cellUboInts[9] |= 0x04; // active multi outline
  4378. if (!canSplit[myIndex] && settings.unsplittableOpacity > 0) cellUboInts[9] |= 0x10;
  4379. }
  4380.  
  4381. let skin = '';
  4382. for (const data of sync.others.values()) {
  4383. if (data.owned.has(cell.id)) {
  4384. if (world.camera.merged) cellUboInts[9] |= 0x08; // inactive multi outline
  4385. if (settings.syncSkin) skin = data.skin;
  4386. break;
  4387. }
  4388. }
  4389.  
  4390. if (settings.selfSkin && (myIndex !== -1 || world.mineDead.has(cell.id))) {
  4391. skin = settings.selfSkin;
  4392. } else {
  4393. if (!skin && aux.settings.showSkins && cell.skin) {
  4394. if (skinReplacement && cell.skin.includes(skinReplacement.original + '.png'))
  4395. skin = skinReplacement.replacement ?? skinReplacement.replaceImg ?? '';
  4396. else
  4397. skin = cell.skin;
  4398. }
  4399. }
  4400.  
  4401. if (skin) {
  4402. const texture = textureFromCache(skin);
  4403. if (texture) {
  4404. cellUboInts[9] |= 0x01; // skin
  4405. gl.bindTexture(gl.TEXTURE_2D, texture.texture);
  4406. }
  4407. }
  4408. }
  4409.  
  4410. gl.bindBuffer(gl.UNIFORM_BUFFER, glconf.uniforms.Cell);
  4411. gl.bufferSubData(gl.UNIFORM_BUFFER, 0, cellUboBuffer);
  4412. gl.bindBuffer(gl.UNIFORM_BUFFER, null);
  4413. gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
  4414.  
  4415. // #2 : draw text
  4416. if (cell.pellet) return;
  4417. const name = cell.name || 'An unnamed cell';
  4418. const showThisName = showNames && cell.nr > 75;
  4419. const showThisMass = aux.settings.showMass && cell.nr > 75;
  4420. const clan = (settings.clans && aux.clans.get(cell.clan)) || '';
  4421. if (!showThisName && !showThisMass && !clan) return;
  4422.  
  4423. gl.useProgram(glconf.programs.text);
  4424. textUboFloats[8] = alpha; // text_alpha
  4425.  
  4426. let useSilhouette = false;
  4427. if (cell.sub) {
  4428. // text_color1 = #eb9500
  4429. textUboFloats[0] = 0xeb / 255; textUboFloats[1] = 0x95 / 255;
  4430. textUboFloats[2] = 0x00 / 255; textUboFloats[3] = 1;
  4431. // text_color2 = #e4b110
  4432. textUboFloats[4] = 0xe4 / 255; textUboFloats[5] = 0xb1 / 255;
  4433. textUboFloats[6] = 0x10 / 255; textUboFloats[7];
  4434. useSilhouette = true;
  4435. } else {
  4436. // text_color1 = text_color2 = #fff
  4437. textUboFloats[0] = textUboFloats[1] = textUboFloats[2] = textUboFloats[3] = 1;
  4438. textUboFloats[4] = textUboFloats[5] = textUboFloats[6] = textUboFloats[7] = 1;
  4439. }
  4440.  
  4441. if (name === nick) {
  4442. const nameColor1 = aux.sigmodSettings?.nameColor1;
  4443. const nameColor2 = aux.sigmodSettings?.nameColor2;
  4444. if (nameColor1) {
  4445. textUboFloats[0] = nameColor1[0]; textUboFloats[1] = nameColor1[1];
  4446. textUboFloats[2] = nameColor1[2]; textUboFloats[3] = nameColor1[3];
  4447. useSilhouette = true;
  4448. }
  4449.  
  4450. if (nameColor2) {
  4451. textUboFloats[4] = nameColor2[0]; textUboFloats[5] = nameColor2[1];
  4452. textUboFloats[6] = nameColor2[2]; textUboFloats[7] = nameColor2[3];
  4453. useSilhouette = true;
  4454. }
  4455. }
  4456.  
  4457. if (clan) {
  4458. const { aspectRatio, text, silhouette } = textFromCache(clan, useSilhouette);
  4459. if (text) {
  4460. textUboFloats[9] = aspectRatio; // text_aspect_ratio
  4461. textUboFloats[10]
  4462. = showThisName ? settings.clanScaleFactor * 0.5 : settings.nameScaleFactor;
  4463. textUboInts[11] = Number(useSilhouette); // text_silhouette_enabled
  4464. textUboFloats[12] = 0; // text_offset.x
  4465. textUboFloats[13] = showThisName
  4466. ? -settings.nameScaleFactor/3 - settings.clanScaleFactor/6 : 0; // text_offset.y
  4467.  
  4468. gl.bindTexture(gl.TEXTURE_2D, text);
  4469. if (silhouette) {
  4470. gl.activeTexture(gl.TEXTURE1);
  4471. gl.bindTexture(gl.TEXTURE_2D, silhouette);
  4472. gl.activeTexture(gl.TEXTURE0);
  4473. }
  4474.  
  4475. gl.bindBuffer(gl.UNIFORM_BUFFER, glconf.uniforms.Text);
  4476. gl.bufferSubData(gl.UNIFORM_BUFFER, 0, textUboBuffer);
  4477. gl.bindBuffer(gl.UNIFORM_BUFFER, null);
  4478. gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
  4479. }
  4480. }
  4481.  
  4482. if (showThisName) {
  4483. const { aspectRatio, text, silhouette } = textFromCache(name, useSilhouette);
  4484. if (text) {
  4485. textUboFloats[9] = aspectRatio; // text_aspect_ratio
  4486. textUboFloats[10] = settings.nameScaleFactor; // text_scale
  4487. textUboInts[11] = Number(silhouette); // text_silhouette_enabled
  4488. textUboFloats[12] = textUboFloats[13] = 0; // text_offset = (0, 0)
  4489.  
  4490. gl.bindTexture(gl.TEXTURE_2D, text);
  4491. if (silhouette) {
  4492. gl.activeTexture(gl.TEXTURE1);
  4493. gl.bindTexture(gl.TEXTURE_2D, silhouette);
  4494. gl.activeTexture(gl.TEXTURE0);
  4495. }
  4496.  
  4497. gl.bindBuffer(gl.UNIFORM_BUFFER, glconf.uniforms.Text);
  4498. gl.bufferSubData(gl.UNIFORM_BUFFER, 0, textUboBuffer);
  4499. gl.bindBuffer(gl.UNIFORM_BUFFER, null);
  4500. gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
  4501. }
  4502. }
  4503.  
  4504. if (showThisMass) {
  4505. textUboFloats[8] = alpha * settings.massOpacity; // text_alpha
  4506. textUboFloats[10] = 0.5 * settings.massScaleFactor; // text_scale
  4507. textUboInts[11] = 0; // text_silhouette_enabled
  4508.  
  4509. let yOffset;
  4510. if (showThisName)
  4511. yOffset = (settings.nameScaleFactor + 0.5 * settings.massScaleFactor) / 3;
  4512. else if (clan)
  4513. yOffset = (1 + 0.5 * settings.massScaleFactor) / 3;
  4514. else
  4515. yOffset = 0;
  4516. // draw each digit separately, as Ubuntu makes them all the same width.
  4517. // significantly reduces the size of the text cache
  4518. const mass = Math.floor(cell.nr * cell.nr / 100).toString();
  4519. for (let i = 0; i < mass.length; ++i) {
  4520. const { aspectRatio, texture } = massTextFromCache(mass[i]);
  4521. textUboFloats[9] = aspectRatio; // text_aspect_ratio
  4522. // text_offset.x
  4523. // thickness 0 => 1.00 multiplier
  4524. // thickness 1 => 0.75
  4525. // probably a reciprocal function
  4526. textUboFloats[12] = (i - (mass.length - 1) / 2)
  4527. * (1 - 0.25 * Math.sqrt(settings.textOutlinesFactor)) * settings.massScaleFactor;
  4528. textUboFloats[13] = yOffset;
  4529. gl.bindTexture(gl.TEXTURE_2D, texture);
  4530.  
  4531. gl.bindBuffer(gl.UNIFORM_BUFFER, glconf.uniforms.Text);
  4532. gl.bufferSubData(gl.UNIFORM_BUFFER, 0, textUboBuffer);
  4533. gl.bindBuffer(gl.UNIFORM_BUFFER, null);
  4534. gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
  4535. }
  4536. }
  4537. }
  4538.  
  4539. // draw static pellets first
  4540. let i = 0;
  4541. /** @param {Cell} pellet */
  4542. const iterateStaticPellet = pellet => {
  4543. // deadTo property should never change in between upload('pellets') calls
  4544. if (pellet.deadTo !== -1) return;
  4545. pelletAlpha[i++] = calcAlpha(pellet);
  4546. };
  4547. if (sync.merge) {
  4548. for (const collection of sync.merge.pellets.values()) {
  4549. if (collection.merged) iterateStaticPellet(collection.merged);
  4550. }
  4551. } else {
  4552. for (const pellet of world.pellets.values()) {
  4553. iterateStaticPellet(pellet);
  4554. }
  4555. }
  4556. gl.bindBuffer(gl.ARRAY_BUFFER, glconf.vao[0].alphaBuffer);
  4557. gl.bufferSubData(gl.ARRAY_BUFFER, 0, pelletAlpha);
  4558. gl.bindBuffer(gl.ARRAY_BUFFER, null); // TODO: necessary unbinding?
  4559.  
  4560. if (settings.pelletGlow && aux.settings.darkTheme) {
  4561. gl.blendFunc(gl.SRC_ALPHA, gl.ONE); // make sure pellets (and glow) are visible in light theme
  4562. }
  4563. gl.bindBuffer(gl.UNIFORM_BUFFER, glconf.uniforms.Circle);
  4564. gl.bufferData(gl.UNIFORM_BUFFER, new Float32Array([ 1, 0 ]), gl.STATIC_DRAW);
  4565. gl.bindBuffer(gl.UNIFORM_BUFFER, null);
  4566.  
  4567. gl.useProgram(glconf.programs.circle);
  4568. gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, uploadedPellets);
  4569. if (settings.pelletGlow) {
  4570. gl.bindBuffer(gl.UNIFORM_BUFFER, glconf.uniforms.Circle);
  4571. gl.bufferData(gl.UNIFORM_BUFFER, new Float32Array([ 0.25, 2 ]), gl.STATIC_DRAW);
  4572. gl.bindBuffer(gl.UNIFORM_BUFFER, null);
  4573.  
  4574. gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, uploadedPellets);
  4575. gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
  4576. }
  4577.  
  4578. // then draw *animated* pellets
  4579. /** @param {Cell} pellet */
  4580. const iterateAnimatedPellet = pellet => {
  4581. if (pellet.deadTo !== -1) draw(pellet); // no rtx glow is fine here
  4582. };
  4583. if (sync.merge) {
  4584. for (const collection of sync.merge.pellets.values()) {
  4585. if (collection.merged) iterateAnimatedPellet(collection.merged);
  4586. }
  4587. } else {
  4588. for (const pellet of world.pellets.values()) {
  4589. iterateAnimatedPellet(pellet);
  4590. }
  4591. }
  4592.  
  4593. /** @type {[Cell, number][]} */
  4594. const sorted = [];
  4595. /** @param {Cell} cell */
  4596. const iterateSortableCell = cell => {
  4597. const rAlpha = Math.min(Math.max((now - cell.updated) / settings.drawDelay, 0), 1);
  4598. const computedR = cell.or + (cell.nr - cell.or) * rAlpha;
  4599. sorted.push([cell, computedR]);
  4600. };
  4601. if (sync.merge) {
  4602. for (const collection of sync.merge.cells.values()) {
  4603. if (collection.merged) iterateSortableCell(collection.merged);
  4604. }
  4605. } else {
  4606. for (const cell of world.cells.values()) {
  4607. iterateSortableCell(cell);
  4608. }
  4609. }
  4610.  
  4611. // sort by smallest to biggest
  4612. sorted.sort(([_a, ar], [_b, br]) => ar - br);
  4613. for (const [cell] of sorted)
  4614. draw(cell);
  4615.  
  4616. if (settings.cellGlow) {
  4617. render.upload('cells', now);
  4618. let i = 0;
  4619. /** @param {Cell} cell */
  4620. const iterateCellGlow = cell => {
  4621. if (cell.jagged) cellAlpha[i++] = 0;
  4622. else {
  4623. let alpha = calcAlpha(cell);
  4624. // it looks kinda weird when cells get sucked in when being eaten
  4625. if (cell.deadTo !== -1) alpha *= 0.25;
  4626. cellAlpha[i++] = alpha;
  4627. }
  4628. };
  4629. if (sync.merge) {
  4630. for (const collection of sync.merge.cells.values()) {
  4631. if (collection.merged) iterateCellGlow(collection.merged);
  4632. }
  4633. } else {
  4634. for (const cell of world.cells.values()) {
  4635. iterateCellGlow(cell);
  4636. }
  4637. }
  4638.  
  4639. gl.bindBuffer(gl.ARRAY_BUFFER, glconf.vao[1].alphaBuffer);
  4640. gl.bufferSubData(gl.ARRAY_BUFFER, 0, cellAlpha);
  4641. gl.bindBuffer(gl.ARRAY_BUFFER, null);
  4642.  
  4643. gl.useProgram(glconf.programs.circle);
  4644. gl.bindVertexArray(glconf.vao[1].vao);
  4645. gl.blendFunc(gl.SRC_ALPHA, gl.ONE);
  4646.  
  4647. gl.bindBuffer(gl.UNIFORM_BUFFER, glconf.uniforms.Circle);
  4648. gl.bufferData(gl.UNIFORM_BUFFER, new Float32Array([ 0.33, 1.5 ]), gl.STATIC_DRAW);
  4649. gl.bindBuffer(gl.UNIFORM_BUFFER, null);
  4650.  
  4651. gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, i);
  4652. gl.bindVertexArray(glconf.vao[0].vao);
  4653. gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
  4654. }
  4655.  
  4656. // draw tracers
  4657. if (settings.tracer) {
  4658. gl.useProgram(glconf.programs.tracer);
  4659.  
  4660. const [x, y] = input.mouse();
  4661. tracerUboFloats[2] = x; // tracer_pos2.x
  4662. tracerUboFloats[3] = y; // tracer_pos2.y
  4663.  
  4664. world.mine.forEach(id => {
  4665. /** @type {Cell | undefined} */
  4666. let cell;
  4667. if (sync.merge) {
  4668. cell = sync.merge.cells.get(id)?.merged;
  4669. } else {
  4670. cell = world.cells.get(id);
  4671. }
  4672. if (!cell || cell.deadAt !== undefined) return;
  4673.  
  4674. let { x, y } = world.xyr(cell, undefined, now);
  4675. tracerUboFloats[0] = x; // tracer_pos1.x
  4676. tracerUboFloats[1] = y; // tracer_pos1.y
  4677. gl.bindBuffer(gl.UNIFORM_BUFFER, glconf.uniforms.Tracer);
  4678. gl.bufferSubData(gl.UNIFORM_BUFFER, 0, tracerUboBuffer);
  4679. gl.bindBuffer(gl.UNIFORM_BUFFER, null);
  4680. gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
  4681. });
  4682. }
  4683. })();
  4684.  
  4685. (function minimap() {
  4686. if (now - lastMinimapDraw < 40) return;
  4687. lastMinimapDraw = now;
  4688.  
  4689. if (!aux.settings.showMinimap) {
  4690. ui.minimap.canvas.style.display = 'none';
  4691. return;
  4692. } else {
  4693. ui.minimap.canvas.style.display = '';
  4694. }
  4695.  
  4696. const { canvas, ctx } = ui.minimap;
  4697. // clears the canvas
  4698. const canvasLength = canvas.width = canvas.height = Math.ceil(200 * devicePixelRatio);
  4699. const sectorSize = canvas.width / 5;
  4700.  
  4701. // cache the background if necessary (25 texts = bad)
  4702. if (minimapCache && minimapCache.bg.width === canvasLength
  4703. && minimapCache.darkTheme === aux.settings.darkTheme) {
  4704. ctx.putImageData(minimapCache.bg, 0, 0);
  4705. } else {
  4706. // draw section names
  4707. ctx.font = `${Math.floor(sectorSize / 3)}px Ubuntu`;
  4708. ctx.fillStyle = '#fff';
  4709. ctx.globalAlpha = aux.settings.darkTheme ? 0.3 : 0.7;
  4710. ctx.textAlign = 'center';
  4711. ctx.textBaseline = 'middle';
  4712.  
  4713. const cols = ['1', '2', '3', '4', '5'];
  4714. const rows = ['A', 'B', 'C', 'D', 'E'];
  4715. cols.forEach((col, y) => {
  4716. rows.forEach((row, x) => {
  4717. ctx.fillText(row + col, (x + 0.5) * sectorSize, (y + 0.5) * sectorSize);
  4718. });
  4719. });
  4720.  
  4721. minimapCache = {
  4722. bg: ctx.getImageData(0, 0, canvas.width, canvas.height),
  4723. darkTheme: aux.settings.darkTheme,
  4724. };
  4725. }
  4726.  
  4727. const { border } = world;
  4728. if (!border) return;
  4729.  
  4730. // sigmod overlay resizes itself differently, so we correct it whenever we need to
  4731. /** @type {HTMLCanvasElement | null} */
  4732. const sigmodMinimap = document.querySelector('canvas.minimap');
  4733. if (sigmodMinimap) {
  4734. // we need to check before updating the canvas, otherwise we will clear it
  4735. if (sigmodMinimap.style.width !== '200px' || sigmodMinimap.style.height !== '200px')
  4736. sigmodMinimap.style.width = sigmodMinimap.style.height = '200px';
  4737.  
  4738. if (sigmodMinimap.width !== canvas.width || sigmodMinimap.height !== canvas.height)
  4739. sigmodMinimap.width = sigmodMinimap.height = canvas.width;
  4740. }
  4741.  
  4742. const gameWidth = (border.r - border.l);
  4743. const gameHeight = (border.b - border.t);
  4744.  
  4745. // highlight current section
  4746. ctx.fillStyle = '#ff0';
  4747. ctx.globalAlpha = 0.3;
  4748.  
  4749. const sectionX = Math.floor((world.camera.x - border.l) / gameWidth * 5);
  4750. const sectionY = Math.floor((world.camera.y - border.t) / gameHeight * 5);
  4751. ctx.fillRect(sectionX * sectorSize, sectionY * sectorSize, sectorSize, sectorSize);
  4752.  
  4753. // draw section names
  4754. ctx.font = `${Math.floor(sectorSize / 3)}px Ubuntu`;
  4755. ctx.fillStyle = aux.settings.darkTheme ? '#fff' : '#000';
  4756. ctx.textAlign = 'center';
  4757. ctx.textBaseline = 'middle';
  4758.  
  4759. ctx.globalAlpha = 1;
  4760.  
  4761. // draw cells
  4762. /** @param {{ nx: number, ny: number, nr: number, Rgb: number, rGb: number, rgB: number }} cell */
  4763. const drawCell = function drawCell(cell) {
  4764. const x = (cell.nx - border.l) / gameWidth * canvas.width;
  4765. const y = (cell.ny - border.t) / gameHeight * canvas.height;
  4766. const r = Math.max(cell.nr / gameWidth * canvas.width, 2);
  4767.  
  4768. ctx.scale(0.01, 0.01); // prevent sigmod from treating minimap cells as pellets
  4769. ctx.fillStyle = aux.rgba2hex(cell.Rgb, cell.rGb, cell.rgB, 1);
  4770. ctx.beginPath();
  4771. ctx.moveTo((x + r) * 100, y * 100);
  4772. ctx.arc(x * 100, y * 100, r * 100, 0, 2 * Math.PI);
  4773. ctx.fill();
  4774. ctx.resetTransform();
  4775. };
  4776.  
  4777. /**
  4778. * @param {number} x
  4779. * @param {number} y
  4780. * @param {string} name
  4781. */
  4782. const drawName = function drawName(x, y, name) {
  4783. x = (x - border.l) / gameWidth * canvas.width;
  4784. y = (y - border.t) / gameHeight * canvas.height;
  4785.  
  4786. ctx.fillStyle = '#fff';
  4787. // add a space to prevent sigmod from detecting names
  4788. ctx.fillText(name + ' ', x, y - 7 * devicePixelRatio - sectorSize / 6);
  4789. };
  4790.  
  4791. // draw clanmates first, below yourself
  4792. // we sort clanmates by color AND name, to ensure clanmates stay separate
  4793. /** @type {Map<string, { name: string, n: number, x: number, y: number }>} */
  4794. const avgPos = new Map();
  4795. world.clanmates.forEach(cell => {
  4796. if (world.mine.includes(cell.id)) return;
  4797. drawCell(cell);
  4798.  
  4799. const name = cell.name || 'An unnamed cell';
  4800. const id = ((name + cell.Rgb) + cell.rGb) + cell.rgB;
  4801. const entry = avgPos.get(id);
  4802. if (entry) {
  4803. ++entry.n;
  4804. entry.x += cell.nx;
  4805. entry.y += cell.ny;
  4806. } else {
  4807. avgPos.set(id, { name, n: 1, x: cell.nx, y: cell.ny });
  4808. }
  4809. });
  4810.  
  4811. avgPos.forEach(entry => {
  4812. drawName(entry.x / entry.n, entry.y / entry.n, entry.name);
  4813. });
  4814.  
  4815. // draw my cells above everyone else
  4816. let myName = '';
  4817. let ownN = 0;
  4818. let ownX = 0;
  4819. let ownY = 0;
  4820. world.mine.forEach(id => {
  4821. const cell = world.cells.get(id);
  4822. if (!cell) return;
  4823.  
  4824. drawCell(cell);
  4825. myName = cell.name || 'An unnamed cell';
  4826. ++ownN;
  4827. ownX += cell.nx;
  4828. ownY += cell.ny;
  4829. });
  4830.  
  4831. if (ownN <= 0) {
  4832. // if no cells were drawn, draw our spectate pos instead
  4833. drawCell({
  4834. nx: world.camera.x, ny: world.camera.y, nr: gameWidth / canvas.width * 5,
  4835. Rgb: 1, rGb: 0.6, rgB: 0.6,
  4836. });
  4837. } else {
  4838. ownX /= ownN;
  4839. ownY /= ownN;
  4840. // draw name above player's cells
  4841. drawName(ownX, ownY, myName);
  4842.  
  4843. // send a hint to sigmod
  4844. ctx.globalAlpha = 0;
  4845. ctx.fillText(`X: ${ownX}, Y: ${ownY}`, 0, -1000);
  4846. }
  4847. })();
  4848.  
  4849. ui.chat.matchTheme();
  4850.  
  4851. requestAnimationFrame(renderGame);
  4852. }
  4853.  
  4854. renderGame();
  4855. return render;
  4856. })();
  4857.  
  4858.  
  4859.  
  4860. // @ts-expect-error for debugging purposes and other scripts. dm me on discord @ 8y8x to guarantee stability
  4861. window.sigfix = {
  4862. destructor, aux, ui, settings, sync, world, net, input, glconf, render,
  4863. };
  4864. })();