parent
							
								
									495a979eb0
								
							
						
					
					
						commit
						b881298231
					
				
								
									
									
										
											35
										
									
									build.js
									
									
									
									
								
								
							
							
										
											35
										
									
									build.js
									
									
									
									
								| 
						 | 
				
			
			@ -2,12 +2,38 @@ import beautifier from 'js-beautify';
 | 
			
		|||
const { js: beautify } = beautifier;
 | 
			
		||||
import UglifyJS from 'uglify-js';
 | 
			
		||||
import fs from 'fs';
 | 
			
		||||
import webpack from 'webpack';
 | 
			
		||||
import path from 'path';
 | 
			
		||||
import applyPatches from './patches.js';
 | 
			
		||||
 | 
			
		||||
if (!fs.existsSync("./build")) fs.mkdirSync("./build");
 | 
			
		||||
fs.cpSync("./static/", "./build/", { recursive: true });
 | 
			
		||||
fs.cpSync("./assets/", "./build/assets/", { recursive: true });
 | 
			
		||||
fs.cpSync("./src/fx_core.js", "./build/fx_core.js");
 | 
			
		||||
fs.writeFileSync("./build/index.html", fs.readFileSync("./build/index.html").toString().replace(/buildTimestamp/g, Date.now()));
 | 
			
		||||
 | 
			
		||||
const buildClientCode = () => new Promise((resolve, reject) => {
 | 
			
		||||
	webpack({
 | 
			
		||||
		mode: 'production',
 | 
			
		||||
		entry: { fxClient: "./src/main.js" },
 | 
			
		||||
		output: {
 | 
			
		||||
		  path: path.resolve(import.meta.dirname, 'build'),
 | 
			
		||||
		  filename: 'fx.bundle.js',
 | 
			
		||||
		},
 | 
			
		||||
	}, (err, stats) => {
 | 
			
		||||
		if (err) {
 | 
			
		||||
			if (err.details) console.error(err.details);
 | 
			
		||||
			return reject(err);
 | 
			
		||||
		}
 | 
			
		||||
		const info = stats.toJson();
 | 
			
		||||
    	if (stats.hasWarnings()) console.warn(info.warnings);
 | 
			
		||||
    	if (stats.hasErrors()) {
 | 
			
		||||
			console.error(info.errors);
 | 
			
		||||
			reject("Webpack compilation error");
 | 
			
		||||
		}
 | 
			
		||||
		else resolve();
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
let script = fs.readFileSync('./game/latest.js', { encoding: 'utf8' }).replace("\n", "").trim();
 | 
			
		||||
 | 
			
		||||
const exposeVarsToGlobalScope = true;
 | 
			
		||||
| 
						 | 
				
			
			@ -127,11 +153,12 @@ rawCodeSegments.forEach(code => {
 | 
			
		|||
	matchDictionaryExpression(expression);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
fs.writeFileSync("./build/fx_core.js", `const dictionary = ${JSON.stringify(dictionary)};\n` + fs.readFileSync("./build/fx_core.js").toString());
 | 
			
		||||
 | 
			
		||||
import applyPatches from './patches.js';
 | 
			
		||||
applyPatches({ replace, replaceOne, replaceRawCode, dictionary, matchOne, matchRawCode, escapeRegExp });
 | 
			
		||||
 | 
			
		||||
await buildClientCode();
 | 
			
		||||
// the dictionary should maybe get embedded into one of the files in the bundle
 | 
			
		||||
fs.writeFileSync("./build/fx.bundle.js", `const dictionary = ${JSON.stringify(dictionary)};\n` + fs.readFileSync("./build/fx.bundle.js").toString());
 | 
			
		||||
 | 
			
		||||
console.log("Formatting code...");
 | 
			
		||||
 | 
			
		||||
script = beautify(script, {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
											
												
													File diff suppressed because it is too large
													Load Diff
												
											
										
									
								| 
						 | 
				
			
			@ -21,5 +21,8 @@
 | 
			
		|||
  "dependencies": {
 | 
			
		||||
    "js-beautify": "^1.14.11",
 | 
			
		||||
    "uglify-js": "^3.19.2"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "webpack": "^5.95.0"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
								
									
									
										
											96
										
									
									patches.js
									
									
									
									
								
								
							
							
										
											96
										
									
									patches.js
									
									
									
									
								| 
						 | 
				
			
			@ -15,7 +15,7 @@ export default ({ replace, replaceOne, replaceRawCode, dictionary, matchOne, mat
 | 
			
		|||
    // Add FX Client version info to the game version window
 | 
			
		||||
    replaceRawCode(`ar.oa(4,1,new s8(__L(),gameVersion+"<br><a href='"+ah.aC5+"' target='_blank'>"+ah.aC5+"</a>",`,
 | 
			
		||||
        `ar.oa(4,1,new s8(__L(),gameVersion+"<br><a href='"+ah.aC5+"' target='_blank'>"+ah.aC5+"</a>"
 | 
			
		||||
+ "<br><br><b>" + "FX Client v" + fx_version + " " + fx_update + "<br><a href='https://discord.gg/dyxcwdNKwK' target='_blank'>FX Client Discord server</a>"
 | 
			
		||||
+ "<br><br><b>" + "FX Client v" + __fx.version + "<br><a href='https://discord.gg/dyxcwdNKwK' target='_blank'>FX Client Discord server</a>"
 | 
			
		||||
+ "<br><a href='https://github.com/fxclient/FXclient' target='_blank'>Github repository</a></b>",`);
 | 
			
		||||
 | 
			
		||||
    // Add update information
 | 
			
		||||
| 
						 | 
				
			
			@ -35,8 +35,8 @@ export default ({ replace, replaceOne, replaceRawCode, dictionary, matchOne, mat
 | 
			
		|||
        replaceOne(new RegExp(/(:(?<valueIndex>\w+)<7\?\w+\.\w+\.\w+\(valuesArray\[\2\]\)):(\w+\.\w+\(valuesArray\[7\]\))}/
 | 
			
		||||
            .source.replace(/valuesArray/g, valuesArray), "g"),
 | 
			
		||||
            '$1 : $<valueIndex> === 7 ? $3 '
 | 
			
		||||
            + `: $<valueIndex> === 8 ? utils.getMaxTroops(${dict.playerData}.${dict.playerTerritories}, ${playerId}) `
 | 
			
		||||
            + `: utils.getDensity(${playerId}) }`);
 | 
			
		||||
            + `: $<valueIndex> === 8 ? __fx.utils.getMaxTroops(${dict.playerData}.${dict.playerTerritories}, ${playerId}) `
 | 
			
		||||
            + `: __fx.utils.getDensity(${playerId}) }`);
 | 
			
		||||
        // increase the size of the side panel by 25% to make the text easier to read
 | 
			
		||||
        replaceOne(/(this\.\w+=Math\.floor\(\(\w+\.\w+\.\w+\(\)\?\.1646:\.126\))\*(\w+\.\w+\),)/g, "$1 * 1.25 * $2");
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -45,8 +45,8 @@ export default ({ replace, replaceOne, replaceRawCode, dictionary, matchOne, mat
 | 
			
		|||
    replaceRawCode(`=function(sE){o.ha(sE,2),b.h9<100?xD(0,__L([a8.jx[sE]]),3,sE,ad.gN,ad.kl,-1,!0):xD(0,__L([a8.jx[sE]]),3,sE,ad.gN,ad.kl,-1,!0)`,
 | 
			
		||||
        `=function(sE){
 | 
			
		||||
		if (${playerId} === sE && !${gIsSingleplayer})
 | 
			
		||||
			wins_counter++, window.localStorage.setItem("fx_winCount", wins_counter),
 | 
			
		||||
			xD(0,"Your Win Count is now " + wins_counter,3,sE,ad.gN,ad.kl,-1,!0);
 | 
			
		||||
			__fx.wins.count++, window.localStorage.setItem("fx_winCount", __fx.wins.count),
 | 
			
		||||
			xD(0,"Your Win Count is now " + __fx.wins.count,3,sE,ad.gN,ad.kl,-1,!0);
 | 
			
		||||
		o.ha(sE,2),b.h9<100?xD(0,__L([a8.jx[sE]]),3,sE,ad.gN,ad.kl,-1,!0):xD(0,__L([a8.jx[sE]]),3,sE,ad.gN,ad.kl,-1,!0)`);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -54,18 +54,18 @@ export default ({ replace, replaceOne, replaceRawCode, dictionary, matchOne, mat
 | 
			
		|||
        // add settings button
 | 
			
		||||
        replaceRawCode(`,new nQ("☰<br>"+__L(),function(){aD6(3)},aa.ks),new nQ("",function(){at.d5(12)},aa.kg,!1)]`,
 | 
			
		||||
            `,new nQ("☰<br>"+__L(),function(){aD6(3)},aa.ks),new nQ("",function(){at.d5(12)},aa.kg,!1),
 | 
			
		||||
            new nQ("FX Client settings", function() { WindowManager.openWindow("settings"); }, "rgba(0, 0, 20, 0.5")]`)
 | 
			
		||||
            new nQ("FX Client settings", function() { __fx.WindowManager.openWindow("settings"); }, "rgba(0, 0, 20, 0.5")]`)
 | 
			
		||||
        // set settings button position
 | 
			
		||||
        replaceRawCode(`aZ.g5.vO(aD3[3].button,x+a0S+gap,a3X+h+gap,a0S,h);`,
 | 
			
		||||
            `aZ.g5.vO(aD3[3].button,x+a0S+gap,a3X+h+gap,a0S,h); aZ.g5.vO(aD3[5].button, x, a3X + h * 2 + gap * 2, a0S * 2 + gap, h / 3);`);
 | 
			
		||||
        // render win count
 | 
			
		||||
        replaceRawCode(`if(_y.a4l(),_r.gI(),_m.gI(),aw.gI(),a0.g8()){ctx.imageSmoothingEnabled=!1;var iQ=a0.a4o("territorial.io"),kL=.84*aD4.gA/iQ.width;`,
 | 
			
		||||
            `if(_y.a4l(),_r.gI(),_m.gI(),aw.gI(),a0.g8()){
 | 
			
		||||
            if (settings.displayWinCounter) {
 | 
			
		||||
            if (__fx.settings.displayWinCounter) {
 | 
			
		||||
                const size = Math.floor(aD4.gA * 0.03);
 | 
			
		||||
                ctx.font = ${dict.fontGeneratorFunction}(1, size);
 | 
			
		||||
                ctx.fillStyle = "#ffffff";
 | 
			
		||||
                const text = "Win count: " + wins_counter;
 | 
			
		||||
                const text = "Win count: " + __fx.wins.count;
 | 
			
		||||
                const textLength = ctx.measureText(text).width;
 | 
			
		||||
                ctx.textAlign = "left";
 | 
			
		||||
                ctx.textBaseline = "middle";
 | 
			
		||||
| 
						 | 
				
			
			@ -99,11 +99,11 @@ canvas.font=aY.g0.g1(1,fontSize),canvas.fillStyle="rgba("+gR+","+tD+","+hj+",0.6
 | 
			
		|||
        // and also register the keybind handler functions
 | 
			
		||||
        replaceOne(/}(function \w+\((\w+)\){return!\(1<\2&&1===(?<attackPercentage>\w+)\|\|\(1<\2&&\2\*\3-\3<1\/1024\?\2=\(\3\+1\/1024\)\/\3:\2<1)/g,
 | 
			
		||||
            "} this.setAbsolutePercentage = function(newPercentage) { $<attackPercentage> = newPercentage; }; "
 | 
			
		||||
            + "keybindFunctions.setAbsolute = this.setAbsolutePercentage; "
 | 
			
		||||
            + `keybindFunctions.setRelative = (arg1) => ${attackBarObject}.${setRelative}(arg1); $1`);
 | 
			
		||||
            + "__fx.keybindFunctions.setAbsolute = this.setAbsolutePercentage; "
 | 
			
		||||
            + `__fx.keybindFunctions.setRelative = (arg1) => ${attackBarObject}.${setRelative}(arg1); $1`);
 | 
			
		||||
        // insert keybind handling code into the keyDown handler function
 | 
			
		||||
        replaceOne(new RegExp(/(function \w+\((?<event>\w+)\){)([^}]+matched)/g.source.replace(/matched/g, escapeRegExp(match)), "g"),
 | 
			
		||||
            "$1 if (keybindHandler($<event>.key)) return; $3");
 | 
			
		||||
            "$1 if (__fx.keybindHandler($<event>.key)) return; $3");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Set the default font to Trebuchet MS
 | 
			
		||||
| 
						 | 
				
			
			@ -111,53 +111,53 @@ canvas.font=aY.g0.g1(1,fontSize),canvas.fillStyle="rgba("+gR+","+tD+","+hj+",0.6
 | 
			
		|||
 | 
			
		||||
    // Realistic bot names setting
 | 
			
		||||
    // matches c4[i] = c4[i].replace(a6U[dx], a6V[dx])
 | 
			
		||||
    replaceOne(/(((\w+)\[\w+\])=\2\.replace\(\w+(\[\w+\]),\w+\4\))/g, "$1; if (settings.realisticNames) $3 = realisticNames;")
 | 
			
		||||
    replaceOne(/(((\w+)\[\w+\])=\2\.replace\(\w+(\[\w+\]),\w+\4\))/g, "$1; if (__fx.settings.realisticNames) $3 = realisticNames;")
 | 
			
		||||
 | 
			
		||||
    // Hide all links in main menu depending on settings
 | 
			
		||||
    //replaceOne(/(this\.\w+=function\(\){)((\w+\.\w+)\[2\]=\3\[3\]=\3\[4\]=(?<linksHidden>!this\.\w+\.\w+),)/g,
 | 
			
		||||
    //"$1 if (settings.hideAllLinks) $3[0] = $3[1] = $<linksHidden>; else $3[0] = $3[1] = true; $2")
 | 
			
		||||
 | 
			
		||||
    // Make the main canvas context have an alpha channel if a custom background is being used
 | 
			
		||||
    replaceOne(/(document\.getElementById\("canvasA"\),\(\w+=\w+\.getContext\("2d",){alpha:!1}/g, "$1 {alpha: makeMainMenuTransparent}")
 | 
			
		||||
    replaceOne(/(document\.getElementById\("canvasA"\),\(\w+=\w+\.getContext\("2d",){alpha:!1}/g, "$1 {alpha: __fx.makeMainMenuTransparent}")
 | 
			
		||||
    // Clear canvas background if a custom background is being used
 | 
			
		||||
    replaceRawCode(`,this.qk=function(){var a4n,a4m;aq.pd?(a4m=aL.gA/aq.eE,a4n=aL.gF/aq.eF,canvas.setTransform(a4m=a4n<a4m?a4m:a4n,0,0,a4m,`,
 | 
			
		||||
        `,this.qk=function(){var a4n,a4m;
 | 
			
		||||
        if (makeMainMenuTransparent) canvas.clearRect(0,0,aL.gA,aL.gF);
 | 
			
		||||
        if (__fx.makeMainMenuTransparent) canvas.clearRect(0,0,aL.gA,aL.gF);
 | 
			
		||||
        else aq.pd?(a4m=aL.gA/aq.eE,a4n=aL.gF/aq.eF,canvas.setTransform(a4m=a4n<a4m?a4m:a4n,0,0,a4m,`);
 | 
			
		||||
 | 
			
		||||
    // Track donations
 | 
			
		||||
    replaceOne(/(this\.\w+=function\((\w+),(\w+)\)\{)(\2===\w+\.\w+&&\(\w+\.\w+\((\w+\.\w+)\[0\],\5\[1\],\3\),this\.(\w+)\[12\]\+=\5\[1\],this\.\6\[16\]\+=\5\[0\]\),\3===\w+\.\w+&&\()/g,
 | 
			
		||||
        "$1 donationsTracker.logDonation($2, $3, $5[0]); $4")
 | 
			
		||||
        "$1 __fx.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+\.\w+\(\2,800,!1,0\),)/g,
 | 
			
		||||
        `, ${dict.game}.${dict.gIsTeamGame} && donationsTracker.displayHistory($2, ${rawPlayerNames}, ${gIsSingleplayer}), $1 && !isEmptySpace $3`);
 | 
			
		||||
        `, ${dict.game}.${dict.gIsTeamGame} && __fx.donationsTracker.displayHistory($2, ${rawPlayerNames}, ${gIsSingleplayer}), $1 && !isEmptySpace $3`);
 | 
			
		||||
 | 
			
		||||
    // Reset donation history and leaderboard filter when a new game is started
 | 
			
		||||
    replaceOne(new RegExp(`,this\\.${dictionary.playerBalances}.fill\\(0\\),`, "g"), "$& donationsTracker.reset(), leaderboardFilter.reset(), ");
 | 
			
		||||
    replaceOne(new RegExp(`,this\\.${dictionary.playerBalances}.fill\\(0\\),`, "g"), "$& __fx.donationsTracker.reset(), __fx.leaderboardFilter.reset(), ");
 | 
			
		||||
 | 
			
		||||
    { // Player list and leaderboard filter tabs
 | 
			
		||||
        // Draw player list button
 | 
			
		||||
        const uiOffset = dictionary.uiSizes + "." + dictionary.gap;
 | 
			
		||||
        const { groups: { drawFunction, topBarHeight } } = replaceOne(/(=1;function (?<drawFunction>\w+)\(\){[^}]+?(?<canvas>\w+)\.fillRect\(0,(?<topBarHeight>\w+),\w+,1\),(?:\3\.fillRect\([^()]+\),)+\3\.font=\w+,(\w+\.\w+)\.textBaseline\(\3,1\),\5\.textAlign\(\3,1\),\3\.fillText\(\w+\(\d+\),Math\.floor\()(\w+)\/2\),(Math\.floor\(\w+\+\w+\/2\)\));/g,
 | 
			
		||||
            "$1($6 + $<topBarHeight> - 22) / 2), $7; playerList.drawButton($<canvas>, 12, 12, $<topBarHeight> - 22);");
 | 
			
		||||
        const buttonBoundsCheck = `utils.isPointInRectangle($<x>, $<y>, ${uiOffset} + 12, ${uiOffset} + 12, ${topBarHeight} - 22, ${topBarHeight} - 22)`
 | 
			
		||||
            "$1($6 + $<topBarHeight> - 22) / 2), $7; __fx.playerList.drawButton($<canvas>, 12, 12, $<topBarHeight> - 22);");
 | 
			
		||||
        const buttonBoundsCheck = `__fx.utils.isPointInRectangle($<x>, $<y>, ${uiOffset} + 12, ${uiOffset} + 12, ${topBarHeight} - 22, ${topBarHeight} - 22)`
 | 
			
		||||
        // Handle player list button and leaderboard tabs mouseDown
 | 
			
		||||
        // and create a function for scrolling the leaderboard to the top
 | 
			
		||||
        replaceOne(/(this\.\w+=function\((?<x>\w+),(?<y>\w+)\){return!!\w+\(\2,\3\))&&(\(\w+=\w+\.\w+,[^}]+),!0\)/g,
 | 
			
		||||
            `leaderboardFilter.scrollToTop = function(){position = 0;}, $1 && ((${buttonBoundsCheck} && playerList.display(${rawPlayerNames}), true)
 | 
			
		||||
		&& !($<y> - ${uiOffset} > leaderboardFilter.verticalClickThreshold && leaderboardFilter.handleMouseDown($<x> - ${uiOffset})) && $4),!0)`);
 | 
			
		||||
            `__fx.leaderboardFilter.scrollToTop = function(){position = 0;}, $1 && ((${buttonBoundsCheck} && __fx.playerList.display(${rawPlayerNames}), true)
 | 
			
		||||
		&& !($<y> - ${uiOffset} > __fx.leaderboardFilter.verticalClickThreshold && __fx.leaderboardFilter.handleMouseDown($<x> - ${uiOffset})) && $4),!0)`);
 | 
			
		||||
        // Handle player list button and leaderboard tabs hover
 | 
			
		||||
        // and create a function for repainting the leaderboard
 | 
			
		||||
        replaceOne(/(this\.\w+=function\((?<x>\w+),(?<y>\w+)\){)(var \w+,\w+=\w+\(\3\);return \w+\?\(\w+=(\w+),\(\5=\w+\(0,\5\+=(?:[^}]+,(?<setRepaintNeeded>\w+\.\w+=!0)){2})/g,
 | 
			
		||||
            `leaderboardFilter.repaintLeaderboard = function() { ${drawFunction}(), $<setRepaintNeeded>; },
 | 
			
		||||
	$1 if (${buttonBoundsCheck}) { playerList.hoveringOverButton === false && (playerList.hoveringOverButton = true, ${drawFunction}(), $<setRepaintNeeded>); }
 | 
			
		||||
	else { playerList.hoveringOverButton === true && (playerList.hoveringOverButton = false, ${drawFunction}(), $<setRepaintNeeded>); }
 | 
			
		||||
	if (leaderboardFilter.setHovering(
 | 
			
		||||
		utils.isPointInRectangle($<x>, $<y>, ${uiOffset}, ${uiOffset} + leaderboardFilter.verticalClickThreshold, leaderboardFilter.windowWidth, leaderboardFilter.tabBarOffset), $<x> - ${uiOffset}
 | 
			
		||||
            `__fx.leaderboardFilter.repaintLeaderboard = function() { ${drawFunction}(), $<setRepaintNeeded>; },
 | 
			
		||||
	$1 if (${buttonBoundsCheck}) { __fx.playerList.hoveringOverButton === false && (__fx.playerList.hoveringOverButton = true, ${drawFunction}(), $<setRepaintNeeded>); }
 | 
			
		||||
	else { __fx.playerList.hoveringOverButton === true && (__fx.playerList.hoveringOverButton = false, ${drawFunction}(), $<setRepaintNeeded>); }
 | 
			
		||||
	if (__fx.leaderboardFilter.setHovering(
 | 
			
		||||
		__fx.utils.isPointInRectangle($<x>, $<y>, ${uiOffset}, ${uiOffset} + __fx.leaderboardFilter.verticalClickThreshold, __fx.leaderboardFilter.windowWidth, __fx.leaderboardFilter.tabBarOffset), $<x> - ${uiOffset}
 | 
			
		||||
	)) return; $4`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -170,10 +170,10 @@ canvas.font=aY.g0.g1(1,fontSize),canvas.fillStyle="rgba("+gR+","+tD+","+hj+",0.6
 | 
			
		|||
            `function a9V(ctx,i,fontSize,x,y,a9S){
 | 
			
		||||
		var ___id = i;
 | 
			
		||||
		i=ac.jv.formatNumber(playerData.playerBalances[i]);a9S>>1&1?(ctx.lineWidth=.05*fontSize,ctx.strokeStyle=a9U(fontSize,a9S%2),ctx.strokeText(i,x,y)):(1<a9S&&(ctx.lineWidth=.12*fontSize,ctx.strokeStyle=a9U(fontSize,a9S),ctx.strokeText(i,x,y)),ctx.fillText(i,x,y));
 | 
			
		||||
		${settingsSwitchNameAndBalance} || settings.showPlayerDensity && (settings.coloredDensity && (ctx.fillStyle = utils.textStyleBasedOnDensity(___id)), ctx.fillText(utils.getDensity(___id), x, y + fontSize))}`)
 | 
			
		||||
		${settingsSwitchNameAndBalance} || __fx.settings.showPlayerDensity && (__fx.settings.coloredDensity && (ctx.fillStyle = __fx.utils.textStyleBasedOnDensity(___id)), ctx.fillText(__fx.utils.getDensity(___id), x, y + fontSize))}`)
 | 
			
		||||
        // Applies when the "Reverse Name/Balance" setting is on (default)
 | 
			
		||||
        replaceOne(/(function \w+\((\w+),(?<fontSize>\w+),(?<x>\w+),(?<y>\w+),(?<canvas>\w+)\){)(\6\.fillText\((?<playerData>\w+)\.(?<playerNames>\w+)\[\2\],\4,\5\)),(\2<(?<game>\w+)\.(?<gHumans>\w+)&&2!==\8\.(?<playerStates>\w+)\[[^}]+)}/g,
 | 
			
		||||
            `$1 var ___id = $2; $7, $10; ${settingsSwitchNameAndBalance} && settings.showPlayerDensity && (settings.coloredDensity && ($<canvas>.fillStyle = utils.textStyleBasedOnDensity(___id)), $<canvas>.fillText(utils.getDensity(___id), $<x>, $<y> + $<fontSize>)); }`);
 | 
			
		||||
            `$1 var ___id = $2; $7, $10; ${settingsSwitchNameAndBalance} && __fx.settings.showPlayerDensity && (__fx.settings.coloredDensity && ($<canvas>.fillStyle = __fx.utils.textStyleBasedOnDensity(___id)), $<canvas>.fillText(__fx.utils.getDensity(___id), $<x>, $<y> + $<fontSize>)); }`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    { // Leaderboard filter
 | 
			
		||||
| 
						 | 
				
			
			@ -182,13 +182,13 @@ canvas.font=aY.g0.g1(1,fontSize),canvas.fillStyle="rgba("+gR+","+tD+","+hj+",0.6
 | 
			
		|||
 | 
			
		||||
            `var leaderboardHasChanged = true;
 | 
			
		||||
		this.playerPos = game.playerId;
 | 
			
		||||
		leaderboardFilter.setUpdateFlag = () => leaderboardHasChanged = true;
 | 
			
		||||
		__fx.leaderboardFilter.setUpdateFlag = () => leaderboardHasChanged = true;
 | 
			
		||||
		function updateFilteredLb() {
 | 
			
		||||
			if (!leaderboardHasChanged) return;
 | 
			
		||||
			leaderboardFilter.filteredLeaderboard = leaderboardFilter.playersToInclude
 | 
			
		||||
			__fx.leaderboardFilter.filteredLeaderboard = __fx.leaderboardFilter.playersToInclude
 | 
			
		||||
				.map(id => leaderboardPositionsById[id]).sort((a, b) => a - b);
 | 
			
		||||
			leaderboardHasChanged = false;
 | 
			
		||||
			this.playerPos = leaderboardFilter.filteredLeaderboard.indexOf(leaderboardPositionsById[game.playerId]);
 | 
			
		||||
			this.playerPos = __fx.leaderboardFilter.filteredLeaderboard.indexOf(leaderboardPositionsById[game.playerId]);
 | 
			
		||||
		}
 | 
			
		||||
		function drawFunction() {
 | 
			
		||||
		a0A.clearRect(0, 0, a04, y9),
 | 
			
		||||
| 
						 | 
				
			
			@ -196,24 +196,24 @@ canvas.font=aY.g0.g1(1,fontSize),canvas.fillStyle="rgba("+gR+","+tD+","+hj+",0.6
 | 
			
		|||
		a0A.fillRect(0, 0, a04, a0F),
 | 
			
		||||
		a0A.fillStyle = aZ.kZ,
 | 
			
		||||
		a0A.fillRect(0, a0F, a04, y9 - a0F);
 | 
			
		||||
		if (leaderboardFilter.enabled) updateFilteredLb();
 | 
			
		||||
		var playerPos = (leaderboardFilter.enabled
 | 
			
		||||
		if (__fx.leaderboardFilter.enabled) updateFilteredLb();
 | 
			
		||||
		var playerPos = (__fx.leaderboardFilter.enabled
 | 
			
		||||
			? this.playerPos
 | 
			
		||||
			: leaderboardPositionsById[game.playerId]
 | 
			
		||||
		);
 | 
			
		||||
		if (leaderboardFilter.hoveringOverTabs) a0P = -1;
 | 
			
		||||
		if (leaderboardFilter.enabled && a0P >= leaderboardFilter.filteredLeaderboard.length) a0P = -1;
 | 
			
		||||
		if (__fx.leaderboardFilter.hoveringOverTabs) a0P = -1;
 | 
			
		||||
		if (__fx.leaderboardFilter.enabled && a0P >= __fx.leaderboardFilter.filteredLeaderboard.length) a0P = -1;
 | 
			
		||||
		playerPos >= position && a0Z(playerPos - position, aZ.kw),
 | 
			
		||||
		0 !== leaderboardPositionsById[game.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.clearRect(0, y9 - __fx.leaderboardFilter.tabBarOffset, a04, __fx.leaderboardFilter.tabBarOffset);
 | 
			
		||||
		a0A.fillRect(0, y9 - __fx.leaderboardFilter.tabBarOffset, a04, __fx.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, y9 - __fx.leaderboardFilter.tabBarOffset, a04, 1),
 | 
			
		||||
		__fx.leaderboardFilter.drawTabs(a0A, a04, y9 - __fx.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),
 | 
			
		||||
| 
						 | 
				
			
			@ -221,8 +221,8 @@ canvas.font=aY.g0.g1(1,fontSize),canvas.fillStyle="rgba("+gR+","+tD+","+hj+",0.6
 | 
			
		|||
        replaceRawCode("var hZ,eh=leaderboardPositionsById[game.playerId]<position+windowHeight-1?1:2;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]);",
 | 
			
		||||
            `var hZ, eh = playerPos < position + windowHeight - 1 ? 1 : 2;
 | 
			
		||||
		
 | 
			
		||||
		if (leaderboardFilter.enabled) {
 | 
			
		||||
			let result = leaderboardFilter.filteredLeaderboard;
 | 
			
		||||
		if (__fx.leaderboardFilter.enabled) {
 | 
			
		||||
			let result = __fx.leaderboardFilter.filteredLeaderboard;
 | 
			
		||||
			if (position !== 0 && position >= result.length - windowHeight)
 | 
			
		||||
				position = (result.length > windowHeight ? result.length : windowHeight) - windowHeight;
 | 
			
		||||
			//if (position >= result.length) position = result.length - 1;
 | 
			
		||||
| 
						 | 
				
			
			@ -245,7 +245,7 @@ canvas.font=aY.g0.g1(1,fontSize),canvas.fillStyle="rgba("+gR+","+tD+","+hj+",0.6
 | 
			
		|||
        // in the leaderboard resize handler: make space for the tab buttons at the bottom of the leaderboard
 | 
			
		||||
        replaceRawCode(",a09.height=y9,a09_ctx=a09.getContext(\"2d\",{alpha:!0}),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)),",
 | 
			
		||||
            `,a09.height=y9,a09_ctx=a09.getContext("2d",{alpha:!0}),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,
 | 
			
		||||
		a09.height = y9 += a0G, __fx.leaderboardFilter.tabBarOffset = Math.floor(a0G * 1.3), __fx.leaderboardFilter.verticalClickThreshold = y9 - __fx.leaderboardFilter.tabBarOffset, __fx.leaderboardFilter.windowWidth = a04,
 | 
			
		||||
		a05=aY.g0.g1(1,Math.floor(.55*a06)),`)
 | 
			
		||||
        // Set the leaderboardHasChanged flag on leaderboard updates
 | 
			
		||||
        replaceRawCode("for(var eM=a0q-1;0<=eM;eM--)a14[eM]=jR[eM],a15[eM]=a8.f8[jR[eM]];a14[a0q]=a0l[b.ed],a15[a0q]=a8.f8[b.ed]",
 | 
			
		||||
| 
						 | 
				
			
			@ -254,16 +254,16 @@ canvas.font=aY.g0.g1(1,fontSize),canvas.fillStyle="rgba("+gR+","+tD+","+hj+",0.6
 | 
			
		|||
        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[game.playerId]>=position+windowHeight-1&&(x=game.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 ? (updateFilteredLb(), leaderboardArray[leaderboardFilter.filteredLeaderboard[a0p + position] ?? (isEmptySpace = true, leaderboardPositionsById[game.playerId])]) : leaderboardArray[a0p + position]), a0p === windowHeight - 1 && (leaderboardFilter.enabled ? this.playerPos : leaderboardPositionsById[game.playerId]) >=
 | 
			
		||||
		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 = (__fx.leaderboardFilter.enabled ? (updateFilteredLb(), leaderboardArray[__fx.leaderboardFilter.filteredLeaderboard[a0p + position] ?? (isEmptySpace = true, leaderboardPositionsById[game.playerId])]) : leaderboardArray[a0p + position]), a0p === windowHeight - 1 && (__fx.leaderboardFilter.enabled ? this.playerPos : leaderboardPositionsById[game.playerId]) >=
 | 
			
		||||
			position + windowHeight - 1 && (x = game.playerId), !isEmptySpace && `);
 | 
			
		||||
        // Get clan parsing function
 | 
			
		||||
        replaceRawCode(`this.uI=function(username){var uK,uJ=username.indexOf("[");return!(uJ<0)&&1<(uK=username.indexOf("]"))-uJ&&uK-uJ<=8?username.substring(uJ+1,uK).toUpperCase().trim():null},`,
 | 
			
		||||
            `this.uI=function(username){var uK,uJ=username.indexOf("[");return!(uJ<0)&&1<(uK=username.indexOf("]"))-uJ&&uK-uJ<=8?username.substring(uJ+1,uK).toUpperCase().trim():null}, leaderboardFilter.parseClanFromPlayerName = this.uI;`);
 | 
			
		||||
            `this.uI=function(username){var uK,uJ=username.indexOf("[");return!(uJ<0)&&1<(uK=username.indexOf("]"))-uJ&&uK-uJ<=8?username.substring(uJ+1,uK).toUpperCase().trim():null}, __fx.leaderboardFilter.parseClanFromPlayerName = this.uI;`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    { // Hovering tooltip
 | 
			
		||||
        replaceRawCode("this.click=function(gG,gH,uH){var fd=an.fe(gG),ff=an.fg(gH),fh=an.fi(fd,ff),fj=an.fk(fh);if(!an.fl(fd,ff))return!1;var fd=(bB.d3.isUIZoomEnabled()?.025:.0144)*aO.g4,ff=performance.now();if(Math.abs(gG-wK)>fd||Math.abs(gH-wL)>fd||dg+500<ff)return!1;if(dg=ff,uH&&!function(gG,gH,fj){a3.ek(fj)||-1===(gG=ao.fr.wq(gG,gH))?l.wp(fj):l.wr(gG)}(gG,gH,fj),",
 | 
			
		||||
            `hoveringTooltip.display = function(mouseX, mouseY) {
 | 
			
		||||
            `__fx.hoveringTooltip.display = function(mouseX, mouseY) {
 | 
			
		||||
			var coordX = an.fe(mouseX), coordY = an.fg(mouseY),
 | 
			
		||||
				coord = an.fi(coordX, coordY), point = an.fk(coord);
 | 
			
		||||
			if (coordX < 0 || coordY < 0) return;
 | 
			
		||||
| 
						 | 
				
			
			@ -273,13 +273,13 @@ canvas.font=aY.g0.g1(1,fontSize),canvas.fillStyle="rgba("+gR+","+tD+","+hj+",0.6
 | 
			
		|||
		}
 | 
			
		||||
		this.click=function(gG,gH,uH){var fd=an.fe(gG),ff=an.fg(gH),fh=an.fi(fd,ff),fj=an.fk(fh);if(!an.fl(fd,ff))return!1;fd=(bB.d3.isUIZoomEnabled()?.025:.0144)*aO.g4,ff=performance.now();if(Math.abs(gG-wK)>fd||Math.abs(gH-wL)>fd||dg+500<ff)return!1;if(dg=ff,uH&&!function(gG,gH,fj){a3.ek(fj)||-1===(gG=ao.fr.wq(gG,gH))?l.wp(fj):l.wr(gG)}(gG,gH,fj),`)
 | 
			
		||||
        replaceRawCode("aK.nH=(window.devicePixelRatio||1)*aEr,",
 | 
			
		||||
            `aK.nH = (window.devicePixelRatio || 1) * aEr, hoveringTooltip.canvasPixelScale = aK.nH,`)
 | 
			
		||||
            `aK.nH = (window.devicePixelRatio || 1) * aEr, __fx.hoveringTooltip.canvasPixelScale = aK.nH,`)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    { // Highlight clan spawnpoints
 | 
			
		||||
        // when rendered on game start
 | 
			
		||||
        replaceRawCode("var x;0!==playerData.eg[i]&&0!==playerData.playerTerritories[i]&&(x=playerData.nu[i]+playerData.nw[i]+1-gv-2>>1,gv=playerData.nv[i]+playerData.nx[i]+1-gv-2>>1,ctx.drawImage(km[game.gIsTeamGame?lV.i6[i]:i<game.gHumans?1:0],x,gv))",
 | 
			
		||||
            `var x, y, highlight = settings.highlightClanSpawns && clanFilter.inOwnClan[i];
 | 
			
		||||
            `var x, y, highlight = __fx.settings.highlightClanSpawns && __fx.clanFilter.inOwnClan[i];
 | 
			
		||||
            if (highlight) gv *= 2;
 | 
			
		||||
            0!==playerData.eg[i]&&0!==playerData.playerTerritories[i]&&(x=playerData.nu[i]+playerData.nw[i]+1-gv-2>>1,y=playerData.nv[i]+playerData.nx[i]+1-gv-2>>1,
 | 
			
		||||
            highlight ? ctx.drawImage(km[game.gIsTeamGame?lV.i6[i]:i<game.gHumans?1:0],x,y, gv, gv)
 | 
			
		||||
| 
						 | 
				
			
			@ -287,7 +287,7 @@ canvas.font=aY.g0.g1(1,fontSize),canvas.fillStyle="rgba("+gR+","+tD+","+hj+",0.6
 | 
			
		|||
        // when rendered during spawn selection
 | 
			
		||||
        replaceRawCode("function(a6,es,bK,hP,hQ,hR,ov){0===dV.eg[a6]||0===dV.ev[a6]||(hQ=ay.ak*((dV.nu[a6]+dV.nw[a6]+1)/2-bK)/(hQ-bK)-.5*es,bK=ay.al*((dV.nv[a6]+dV.nx[a6]+1)/2-hP)/(hR-hP)-.5*es,hQ>ay.ak)||bK>ay.al||hQ<-es||bK<-es||(bI.setTransform(cz,0,0,cz,hQ,bK),",
 | 
			
		||||
            `function(a6,es,bK,hP,hQ,hR,ov){
 | 
			
		||||
            var highlight = settings.highlightClanSpawns && clanFilter.inOwnClan[a6];
 | 
			
		||||
            var highlight = __fx.settings.highlightClanSpawns && __fx.clanFilter.inOwnClan[a6];
 | 
			
		||||
            if (highlight) es *= 2;
 | 
			
		||||
            0===dV.eg[a6]||0===dV.ev[a6]||(hQ=ay.ak*((dV.nu[a6]+dV.nw[a6]+1)/2-bK)/(hQ-bK)-.5*es,bK=ay.al*((dV.nv[a6]+dV.nx[a6]+1)/2-hP)/(hR-hP)-.5*es,hQ>ay.ak)||bK>ay.al||hQ<-es||bK<-es||(bI.setTransform(highlight?cz*2:cz,0,0,highlight?cz*2:cz,hQ,bK),`
 | 
			
		||||
        )
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,102 @@
 | 
			
		|||
import { getVar } from "./gameInterface.js";
 | 
			
		||||
 | 
			
		||||
export 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.setUpdateFlag = () => {};
 | 
			
		||||
    this.parseClanFromPlayerName = () => { console.warn("parse function not set"); };
 | 
			
		||||
    
 | 
			
		||||
    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) => {
 | 
			
		||||
        const tab = Math.floor(xRelative / (this.windowWidth / this.tabLabels.length));
 | 
			
		||||
        if (this.selectedTab !== tab) {
 | 
			
		||||
            this.selectedTab = tab;
 | 
			
		||||
            if (this.selectedTab === 0) this.clearFilter();
 | 
			
		||||
            else if (this.selectedTab === 1) {
 | 
			
		||||
                this.filterByOwnClan();
 | 
			
		||||
                this.setUpdateFlag();
 | 
			
		||||
            }
 | 
			
		||||
            this.repaintLeaderboard();
 | 
			
		||||
        }
 | 
			
		||||
        return true;
 | 
			
		||||
    };
 | 
			
		||||
    this.filterByOwnClan = () => {
 | 
			
		||||
        this.playersToInclude = [];
 | 
			
		||||
        const playerId = getVar("playerId");
 | 
			
		||||
        const ownClan = this.parseClanFromPlayerName(getVar("rawPlayerNames")[playerId]);
 | 
			
		||||
        getVar("rawPlayerNames").forEach((name, id) => {
 | 
			
		||||
            if (id === playerId || this.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;
 | 
			
		||||
        clanFilter.refresh();
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const clanFilter = new (function() {
 | 
			
		||||
    this.inOwnClan = new Array(512);
 | 
			
		||||
    this.inOwnClan.fill(false);
 | 
			
		||||
    this.refresh = () => {
 | 
			
		||||
        const gHumans = getVar("gHumans");
 | 
			
		||||
        const ownClan = leaderboardFilter.parseClanFromPlayerName(getVar("rawPlayerNames")[getVar("playerId")]);
 | 
			
		||||
        if (ownClan === null) this.inOwnClan.fill(false);
 | 
			
		||||
        else getVar("rawPlayerNames").forEach((name, id) => {
 | 
			
		||||
            this.inOwnClan[id] = id < gHumans && leaderboardFilter.parseClanFromPlayerName(name) === ownClan;
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,58 @@
 | 
			
		|||
import WindowManager from "./windowManager.js";
 | 
			
		||||
import { getVar } from "./gameInterface.js";
 | 
			
		||||
import { escapeHtml } from "./utils.js";
 | 
			
		||||
 | 
			
		||||
WindowManager.add({
 | 
			
		||||
    name: "donationHistory",
 | 
			
		||||
    element: document.querySelector("#donationhistory"),
 | 
			
		||||
    beforeOpen: function(isSingleplayer) {
 | 
			
		||||
        document.getElementById("donationhistory_note").style.display = (/*(settings.showBotDonations || isSingleplayer)*/ true ? "none" : "block");
 | 
			
		||||
    },
 | 
			
		||||
    onClose: function() { donationsTracker.openedWindowPlayerID = null; }
 | 
			
		||||
});
 | 
			
		||||
const donationsTracker = new (function(){
 | 
			
		||||
    this.openedWindowPlayerID = null;
 | 
			
		||||
    this.contentElement = document.querySelector("#donationhistory_content");
 | 
			
		||||
    this.donationHistory = Array(512);
 | 
			
		||||
    // fill the array with empty arrays with length of 3
 | 
			
		||||
    //for (var i = 0; i < 512; i++) this.donationHistory.push([]); // not needed as .reset is called on game start
 | 
			
		||||
    this.getHistoryOf = function(playerID) {
 | 
			
		||||
        return this.donationHistory[playerID].toReversed();
 | 
			
		||||
    }
 | 
			
		||||
    this.reset = function() { for (var i = 0; i < 512; i++) this.donationHistory[i] = []; };
 | 
			
		||||
    this.logDonation = function(senderID, receiverID, amount) {
 | 
			
		||||
        const donationInfo = [senderID, receiverID, amount];
 | 
			
		||||
        this.donationHistory[receiverID].push(donationInfo);
 | 
			
		||||
        this.donationHistory[senderID].push(donationInfo);
 | 
			
		||||
        if (this.openedWindowPlayerID === senderID || this.openedWindowPlayerID === receiverID) {
 | 
			
		||||
            const indexOfNewItem = this.donationHistory[this.openedWindowPlayerID === senderID ? senderID : receiverID].length;
 | 
			
		||||
            this.contentElement.prepend(generateTableRowItem(donationInfo, indexOfNewItem, this.openedWindowPlayerID, true));
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
    function generateTableRowItem(historyItem, index, playerID, isNew) {
 | 
			
		||||
        const rawPlayerNames = getVar("rawPlayerNames");
 | 
			
		||||
        const row = document.createElement("tr");
 | 
			
		||||
        if (isNew) row.setAttribute("class", "new");
 | 
			
		||||
        let content = `<td><span class="color-light-gray">${index}.</span> `;
 | 
			
		||||
        if (playerID === historyItem[1])
 | 
			
		||||
            content += `Received <span class="color-green">${historyItem[2]}</span> resources from ${escapeHtml(rawPlayerNames[historyItem[0]])}`;
 | 
			
		||||
        else content += `Sent <span class="color-red">${historyItem[2]}</span> resources to ${escapeHtml(rawPlayerNames[historyItem[1]])}`;
 | 
			
		||||
        content += "</td>";
 | 
			
		||||
        row.innerHTML = content;
 | 
			
		||||
        return row;
 | 
			
		||||
    }
 | 
			
		||||
    this.displayHistory = function displayDonationsHistory(playerID, playerNames = getVar("rawPlayerNames"), isSingleplayer = getVar("gIsSingleplayer")) {
 | 
			
		||||
        var history = donationsTracker.getHistoryOf(playerID);
 | 
			
		||||
        console.log("History for " + playerNames[playerID] + ":");
 | 
			
		||||
        console.log(history);
 | 
			
		||||
        document.querySelector("#donationhistory h1").innerHTML = "Donation history for " + escapeHtml(playerNames[playerID]);
 | 
			
		||||
        this.contentElement.innerHTML = "";
 | 
			
		||||
        if (history.length > 0) history.forEach((historyItem, index) => {
 | 
			
		||||
            this.contentElement.appendChild(generateTableRowItem(historyItem, history.length - index, playerID));
 | 
			
		||||
        });
 | 
			
		||||
        else this.contentElement.innerText = "Nothing to display";
 | 
			
		||||
        this.openedWindowPlayerID = playerID;
 | 
			
		||||
        WindowManager.openWindow("donationHistory", isSingleplayer);
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
export default donationsTracker;
 | 
			
		||||
								
									
									
										
											606
										
									
									src/fx_core.js
									
									
									
									
								
								
							
							
										
											606
										
									
									src/fx_core.js
									
									
									
									
								| 
						 | 
				
			
			@ -1,606 +0,0 @@
 | 
			
		|||
const fx_version = '0.6.5.5'; // FX Client Version
 | 
			
		||||
const fx_update = 'Aug 26'; // FX Client Last Updated
 | 
			
		||||
 | 
			
		||||
if (localStorage.getItem("fx_winCount") == undefined || localStorage.getItem("fx_winCount") == null) {
 | 
			
		||||
    var wins_counter = 0;
 | 
			
		||||
    console.log('Couldn\'t find a saved win data. creating one...');
 | 
			
		||||
} else if (localStorage.getItem("fx_winCount") != undefined || localStorage.getItem("fx_winCount") != null) {
 | 
			
		||||
    var wins_counter = localStorage.getItem("fx_winCount");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const playerDataProperties = ["playerTerritories", "playerBalances", "rawPlayerNames"];
 | 
			
		||||
const gameObjectProperties = ["playerId", "gIsTeamGame", "gHumans", "gLobbyMaxJoin", "gameState", "gIsSingleplayer"];
 | 
			
		||||
const getVar = varName => {
 | 
			
		||||
    if (playerDataProperties.includes(varName)) return window[dictionary.playerData][dictionary[varName]];
 | 
			
		||||
    if (gameObjectProperties.includes(varName)) return window[dictionary.game][dictionary[varName]];
 | 
			
		||||
    return window[dictionary[varName]]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// https://stackoverflow.com/a/6234804
 | 
			
		||||
function escapeHtml(unsafe) {
 | 
			
		||||
    return unsafe.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function KeybindsInput(containerElement) {
 | 
			
		||||
    const header = document.createElement("p");
 | 
			
		||||
    header.innerText = "Attack Percentage Keybinds";
 | 
			
		||||
    const keybindContainer = document.createElement("div");
 | 
			
		||||
    keybindContainer.className = "arrayinput";
 | 
			
		||||
    const keybindAddButton = document.createElement("button");
 | 
			
		||||
    keybindAddButton.innerText = "Add";
 | 
			
		||||
    containerElement.append(header, keybindContainer, keybindAddButton);
 | 
			
		||||
    this.container = keybindContainer;
 | 
			
		||||
    this.keys = [ "key", "type", "value" ];
 | 
			
		||||
    this.objectArray = [];
 | 
			
		||||
    this.addObject = function () {
 | 
			
		||||
        this.objectArray.push({ key: "", type: "absolute", value: 0.8 });
 | 
			
		||||
        this.displayObjects();
 | 
			
		||||
        keybindAddButton.scrollIntoView(false);
 | 
			
		||||
    };
 | 
			
		||||
    this.update = function () {
 | 
			
		||||
        this.objectArray = settings.attackPercentageKeybinds;
 | 
			
		||||
        this.displayObjects();
 | 
			
		||||
    }
 | 
			
		||||
    keybindAddButton.addEventListener("click", this.addObject.bind(this));
 | 
			
		||||
    this.displayObjects = function () {
 | 
			
		||||
        // Clear the content of the container
 | 
			
		||||
        this.container.innerHTML = "";
 | 
			
		||||
        if (this.objectArray.length === 0) return this.container.innerText = "No custom attack percentage keybinds added";
 | 
			
		||||
        // Loop through the array and display input fields for each object
 | 
			
		||||
        for (var i = 0; i < this.objectArray.length; i++) {
 | 
			
		||||
            var objectDiv = document.createElement("div");
 | 
			
		||||
            // Create input fields for each key
 | 
			
		||||
            this.keys.forEach(function (key) {
 | 
			
		||||
                let inputField = document.createElement(key === "type" ? "select" : "input");
 | 
			
		||||
                if (key === "type") {
 | 
			
		||||
                    inputField.innerHTML = '<option value="absolute">Absolute</option><option value="relative">Relative</option>';
 | 
			
		||||
                    inputField.addEventListener("change", this.updateObject.bind(this, i, key));
 | 
			
		||||
                } else if (key === "key") {
 | 
			
		||||
                    inputField.type = "text";
 | 
			
		||||
                    inputField.setAttribute("readonly", "");
 | 
			
		||||
                    inputField.setAttribute("placeholder", "No key set");
 | 
			
		||||
                    inputField.addEventListener("click", this.startKeyInput.bind(this, i, key));
 | 
			
		||||
                } else { // key === "value"
 | 
			
		||||
                    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));
 | 
			
		||||
                }
 | 
			
		||||
                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);
 | 
			
		||||
            // Button to delete the object
 | 
			
		||||
            var deleteButton = document.createElement("button");
 | 
			
		||||
            deleteButton.textContent = "Delete";
 | 
			
		||||
            deleteButton.addEventListener("click", this.deleteObject.bind(this, i));
 | 
			
		||||
            // Append delete button to the object div
 | 
			
		||||
            objectDiv.appendChild(deleteButton);
 | 
			
		||||
            // Append the object div to the container
 | 
			
		||||
            this.container.appendChild(objectDiv);
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
    /** @param {PointerEvent} event */
 | 
			
		||||
    this.startKeyInput = function (index, property, event) {
 | 
			
		||||
        event.target.value = "Press any key";
 | 
			
		||||
        const handler = this.updateObject.bind(this, index, property);
 | 
			
		||||
        event.target.addEventListener('keydown', handler, { once: true });
 | 
			
		||||
        event.target.addEventListener("blur", () => {
 | 
			
		||||
            event.target.removeEventListener('keydown', handler, { once: true });
 | 
			
		||||
            event.target.value = this.objectArray[index][property];
 | 
			
		||||
            //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" ? (
 | 
			
		||||
            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();
 | 
			
		||||
    };
 | 
			
		||||
    this.deleteObject = function (index) {
 | 
			
		||||
        // Remove the object from the array
 | 
			
		||||
        this.objectArray.splice(index, 1);
 | 
			
		||||
        // Display the updated input fields for objects
 | 
			
		||||
        this.displayObjects();
 | 
			
		||||
    };
 | 
			
		||||
    return this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var settings = {
 | 
			
		||||
    //"fontName": "Trebuchet MS",
 | 
			
		||||
    //"showBotDonations": false,
 | 
			
		||||
    "displayWinCounter": true,
 | 
			
		||||
    "useFullscreenMode": false,
 | 
			
		||||
    "hoveringTooltip": true,
 | 
			
		||||
    //"hideAllLinks": false,
 | 
			
		||||
    "realisticNames": false,
 | 
			
		||||
    "showPlayerDensity": true,
 | 
			
		||||
    "coloredDensity": true,
 | 
			
		||||
    "densityDisplayStyle": "percentage",
 | 
			
		||||
    "highlightClanSpawns": false,
 | 
			
		||||
    //"customMapFileBtn": true
 | 
			
		||||
    "customBackgroundUrl": "",
 | 
			
		||||
    "attackPercentageKeybinds": [],
 | 
			
		||||
};
 | 
			
		||||
const discontinuedSettings = [ "hideAllLinks", "fontName" ];
 | 
			
		||||
let makeMainMenuTransparent = false;
 | 
			
		||||
var settingsManager = new (function() {
 | 
			
		||||
    const settingsStructure = [
 | 
			
		||||
        { for: "displayWinCounter", type: "checkbox", label: "Display win counter",
 | 
			
		||||
            note: "The win counter tracks multiplayer solo wins (not in team games)" },
 | 
			
		||||
        { type: "button", text: "Reset win counter", action: removeWins },
 | 
			
		||||
        { 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" },
 | 
			
		||||
        { for: "coloredDensity", type: "checkbox", label: "Colored density", note: "Display the density with a color between red and green depending on the density value" },
 | 
			
		||||
        { for: "densityDisplayStyle", type: "selectMenu", label: "Density value display style:", tooltip: "Controls how the territorial density value should be rendered", options: [
 | 
			
		||||
            { value: "percentage", label: "Percentage" },
 | 
			
		||||
            { value: "absoluteQuotient", label: "Value from 0 to 150 (BetterTT style)" }
 | 
			
		||||
        ]},
 | 
			
		||||
        { for: "highlightClanSpawns", type: "checkbox", label: "Highlight clan spawnpoints",
 | 
			
		||||
            note: "Increases the spawnpoint glow size for members of your clan" },
 | 
			
		||||
        { for: "customBackgroundUrl", type: "textInput", label: "Custom main menu background:", placeholder: "Enter an image URL here", tooltip: "A custom image to be shown as the main menu background instead of the currently selected map." },
 | 
			
		||||
        KeybindsInput
 | 
			
		||||
    ];
 | 
			
		||||
    const settingsContainer = document.querySelector(".settings .scrollable");
 | 
			
		||||
    var inputFields = {}; // (includes select menus)
 | 
			
		||||
    var checkboxFields = {};
 | 
			
		||||
    var customElements = [];
 | 
			
		||||
    settingsStructure.forEach(item => {
 | 
			
		||||
        if (typeof item === "function") {
 | 
			
		||||
            const container = document.createElement("div");
 | 
			
		||||
            customElements.push(new item(container));
 | 
			
		||||
            return settingsContainer.append(container);
 | 
			
		||||
        }
 | 
			
		||||
        const label = document.createElement("label");
 | 
			
		||||
        if (item.tooltip) label.title = item.tooltip;
 | 
			
		||||
        const isValueInput = item.type.endsWith("Input");
 | 
			
		||||
        const element = document.createElement(isValueInput || item.type === "checkbox" ? "input" : item.type === "selectMenu" ? "select" : "button");
 | 
			
		||||
        if (item.type === "textInput") element.type = "text";
 | 
			
		||||
        if (item.placeholder) element.placeholder = item.placeholder;
 | 
			
		||||
        if (isValueInput || item.type === "selectMenu") inputFields[item.for] = element;
 | 
			
		||||
        if (item.text) element.innerText = item.text;
 | 
			
		||||
        if (item.action) element.addEventListener("click", item.action);
 | 
			
		||||
        if (item.label) label.append(item.label + " ");
 | 
			
		||||
        if (item.note) {
 | 
			
		||||
            const note = document.createElement("small");
 | 
			
		||||
            note.innerText = item.note;
 | 
			
		||||
            label.append(document.createElement("br"), note)
 | 
			
		||||
        }
 | 
			
		||||
        if (item.options) item.options.forEach(option => {
 | 
			
		||||
            const optionElement = document.createElement("option");
 | 
			
		||||
            optionElement.setAttribute("value", option.value);
 | 
			
		||||
            optionElement.innerText = option.label;
 | 
			
		||||
            element.append(optionElement);
 | 
			
		||||
        });
 | 
			
		||||
        label.append(element);
 | 
			
		||||
        if (item.type === "checkbox") {
 | 
			
		||||
            element.type = "checkbox";
 | 
			
		||||
            const checkmark = document.createElement("span");
 | 
			
		||||
            checkmark.className = "checkmark";
 | 
			
		||||
            label.className = "checkbox";
 | 
			
		||||
            label.append(checkmark);
 | 
			
		||||
            checkboxFields[item.for] = element;
 | 
			
		||||
        } else label.append(document.createElement("br"));
 | 
			
		||||
        settingsContainer.append(label, document.createElement("br"));
 | 
			
		||||
    });
 | 
			
		||||
    this.save = function() {
 | 
			
		||||
        Object.keys(inputFields).forEach(function(key) { settings[key] = inputFields[key].value.trim(); });
 | 
			
		||||
        Object.keys(checkboxFields).forEach(function(key) { settings[key] = checkboxFields[key].checked; });
 | 
			
		||||
        this.applySettings();
 | 
			
		||||
        WindowManager.closeWindow("settings");
 | 
			
		||||
        discontinuedSettings.forEach(settingName => delete settings[settingName]);
 | 
			
		||||
        localStorage.setItem("fx_settings", JSON.stringify(settings));
 | 
			
		||||
        // should probably firgure out a way to do this without reloading - // You can't do it, localstorages REQUIRE you to reload
 | 
			
		||||
        window.location.reload();
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const fileInput = document.createElement("input");
 | 
			
		||||
    fileInput.type = "file";
 | 
			
		||||
    function handleFileSelect(event) {
 | 
			
		||||
        const input = event.target;
 | 
			
		||||
        /** @type {File} */
 | 
			
		||||
        const selectedFile = input.files[0];
 | 
			
		||||
        if (!selectedFile) return;
 | 
			
		||||
 | 
			
		||||
        input.removeEventListener("change", handleFileSelect);
 | 
			
		||||
        input.value = "";
 | 
			
		||||
        if (!selectedFile.name.endsWith(".json")) return alert("Invalid file format");
 | 
			
		||||
        const fileReader = new FileReader();
 | 
			
		||||
        fileReader.onload = function() {
 | 
			
		||||
            let result;
 | 
			
		||||
            try {    
 | 
			
		||||
                result = JSON.parse(fileReader.result);
 | 
			
		||||
                if (confirm("Warning: This will override all current settings, click \"OK\" to confirm")) settings = result;
 | 
			
		||||
                localStorage.setItem("fx_settings", JSON.stringify(settings));
 | 
			
		||||
                window.location.reload();
 | 
			
		||||
            } catch (error) {
 | 
			
		||||
                alert("Error\n" + error)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        fileReader.readAsText(selectedFile);
 | 
			
		||||
    }
 | 
			
		||||
    this.importFromFile = function() {
 | 
			
		||||
        fileInput.click();
 | 
			
		||||
        fileInput.addEventListener('change', handleFileSelect);
 | 
			
		||||
    };
 | 
			
		||||
    // https://stackoverflow.com/a/34156339
 | 
			
		||||
    function saveFile(content, fileName, contentType) {
 | 
			
		||||
        var a = document.createElement("a");
 | 
			
		||||
        var file = new Blob([content], {type: contentType});
 | 
			
		||||
        a.href = URL.createObjectURL(file);
 | 
			
		||||
        a.download = fileName;
 | 
			
		||||
        a.click();
 | 
			
		||||
        URL.revokeObjectURL(a.href);
 | 
			
		||||
    }
 | 
			
		||||
    this.exportToFile = function() {
 | 
			
		||||
        saveFile(JSON.stringify(settings), 'FX_client_settings.json', 'application/json');
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    this.syncFields = function() {
 | 
			
		||||
        Object.keys(inputFields).forEach(function(key) { inputFields[key].value = settings[key]; });
 | 
			
		||||
        Object.keys(checkboxFields).forEach(function(key) { checkboxFields[key].checked = settings[key]; });
 | 
			
		||||
        customElements.forEach(element => element.update());
 | 
			
		||||
    };
 | 
			
		||||
    this.resetAll = function() {
 | 
			
		||||
        if (!confirm("Are you Really SURE you want to RESET ALL SETTINGS back to the default?")) return;
 | 
			
		||||
        localStorage.removeItem("fx_settings");
 | 
			
		||||
        window.location.reload();
 | 
			
		||||
    };
 | 
			
		||||
    this.applySettings = function() {
 | 
			
		||||
        //setVarByName("bu", "px " + settings.fontName);
 | 
			
		||||
        if (settings.useFullscreenMode && document.fullscreenEnabled) {
 | 
			
		||||
            function tryEnterFullscreen() {
 | 
			
		||||
                if (document.fullscreenElement !== null) return;
 | 
			
		||||
                document.documentElement.requestFullscreen({ navigationUI: "hide" })
 | 
			
		||||
                    .then(() => { console.log('Fullscreen mode activated'); })
 | 
			
		||||
                    .catch((error) => { console.warn('Could not enter fullscreen mode:', error); });
 | 
			
		||||
            }
 | 
			
		||||
            document.addEventListener('mousedown', tryEnterFullscreen, { once: true });
 | 
			
		||||
            document.addEventListener('click', tryEnterFullscreen, { once: true });
 | 
			
		||||
        }
 | 
			
		||||
        if (settings.customBackgroundUrl !== "") {
 | 
			
		||||
            document.body.style.backgroundImage = "url(" + settings.customBackgroundUrl + ")";
 | 
			
		||||
            document.body.style.backgroundSize = "cover";
 | 
			
		||||
            document.body.style.backgroundPosition = "center";
 | 
			
		||||
        }
 | 
			
		||||
        makeMainMenuTransparent = settings.customBackgroundUrl !== "";
 | 
			
		||||
    };
 | 
			
		||||
});
 | 
			
		||||
function removeWins() {
 | 
			
		||||
    var confirm1 = confirm('Do you really want to reset your Wins?');
 | 
			
		||||
    if (confirm1) {
 | 
			
		||||
        wins_counter = 0;
 | 
			
		||||
        localStorage.removeItem('fx_winCount');
 | 
			
		||||
        alert("Successfully reset wins");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
const openCustomBackgroundFilePicker = () => {
 | 
			
		||||
    const fileInput = document.getElementById("customBackgroundFileInput");
 | 
			
		||||
    fileInput.click();
 | 
			
		||||
    fileInput.addEventListener('change', handleFileSelect);
 | 
			
		||||
}
 | 
			
		||||
function handleFileSelect(event) {
 | 
			
		||||
    const fileInput = event.target;
 | 
			
		||||
    const selectedFile = fileInput.files[0];
 | 
			
		||||
    console.log(fileInput.files);
 | 
			
		||||
    console.log(fileInput.files[0]);
 | 
			
		||||
    if (selectedFile) {
 | 
			
		||||
        const fileUrl = URL.createObjectURL(selectedFile);
 | 
			
		||||
        console.log("File URL:", fileUrl);
 | 
			
		||||
        fileInput.value = "";
 | 
			
		||||
        fileInput.removeEventListener("change", handleFileSelect);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var WindowManager = new (function() {
 | 
			
		||||
    var windows = {};
 | 
			
		||||
    this.add = function(newWindow) {
 | 
			
		||||
        windows[newWindow.name] = newWindow;
 | 
			
		||||
        windows[newWindow.name].isOpen = false;
 | 
			
		||||
    };
 | 
			
		||||
    this.openWindow = function(windowName, ...args) {
 | 
			
		||||
        if (windows[windowName].isOpen === true) return;
 | 
			
		||||
        if (windows[windowName].beforeOpen !== undefined) windows[windowName].beforeOpen(...args);
 | 
			
		||||
        windows[windowName].isOpen = true;
 | 
			
		||||
        windows[windowName].element.style.display = null;
 | 
			
		||||
    };
 | 
			
		||||
    this.closeWindow = function(windowName) {
 | 
			
		||||
        if (windows[windowName].isOpen === false) return;
 | 
			
		||||
        windows[windowName].isOpen = false;
 | 
			
		||||
        windows[windowName].element.style.display = "none";
 | 
			
		||||
        if (windows[windowName].onClose !== undefined) windows[windowName].onClose();
 | 
			
		||||
    };
 | 
			
		||||
    this.closeAll = function() {
 | 
			
		||||
        Object.values(windows).forEach(function(windowObj) {
 | 
			
		||||
            WindowManager.closeWindow(windowObj.name);
 | 
			
		||||
        });
 | 
			
		||||
    };
 | 
			
		||||
});
 | 
			
		||||
WindowManager.add({
 | 
			
		||||
    name: "settings",
 | 
			
		||||
    element: document.querySelector(".settings"),
 | 
			
		||||
    beforeOpen: function() { settingsManager.syncFields(); }
 | 
			
		||||
});
 | 
			
		||||
WindowManager.add({
 | 
			
		||||
    name: "donationHistory",
 | 
			
		||||
    element: document.querySelector("#donationhistory"),
 | 
			
		||||
    beforeOpen: function(isSingleplayer) {
 | 
			
		||||
        document.getElementById("donationhistory_note").style.display = ((true || settings.showBotDonations || /*getVarByName("dt")*/ isSingleplayer) ? "none" : "block");
 | 
			
		||||
    },
 | 
			
		||||
    onClose: function() { donationsTracker.openedWindowPlayerID = null; }
 | 
			
		||||
});
 | 
			
		||||
WindowManager.add({
 | 
			
		||||
    name: "playerList",
 | 
			
		||||
    element: document.getElementById("playerlist"),
 | 
			
		||||
    beforeOpen: function() {}
 | 
			
		||||
});
 | 
			
		||||
document.getElementById("canvasA").addEventListener("mousedown", WindowManager.closeAll);
 | 
			
		||||
document.getElementById("canvasA").addEventListener("touchstart", WindowManager.closeAll, { passive: true });
 | 
			
		||||
document.addEventListener("keydown", event => { if (event.key === "Escape") WindowManager.closeAll(); });
 | 
			
		||||
var settingsGearIcon = document.createElement('img');
 | 
			
		||||
settingsGearIcon.setAttribute('src', 'assets/geari_white.png');
 | 
			
		||||
 | 
			
		||||
const playerList = new (function () {
 | 
			
		||||
    const playersIcon = document.createElement('img');
 | 
			
		||||
    playersIcon.setAttribute('src', 'assets/players_icon.png');
 | 
			
		||||
    document.getElementById("playerlist_content").addEventListener("click", event => {
 | 
			
		||||
        const playerId = event.target.closest("tr[data-player-id]")?.getAttribute("data-player-id");
 | 
			
		||||
        if (!playerId) return;
 | 
			
		||||
        if (getVar("gIsTeamGame")) WindowManager.closeWindow("playerList"), donationsTracker.displayHistory(playerId);
 | 
			
		||||
    });
 | 
			
		||||
    this.display = function displayPlayerList(playerNames) {
 | 
			
		||||
        const gHumans = getVar("gHumans");
 | 
			
		||||
        const gLobbyMaxJoin = getVar("gLobbyMaxJoin");
 | 
			
		||||
        let listContent = `<h3>Players (${gHumans})</h3>`;
 | 
			
		||||
        for (let i = 0; i < gLobbyMaxJoin; i++) {
 | 
			
		||||
            if (i === gHumans) listContent += `<h3>Bots (${gLobbyMaxJoin - gHumans})</h3>`;
 | 
			
		||||
            listContent += `<tr data-player-id="${i}"><td><span class="color-light-gray">${i + 1}.</span> ${escapeHtml(playerNames[i])}</td></tr>`
 | 
			
		||||
        }
 | 
			
		||||
        document.getElementById("playerlist_content").innerHTML = listContent;
 | 
			
		||||
        document.getElementById("playerlist_content").setAttribute("class", getVar("gIsTeamGame") ? "clickable" : "");
 | 
			
		||||
        WindowManager.openWindow("playerList");
 | 
			
		||||
    }
 | 
			
		||||
    this.hoveringOverButton = false;
 | 
			
		||||
    this.drawButton = (canvas, x, y, size) => {
 | 
			
		||||
        canvas.fillRect(x, y, size, size);
 | 
			
		||||
        canvas.fillStyle = this.hoveringOverButton ? "#aaaaaaaa" : "#000000aa";
 | 
			
		||||
        canvas.clearRect(x + 1, y + 1, size - 2, size - 2);
 | 
			
		||||
        canvas.fillRect(x + 1, y + 1, size - 2, size - 2);
 | 
			
		||||
        canvas.fillStyle = "#ffffff";
 | 
			
		||||
        canvas.imageSmoothingEnabled = true;
 | 
			
		||||
        canvas.drawImage(playersIcon, x + 2, y + 2, size - 4, size - 4);
 | 
			
		||||
        canvas.imageSmoothingEnabled = false;
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
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.setUpdateFlag = () => {};
 | 
			
		||||
    this.parseClanFromPlayerName = () => { console.warn("parse function not set"); };
 | 
			
		||||
    
 | 
			
		||||
    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) => {
 | 
			
		||||
        const tab = Math.floor(xRelative / (this.windowWidth / this.tabLabels.length));
 | 
			
		||||
        if (this.selectedTab !== tab) {
 | 
			
		||||
            this.selectedTab = tab;
 | 
			
		||||
            if (this.selectedTab === 0) this.clearFilter();
 | 
			
		||||
            else if (this.selectedTab === 1) {
 | 
			
		||||
                this.filterByOwnClan();
 | 
			
		||||
                this.setUpdateFlag();
 | 
			
		||||
            }
 | 
			
		||||
            this.repaintLeaderboard();
 | 
			
		||||
        }
 | 
			
		||||
        return true;
 | 
			
		||||
    };
 | 
			
		||||
    this.filterByOwnClan = () => {
 | 
			
		||||
        this.playersToInclude = [];
 | 
			
		||||
        const playerId = getVar("playerId");
 | 
			
		||||
        const ownClan = this.parseClanFromPlayerName(getVar("rawPlayerNames")[playerId]);
 | 
			
		||||
        getVar("rawPlayerNames").forEach((name, id) => {
 | 
			
		||||
            if (id === playerId || this.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;
 | 
			
		||||
        clanFilter.refresh();
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const clanFilter = new (function() {
 | 
			
		||||
    this.inOwnClan = new Array(512);
 | 
			
		||||
    this.inOwnClan.fill(false);
 | 
			
		||||
    this.refresh = () => {
 | 
			
		||||
        const gHumans = getVar("gHumans");
 | 
			
		||||
        const ownClan = leaderboardFilter.parseClanFromPlayerName(getVar("rawPlayerNames")[getVar("playerId")]);
 | 
			
		||||
        if (ownClan === null) this.inOwnClan.fill(false);
 | 
			
		||||
        else getVar("rawPlayerNames").forEach((name, id) => {
 | 
			
		||||
            this.inOwnClan[id] = id < gHumans && leaderboardFilter.parseClanFromPlayerName(name) === ownClan;
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const hoveringTooltip = new (function() {
 | 
			
		||||
    let recentlyShown = false;
 | 
			
		||||
    this.display = () => {}; // this gets populated by the modified game script
 | 
			
		||||
    this.canvasPixelScale = 1;
 | 
			
		||||
    function handler(e) {
 | 
			
		||||
        if (!settings.hoveringTooltip || !getVar("gameState") || recentlyShown) return;
 | 
			
		||||
        let x, y;
 | 
			
		||||
        // https://stackoverflow.com/a/61732450
 | 
			
		||||
        if (e.type.includes(`touch`)) {
 | 
			
		||||
            const { touches, changedTouches } = e.originalEvent ?? e;
 | 
			
		||||
            const touch = touches[0] ?? changedTouches[0];
 | 
			
		||||
            x = touch.pageX;
 | 
			
		||||
            y = touch.pageY;
 | 
			
		||||
        } else if (e.type.includes(`mouse`)) {
 | 
			
		||||
            x = e.clientX;
 | 
			
		||||
            y = e.clientY;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        recentlyShown = true;
 | 
			
		||||
        try {
 | 
			
		||||
            this.display(this.canvasPixelScale * x, this.canvasPixelScale * y);
 | 
			
		||||
        } catch (e) { console.error(e) }
 | 
			
		||||
        // for better performance, reduce the tooltip display frequency to no more than once every 100 ms
 | 
			
		||||
        setTimeout(() => recentlyShown = false, 100);
 | 
			
		||||
    }
 | 
			
		||||
    document.getElementById("canvasA").addEventListener("mousemove", handler.bind(this));
 | 
			
		||||
    document.getElementById("canvasA").addEventListener("touchstart", handler.bind(this));
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
var donationsTracker = new (function(){
 | 
			
		||||
    this.openedWindowPlayerID = null;
 | 
			
		||||
    this.contentElement = document.querySelector("#donationhistory_content");
 | 
			
		||||
    this.donationHistory = Array(512);
 | 
			
		||||
    // fill the array with empty arrays with length of 3
 | 
			
		||||
    //for (var i = 0; i < 512; i++) this.donationHistory.push([]); // not needed as .reset is called on game start
 | 
			
		||||
    this.getHistoryOf = function(playerID) {
 | 
			
		||||
        return this.donationHistory[playerID].toReversed();
 | 
			
		||||
    }
 | 
			
		||||
    this.reset = function() { for (var i = 0; i < 512; i++) this.donationHistory[i] = []; };
 | 
			
		||||
    this.logDonation = function(senderID, receiverID, amount) {
 | 
			
		||||
        const donationInfo = [senderID, receiverID, amount];
 | 
			
		||||
        this.donationHistory[receiverID].push(donationInfo);
 | 
			
		||||
        this.donationHistory[senderID].push(donationInfo);
 | 
			
		||||
        if (this.openedWindowPlayerID === senderID || this.openedWindowPlayerID === receiverID) {
 | 
			
		||||
            const indexOfNewItem = this.donationHistory[this.openedWindowPlayerID === senderID ? senderID : receiverID].length;
 | 
			
		||||
            this.contentElement.prepend(generateTableRowItem(donationInfo, indexOfNewItem, this.openedWindowPlayerID, true));
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
    function generateTableRowItem(historyItem, index, playerID, isNew) {
 | 
			
		||||
        const rawPlayerNames = getVar("rawPlayerNames");
 | 
			
		||||
        const row = document.createElement("tr");
 | 
			
		||||
        if (isNew) row.setAttribute("class", "new");
 | 
			
		||||
        let content = `<td><span class="color-light-gray">${index}.</span> `;
 | 
			
		||||
        if (playerID === historyItem[1])
 | 
			
		||||
            content += `Received <span class="color-green">${historyItem[2]}</span> resources from ${escapeHtml(rawPlayerNames[historyItem[0]])}`;
 | 
			
		||||
        else content += `Sent <span class="color-red">${historyItem[2]}</span> resources to ${escapeHtml(rawPlayerNames[historyItem[1]])}`;
 | 
			
		||||
        content += "</td>";
 | 
			
		||||
        row.innerHTML = content;
 | 
			
		||||
        return row;
 | 
			
		||||
    }
 | 
			
		||||
    this.displayHistory = function displayDonationsHistory(playerID, playerNames = getVar("rawPlayerNames"), isSingleplayer = getVar("gIsSingleplayer")) {
 | 
			
		||||
        var history = donationsTracker.getHistoryOf(playerID);
 | 
			
		||||
        console.log("History for " + playerNames[playerID] + ":");
 | 
			
		||||
        console.log(history);
 | 
			
		||||
        document.querySelector("#donationhistory h1").innerHTML = "Donation history for " + escapeHtml(playerNames[playerID]);
 | 
			
		||||
        this.contentElement.innerHTML = "";
 | 
			
		||||
        if (history.length > 0) history.forEach((historyItem, index) => {
 | 
			
		||||
            this.contentElement.appendChild(generateTableRowItem(historyItem, history.length - index, playerID));
 | 
			
		||||
        });
 | 
			
		||||
        else this.contentElement.innerText = "Nothing to display";
 | 
			
		||||
        this.openedWindowPlayerID = playerID;
 | 
			
		||||
        WindowManager.openWindow("donationHistory", isSingleplayer);
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
var utils = new (function() {
 | 
			
		||||
    this.getMaxTroops = function(playerTerritories, playerID) { return (playerTerritories[playerID]*150).toString(); };
 | 
			
		||||
    this.getDensity = function(playerID, playerBalances = getVar("playerBalances"), playerTerritories = getVar("playerTerritories")) {
 | 
			
		||||
        if (settings.densityDisplayStyle === "percentage") return (((playerBalances[playerID] / ((playerTerritories[playerID] === 0 ? 1 : playerTerritories[playerID]) * 150)) * 100).toFixed(1) + "%");
 | 
			
		||||
        else return (playerBalances[playerID] / (playerTerritories[playerID] === 0 ? 1 : playerTerritories[playerID])).toFixed(1);
 | 
			
		||||
    };
 | 
			
		||||
    this.isPointInRectangle = function(x, y, rectangleStartX, rectangleStartY, width, height) {
 | 
			
		||||
        return x >= rectangleStartX && x <= rectangleStartX + width && y >= rectangleStartY && y <= rectangleStartY + height;
 | 
			
		||||
    };
 | 
			
		||||
    /** @param {CanvasRenderingContext2D} canvas @param {string} text */
 | 
			
		||||
    this.fillTextMultiline = function(canvas, text, x, y, maxWidth) {
 | 
			
		||||
        const lineHeight = parseInt(canvas.font.split(" ").find(part => part.endsWith("px")).slice(0, -2));
 | 
			
		||||
        text.split("\n").forEach((line, index) => canvas.fillText(line, x, y + index * lineHeight, maxWidth));
 | 
			
		||||
    }
 | 
			
		||||
    this.textStyleBasedOnDensity = function(playerID) {
 | 
			
		||||
        const playerBalances = getVar("playerBalances"), playerTerritories = getVar("playerTerritories");
 | 
			
		||||
        return `hsl(${playerBalances[playerID] / (playerTerritories[playerID] * 1.5)}, 100%, 50%, 1)`;
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const keybindFunctions = { setAbsolute: () => {}, setRelative: () => {} };
 | 
			
		||||
const keybindHandler = key => {
 | 
			
		||||
    const keybindData = settings.attackPercentageKeybinds.find(keybind => keybind.key === key);
 | 
			
		||||
    if (keybindData === undefined) return false;
 | 
			
		||||
    if (keybindData.type === "absolute") keybindFunctions.setAbsolute(keybindData.value);
 | 
			
		||||
    else keybindFunctions.setRelative(keybindData.value);
 | 
			
		||||
    return true;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
if (localStorage.getItem("fx_settings") !== null) {
 | 
			
		||||
    settings = {...settings, ...JSON.parse(localStorage.getItem("fx_settings"))};
 | 
			
		||||
}
 | 
			
		||||
settingsManager.applySettings();
 | 
			
		||||
 | 
			
		||||
console.log('Successfully loaded FX Client');
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,7 @@
 | 
			
		|||
const playerDataProperties = ["playerTerritories", "playerBalances", "rawPlayerNames"];
 | 
			
		||||
const gameObjectProperties = ["playerId", "gIsTeamGame", "gHumans", "gLobbyMaxJoin", "gameState", "gIsSingleplayer"];
 | 
			
		||||
export const getVar = varName => {
 | 
			
		||||
    if (playerDataProperties.includes(varName)) return window[dictionary.playerData][dictionary[varName]];
 | 
			
		||||
    if (gameObjectProperties.includes(varName)) return window[dictionary.game][dictionary[varName]];
 | 
			
		||||
    return window[dictionary[varName]]
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,23 @@
 | 
			
		|||
import { getSettings } from "./settings.js";
 | 
			
		||||
import { getVar } from "./gameInterface.js";
 | 
			
		||||
 | 
			
		||||
const utils = new (function() {
 | 
			
		||||
    this.getMaxTroops = function(playerTerritories, playerID) { return (playerTerritories[playerID]*150).toString(); };
 | 
			
		||||
    this.getDensity = function(playerID, playerBalances = getVar("playerBalances"), playerTerritories = getVar("playerTerritories")) {
 | 
			
		||||
        if (getSettings().densityDisplayStyle === "percentage") return (((playerBalances[playerID] / ((playerTerritories[playerID] === 0 ? 1 : playerTerritories[playerID]) * 150)) * 100).toFixed(1) + "%");
 | 
			
		||||
        else return (playerBalances[playerID] / (playerTerritories[playerID] === 0 ? 1 : playerTerritories[playerID])).toFixed(1);
 | 
			
		||||
    };
 | 
			
		||||
    this.isPointInRectangle = function(x, y, rectangleStartX, rectangleStartY, width, height) {
 | 
			
		||||
        return x >= rectangleStartX && x <= rectangleStartX + width && y >= rectangleStartY && y <= rectangleStartY + height;
 | 
			
		||||
    };
 | 
			
		||||
    /** @param {CanvasRenderingContext2D} canvas @param {string} text */
 | 
			
		||||
    this.fillTextMultiline = function(canvas, text, x, y, maxWidth) {
 | 
			
		||||
        const lineHeight = parseInt(canvas.font.split(" ").find(part => part.endsWith("px")).slice(0, -2));
 | 
			
		||||
        text.split("\n").forEach((line, index) => canvas.fillText(line, x, y + index * lineHeight, maxWidth));
 | 
			
		||||
    }
 | 
			
		||||
    this.textStyleBasedOnDensity = function(playerID) {
 | 
			
		||||
        const playerBalances = getVar("playerBalances"), playerTerritories = getVar("playerTerritories");
 | 
			
		||||
        return `hsl(${playerBalances[playerID] / (playerTerritories[playerID] * 1.5)}, 100%, 50%, 1)`;
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
export default utils
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,32 @@
 | 
			
		|||
import { getSettings } from "./settings.js";
 | 
			
		||||
import { getVar } from "./gameInterface.js";
 | 
			
		||||
 | 
			
		||||
const hoveringTooltip = new (function() {
 | 
			
		||||
    let recentlyShown = false;
 | 
			
		||||
    this.display = () => {}; // this gets populated by the modified game script
 | 
			
		||||
    this.canvasPixelScale = 1;
 | 
			
		||||
    function handler(e) {
 | 
			
		||||
        if (!getSettings().hoveringTooltip || !getVar("gameState") || recentlyShown) return;
 | 
			
		||||
        let x, y;
 | 
			
		||||
        // https://stackoverflow.com/a/61732450
 | 
			
		||||
        if (e.type.includes(`touch`)) {
 | 
			
		||||
            const { touches, changedTouches } = e.originalEvent ?? e;
 | 
			
		||||
            const touch = touches[0] ?? changedTouches[0];
 | 
			
		||||
            x = touch.pageX;
 | 
			
		||||
            y = touch.pageY;
 | 
			
		||||
        } else if (e.type.includes(`mouse`)) {
 | 
			
		||||
            x = e.clientX;
 | 
			
		||||
            y = e.clientY;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        recentlyShown = true;
 | 
			
		||||
        try {
 | 
			
		||||
            this.display(this.canvasPixelScale * x, this.canvasPixelScale * y);
 | 
			
		||||
        } catch (e) { console.error(e) }
 | 
			
		||||
        // for better performance, reduce the tooltip display frequency to no more than once every 100 ms
 | 
			
		||||
        setTimeout(() => recentlyShown = false, 100);
 | 
			
		||||
    }
 | 
			
		||||
    document.getElementById("canvasA").addEventListener("mousemove", handler.bind(this));
 | 
			
		||||
    document.getElementById("canvasA").addEventListener("touchstart", handler.bind(this));
 | 
			
		||||
});
 | 
			
		||||
export default hoveringTooltip
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,10 @@
 | 
			
		|||
import { getSettings } from "./settings.js";
 | 
			
		||||
 | 
			
		||||
export const keybindFunctions = { setAbsolute: () => {}, setRelative: () => {} };
 | 
			
		||||
export const keybindHandler = key => {
 | 
			
		||||
    const keybindData = getSettings().attackPercentageKeybinds.find(keybind => keybind.key === key);
 | 
			
		||||
    if (keybindData === undefined) return false;
 | 
			
		||||
    if (keybindData.type === "absolute") keybindFunctions.setAbsolute(keybindData.value);
 | 
			
		||||
    else keybindFunctions.setRelative(keybindData.value);
 | 
			
		||||
    return true;
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,97 @@
 | 
			
		|||
export function KeybindsInput(containerElement) {
 | 
			
		||||
    const header = document.createElement("p");
 | 
			
		||||
    header.innerText = "Attack Percentage Keybinds";
 | 
			
		||||
    const keybindContainer = document.createElement("div");
 | 
			
		||||
    keybindContainer.className = "arrayinput";
 | 
			
		||||
    const keybindAddButton = document.createElement("button");
 | 
			
		||||
    keybindAddButton.innerText = "Add";
 | 
			
		||||
    containerElement.append(header, keybindContainer, keybindAddButton);
 | 
			
		||||
    this.container = keybindContainer;
 | 
			
		||||
    this.keys = [ "key", "type", "value" ];
 | 
			
		||||
    this.objectArray = [];
 | 
			
		||||
    this.addObject = function () {
 | 
			
		||||
        this.objectArray.push({ key: "", type: "absolute", value: 0.8 });
 | 
			
		||||
        this.displayObjects();
 | 
			
		||||
        keybindAddButton.scrollIntoView(false);
 | 
			
		||||
    };
 | 
			
		||||
    this.update = function (settings) {
 | 
			
		||||
        this.objectArray = settings.attackPercentageKeybinds;
 | 
			
		||||
        this.displayObjects();
 | 
			
		||||
    }
 | 
			
		||||
    keybindAddButton.addEventListener("click", this.addObject.bind(this));
 | 
			
		||||
    this.displayObjects = function () {
 | 
			
		||||
        // Clear the content of the container
 | 
			
		||||
        this.container.innerHTML = "";
 | 
			
		||||
        if (this.objectArray.length === 0) return this.container.innerText = "No custom attack percentage keybinds added";
 | 
			
		||||
        // Loop through the array and display input fields for each object
 | 
			
		||||
        for (var i = 0; i < this.objectArray.length; i++) {
 | 
			
		||||
            var objectDiv = document.createElement("div");
 | 
			
		||||
            // Create input fields for each key
 | 
			
		||||
            this.keys.forEach(function (key) {
 | 
			
		||||
                let inputField = document.createElement(key === "type" ? "select" : "input");
 | 
			
		||||
                if (key === "type") {
 | 
			
		||||
                    inputField.innerHTML = '<option value="absolute">Absolute</option><option value="relative">Relative</option>';
 | 
			
		||||
                    inputField.addEventListener("change", this.updateObject.bind(this, i, key));
 | 
			
		||||
                } else if (key === "key") {
 | 
			
		||||
                    inputField.type = "text";
 | 
			
		||||
                    inputField.setAttribute("readonly", "");
 | 
			
		||||
                    inputField.setAttribute("placeholder", "No key set");
 | 
			
		||||
                    inputField.addEventListener("click", this.startKeyInput.bind(this, i, key));
 | 
			
		||||
                } else { // key === "value"
 | 
			
		||||
                    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));
 | 
			
		||||
                }
 | 
			
		||||
                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);
 | 
			
		||||
            // Button to delete the object
 | 
			
		||||
            var deleteButton = document.createElement("button");
 | 
			
		||||
            deleteButton.textContent = "Delete";
 | 
			
		||||
            deleteButton.addEventListener("click", this.deleteObject.bind(this, i));
 | 
			
		||||
            // Append delete button to the object div
 | 
			
		||||
            objectDiv.appendChild(deleteButton);
 | 
			
		||||
            // Append the object div to the container
 | 
			
		||||
            this.container.appendChild(objectDiv);
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
    this.startKeyInput = function (index, property, event) {
 | 
			
		||||
        event.target.value = "Press any key";
 | 
			
		||||
        const handler = this.updateObject.bind(this, index, property);
 | 
			
		||||
        event.target.addEventListener('keydown', handler, { once: true });
 | 
			
		||||
        event.target.addEventListener("blur", () => {
 | 
			
		||||
            event.target.removeEventListener('keydown', handler);
 | 
			
		||||
            event.target.value = this.objectArray[index][property];
 | 
			
		||||
            //this.displayObjects();
 | 
			
		||||
        }, { once: true });
 | 
			
		||||
    };
 | 
			
		||||
    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" ? (
 | 
			
		||||
            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();
 | 
			
		||||
    };
 | 
			
		||||
    this.deleteObject = function (index) {
 | 
			
		||||
        // Remove the object from the array
 | 
			
		||||
        this.objectArray.splice(index, 1);
 | 
			
		||||
        // Display the updated input fields for objects
 | 
			
		||||
        this.displayObjects();
 | 
			
		||||
    };
 | 
			
		||||
    return this;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,30 @@
 | 
			
		|||
const fx_version = '0.6.5.6'; // FX Client Version
 | 
			
		||||
const fx_update = 'Oct 3'; // FX Client Last Updated
 | 
			
		||||
 | 
			
		||||
import settingsManager from './settings.js';
 | 
			
		||||
import { clanFilter, leaderboardFilter } from "./clanFilters.js";
 | 
			
		||||
import WindowManager from "./windowManager.js";
 | 
			
		||||
import donationsTracker from "./donationsTracker.js";
 | 
			
		||||
import winCounter from "./winCounter.js";
 | 
			
		||||
import playerList from "./playerList.js";
 | 
			
		||||
import gameScriptUtils from "./gameScriptUtils.js";
 | 
			
		||||
import hoveringTooltip from "./hoveringTooltip.js";
 | 
			
		||||
import { keybindFunctions, keybindHandler } from "./keybinds.js";
 | 
			
		||||
 | 
			
		||||
window.__fx = window.__fx || {};
 | 
			
		||||
const __fx = window.__fx;
 | 
			
		||||
__fx.version = fx_version + " " + fx_update;
 | 
			
		||||
 | 
			
		||||
__fx.settingsManager = settingsManager;
 | 
			
		||||
__fx.leaderboardFilter = leaderboardFilter;
 | 
			
		||||
__fx.utils = gameScriptUtils;
 | 
			
		||||
__fx.WindowManager = WindowManager;
 | 
			
		||||
__fx.keybindFunctions = keybindFunctions;
 | 
			
		||||
__fx.keybindHandler = keybindHandler;
 | 
			
		||||
__fx.donationsTracker = donationsTracker;
 | 
			
		||||
__fx.playerList = playerList;
 | 
			
		||||
__fx.hoveringTooltip = hoveringTooltip;
 | 
			
		||||
__fx.clanFilter = clanFilter;
 | 
			
		||||
__fx.wins = winCounter;
 | 
			
		||||
 | 
			
		||||
console.log('Successfully loaded FX Client');
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,43 @@
 | 
			
		|||
import { getVar } from "./gameInterface.js";
 | 
			
		||||
import { escapeHtml } from "./utils.js";
 | 
			
		||||
import donationsTracker from "./donationsTracker.js";
 | 
			
		||||
import WindowManager from "./windowManager.js";
 | 
			
		||||
 | 
			
		||||
const playerList = new (function () {
 | 
			
		||||
    const playersIcon = document.createElement('img');
 | 
			
		||||
    playersIcon.setAttribute('src', 'assets/players_icon.png');
 | 
			
		||||
    document.getElementById("playerlist_content").addEventListener("click", event => {
 | 
			
		||||
        const playerId = event.target.closest("tr[data-player-id]")?.getAttribute("data-player-id");
 | 
			
		||||
        if (!playerId) return;
 | 
			
		||||
        if (getVar("gIsTeamGame")) WindowManager.closeWindow("playerList"), donationsTracker.displayHistory(playerId);
 | 
			
		||||
    });
 | 
			
		||||
    this.display = function displayPlayerList(playerNames) {
 | 
			
		||||
        const gHumans = getVar("gHumans");
 | 
			
		||||
        const gLobbyMaxJoin = getVar("gLobbyMaxJoin");
 | 
			
		||||
        let listContent = `<h3>Players (${gHumans})</h3>`;
 | 
			
		||||
        for (let i = 0; i < gLobbyMaxJoin; i++) {
 | 
			
		||||
            if (i === gHumans) listContent += `<h3>Bots (${gLobbyMaxJoin - gHumans})</h3>`;
 | 
			
		||||
            listContent += `<tr data-player-id="${i}"><td><span class="color-light-gray">${i + 1}.</span> ${escapeHtml(playerNames[i])}</td></tr>`
 | 
			
		||||
        }
 | 
			
		||||
        document.getElementById("playerlist_content").innerHTML = listContent;
 | 
			
		||||
        document.getElementById("playerlist_content").setAttribute("class", getVar("gIsTeamGame") ? "clickable" : "");
 | 
			
		||||
        WindowManager.openWindow("playerList");
 | 
			
		||||
    }
 | 
			
		||||
    this.hoveringOverButton = false;
 | 
			
		||||
    this.drawButton = (canvas, x, y, size) => {
 | 
			
		||||
        canvas.fillRect(x, y, size, size);
 | 
			
		||||
        canvas.fillStyle = this.hoveringOverButton ? "#aaaaaaaa" : "#000000aa";
 | 
			
		||||
        canvas.clearRect(x + 1, y + 1, size - 2, size - 2);
 | 
			
		||||
        canvas.fillRect(x + 1, y + 1, size - 2, size - 2);
 | 
			
		||||
        canvas.fillStyle = "#ffffff";
 | 
			
		||||
        canvas.imageSmoothingEnabled = true;
 | 
			
		||||
        canvas.drawImage(playersIcon, x + 2, y + 2, size - 4, size - 4);
 | 
			
		||||
        canvas.imageSmoothingEnabled = false;
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
WindowManager.add({
 | 
			
		||||
    name: "playerList",
 | 
			
		||||
    element: document.getElementById("playerlist")
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default playerList
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,209 @@
 | 
			
		|||
import { KeybindsInput } from "./keybindsInput.js";
 | 
			
		||||
import winCounter from "./winCounter.js";
 | 
			
		||||
import WindowManager from "./windowManager.js";
 | 
			
		||||
 | 
			
		||||
window.__fx = window.__fx || {};
 | 
			
		||||
const __fx = window.__fx;
 | 
			
		||||
 | 
			
		||||
var settings = {
 | 
			
		||||
    //"fontName": "Trebuchet MS",
 | 
			
		||||
    //"showBotDonations": false,
 | 
			
		||||
    "displayWinCounter": true,
 | 
			
		||||
    "useFullscreenMode": false,
 | 
			
		||||
    "hoveringTooltip": true,
 | 
			
		||||
    //"hideAllLinks": false,
 | 
			
		||||
    "realisticNames": false,
 | 
			
		||||
    "showPlayerDensity": true,
 | 
			
		||||
    "coloredDensity": true,
 | 
			
		||||
    "densityDisplayStyle": "percentage",
 | 
			
		||||
    "highlightClanSpawns": false,
 | 
			
		||||
    //"customMapFileBtn": true
 | 
			
		||||
    "customBackgroundUrl": "",
 | 
			
		||||
    "attackPercentageKeybinds": [],
 | 
			
		||||
};
 | 
			
		||||
__fx.settings = settings;
 | 
			
		||||
const discontinuedSettings = [ "hideAllLinks", "fontName" ];
 | 
			
		||||
__fx.makeMainMenuTransparent = false;
 | 
			
		||||
 | 
			
		||||
/*var settingsGearIcon = document.createElement('img');
 | 
			
		||||
settingsGearIcon.setAttribute('src', 'assets/geari_white.png');*/
 | 
			
		||||
 | 
			
		||||
const settingsManager = new (function() {
 | 
			
		||||
    const settingsStructure = [
 | 
			
		||||
        { for: "displayWinCounter", type: "checkbox", label: "Display win counter",
 | 
			
		||||
            note: "The win counter tracks multiplayer solo wins (not in team games)" },
 | 
			
		||||
        { type: "button", text: "Reset win counter", action: winCounter.removeWins },
 | 
			
		||||
        { 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" },
 | 
			
		||||
        { for: "coloredDensity", type: "checkbox", label: "Colored density", note: "Display the density with a color between red and green depending on the density value" },
 | 
			
		||||
        { for: "densityDisplayStyle", type: "selectMenu", label: "Density value display style:", tooltip: "Controls how the territorial density value should be rendered", options: [
 | 
			
		||||
            { value: "percentage", label: "Percentage" },
 | 
			
		||||
            { value: "absoluteQuotient", label: "Value from 0 to 150 (BetterTT style)" }
 | 
			
		||||
        ]},
 | 
			
		||||
        { for: "highlightClanSpawns", type: "checkbox", label: "Highlight clan spawnpoints",
 | 
			
		||||
            note: "Increases the spawnpoint glow size for members of your clan" },
 | 
			
		||||
        { for: "customBackgroundUrl", type: "textInput", label: "Custom main menu background:", placeholder: "Enter an image URL here", tooltip: "A custom image to be shown as the main menu background instead of the currently selected map." },
 | 
			
		||||
        KeybindsInput
 | 
			
		||||
    ];
 | 
			
		||||
    const settingsContainer = document.querySelector(".settings .scrollable");
 | 
			
		||||
    var inputFields = {}; // (includes select menus)
 | 
			
		||||
    var checkboxFields = {};
 | 
			
		||||
    var customElements = [];
 | 
			
		||||
    settingsStructure.forEach(item => {
 | 
			
		||||
        if (typeof item === "function") {
 | 
			
		||||
            const container = document.createElement("div");
 | 
			
		||||
            customElements.push(new item(container));
 | 
			
		||||
            return settingsContainer.append(container);
 | 
			
		||||
        }
 | 
			
		||||
        const label = document.createElement("label");
 | 
			
		||||
        if (item.tooltip) label.title = item.tooltip;
 | 
			
		||||
        const isValueInput = item.type.endsWith("Input");
 | 
			
		||||
        const element = document.createElement(isValueInput || item.type === "checkbox" ? "input" : item.type === "selectMenu" ? "select" : "button");
 | 
			
		||||
        if (item.type === "textInput") element.type = "text";
 | 
			
		||||
        if (item.placeholder) element.placeholder = item.placeholder;
 | 
			
		||||
        if (isValueInput || item.type === "selectMenu") inputFields[item.for] = element;
 | 
			
		||||
        if (item.text) element.innerText = item.text;
 | 
			
		||||
        if (item.action) element.addEventListener("click", item.action);
 | 
			
		||||
        if (item.label) label.append(item.label + " ");
 | 
			
		||||
        if (item.note) {
 | 
			
		||||
            const note = document.createElement("small");
 | 
			
		||||
            note.innerText = item.note;
 | 
			
		||||
            label.append(document.createElement("br"), note)
 | 
			
		||||
        }
 | 
			
		||||
        if (item.options) item.options.forEach(option => {
 | 
			
		||||
            const optionElement = document.createElement("option");
 | 
			
		||||
            optionElement.setAttribute("value", option.value);
 | 
			
		||||
            optionElement.innerText = option.label;
 | 
			
		||||
            element.append(optionElement);
 | 
			
		||||
        });
 | 
			
		||||
        label.append(element);
 | 
			
		||||
        if (item.type === "checkbox") {
 | 
			
		||||
            element.type = "checkbox";
 | 
			
		||||
            const checkmark = document.createElement("span");
 | 
			
		||||
            checkmark.className = "checkmark";
 | 
			
		||||
            label.className = "checkbox";
 | 
			
		||||
            label.append(checkmark);
 | 
			
		||||
            checkboxFields[item.for] = element;
 | 
			
		||||
        } else label.append(document.createElement("br"));
 | 
			
		||||
        settingsContainer.append(label, document.createElement("br"));
 | 
			
		||||
    });
 | 
			
		||||
    this.save = function() {
 | 
			
		||||
        Object.keys(inputFields).forEach(function(key) { settings[key] = inputFields[key].value.trim(); });
 | 
			
		||||
        Object.keys(checkboxFields).forEach(function(key) { settings[key] = checkboxFields[key].checked; });
 | 
			
		||||
        this.applySettings();
 | 
			
		||||
        WindowManager.closeWindow("settings");
 | 
			
		||||
        discontinuedSettings.forEach(settingName => delete settings[settingName]);
 | 
			
		||||
        localStorage.setItem("fx_settings", JSON.stringify(settings));
 | 
			
		||||
        // should probably firgure out a way to do this without reloading - // You can't do it, localstorages REQUIRE you to reload
 | 
			
		||||
        window.location.reload();
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const fileInput = document.createElement("input");
 | 
			
		||||
    fileInput.type = "file";
 | 
			
		||||
    function handleFileSelect(event) {
 | 
			
		||||
        const input = event.target;
 | 
			
		||||
        /** @type {File} */
 | 
			
		||||
        const selectedFile = input.files[0];
 | 
			
		||||
        if (!selectedFile) return;
 | 
			
		||||
 | 
			
		||||
        input.removeEventListener("change", handleFileSelect);
 | 
			
		||||
        input.value = "";
 | 
			
		||||
        if (!selectedFile.name.endsWith(".json")) return alert("Invalid file format");
 | 
			
		||||
        const fileReader = new FileReader();
 | 
			
		||||
        fileReader.onload = function() {
 | 
			
		||||
            let result;
 | 
			
		||||
            try {    
 | 
			
		||||
                result = JSON.parse(fileReader.result);
 | 
			
		||||
                if (confirm("Warning: This will override all current settings, click \"OK\" to confirm")) __fx.settings = settings = result;
 | 
			
		||||
                localStorage.setItem("fx_settings", JSON.stringify(settings));
 | 
			
		||||
                window.location.reload();
 | 
			
		||||
            } catch (error) {
 | 
			
		||||
                alert("Error\n" + error)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        fileReader.readAsText(selectedFile);
 | 
			
		||||
    }
 | 
			
		||||
    this.importFromFile = function() {
 | 
			
		||||
        fileInput.click();
 | 
			
		||||
        fileInput.addEventListener('change', handleFileSelect);
 | 
			
		||||
    };
 | 
			
		||||
    // https://stackoverflow.com/a/34156339
 | 
			
		||||
    function saveFile(content, fileName, contentType) {
 | 
			
		||||
        var a = document.createElement("a");
 | 
			
		||||
        var file = new Blob([content], {type: contentType});
 | 
			
		||||
        a.href = URL.createObjectURL(file);
 | 
			
		||||
        a.download = fileName;
 | 
			
		||||
        a.click();
 | 
			
		||||
        URL.revokeObjectURL(a.href);
 | 
			
		||||
    }
 | 
			
		||||
    this.exportToFile = function() {
 | 
			
		||||
        saveFile(JSON.stringify(settings), 'FX_client_settings.json', 'application/json');
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    this.syncFields = function() {
 | 
			
		||||
        Object.keys(inputFields).forEach(function(key) { inputFields[key].value = settings[key]; });
 | 
			
		||||
        Object.keys(checkboxFields).forEach(function(key) { checkboxFields[key].checked = settings[key]; });
 | 
			
		||||
        customElements.forEach(element => element.update(settings));
 | 
			
		||||
    };
 | 
			
		||||
    this.resetAll = function() {
 | 
			
		||||
        if (!confirm("Are you Really SURE you want to RESET ALL SETTINGS back to the default?")) return;
 | 
			
		||||
        localStorage.removeItem("fx_settings");
 | 
			
		||||
        window.location.reload();
 | 
			
		||||
    };
 | 
			
		||||
    this.applySettings = function() {
 | 
			
		||||
        //setVarByName("bu", "px " + settings.fontName);
 | 
			
		||||
        if (settings.useFullscreenMode && document.fullscreenEnabled) {
 | 
			
		||||
            function tryEnterFullscreen() {
 | 
			
		||||
                if (document.fullscreenElement !== null) return;
 | 
			
		||||
                document.documentElement.requestFullscreen({ navigationUI: "hide" })
 | 
			
		||||
                    .then(() => { console.log('Fullscreen mode activated'); })
 | 
			
		||||
                    .catch((error) => { console.warn('Could not enter fullscreen mode:', error); });
 | 
			
		||||
            }
 | 
			
		||||
            document.addEventListener('mousedown', tryEnterFullscreen, { once: true });
 | 
			
		||||
            document.addEventListener('click', tryEnterFullscreen, { once: true });
 | 
			
		||||
        }
 | 
			
		||||
        if (settings.customBackgroundUrl !== "") {
 | 
			
		||||
            document.body.style.backgroundImage = "url(" + settings.customBackgroundUrl + ")";
 | 
			
		||||
            document.body.style.backgroundSize = "cover";
 | 
			
		||||
            document.body.style.backgroundPosition = "center";
 | 
			
		||||
        }
 | 
			
		||||
        __fx.makeMainMenuTransparent = settings.customBackgroundUrl !== "";
 | 
			
		||||
    };
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const openCustomBackgroundFilePicker = () => {
 | 
			
		||||
    const fileInput = document.getElementById("customBackgroundFileInput");
 | 
			
		||||
    fileInput.click();
 | 
			
		||||
    fileInput.addEventListener('change', handleFileSelect);
 | 
			
		||||
}
 | 
			
		||||
function handleFileSelect(event) {
 | 
			
		||||
    const fileInput = event.target;
 | 
			
		||||
    const selectedFile = fileInput.files[0];
 | 
			
		||||
    console.log(fileInput.files);
 | 
			
		||||
    console.log(fileInput.files[0]);
 | 
			
		||||
    if (selectedFile) {
 | 
			
		||||
        const fileUrl = URL.createObjectURL(selectedFile);
 | 
			
		||||
        console.log("File URL:", fileUrl);
 | 
			
		||||
        fileInput.value = "";
 | 
			
		||||
        fileInput.removeEventListener("change", handleFileSelect);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
WindowManager.add({
 | 
			
		||||
    name: "settings",
 | 
			
		||||
    element: document.querySelector(".settings"),
 | 
			
		||||
    beforeOpen: function() { settingsManager.syncFields(); }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
if (localStorage.getItem("fx_settings") !== null) {
 | 
			
		||||
    __fx.settings = settings = {...settings, ...JSON.parse(localStorage.getItem("fx_settings"))};
 | 
			
		||||
}
 | 
			
		||||
settingsManager.applySettings();
 | 
			
		||||
 | 
			
		||||
export default settingsManager;
 | 
			
		||||
export function getSettings() { return settings; };
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,4 @@
 | 
			
		|||
// https://stackoverflow.com/a/6234804
 | 
			
		||||
export function escapeHtml(unsafe) {
 | 
			
		||||
    return unsafe.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,12 @@
 | 
			
		|||
const winCounter = { count: 0, removeWins };
 | 
			
		||||
if (localStorage.getItem("fx_winCount") !== null) winCounter.count = localStorage.getItem("fx_winCount");
 | 
			
		||||
 | 
			
		||||
function removeWins() {
 | 
			
		||||
    if (confirm('Do you really want to reset your wins?')) {
 | 
			
		||||
        winCounter.count = 0;
 | 
			
		||||
        localStorage.removeItem('fx_winCount');
 | 
			
		||||
        alert("Successfully reset wins");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default winCounter
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,27 @@
 | 
			
		|||
var windows = {};
 | 
			
		||||
function add(newWindow) {
 | 
			
		||||
    windows[newWindow.name] = newWindow;
 | 
			
		||||
    windows[newWindow.name].isOpen = false;
 | 
			
		||||
};
 | 
			
		||||
function openWindow(windowName, ...args) {
 | 
			
		||||
    if (windows[windowName].isOpen === true) return;
 | 
			
		||||
    if (windows[windowName].beforeOpen !== undefined) windows[windowName].beforeOpen(...args);
 | 
			
		||||
    windows[windowName].isOpen = true;
 | 
			
		||||
    windows[windowName].element.style.display = null;
 | 
			
		||||
};
 | 
			
		||||
function closeWindow(windowName) {
 | 
			
		||||
    if (windows[windowName].isOpen === false) return;
 | 
			
		||||
    windows[windowName].isOpen = false;
 | 
			
		||||
    windows[windowName].element.style.display = "none";
 | 
			
		||||
    if (windows[windowName].onClose !== undefined) windows[windowName].onClose();
 | 
			
		||||
};
 | 
			
		||||
function closeAll() {
 | 
			
		||||
    Object.values(windows).forEach(function (windowObj) {
 | 
			
		||||
        closeWindow(windowObj.name);
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
document.getElementById("canvasA").addEventListener("mousedown", closeAll);
 | 
			
		||||
document.getElementById("canvasA").addEventListener("touchstart", closeAll, { passive: true });
 | 
			
		||||
document.addEventListener("keydown", event => { if (event.key === "Escape") closeAll(); });
 | 
			
		||||
 | 
			
		||||
export default { add, openWindow, closeWindow, closeAll }
 | 
			
		||||
| 
						 | 
				
			
			@ -64,10 +64,10 @@
 | 
			
		|||
        <div class="scrollable"></div>
 | 
			
		||||
        <hr>
 | 
			
		||||
        <footer>
 | 
			
		||||
            <button onclick="settingsManager.resetAll()">Reset Settings</button>
 | 
			
		||||
            <button onclick="settingsManager.save()">Save Settings</button>
 | 
			
		||||
            <button onclick="settingsManager.importFromFile()">Import</button>
 | 
			
		||||
            <button onclick="settingsManager.exportToFile()">Export</button>
 | 
			
		||||
            <button onclick="__fx.settingsManager.resetAll()">Reset Settings</button>
 | 
			
		||||
            <button onclick="__fx.settingsManager.save()">Save Settings</button>
 | 
			
		||||
            <button onclick="__fx.settingsManager.importFromFile()">Import</button>
 | 
			
		||||
            <button onclick="__fx.settingsManager.exportToFile()">Export</button>
 | 
			
		||||
        </footer>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="window scrollable selectable" id="playerlist" style="display: none;">
 | 
			
		||||
| 
						 | 
				
			
			@ -80,7 +80,7 @@
 | 
			
		|||
        <table><tbody id="donationhistory_content"></tbody></table>
 | 
			
		||||
    </div></span>
 | 
			
		||||
    <script src="variables.js?buildTimestamp"></script>
 | 
			
		||||
    <script src="fx_core.js?buildTimestamp"></script>
 | 
			
		||||
    <script src="fx.bundle.js?buildTimestamp"></script>
 | 
			
		||||
    <script src="game.js?buildTimestamp"></script>
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue