Compare commits

...

5 Commits

Author SHA1 Message Date
peshomir 23a272385c Bump version 2025-07-08 18:06:08 +03:00
peshomir 1b5a60d624 Set the BetterTT density style as the default 2025-07-08 18:05:10 +03:00
peshomir ade2d45708 Add automatic error reporting 2025-07-08 17:53:39 +03:00
peshomir 998ecc3bd4 Add "build-client" and "patch" scripts to package.json 2025-07-03 18:22:06 +03:00
peshomir 14c94507f3 Fix floating point number imprecision when configuring keybinds
Refactor keybind input code to improve performance and fix inconsistencies
2025-07-03 16:21:11 +03:00
7 changed files with 224 additions and 171 deletions

View File

@ -15,6 +15,7 @@ fs.writeFileSync("./build/index.html", fs.readFileSync("./build/index.html").toS
fs.writeFileSync("./build/sw.js", fs.readFileSync("./build/sw.js").toString().replace("buildTimestamp", buildTimestamp));
const buildClientCode = () => /** @type {Promise<void>} */(new Promise((resolve, reject) => {
console.log("Building client code...");
webpack({
mode: 'production',
entry: { fxClient: "./src/main.js" },
@ -27,16 +28,22 @@ const buildClientCode = () => /** @type {Promise<void>} */(new Promise((resolve,
if (err.details) console.error(err.details);
return reject(err);
}
const info = stats.toJson();
if (stats.hasWarnings()) console.warn(info.warnings);
if (stats.hasErrors()) {
console.error(info.errors);
reject("Webpack compilation error");
const info = stats?.toJson();
if (stats?.hasWarnings()) console.warn(info?.warnings);
if (stats?.hasErrors()) {
console.error(info?.errors);
return reject("Webpack compilation error");
}
else resolve();
fs.writeFileSync(
"./build/fx.bundle.js",
Buffer.concat([fs.readFileSync("./game/build_artefacts.js"), fs.readFileSync("./build/fx.bundle.js")])
);
resolve();
});
}));
async function patchGameCode() {
let script = fs.readFileSync('./game/latest.js', { encoding: 'utf8' }).trim();
const exposeVarsToGlobalScope = true;
@ -55,7 +62,7 @@ script = script.replace(/\bS\[(\d+)\]/g, (_match, index) => `"${stringArray[inde
const modUtils = new ModUtils(minifyCode(script));
import applyPatches from './patches/main.js';
const { default: applyPatches } = await import('./patches/main.js');
console.log("Applying patches...");
applyPatches(modUtils);
@ -103,14 +110,10 @@ rawCodeSegments.forEach(code => {
modUtils.executePostMinifyHandlers();
script = modUtils.script;
console.log("Building client code...")
await buildClientCode();
// the dictionary should maybe get embedded into one of the files in the bundle
fs.writeFileSync(
"./build/fx.bundle.js",
"./game/build_artefacts.js",
`const buildTimestamp = "${buildTimestamp}"; const dictionary = ${JSON.stringify(dictionary)};\n`
+ fs.readFileSync("./build/fx.bundle.js").toString()
);
console.log("Formatting code...");
@ -138,4 +141,9 @@ script = beautify(script, {
fs.writeFileSync("./build/game.js", script);
console.log("Wrote ./build/game.js");
}
if (!process.argv.includes("--skip-patching")) await patchGameCode();
if (!process.argv.includes("--patch-only")) await buildClientCode();
console.log("Build done");

View File

@ -6,7 +6,9 @@
"type": "module",
"scripts": {
"build": "node index.js",
"build-only": "node build.js"
"build-only": "node build.js",
"build-client": "node build.js --skip-patching",
"patch": "node build.js --patch-only"
},
"repository": {
"type": "git",

View File

@ -2,6 +2,15 @@ import assets from '../assets.js';
import ModUtils from '../modUtils.js';
export default (/** @type {ModUtils} */ modUtils) => {
// Disable built-in Territorial.io error reporting
modUtils.insertCode(
`window.removeEventListener("error", err);
msg = e.lineno + " " + e.colno + "|" + getStack(e); /* here */`,
`__fx.utils.reportError(e, msg);
return alert("Error:\\n" + e.filename + " " + e.lineno + " " + e.colno + " " + e.message);`
)
modUtils.waitForMinification(() => applyPatches(modUtils))
}
//export const requiredVariables = ["game", "playerId", "playerData", "rawPlayerNames", "gIsSingleplayer", "playerTerritories"];
@ -301,10 +310,6 @@ canvas.font=aY.g0.g1(1,fontSize),canvas.fillStyle="rgba("+gR+","+tD+","+hj+",0.6
replaceRawCode(`,this.hostnameIsValid=0<=window.location.hostname.toLowerCase().indexOf("territorial.io"),`,
`,this.hostnameIsValid=true,`)
// Disable built-in Territorial.io error reporting
replaceOne(/window\.addEventListener\("error",function (\w+)\((\w+)\){/g,
'$& window.removeEventListener("error", $1); return alert("Error:\\n" + $2.filename + " " + $2.lineno + " " + $2.colno + " " + $2.message);');
console.log('Removing ads...');
// Remove ads
replace('//api.adinplay.com/libs/aiptag/pub/TRT/territorial.io/tag.min.js', '');

View File

@ -1,23 +1,43 @@
import { getSettings } from "./settings.js";
import { getVar } from "./gameInterface.js";
const utils = new (function() {
this.getMaxTroops = function(playerTerritories, playerID) { return (playerTerritories[playerID]*150).toString(); };
this.getDensity = function(playerID, playerBalances = getVar("playerBalances"), playerTerritories = getVar("playerTerritories")) {
// Example usage from game script: __fx.utils.getMaxTroops(...)
function getMaxTroops(playerTerritories, playerID) {
return (playerTerritories[playerID] * 150).toString();
};
function getDensity(playerID, playerBalances = getVar("playerBalances"), playerTerritories = getVar("playerTerritories")) {
if (getSettings().densityDisplayStyle === "percentage") return (((playerBalances[playerID] / ((playerTerritories[playerID] === 0 ? 1 : playerTerritories[playerID]) * 150)) * 100).toFixed(1) + "%");
else return (playerBalances[playerID] / (playerTerritories[playerID] === 0 ? 1 : playerTerritories[playerID])).toFixed(1);
};
this.isPointInRectangle = function(x, y, rectangleStartX, rectangleStartY, width, height) {
function isPointInRectangle(x, y, rectangleStartX, rectangleStartY, width, height) {
return x >= rectangleStartX && x <= rectangleStartX + width && y >= rectangleStartY && y <= rectangleStartY + height;
};
/** @param {CanvasRenderingContext2D} canvas @param {string} text */
this.fillTextMultiline = function(canvas, text, x, y, maxWidth) {
function fillTextMultiline(canvas, text, x, y, maxWidth) {
const lineHeight = parseInt(canvas.font.split(" ").find(part => part.endsWith("px")).slice(0, -2));
text.split("\n").forEach((line, index) => canvas.fillText(line, x, y + index * lineHeight, maxWidth));
}
this.textStyleBasedOnDensity = function(playerID) {
function textStyleBasedOnDensity(playerID) {
const playerBalances = getVar("playerBalances"), playerTerritories = getVar("playerTerritories");
return `hsl(${playerBalances[playerID] / (playerTerritories[playerID] * 1.5)}, 100%, 50%, 1)`;
}
});
export default utils
function reportError(e, message) {
message = e.filename + " " + e.lineno + " " + e.colno + " " + e.message + "\n" + message;
fetch("https://fx.peshomir.workers.dev/stats/errors", {
body: JSON.stringify({
message,
context: {
swState: navigator.serviceWorker?.controller?.state,
location: window.location.toString(),
userAgent: navigator.userAgent,
dictionary,
buildTimestamp,
scripts: Array.from(document.scripts).map(s => s.src)
}
}),
method: "POST"
}).catch(e => alert("Failed to report error: " + e));
}
export default { getMaxTroops, getDensity, isPointInRectangle, fillTextMultiline, textStyleBasedOnDensity, reportError }

View File

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

View File

@ -17,7 +17,7 @@ var settings = {
realisticNames: false,
showPlayerDensity: true,
coloredDensity: true,
densityDisplayStyle: "percentage",
densityDisplayStyle: "absoluteQuotient",
hideBotNames: false,
highlightClanSpawns: false,
detailedTeamPercentage: false,

View File

@ -1,7 +1,10 @@
{
"version": "0.6.9.1",
"lastUpdated": "Jul 3",
"version": "0.6.10",
"lastUpdated": "Jul 8",
"changes": [
"Updated FX Client to the latest game version"
"Fixed floating point number imprecision when configuring keybinds",
"Minor improvements to the keybinds settings menu",
"Added automatic error reporting to simplify bug identification and removal",
"The BetterTT density style (a value from 0 to 150) is now the default way that density is displayed. The percentage style will remain as is it for players who have already used FX Client, but you can change it in the settings at any time."
]
}