Compare commits

..

59 Commits

Author SHA1 Message Date
peshomir b881298231 Split the client-side code into modules
(Update v0.6.5.6)
2024-10-03 17:27:39 +03:00
peshomir 495a979eb0 Patch for the new update 2024-09-16 13:03:05 +03:00
peshomir de81ad1a8e Patch for the new update 2024-09-09 15:58:56 +03:00
peshomir e2ce2d86d3 Merge branch 'main' of https://github.com/fxclient/FXclient 2024-09-04 13:44:35 +03:00
peshomir 6b0dd736be Patch for the new update 2024-09-04 13:44:15 +03:00
peshomir 8a3ff4896c
Improve the description in readme.md 2024-08-28 18:33:07 +03:00
peshomir 388dfb7f0f Add automatic localizer function matching - Fix for v1.99.6.4 2024-08-26 14:31:24 +03:00
peshomir fafefddfb2 Patch for new update 2024-08-25 12:12:59 +03:00
peshomir 68d5ac07e0 Update v0.6.5.4 - Add a message on the "New game update" screen to clarify that FX Client is not yet up to date with the latest game version 2024-08-24 17:04:39 +03:00
peshomir 287f58db62 Fixes for game versions ^1.99.5.7 2024-08-24 15:58:30 +03:00
peshomir 8b01bb6642 Experimental fix for game version ^1.99.5.2 2024-08-18 16:33:41 +03:00
peshomir e030456063
Patch for the new update 2024-08-16 15:56:58 +03:00
peshomir e6cba935c4
Patch for the new update 2024-08-16 15:38:34 +03:00
peshomir 92f26a1b10
Patch for the new update 2024-08-16 14:13:47 +03:00
peshomir 3e6f447663
Patch for the new update 2024-08-16 13:57:42 +03:00
peshomir 4508d17dea Patch for the new update 2024-08-11 13:14:05 +03:00
peshomir cd484263e0 Fix for the the hovering tooltip on mobile, again 2024-08-04 09:51:50 +03:00
peshomir fd36f6b42c Fix hovering tooltip errors 2024-08-04 09:40:17 +03:00
peshomir 9f8669c94c Merge branch 'main' of https://github.com/fxclient/FXclient 2024-08-03 22:22:20 +03:00
peshomir 3bba13ff51 Update v0.6.5.3 - Fix hovering tooltip not working on mobile 2024-08-03 22:22:01 +03:00
peshomir 3c0a8fc4e6
Clean up index.html 2024-08-01 11:14:07 +03:00
peshomir 8d9d25f477
Patch for the new update 2024-07-27 19:59:59 +03:00
peshomir c8497d6016
Patch for the new update 2024-07-27 19:55:17 +03:00
peshomir 8a49b409a5 Bump version 2024-07-21 14:34:51 +03:00
peshomir e667601bb4 Fully activate invalid hostname detection avoidance 2024-07-21 14:21:46 +03:00
peshomir 1a04ef0376 Patch for new game version 2024-07-21 14:20:43 +03:00
peshomir 50ad959276
Experimental patch for new update 2024-07-15 15:59:40 +03:00
peshomir 4756c06cab
Patch for new update 2024-07-13 14:04:56 +03:00
peshomir 3662499a4f Update v0.6.5.1 - Highlight clan spawnpoints 2024-07-12 20:15:19 +03:00
peshomir d676eb492f Patch for the new game version 2024-07-10 12:06:05 +03:00
peshomir 2eff0268c3 Small patch for the new game version 2024-07-08 21:45:20 +03:00
peshomir 7fcb584843 Change UI font size calculation 2024-07-05 11:44:57 +03:00
peshomir d3214ed1c9 Update v0.6.5 - Settings import/export, main menu logo & assets improvements 2024-07-05 10:55:49 +03:00
peshomir 1dbcf8af69 Change probability of invalid hostname detection avoidance 2024-07-02 11:44:15 +03:00
peshomir dd8c596c03 Fixes for game version 1.96.1 2024-06-26 15:31:22 +03:00
peshomir 85cd9fe798 Update v0.6.4.8 - hovering tooltip performance improvement, partially implement fx client usage avoidance detection 2024-06-24 19:01:14 +03:00
peshomir 14c13ce1ca Bump version to 0.6.4.7 2024-06-20 16:23:46 +03:00
peshomir 8a279ac2e5 Fixes for game updates ^1.95.5 2024-06-20 16:22:27 +03:00
peshomir 7c0f0e0a2c Moved the code which patched the game's code to a separate file to clean up the build script 2024-06-11 22:14:16 +03:00
peshomir 44f3ebdb96 Convert build scripts to ES modules 2024-06-07 12:08:47 +03:00
peshomir 1ac752275c Move archives to a separate branch 2024-06-05 18:20:24 +03:00
peshomir acf79bcc9c Update v0.6.4.6 - Fix leaderboard filter bug, hovering tooltip improvements, update fixes 2024-06-05 10:47:09 +03:00
peshomir 8672f00ac1 Update v0.6.4.5 - Fix leaderboard filter desynchronization bug 2024-06-03 20:16:36 +03:00
peshomir 1014c1df67 Update v0.6.4.4; Fixed the leaderboard filter and donation history not being reset properly,
fixed the leaderboard tab buttons being unclickable on mobile unless you moved over them
2024-06-03 19:06:02 +03:00
peshomir cc7876639c Merge branch 'main' of https://github.com/fxclient/FXclient 2024-06-01 14:23:45 +03:00
peshomir 3db6d61262 Update v0.6.4.3 - Fixes for game versions ^1.94.6
Improved the build script code replacement errors: now the problematic raw code is shown too, previously it would just display a "no match" error for the regular expression;
Removed some old commented code from the build script;
In the build script, added constants for easy usage of otherwise long variable access expressions (like the playerId variable which is now a property of the "game" object, meaning you would have to reference the dictionary twice, which is long and repetitive; now you can just use the computed playerId constant)
2024-06-01 14:23:42 +03:00
Mohsen Taghavi 4d0797b73a
added new google analytics 2024-05-22 23:47:48 +03:30
peshomir 3537ffc9ad Update v0.6.4.2 - fix clan leaderboard parsing bug
- Changed most uses of playerNames to rawPlayerNames
- The leaderboard filter now uses the clan parsing implementation from the vanilla game
2024-05-22 21:48:44 +03:00
peshomir ad6f0e2f05 Update readme 2024-05-20 21:24:29 +03:00
peshomir 9e45327969 Update v0.6.4.1 - Small hotfix to resolve issues with the hovering tooltip 2024-05-20 18:38:54 +03:00
peshomir 0e4ecbb36f Update v0.6.4 - Clan leaderboard and hovering tooltip
- Added a leaderboard filter with tabs "All" and "Clan"
- Added the "hovering tooltip" (constantly displaying the territory information normally visible when right clicking on the map) and an option to toggle it in the settings
- The absolute keybinds' value is now displayed as a percentage
2024-05-20 17:09:29 +03:00
peshomir ce2e2468fd Update v0.6.3.3; Fixes to the win counter
Fixed the win counter not counting wins correctly,
disabled win tracking for singleplayer games,
added a clarification in the settings menu about which wins are actually tracked (only solo multiplayer wins),
and fixed the win counter text in the main menu becoming black when the emoji or color picker were opened.
2024-04-11 12:19:26 +03:00
peshomir a32302be5f Merge branch 'main' of https://github.com/fxclient/FXclient 2024-04-03 10:47:14 +03:00
peshomir 5ec3a8e2eb Update v0.6.3.2; Fixes for game version 1.93.8
Links to the FX Client Discord server and the Github repostory have been added to the "Game Version" page in the "More" menu
2024-04-03 10:47:11 +03:00
peshomir acf5da7ac0
Update deploy_github_pages.yml 2024-03-30 16:23:05 +02:00
peshomir 5aa22162a6 Merge branch 'main' of https://github.com/mohsenemx/FXclient 2024-03-30 15:56:27 +02:00
Mohsen Taghavi 53f04151cf
Update deploy_github_pages.yml 2024-03-30 16:19:01 +03:30
Mohsen Taghavi 00e4072017
Update deploy_github_pages.yml 2024-03-30 16:16:07 +03:30
Mohsen Taghavi 9391d3c255
Update deploy_github_pages.yml 2024-03-30 16:07:14 +03:30
33 changed files with 2578 additions and 12856 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

File diff suppressed because one or more lines are too long

View File

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

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

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

261
build.js
View File

@ -1,19 +1,64 @@
const beautify = require('js-beautify').js;
const fs = require('fs');
import beautifier from 'js-beautify';
const { js: beautify } = beautifier;
import UglifyJS from 'uglify-js';
import fs from 'fs';
import webpack from 'webpack';
import path from 'path';
import applyPatches from './patches.js';
if (!fs.existsSync("./build")) fs.mkdirSync("./build");
fs.cpSync("./static/", "./build/", { recursive: true });
fs.cpSync("./assets/", "./build/assets/", { recursive: true });
fs.cpSync("./src/fx_core.js", "./build/fx_core.js");
fs.writeFileSync("./build/index.html", fs.readFileSync("./build/index.html").toString().replace(/buildTimestamp/g, Date.now()));
const buildClientCode = () => new Promise((resolve, reject) => {
webpack({
mode: 'production',
entry: { fxClient: "./src/main.js" },
output: {
path: path.resolve(import.meta.dirname, 'build'),
filename: 'fx.bundle.js',
},
}, (err, stats) => {
if (err) {
if (err.details) console.error(err.details);
return reject(err);
}
const info = stats.toJson();
if (stats.hasWarnings()) console.warn(info.warnings);
if (stats.hasErrors()) {
console.error(info.errors);
reject("Webpack compilation error");
}
else resolve();
});
});
let script = fs.readFileSync('./game/latest.js', { encoding: 'utf8' }).replace("\n", "").trim();
const exposeVarsToGlobalScope = true;
// 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 result = matchOne(expression);
// this (below) works correctly because expression.lastIndex gets reset above in matchOne when there is no match
script = script.replace(expression, replaceValue);
return result;
}
const replace = (...args) => script = script.replace(...args);
const matchOne = (expression) => {
const result = expression.exec(script);
if (result === null) throw new Error("no match for: ") + expression;
@ -25,10 +70,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' };
//if (!script.includes(`"${dictionary.__dictionaryVersion}"`)) throw new Error("Dictionary is outdated.");
let dictionary = {};
const dictionary = {};
const matchDictionaryExpression = expression => {
result = expression.exec(script);
const result = expression.exec(script);
if (result === null) throw new Error("no 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;
@ -39,11 +84,19 @@ const matchDictionaryExpression = expression => {
// 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 { expression, groups } = generateRegularExpression(raw, false, nameMappings);
let replacementString = result.replaceAll("$", "$$").replace(/\w+/g, match => {
let localizerCount = 0;
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;
});
//console.log(replacementString);
const expressionMatchResult = replaceOne(expression, replacementString);
let expressionMatchResult;
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]]));
}
const matchRawCode = (/** @type {string} */ raw, nameMappings) => {
@ -54,11 +107,20 @@ const matchRawCode = (/** @type {string} */ raw, nameMappings) => {
const generateRegularExpression = (/** @type {string} */ code, /** @type {boolean} */ isForDictionary, nameMappings) => {
const groups = {};
let groupNumberCounter = 1;
let raw = escapeRegExp(code).replace(isForDictionary ? /(?:@@)*(@?)(\w+)/g : /()(\w+)/g, (_match, modifier, word) => {
let localizerCount = 0;
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 (nameMappings && nameMappings.hasOwnProperty(word)) return nameMappings[word];
// 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;
// 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 {
groups[word] = groupNumberCounter++;
@ -70,20 +132,19 @@ const generateRegularExpression = (/** @type {string} */ code, /** @type {boolea
}
[
///=(?<gIsSingleplayer>\w+)\?"Players":"Bots"/g,
/,(?<gIsTeamGame>\w+)=\(\w+=\w+\)<7\|\|9===\w+,/g,
/=function\((\w+),(\w+),\w+\){\1===(?<playerId>\w+)\?\w+\(175,\w+\.\w+\(18,\[(?<playerNames>\w+)\[\2\]\]\),1001,\2,\w+\(/g,
// this one broke in 1.91.3 /{\w+===(?<playerId>\w+)\?\w+\(175," Message to "/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
/,this\.(?<gIsTeamGame>\w+)=this\.\w+<7\|\|9===this\.\w+,/g,
/=function\((\w+),(\w+),\w+\){\1===(?<game>\w+)\.(?<playerId>\w+)\?\w+\(175," "\+\w+\(\d+,\[(?<playerData>\w+)\.(?<playerNames>\w+)\[\2\]\]\)\+": ",1001,\2,\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+),(?<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,
/\w+\.font=(?<fontGeneratorFunction>\w+\.\w+\.\w+)\(1,\.39\*this\.\w+\),/g
].forEach(matchDictionaryExpression);
const rawCodeSegments = [
"[0]=aV.nU[70],a0T[1]=@gIsSingleplayer?aV.nU[71]:aV.nU[72],",
"?(this.gB=Math.floor(.0536*aK.fw),g5=aK.g5-4*@uiSizes.@gap-this.gB):"
`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])])+" ")+`,
"this.@gIsSingleplayer?this.@gLobbyMaxJoin=@SingleplayerMenu.@getSingleplayerPlayerCount():this.gLobbyMaxJoin=this.@gMaxPlayers,this.@gBots=this.gLobbyMaxJoin-this.@gHumans,this.sg=0,",
"[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 => {
@ -92,168 +153,14 @@ rawCodeSegments.forEach(code => {
matchDictionaryExpression(expression);
});
fs.writeFileSync("./build/fx_core.js", `const dictionary = ${JSON.stringify(dictionary)};\n` + fs.readFileSync("./build/fx_core.js").toString());
applyPatches({ replace, replaceOne, replaceRawCode, dictionary, matchOne, matchRawCode, escapeRegExp });
// Replace assets
const assets = require('./assets.js');
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','');
await buildClientCode();
// the dictionary should maybe get embedded into one of the files in the bundle
fs.writeFileSync("./build/fx.bundle.js", `const dictionary = ${JSON.stringify(dictionary)};\n` + fs.readFileSync("./build/fx.bundle.js").toString());
console.log("Formatting code...");
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, {
"indent_size": "1",
"indent_char": "\t",

View File

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

View File

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

1482
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

307
patches.js 100644
View File

@ -0,0 +1,307 @@
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,27 +14,28 @@
</p>
## What is FX Client?
FX Client is the first Territorial.io client, targeting better User Interface and better User Experience, It's basically Territorial.io but better.
FX Client is the first Territorial.io client, offering a 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/**
## Features:
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.
4. Displays your troop density and maximum troops
5. Displays the density of players and bots
6. Adds a win counter
7. 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. Can be installed as a PWA (progressive web app) ensuring maximum enjoyment on consoles, phones and even desktop devices
6. Adds a "Clan" tab on the leaderboard, allowing you to easily see your clanmates
7. Hovering tooltip: makes the territory map information (normally visible on right click) be visible constantly (on hover)
8. Adds a 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
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:
10. Change the game font
11. Make fullscreen mode trigger automatically
12. Set a custom main menu background
13. Create custom attack percentage keybinds
12. Make fullscreen mode trigger automatically
13. Set a custom main menu background
14. Create custom attack percentage keybinds
## Building Locally

102
src/clanFilters.js 100644
View File

@ -0,0 +1,102 @@
import { getVar } from "./gameInterface.js";
export const leaderboardFilter = new (function() {
//this.playersToInclude = [0,1,8,20,24,30,32,42,50,69,200,400,500,510,511]; // for testing
this.playersToInclude = [];
this.tabLabels = ["ALL", "CLAN"];
// these get populated by the modified game code
this.filteredLeaderboard = [];
this.tabBarOffset = 0;
this.windowWidth = 0;
this.verticalClickThreshold = 1000;
this.hoveringOverTabs = false;
this.scrollToTop = () => {};
this.repaintLeaderboard = () => {};
this.setUpdateFlag = () => {};
this.parseClanFromPlayerName = () => { console.warn("parse function not set"); };
this.selectedTab = 0;
this.tabHovering = -1;
this.enabled = false;
//this.enabled = true;
this.drawTabs = function(canvas, totalWidth, verticalOffset, colorForSelectedTab) {
canvas.textBaseline = "middle";
canvas.textAlign = "center";
const tabWidth = totalWidth / this.tabLabels.length;
const textOffsetY = verticalOffset + this.tabBarOffset / 2;
//console.log(verticalOffset, this.tabBarOffset, textOffsetY);
this.tabLabels.forEach((label, index) => {
if (index !== 0) canvas.fillRect(tabWidth * index, verticalOffset, 1, this.tabBarOffset);
if (this.selectedTab === index) {
canvas.fillStyle = colorForSelectedTab;
canvas.fillRect(tabWidth * index, verticalOffset, tabWidth, this.tabBarOffset);
canvas.fillStyle = "rgb(255,255,255)";
}
if (this.tabHovering === index) {
canvas.fillStyle = "rgba(255,255,255,0.3)";
canvas.fillRect(tabWidth * index, verticalOffset, tabWidth, this.tabBarOffset);
canvas.fillStyle = "rgb(255,255,255)";
}
canvas.fillText(label, tabWidth * index + tabWidth / 2, textOffsetY);
});
}
this.setHovering = (isHovering, xRelative) => {
let repaintNeeded = false;
if (isHovering) {
const tab = Math.floor(xRelative / (this.windowWidth / this.tabLabels.length));
if (this.tabHovering !== tab) {
this.tabHovering = tab;
repaintNeeded = true;
}
}
if (isHovering !== this.hoveringOverTabs) {
this.hoveringOverTabs = isHovering;
if (isHovering === false) this.tabHovering = -1;
if (!isHovering) repaintNeeded = true;
}
if (repaintNeeded) this.repaintLeaderboard();
return isHovering;
}
this.handleMouseDown = (xRelative) => {
const tab = Math.floor(xRelative / (this.windowWidth / this.tabLabels.length));
if (this.selectedTab !== tab) {
this.selectedTab = tab;
if (this.selectedTab === 0) this.clearFilter();
else if (this.selectedTab === 1) {
this.filterByOwnClan();
this.setUpdateFlag();
}
this.repaintLeaderboard();
}
return true;
};
this.filterByOwnClan = () => {
this.playersToInclude = [];
const playerId = getVar("playerId");
const ownClan = this.parseClanFromPlayerName(getVar("rawPlayerNames")[playerId]);
getVar("rawPlayerNames").forEach((name, id) => {
if (id === playerId || this.parseClanFromPlayerName(name) === ownClan) this.playersToInclude.push(id);
});
this.enabled = true;
this.scrollToTop();
};
this.clearFilter = () => { this.enabled = false; }
this.reset = () => {
this.enabled = false;
this.selectedTab = 0;
clanFilter.refresh();
}
});
export const clanFilter = new (function() {
this.inOwnClan = new Array(512);
this.inOwnClan.fill(false);
this.refresh = () => {
const gHumans = getVar("gHumans");
const ownClan = leaderboardFilter.parseClanFromPlayerName(getVar("rawPlayerNames")[getVar("playerId")]);
if (ownClan === null) this.inOwnClan.fill(false);
else getVar("rawPlayerNames").forEach((name, id) => {
this.inOwnClan[id] = id < gHumans && leaderboardFilter.parseClanFromPlayerName(name) === ownClan;
});
}
});

View File

@ -0,0 +1,58 @@
import WindowManager from "./windowManager.js";
import { getVar } from "./gameInterface.js";
import { escapeHtml } from "./utils.js";
WindowManager.add({
name: "donationHistory",
element: document.querySelector("#donationhistory"),
beforeOpen: function(isSingleplayer) {
document.getElementById("donationhistory_note").style.display = (/*(settings.showBotDonations || isSingleplayer)*/ true ? "none" : "block");
},
onClose: function() { donationsTracker.openedWindowPlayerID = null; }
});
const donationsTracker = new (function(){
this.openedWindowPlayerID = null;
this.contentElement = document.querySelector("#donationhistory_content");
this.donationHistory = Array(512);
// fill the array with empty arrays with length of 3
//for (var i = 0; i < 512; i++) this.donationHistory.push([]); // not needed as .reset is called on game start
this.getHistoryOf = function(playerID) {
return this.donationHistory[playerID].toReversed();
}
this.reset = function() { for (var i = 0; i < 512; i++) this.donationHistory[i] = []; };
this.logDonation = function(senderID, receiverID, amount) {
const donationInfo = [senderID, receiverID, amount];
this.donationHistory[receiverID].push(donationInfo);
this.donationHistory[senderID].push(donationInfo);
if (this.openedWindowPlayerID === senderID || this.openedWindowPlayerID === receiverID) {
const indexOfNewItem = this.donationHistory[this.openedWindowPlayerID === senderID ? senderID : receiverID].length;
this.contentElement.prepend(generateTableRowItem(donationInfo, indexOfNewItem, this.openedWindowPlayerID, true));
}
};
function generateTableRowItem(historyItem, index, playerID, isNew) {
const rawPlayerNames = getVar("rawPlayerNames");
const row = document.createElement("tr");
if (isNew) row.setAttribute("class", "new");
let content = `<td><span class="color-light-gray">${index}.</span> `;
if (playerID === historyItem[1])
content += `Received <span class="color-green">${historyItem[2]}</span> resources from ${escapeHtml(rawPlayerNames[historyItem[0]])}`;
else content += `Sent <span class="color-red">${historyItem[2]}</span> resources to ${escapeHtml(rawPlayerNames[historyItem[1]])}`;
content += "</td>";
row.innerHTML = content;
return row;
}
this.displayHistory = function displayDonationsHistory(playerID, playerNames = getVar("rawPlayerNames"), isSingleplayer = getVar("gIsSingleplayer")) {
var history = donationsTracker.getHistoryOf(playerID);
console.log("History for " + playerNames[playerID] + ":");
console.log(history);
document.querySelector("#donationhistory h1").innerHTML = "Donation history for " + escapeHtml(playerNames[playerID]);
this.contentElement.innerHTML = "";
if (history.length > 0) history.forEach((historyItem, index) => {
this.contentElement.appendChild(generateTableRowItem(historyItem, history.length - index, playerID));
});
else this.contentElement.innerText = "Nothing to display";
this.openedWindowPlayerID = playerID;
WindowManager.openWindow("donationHistory", isSingleplayer);
}
});
export default donationsTracker;

View File

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

@ -0,0 +1,7 @@
const playerDataProperties = ["playerTerritories", "playerBalances", "rawPlayerNames"];
const gameObjectProperties = ["playerId", "gIsTeamGame", "gHumans", "gLobbyMaxJoin", "gameState", "gIsSingleplayer"];
export const getVar = varName => {
if (playerDataProperties.includes(varName)) return window[dictionary.playerData][dictionary[varName]];
if (gameObjectProperties.includes(varName)) return window[dictionary.game][dictionary[varName]];
return window[dictionary[varName]]
};

View File

@ -0,0 +1,23 @@
import { getSettings } from "./settings.js";
import { getVar } from "./gameInterface.js";
const utils = new (function() {
this.getMaxTroops = function(playerTerritories, playerID) { return (playerTerritories[playerID]*150).toString(); };
this.getDensity = function(playerID, playerBalances = getVar("playerBalances"), playerTerritories = getVar("playerTerritories")) {
if (getSettings().densityDisplayStyle === "percentage") return (((playerBalances[playerID] / ((playerTerritories[playerID] === 0 ? 1 : playerTerritories[playerID]) * 150)) * 100).toFixed(1) + "%");
else return (playerBalances[playerID] / (playerTerritories[playerID] === 0 ? 1 : playerTerritories[playerID])).toFixed(1);
};
this.isPointInRectangle = function(x, y, rectangleStartX, rectangleStartY, width, height) {
return x >= rectangleStartX && x <= rectangleStartX + width && y >= rectangleStartY && y <= rectangleStartY + height;
};
/** @param {CanvasRenderingContext2D} canvas @param {string} text */
this.fillTextMultiline = function(canvas, text, x, y, maxWidth) {
const lineHeight = parseInt(canvas.font.split(" ").find(part => part.endsWith("px")).slice(0, -2));
text.split("\n").forEach((line, index) => canvas.fillText(line, x, y + index * lineHeight, maxWidth));
}
this.textStyleBasedOnDensity = function(playerID) {
const playerBalances = getVar("playerBalances"), playerTerritories = getVar("playerTerritories");
return `hsl(${playerBalances[playerID] / (playerTerritories[playerID] * 1.5)}, 100%, 50%, 1)`;
}
});
export default utils

View File

@ -0,0 +1,32 @@
import { getSettings } from "./settings.js";
import { getVar } from "./gameInterface.js";
const hoveringTooltip = new (function() {
let recentlyShown = false;
this.display = () => {}; // this gets populated by the modified game script
this.canvasPixelScale = 1;
function handler(e) {
if (!getSettings().hoveringTooltip || !getVar("gameState") || recentlyShown) return;
let x, y;
// https://stackoverflow.com/a/61732450
if (e.type.includes(`touch`)) {
const { touches, changedTouches } = e.originalEvent ?? e;
const touch = touches[0] ?? changedTouches[0];
x = touch.pageX;
y = touch.pageY;
} else if (e.type.includes(`mouse`)) {
x = e.clientX;
y = e.clientY;
}
recentlyShown = true;
try {
this.display(this.canvasPixelScale * x, this.canvasPixelScale * y);
} catch (e) { console.error(e) }
// for better performance, reduce the tooltip display frequency to no more than once every 100 ms
setTimeout(() => recentlyShown = false, 100);
}
document.getElementById("canvasA").addEventListener("mousemove", handler.bind(this));
document.getElementById("canvasA").addEventListener("touchstart", handler.bind(this));
});
export default hoveringTooltip

10
src/keybinds.js 100644
View File

@ -0,0 +1,10 @@
import { getSettings } from "./settings.js";
export const keybindFunctions = { setAbsolute: () => {}, setRelative: () => {} };
export const keybindHandler = key => {
const keybindData = getSettings().attackPercentageKeybinds.find(keybind => keybind.key === key);
if (keybindData === undefined) return false;
if (keybindData.type === "absolute") keybindFunctions.setAbsolute(keybindData.value);
else keybindFunctions.setRelative(keybindData.value);
return true;
};

View File

@ -0,0 +1,97 @@
export function KeybindsInput(containerElement) {
const header = document.createElement("p");
header.innerText = "Attack Percentage Keybinds";
const keybindContainer = document.createElement("div");
keybindContainer.className = "arrayinput";
const keybindAddButton = document.createElement("button");
keybindAddButton.innerText = "Add";
containerElement.append(header, keybindContainer, keybindAddButton);
this.container = keybindContainer;
this.keys = [ "key", "type", "value" ];
this.objectArray = [];
this.addObject = function () {
this.objectArray.push({ key: "", type: "absolute", value: 0.8 });
this.displayObjects();
keybindAddButton.scrollIntoView(false);
};
this.update = function (settings) {
this.objectArray = settings.attackPercentageKeybinds;
this.displayObjects();
}
keybindAddButton.addEventListener("click", this.addObject.bind(this));
this.displayObjects = function () {
// Clear the content of the container
this.container.innerHTML = "";
if (this.objectArray.length === 0) return this.container.innerText = "No custom attack percentage keybinds added";
// Loop through the array and display input fields for each object
for (var i = 0; i < this.objectArray.length; i++) {
var objectDiv = document.createElement("div");
// Create input fields for each key
this.keys.forEach(function (key) {
let inputField = document.createElement(key === "type" ? "select" : "input");
if (key === "type") {
inputField.innerHTML = '<option value="absolute">Absolute</option><option value="relative">Relative</option>';
inputField.addEventListener("change", this.updateObject.bind(this, i, key));
} else if (key === "key") {
inputField.type = "text";
inputField.setAttribute("readonly", "");
inputField.setAttribute("placeholder", "No key set");
inputField.addEventListener("click", this.startKeyInput.bind(this, i, key));
} else { // key === "value"
const isAbsolute = this.objectArray[i].type === "absolute";
inputField.type = isAbsolute ? "text" : "number";
if (isAbsolute) inputField.addEventListener("click", this.convertIntoNumberInput.bind(this, i, key), { once: true });
else inputField.setAttribute("step", "0.1");
inputField.addEventListener("input", this.updateObject.bind(this, i, key));
}
if (key === "value" && this.objectArray[i].type === "absolute")
inputField.value = this.objectArray[i][key] * 100 + "%";
else inputField.value = this.objectArray[i][key];
// Append input field to the object div
objectDiv.appendChild(inputField);
}, this);
// Button to delete the object
var deleteButton = document.createElement("button");
deleteButton.textContent = "Delete";
deleteButton.addEventListener("click", this.deleteObject.bind(this, i));
// Append delete button to the object div
objectDiv.appendChild(deleteButton);
// Append the object div to the container
this.container.appendChild(objectDiv);
}
};
this.startKeyInput = function (index, property, event) {
event.target.value = "Press any key";
const handler = this.updateObject.bind(this, index, property);
event.target.addEventListener('keydown', handler, { once: true });
event.target.addEventListener("blur", () => {
event.target.removeEventListener('keydown', handler);
event.target.value = this.objectArray[index][property];
//this.displayObjects();
}, { once: true });
};
this.convertIntoNumberInput = function (index, property, event) {
event.target.value = event.target.value.slice(0, -1);
event.target.type = "number";
event.target.addEventListener("blur", () => {
//event.target.value = this.objectArray[index][property];
this.displayObjects();
}, { once: true });
};
this.updateObject = function (index, property, event) {
if (index >= this.objectArray.length) return;
// Update the corresponding property of the object in the array
const value = property === "value" ? (
this.objectArray[index].type === "absolute" ? parseFloat(event.target.value) / 100 : parseFloat(event.target.value)
) : property === "key" ? event.key : event.target.value;
this.objectArray[index][property] = value;
if (property === "key") this.displayObjects();
};
this.deleteObject = function (index) {
// Remove the object from the array
this.objectArray.splice(index, 1);
// Display the updated input fields for objects
this.displayObjects();
};
return this;
}

30
src/main.js 100644
View File

@ -0,0 +1,30 @@
const fx_version = '0.6.5.6'; // FX Client Version
const fx_update = 'Oct 3'; // FX Client Last Updated
import settingsManager from './settings.js';
import { clanFilter, leaderboardFilter } from "./clanFilters.js";
import WindowManager from "./windowManager.js";
import donationsTracker from "./donationsTracker.js";
import winCounter from "./winCounter.js";
import playerList from "./playerList.js";
import gameScriptUtils from "./gameScriptUtils.js";
import hoveringTooltip from "./hoveringTooltip.js";
import { keybindFunctions, keybindHandler } from "./keybinds.js";
window.__fx = window.__fx || {};
const __fx = window.__fx;
__fx.version = fx_version + " " + fx_update;
__fx.settingsManager = settingsManager;
__fx.leaderboardFilter = leaderboardFilter;
__fx.utils = gameScriptUtils;
__fx.WindowManager = WindowManager;
__fx.keybindFunctions = keybindFunctions;
__fx.keybindHandler = keybindHandler;
__fx.donationsTracker = donationsTracker;
__fx.playerList = playerList;
__fx.hoveringTooltip = hoveringTooltip;
__fx.clanFilter = clanFilter;
__fx.wins = winCounter;
console.log('Successfully loaded FX Client');

43
src/playerList.js 100644
View File

@ -0,0 +1,43 @@
import { getVar } from "./gameInterface.js";
import { escapeHtml } from "./utils.js";
import donationsTracker from "./donationsTracker.js";
import WindowManager from "./windowManager.js";
const playerList = new (function () {
const playersIcon = document.createElement('img');
playersIcon.setAttribute('src', 'assets/players_icon.png');
document.getElementById("playerlist_content").addEventListener("click", event => {
const playerId = event.target.closest("tr[data-player-id]")?.getAttribute("data-player-id");
if (!playerId) return;
if (getVar("gIsTeamGame")) WindowManager.closeWindow("playerList"), donationsTracker.displayHistory(playerId);
});
this.display = function displayPlayerList(playerNames) {
const gHumans = getVar("gHumans");
const gLobbyMaxJoin = getVar("gLobbyMaxJoin");
let listContent = `<h3>Players (${gHumans})</h3>`;
for (let i = 0; i < gLobbyMaxJoin; i++) {
if (i === gHumans) listContent += `<h3>Bots (${gLobbyMaxJoin - gHumans})</h3>`;
listContent += `<tr data-player-id="${i}"><td><span class="color-light-gray">${i + 1}.</span> ${escapeHtml(playerNames[i])}</td></tr>`
}
document.getElementById("playerlist_content").innerHTML = listContent;
document.getElementById("playerlist_content").setAttribute("class", getVar("gIsTeamGame") ? "clickable" : "");
WindowManager.openWindow("playerList");
}
this.hoveringOverButton = false;
this.drawButton = (canvas, x, y, size) => {
canvas.fillRect(x, y, size, size);
canvas.fillStyle = this.hoveringOverButton ? "#aaaaaaaa" : "#000000aa";
canvas.clearRect(x + 1, y + 1, size - 2, size - 2);
canvas.fillRect(x + 1, y + 1, size - 2, size - 2);
canvas.fillStyle = "#ffffff";
canvas.imageSmoothingEnabled = true;
canvas.drawImage(playersIcon, x + 2, y + 2, size - 4, size - 4);
canvas.imageSmoothingEnabled = false;
}
});
WindowManager.add({
name: "playerList",
element: document.getElementById("playerlist")
});
export default playerList

209
src/settings.js 100644
View File

@ -0,0 +1,209 @@
import { KeybindsInput } from "./keybindsInput.js";
import winCounter from "./winCounter.js";
import WindowManager from "./windowManager.js";
window.__fx = window.__fx || {};
const __fx = window.__fx;
var settings = {
//"fontName": "Trebuchet MS",
//"showBotDonations": false,
"displayWinCounter": true,
"useFullscreenMode": false,
"hoveringTooltip": true,
//"hideAllLinks": false,
"realisticNames": false,
"showPlayerDensity": true,
"coloredDensity": true,
"densityDisplayStyle": "percentage",
"highlightClanSpawns": false,
//"customMapFileBtn": true
"customBackgroundUrl": "",
"attackPercentageKeybinds": [],
};
__fx.settings = settings;
const discontinuedSettings = [ "hideAllLinks", "fontName" ];
__fx.makeMainMenuTransparent = false;
/*var settingsGearIcon = document.createElement('img');
settingsGearIcon.setAttribute('src', 'assets/geari_white.png');*/
const settingsManager = new (function() {
const settingsStructure = [
{ for: "displayWinCounter", type: "checkbox", label: "Display win counter",
note: "The win counter tracks multiplayer solo wins (not in team games)" },
{ type: "button", text: "Reset win counter", action: winCounter.removeWins },
{ for: "useFullscreenMode", type: "checkbox", label: "Use fullscreen mode",
note: "Note: fullscreen mode will trigger after you click anywhere on the page due to browser policy restrictions." },
{ for: "hoveringTooltip", type: "checkbox", label: "Hovering tooltip",
note: "Display map territory info constantly (on mouse hover) instead of only when right clicking on the map" },
//{ for: "hideAllLinks", type: "checkbox", label: "Hide Links option also hides app store links" },
{ for: "realisticNames", type: "checkbox", label: "Realistic Bot Names" },
{ for: "showPlayerDensity", type: "checkbox", label: "Show player density" },
{ for: "coloredDensity", type: "checkbox", label: "Colored density", note: "Display the density with a color between red and green depending on the density value" },
{ for: "densityDisplayStyle", type: "selectMenu", label: "Density value display style:", tooltip: "Controls how the territorial density value should be rendered", options: [
{ value: "percentage", label: "Percentage" },
{ value: "absoluteQuotient", label: "Value from 0 to 150 (BetterTT style)" }
]},
{ for: "highlightClanSpawns", type: "checkbox", label: "Highlight clan spawnpoints",
note: "Increases the spawnpoint glow size for members of your clan" },
{ for: "customBackgroundUrl", type: "textInput", label: "Custom main menu background:", placeholder: "Enter an image URL here", tooltip: "A custom image to be shown as the main menu background instead of the currently selected map." },
KeybindsInput
];
const settingsContainer = document.querySelector(".settings .scrollable");
var inputFields = {}; // (includes select menus)
var checkboxFields = {};
var customElements = [];
settingsStructure.forEach(item => {
if (typeof item === "function") {
const container = document.createElement("div");
customElements.push(new item(container));
return settingsContainer.append(container);
}
const label = document.createElement("label");
if (item.tooltip) label.title = item.tooltip;
const isValueInput = item.type.endsWith("Input");
const element = document.createElement(isValueInput || item.type === "checkbox" ? "input" : item.type === "selectMenu" ? "select" : "button");
if (item.type === "textInput") element.type = "text";
if (item.placeholder) element.placeholder = item.placeholder;
if (isValueInput || item.type === "selectMenu") inputFields[item.for] = element;
if (item.text) element.innerText = item.text;
if (item.action) element.addEventListener("click", item.action);
if (item.label) label.append(item.label + " ");
if (item.note) {
const note = document.createElement("small");
note.innerText = item.note;
label.append(document.createElement("br"), note)
}
if (item.options) item.options.forEach(option => {
const optionElement = document.createElement("option");
optionElement.setAttribute("value", option.value);
optionElement.innerText = option.label;
element.append(optionElement);
});
label.append(element);
if (item.type === "checkbox") {
element.type = "checkbox";
const checkmark = document.createElement("span");
checkmark.className = "checkmark";
label.className = "checkbox";
label.append(checkmark);
checkboxFields[item.for] = element;
} else label.append(document.createElement("br"));
settingsContainer.append(label, document.createElement("br"));
});
this.save = function() {
Object.keys(inputFields).forEach(function(key) { settings[key] = inputFields[key].value.trim(); });
Object.keys(checkboxFields).forEach(function(key) { settings[key] = checkboxFields[key].checked; });
this.applySettings();
WindowManager.closeWindow("settings");
discontinuedSettings.forEach(settingName => delete settings[settingName]);
localStorage.setItem("fx_settings", JSON.stringify(settings));
// should probably firgure out a way to do this without reloading - // You can't do it, localstorages REQUIRE you to reload
window.location.reload();
};
const fileInput = document.createElement("input");
fileInput.type = "file";
function handleFileSelect(event) {
const input = event.target;
/** @type {File} */
const selectedFile = input.files[0];
if (!selectedFile) return;
input.removeEventListener("change", handleFileSelect);
input.value = "";
if (!selectedFile.name.endsWith(".json")) return alert("Invalid file format");
const fileReader = new FileReader();
fileReader.onload = function() {
let result;
try {
result = JSON.parse(fileReader.result);
if (confirm("Warning: This will override all current settings, click \"OK\" to confirm")) __fx.settings = settings = result;
localStorage.setItem("fx_settings", JSON.stringify(settings));
window.location.reload();
} catch (error) {
alert("Error\n" + error)
}
}
fileReader.readAsText(selectedFile);
}
this.importFromFile = function() {
fileInput.click();
fileInput.addEventListener('change', handleFileSelect);
};
// https://stackoverflow.com/a/34156339
function saveFile(content, fileName, contentType) {
var a = document.createElement("a");
var file = new Blob([content], {type: contentType});
a.href = URL.createObjectURL(file);
a.download = fileName;
a.click();
URL.revokeObjectURL(a.href);
}
this.exportToFile = function() {
saveFile(JSON.stringify(settings), 'FX_client_settings.json', 'application/json');
};
this.syncFields = function() {
Object.keys(inputFields).forEach(function(key) { inputFields[key].value = settings[key]; });
Object.keys(checkboxFields).forEach(function(key) { checkboxFields[key].checked = settings[key]; });
customElements.forEach(element => element.update(settings));
};
this.resetAll = function() {
if (!confirm("Are you Really SURE you want to RESET ALL SETTINGS back to the default?")) return;
localStorage.removeItem("fx_settings");
window.location.reload();
};
this.applySettings = function() {
//setVarByName("bu", "px " + settings.fontName);
if (settings.useFullscreenMode && document.fullscreenEnabled) {
function tryEnterFullscreen() {
if (document.fullscreenElement !== null) return;
document.documentElement.requestFullscreen({ navigationUI: "hide" })
.then(() => { console.log('Fullscreen mode activated'); })
.catch((error) => { console.warn('Could not enter fullscreen mode:', error); });
}
document.addEventListener('mousedown', tryEnterFullscreen, { once: true });
document.addEventListener('click', tryEnterFullscreen, { once: true });
}
if (settings.customBackgroundUrl !== "") {
document.body.style.backgroundImage = "url(" + settings.customBackgroundUrl + ")";
document.body.style.backgroundSize = "cover";
document.body.style.backgroundPosition = "center";
}
__fx.makeMainMenuTransparent = settings.customBackgroundUrl !== "";
};
});
const openCustomBackgroundFilePicker = () => {
const fileInput = document.getElementById("customBackgroundFileInput");
fileInput.click();
fileInput.addEventListener('change', handleFileSelect);
}
function handleFileSelect(event) {
const fileInput = event.target;
const selectedFile = fileInput.files[0];
console.log(fileInput.files);
console.log(fileInput.files[0]);
if (selectedFile) {
const fileUrl = URL.createObjectURL(selectedFile);
console.log("File URL:", fileUrl);
fileInput.value = "";
fileInput.removeEventListener("change", handleFileSelect);
}
}
WindowManager.add({
name: "settings",
element: document.querySelector(".settings"),
beforeOpen: function() { settingsManager.syncFields(); }
});
if (localStorage.getItem("fx_settings") !== null) {
__fx.settings = settings = {...settings, ...JSON.parse(localStorage.getItem("fx_settings"))};
}
settingsManager.applySettings();
export default settingsManager;
export function getSettings() { return settings; };

4
src/utils.js 100644
View File

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

12
src/winCounter.js 100644
View File

@ -0,0 +1,12 @@
const winCounter = { count: 0, removeWins };
if (localStorage.getItem("fx_winCount") !== null) winCounter.count = localStorage.getItem("fx_winCount");
function removeWins() {
if (confirm('Do you really want to reset your wins?')) {
winCounter.count = 0;
localStorage.removeItem('fx_winCount');
alert("Successfully reset wins");
}
}
export default winCounter

View File

@ -0,0 +1,27 @@
var windows = {};
function add(newWindow) {
windows[newWindow.name] = newWindow;
windows[newWindow.name].isOpen = false;
};
function openWindow(windowName, ...args) {
if (windows[windowName].isOpen === true) return;
if (windows[windowName].beforeOpen !== undefined) windows[windowName].beforeOpen(...args);
windows[windowName].isOpen = true;
windows[windowName].element.style.display = null;
};
function closeWindow(windowName) {
if (windows[windowName].isOpen === false) return;
windows[windowName].isOpen = false;
windows[windowName].element.style.display = "none";
if (windows[windowName].onClose !== undefined) windows[windowName].onClose();
};
function closeAll() {
Object.values(windows).forEach(function (windowObj) {
closeWindow(windowObj.name);
});
};
document.getElementById("canvasA").addEventListener("mousedown", closeAll);
document.getElementById("canvasA").addEventListener("touchstart", closeAll, { passive: true });
document.addEventListener("keydown", event => { if (event.key === "Escape") closeAll(); });
export default { add, openWindow, closeWindow, closeAll }

View File

@ -2,13 +2,14 @@
<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>-->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-WYYDMY13BG"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-WYYDMY13BG');
</script>
<meta charset="utf-8" />
<title>FX Client</title>
<meta name="description" content="Modified Version of Territorial.io - FX Client">
@ -58,58 +59,15 @@
<body onload="aiCommand746(0);">
<canvas id="canvasA" width="128" height="128"></canvas>
<div class="window flex settings" style="display:none">
<span><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 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>
<div class="scrollable"></div>
<hr>
<footer>
<button onclick="settingsManager.resetAll()">Reset Settings</button>
<button onclick="settingsManager.save()">Save Settings</button>
<button onclick="__fx.settingsManager.resetAll()">Reset Settings</button>
<button onclick="__fx.settingsManager.save()">Save Settings</button>
<button onclick="__fx.settingsManager.importFromFile()">Import</button>
<button onclick="__fx.settingsManager.exportToFile()">Export</button>
</footer>
</div>
<div class="window scrollable selectable" id="playerlist" style="display: none;">
@ -120,9 +78,9 @@
<h1>Donation history for </h1>
<p id="donationhistory_note">Note: donations from bots are not shown here</p>
<table><tbody id="donationhistory_content"></tbody></table>
</div>
</div></span>
<script src="variables.js?buildTimestamp"></script>
<script src="fx_core.js?buildTimestamp"></script>
<script src="fx.bundle.js?buildTimestamp"></script>
<script src="game.js?buildTimestamp"></script>
</body>
</html>

View File

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