Compare commits

..

No commits in common. "2688e86b9daaa2468b9ba89bcac3ab8912ee5f1a" and "b88129823195027dcd49eb63019c3c31eb315598" have entirely different histories.

14 changed files with 343 additions and 1119 deletions

View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2025 FX Client
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

139
build.js
View File

@ -1,21 +1,17 @@
// @ts-check
import beautifier from 'js-beautify'; import beautifier from 'js-beautify';
const { js: beautify } = beautifier; const { js: beautify } = beautifier;
import UglifyJS from 'uglify-js'; import UglifyJS from 'uglify-js';
import fs from 'fs'; import fs from 'fs';
import webpack from 'webpack'; import webpack from 'webpack';
import path from 'path'; import path from 'path';
import applyPatches from './patches/patches.js'; import applyPatches from './patches.js';
import ModUtils, { minifyCode } from './modUtils.js';
if (!fs.existsSync("./build")) fs.mkdirSync("./build"); if (!fs.existsSync("./build")) fs.mkdirSync("./build");
fs.cpSync("./static/", "./build/", { recursive: true }); fs.cpSync("./static/", "./build/", { recursive: true });
fs.cpSync("./assets/", "./build/assets/", { recursive: true }); fs.cpSync("./assets/", "./build/assets/", { recursive: true });
const buildTimestamp = Date.now().toString(); fs.writeFileSync("./build/index.html", fs.readFileSync("./build/index.html").toString().replace(/buildTimestamp/g, Date.now()));
fs.writeFileSync("./build/index.html", fs.readFileSync("./build/index.html").toString().replace(/buildTimestamp/g, buildTimestamp));
fs.writeFileSync("./build/sw.js", fs.readFileSync("./build/sw.js").toString().replace("buildTimestamp", buildTimestamp));
const buildClientCode = () => /** @type {Promise<void>} */(new Promise((resolve, reject) => { const buildClientCode = () => new Promise((resolve, reject) => {
webpack({ webpack({
mode: 'production', mode: 'production',
entry: { fxClient: "./src/main.js" }, entry: { fxClient: "./src/main.js" },
@ -36,9 +32,9 @@ const buildClientCode = () => /** @type {Promise<void>} */(new Promise((resolve,
} }
else resolve(); else resolve();
}); });
})); });
let script = fs.readFileSync('./game/latest.js', { encoding: 'utf8' }).trim(); let script = fs.readFileSync('./game/latest.js', { encoding: 'utf8' }).replace("\n", "").trim();
const exposeVarsToGlobalScope = true; const exposeVarsToGlobalScope = true;
// need to first remove the iife wrapper so the top-level functions aren't inlined // need to first remove the iife wrapper so the top-level functions aren't inlined
@ -47,35 +43,93 @@ if (exposeVarsToGlobalScope && script.startsWith("\"use strict\"; (function (
if (exposeVarsToGlobalScope && script.startsWith("(function () {") && script.endsWith("})();")) if (exposeVarsToGlobalScope && script.startsWith("(function () {") && script.endsWith("})();"))
script = script.slice("(function () {".length, -"})();".length); script = script.slice("(function () {".length, -"})();".length);
// uncompress strings
// this will break if there is a closing square bracket ("]") in one of the strings
const stringArrayRaw = script.match(/var S=(\[[^\]]+\]);/)?.[1];
if (stringArrayRaw === undefined) throw new Error("cannot find the string array");
const stringArray = JSON.parse(stringArrayRaw);
script = script.replace(/\bS\[(\d+)\]/g, (_match, index) => `"${stringArray[index]}"`);
const modUtils = new ModUtils(minifyCode(script));
import customLobbyPatches from './patches/customLobby.js';
customLobbyPatches(modUtils);
// for versions ^1.99.5.2 // for versions ^1.99.5.2
const minificationResult = UglifyJS.minify(modUtils.script, { const minificationResult = UglifyJS.minify(script, {
"compress": { "arrows": false }, "compress": { "arrows": false },
"mangle": false "mangle": false
}); });
if (minificationResult.error) { if (minificationResult.error) console.log(minificationResult.error);
console.log("error while passing through UglifyJS, replaceCode replacements might have caused errors");
throw minificationResult.error;
}
if (minificationResult.warnings) console.log(minificationResult.warnings); if (minificationResult.warnings) console.log(minificationResult.warnings);
modUtils.script = minificationResult.code; script = minificationResult.code;
const { const replaceOne = (expression, replaceValue) => {
matchDictionaryExpression, const result = matchOne(expression);
generateRegularExpression // this (below) works correctly because expression.lastIndex gets reset above in matchOne when there is no match
} = modUtils; script = script.replace(expression, replaceValue);
const dictionary = modUtils.dictionary; 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;
if (expression.exec(script) !== null) throw new Error("more than one match for: " + expression);
return result;
}
// https://stackoverflow.com/a/63838890
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.");
const dictionary = {};
const matchDictionaryExpression = expression => {
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;
}
// Return value example:
// When replaceRawCode or matchRawCode are called with "var1=var2+1;" as the code
// 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 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);
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) => {
const { expression, groups } = generateRegularExpression(raw, false, nameMappings);
const expressionMatchResult = matchOne(expression);
return Object.fromEntries(Object.entries(groups).map(([identifier, groupNumber]) => [identifier, expressionMatchResult[groupNumber]]));
}
const generateRegularExpression = (/** @type {string} */ code, /** @type {boolean} */ isForDictionary, nameMappings) => {
const groups = {};
let groupNumberCounter = 1;
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++;
return modifier === "@" ? `(?<${word}>\\w+)` : "(\\w+)";
}
});
let expression = new RegExp(isForDictionary ? raw.replaceAll("@@", "@") : raw, "g");
return { expression, groups };
}
[ [
/,this\.(?<gIsTeamGame>\w+)=this\.\w+<7\|\|9===this\.\w+,/g, /,this\.(?<gIsTeamGame>\w+)=this\.\w+<7\|\|9===this\.\w+,/g,
@ -86,12 +140,11 @@ const dictionary = modUtils.dictionary;
].forEach(matchDictionaryExpression); ].forEach(matchDictionaryExpression);
const rawCodeSegments = [ const rawCodeSegments = [
`aQ.eI(e0)?aQ.eE(e0)?a38=__L([a38]):(player=aQ.eF(e0),oq=__L([b0.uS.zG(@playerData.@rawPlayerNames[player],b0.p9.qQ(0,10),150)])+" ",oq=(oq+=__L([b0.wx.a07(playerData.@playerBalances[player])])+" ")+__L([b0.wx.a07(playerData.@playerTerritories[player])])+" ",`, `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.@gLobbyMaxJoin=1===dg?this.@gHumans:this.@data.@playerCount,this.tZ=this.gLobbyMaxJoin,this.@gBots=this.gLobbyMaxJoin-this.gHumans,this.sg=0,", "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(),", "[0]=__L(),@strs[1]=@game.@gIsSingleplayer?__L():__L(),",
"?(this.gB=Math.floor(.066*aK.fw),g5=aK.g5-4*@uiSizes.@gap-this.gB):", "?(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);`, `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);`,
`var dt=@MenuManager.@getState();if(6===dt){if(4211===d)`
] ]
rawCodeSegments.forEach(code => { rawCodeSegments.forEach(code => {
@ -100,24 +153,18 @@ rawCodeSegments.forEach(code => {
matchDictionaryExpression(expression); matchDictionaryExpression(expression);
}); });
modUtils.executePostMinifyHandlers(); applyPatches({ replace, replaceOne, replaceRawCode, dictionary, matchOne, matchRawCode, escapeRegExp });
applyPatches(modUtils);
script = modUtils.script;
await buildClientCode(); await buildClientCode();
// the dictionary should maybe get embedded into one of the files in the bundle // the dictionary should maybe get embedded into one of the files in the bundle
fs.writeFileSync( fs.writeFileSync("./build/fx.bundle.js", `const dictionary = ${JSON.stringify(dictionary)};\n` + fs.readFileSync("./build/fx.bundle.js").toString());
"./build/fx.bundle.js",
`const buildTimestamp = "${buildTimestamp}"; const dictionary = ${JSON.stringify(dictionary)};\n`
+ fs.readFileSync("./build/fx.bundle.js").toString()
);
console.log("Formatting code..."); console.log("Formatting code...");
script = beautify(script, { script = beautify(script, {
"indent_size": 1, "indent_size": "1",
"indent_char": "\t", "indent_char": "\t",
"max_preserve_newlines": 5, "max_preserve_newlines": "5",
"preserve_newlines": true, "preserve_newlines": true,
"keep_array_indentation": false, "keep_array_indentation": false,
"break_chained_methods": false, "break_chained_methods": false,
@ -128,7 +175,7 @@ script = beautify(script, {
"unescape_strings": false, "unescape_strings": false,
"jslint_happy": false, "jslint_happy": false,
"end_with_newline": false, "end_with_newline": false,
"wrap_line_length": 250, "wrap_line_length": "250",
"indent_inner_html": false, "indent_inner_html": false,
"comma_first": false, "comma_first": false,
"e4x": false, "e4x": false,

View File

@ -1,134 +0,0 @@
// @ts-check
import UglifyJS from 'uglify-js';
// https://stackoverflow.com/a/63838890
const escapeRegExp = (/** @type {string} */ string) => string.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&');
export function minifyCode(/** @type {string} */ script) {
const output = UglifyJS.minify(script, {
compress: false,
mangle: false
});
if (output.error) throw output.error;
if (output.warnings) throw (output.warnings);
if (output.warnings) console.warn(output.warnings);
return output.code;
}
class ModUtils {
script = "";
/** @type {{[key: string]: string}} */
dictionary = {};
/** @type {Function[]} */
postMinifyHandlers = [];
constructor(/** @type {string} */ script) {
this.script = script;
// Bind methods
this.matchDictionaryExpression = this.matchDictionaryExpression.bind(this);
this.generateRegularExpression = this.generateRegularExpression.bind(this);
this.replace = this.replace.bind(this);
this.replaceOne = this.replaceOne.bind(this);
this.replaceRawCode = this.replaceRawCode.bind(this);
this.matchOne = this.matchOne.bind(this);
this.matchRawCode = this.matchRawCode.bind(this);
this.replaceCode = this.replaceCode.bind(this);
this.waitForMinification = this.waitForMinification.bind(this);
}
/** @param {RegExp} expression */
matchDictionaryExpression(expression) {
const result = this.matchOne(expression);
// @ts-ignore
for (let [key, value] of Object.entries(result.groups)) this.addToDictionary(key, value);
}
replace(/** @type {Parameters<typeof String.prototype.replace>} */ ...args) {
return this.script = this.script.replace(...args);
};
/** Expressions passed to this function must have the global flag set. */
matchOne(/** @type {RegExp} */ expression) {
const result = expression.exec(this.script);
if (result === null) throw new Error("no match for: " + expression.toString());
if (expression.exec(this.script) !== null) throw new Error("more than one match for: " + expression.toString());
return result;
};
addToDictionary(/** @type {string} */ key, /** @type {string} */ value) {
if (this.dictionary[key] !== undefined && this.dictionary[key] !== value)
throw new Error("name different from existing one:\n KEY: " + key + "\n VALUE: " + value + "\n Value in dictionary: " + this.dictionary[key]);
this.dictionary[key] = value;
};
/**
* @param {RegExp} expression
* @param {string} replaceValue
*/
replaceOne(expression, replaceValue) {
const result = this.matchOne(expression);
// this (below) works correctly because expression.lastIndex gets reset above in matchOne when there is no match
this.script = this.script.replace(expression, replaceValue);
return result;
};
// Return value example:
// When replaceRawCode or matchRawCode are called with "var1=var2+1;" as the code
// and this matches "a=b+1;", the returned value will be the object: { var1: "a", var2: "b" }
replaceRawCode(/** @type {string} */ raw, /** @type {string} */ result, nameMappings) {
const { expression, groups } = this.generateRegularExpression(raw, false, nameMappings);
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);
let expressionMatchResult;
try { expressionMatchResult = this.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]]));
}
matchRawCode(/** @type {string} */ raw, nameMappings) {
const { expression, groups } = this.generateRegularExpression(raw, false, nameMappings);
const expressionMatchResult = this.matchOne(expression);
return Object.fromEntries(Object.entries(groups).map(([identifier, groupNumber]) => [identifier, expressionMatchResult[groupNumber]]));
}
generateRegularExpression(/** @type {string} */ code, /** @type {boolean} */ isForDictionary, nameMappings) {
const groups = {};
let groupNumberCounter = 1;
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", "new", "Math", "WebSocket"].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++;
return modifier === "@" ? `(?<${word}>\\w+)` : "(\\w+)";
}
});
let expression = new RegExp(isForDictionary ? raw.replaceAll("@@", "@") : raw, "g");
return { expression, groups };
}
replaceCode(code, replacement, options) {
return this.replaceRawCode(minifyCode(code), replacement);
}
waitForMinification(/** @type {Function} */ handler) {
this.postMinifyHandlers.push(handler);
}
executePostMinifyHandlers() {
this.postMinifyHandlers.forEach(handler => handler());
}
escapeRegExp = escapeRegExp
}
export default ModUtils;

View File

@ -1,6 +1,5 @@
import assets from '../assets.js'; import assets from './assets.js';
import ModUtils from '../modUtils.js'; export default ({ replace, replaceOne, replaceRawCode, dictionary, matchOne, matchRawCode, escapeRegExp }) => {
export default (/** @type {ModUtils} */ { replace, replaceOne, replaceRawCode, dictionary, matchOne, matchRawCode, escapeRegExp }) => {
// Constants for easy usage of otherwise long variable access expressions // Constants for easy usage of otherwise long variable access expressions
const dict = dictionary; const dict = dictionary;
@ -29,10 +28,10 @@ export default (/** @type {ModUtils} */ { replace, replaceOne, replaceRawCode, d
// TODO: test this; it might cause issues with new boat mechanics? // TODO: test this; it might cause issues with new boat mechanics?
{ // Add Troop Density and Maximum Troops in side panel { // Add Troop Density and Maximum Troops in side panel
const { valuesArray } = replaceRawCode(`,labels[5]=__L(0,"Interest"),labels[6]=__L(),labels[7]=__L(),(truncatedLabels=new Array(labels.length)).fill(""),(valuesArray=new Array(labels.length))[0]=game.io?`, const { valuesArray } = replaceRawCode(`,labels[5]=__L(),labels[6]=__L(),labels[7]=__L(),(valuesArray=new Array(labels.length))[0]=game.io?`,
`,labels[5]=__L(0,"Interest"),labels[6]=__L(),labels[7]=__L(), `,labels[5]=__L(),labels[6]=__L(),labels[7]=__L(),
labels.push("Max Troops", "Density"), // add labels labels.push("Max Troops", "Density"), // add labels
(truncatedLabels=new Array(labels.length)).fill(""),(valuesArray=new Array(labels.length))[0]=game.io?`); (valuesArray=new Array(labels.length))[0]=game.io?`);
replaceOne(new RegExp(/(:(?<valueIndex>\w+)<7\?\w+\.\w+\.\w+\(valuesArray\[\2\]\)):(\w+\.\w+\(valuesArray\[7\]\))}/ replaceOne(new RegExp(/(:(?<valueIndex>\w+)<7\?\w+\.\w+\.\w+\(valuesArray\[\2\]\)):(\w+\.\w+\(valuesArray\[7\]\))}/
.source.replace(/valuesArray/g, valuesArray), "g"), .source.replace(/valuesArray/g, valuesArray), "g"),
'$1 : $<valueIndex> === 7 ? $3 ' '$1 : $<valueIndex> === 7 ? $3 '
@ -43,25 +42,22 @@ export default (/** @type {ModUtils} */ { replace, replaceOne, replaceRawCode, d
} }
// Increment win counter on wins // Increment win counter on wins
replaceRawCode(`=function(sE){a8.gD[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))`, 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){ `=function(sE){
if (${playerId} === sE && !${gIsSingleplayer}) if (${playerId} === sE && !${gIsSingleplayer})
__fx.wins.count++, window.localStorage.setItem("fx_winCount", __fx.wins.count), __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); xD(0,"Your Win Count is now " + __fx.wins.count,3,sE,ad.gN,ad.kl,-1,!0);
a8.gD[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))`); 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, custom lobby button and win count { // Add settings button and win count
// add buttons // add settings button
replaceRawCode(`,new nQ("☰<br>"+__L(),function(){aD6(3)},aa.ks),new nQ("",function(){at.d5(12)},aa.kg,!1)]`, 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("☰<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)"), new nQ("FX Client settings", function() { __fx.WindowManager.openWindow("settings"); }, "rgba(0, 0, 20, 0.5")]`)
new nQ("Join/Create custom lobby", function() { __fx.customLobby.showJoinPrompt(); }, "rgba(20, 9, 77, 0.5)")]`) // set settings button position
// set position
replaceRawCode(`aZ.g5.vO(aD3[3].button,x+a0S+gap,a3X+h+gap,a0S,h);`, 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[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);`);
aZ.g5.vO(aD3[5].button, x, a3X + h * 2 + gap * 2, a0S * 2 + gap, h / 3);
aZ.g5.vO(aD3[6].button, x, a3X + h * 2.33 + gap * 3, a0S * 2 + gap, h / 3);`);
// render win count // 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;`, 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(_y.a4l(),_r.gI(),_m.gI(),aw.gI(),a0.g8()){
@ -98,7 +94,7 @@ canvas.font=aY.g0.g1(1,fontSize),canvas.fillStyle="rgba("+gR+","+tD+","+hj+",0.6
{ // Keybinds { // Keybinds
// match required variables // match required variables
const { 0: match, groups: { attackBarObject, setRelative } } = matchOne(/:\w+\.\w+\(\w+,8\)\?(?<attackBarObject>\w+)\.(?<setRelative>\w+)\(32\/31\):/g); 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, // create a setAbsolutePercentage function on the attack percentage bar object,
// and also register the keybind handler functions // 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, 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,
@ -141,14 +137,12 @@ canvas.font=aY.g0.g1(1,fontSize),canvas.fillStyle="rgba("+gR+","+tD+","+hj+",0.6
`, ${dict.game}.${dict.gIsTeamGame} && __fx.donationsTracker.displayHistory($2, ${rawPlayerNames}, ${gIsSingleplayer}), $1 && !isEmptySpace $3`); `, ${dict.game}.${dict.gIsTeamGame} && __fx.donationsTracker.displayHistory($2, ${rawPlayerNames}, ${gIsSingleplayer}), $1 && !isEmptySpace $3`);
// Reset donation history and leaderboard filter when a new game is started // Reset donation history and leaderboard filter when a new game is started
replaceRawCode(",ab.dP(),ad.a10(),b5.nZ.oJ=[],bc.dP(),this.wE=1,", replaceOne(new RegExp(`,this\\.${dictionary.playerBalances}.fill\\(0\\),`, "g"), "$& __fx.donationsTracker.reset(), __fx.leaderboardFilter.reset(), ");
`,ab.dP(),ad.a10(),b5.nZ.oJ=[],bc.dP(),this.wE=1,
__fx.donationsTracker.reset(), __fx.leaderboardFilter.reset(), __fx.customLobby.isActive() && __fx.customLobby.hideWindow(),`)
{ // Player list and leaderboard filter tabs { // Player list and leaderboard filter tabs
// Draw player list button // Draw player list button
const uiOffset = dictionary.uiSizes + "." + dictionary.gap; const uiOffset = dictionary.uiSizes + "." + dictionary.gap;
const { groups: { drawFunction, topBarHeight } } = replaceOne(/(="";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+,Math\.floor\()(\w+)\/2\),(Math\.floor\(\w+\+\w+\/2\)\));/g, 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);"); "$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)` const buttonBoundsCheck = `__fx.utils.isPointInRectangle($<x>, $<y>, ${uiOffset} + 12, ${uiOffset} + 12, ${topBarHeight} - 22, ${topBarHeight} - 22)`
// Handle player list button and leaderboard tabs mouseDown // Handle player list button and leaderboard tabs mouseDown
@ -167,23 +161,19 @@ canvas.font=aY.g0.g1(1,fontSize),canvas.fillStyle="rgba("+gR+","+tD+","+hj+",0.6
)) return; $4`); )) return; $4`);
} }
{ // Name rendering patches - Display density of other players & Hide bot names features { // Display density of other players
const { placeBalanceAbove } = matchRawCode(`,aGH+=Math.floor(.78*fontSize),placeBalanceAbove?aGN(a7,aGJ,aGG,aGH,hT):aGM(hT,a7,aGJ,aGG,aGH,aGI)`); const r = matchRawCode(`bD.dO.data[7].value?a9W(i,jm,jk,jl,ctx):a9V(ctx,i,jm,jk,jl,a9S)))`);
// Balance rendering; Renders density when the "Reverse Name/Balance" setting is off 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))}", 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){ `function a9V(ctx,i,fontSize,x,y,a9S){
var ___id = i; 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)); 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));
${placeBalanceAbove} || __fx.settings.showPlayerDensity && (__fx.settings.coloredDensity && (ctx.fillStyle = __fx.utils.textStyleBasedOnDensity(___id)), ctx.fillText(__fx.utils.getDensity(___id), x, y + fontSize))}`) ${settingsSwitchNameAndBalance} || __fx.settings.showPlayerDensity && (__fx.settings.coloredDensity && (ctx.fillStyle = __fx.utils.textStyleBasedOnDensity(___id)), ctx.fillText(__fx.utils.getDensity(___id), x, y + fontSize))}`)
// Name rendering; Renders density when the "Reverse Name/Balance" setting is on (default) // Applies when the "Reverse Name/Balance" setting is on (default)
replaceOne(/(function \w+\((?<i>\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, 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; `$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>)); }`);
var showName = $<i> < $<game>.$<gHumans> || !__fx.settings.hideBotNames;
if (showName) $7, $10;
${placeBalanceAbove} && __fx.settings.showPlayerDensity && (
__fx.settings.coloredDensity && ($<canvas>.fillStyle = __fx.utils.textStyleBasedOnDensity(___id)),
$<canvas>.fillText(__fx.utils.getDensity(___id), $<x>, showName ? $<y> + $<fontSize> : $<y>)
); }`);
} }
{ // Leaderboard filter { // Leaderboard filter
@ -303,11 +293,6 @@ canvas.font=aY.g0.g1(1,fontSize),canvas.fillStyle="rgba("+gR+","+tD+","+hj+",0.6
) )
} }
// Detailed team pie chart percentage
replaceRawCode(`qr=Math.floor(100*f0+.5)+"%"`,
`qr = (__fx.settings.detailedTeamPercentage ? (100*f0).toFixed(2) : Math.floor(100*f0+.5)) + "%"`)
replaceRawCode(",fontSize=+dz*Math.min(f0,.37);", ",fontSize=(__fx.settings.detailedTeamPercentage ? 0.75 : 1)*dz*Math.min(f0,.37);")
// Invalid hostname detection avoidance // Invalid hostname detection avoidance
replaceRawCode(`,hostnameIsValid=0<=window.location.hostname.toLowerCase().indexOf("territorial.io"),`, replaceRawCode(`,hostnameIsValid=0<=window.location.hostname.toLowerCase().indexOf("territorial.io"),`,
`,hostnameIsValid=true,`) `,hostnameIsValid=true,`)

View File

@ -1,81 +0,0 @@
import ModUtils from '../modUtils.js';
// Custom lobby patches
export default (/** @type {ModUtils} */ { replaceCode, replaceRawCode, dictionary: dict, waitForMinification }) => {
// set player id correctly
replaceCode(`function aBG(aBE) {
if (!Lobby.aAl) { return -1; }
var s = aBE.length;
var qu = Lobby.aAl.qu;
for (var i = 0; i < s; i++) { if (aBE[i].qu === qu) { return i; } }
return -1;
}`, `function aBG(aBE) {
if (!Lobby.aAl) { return -1; }
if (__fx.customLobby.isActive()) return __fx.customLobby.getPlayerId();
var s = aBE.length;
var qu = Lobby.aAl.qu;
for (var i = 0; i < s; i++) { if (aBE[i].qu === qu) { return i; } }
return -1;
}`);
waitForMinification(() => {
replaceRawCode("this.aHm=function(){i___.rX(),aM.a7U(0),aM.init()}",
`this.aHm=function(){i___.rX(),aM.a7U(0),aM.init()},
__fx.customLobby.setJoinFunction(() => { i___.rX(); aM.a7U(0); aM.init(); })`
)
replaceRawCode(`(socketId-aq.kt.a82)+"/",(socket=new WebSocket(url)`,
`(socketId-aq.kt.a82)+"/",(socket=new WebSocket(__fx.customLobby.isActive() && socketId === 1 ? __fx.customLobby.getSocketURL() : url)`)
replaceRawCode("this.send=function(socketId,data){aJE(socketId),aJ4[socketId].send(data)}",
"this.send=function(socketId,data){aJE(socketId),aJ4[socketId].send(data)},__fx.customLobby.setSendFunction(this.send)")
replaceRawCode("b7.dH(a0),0===b7.size?aq.kt.aJJ(wR,3205):",
"b7.dH(a0),0===b7.size?aq.kt.aJJ(wR,3205):__fx.customLobby.isCustomMessage(a0)||")
// set the custom lobby to inactive when clicking the "Back" button on the connection screen or leaving the lobby
replaceRawCode("this.xZ=function(){Sockets.kt.wf(3260),i___.kt.we()}",
"this.xZ=function(){Sockets.kt.wf(3260),__fx.customLobby.setActive(false),i___.kt.we()}")
replaceRawCode("function(){n.r(),bl.zf(),Sockets.s.ze(3240),n.o(5,5)}",
`(__fx.customLobby.setLeaveFunction(() => {n.r(),bl.zf(),Sockets.s.ze(3240),__fx.customLobby.setActive(false),n.o(5,5)}),
function(){n.r(),bl.zf(),Sockets.s.ze(3240),__fx.customLobby.setActive(false),n.o(5,5)})`)
// when a socket error occurs on the custom lobby socket
replaceRawCode("this.wQ=function(wR,d){if(8===i.pz&&0===wR)if(4211===d)wS(d);",
`this.wQ=function(wR,d){
wR===1 && __fx.customLobby.isActive() && ${dict.MenuManager}.${dict.getState}() !== 6 && __fx.customLobby.setActive(false);
if(8===i.pz&&0===wR)if(4211===d)wS(d);`)
// when leaving a game
replaceRawCode("this.wl=function(zs){a1.gZ||az.oO.a11.length||(az.oO.a11=az.a12.vd()),ap.ky.zt(),this.vH=0,bU.zu(),m.n.setState(0),zs||bJ.df.show(),aN.setState(0),2===this.a3D?i.ky.a3U():1===this.a3D?i.j(19):i.j(5,5)}",
`this.wl=function(zs){a1.gZ||az.oO.a11.length||(az.oO.a11=az.a12.vd()),
__fx.customLobby.isActive() === false && ap.ky.zt(),
this.vH=0,bU.zu(),m.n.setState(0),zs||bJ.df.show(),aN.setState(0);
if (__fx.customLobby.isActive()) __fx.customLobby.rejoinLobby(); else 2===this.a3D?i.ky.a3U():1===this.a3D?i.j(19):i.j(5,5)}`)
// do not display lobby UI
replaceRawCode(`(sV.style.backdropFilter="blur(4px)",sV.style.webkitBackdropFilter="blur(4px)"),`,
`(sV.style.backdropFilter="blur(4px)",sV.style.webkitBackdropFilter="blur(4px)"),
__fx.customLobby.isActive() && (sV.style.display = "none"),`);
// allow games with one player
replaceRawCode("if((t3=bk.t1.t3[e0])<2)return!1;", "if((t3=bk.t1.t3[e0])<2 && !__fx.customLobby.isActive())return!1;")
// if the server is unreachable
replaceRawCode("{g.wc(3249)}", "{__fx.customLobby.isActive()?(g.wc(3249),__fx.customLobby.setActive(false)):g.wc(3249)}")
// error descriptions
const errors = { 3249: "No servers found", 4705: "Lobby not found", 4730: "Kicked from lobby" };
replaceRawCode(`m.n___(4,5,new o(__L(),xT(e),!0))`,
`m.n___(4,5,new o(__L(),${JSON.stringify(errors)}[e] ?? xT(e),!0))`)
// map info (for the map selection menu)
replaceRawCode("this.info=new Array(Maps.totalMapCount+1),this.info[0]={name:__L(),",
"this.info=new Array(Maps.totalMapCount+1),__fx.customLobby.setMapInfo(this.info),this.info[0]={name:__L(),")
// to not set custom lobby games as singleplayer
replaceRawCode("this.vK=this.jS=this.data.a0f,this.gameIsSingleplayer=1===this.vK,",
"this.vK=this.jS=this.data.a0f,this.gameIsSingleplayer=1===this.vK&&!__fx.customLobby.isActive(),")
// custom difficulty
replaceRawCode("if(9===a1.jq)this.jr();else if(a1.js)if(3===a1.data.jv)for(z=a1.ju-1;0<=z;z--){var jw=z+jp;this.ie[jw]=",
`if(9===a1.jq)this.jr();
else if (__fx.customLobby.isActive()) for(z=a1.ju-1;0<=z;z--) this.ie[z+jp] = __fx.customLobby.gameInfo.difficulty;
else if(a1.js)if(3===a1.data.jv)for(z=a1.ju-1;0<=z;z--){var jw=z+jp;this.ie[jw]=`)
// spawn selection
replaceRawCode(":50,this.a=this.b=this.data.c,this.d=this.b?new e:null,",
":50,this.a=this.b=__fx.customLobby.isActive() ? __fx.customLobby.gameInfo.spawnSelection : this.data.c,this.d=this.b?new e:null,")
// bot count
replaceRawCode(",this.gLobbyMaxJoin=1===dg?this.gHumans:this.data.playerCount,this.maxPlayers=this.gLobbyMaxJoin,this.gBots=this.gLobbyMaxJoin-this.gHumans,this.sg=0,",
`,this.gLobbyMaxJoin = __fx.customLobby.isActive() ? Math.max(Math.min(__fx.customLobby.gameInfo.botCount, this.data.playerCount), this.gHumans) : 1===dg?this.gHumans:this.data.playerCount,
this.maxPlayers=this.gLobbyMaxJoin,this.gBots=this.gLobbyMaxJoin-this.gHumans,this.sg=0,`)
});
}

View File

@ -25,18 +25,17 @@ FX Client is the first Territorial.io client, offering a better User Interface a
4. Displays your troop density and maximum troops 4. Displays your troop density and maximum troops
5. Displays the density of players and bots 5. Displays the density of players and bots
6. Adds a "Clan" tab on the leaderboard, allowing you to easily see your clanmates 6. Adds a "Clan" tab on the leaderboard, allowing you to easily see your clanmates
7. Adds custom lobbies 7. Hovering tooltip: makes the territory map information (normally visible on right click) be visible constantly (on hover)
8. Hovering tooltip: makes the territory map information (normally visible on right click) be visible constantly (on hover) 8. Adds a player list
9. 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 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. Adds a win counter 11. Can be installed as a PWA (progressive web app) ensuring maximum enjoyment on consoles, phones and even desktop devices
12. Can be installed as a PWA (progressive web app) ensuring maximum enjoyment on consoles, phones and even desktop devices
#### The client has a settings menu, from which you can: #### The client has a settings menu, from which you can:
13. Make fullscreen mode trigger automatically 12. Make fullscreen mode trigger automatically
14. Set a custom main menu background 13. Set a custom main menu background
15. Create custom attack percentage keybinds 14. Create custom attack percentage keybinds
## Building Locally ## Building Locally

View File

@ -1,306 +0,0 @@
import WindowManager from "./windowManager.js";
//const socketURL = "ws://localhost:8080/";
const socketURL = "wss://fx.peshomir.workers.dev/";
const customMessageMarker = 120;
let isActive = false;
let currentCode = "";
let joinLobby = () => { };
let leaveLobby = () => { };
let sendRaw = (socketId, data) => { };
const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder();
WindowManager.add({
name: "lobbyJoinMenu",
element: document.getElementById("customLobbyJoinMenu")
})
const windowElement = WindowManager.create({
name: "customLobby",
classes: "scrollable selectable flex-column text-align-center",
closable: false
});
const header = document.createElement("h2");
header.textContent = "Custom Lobby";
const main = document.createElement("div");
main.className = "customlobby-main";
const playerListContainer = document.createElement("div");
const playerCount = document.createElement("p");
playerCount.textContent = "0 Players";
const playerListDiv = document.createElement("div");
playerListContainer.append(playerCount, playerListDiv);
const optionsContainer = document.createElement("div");
optionsContainer.className = "text-align-left";
const optionsStructure = {
mode: {
label: "Mode:", type: "selectMenu", options: [
{ value: 0, label: "2 Teams" },
{ value: 1, label: "3 Teams" },
{ value: 2, label: "4 Teams" },
{ value: 3, label: "5 Teams" },
{ value: 4, label: "6 Teams" },
{ value: 5, label: "7 Teams" },
{ value: 6, label: "8 Teams" },
{ value: 7, label: "Battle Royale" },
{ value: 10, label: "No Fullsend Battle Royale" },
{ value: 9, label: "Zombie mode" }
]
},
map: { label: "Map:", type: "selectMenu" },
difficulty: { label: "Difficulty:", type: "selectMenu", options: [
{ value: 0, label: "Very Easy (Default)" },
{ value: 1, label: "Easy (1v1)" },
{ value: 2, label: "Normal" },
{ value: 3, label: "Hard" },
{ value: 4, label: "Very Hard" },
{ value: 5, label: "Impossible" }
]},
spawnSelection: { label: "Spawn selection", type: "checkbox" },
botCount: { label: "Bot & player count:", type: "numberInput", attributes: { min: "1", max: "512" } }
}
const optionsElements = {};
const optionsValues = {};
function updateOption(option, value) {
if (optionsStructure[option].type === "checkbox")
optionsElements[option].checked = (value === 0 ? false : true);
else optionsElements[option].value = value.toString();
optionsValues[option] = value;
}
function inputUpdateHandler(key, e) {
sendMessage("options", [key, parseInt(e.target.value)])
}
function checkboxUpdateHandler(key, e) {
sendMessage("options", [key, e.target.checked ? 1 : 0])
}
Object.entries(optionsStructure).forEach(([key, item]) => {
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"
);
optionsElements[key] = element;
if (item.type === "textInput") element.type = "text";
if (item.type === "numberInput") element.type = "number";
if (item.placeholder) element.placeholder = item.placeholder;
if (isValueInput || item.type === "selectMenu")
element.addEventListener("change", inputUpdateHandler.bind(undefined, key))
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) setSelectMenuOptions(item.options, element);
if (item.attributes) Object.entries(item.attributes).forEach(
([name, value]) => element.setAttribute(name, value)
);
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;
element.addEventListener("change", checkboxUpdateHandler.bind(undefined, key))
} else label.append(document.createElement("br"));
optionsContainer.append(label/*, document.createElement("br")*/);
});
function setMapInfo(maps) {
setTimeout(() => setSelectMenuOptions(maps.map((info, index) => ({ value: index.toString(), label: info.name })), optionsElements["map"]), 0);
}
main.append(playerListContainer, optionsContainer);
const footer = document.createElement("footer");
footer.style.marginTop = "10px";
const startButton = document.createElement("button");
const leaveButton = document.createElement("button");
startButton.textContent = "Start game";
leaveButton.textContent = "Leave lobby";
startButton.addEventListener("click", startGame);
leaveButton.addEventListener("click", () => leaveLobby());
footer.append(startButton, leaveButton);
windowElement.append(header, main, footer);
/** @param {HTMLSelectElement} element */
function setSelectMenuOptions(options, element) {
options.forEach(data => {
const option = document.createElement("option");
option.setAttribute("value", data.value);
option.textContent = data.label;
element.append(option);
})
}
function showJoinPrompt() {
WindowManager.openWindow("lobbyJoinMenu");
}
document.getElementById("lobbyCode").addEventListener("input", ({ target: input }) => {
if (input.value.length !== 5) return;
currentCode = input.value.toLowerCase();
input.value = "";
WindowManager.closeWindow("lobbyJoinMenu");
isActive = true;
joinLobby();
});
document.getElementById("createLobbyButton").addEventListener("click", () => {
currentCode = "";
WindowManager.closeWindow("lobbyJoinMenu");
isActive = true;
joinLobby();
});
function sendMessage(type, data) {
const message = data !== undefined ? { t: type, d: data } : { t: type }
const originalArray = textEncoder.encode(JSON.stringify(message));
const buffer = new ArrayBuffer(originalArray.length + 1);
const view = new DataView(buffer);
// Set the first byte to the custom message marker
view.setUint8(0, customMessageMarker);
// Copy the original array starting from the second byte
const uint8ArrayView = new Uint8Array(buffer, 1);
uint8ArrayView.set(originalArray);
sendRaw(1, buffer);
}
let playerIsHost = false;
/** @param {Uint8Array} raw */
function isCustomMessage(raw) {
if (raw[0] !== customMessageMarker) return false;
if (raw.length === 1) return true; // ping
const subArray = new Uint8Array(raw.buffer, 1);
const message = JSON.parse(textDecoder.decode(subArray));
const { t: type, d: data } = message;
if (type === "lobby") {
WindowManager.openWindow("customLobby");
header.textContent = "Custom Lobby " + data.code;
currentCode = data.code;
playerIsHost = data.isHost;
startButton.disabled = !playerIsHost;
if (playerIsHost) optionsContainer.classList.remove("disabled");
else optionsContainer.classList.add("disabled");
Object.entries(data.options).forEach(([option, value]) => updateOption(option, value));
displayPlayers(data.players);
} else if (type === "addPlayer") {
addPlayer({ name: data.name, inGame: false, isHost: false });
updatePlayerCount();
} else if (type === "removePlayer") {
const index = data;
playerList[index].element.remove();
playerList.splice(index, 1);
updatePlayerCount();
} else if (type === "inLobby") {
const index = data;
playerList[index].inGameBadge.className = "d-none";
} else if (type === "options") {
const [option, value] = data;
updateOption(option, value);
} else if (type === "setHost") {
const index = data;
playerList[index].isHost = true;
playerList[index].hostBadge.className = "";
} else if (type === "host") {
playerIsHost = true;
startButton.disabled = false;
optionsContainer.classList.remove("disabled");
playerList.forEach(p => { if (!p.isHost) p.kickButton.className = "" });
} else if (type === "serverMessage") alert(data);
return true;
}
/** @typedef {{ element: HTMLDivElement, hostBadge: HTMLSpanElement, inGameBadge: HTMLSpanElement, kickButton: HTMLButtonElement, isHost: boolean, inGame: boolean }} PlayerListEntry */
/** @type {PlayerListEntry[]} */
let playerList = [];
/** @type {PlayerListEntry} */
let thisPlayer;
function createBadge(text, visible) {
const badge = document.createElement("span");
badge.textContent = text;
badge.className = visible ? "" : "d-none";
return badge;
}
/** @typedef {{ name: string, isHost: boolean, inGame: boolean }} PlayerInfo */
/** @param {PlayerInfo} player */
function addPlayer(player) {
const div = document.createElement("div");
div.className = "lobby-player";
div.textContent = player.name;
const kickButton = document.createElement("button");
kickButton.textContent = "Kick";
kickButton.className = playerIsHost && !player.isHost ? "" : "d-none";
kickButton.addEventListener("click", kickButtonHandler);
const hostBadge = createBadge("Host", player.isHost);
const inGameBadge = createBadge("In Game", player.inGame);
div.append(hostBadge, inGameBadge, kickButton);
playerListDiv.append(div);
playerList.push({ element: div, hostBadge, inGameBadge, kickButton, isHost: player.isHost, inGame: player.inGame });
}
function kickButtonHandler(event) {
const button = event.target;
for (let index = 0; index < playerList.length; index++) {
if (playerList[index].kickButton === button) {
sendMessage("kick", index);
break;
}
}
}
/** @param {PlayerInfo[]} players */
function displayPlayers(players) {
playerList = [];
playerListDiv.innerHTML = "";
players.forEach(addPlayer);
thisPlayer = playerList[playerList.length - 1];
updatePlayerCount();
}
function updatePlayerCount() {
playerCount.textContent = `${playerList.length} Player${playerList.length === 1 ? "" : "s"}`
}
function getSocketURL() {
return socketURL + (currentCode === "" ? "create" : "join?" + currentCode)
}
function getPlayerId() {
let id = 0;
for (let i = 0; i < playerList.length; i++) {
const player = playerList[i];
if (player === thisPlayer) return id;
if (player.inGame === false) id++;
}
}
function startGame() {
WindowManager.closeWindow("customLobby");
sendMessage("startGame");
}
function rejoinLobby() {
joinLobby();
}
function setJoinFunction(f) { joinLobby = f; }
function setLeaveFunction(f) { leaveLobby = f; }
function setSendFunction(f) { sendRaw = f; }
function setActive(active) {
isActive = active;
if (active === false) WindowManager.closeWindow("customLobby");
}
function hideWindow() {
WindowManager.closeWindow("customLobby");
}
const gameInterface = { gameInfo: optionsValues, showJoinPrompt, isCustomMessage, getSocketURL, getPlayerId, setJoinFunction, setLeaveFunction, setSendFunction, setMapInfo, rejoinLobby, hideWindow, isActive: () => isActive, setActive }
const customLobby = gameInterface
export default customLobby

View File

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

View File

@ -1,16 +1,5 @@
const fx_version = '0.6.7'; // FX Client Version const fx_version = '0.6.5.6'; // FX Client Version
const fx_update = 'Feb 8'; // FX Client Last Updated const fx_update = 'Oct 3'; // FX Client Last Updated
if ("serviceWorker" in navigator) {
navigator.serviceWorker.addEventListener("message", (e) => {
const message = e.data;
if (message.event === "activate" && buildTimestamp !== message.version) {
// worker was updated in the background
document.getElementById("updateNotification").style.display = "block";
}
});
navigator.serviceWorker.register("./sw.js");
}
import settingsManager from './settings.js'; import settingsManager from './settings.js';
import { clanFilter, leaderboardFilter } from "./clanFilters.js"; import { clanFilter, leaderboardFilter } from "./clanFilters.js";
@ -21,7 +10,6 @@ import playerList from "./playerList.js";
import gameScriptUtils from "./gameScriptUtils.js"; import gameScriptUtils from "./gameScriptUtils.js";
import hoveringTooltip from "./hoveringTooltip.js"; import hoveringTooltip from "./hoveringTooltip.js";
import { keybindFunctions, keybindHandler } from "./keybinds.js"; import { keybindFunctions, keybindHandler } from "./keybinds.js";
import customLobby from './customLobby.js';
window.__fx = window.__fx || {}; window.__fx = window.__fx || {};
const __fx = window.__fx; const __fx = window.__fx;
@ -38,6 +26,5 @@ __fx.playerList = playerList;
__fx.hoveringTooltip = hoveringTooltip; __fx.hoveringTooltip = hoveringTooltip;
__fx.clanFilter = clanFilter; __fx.clanFilter = clanFilter;
__fx.wins = winCounter; __fx.wins = winCounter;
__fx.customLobby = customLobby;
console.log('Successfully loaded FX Client'); console.log('Successfully loaded FX Client');

View File

@ -8,106 +8,53 @@ const __fx = window.__fx;
var settings = { var settings = {
//"fontName": "Trebuchet MS", //"fontName": "Trebuchet MS",
//"showBotDonations": false, //"showBotDonations": false,
displayWinCounter: true, "displayWinCounter": true,
useFullscreenMode: false, "useFullscreenMode": false,
hoveringTooltip: true, "hoveringTooltip": true,
//"hideAllLinks": false, //"hideAllLinks": false,
realisticNames: false, "realisticNames": false,
showPlayerDensity: true, "showPlayerDensity": true,
coloredDensity: true, "coloredDensity": true,
densityDisplayStyle: "percentage", "densityDisplayStyle": "percentage",
hideBotNames: false, "highlightClanSpawns": false,
highlightClanSpawns: false,
detailedTeamPercentage: false,
//"customMapFileBtn": true //"customMapFileBtn": true
customBackgroundUrl: "", "customBackgroundUrl": "",
attackPercentageKeybinds: [], "attackPercentageKeybinds": [],
}; };
__fx.settings = settings; __fx.settings = settings;
const discontinuedSettings = ["hideAllLinks", "fontName"]; const discontinuedSettings = [ "hideAllLinks", "fontName" ];
__fx.makeMainMenuTransparent = false; __fx.makeMainMenuTransparent = false;
/*var settingsGearIcon = document.createElement('img'); /*var settingsGearIcon = document.createElement('img');
settingsGearIcon.setAttribute('src', 'assets/geari_white.png');*/ settingsGearIcon.setAttribute('src', 'assets/geari_white.png');*/
const settingsManager = new (function () { const settingsManager = new (function() {
const settingsStructure = [ const settingsStructure = [
{ { for: "displayWinCounter", type: "checkbox", label: "Display win counter",
for: "displayWinCounter", note: "The win counter tracks multiplayer solo wins (not in team games)" },
type: "checkbox", { type: "button", text: "Reset win counter", action: winCounter.removeWins },
label: "Display win counter", { for: "useFullscreenMode", type: "checkbox", label: "Use fullscreen mode",
note: "The win counter tracks multiplayer solo wins (not in team games)", 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" },
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: "hideAllLinks", type: "checkbox", label: "Hide Links option also hides app store links" },
{ for: "realisticNames", type: "checkbox", label: "Realistic Bot Names" }, { for: "realisticNames", type: "checkbox", label: "Realistic Bot Names" },
{ { for: "showPlayerDensity", type: "checkbox", label: "Show player density" },
for: "showPlayerDensity", { for: "coloredDensity", type: "checkbox", label: "Colored density", note: "Display the density with a color between red and green depending on the density value" },
type: "checkbox", { for: "densityDisplayStyle", type: "selectMenu", label: "Density value display style:", tooltip: "Controls how the territorial density value should be rendered", options: [
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: "percentage", label: "Percentage" },
{ { value: "absoluteQuotient", label: "Value from 0 to 150 (BetterTT style)" }
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
{ for: "hideBotNames", type: "checkbox", label: "Hide bot names" },
{
for: "highlightClanSpawns",
type: "checkbox",
label: "Highlight clan spawnpoints",
note: "Increases the spawnpoint glow size for members of your clan",
},
{
for: "detailedTeamPercentage", type: "checkbox",
label: "Detailed team pie chart percentage",
note: "For example: this would show 25.82% instead of 26% on the pie chart in team games"
},
{
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"); const settingsContainer = document.querySelector(".settings .scrollable");
var inputFields = {}; // (includes select menus) var inputFields = {}; // (includes select menus)
var checkboxFields = {}; var checkboxFields = {};
var customElements = []; var customElements = [];
settingsStructure.forEach((item) => { settingsStructure.forEach(item => {
if (typeof item === "function") { if (typeof item === "function") {
const container = document.createElement("div"); const container = document.createElement("div");
customElements.push(new item(container)); customElements.push(new item(container));
@ -116,27 +63,19 @@ const settingsManager = new (function () {
const label = document.createElement("label"); const label = document.createElement("label");
if (item.tooltip) label.title = item.tooltip; if (item.tooltip) label.title = item.tooltip;
const isValueInput = item.type.endsWith("Input"); const isValueInput = item.type.endsWith("Input");
const element = document.createElement( const element = document.createElement(isValueInput || item.type === "checkbox" ? "input" : item.type === "selectMenu" ? "select" : "button");
isValueInput || item.type === "checkbox"
? "input"
: item.type === "selectMenu"
? "select"
: "button"
);
if (item.type === "textInput") element.type = "text"; if (item.type === "textInput") element.type = "text";
if (item.placeholder) element.placeholder = item.placeholder; if (item.placeholder) element.placeholder = item.placeholder;
if (isValueInput || item.type === "selectMenu") if (isValueInput || item.type === "selectMenu") inputFields[item.for] = element;
inputFields[item.for] = element;
if (item.text) element.innerText = item.text; if (item.text) element.innerText = item.text;
if (item.action) element.addEventListener("click", item.action); if (item.action) element.addEventListener("click", item.action);
if (item.label) label.append(item.label + " "); if (item.label) label.append(item.label + " ");
if (item.note) { if (item.note) {
const note = document.createElement("small"); const note = document.createElement("small");
note.innerText = item.note; note.innerText = item.note;
label.append(document.createElement("br"), note); label.append(document.createElement("br"), note)
} }
if (item.options) if (item.options) item.options.forEach(option => {
item.options.forEach((option) => {
const optionElement = document.createElement("option"); const optionElement = document.createElement("option");
optionElement.setAttribute("value", option.value); optionElement.setAttribute("value", option.value);
optionElement.innerText = option.label; optionElement.innerText = option.label;
@ -153,16 +92,12 @@ const settingsManager = new (function () {
} else label.append(document.createElement("br")); } else label.append(document.createElement("br"));
settingsContainer.append(label, document.createElement("br")); settingsContainer.append(label, document.createElement("br"));
}); });
this.save = function () { this.save = function() {
Object.keys(inputFields).forEach(function (key) { Object.keys(inputFields).forEach(function(key) { settings[key] = inputFields[key].value.trim(); });
settings[key] = inputFields[key].value.trim(); Object.keys(checkboxFields).forEach(function(key) { settings[key] = checkboxFields[key].checked; });
});
Object.keys(checkboxFields).forEach(function (key) {
settings[key] = checkboxFields[key].checked;
});
this.applySettings(); this.applySettings();
WindowManager.closeWindow("settings"); WindowManager.closeWindow("settings");
discontinuedSettings.forEach((settingName) => delete settings[settingName]); discontinuedSettings.forEach(settingName => delete settings[settingName]);
localStorage.setItem("fx_settings", JSON.stringify(settings)); 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 // should probably firgure out a way to do this without reloading - // You can't do it, localstorages REQUIRE you to reload
window.location.reload(); window.location.reload();
@ -178,99 +113,74 @@ const settingsManager = new (function () {
input.removeEventListener("change", handleFileSelect); input.removeEventListener("change", handleFileSelect);
input.value = ""; input.value = "";
if (!selectedFile.name.endsWith(".json")) if (!selectedFile.name.endsWith(".json")) return alert("Invalid file format");
return alert("Invalid file format");
const fileReader = new FileReader(); const fileReader = new FileReader();
fileReader.onload = function () { fileReader.onload = function() {
let result; let result;
try { try {
result = JSON.parse(fileReader.result); result = JSON.parse(fileReader.result);
if ( if (confirm("Warning: This will override all current settings, click \"OK\" to confirm")) __fx.settings = settings = result;
confirm(
'Warning: This will override all current settings, click "OK" to confirm'
)
)
__fx.settings = settings = result;
localStorage.setItem("fx_settings", JSON.stringify(settings)); localStorage.setItem("fx_settings", JSON.stringify(settings));
window.location.reload(); window.location.reload();
} catch (error) { } catch (error) {
alert("Error\n" + error); alert("Error\n" + error)
}
} }
};
fileReader.readAsText(selectedFile); fileReader.readAsText(selectedFile);
} }
this.importFromFile = function () { this.importFromFile = function() {
fileInput.click(); fileInput.click();
fileInput.addEventListener("change", handleFileSelect); fileInput.addEventListener('change', handleFileSelect);
}; };
// https://stackoverflow.com/a/34156339 // https://stackoverflow.com/a/34156339
function saveFile(content, fileName, contentType) { function saveFile(content, fileName, contentType) {
var a = document.createElement("a"); var a = document.createElement("a");
var file = new Blob([content], { type: contentType }); var file = new Blob([content], {type: contentType});
a.href = URL.createObjectURL(file); a.href = URL.createObjectURL(file);
a.download = fileName; a.download = fileName;
a.click(); a.click();
URL.revokeObjectURL(a.href); URL.revokeObjectURL(a.href);
} }
this.exportToFile = function () { this.exportToFile = function() {
saveFile( saveFile(JSON.stringify(settings), 'FX_client_settings.json', 'application/json');
JSON.stringify(settings),
"FX_client_settings.json",
"application/json"
);
}; };
this.syncFields = function () { this.syncFields = function() {
Object.keys(inputFields).forEach(function (key) { Object.keys(inputFields).forEach(function(key) { inputFields[key].value = settings[key]; });
inputFields[key].value = settings[key]; Object.keys(checkboxFields).forEach(function(key) { checkboxFields[key].checked = settings[key]; });
}); customElements.forEach(element => element.update(settings));
Object.keys(checkboxFields).forEach(function (key) {
checkboxFields[key].checked = settings[key];
});
customElements.forEach((element) => element.update(settings));
}; };
this.resetAll = function () { this.resetAll = function() {
if ( if (!confirm("Are you Really SURE you want to RESET ALL SETTINGS back to the default?")) return;
!confirm(
"Are you Really SURE you want to RESET ALL SETTINGS back to the default?"
)
)
return;
localStorage.removeItem("fx_settings"); localStorage.removeItem("fx_settings");
window.location.reload(); window.location.reload();
}; };
this.applySettings = function () { this.applySettings = function() {
//setVarByName("bu", "px " + settings.fontName); //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 !== "") { if (settings.customBackgroundUrl !== "") {
document.body.style.backgroundImage = document.body.style.backgroundImage = "url(" + settings.customBackgroundUrl + ")";
"url(" + settings.customBackgroundUrl + ")";
document.body.style.backgroundSize = "cover"; document.body.style.backgroundSize = "cover";
document.body.style.backgroundPosition = "center"; document.body.style.backgroundPosition = "center";
} }
__fx.makeMainMenuTransparent = settings.customBackgroundUrl !== ""; __fx.makeMainMenuTransparent = settings.customBackgroundUrl !== "";
}; };
});
if (settings.useFullscreenMode) tryEnterFullscreen();
})();
export function tryEnterFullscreen() {
if (document.fullscreenElement !== null || !document.fullscreenEnabled) return;
document.documentElement
.requestFullscreen({ navigationUI: "hide" })
.then(() => {
console.log("Fullscreen mode activated");
})
.catch((error) => {
console.warn("Could not enter fullscreen mode:", error);
});
}
const openCustomBackgroundFilePicker = () => { const openCustomBackgroundFilePicker = () => {
const fileInput = document.getElementById("customBackgroundFileInput"); const fileInput = document.getElementById("customBackgroundFileInput");
fileInput.click(); fileInput.click();
fileInput.addEventListener("change", handleFileSelect); fileInput.addEventListener('change', handleFileSelect);
}; }
function handleFileSelect(event) { function handleFileSelect(event) {
const fileInput = event.target; const fileInput = event.target;
const selectedFile = fileInput.files[0]; const selectedFile = fileInput.files[0];
@ -287,20 +197,13 @@ function handleFileSelect(event) {
WindowManager.add({ WindowManager.add({
name: "settings", name: "settings",
element: document.querySelector(".settings"), element: document.querySelector(".settings"),
beforeOpen: function () { beforeOpen: function() { settingsManager.syncFields(); }
settingsManager.syncFields();
},
}); });
if (localStorage.getItem("fx_settings") !== null) { if (localStorage.getItem("fx_settings") !== null) {
__fx.settings = settings = { __fx.settings = settings = {...settings, ...JSON.parse(localStorage.getItem("fx_settings"))};
...settings,
...JSON.parse(localStorage.getItem("fx_settings")),
};
} }
settingsManager.applySettings(); settingsManager.applySettings();
export default settingsManager; export default settingsManager;
export function getSettings() { export function getSettings() { return settings; };
return settings;
}

View File

@ -1,63 +1,27 @@
import { getSettings, tryEnterFullscreen } from "./settings.js";
var windows = {}; var windows = {};
const container = document.getElementById("windowContainer");
function create(info) {
const window = document.createElement("div");
info.element = window;
window.className =
"window" +
(info.classes !== undefined
? " " + info.classes
: " scrollable selectable");
window.style.display = "none";
container.appendChild(window);
add(info);
return window;
}
function add(newWindow) { function add(newWindow) {
windows[newWindow.name] = newWindow; windows[newWindow.name] = newWindow;
windows[newWindow.name].isOpen = false; windows[newWindow.name].isOpen = false;
} };
function openWindow(windowName, ...args) { function openWindow(windowName, ...args) {
if (windows[windowName].isOpen === true) return; if (windows[windowName].isOpen === true) return;
if (windows[windowName].beforeOpen !== undefined) if (windows[windowName].beforeOpen !== undefined) windows[windowName].beforeOpen(...args);
windows[windowName].beforeOpen(...args);
windows[windowName].isOpen = true; windows[windowName].isOpen = true;
windows[windowName].element.style.display = null; windows[windowName].element.style.display = null;
} };
function closeWindow(windowName) { function closeWindow(windowName) {
if (windows[windowName].isOpen === false) return; if (windows[windowName].isOpen === false) return;
windows[windowName].isOpen = false; windows[windowName].isOpen = false;
windows[windowName].element.style.display = "none"; windows[windowName].element.style.display = "none";
if (windows[windowName].onClose !== undefined) windows[windowName].onClose(); if (windows[windowName].onClose !== undefined) windows[windowName].onClose();
} };
function closeAll() { function closeAll() {
Object.values(windows).forEach(function (windowObj) { Object.values(windows).forEach(function (windowObj) {
if (windowObj.closable !== false) closeWindow(windowObj.name); closeWindow(windowObj.name);
}); });
} };
document.addEventListener( document.getElementById("canvasA").addEventListener("mousedown", closeAll);
"mousedown", document.getElementById("canvasA").addEventListener("touchstart", closeAll, { passive: true });
(e) => { document.addEventListener("keydown", event => { if (event.key === "Escape") closeAll(); });
// when clicking outside a window
if (!container.contains(e.target)) closeAll();
const isFullScreenEnabled = getSettings().useFullscreenMode; export default { add, openWindow, closeWindow, closeAll }
if (isFullScreenEnabled) {
tryEnterFullscreen();
}
},
{ passive: true, capture: true }
);
document
.getElementById("canvasA")
.addEventListener("touchstart", closeAll, { passive: true });
document.addEventListener("keydown", (event) => {
if (event.key === "Escape") closeAll();
});
export default { create, add, openWindow, closeWindow, closeAll };

View File

@ -2,14 +2,14 @@
<html lang="en"> <html lang="en">
<head> <head>
<!-- Google tag (gtag.js) --> <!-- Google tag (gtag.js) -->
<!--<script async src="https://www.googletagmanager.com/gtag/js?id=G-WYYDMY13BG"></script> <script async src="https://www.googletagmanager.com/gtag/js?id=G-WYYDMY13BG"></script>
<script> <script>
window.dataLayer = window.dataLayer || []; window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);} function gtag(){dataLayer.push(arguments);}
gtag('js', new Date()); gtag('js', new Date());
gtag('config', 'G-WYYDMY13BG'); gtag('config', 'G-WYYDMY13BG');
</script>--> </script>
<meta charset="utf-8" /> <meta charset="utf-8" />
<title>FX Client</title> <title>FX Client</title>
<meta name="description" content="Modified Version of Territorial.io - FX Client"> <meta name="description" content="Modified Version of Territorial.io - FX Client">
@ -59,7 +59,7 @@
<body onload="aiCommand746(0);"> <body onload="aiCommand746(0);">
<canvas id="canvasA" width="128" height="128"></canvas> <canvas id="canvasA" width="128" height="128"></canvas>
<span id="windowContainer"><div class="window flex-column settings" style="display:none"> <span><div class="window flex settings" style="display:none">
<h1>Settings</h1> <h1>Settings</h1>
<div class="scrollable"></div> <div class="scrollable"></div>
<hr> <hr>
@ -70,11 +70,6 @@
<button onclick="__fx.settingsManager.exportToFile()">Export</button> <button onclick="__fx.settingsManager.exportToFile()">Export</button>
</footer> </footer>
</div> </div>
<div class="window flex-column" id="customLobbyJoinMenu" style="display: none">
<input type="text" id="lobbyCode" placeholder="Enter lobby code">
or
<button id="createLobbyButton">Create new lobby</button>
</div>
<div class="window scrollable selectable" id="playerlist" style="display: none;"> <div class="window scrollable selectable" id="playerlist" style="display: none;">
<h1>Player List</h1> <h1>Player List</h1>
<table><tbody id="playerlist_content"></tbody></table> <table><tbody id="playerlist_content"></tbody></table>
@ -83,11 +78,6 @@
<h1>Donation history for </h1> <h1>Donation history for </h1>
<p id="donationhistory_note">Note: donations from bots are not shown here</p> <p id="donationhistory_note">Note: donations from bots are not shown here</p>
<table><tbody id="donationhistory_content"></tbody></table> <table><tbody id="donationhistory_content"></tbody></table>
</div>
<div class="window" style="display: none" id="updateNotification">
<h3>A new version of FX is available! Reload to update</h3>
<button onclick="window.location.reload()">Reload</button>
<button onclick="document.getElementById('updateNotification').style.display = 'none'">Dismiss</button>
</div></span> </div></span>
<script src="variables.js?buildTimestamp"></script> <script src="variables.js?buildTimestamp"></script>
<script src="fx.bundle.js?buildTimestamp"></script> <script src="fx.bundle.js?buildTimestamp"></script>

View File

@ -34,6 +34,15 @@
z-index : 10; z-index : 10;
} }
.window.flex {
display : flex;
flex-direction: column;
}
hr {
width: 100%;
}
.window button, .window button,
.window input, .window input,
.window select { .window select {
@ -45,77 +54,6 @@
transition : 0.2s; transition : 0.2s;
border : 1px solid #fff; border : 1px solid #fff;
border-radius : 5px; border-radius : 5px;
margin : 5px;
}
.window :disabled, .window .disabled {
pointer-events: none;
opacity: 0.65;
}
.window.settings button,
.window.settings input,
.window.settings select {
margin: 0px;
}
.flex {
display: flex;
}
.flex-column {
display : flex;
flex-direction: column;
}
#customLobbyJoinMenu {
align-items: center;
}
.customlobby-main {
display: flex;
justify-content: space-evenly;
flex-wrap: wrap;
gap: 10px;
}
.lobby-player {
margin: 5px;
width: 15rem;
display: flex;
align-items: center;
justify-content: center;
}
.lobby-player span {
margin: 0px 5px;
font-size: .7em;
border-style: solid;
border-width: 1px;
padding: 3px 5px;
border-color: #ffffff7d;
border-radius: 5px;
}
.lobby-player button {
font-size: 0.7em;
margin: 0px 5px;
padding: 3px 5px;
}
.d-none {
display: none;
}
.text-align-center {
text-align: center;
}
.text-align-left {
text-align: left;
}
hr {
width: 100%;
} }
h1 { h1 {

View File

@ -1,47 +0,0 @@
const cacheName = "buildTimestamp"; // this gets replaced by the build script
self.addEventListener("install", (e) => {
console.log("[Service Worker] Install");
self.skipWaiting();
});
self.addEventListener("fetch", (e) => {
const url = e.request.url;
// Cache http and https only, skip unsupported chrome-extension:// and file://...
if (!(url.startsWith('http:') || url.startsWith('https:'))) {
return;
}
e.respondWith(
(async () => {
const r = await caches.match(e.request);
console.log(`[Service Worker] Fetching resource: ${url}`);
if (r) {
return r;
}
const response = await fetch(e.request);
const cache = await caches.open(cacheName);
console.log(`[Service Worker] Caching new resource: ${url}`);
cache.put(e.request, response.clone());
return response;
})(),
);
});
self.addEventListener("activate", (e) => {
console.log("[Service Worker] Activated", cacheName);
self.clients.matchAll().then(clients => {
clients.forEach(client => client.postMessage({ event: "activate", version: cacheName }));
});
e.waitUntil(
caches.keys().then((keyList) => {
return Promise.all(
keyList.map((key) => {
if (key === cacheName) {
return;
}
return caches.delete(key);
}),
);
}),
);
});