Compare commits
27 Commits
b881298231
...
2688e86b9d
Author | SHA1 | Date |
---|---|---|
|
2688e86b9d | |
|
162bd36f29 | |
|
23b92f6f63 | |
|
4503c26c42 | |
|
a02d49782d | |
|
38edf66a73 | |
|
d673f7744e | |
|
f214035dd6 | |
|
b3e8562113 | |
|
610df24eff | |
|
9499709fca | |
|
e41be64891 | |
|
fa3810e735 | |
|
0bde4ac648 | |
|
175eed0f44 | |
|
51e6f2ebaa | |
|
f909347059 | |
|
8a85790caf | |
|
9b85d5d9b4 | |
|
407716b11b | |
|
421a0f3f02 | |
|
14bf32b846 | |
|
12a4a8e937 | |
|
863d702471 | |
|
f4fb7af669 | |
|
6259f52b3e | |
|
a59ca4355a |
|
@ -0,0 +1,21 @@
|
||||||
|
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
139
build.js
|
@ -1,17 +1,21 @@
|
||||||
|
// @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.js';
|
import applyPatches from './patches/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 });
|
||||||
fs.writeFileSync("./build/index.html", fs.readFileSync("./build/index.html").toString().replace(/buildTimestamp/g, Date.now()));
|
const buildTimestamp = Date.now().toString();
|
||||||
|
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 = () => new Promise((resolve, reject) => {
|
const buildClientCode = () => /** @type {Promise<void>} */(new Promise((resolve, reject) => {
|
||||||
webpack({
|
webpack({
|
||||||
mode: 'production',
|
mode: 'production',
|
||||||
entry: { fxClient: "./src/main.js" },
|
entry: { fxClient: "./src/main.js" },
|
||||||
|
@ -32,9 +36,9 @@ const buildClientCode = () => new Promise((resolve, reject) => {
|
||||||
}
|
}
|
||||||
else resolve();
|
else resolve();
|
||||||
});
|
});
|
||||||
});
|
}));
|
||||||
|
|
||||||
let script = fs.readFileSync('./game/latest.js', { encoding: 'utf8' }).replace("\n", "").trim();
|
let script = fs.readFileSync('./game/latest.js', { encoding: 'utf8' }).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
|
||||||
|
@ -43,93 +47,35 @@ 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(script, {
|
const minificationResult = UglifyJS.minify(modUtils.script, {
|
||||||
"compress": { "arrows": false },
|
"compress": { "arrows": false },
|
||||||
"mangle": false
|
"mangle": false
|
||||||
});
|
});
|
||||||
if (minificationResult.error) console.log(minificationResult.error);
|
if (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);
|
||||||
script = minificationResult.code;
|
modUtils.script = minificationResult.code;
|
||||||
|
|
||||||
const replaceOne = (expression, replaceValue) => {
|
const {
|
||||||
const result = matchOne(expression);
|
matchDictionaryExpression,
|
||||||
// this (below) works correctly because expression.lastIndex gets reset above in matchOne when there is no match
|
generateRegularExpression
|
||||||
script = script.replace(expression, replaceValue);
|
} = modUtils;
|
||||||
return result;
|
const dictionary = modUtils.dictionary;
|
||||||
}
|
|
||||||
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,
|
||||||
|
@ -140,11 +86,12 @@ const generateRegularExpression = (/** @type {string} */ code, /** @type {boolea
|
||||||
].forEach(matchDictionaryExpression);
|
].forEach(matchDictionaryExpression);
|
||||||
|
|
||||||
const rawCodeSegments = [
|
const rawCodeSegments = [
|
||||||
`aR.f1(fy)?aR.fB(fy)?a0z=__L([a0z]):(player=aR.fA(fy),oM=__L([b1.t9.xw(@playerData.@rawPlayerNames[player],b1.kx.l2(0,10),150)])+" ",a0z=(oM+=__L([b1.l5.l6(playerData.@playerBalances[player])])+" ")+(__L([b1.l5.l6(playerData.@playerTerritories[player])])+" ")+`,
|
`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])])+" ",`,
|
||||||
"this.@gIsSingleplayer?this.@gLobbyMaxJoin=@SingleplayerMenu.@getSingleplayerPlayerCount():this.gLobbyMaxJoin=this.@gMaxPlayers,this.@gBots=this.gLobbyMaxJoin-this.@gHumans,this.sg=0,",
|
"this.@gLobbyMaxJoin=1===dg?this.@gHumans:this.@data.@playerCount,this.tZ=this.gLobbyMaxJoin,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 => {
|
||||||
|
@ -153,18 +100,24 @@ rawCodeSegments.forEach(code => {
|
||||||
matchDictionaryExpression(expression);
|
matchDictionaryExpression(expression);
|
||||||
});
|
});
|
||||||
|
|
||||||
applyPatches({ replace, replaceOne, replaceRawCode, dictionary, matchOne, matchRawCode, escapeRegExp });
|
modUtils.executePostMinifyHandlers();
|
||||||
|
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("./build/fx.bundle.js", `const dictionary = ${JSON.stringify(dictionary)};\n` + fs.readFileSync("./build/fx.bundle.js").toString());
|
fs.writeFileSync(
|
||||||
|
"./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,
|
||||||
|
@ -175,7 +128,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,
|
||||||
|
|
|
@ -0,0 +1,134 @@
|
||||||
|
// @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;
|
|
@ -0,0 +1,81 @@
|
||||||
|
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,`)
|
||||||
|
});
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
import assets from './assets.js';
|
import assets from '../assets.js';
|
||||||
export default ({ replace, replaceOne, replaceRawCode, dictionary, matchOne, matchRawCode, escapeRegExp }) => {
|
import ModUtils from '../modUtils.js';
|
||||||
|
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;
|
||||||
|
@ -28,10 +29,10 @@ export default ({ replace, replaceOne, replaceRawCode, dictionary, matchOne, mat
|
||||||
// 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(),labels[6]=__L(),labels[7]=__L(),(valuesArray=new Array(labels.length))[0]=game.io?`,
|
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?`,
|
||||||
`,labels[5]=__L(),labels[6]=__L(),labels[7]=__L(),
|
`,labels[5]=__L(0,"Interest"),labels[6]=__L(),labels[7]=__L(),
|
||||||
labels.push("Max Troops", "Density"), // add labels
|
labels.push("Max Troops", "Density"), // add labels
|
||||||
(valuesArray=new Array(labels.length))[0]=game.io?`);
|
(truncatedLabels=new Array(labels.length)).fill(""),(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 '
|
||||||
|
@ -42,22 +43,25 @@ export default ({ replace, replaceOne, replaceRawCode, dictionary, matchOne, mat
|
||||||
}
|
}
|
||||||
|
|
||||||
// Increment win counter on wins
|
// 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)`,
|
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))`,
|
||||||
`=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);
|
||||||
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)`);
|
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))`);
|
||||||
|
|
||||||
|
|
||||||
{ // Add settings button and win count
|
{ // Add settings button, custom lobby button and win count
|
||||||
// add settings button
|
// add buttons
|
||||||
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)"),
|
||||||
// set settings button position
|
new nQ("Join/Create custom lobby", function() { __fx.customLobby.showJoinPrompt(); }, "rgba(20, 9, 77, 0.5)")]`)
|
||||||
|
// 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[5].button, x, a3X + h * 2 + gap * 2, a0S * 2 + gap, h / 3);`);
|
`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[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()){
|
||||||
|
@ -94,7 +98,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+\.key)\?(?<attackBarObject>\w+)\.(?<setRelative>\w+)\(31\/32\):"."===\1\?\2\.\3\(32\/31\):/g,);
|
const { 0: match, groups: { attackBarObject, setRelative } } = matchOne(/:\w+\.\w+\(\w+,8\)\?(?<attackBarObject>\w+)\.(?<setRelative>\w+)\(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,
|
||||||
|
@ -137,12 +141,14 @@ 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
|
||||||
replaceOne(new RegExp(`,this\\.${dictionary.playerBalances}.fill\\(0\\),`, "g"), "$& __fx.donationsTracker.reset(), __fx.leaderboardFilter.reset(), ");
|
replaceRawCode(",ab.dP(),ad.a10(),b5.nZ.oJ=[],bc.dP(),this.wE=1,",
|
||||||
|
`,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(/(=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,
|
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,
|
||||||
"$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
|
||||||
|
@ -161,19 +167,23 @@ canvas.font=aY.g0.g1(1,fontSize),canvas.fillStyle="rgba("+gR+","+tD+","+hj+",0.6
|
||||||
)) return; $4`);
|
)) return; $4`);
|
||||||
}
|
}
|
||||||
|
|
||||||
{ // Display density of other players
|
{ // Name rendering patches - Display density of other players & Hide bot names features
|
||||||
const r = matchRawCode(`bD.dO.data[7].value?a9W(i,jm,jk,jl,ctx):a9V(ctx,i,jm,jk,jl,a9S)))`);
|
const { placeBalanceAbove } = matchRawCode(`,aGH+=Math.floor(.78*fontSize),placeBalanceAbove?aGN(a7,aGJ,aGG,aGH,hT):aGM(hT,a7,aGJ,aGG,aGH,aGI)`);
|
||||||
const settingsSwitchNameAndBalance = `${r.bD}.${r.dO}.${r.data}[7].${r.value}`;
|
// Balance rendering; Renders density when the "Reverse Name/Balance" setting is off
|
||||||
//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));
|
||||||
${settingsSwitchNameAndBalance} || __fx.settings.showPlayerDensity && (__fx.settings.coloredDensity && (ctx.fillStyle = __fx.utils.textStyleBasedOnDensity(___id)), ctx.fillText(__fx.utils.getDensity(___id), x, y + fontSize))}`)
|
${placeBalanceAbove} || __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)
|
// Name rendering; Renders density 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,
|
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,
|
||||||
`$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>)); }`);
|
`$1 var ___id = $2;
|
||||||
|
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
|
||||||
|
@ -293,6 +303,11 @@ 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,`)
|
17
readme.md
17
readme.md
|
@ -25,17 +25,18 @@ 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. Hovering tooltip: makes the territory map information (normally visible on right click) be visible constantly (on hover)
|
7. Adds custom lobbies
|
||||||
8. Adds a player list
|
8. Hovering tooltip: makes the territory map information (normally visible on right click) be visible constantly (on hover)
|
||||||
9. Adds the ability to view the history of who donated to a player during a team game by clicking on their name in the leaderboard or the player list
|
9. Adds a player list
|
||||||
10. Adds a win counter
|
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
|
||||||
11. Can be installed as a PWA (progressive web app) ensuring maximum enjoyment on consoles, phones and even desktop devices
|
11. Adds a win counter
|
||||||
|
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:
|
||||||
|
|
||||||
12. Make fullscreen mode trigger automatically
|
13. Make fullscreen mode trigger automatically
|
||||||
13. Set a custom main menu background
|
14. Set a custom main menu background
|
||||||
14. Create custom attack percentage keybinds
|
15. Create custom attack percentage keybinds
|
||||||
|
|
||||||
## Building Locally
|
## Building Locally
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,306 @@
|
||||||
|
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
|
|
@ -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]]
|
||||||
};
|
};
|
19
src/main.js
19
src/main.js
|
@ -1,5 +1,16 @@
|
||||||
const fx_version = '0.6.5.6'; // FX Client Version
|
const fx_version = '0.6.7'; // FX Client Version
|
||||||
const fx_update = 'Oct 3'; // FX Client Last Updated
|
const fx_update = 'Feb 8'; // 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";
|
||||||
|
@ -10,6 +21,7 @@ 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;
|
||||||
|
@ -26,5 +38,6 @@ __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');
|
||||||
|
|
443
src/settings.js
443
src/settings.js
|
@ -6,204 +6,301 @@ window.__fx = window.__fx || {};
|
||||||
const __fx = window.__fx;
|
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",
|
||||||
"highlightClanSpawns": false,
|
hideBotNames: false,
|
||||||
//"customMapFileBtn": true
|
highlightClanSpawns: false,
|
||||||
"customBackgroundUrl": "",
|
detailedTeamPercentage: false,
|
||||||
"attackPercentageKeybinds": [],
|
//"customMapFileBtn": true
|
||||||
|
customBackgroundUrl: "",
|
||||||
|
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",
|
{
|
||||||
note: "The win counter tracks multiplayer solo wins (not in team games)" },
|
for: "displayWinCounter",
|
||||||
{ type: "button", text: "Reset win counter", action: winCounter.removeWins },
|
type: "checkbox",
|
||||||
{ for: "useFullscreenMode", type: "checkbox", label: "Use fullscreen mode",
|
label: "Display win counter",
|
||||||
note: "Note: fullscreen mode will trigger after you click anywhere on the page due to browser policy restrictions." },
|
note: "The win counter tracks multiplayer solo wins (not in team games)",
|
||||||
{ 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" },
|
type: "button",
|
||||||
{ for: "realisticNames", type: "checkbox", label: "Realistic Bot Names" },
|
text: "Reset win counter",
|
||||||
{ for: "showPlayerDensity", type: "checkbox", label: "Show player density" },
|
action: winCounter.removeWins,
|
||||||
{ 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" },
|
for: "useFullscreenMode",
|
||||||
{ value: "absoluteQuotient", label: "Value from 0 to 150 (BetterTT style)" }
|
type: "checkbox",
|
||||||
]},
|
label: "Use fullscreen mode",
|
||||||
{ for: "highlightClanSpawns", type: "checkbox", label: "Highlight clan spawnpoints",
|
note: "Note: fullscreen mode will trigger after you click anywhere on the page due to browser policy restrictions.",
|
||||||
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: "hoveringTooltip",
|
||||||
];
|
type: "checkbox",
|
||||||
const settingsContainer = document.querySelector(".settings .scrollable");
|
label: "Hovering tooltip",
|
||||||
var inputFields = {}; // (includes select menus)
|
note: "Display map territory info constantly (on mouse hover) instead of only when right clicking on the map",
|
||||||
var checkboxFields = {};
|
},
|
||||||
var customElements = [];
|
//{ for: "hideAllLinks", type: "checkbox", label: "Hide Links option also hides app store links" },
|
||||||
settingsStructure.forEach(item => {
|
{ for: "realisticNames", type: "checkbox", label: "Realistic Bot Names" },
|
||||||
if (typeof item === "function") {
|
{
|
||||||
const container = document.createElement("div");
|
for: "showPlayerDensity",
|
||||||
customElements.push(new item(container));
|
type: "checkbox",
|
||||||
return settingsContainer.append(container);
|
label: "Show player density",
|
||||||
}
|
},
|
||||||
const label = document.createElement("label");
|
{
|
||||||
if (item.tooltip) label.title = item.tooltip;
|
for: "coloredDensity",
|
||||||
const isValueInput = item.type.endsWith("Input");
|
type: "checkbox",
|
||||||
const element = document.createElement(isValueInput || item.type === "checkbox" ? "input" : item.type === "selectMenu" ? "select" : "button");
|
label: "Colored density",
|
||||||
if (item.type === "textInput") element.type = "text";
|
note: "Display the density with a color between red and green depending on the density value",
|
||||||
if (item.placeholder) element.placeholder = item.placeholder;
|
},
|
||||||
if (isValueInput || item.type === "selectMenu") inputFields[item.for] = element;
|
{
|
||||||
if (item.text) element.innerText = item.text;
|
for: "densityDisplayStyle",
|
||||||
if (item.action) element.addEventListener("click", item.action);
|
type: "selectMenu",
|
||||||
if (item.label) label.append(item.label + " ");
|
label: "Density value display style:",
|
||||||
if (item.note) {
|
tooltip: "Controls how the territorial density value should be rendered",
|
||||||
const note = document.createElement("small");
|
options: [
|
||||||
note.innerText = item.note;
|
{ value: "percentage", label: "Percentage" },
|
||||||
label.append(document.createElement("br"), note)
|
{
|
||||||
}
|
value: "absoluteQuotient",
|
||||||
if (item.options) item.options.forEach(option => {
|
label: "Value from 0 to 150 (BetterTT style)",
|
||||||
const optionElement = document.createElement("option");
|
},
|
||||||
optionElement.setAttribute("value", option.value);
|
],
|
||||||
optionElement.innerText = option.label;
|
},
|
||||||
element.append(optionElement);
|
{ for: "hideBotNames", type: "checkbox", label: "Hide bot names" },
|
||||||
});
|
{
|
||||||
label.append(element);
|
for: "highlightClanSpawns",
|
||||||
if (item.type === "checkbox") {
|
type: "checkbox",
|
||||||
element.type = "checkbox";
|
label: "Highlight clan spawnpoints",
|
||||||
const checkmark = document.createElement("span");
|
note: "Increases the spawnpoint glow size for members of your clan",
|
||||||
checkmark.className = "checkmark";
|
},
|
||||||
label.className = "checkbox";
|
{
|
||||||
label.append(checkmark);
|
for: "detailedTeamPercentage", type: "checkbox",
|
||||||
checkboxFields[item.for] = element;
|
label: "Detailed team pie chart percentage",
|
||||||
} else label.append(document.createElement("br"));
|
note: "For example: this would show 25.82% instead of 26% on the pie chart in team games"
|
||||||
settingsContainer.append(label, document.createElement("br"));
|
},
|
||||||
|
{
|
||||||
|
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();
|
||||||
});
|
});
|
||||||
this.save = function() {
|
Object.keys(checkboxFields).forEach(function (key) {
|
||||||
Object.keys(inputFields).forEach(function(key) { settings[key] = inputFields[key].value.trim(); });
|
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));
|
||||||
|
// 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));
|
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();
|
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"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const fileInput = document.createElement("input");
|
this.syncFields = function () {
|
||||||
fileInput.type = "file";
|
Object.keys(inputFields).forEach(function (key) {
|
||||||
function handleFileSelect(event) {
|
inputFields[key].value = settings[key];
|
||||||
const input = event.target;
|
});
|
||||||
/** @type {File} */
|
Object.keys(checkboxFields).forEach(function (key) {
|
||||||
const selectedFile = input.files[0];
|
checkboxFields[key].checked = settings[key];
|
||||||
if (!selectedFile) return;
|
});
|
||||||
|
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);
|
||||||
|
|
||||||
input.removeEventListener("change", handleFileSelect);
|
if (settings.customBackgroundUrl !== "") {
|
||||||
input.value = "";
|
document.body.style.backgroundImage =
|
||||||
if (!selectedFile.name.endsWith(".json")) return alert("Invalid file format");
|
"url(" + settings.customBackgroundUrl + ")";
|
||||||
const fileReader = new FileReader();
|
document.body.style.backgroundSize = "cover";
|
||||||
fileReader.onload = function() {
|
document.body.style.backgroundPosition = "center";
|
||||||
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() {
|
__fx.makeMainMenuTransparent = settings.customBackgroundUrl !== "";
|
||||||
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() {
|
if (settings.useFullscreenMode) tryEnterFullscreen();
|
||||||
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));
|
export function tryEnterFullscreen() {
|
||||||
};
|
if (document.fullscreenElement !== null || !document.fullscreenEnabled) return;
|
||||||
this.resetAll = function() {
|
document.documentElement
|
||||||
if (!confirm("Are you Really SURE you want to RESET ALL SETTINGS back to the default?")) return;
|
.requestFullscreen({ navigationUI: "hide" })
|
||||||
localStorage.removeItem("fx_settings");
|
.then(() => {
|
||||||
window.location.reload();
|
console.log("Fullscreen mode activated");
|
||||||
};
|
})
|
||||||
this.applySettings = function() {
|
.catch((error) => {
|
||||||
//setVarByName("bu", "px " + settings.fontName);
|
console.warn("Could not enter fullscreen mode:", error);
|
||||||
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 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];
|
||||||
console.log(fileInput.files);
|
console.log(fileInput.files);
|
||||||
console.log(fileInput.files[0]);
|
console.log(fileInput.files[0]);
|
||||||
if (selectedFile) {
|
if (selectedFile) {
|
||||||
const fileUrl = URL.createObjectURL(selectedFile);
|
const fileUrl = URL.createObjectURL(selectedFile);
|
||||||
console.log("File URL:", fileUrl);
|
console.log("File URL:", fileUrl);
|
||||||
fileInput.value = "";
|
fileInput.value = "";
|
||||||
fileInput.removeEventListener("change", handleFileSelect);
|
fileInput.removeEventListener("change", handleFileSelect);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
WindowManager.add({
|
WindowManager.add({
|
||||||
name: "settings",
|
name: "settings",
|
||||||
element: document.querySelector(".settings"),
|
element: document.querySelector(".settings"),
|
||||||
beforeOpen: function() { settingsManager.syncFields(); }
|
beforeOpen: function () {
|
||||||
|
settingsManager.syncFields();
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (localStorage.getItem("fx_settings") !== null) {
|
if (localStorage.getItem("fx_settings") !== null) {
|
||||||
__fx.settings = settings = {...settings, ...JSON.parse(localStorage.getItem("fx_settings"))};
|
__fx.settings = settings = {
|
||||||
|
...settings,
|
||||||
|
...JSON.parse(localStorage.getItem("fx_settings")),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
settingsManager.applySettings();
|
settingsManager.applySettings();
|
||||||
|
|
||||||
export default settingsManager;
|
export default settingsManager;
|
||||||
export function getSettings() { return settings; };
|
export function getSettings() {
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
|
|
@ -1,27 +1,63 @@
|
||||||
var windows = {};
|
import { getSettings, tryEnterFullscreen } from "./settings.js";
|
||||||
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 }
|
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) {
|
||||||
|
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) {
|
||||||
|
if (windowObj.closable !== false) closeWindow(windowObj.name);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
document.addEventListener(
|
||||||
|
"mousedown",
|
||||||
|
(e) => {
|
||||||
|
// when clicking outside a window
|
||||||
|
if (!container.contains(e.target)) closeAll();
|
||||||
|
|
||||||
|
const isFullScreenEnabled = getSettings().useFullscreenMode;
|
||||||
|
|
||||||
|
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 };
|
||||||
|
|
|
@ -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><div class="window flex settings" style="display:none">
|
<span id="windowContainer"><div class="window flex-column settings" style="display:none">
|
||||||
<h1>Settings</h1>
|
<h1>Settings</h1>
|
||||||
<div class="scrollable"></div>
|
<div class="scrollable"></div>
|
||||||
<hr>
|
<hr>
|
||||||
|
@ -70,6 +70,11 @@
|
||||||
<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>
|
||||||
|
@ -78,6 +83,11 @@
|
||||||
<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>
|
||||||
|
|
|
@ -34,15 +34,6 @@
|
||||||
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 {
|
||||||
|
@ -54,6 +45,77 @@ hr {
|
||||||
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 {
|
||||||
|
@ -96,16 +158,16 @@ td {
|
||||||
#playerlist_content.clickable td:hover { background-color: #00ff0040; }
|
#playerlist_content.clickable td:hover { background-color: #00ff0040; }
|
||||||
|
|
||||||
tr.new {
|
tr.new {
|
||||||
animation: flashAnimation 0.4s ease-out;
|
animation: flashAnimation 0.4s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes flashAnimation {
|
@keyframes flashAnimation {
|
||||||
0% {
|
0% {
|
||||||
background-color: #ffffffaa;
|
background-color: #ffffffaa;
|
||||||
}
|
}
|
||||||
100% {
|
100% {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
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);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
Loading…
Reference in New Issue