Compare commits

..

No commits in common. "b88129823195027dcd49eb63019c3c31eb315598" and "4b45939ce77d4a44cfcf8c430b5032a1e9cdf066" have entirely different histories.

33 changed files with 12856 additions and 2578 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,94 @@
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-Q96FGB3L05"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() { dataLayer.push(arguments); }
gtag('js', new Date());
gtag('config', 'G-Q96FGB3L05');
</script>
<meta charset="utf-8" />
<title>FX Client</title>
<meta name="description" content="Modified Version of Territorial.io - FX Client">
<meta name="keywords"
content="territorial.io,territory games,territorial io,map games,conquest games,conquest game,david tschacher,territorial,territory game,io game,io games,territory.io,territory io,territory games io">
<meta name="author" content="MohsenEMX, peshomir,orlemley1, David Tschacher">
<link rel="shortcut icon" href="favicon.ico" type="image/x-icon">
<meta name="og:image" content="https://fxclient.cf/logo.png" />
<meta property="og:url" content="https://fxclient.cf">
<meta property="og:type" content="website">
<meta property="og:title" content="FXclient.cf">
<meta property="og:description" content="Modified Version of Territorial.io - FX Client">
<meta property="og:image" content="https://fxclient.cf/logo.png">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="FX Client">
<meta name="twitter:description" content="Modified Version of Territorial.io - FX Client">
<meta name="twitter:image" content="https://fxclient.cf/logo.png">
<meta itemprop="name" content="FXclient.cf">
<meta itemprop="description" content="Modified Version of Territorial.io - FX Client">
<meta itemprop="image" content="https://fxclient.cf/logo.png">
<!-- FX Client CSS -->
<link rel="stylesheet" href="main.css">
<!-- Game CSS -->
<style>
html,
body {
overflow: hidden;
padding: 0;
margin: 0;
background: rgb(0, 0, 0);
touch-action: none;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
</style>
</head>
<body onload="aiCommand746(0);">
<canvas id="canvasA" width="128" height="128"></canvas>
<div class="window flex settings" style="display:none">
<h1>Settings</h1>
<div class="scrollable">
<label title="Name of the font to be used for rendering. For example: Arial, Georgia, sans-serif, serif, Comic Sans MS, ...">
Font name: <input id="settings_fontname" placeholder="Enter font name" value="Arial"></label><br>
<br><button onclick="removeWins()">Reset Wins Counter</button><br><br>
<label for="settings_donations_bots" class="checkbox">
Display donations from bots (applies to multiplayer bots only)
<input type="checkbox" id="settings_donations_bots"><span class="checkmark"></span>
</label><br>
<label for="settings_hidealllinks" class="checkbox">
Hide Links option also hides app store links
<input type="checkbox" id="settings_hidealllinks"><span class="checkmark"></span>
</label><br>
<label for="settings_realisticnames" class="checkbox">
Realistic Bot Names
<input type="checkbox" id="settings_realisticnames"><span class="checkmark"></span>
</label><br>
<label for="settings_custommapfileinput" class="checkbox">
Bring back the custom map file button after the creator removed it in 1.83.0
<input type="checkbox" id="settings_custommapfileinput"><span class="checkmark"></span>
</label>
</div>
<hr>
<footer>
<button onclick="settingsManager.resetAll()">Reset Settings</button>
<button onclick="settingsManager.save()">Save Settings</button>
</footer>
</div>
<div class="window scrollable" id="donationhistory" style="display:none">
<h1>Donation history for </h1>
<p id="donationhistory_note">Note: donations from bots are not shown here</p>
<p id="donationhistory_text"></p>
</div>
<script src="variables.js"></script>
<script src="game.js"></script>
</body>
</html>

View File

@ -0,0 +1,149 @@
.scrollable {
overflow-y: auto;
}
.window {
position : fixed;
background-color: rgba(0, 0, 0, 0.7);
width : 90%;
top : 0;
color : white;
font-family : 'Franklin Gothic Medium', 'Arial Narrow', Arial, sans-serif;
margin : auto;
margin-top : 20px;
right : 0;
left : 0;
padding : 15px;
box-sizing : border-box;
border-color : white;
border-style : solid;
border-width : 2px;
border-width : calc(0.15 * (1vw + 1vh));
font-size : 20px;
font-size : calc(14px + ((0.4 * (0.8vw + 1vh)) + 0.15rem));
max-height : 90%;
transition : 0.2s;
z-index : 10;
}
.window.flex {
display : flex;
flex-direction: column;
}
hr {
width: 100%;
}
.window button,
.window input {
background-color: rgba(0, 0, 0, 0.7);
color : white;
font-size : 20px;
font-size : 0.9em;
padding : 0.4rem;
transition : 0.2s;
border : 1px solid #fff;
border-radius : 5px;
}
h1 {
font-weight : normal;
margin-block-start: 0.5em;
margin-block-end : 0.5em;
transition : 0.2s;
}
canvas {
transition: 0.2s;
}
button:hover {
background-color: rgba(222, 222, 222, 0.52);
border-radius : 8px;
}
.window input:focus {
background-color: rgba(222, 222, 222, 0.36);
border-radius : 8px;
}
input#userna,
input#inputfilebtn {
transition: 0.2s
}
/* Checkbox */
label.checkbox {
display : block;
position : relative;
padding-left : 35px;
/*margin-bottom: 12px;*/
cursor : pointer;
}
label.checkbox input {
position: absolute;
opacity : 0;
cursor : pointer;
height : 0;
width : 0;
}
.checkmark {
position : absolute;
top : 0;
left : 0;
height : 25px;
width : 25px;
/*background-color: #eee;*/
background-color : rgb(255 255 255 / 70%);
}
label.checkbox:hover .checkmark {
background-color: #ccc;
}
label.checkbox input:checked~.checkmark {
/*background-color: #2196F3;*/
background-color: rgba(0, 255, 0, 0.5);
}
.checkmark::after {
content : "";
position: absolute;
display : none;
}
label.checkbox input:checked~.checkmark:after {
display: block;
}
label.checkbox .checkmark:after {
left : 9px;
top : 5px;
width : 5px;
height : 10px;
border : solid white;
border-width : 0 3px 3px 0;
-webkit-transform: rotate(45deg);
-ms-transform : rotate(45deg);
transform : rotate(45deg);
}
/* Custom Map Button */
::-webkit-file-upload-button {
color : white;
background-color: #120076;
border : 2px solid #fff;
top : 0;
bottom : 0;
padding-top : 20px;
padding-bottom : 20px;
padding-left : 20px;
padding-right : 20px;
font-family : 'Trebuchet MS', 'Arial Narrow', Arial, sans-serif;
text-align : center;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

261
build.js
View File

@ -1,64 +1,19 @@
import beautifier from 'js-beautify'; const beautify = require('js-beautify').js;
const { js: beautify } = beautifier; const fs = require('fs');
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"); if (!fs.existsSync("./build")) fs.mkdirSync("./build");
fs.cpSync("./static/", "./build/", { recursive: true }); fs.cpSync("./static/", "./build/", { recursive: true });
fs.cpSync("./assets/", "./build/assets/", { 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())); 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(); let script = fs.readFileSync('./game/latest.js', { encoding: 'utf8' }).replace("\n", "").trim();
const exposeVarsToGlobalScope = true;
// need to first remove the iife wrapper so the top-level functions aren't inlined
if (exposeVarsToGlobalScope && script.startsWith("\"use strict\"; (function () {") && script.endsWith("})();"))
script = script.slice("\"use strict\"; (function () {".length, -"})();".length);
if (exposeVarsToGlobalScope && script.startsWith("(function () {") && script.endsWith("})();"))
script = script.slice("(function () {".length, -"})();".length);
// for versions ^1.99.5.2
const minificationResult = UglifyJS.minify(script, {
"compress": { "arrows": false },
"mangle": false
});
if (minificationResult.error) console.log(minificationResult.error);
if (minificationResult.warnings) console.log(minificationResult.warnings);
script = minificationResult.code;
const replaceOne = (expression, replaceValue) => { const replaceOne = (expression, replaceValue) => {
const result = matchOne(expression); const result = matchOne(expression);
// this (below) works correctly because expression.lastIndex gets reset above in matchOne when there is no match // this (below) works correctly because expression.lastIndex gets reset above in matchOne when there is no match
script = script.replace(expression, replaceValue); script = script.replace(expression, replaceValue);
return result; return result;
} }
const replace = (...args) => script = script.replace(...args);
const matchOne = (expression) => { const matchOne = (expression) => {
const result = expression.exec(script); const result = expression.exec(script);
if (result === null) throw new Error("no match for: ") + expression; if (result === null) throw new Error("no match for: ") + expression;
@ -70,10 +25,10 @@ const escapeRegExp = (string) => string.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&')
//const dictionary = { __dictionaryVersion: '1.90.0 4 Feb 2024', playerId: 'bB', playerNames: 'hA', playerBalances: 'bC', playerTerritories: 'bj', gIsSingleplayer: 'fc', gIsTeamGame: 'cH' }; //const dictionary = { __dictionaryVersion: '1.90.0 4 Feb 2024', playerId: 'bB', playerNames: 'hA', playerBalances: 'bC', playerTerritories: 'bj', gIsSingleplayer: 'fc', gIsTeamGame: 'cH' };
//if (!script.includes(`"${dictionary.__dictionaryVersion}"`)) throw new Error("Dictionary is outdated."); //if (!script.includes(`"${dictionary.__dictionaryVersion}"`)) throw new Error("Dictionary is outdated.");
const dictionary = {}; let dictionary = {};
const matchDictionaryExpression = expression => { const matchDictionaryExpression = expression => {
const result = expression.exec(script); result = expression.exec(script);
if (result === null) throw new Error("no match for ") + expression; if (result === null) throw new Error("no match for ") + expression;
if (expression.exec(script) !== null) throw new Error("more than one match for: ") + expression; if (expression.exec(script) !== null) throw new Error("more than one match for: ") + expression;
for (let [key, value] of Object.entries(result.groups)) dictionary[key] = value; for (let [key, value] of Object.entries(result.groups)) dictionary[key] = value;
@ -84,19 +39,11 @@ const matchDictionaryExpression = expression => {
// and this matches "a=b+1;", the returned value will be the object: { var1: "a", var2: "b" } // and this matches "a=b+1;", the returned value will be the object: { var1: "a", var2: "b" }
const replaceRawCode = (/** @type {string} */ raw, /** @type {string} */ result, nameMappings) => { const replaceRawCode = (/** @type {string} */ raw, /** @type {string} */ result, nameMappings) => {
const { expression, groups } = generateRegularExpression(raw, false, nameMappings); const { expression, groups } = generateRegularExpression(raw, false, nameMappings);
let localizerCount = 0; let replacementString = result.replaceAll("$", "$$").replace(/\w+/g, match => {
let replacementString = result.replaceAll("$", "$$").replaceAll("__L()", "__L)").replaceAll("__L(", "__L,")
.replace(/\w+/g, match => {
// these would get stored as "___localizer1", "___localizer2", ...
if (match === "__L") match = "___localizer" + (++localizerCount);
return groups.hasOwnProperty(match) ? "$" + groups[match] : match; return groups.hasOwnProperty(match) ? "$" + groups[match] : match;
}); });
//console.log(replacementString); //console.log(replacementString);
let expressionMatchResult; const expressionMatchResult = replaceOne(expression, replacementString);
try { expressionMatchResult = replaceOne(expression, replacementString); }
catch (e) {
throw new Error("replaceRawCode match error:\n\n" + e + "\n\nRaw code: " + raw + "\n");
}
return Object.fromEntries(Object.entries(groups).map(([identifier, groupNumber]) => [identifier, expressionMatchResult[groupNumber]])); return Object.fromEntries(Object.entries(groups).map(([identifier, groupNumber]) => [identifier, expressionMatchResult[groupNumber]]));
} }
const matchRawCode = (/** @type {string} */ raw, nameMappings) => { const matchRawCode = (/** @type {string} */ raw, nameMappings) => {
@ -107,20 +54,11 @@ const matchRawCode = (/** @type {string} */ raw, nameMappings) => {
const generateRegularExpression = (/** @type {string} */ code, /** @type {boolean} */ isForDictionary, nameMappings) => { const generateRegularExpression = (/** @type {string} */ code, /** @type {boolean} */ isForDictionary, nameMappings) => {
const groups = {}; const groups = {};
let groupNumberCounter = 1; let groupNumberCounter = 1;
let localizerCount = 0; let raw = escapeRegExp(code).replace(isForDictionary ? /(?:@@)*(@?)(\w+)/g : /()(\w+)/g, (_match, modifier, word) => {
let raw = escapeRegExp(code).replaceAll("__L\\(\\)", "___localizer\\)")
// when there is a parameter, add a comma to separate it from the added number
.replaceAll("__L\\(", "___localizer,");
raw = raw.replace(isForDictionary ? /(?:@@)*(@?)(\w+)/g : /()(\w+)/g, (_match, modifier, word) => {
// if a substitution string for the "word" is specified in the nameMappings, use it // if a substitution string for the "word" is specified in the nameMappings, use it
if (nameMappings && nameMappings.hasOwnProperty(word)) return nameMappings[word]; if (nameMappings && nameMappings.hasOwnProperty(word)) return nameMappings[word];
// if the "word" is a number or is one of these specific words, ingore it // if the "word" is a number or is one of these specific words, ingore it
if (/^\d/.test(word) || ["return", "this", "var", "function", "Math"].includes(word)) return word; if (/^\d/.test(word) || ["return", "this", "var", "function", "Math"].includes(word)) return word;
// for easy localizer function matching
else if (word === "___localizer") {
groups[word + (++localizerCount)] = groupNumberCounter++;
return "\\b(L\\(\\d+)"; // would match "L(123", "L(50" and etc. when using "__L("
}
else if (groups.hasOwnProperty(word)) return "\\" + groups[word]; // regex numeric reference to the group else if (groups.hasOwnProperty(word)) return "\\" + groups[word]; // regex numeric reference to the group
else { else {
groups[word] = groupNumberCounter++; groups[word] = groupNumberCounter++;
@ -132,19 +70,20 @@ const generateRegularExpression = (/** @type {string} */ code, /** @type {boolea
} }
[ [
/,this\.(?<gIsTeamGame>\w+)=this\.\w+<7\|\|9===this\.\w+,/g, ///=(?<gIsSingleplayer>\w+)\?"Players":"Bots"/g,
/=function\((\w+),(\w+),\w+\){\1===(?<game>\w+)\.(?<playerId>\w+)\?\w+\(175," "\+\w+\(\d+,\[(?<playerData>\w+)\.(?<playerNames>\w+)\[\2\]\]\)\+": ",1001,\2,\w+\(/g, /,(?<gIsTeamGame>\w+)=\(\w+=\w+\)<7\|\|9===\w+,/g,
/function \w+\(\)\{if\(2===(?<game>\w+)\.(?<gameState>\w+)\)return 1;\w+\.\w+\(\),\1\.\2=2,\1\.\w+=\1.\w+\}/g, /=function\((\w+),(\w+),\w+\){\1===(?<playerId>\w+)\?\w+\(175,\w+\.\w+\(18,\[(?<playerNames>\w+)\[\2\]\]\),1001,\2,\w+\(/g,
/(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, // this one broke in 1.91.3 /{\w+===(?<playerId>\w+)\?\w+\(175," Message to "/g,
/\w+\.font=(?<fontGeneratorFunction>\w+\.\w+\.\w+)\(1,\.39\*this\.\w+\),/g /\w+\.\w+\((\w+)\)\?\w+\.\w+\(\1\)\?(\w+)=(\w+\.\w+)\(13,\[\2\]\):\(\w+=\w+\.\w+\(\1\),\2=\3\(14,\[(?<playerNames>\w+)\[(\w+)\],(\w+\.\w+\.\w+\()(?<playerBalances>\w+)\[\5\]\),\6(?<playerTerritories>\w+)\[\5\]\),\2\]\),\w+=!0\):\2=/g,
// this one also broke in 1.91.3 /,\w+="Player: "\+(?<playerNames>\w+)\[\w+\],\w+=\(\w\+=" Balance: "\+\w+\.\w+\((?<playerBalances>\w+)\[\w+\]\)\)\+\(" Territory: "\+\w+\.\w+\((?<playerTerritories>\w+)\[\w+\]\)\)\+\(" Coords: "/g,
///\((?<uiOffset>\w+)=Math\.floor\(\(\w+\?\.0114:\.01296\)\*\w+\)\)/g,
/(function \w+\((\w+),(\w+),(\w+),(\w+),(\w+)\){\6\.fillText\((?<playerNames>\w+)\[\2\],\4,\5\)),(\2<(?<gHumans>\w+)&&2!==(?<playerStates>\w+)\[)/g,
/,\w+=512,(?<gLobbyMaxJoin>\w+)=\w+,(?<gIsSingleplayer>\w+)&&\(\1=\w+\.\w+\(\)\),\w+=\1-\w+,\w+=0,/g
].forEach(matchDictionaryExpression); ].forEach(matchDictionaryExpression);
const rawCodeSegments = [ const rawCodeSegments = [
`aR.f1(fy)?aR.fB(fy)?a0z=__L([a0z]):(player=aR.fA(fy),oM=__L([b1.t9.xw(@playerData.@rawPlayerNames[player],b1.kx.l2(0,10),150)])+" ",a0z=(oM+=__L([b1.l5.l6(playerData.@playerBalances[player])])+" ")+(__L([b1.l5.l6(playerData.@playerTerritories[player])])+" ")+`, "[0]=aV.nU[70],a0T[1]=@gIsSingleplayer?aV.nU[71]:aV.nU[72],",
"this.@gIsSingleplayer?this.@gLobbyMaxJoin=@SingleplayerMenu.@getSingleplayerPlayerCount():this.gLobbyMaxJoin=this.@gMaxPlayers,this.@gBots=this.gLobbyMaxJoin-this.@gHumans,this.sg=0,", "?(this.gB=Math.floor(.0536*aK.fw),g5=aK.g5-4*@uiSizes.@gap-this.gB):"
"[0]=__L(),@strs[1]=@game.@gIsSingleplayer?__L():__L(),",
"?(this.gB=Math.floor(.066*aK.fw),g5=aK.g5-4*@uiSizes.@gap-this.gB):",
`for(a0L=new Array(@game.@gMaxPlayers),a0A.font=a07,@i=game.gMaxPlayers-1;0<=i;i--)a0L[i]=i+1+".",@playerData.@playerNames[i]=aY.qW.tm(playerData.@rawPlayerNames[i],a07,a0W),a0K[i]=Math.floor(a0A.measureText(playerData.playerNames[i]).width);`,
] ]
rawCodeSegments.forEach(code => { rawCodeSegments.forEach(code => {
@ -153,14 +92,168 @@ rawCodeSegments.forEach(code => {
matchDictionaryExpression(expression); matchDictionaryExpression(expression);
}); });
applyPatches({ replace, replaceOne, replaceRawCode, dictionary, matchOne, matchRawCode, escapeRegExp }); fs.writeFileSync("./build/fx_core.js", `const dictionary = ${JSON.stringify(dictionary)};\n` + fs.readFileSync("./build/fx_core.js").toString());
await buildClientCode(); // Replace assets
// the dictionary should maybe get embedded into one of the files in the bundle const assets = require('./assets.js');
fs.writeFileSync("./build/fx.bundle.js", `const dictionary = ${JSON.stringify(dictionary)};\n` + fs.readFileSync("./build/fx.bundle.js").toString()); replaceOne(/(\(4,"crown",4,")[^"]+"\),/g, "$1" + assets.crownIcon + "\"),");
replaceOne(/(\(6,"territorial\.io",6,")[^"]+"\),/g, "$1" + assets.fxClientLogo + "\"),");
/*// Add FXClient menu item in "More" menu
// match },ug[0][5]={name:a79,id:5,mf:90,oU:0,e8:0},
replaceOne(/(},(\w+\[0\])\[\d+\]={(\w+):\w+,(\w+):\d+,(\w+):90,(\w+):0,(\w+):0},)/g,
'$1$2.push({$3:"FX Client v" + fx_version + " " + fx_update, $4: 20, $5: 0, $6: 0, $7: 70}),');
// Do not display hover effect on the last 2 items (territorial.io version and FX Client version) instead of only the last item
// match 0 === a9P ? ug[a9P].length - 1 : ug[a9P].length : 1,
replaceOne(/(0===(\w+)\?(\w+)\[\2\]\.length)-1:(\3\[\2\]\.length:1,)/g, "$1 - 2 : $4");*/
// Add FX Client version info to the game version window
replaceRawCode(`ar.aAx("MenuGameVersion")||ar.aAz(new aB3(" "+aV.nU[84],gameVersion+"<br><a href='"`,
`ar.aAx("MenuGameVersion")||ar.aAz(new aB3(" "+aV.nU[84],gameVersion + "<br><b>" + "FX Client v" + fx_version + " " + fx_update + "</b><br><a href='"`);
// Max size for custom maps: from 4096x4096 to 8192x8192
// TODO: test this; it might cause issues with new boat mechanics?
{ // Add Troop Density and Maximum Troops in side panel
/*const { groups: { valuesArray } } = replaceOne(/(,(?<labelsArray>\w+)\[\d\]="Interest",\2\[\d\]="Income",\2\[\d\]="Time"),(\w+=\w+-\w+\(\w+,100\),\((?<valuesArray>\w+)=new Array\(\2\.length\)\)\[0\]=\w+)/g,
'$1, $<labelsArray>.push("Max Troops", "Density"), $3'); // add labels*/
const { valuesArray } = replaceRawCode(`,labels[5]=aV.nU[76],labels[6]=aV.nU[77],labels[7]=aV.nU[78],a0Z=tn-eT(tn,100),(valuesArray=new Array(labels.length))[0]=io?`,
`,labels[5]=aV.nU[76],labels[6]=aV.nU[77],labels[7]=aV.nU[78],
labels.push("Max Troops", "Density"), // add labels
a0Z=tn-eT(tn,100),(valuesArray=new Array(labels.length))[0]=io?`);
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(${dictionary.playerTerritories}, ${dictionary.playerId}) `
+ `: utils.getDensity(${dictionary.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");
}
// Increment win counter on wins
replaceOne(/(=function\((\w+)\){)([^}]+),((\w+\(0),\w+<100\?(\w+\.\w+)\(11,(\[\w+\[\w+\]\])\):\6\(12,\7\),(3,\2,[^()]+?\))),(?<end>[^}]+},)/g,
`$1 if (${dictionary.playerId} === $2) wins_counter++, window.localStorage.setItem("fx_winCount", wins_counter); ` +
`$3, $4, $5, "Your Current Win Count is Now " + wins_counter, $8, $<end>`);
{ // Add settings button and win count
// render gear icon and win count
/*// cV.textAlign=cX,cV.textBaseline=cW,a03(a9Y.gb,a9Y.gc,a9Y.m5,a9Y.tD,ug[a9P][0].mf,ug[a9P][0].oU,ug[a9P][0].e8,0===yk,ug[a9P][0].name),a9O))
// l(A.f3, A.f4, A.hw, A.nI, z[0].f7, z[0].mx, z[0].cm, 0 === t, z[0].name, .6);
// cH.drawImage(settingsGearIcon,A.f3-A.hw/2,A.f4,A.nI,A.nI);
// cH.font = bt + Math.floor(A.nI * 0.4) + bu;
// cH.fillText("Win count: " + wins_counter, Math.floor(A.f3 + A.hw / 2), Math.floor((A.f4 + A.nI / 2) * 2.1));
const { groups } = replaceOne(/((?<canvas>\w+)\.textAlign=\w+,\2\.textBaseline=\w+,\w+\((?<x>(?<coords>\w+).\w+),(?<y>\4.\w+),(?<w>\4.\w+),(?<h>\4.\w+),[^)]+\)),(?<end>(?<isMenuOpened>\w+)\)\))/g, '$1, ' +
'$<canvas>.imageSmoothingEnabled = true, ' +
'$<canvas>.drawImage(settingsGearIcon, $<x>-$<w>/2, $<y>, $<h>, $<h>), ' +
'$<canvas>.imageSmoothingEnabled = false, ' +
'$<canvas>.font = "bold " + Math.floor($<h> * 0.4) + "px " + settings.fontName, ' +
'(settings.displayWinCounter && !$<isMenuOpened> && $<canvas>.fillText("Win count: " + wins_counter, Math.floor($<x> + $<w> / 2), Math.floor(($<y> + $<h> / 2) * 2.1))), ' +
'$<end>');*/
replaceRawCode(`,fy=aV.nU[80],fontSize=.65*height,canvas.font=aY.g0.g1(1,fontSize),canvas.fillStyle="rgba("+gR+","+tD+","+hj+",0.6)",canvas.fillRect(x,y,width,height),`,
`,fy=aV.nU[80],fontSize=.65*height,
canvas.imageSmoothingEnabled = true,
canvas.drawImage(settingsGearIcon, x - width / 2, y, height, height),
canvas.imageSmoothingEnabled = false,
(settings.displayWinCounter && ( canvas.font = aY.g0.g1(1, Math.floor(height * 0.4)), canvas.fillText("Win count: " + wins_counter, Math.floor(x + width / 2), Math.floor((y + height / 2) * 2)) ) ),
canvas.font=aY.g0.g1(1,fontSize),canvas.fillStyle="rgba("+gR+","+tD+","+hj+",0.6)",canvas.fillRect(x,y,width,height),`);
// handle settings button click
/*replaceOne(/(this\.\w+=function\((?<mouseX>\w+),(?<mouseY>\w+)\){[^}]+?)if\((?<coordsGet>\w+=\w+\(\)),(?<isMenuOpened>\w+)\)(?<end>{for\([^}]+"Lobby ")/g,
'$1 $<coordsGet>; ' +
`var gearIconX = ${groups.x}-${groups.w}/2; ` +
// if (y > (C.f3-C.hw/2) && y < ((C.f3-C.hw/2)+C.nI) && A > C.f4 && A < (C.f4 + C.nI)) WindowManager.openWindow("settings");
`if ($<mouseX> > gearIconX && $<mouseX> < (gearIconX+${groups.h}) && $<mouseY> > ${groups.y} && $<mouseY> < (${groups.y}+${groups.h})) return WindowManager.openWindow("settings"); ` +
'if ($<isMenuOpened>) $<end>');*/
replaceRawCode(`(q6=Math.floor((b7.cv.fv()?.145:.09)*aK.fw),gap=Math.floor(.065*(b7.cv.fv()?.53:.36)*aK.fw),gap=aK.g5-q6-gap,jd=b0.gap,q6=Math.floor(.35*q6),gap<=mouseX&&mouseY<jd+q6&&ar.v2())`,
`(q6=Math.floor((b7.cv.fv()?.145:.09)*aK.fw),gap=Math.floor(.065*(b7.cv.fv()?.53:.36)*aK.fw),gap=aK.g5-q6-gap,jd=b0.gap,q6=Math.floor(.35*q6),
(gap <= mouseX && mouseY < jd + q6 && (ar.v2(), true)) || (mouseX >= gap - q6 / 0.7 && mouseY < jd + q6 && WindowManager.openWindow("settings"))
)`);
}
{ // Keybinds
// match required variables
const { 0: match, groups: { attackBarObject, setRelative } } = matchOne(/:"."===(\w+\.key)\?(?<attackBarObject>\w+)\.(?<setRelative>\w+)\(31\/32\):"."===\1\?\2\.\3\(32\/31\):/g,);
// create a setAbsolutePercentage function on the attack percentage bar object,
// 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`);
// 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");
}
// Set the default font to Trebuchet MS
script = script.replace(/sans-serif"/g, 'Trebuchet MS"');
// 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;")
// 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}")
// Clear canvas background if a custom background is being used
replaceOne(/(this\.\w+=function\(\){var (\w+),(\w+);)(\w+\.\w+\?\([^()]+setTransform\(\3=\2<\3\?\3:\2,0,0,\3,(?:Math\.floor\(\([^)]+\)\/2\)[,)]){2},(?:[^)]+\),){2}[^)]+\):(?<canvas>\w+)\.fillStyle=\w+\.\w+,\5\.fillRect\((?<wholeCanvas>0,0,\w+\.\w+,\w+\.\w+)\)}})/g,
'$1 if (makeMainMenuTransparent) $<canvas>.clearRect($<wholeCanvas>); else $4')
// Track donations
replaceOne(/(this\.\w+=function\((\w+),(\w+)\)\{)(\2===\w+&&\(\w+\.\w+\((\w+\.\w+)\[0\],\5\[1\],\3\),this\.(\w+)\[12\]\+=\5\[1\],this\.\6\[16\]\+=\5\[0\]\),\3===\w+&&\()/g,
"$1 donationsTracker.logDonation($2, $3, $5[0]); $4")
// Display donations for a player when clicking on them in the leaderboard
// match , 0 !== dG[x]) && fq.hB(x, 800, false, 0),
replaceOne(/,(0!==\w+\[(\w+)\]\)&&\w+\.\w+\(\2,800,!1,0\),)/g,
`, ${dictionary.gIsTeamGame} && donationsTracker.displayHistory($2, ${dictionary.playerNames}, ${dictionary.gIsSingleplayer}), $1`);
// Reset donation history when a new game is started
replaceOne(new RegExp(`,${dictionary.playerBalances}=new Uint32Array\\(\\w+\\),`, "g"), "$& donationsTracker.reset(), ");
{ // Player list
// 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+\.\w+\[65\],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)`
// Handle player list button mouseDown
replaceOne(/(this\.\w+=function\((?<x>\w+),(?<y>\w+)\){return!!\w+\(\2,\3\))&&(\(\w+=\w+\.\w+,)/g,
`$1 && (${buttonBoundsCheck} && playerList.display(${dictionary.playerNames}), true) && $4`);
// Handle player list button hover
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,
`$1 if (${buttonBoundsCheck}) { playerList.hoveringOverButton === false && (playerList.hoveringOverButton = true, ${drawFunction}(), $<setRepaintNeeded>); } `
+ ` else { playerList.hoveringOverButton === true && (playerList.hoveringOverButton = false, ${drawFunction}(), $<setRepaintNeeded>); } $4`);
}
{ // Display density of other players
// Applies when the "Reverse Name/Balance" setting is off
const { groups: { settingsSwitchNameAndBalance } } = replaceOne(/(,(?<settingsSwitchNameAndBalance>\w+\.\w+\.\w+\[7\]\.\w+)\?(?<nameDrawingFunction>\w+)\(\w+,\w+,(?<x>\w+),(?<y>\w+)\+\.78\*(?<fontSize>\w+),(?<canvas>\w+)\)):(\7\.fillText\(\w+\.\w+\.\w+\(\w+\[(\w+)\]\),\4,\5\+\.78\*\6\))\)\)/g,
`$1 : ($8, settings.showPlayerDensity && (settings.coloredDensity && ($<canvas>.fillStyle = utils.textStyleBasedOnDensity($9)), $<canvas>.fillText(utils.getDensity($9), $<x>, $<y> + $<fontSize> * 1.5)) ) ) )`);
// Applies when the "Reverse Name/Balance" setting is on (default)
replaceOne(/(function \w+\((\w+),(?<fontSize>\w+),(?<x>\w+),(?<y>\w+),(?<canvas>\w+)\){)(\6\.fillText\((?<playerNames>\w+)\[\2\],\4,\5\)),(\2<(?<gHumans>\w+)&&2!==(?<playerStates>\w+)\[[^}]+)}/g,
`$1 var ___id = $2; $7, $9; ${settingsSwitchNameAndBalance} && settings.showPlayerDensity && (settings.coloredDensity && ($<canvas>.fillStyle = utils.textStyleBasedOnDensity(___id)), $<canvas>.fillText(utils.getDensity(___id), $<x>, $<y> + $<fontSize>)); }`);
}
// Disable built-in Territorial.io error reporting
replaceOne(/window\.addEventListener\("error",function (\w+)\((\w+)\){/g,
'$& window.removeEventListener("error", $1); return alert("Error:\\n" + $2.filename + " " + $2.lineno + " " + $2.colno + " " + $2.message);');
console.log('Removing ads...');
// Remove ads
script = script.replace('//api.adinplay.com/libs/aiptag/pub/TRT/territorial.io/tag.min.js','');
console.log("Formatting code..."); console.log("Formatting code...");
exposeVarsToGlobalScope = true;
if (exposeVarsToGlobalScope && script.startsWith("\"use strict\"; (function () {") && script.endsWith("})();"))
script = script.slice("\"use strict\"; (function () {".length, -"})();".length);
if (exposeVarsToGlobalScope && script.startsWith("(function () {") && script.endsWith("})();"))
script = script.slice("(function () {".length, -"})();".length);
script = beautify(script, { script = beautify(script, {
"indent_size": "1", "indent_size": "1",
"indent_char": "\t", "indent_char": "\t",

View File

@ -1,9 +1,9 @@
const downloadGame = () => new Promise(resolve => {
// Download game // Download game
// https://stackoverflow.com/a/11944984 // https://stackoverflow.com/a/11944984
import https from 'https'; // or 'https' for https:// URLs const https = require('https'); // or 'https' for https:// URLs
import fs from 'fs'; const fs = require('fs');
const downloadGame = () => new Promise(resolve => {
if (!fs.existsSync("./game")) fs.mkdirSync("./game"); if (!fs.existsSync("./game")) fs.mkdirSync("./game");
const file = fs.createWriteStream("./game/latest.html"); const file = fs.createWriteStream("./game/latest.html");
// Download the game's code from the website // Download the game's code from the website
@ -32,4 +32,4 @@ const request = https.get("https://territorial.io", function (response) {
}); });
}); });
}); });
export default downloadGame; module.exports = downloadGame;

View File

@ -1,4 +1,2 @@
import downloadGame from "./download.js"; console.log("Building FXClient");
console.log("Building FX Client"); require("./download.js")().then(() => require("./build.js"));
await downloadGame();
import("./build.js");

1482
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,7 +3,6 @@
"version": "0.5.3", "version": "0.5.3",
"description": "A modded territorial.io client", "description": "A modded territorial.io client",
"main": "index.js", "main": "index.js",
"type": "module",
"scripts": { "scripts": {
"build": "node index.js", "build": "node index.js",
"build-only": "node build.js" "build-only": "node build.js"
@ -19,10 +18,6 @@
}, },
"homepage": "https://github.com/fxclient/FXclient#readme", "homepage": "https://github.com/fxclient/FXclient#readme",
"dependencies": { "dependencies": {
"js-beautify": "^1.14.11", "js-beautify": "^1.14.11"
"uglify-js": "^3.19.2"
},
"devDependencies": {
"webpack": "^5.95.0"
} }
} }

View File

@ -1,307 +0,0 @@
import assets from './assets.js';
export default ({ replace, replaceOne, replaceRawCode, dictionary, matchOne, matchRawCode, escapeRegExp }) => {
// Constants for easy usage of otherwise long variable access expressions
const dict = dictionary;
const playerId = `${dict.game}.${dict.playerId}`;
const rawPlayerNames = `${dict.playerData}.${dict.rawPlayerNames}`;
const gIsSingleplayer = `${dict.game}.${dict.gIsSingleplayer}`;
// Replace assets
replaceOne(/(\(4,"crown",4,")[^"]+"\),/g, "$1" + assets.crownIcon + "\"),");
replaceOne(/(\(6,"territorial\.io",6,")[^"]+"\),/g, "$1" + assets.fxClientLogo + "\"),");
replaceOne(/(\(22,"logo",8,")[^"]+"\)/g, "$1" + assets.smallLogo + "\")");
// 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 + "<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
replaceRawCode(`new k("🚀 New Game Update","The game has been updated! Please reload the game.",!0,[`,
`new k("🚀 New Game Update","The game has been updated! Please reload the game."
+ "<div style='border: white; border-width: 1px; border-style: solid; margin: 10px; padding: 5px;'><h2>FX Client is not yet compatible with the latest version of the game.</h2><p>Updates should normally be available within a few hours.<br>You can still use FX to play in singleplayer mode.</p></div>",!0,[`
);
// Max size for custom maps: from 4096x4096 to 8192x8192
// TODO: test this; it might cause issues with new boat mechanics?
{ // Add Troop Density and Maximum Troops in side panel
const { valuesArray } = replaceRawCode(`,labels[5]=__L(),labels[6]=__L(),labels[7]=__L(),(valuesArray=new Array(labels.length))[0]=game.io?`,
`,labels[5]=__L(),labels[6]=__L(),labels[7]=__L(),
labels.push("Max Troops", "Density"), // add labels
(valuesArray=new Array(labels.length))[0]=game.io?`);
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 ? __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");
}
// Increment win counter on wins
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})
__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)`);
{ // Add settings button and win count
// 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() { __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 (__fx.settings.displayWinCounter) {
const size = Math.floor(aD4.gA * 0.03);
ctx.font = ${dict.fontGeneratorFunction}(1, size);
ctx.fillStyle = "#ffffff";
const text = "Win count: " + __fx.wins.count;
const textLength = ctx.measureText(text).width;
ctx.textAlign = "left";
ctx.textBaseline = "middle";
ctx.fillText(text, ctx.canvas.width - textLength - size / 2, size);
};
ctx.imageSmoothingEnabled=!1;var iQ=a0.a4o("territorial.io"),kL=.84*aD4.gA/iQ.width;`)
/*// render gear icon and win count
replaceRawCode(`,fy=aV.nU[80],fontSize=.65*height,canvas.font=aY.g0.g1(1,fontSize),canvas.fillStyle="rgba("+gR+","+tD+","+hj+",0.6)",canvas.fillRect(x,y,width,height),`,
`,fy=aV.nU[80],fontSize=.65*height,
canvas.imageSmoothingEnabled = true,
canvas.drawImage(settingsGearIcon, x - width / 2, y, height, height),
canvas.imageSmoothingEnabled = false,
(settings.displayWinCounter && (
canvas.font = aY.g0.g1(1, Math.floor(height * 0.4)),
canvas.fillStyle = "#ffffff",
canvas.fillText("Win count: " + wins_counter, Math.floor(x + width / 2), Math.floor((y + height / 2) * 2))
)),
canvas.font=aY.g0.g1(1,fontSize),canvas.fillStyle="rgba("+gR+","+tD+","+hj+",0.6)",canvas.fillRect(x,y,width,height),`);
// handle settings button click
replaceRawCode(`(q6=Math.floor((b7.cv.fv()?.145:.09)*aK.fw),gap=Math.floor(.065*(b7.cv.fv()?.53:.36)*aK.fw),gap=aK.g5-q6-gap,jd=b0.gap,q6=Math.floor(.35*q6),gap<=mouseX&&mouseY<jd+q6&&ar.v2(1))`,
`(q6=Math.floor((b7.cv.fv()?.145:.09)*aK.fw),gap=Math.floor(.065*(b7.cv.fv()?.53:.36)*aK.fw),gap=aK.g5-q6-gap,jd=b0.gap,q6=Math.floor(.35*q6),
(gap <= mouseX && mouseY < jd + q6 && (ar.v2(1), true)) || (mouseX >= gap - q6 / 0.7 && mouseY < jd + q6 && WindowManager.openWindow("settings"))
)`);*/
}
{ // Keybinds
// match required variables
const { 0: match, groups: { attackBarObject, setRelative } } = matchOne(/:"."===(\w+\.key)\?(?<attackBarObject>\w+)\.(?<setRelative>\w+)\(31\/32\):"."===\1\?\2\.\3\(32\/31\):/g,);
// create a setAbsolutePercentage function on the attack percentage bar object,
// 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; }; "
+ "__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 (__fx.keybindHandler($<event>.key)) return; $3");
}
// Set the default font to Trebuchet MS
replace(/sans-serif"/g, 'Trebuchet MS"');
// 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 (__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: __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 (__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 __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} && __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"), "$& __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; __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,
`__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,
`__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`);
}
{ // Display density of other players
const r = matchRawCode(`bD.dO.data[7].value?a9W(i,jm,jk,jl,ctx):a9V(ctx,i,jm,jk,jl,a9S)))`);
const settingsSwitchNameAndBalance = `${r.bD}.${r.dO}.${r.data}[7].${r.value}`;
//console.log(settingsSwitchNameAndBalance);
// Applies when the "Reverse Name/Balance" setting is off
replaceRawCode("function a9V(ctx,i,fontSize,x,y,a9S){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))}",
`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} || __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} && __fx.settings.showPlayerDensity && (__fx.settings.coloredDensity && ($<canvas>.fillStyle = __fx.utils.textStyleBasedOnDensity(___id)), $<canvas>.fillText(__fx.utils.getDensity(___id), $<x>, $<y> + $<fontSize>)); }`);
}
{ // Leaderboard filter
// for the leaderboard draw function:
replaceRawCode("function drawFunction(){a0A.clearRect(0,0,a04,y9),a0A.fillStyle=aZ.lE,a0A.fillRect(0,0,a04,a0F),a0A.fillStyle=aZ.kZ,a0A.fillRect(0,a0F,a04,y9-a0F),leaderboardPositionsById[game.playerId]>=position&&a0Z(leaderboardPositionsById[game.playerId]-position,aZ.kw),0!==leaderboardPositionsById[game.playerId]&&0===position&&a0Z(0,aZ.lJ),-1!==a0P&&a0Z(a0P,aZ.kd),a0A.fillStyle=aZ.gF,a0A.fillRect(0,a0F,a04,1),a0A.fillRect(0,0,a04,b0.ur),a0A.fillRect(0,0,b0.ur,y9),a0A.fillRect(a04-b0.ur,0,b0.ur,y9),a0A.fillRect(0,y9-b0.ur,a04,b0.ur),",
`var leaderboardHasChanged = true;
this.playerPos = game.playerId;
__fx.leaderboardFilter.setUpdateFlag = () => leaderboardHasChanged = true;
function updateFilteredLb() {
if (!leaderboardHasChanged) return;
__fx.leaderboardFilter.filteredLeaderboard = __fx.leaderboardFilter.playersToInclude
.map(id => leaderboardPositionsById[id]).sort((a, b) => a - b);
leaderboardHasChanged = false;
this.playerPos = __fx.leaderboardFilter.filteredLeaderboard.indexOf(leaderboardPositionsById[game.playerId]);
}
function drawFunction() {
a0A.clearRect(0, 0, a04, y9),
a0A.fillStyle = aZ.lE,
a0A.fillRect(0, 0, a04, a0F),
a0A.fillStyle = aZ.kZ,
a0A.fillRect(0, a0F, a04, y9 - a0F);
if (__fx.leaderboardFilter.enabled) updateFilteredLb();
var playerPos = (__fx.leaderboardFilter.enabled
? this.playerPos
: leaderboardPositionsById[game.playerId]
);
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 - __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 - __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),
a0A.fillRect(0, y9 - b0.ur, a04, b0.ur),`)
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 (__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;
for (a0A.font = a07, aY.g0.textAlign(a0A, 0), hZ = windowHeight - eh; 0 <= hZ; hZ--) {
const pos = result[hZ + position];
if (pos !== undefined)
a0a(leaderboardArray[pos]), a0b(hZ, pos, leaderboardArray[pos]);
}
for (aY.g0.textAlign(a0A, 2), hZ = windowHeight - eh; 0 <= hZ; hZ--) {
const pos = result[hZ + position];
if (pos !== undefined)
a0a(leaderboardArray[pos]), a0c(hZ, leaderboardArray[pos]);
}
} else {
for (a0A.font = a07, aY.g0.textAlign(a0A, 0), hZ = windowHeight - eh; 0 <= hZ; hZ--)
a0a(leaderboardArray[hZ + position]), a0b(hZ, hZ + position, leaderboardArray[hZ + position]);
for (aY.g0.textAlign(a0A, 2), hZ = windowHeight - eh; 0 <= hZ; hZ--)
a0a(leaderboardArray[hZ + position]), a0c(hZ, leaderboardArray[hZ + position]);
}`)
// in the leaderboard resize handler: make space for the tab buttons at the bottom of the leaderboard
replaceRawCode(",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, __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]",
`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]; leaderboardHasChanged = true;`);
// handle clicking on a player in the leaderboard
replaceRawCode("var a0p=a0q(fJ);return ag.tQ()&&-1!==a0P&&(a0P=-1,a0Y(),b3.d1=!0),b3.dY-a0Q<350&&a0T===a0p&&-1!==(a0p=(a0p=yr(-1,a0p,windowHeight))!==windowHeight&&vU(x,y)?a0p:-1)&&(x=leaderboardArray[a0p+position],a0p===windowHeight-1&&leaderboardPositionsById[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 = (__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}, __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),",
`__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;
(function(gG, gH, fj) {
a3.ek(fj) || -1 === (gG = ao.fr.wq(gG, gH)) ? l.wp(fj) : l.wr(gG)
}(mouseX, mouseY, point))
}
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, __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 = __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)
: ctx.drawImage(km[game.gIsTeamGame?lV.i6[i]:i<game.gHumans?1:0],x,y))`)
// 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 = __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),`
)
}
// Invalid hostname detection avoidance
replaceRawCode(`,hostnameIsValid=0<=window.location.hostname.toLowerCase().indexOf("territorial.io"),`,
`,hostnameIsValid=true,`)
// Disable built-in Territorial.io error reporting
replaceOne(/window\.addEventListener\("error",function (\w+)\((\w+)\){/g,
'$& window.removeEventListener("error", $1); return alert("Error:\\n" + $2.filename + " " + $2.lineno + " " + $2.colno + " " + $2.message);');
console.log('Removing ads...');
// Remove ads
replace('//api.adinplay.com/libs/aiptag/pub/TRT/territorial.io/tag.min.js', '');
}

View File

@ -14,28 +14,27 @@
</p> </p>
## What is FX Client? ## What is FX Client?
FX Client is the first Territorial.io client, offering a better User Interface and better User Experience. It's basically Territorial.io but better. FX Client is the first Territorial.io client, targeting better User Interface and better User Experience, It's basically Territorial.io but better.
**You can use the latest version of the client here: https://fxclient.github.io/FXclient/** **You can use the latest version of the client here: https://fxclient.github.io/FXclient/**
## Features: ## Features:
1. It's 100% free and open source on Github 1. It's 100% free and open source on Github
2. It's ad-free and removes game's default ads. 2. It's Ad free and removes game's default ads.
3. It makes game look cooler, by replacing default assets with new ones. 3. It makes game look cooler, by replacing default assets with new ones.
4. Displays your troop density and maximum troops 4. Displays your troop density and maximum troops
5. Displays the density of players and bots 5. Displays the density of players and bots
6. Adds a "Clan" tab on the leaderboard, allowing you to easily see your clanmates 6. Adds a win counter
7. Hovering tooltip: makes the territory map information (normally visible on right click) be visible constantly (on hover) 7. Adds a player list
8. Adds a player list 8. Adds the ability to view the history of who donated to a player during the game by clicking on their name in the leaderboard or the player list
9. Adds the ability to view the history of who donated to a player during a team game by clicking on their name in the leaderboard or the player list 9. Can be installed as a PWA (progressive web app) ensuring maximum enjoyment on consoles, phones and even desktop devices
10. Adds a win counter
11. Can be installed as a PWA (progressive web app) ensuring maximum enjoyment on consoles, phones and even desktop devices
#### The client has a settings menu, from which you can: #### The client has a settings menu, from which you can:
12. Make fullscreen mode trigger automatically 10. Change the game font
13. Set a custom main menu background 11. Make fullscreen mode trigger automatically
14. Create custom attack percentage keybinds 12. Set a custom main menu background
13. Create custom attack percentage keybinds
## Building Locally ## Building Locally

View File

@ -1,102 +0,0 @@
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;
});
}
});

View File

@ -1,58 +0,0 @@
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;

403
src/fx_core.js 100644
View File

@ -0,0 +1,403 @@
const fx_version = '0.6.3.1'; // FX Client Version
const fx_update = 'Mar 30'; // 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 getVar = varName => window[dictionary[varName]];
// https://stackoverflow.com/a/6234804
function escapeHtml(unsafe) {
return unsafe.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
}
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: 1 });
this.displayObjects();
};
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"
inputField.type = "number";
inputField.setAttribute("step", "0.1");
inputField.addEventListener("input", this.updateObject.bind(this, i, key));
}
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 });
};
this.updateObject = function (index, property, event) {
if (index >= this.objectArray.length) return;
// Update the corresponding property of the object in the array
const value = property === "value" ? parseFloat(event.target.value) : property === "key" ? event.key : event.target.value;
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,
//"hideAllLinks": false,
"realisticNames": false,
"showPlayerDensity": true,
"coloredDensity": true,
"densityDisplayStyle": "percentage",
//"customMapFileBtn": true
"customBackgroundUrl": "",
"attackPercentageKeybinds": [],
};
const discontinuedSettings = [ "hideAllLinks", "fontName" ];
let makeMainMenuTransparent = false;
var settingsManager = new (function() {
const settingsStructure = [
//{ for: "fontName", type: "textInput", label: "Font name:", placeholder: "Enter font name", tooltip: "Name of the font to be used for rendering. For example: Arial, Georgia, sans-serif, serif, Comic Sans MS, ..."},
{ type: "button", text: "Reset Wins Counter", action: removeWins },
{ for: "displayWinCounter", type: "checkbox", label: "Display win counter" },
{ 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: "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: "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();
};
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;
}
});
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 playerNames = getVar("playerNames");
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(playerNames[historyItem[0]])}`;
else content += `Sent <span class="color-red">${historyItem[2]}</span> resources to ${escapeHtml(playerNames[historyItem[1]])}`;
content += "</td>";
row.innerHTML = content;
return row;
}
this.displayHistory = function displayDonationsHistory(playerID, playerNames = getVar("playerNames"), 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');

View File

@ -1,7 +0,0 @@
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]]
};

View File

@ -1,23 +0,0 @@
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

View File

@ -1,32 +0,0 @@
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

View File

@ -1,10 +0,0 @@
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;
};

View File

@ -1,97 +0,0 @@
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;
}

View File

@ -1,30 +0,0 @@
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');

View File

@ -1,43 +0,0 @@
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

View File

@ -1,209 +0,0 @@
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; };

View File

@ -1,4 +0,0 @@
// https://stackoverflow.com/a/6234804
export function escapeHtml(unsafe) {
return unsafe.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
}

View File

@ -1,12 +0,0 @@
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

View File

@ -1,27 +0,0 @@
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 }

View File

@ -2,14 +2,13 @@
<html lang="en"> <html lang="en">
<head> <head>
<!-- Google tag (gtag.js) --> <!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-WYYDMY13BG"></script> <!--<script async src="https://www.googletagmanager.com/gtag/js?id=G-Q96FGB3L05"></script>
<script> <script>
window.dataLayer = window.dataLayer || []; window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);} function gtag() { dataLayer.push(arguments); }
gtag('js', new Date()); gtag('js', new Date());
gtag('config', 'G-Q96FGB3L05');
gtag('config', 'G-WYYDMY13BG'); </script>-->
</script>
<meta charset="utf-8" /> <meta charset="utf-8" />
<title>FX Client</title> <title>FX Client</title>
<meta name="description" content="Modified Version of Territorial.io - FX Client"> <meta name="description" content="Modified Version of Territorial.io - FX Client">
@ -59,15 +58,58 @@
<body onload="aiCommand746(0);"> <body onload="aiCommand746(0);">
<canvas id="canvasA" width="128" height="128"></canvas> <canvas id="canvasA" width="128" height="128"></canvas>
<span><div class="window flex settings" style="display:none"> <div class="window flex settings" style="display:none">
<h1>Settings</h1> <h1>Settings</h1>
<div class="scrollable"></div> <div class="scrollable">
<!--<label title="Name of the font to be used for rendering. For example: Arial, Georgia, sans-serif, serif, Comic Sans MS, ...">
Font name: <input id="settings_fontname" placeholder="Enter font name" value="Arial"></label><br>
<br><button onclick="removeWins()">Reset Wins Counter</button><br><br>
<!- -<label for="settings_donations_bots" class="checkbox">
Display donations from bots in donation history viewer (applies to multiplayer only)
<input type="checkbox" id="settings_donations_bots"><span class="checkmark"></span>
</label><br>- ->
<label for="settings_displaywincounter" class="checkbox">
Display win counter
<input type="checkbox" id="settings_displaywincounter"><span class="checkmark"></span>
</label><br>
<label for="settings_usefullscreenmode" class="checkbox">
Use fullscreen mode<br>
<small>Note: fullscreen mode will trigger after you click anywhere on the page due to browser policy restrictions.</small>
<input type="checkbox" id="settings_usefullscreenmode"><span class="checkmark"></span>
</label><br>
<label for="settings_hidealllinks" class="checkbox">
Hide Links option also hides app store links
<input type="checkbox" id="settings_hidealllinks"><span class="checkmark"></span>
</label><br>
<label for="settings_realisticnames" class="checkbox">
Realistic Bot Names
<input type="checkbox" id="settings_realisticnames"><span class="checkmark"></span>
</label><br>
<label for="settings_showPlayerDensity" class="checkbox">
Show player density
<input type="checkbox" id="settings_showPlayerDensity"><span class="checkmark"></span>
</label><br>
<label title="Controls how the territorial density value should be rendered">
Density value display style: <select id="settings_densityDisplayStyle">
<option value="percentage">Percentage</option>
<option value="absoluteQuotient">Value from 0 to 150 (BetterTT style)</option>
</select></label><br><br>
<label title="A custom image to be shown in the main menu background instead of the currently selected map.">
Custom main menu background: <input id="settings_custombackgroundurl" placeholder="Enter an image URL here"></label>
<!- -<input type="file" id="customBackgroundFileInput" style="display:none;">
or <button onclick="openCustomBackgroundFilePicker()">Open a local file</button>- -><br><br>
<!- -<label for="settings_custommapfileinput" class="checkbox">
Bring back the custom map file button after the creator removed it in 1.83.0
<input type="checkbox" id="settings_custommapfileinput"><span class="checkmark"></span>
</label>- ->
<p>Attack Percentage Keybinds</p>
<div id="keybinds" class="arrayinput"></div>
<button id="keybindAddButton">Add</button>-->
</div>
<hr> <hr>
<footer> <footer>
<button onclick="__fx.settingsManager.resetAll()">Reset Settings</button> <button onclick="settingsManager.resetAll()">Reset Settings</button>
<button onclick="__fx.settingsManager.save()">Save Settings</button> <button onclick="settingsManager.save()">Save Settings</button>
<button onclick="__fx.settingsManager.importFromFile()">Import</button>
<button onclick="__fx.settingsManager.exportToFile()">Export</button>
</footer> </footer>
</div> </div>
<div class="window scrollable selectable" id="playerlist" style="display: none;"> <div class="window scrollable selectable" id="playerlist" style="display: none;">
@ -78,9 +120,9 @@
<h1>Donation history for </h1> <h1>Donation history for </h1>
<p id="donationhistory_note">Note: donations from bots are not shown here</p> <p id="donationhistory_note">Note: donations from bots are not shown here</p>
<table><tbody id="donationhistory_content"></tbody></table> <table><tbody id="donationhistory_content"></tbody></table>
</div></span> </div>
<script src="variables.js?buildTimestamp"></script> <script src="variables.js?buildTimestamp"></script>
<script src="fx.bundle.js?buildTimestamp"></script> <script src="fx_core.js?buildTimestamp"></script>
<script src="game.js?buildTimestamp"></script> <script src="game.js?buildTimestamp"></script>
</body> </body>
</html> </html>

View File

@ -28,7 +28,7 @@
border-width : 2px; border-width : 2px;
border-width : calc(0.15 * (1vw + 1vh)); border-width : calc(0.15 * (1vw + 1vh));
font-size : 20px; font-size : 20px;
font-size : calc(14px + ((0.5 * (1.1vw - 0.1vh)) + 0.14rem)); font-size : calc(14px + ((0.4 * (0.8vw + 1vh)) + 0.15rem));
max-height : 90%; max-height : 90%;
transition : 0.2s; transition : 0.2s;
z-index : 10; z-index : 10;
@ -78,7 +78,7 @@ textarea {
transition: 0.2s; transition: 0.2s;
} }
.window button:hover { button:hover {
background-color: rgba(222, 222, 222, 0.52); background-color: rgba(222, 222, 222, 0.52);
border-radius : 8px; border-radius : 8px;
} }