From 0e4ecbb36f5909d5c8978e271904fd200ae483e8 Mon Sep 17 00:00:00 2001 From: peshomir <80340328+peshomir@users.noreply.github.com> Date: Mon, 20 May 2024 17:09:29 +0300 Subject: [PATCH] Update v0.6.4 - Clan leaderboard and hovering tooltip - Added a leaderboard filter with tabs "All" and "Clan" - Added the "hovering tooltip" (constantly displaying the territory information normally visible when right clicking on the map) and an option to toggle it in the settings - The absolute keybinds' value is now displayed as a percentage --- build.js | 122 ++++++++++++++++++++++++++++++++++++++++----- src/fx_core.js | 133 ++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 236 insertions(+), 19 deletions(-) diff --git a/build.js b/build.js index b5908c1..313a397 100644 --- a/build.js +++ b/build.js @@ -78,7 +78,8 @@ const generateRegularExpression = (/** @type {string} */ code, /** @type {boolea // this one also broke in 1.91.3 /,\w+="Player: "\+(?\w+)\[\w+\],\w+=\(\w\+=" Balance: "\+\w+\.\w+\((?\w+)\[\w+\]\)\)\+\(" Territory: "\+\w+\.\w+\((?\w+)\[\w+\]\)\)\+\(" Coords: "/g, ///\((?\w+)=Math\.floor\(\(\w+\?\.0114:\.01296\)\*\w+\)\)/g, /(function \w+\((\w+),(\w+),(\w+),(\w+),(\w+)\){\6\.fillText\((?\w+)\[\2\],\4,\5\)),(\2<(?\w+)&&2!==(?\w+)\[)/g, - /,\w+=512,(?\w+)=\w+,(?\w+)&&\(\1=\w+\.\w+\(\)\),\w+=\1-\w+,\w+=0,/g + /,\w+=512,(?\w+)=\w+,(?\w+)&&\(\1=\w+\.\w+\(\)\),\w+=\1-\w+,\w+=0,/g, + /function \w+\(\)\{if\(2===(?\w+)\)return 1;\w+\.\w+\(\),\1=2,\w+=\w+\}/g ].forEach(matchDictionaryExpression); const rawCodeSegments = [ @@ -219,26 +220,35 @@ replaceOne(/(this\.\w+=function\((\w+),(\w+)\)\{)(\2===\w+&&\(\w+\.\w+\((\w+\.\w "$1 donationsTracker.logDonation($2, $3, $5[0]); $4") // Display donations for a player when clicking on them in the leaderboard +// and skip handling clicks when clicking on an empty space (see the isEmptySpace +// variable in the modified leaderboard click handler from the leaderboard filter) // match , 0 !== dG[x]) && fq.hB(x, 800, false, 0), -replaceOne(/,(0!==\w+\[(\w+)\]\)&&\w+\.\w+\(\2,800,!1,0\),)/g, - `, ${dictionary.gIsTeamGame} && donationsTracker.displayHistory($2, ${dictionary.playerNames}, ${dictionary.gIsSingleplayer}), $1`); +replaceOne(/,(0!==\w+\[(\w+)\])(\)&&\w+\.\w+\(\2,800,!1,0\),)/g, + `, ${dictionary.gIsTeamGame} && donationsTracker.displayHistory($2, ${dictionary.playerNames}, ${dictionary.gIsSingleplayer}), $1 && !isEmptySpace $3`); -// Reset donation history when a new game is started -replaceOne(new RegExp(`,${dictionary.playerBalances}=new Uint32Array\\(\\w+\\),`, "g"), "$& donationsTracker.reset(), "); +// Reset donation history and leaderboard filter when a new game is started +replaceOne(new RegExp(`,${dictionary.playerBalances}=new Uint32Array\\(\\w+\\),`, "g"), "$& donationsTracker.reset(), leaderboardFilter.reset(), "); -{ // Player list +{ // Player list and leaderboard filter tabs // Draw player list button const uiOffset = dictionary.uiSizes + "." + dictionary.gap; const { groups: { drawFunction, topBarHeight } } = replaceOne(/(=1;function (?\w+)\(\){[^}]+?(?\w+)\.fillRect\(0,(?\w+),\w+,1\),(?:\3\.fillRect\([^()]+\),)+\3\.font=\w+,(\w+\.\w+)\.textBaseline\(\3,1\),\5\.textAlign\(\3,1\),\3\.fillText\(\w+\.\w+\[65\],Math\.floor\()(\w+)\/2\),(Math\.floor\(\w+\+\w+\/2\)\));/g, "$1($6 + $ - 22) / 2), $7; playerList.drawButton($, 12, 12, $ - 22);"); const buttonBoundsCheck = `utils.isPointInRectangle($, $, ${uiOffset} + 12, ${uiOffset} + 12, ${topBarHeight} - 22, ${topBarHeight} - 22)` - // Handle player list button mouseDown - replaceOne(/(this\.\w+=function\((?\w+),(?\w+)\){return!!\w+\(\2,\3\))&&(\(\w+=\w+\.\w+,)/g, - `$1 && (${buttonBoundsCheck} && playerList.display(${dictionary.playerNames}), true) && $4`); - // Handle player list button hover + // Handle player list button and leaderboard tabs mouseDown + // and create a function for scrolling the leaderboard to the top + replaceOne(/(this\.\w+=function\((?\w+),(?\w+)\){return!!\w+\(\2,\3\))&&(\(\w+=\w+\.\w+,[^}]+),!0\)/g, + `leaderboardFilter.scrollToTop = function(){position = 0;}, $1 && ((${buttonBoundsCheck} && playerList.display(${dictionary.playerNames}), true) + && !($ - ${uiOffset} > leaderboardFilter.verticalClickThreshold && leaderboardFilter.handleMouseDown($ - ${uiOffset})) && $4),!0)`); + // Handle player list button and leaderboard tabs hover + // and create a function for repainting the leaderboard replaceOne(/(this\.\w+=function\((?\w+),(?\w+)\){)(var \w+,\w+=\w+\(\3\);return \w+\?\(\w+=(\w+),\(\5=\w+\(0,\5\+=(?:[^}]+,(?\w+\.\w+=!0)){2})/g, - `$1 if (${buttonBoundsCheck}) { playerList.hoveringOverButton === false && (playerList.hoveringOverButton = true, ${drawFunction}(), $); } ` - + ` else { playerList.hoveringOverButton === true && (playerList.hoveringOverButton = false, ${drawFunction}(), $); } $4`); + `leaderboardFilter.repaintLeaderboard = function() { ${drawFunction}(), $; }, + $1 if (${buttonBoundsCheck}) { playerList.hoveringOverButton === false && (playerList.hoveringOverButton = true, ${drawFunction}(), $); } + else { playerList.hoveringOverButton === true && (playerList.hoveringOverButton = false, ${drawFunction}(), $); } + if (leaderboardFilter.setHovering( + utils.isPointInRectangle($, $, ${uiOffset}, ${uiOffset} + leaderboardFilter.verticalClickThreshold, leaderboardFilter.windowWidth, leaderboardFilter.tabBarOffset), $ - ${uiOffset} + )) return; $4`); } { // Display density of other players @@ -250,6 +260,94 @@ replaceOne(new RegExp(`,${dictionary.playerBalances}=new Uint32Array\\(\\w+\\),` `$1 var ___id = $2; $7, $9; ${settingsSwitchNameAndBalance} && settings.showPlayerDensity && (settings.coloredDensity && ($.fillStyle = utils.textStyleBasedOnDensity(___id)), $.fillText(utils.getDensity(___id), $, $ + $)); }`); } +{ // Leaderboard filter + // for the leaderboard draw function: + replaceRawCode("a0A.clearRect(0,0,a04,y9),a0A.fillStyle=aZ.lE,a0A.fillRect(0,0,a04,a0F),a0A.fillStyle=aZ.kZ,a0A.fillRect(0,a0F,a04,y9-a0F),leaderboardPositionsById[playerId]>=position&&a0Z(leaderboardPositionsById[playerId]-position,aZ.kw),0!==leaderboardPositionsById[playerId]&&0===position&&a0Z(0,aZ.lJ),-1!==a0P&&a0Z(a0P,aZ.kd),a0A.fillStyle=aZ.gF,a0A.fillRect(0,a0F,a04,1),a0A.fillRect(0,0,a04,b0.ur),a0A.fillRect(0,0,b0.ur,y9),a0A.fillRect(a04-b0.ur,0,b0.ur,y9),a0A.fillRect(0,y9-b0.ur,a04,b0.ur),", + `a0A.clearRect(0, 0, a04, y9), + a0A.fillStyle = aZ.lE, + a0A.fillRect(0, 0, a04, a0F), + a0A.fillStyle = aZ.kZ, + a0A.fillRect(0, a0F, a04, y9 - a0F); + if (leaderboardFilter.enabled) { + leaderboardFilter.filteredLeaderboard = leaderboardFilter.playersToInclude + .map(id => leaderboardPositionsById[id]).sort((a, b) => a - b); + } + var playerPos = (leaderboardFilter.enabled + ? leaderboardFilter.filteredLeaderboard.indexOf(leaderboardPositionsById[playerId]) + : leaderboardPositionsById[playerId] + ); + this.playerPos = playerPos; + if (leaderboardFilter.hoveringOverTabs) a0P = -1; + if (leaderboardFilter.enabled && a0P >= leaderboardFilter.filteredLeaderboard.length) a0P = -1; + playerPos >= position && a0Z(playerPos - position, aZ.kw), + 0 !== leaderboardPositionsById[playerId] && 0 === position && a0Z(0, aZ.lJ), + -1 !== a0P && a0Z(a0P, aZ.kd), + a0A.fillStyle = aZ.kZ, + //console.log("drawing", a0P), + a0A.clearRect(0, y9 - leaderboardFilter.tabBarOffset, a04, leaderboardFilter.tabBarOffset); + a0A.fillRect(0, y9 - leaderboardFilter.tabBarOffset, a04, leaderboardFilter.tabBarOffset); + a0A.fillStyle = aZ.gF, + a0A.fillRect(0, a0F, a04, 1), + a0A.fillRect(0, y9 - leaderboardFilter.tabBarOffset, a04, 1), + leaderboardFilter.drawTabs(a0A, a04, y9 - leaderboardFilter.tabBarOffset, aZ.kw), + a0A.fillRect(0, 0, a04, b0.ur), + a0A.fillRect(0, 0, b0.ur, y9), + a0A.fillRect(a04 - b0.ur, 0, b0.ur, y9), + a0A.fillRect(0, y9 - b0.ur, a04, b0.ur),`) + replaceRawCode("var hZ,eh=leaderboardPositionsById[playerId]= result.length - windowHeight) + position = (result.length > windowHeight ? result.length : windowHeight) - windowHeight; + //if (position >= result.length) position = result.length - 1; + for (a0A.font = a07, aY.g0.textAlign(a0A, 0), hZ = windowHeight - eh; 0 <= hZ; hZ--) { + const pos = result[hZ + position]; + if (pos !== undefined) + a0a(leaderboardArray[pos]), a0b(hZ, pos, leaderboardArray[pos]); + } + for (aY.g0.textAlign(a0A, 2), hZ = windowHeight - eh; 0 <= hZ; hZ--) { + const pos = result[hZ + position]; + if (pos !== undefined) + a0a(leaderboardArray[pos]), a0c(hZ, leaderboardArray[pos]); + } + } else { + for (a0A.font = a07, aY.g0.textAlign(a0A, 0), hZ = windowHeight - eh; 0 <= hZ; hZ--) + a0a(leaderboardArray[hZ + position]), a0b(hZ, hZ + position, leaderboardArray[hZ + position]); + for (aY.g0.textAlign(a0A, 2), hZ = windowHeight - eh; 0 <= hZ; hZ--) + a0a(leaderboardArray[hZ + position]), a0c(hZ, leaderboardArray[hZ + position]); + }`) + // in the leaderboard resize handler: make space for the tab buttons at the bottom of the leaderboard + replaceRawCode(",a0D=.025*a04,a06=.16*a04,a0E=0*a04,a0F=Math.floor(.45*a0D+a06),a0G=(y9-a06-2*a0D-a0E)/a08,a05=aY.g0.g1(1,Math.floor(.55*a06)),", + `,a0D=.025*a04,a06=.16*a04,a0E=0*a04,a0F=Math.floor(.45*a0D+a06),a0G=(y9-a06-2*a0D-a0E)/a08, + a09.height = y9 += a0G, leaderboardFilter.tabBarOffset = Math.floor(a0G * 1.3), leaderboardFilter.verticalClickThreshold = y9 - leaderboardFilter.tabBarOffset, leaderboardFilter.windowWidth = a04, + a05=aY.g0.g1(1,Math.floor(.55*a06)),`) + // handle clicking on a player in the leaderboard + replaceRawCode("var a0p=a0q(fJ);return ag.tQ()&&-1!==a0P&&(a0P=-1,a0Y(),b3.d1=!0),b3.dY-a0Q<350&&a0T===a0p&&-1!==(a0p=(a0p=yr(-1,a0p,windowHeight))!==windowHeight&&vU(x,y)?a0p:-1)&&(x=leaderboardArray[a0p+position],a0p===windowHeight-1&&leaderboardPositionsById[playerId]>=position+windowHeight-1&&(x=playerId),", + `var a0p = a0q(fJ); + var isEmptySpace = false; + return ag.tQ() && -1 !== a0P && (a0P = -1, a0Y(), b3.d1 = !0), b3.dY - a0Q < 350 && a0T === a0p && -1 !== (a0p = (a0p = yr(-1, a0p, windowHeight)) !== windowHeight && vU(x, y) ? a0p : -1) && (x = (leaderboardFilter.enabled ? leaderboardArray[leaderboardFilter.filteredLeaderboard[a0p + position] ?? (isEmptySpace = true, leaderboardPositionsById[playerId])] : leaderboardArray[a0p + position]), a0p === windowHeight - 1 && (leaderboardFilter.enabled ? this.playerPos : leaderboardPositionsById[playerId]) >= + position + windowHeight - 1 && (x = playerId), !isEmptySpace && `); +} + +{ // Hovering tooltip + replaceRawCode("this.click=function(g8,g9,tE){var fT=aj.fU(g8),fV=aj.fW(g9),fX=aj.fY(fT,fV),fZ=aj.fa(fX);return!(!aj.fb(fT,fV)||(fT=(b7.cv.fv()?.025:.0144)*aK.fw,fV=performance.now(),Math.abs(g8-uu)>fT)||Math.abs(g9-uv)>fT||dY+500 fT) || Math.abs(g9 - uv) > fT || dY + 500 < fV) && (dY = fV, tE ? (function(g8, g9, fZ) { + a2.eb(fZ) || -1 === (g8 = ak.ff.vR(g8, g9)) ? k.vQ(fZ) : k.vS(g8) + }(g8, g9, fZ), false)`) +} + // Disable built-in Territorial.io error reporting replaceOne(/window\.addEventListener\("error",function (\w+)\((\w+)\){/g, '$& window.removeEventListener("error", $1); return alert("Error:\\n" + $2.filename + " " + $2.lineno + " " + $2.colno + " " + $2.message);'); diff --git a/src/fx_core.js b/src/fx_core.js index 0a22d2c..7a80e8a 100644 --- a/src/fx_core.js +++ b/src/fx_core.js @@ -1,5 +1,5 @@ -const fx_version = '0.6.3.3'; // FX Client Version -const fx_update = 'Apr 11'; // FX Client Last Updated +const fx_version = '0.6.4'; // FX Client Version +const fx_update = 'May 20'; // FX Client Last Updated if (localStorage.getItem("fx_winCount") == undefined || localStorage.getItem("fx_winCount") == null) { var wins_counter = 0; @@ -27,8 +27,9 @@ function KeybindsInput(containerElement) { this.keys = [ "key", "type", "value" ]; this.objectArray = []; this.addObject = function () { - this.objectArray.push({ key: "", type: "absolute", value: 1 }); + this.objectArray.push({ key: "", type: "absolute", value: 0.8 }); this.displayObjects(); + keybindAddButton.scrollIntoView(false); }; this.update = function () { this.objectArray = settings.attackPercentageKeybinds; @@ -54,11 +55,15 @@ function KeybindsInput(containerElement) { inputField.setAttribute("placeholder", "No key set"); inputField.addEventListener("click", this.startKeyInput.bind(this, i, key)); } else { // key === "value" - inputField.type = "number"; - inputField.setAttribute("step", "0.1"); + const isAbsolute = this.objectArray[i].type === "absolute"; + inputField.type = isAbsolute ? "text" : "number"; + if (isAbsolute) inputField.addEventListener("click", this.convertIntoNumberInput.bind(this, i, key), { once: true }); + else inputField.setAttribute("step", "0.1"); inputField.addEventListener("input", this.updateObject.bind(this, i, key)); } - inputField.value = this.objectArray[i][key]; + if (key === "value" && this.objectArray[i].type === "absolute") + inputField.value = this.objectArray[i][key] * 100 + "%"; + else inputField.value = this.objectArray[i][key]; // Append input field to the object div objectDiv.appendChild(inputField); }, this); @@ -83,10 +88,21 @@ function KeybindsInput(containerElement) { //this.displayObjects(); }, { once: true }); }; + /** @param {PointerEvent} event */ + this.convertIntoNumberInput = function (index, property, event) { + event.target.value = event.target.value.slice(0, -1); + event.target.type = "number"; + event.target.addEventListener("blur", () => { + //event.target.value = this.objectArray[index][property]; + this.displayObjects(); + }, { once: true }); + }; this.updateObject = function (index, property, event) { if (index >= this.objectArray.length) return; // Update the corresponding property of the object in the array - const value = property === "value" ? parseFloat(event.target.value) : property === "key" ? event.key : event.target.value; + const value = property === "value" ? ( + this.objectArray[index].type === "absolute" ? parseFloat(event.target.value) / 100 : parseFloat(event.target.value) + ) : property === "key" ? event.key : event.target.value; this.objectArray[index][property] = value; if (property === "key") this.displayObjects(); }; @@ -104,6 +120,7 @@ var settings = { //"showBotDonations": false, "displayWinCounter": true, "useFullscreenMode": false, + "hoveringTooltip": true, //"hideAllLinks": false, "realisticNames": false, "showPlayerDensity": true, @@ -123,6 +140,8 @@ var settingsManager = new (function() { note: "The win counter tracks multiplayer solo wins (not in team games)" }, { for: "useFullscreenMode", type: "checkbox", label: "Use fullscreen mode", note: "Note: fullscreen mode will trigger after you click anywhere on the page due to browser policy restrictions." }, + { for: "hoveringTooltip", type: "checkbox", label: "Hovering tooltip", + note: "Display map territory info constantly (on mouse hover) instead of only when right clicking on the map" }, //{ for: "hideAllLinks", type: "checkbox", label: "Hide Links option also hides app store links" }, { for: "realisticNames", type: "checkbox", label: "Realistic Bot Names" }, { for: "showPlayerDensity", type: "checkbox", label: "Show player density" }, @@ -322,6 +341,106 @@ const playerList = new (function () { canvas.imageSmoothingEnabled = false; } }); + +/** @param {string} name */ +function parseClanFromPlayerName(name) { + const startIndex = name.indexOf("["); + // this is probably how the algorithm works, since a player with + // the name "][a]" will count as not being in a clan in the base game + return startIndex === -1 ? "" : name.slice(startIndex + 1, name.indexOf("]")).toUpperCase(); +} + +const leaderboardFilter = new (function() { + this.playersToInclude = [0,1,8,20,24,30,32,42,50,69,200,400,500,510,511]; // for testing + //this.playersToInclude = []; + this.tabLabels = ["ALL", "CLAN"]; + // these get populated by the modified game code + this.filteredLeaderboard = []; + this.tabBarOffset = 0; + this.windowWidth = 0; + this.verticalClickThreshold = 1000; + this.hoveringOverTabs = false; + this.scrollToTop = () => {}; + this.repaintLeaderboard = () => {}; + + this.selectedTab = 0; + this.tabHovering = -1; + //this.enabled = false; + this.enabled = true; + this.drawTabs = function(canvas, totalWidth, verticalOffset, colorForSelectedTab) { + canvas.textBaseline = "middle"; + canvas.textAlign = "center"; + const tabWidth = totalWidth / this.tabLabels.length; + const textOffsetY = verticalOffset + this.tabBarOffset / 2; + //console.log(verticalOffset, this.tabBarOffset, textOffsetY); + this.tabLabels.forEach((label, index) => { + if (index !== 0) canvas.fillRect(tabWidth * index, verticalOffset, 1, this.tabBarOffset); + if (this.selectedTab === index) { + canvas.fillStyle = colorForSelectedTab; + canvas.fillRect(tabWidth * index, verticalOffset, tabWidth, this.tabBarOffset); + canvas.fillStyle = "rgb(255,255,255)"; + } + if (this.tabHovering === index) { + canvas.fillStyle = "rgba(255,255,255,0.3)"; + canvas.fillRect(tabWidth * index, verticalOffset, tabWidth, this.tabBarOffset); + canvas.fillStyle = "rgb(255,255,255)"; + } + canvas.fillText(label, tabWidth * index + tabWidth / 2, textOffsetY); + }); + } + this.setHovering = (isHovering, xRelative) => { + let repaintNeeded = false; + if (isHovering) { + const tab = Math.floor(xRelative / (this.windowWidth / this.tabLabels.length)); + if (this.tabHovering !== tab) { + this.tabHovering = tab; + repaintNeeded = true; + } + } + if (isHovering !== this.hoveringOverTabs) { + this.hoveringOverTabs = isHovering; + if (isHovering === false) this.tabHovering = -1; + if (!isHovering) repaintNeeded = true; + } + if (repaintNeeded) this.repaintLeaderboard(); + return isHovering; + } + this.handleMouseDown = (xRelative) => { + //console.log("click; x: ", xRelative); + if (this.tabHovering !== this.selectedTab) { + this.selectedTab = this.tabHovering; + if (this.selectedTab === 0) this.clearFilter(); + else if (this.selectedTab === 1) this.filterByOwnClan(); + this.repaintLeaderboard(); + } + return true; + }; + this.filterByOwnClan = () => { + this.playersToInclude = []; + const ownClan = parseClanFromPlayerName(getVar("playerNames")[getVar("playerId")]); + getVar("playerNames").forEach((name, id) => { + if (parseClanFromPlayerName(name) === ownClan) this.playersToInclude.push(id); + }); + this.enabled = true; + this.scrollToTop(); + }; + this.clearFilter = () => { this.enabled = false; } + this.reset = () => { + this.enabled = false; + this.selectedTab = 0; + } +}); + +const hoveringTooltip = new (function() { + this.display = () => {}; // this gets populated by the modified game script + document.getElementById("canvasA").addEventListener("mousemove", e => { + if (!settings.hoveringTooltip || !getVar("gameState")) return; + try { + this.display(e.clientX, e.clientY); + } catch (e) { console.error(e) } + }); +}); + var donationsTracker = new (function(){ this.openedWindowPlayerID = null; this.contentElement = document.querySelector("#donationhistory_content");