Compare commits

..

No commits in common. "23a272385cdaac54f10b26c1606463354f80b8e2" and "05f59c31bda4de1747214d1b7fe8f806d4907946" have entirely different histories.

7 changed files with 171 additions and 224 deletions

210
build.js
View File

@ -15,135 +15,127 @@ 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)); fs.writeFileSync("./build/sw.js", fs.readFileSync("./build/sw.js").toString().replace("buildTimestamp", buildTimestamp));
const buildClientCode = () => /** @type {Promise<void>} */(new Promise((resolve, reject) => { const buildClientCode = () => /** @type {Promise<void>} */(new Promise((resolve, reject) => {
console.log("Building client code...");
webpack({ webpack({
mode: 'production', mode: 'production',
entry: { fxClient: "./src/main.js" }, entry: { fxClient: "./src/main.js" },
output: { output: {
path: path.resolve(import.meta.dirname, 'build'), path: path.resolve(import.meta.dirname, 'build'),
filename: 'fx.bundle.js', filename: 'fx.bundle.js',
}, },
}, (err, stats) => { }, (err, stats) => {
if (err) { if (err) {
if (err.details) console.error(err.details); if (err.details) console.error(err.details);
return reject(err); return reject(err);
} }
const info = stats?.toJson(); const info = stats.toJson();
if (stats?.hasWarnings()) console.warn(info?.warnings); if (stats.hasWarnings()) console.warn(info.warnings);
if (stats?.hasErrors()) { if (stats.hasErrors()) {
console.error(info?.errors); console.error(info.errors);
return reject("Webpack compilation error"); reject("Webpack compilation error");
} }
fs.writeFileSync( else resolve();
"./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();
let script = fs.readFileSync('./game/latest.js', { encoding: 'utf8' }).trim(); const exposeVarsToGlobalScope = true;
// need to first remove the iife wrapper so the top-level functions aren't inlined
if (exposeVarsToGlobalScope && script.startsWith("\"use strict\"; (function () {") && script.endsWith("})();"))
script = script.slice("\"use strict\"; (function () {".length, -"})();".length);
if (exposeVarsToGlobalScope && script.startsWith("(function () {") && script.endsWith("})();"))
script = script.slice("(function () {".length, -"})();".length);
const exposeVarsToGlobalScope = true; // uncompress strings
// need to first remove the iife wrapper so the top-level functions aren't inlined // this will break if the sequence `"];` appears in one of the strings
if (exposeVarsToGlobalScope && script.startsWith("\"use strict\"; (function () {") && script.endsWith("})();")) const stringArrayRaw = script.match(/var S=(\[.+?"\]);/)?.[1];
script = script.slice("\"use strict\"; (function () {".length, -"})();".length); if (stringArrayRaw === undefined) throw new Error("cannot find the string array");
if (exposeVarsToGlobalScope && script.startsWith("(function () {") && script.endsWith("})();")) const stringArray = JSON.parse(stringArrayRaw);
script = script.slice("(function () {".length, -"})();".length); script = script.replace(/\bS\[(\d+)\]/g, (_match, index) => `"${stringArray[index]}"`);
// uncompress strings const modUtils = new ModUtils(minifyCode(script));
// this will break if the sequence `"];` appears 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 applyPatches from './patches/main.js';
console.log("Applying patches...");
applyPatches(modUtils);
const { default: applyPatches } = await import('./patches/main.js'); // for versions ^1.99.5.2
console.log("Applying patches..."); const minificationResult = UglifyJS.minify(modUtils.script, {
applyPatches(modUtils); "compress": { "arrows": false },
"mangle": false
// for versions ^1.99.5.2 });
const minificationResult = UglifyJS.minify(modUtils.script, { if (minificationResult.error) {
"compress": { "arrows": false }, console.log("error while passing through UglifyJS, replaceCode replacements might have caused errors");
"mangle": false throw 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);
modUtils.script = minificationResult.code;
const {
matchDictionaryExpression,
generateRegularExpression
} = modUtils;
const dictionary = modUtils.dictionary;
[
/,this\.(?<gIsTeamGame>\w+)=this\.\w+<7\|\|9===this\.\w+,/g,
/=function\((\w+),(\w+),\w+\){\1===(?<game>\w+)\.(?<playerId>\w+)\?\w+\(175," "\+\w+\(\d+,\[(?<playerData>\w+)\.(?<playerNames>\w+)\[\2\]\]\)\+": ",1001,\2,\w+\(/g,
/function \w+\(\)\{if\(2===(?<game>\w+)\.(?<gameState>\w+)\)return 1;\w+\.\w+\(\),\1\.\2=2,\1\.\w+=\1.\w+\}/g,
/(function \w+\((\w+),(?<fontSize>\w+),(?<x>\w+),(?<y>\w+),(?<canvas>\w+)\){)(\6\.fillText\((?<playerData>\w+)\.(?<playerNames>\w+)\[\2\],\4,\5\)),(\2<(?<game>\w+)\.(?<gHumans>\w+)&&2!==\8\.(?<playerStates>\w+)\[[^}]+)}/g,
/\w+\.font=(?<fontGeneratorFunction>\w+\.\w+\.\w+)\(1,\.39\*this\.\w+\),/g
].forEach(matchDictionaryExpression);
const rawCodeSegments = [
`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])])+" ",`,
"1===a.b?this.@gLobbyMaxJoin=this.@gHumans:this.gLobbyMaxJoin=this.@data.@playerCount,this.tZ=this.gLobbyMaxJoin,this.@gBots=this.gLobbyMaxJoin-this.gHumans,this.sg=0,",
"[0]=__L(),@strs[1]=@game.@gIsSingleplayer?__L():__L(),",
"?(this.gB=Math.floor(.066*aK.fw),g5=aK.g5-4*@uiSizes.@gap-this.gB):",
`for(a0L=new Array(@game.@gMaxPlayers),a0A.font=a07,@i=game.gMaxPlayers-1;0<=i;i--)a0L[i]=i+1+".",@playerData.@playerNames[i]=aY.qW.tm(playerData.@rawPlayerNames[i],a07,a0W),a0K[i]=Math.floor(a0A.measureText(playerData.playerNames[i]).width);`,
`var dt=@MenuManager.@getState();if(6===dt){if(4211===d)`
]
rawCodeSegments.forEach(code => {
const { expression } = generateRegularExpression(code, true);
//console.log(expression);
matchDictionaryExpression(expression);
});
modUtils.executePostMinifyHandlers();
script = modUtils.script;
// the dictionary should maybe get embedded into one of the files in the bundle
fs.writeFileSync(
"./game/build_artefacts.js",
`const buildTimestamp = "${buildTimestamp}"; const dictionary = ${JSON.stringify(dictionary)};\n`
);
console.log("Formatting code...");
script = beautify(script, {
"indent_size": 1,
"indent_char": "\t",
"max_preserve_newlines": 5,
"preserve_newlines": true,
"keep_array_indentation": false,
"break_chained_methods": false,
"indent_scripts": "normal",
"brace_style": "collapse",
//"brace_style": "expand",
"space_before_conditional": true,
"unescape_strings": false,
"jslint_happy": false,
"end_with_newline": false,
"wrap_line_length": 250,
"indent_inner_html": false,
"comma_first": false,
"e4x": false,
"indent_empty_lines": false
});
fs.writeFileSync("./build/game.js", script);
console.log("Wrote ./build/game.js");
} }
if (minificationResult.warnings) console.log(minificationResult.warnings);
modUtils.script = minificationResult.code;
if (!process.argv.includes("--skip-patching")) await patchGameCode(); const {
if (!process.argv.includes("--patch-only")) await buildClientCode(); matchDictionaryExpression,
generateRegularExpression
} = modUtils;
const dictionary = modUtils.dictionary;
[
/,this\.(?<gIsTeamGame>\w+)=this\.\w+<7\|\|9===this\.\w+,/g,
/=function\((\w+),(\w+),\w+\){\1===(?<game>\w+)\.(?<playerId>\w+)\?\w+\(175," "\+\w+\(\d+,\[(?<playerData>\w+)\.(?<playerNames>\w+)\[\2\]\]\)\+": ",1001,\2,\w+\(/g,
/function \w+\(\)\{if\(2===(?<game>\w+)\.(?<gameState>\w+)\)return 1;\w+\.\w+\(\),\1\.\2=2,\1\.\w+=\1.\w+\}/g,
/(function \w+\((\w+),(?<fontSize>\w+),(?<x>\w+),(?<y>\w+),(?<canvas>\w+)\){)(\6\.fillText\((?<playerData>\w+)\.(?<playerNames>\w+)\[\2\],\4,\5\)),(\2<(?<game>\w+)\.(?<gHumans>\w+)&&2!==\8\.(?<playerStates>\w+)\[[^}]+)}/g,
/\w+\.font=(?<fontGeneratorFunction>\w+\.\w+\.\w+)\(1,\.39\*this\.\w+\),/g
].forEach(matchDictionaryExpression);
const rawCodeSegments = [
`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])])+" ",`,
"1===a.b?this.@gLobbyMaxJoin=this.@gHumans:this.gLobbyMaxJoin=this.@data.@playerCount,this.tZ=this.gLobbyMaxJoin,this.@gBots=this.gLobbyMaxJoin-this.gHumans,this.sg=0,",
"[0]=__L(),@strs[1]=@game.@gIsSingleplayer?__L():__L(),",
"?(this.gB=Math.floor(.066*aK.fw),g5=aK.g5-4*@uiSizes.@gap-this.gB):",
`for(a0L=new Array(@game.@gMaxPlayers),a0A.font=a07,@i=game.gMaxPlayers-1;0<=i;i--)a0L[i]=i+1+".",@playerData.@playerNames[i]=aY.qW.tm(playerData.@rawPlayerNames[i],a07,a0W),a0K[i]=Math.floor(a0A.measureText(playerData.playerNames[i]).width);`,
`var dt=@MenuManager.@getState();if(6===dt){if(4211===d)`
]
rawCodeSegments.forEach(code => {
const { expression } = generateRegularExpression(code, true);
//console.log(expression);
matchDictionaryExpression(expression);
});
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",
`const buildTimestamp = "${buildTimestamp}"; const dictionary = ${JSON.stringify(dictionary)};\n`
+ fs.readFileSync("./build/fx.bundle.js").toString()
);
console.log("Formatting code...");
script = beautify(script, {
"indent_size": 1,
"indent_char": "\t",
"max_preserve_newlines": 5,
"preserve_newlines": true,
"keep_array_indentation": false,
"break_chained_methods": false,
"indent_scripts": "normal",
"brace_style": "collapse",
//"brace_style": "expand",
"space_before_conditional": true,
"unescape_strings": false,
"jslint_happy": false,
"end_with_newline": false,
"wrap_line_length": 250,
"indent_inner_html": false,
"comma_first": false,
"e4x": false,
"indent_empty_lines": false
});
fs.writeFileSync("./build/game.js", script);
console.log("Wrote ./build/game.js");
console.log("Build done"); console.log("Build done");

View File

@ -6,9 +6,7 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"build": "node index.js", "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": { "repository": {
"type": "git", "type": "git",

View File

@ -2,15 +2,6 @@ import assets from '../assets.js';
import ModUtils from '../modUtils.js'; import ModUtils from '../modUtils.js';
export default (/** @type {ModUtils} */ modUtils) => { 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)) modUtils.waitForMinification(() => applyPatches(modUtils))
} }
//export const requiredVariables = ["game", "playerId", "playerData", "rawPlayerNames", "gIsSingleplayer", "playerTerritories"]; //export const requiredVariables = ["game", "playerId", "playerData", "rawPlayerNames", "gIsSingleplayer", "playerTerritories"];
@ -310,6 +301,10 @@ 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"),`, replaceRawCode(`,this.hostnameIsValid=0<=window.location.hostname.toLowerCase().indexOf("territorial.io"),`,
`,this.hostnameIsValid=true,`) `,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...'); console.log('Removing ads...');
// Remove ads // Remove ads
replace('//api.adinplay.com/libs/aiptag/pub/TRT/territorial.io/tag.min.js', ''); replace('//api.adinplay.com/libs/aiptag/pub/TRT/territorial.io/tag.min.js', '');

View File

@ -1,43 +1,23 @@
import { getSettings } from "./settings.js"; import { getSettings } from "./settings.js";
import { getVar } from "./gameInterface.js"; import { getVar } from "./gameInterface.js";
// Example usage from game script: __fx.utils.getMaxTroops(...) const utils = new (function() {
this.getMaxTroops = function(playerTerritories, playerID) { return (playerTerritories[playerID]*150).toString(); };
function getMaxTroops(playerTerritories, playerID) { this.getDensity = function(playerID, playerBalances = getVar("playerBalances"), playerTerritories = getVar("playerTerritories")) {
return (playerTerritories[playerID] * 150).toString(); 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);
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) + "%"); this.isPointInRectangle = function(x, y, rectangleStartX, rectangleStartY, width, height) {
else return (playerBalances[playerID] / (playerTerritories[playerID] === 0 ? 1 : playerTerritories[playerID])).toFixed(1); return x >= rectangleStartX && x <= rectangleStartX + width && y >= rectangleStartY && y <= rectangleStartY + height;
}; };
function isPointInRectangle(x, y, rectangleStartX, rectangleStartY, width, height) { /** @param {CanvasRenderingContext2D} canvas @param {string} text */
return x >= rectangleStartX && x <= rectangleStartX + width && y >= rectangleStartY && y <= rectangleStartY + height; this.fillTextMultiline = function(canvas, text, x, y, maxWidth) {
}; const lineHeight = parseInt(canvas.font.split(" ").find(part => part.endsWith("px")).slice(0, -2));
/** @param {CanvasRenderingContext2D} canvas @param {string} text */ text.split("\n").forEach((line, index) => canvas.fillText(line, x, y + index * lineHeight, maxWidth));
function fillTextMultiline(canvas, text, x, y, maxWidth) { }
const lineHeight = parseInt(canvas.font.split(" ").find(part => part.endsWith("px")).slice(0, -2)); this.textStyleBasedOnDensity = function(playerID) {
text.split("\n").forEach((line, index) => canvas.fillText(line, x, y + index * lineHeight, maxWidth)); const playerBalances = getVar("playerBalances"), playerTerritories = getVar("playerTerritories");
} return `hsl(${playerBalances[playerID] / (playerTerritories[playerID] * 1.5)}, 100%, 50%, 1)`;
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,7 +1,4 @@
const truncate = n => parseFloat(n.toFixed(12));
export function KeybindsInput(/** @type {HTMLElement} */ containerElement) { export function KeybindsInput(/** @type {HTMLElement} */ containerElement) {
const header = document.createElement("p"); const header = document.createElement("p");
header.innerText = "Attack Percentage Keybinds"; header.innerText = "Attack Percentage Keybinds";
const keybindContainer = document.createElement("div"); const keybindContainer = document.createElement("div");
@ -9,70 +6,61 @@ export function KeybindsInput(/** @type {HTMLElement} */ containerElement) {
const keybindAddButton = document.createElement("button"); const keybindAddButton = document.createElement("button");
keybindAddButton.innerText = "Add"; keybindAddButton.innerText = "Add";
containerElement.append(header, keybindContainer, keybindAddButton); containerElement.append(header, keybindContainer, keybindAddButton);
containerElement.className = "keybinds-input"; containerElement.className = "keybinds-input";
this.container = keybindContainer; this.container = keybindContainer;
this.objectKeys = ["key", "type", "value"]; this.keys = [ "key", "type", "value" ];
this.objectArray = []; this.objectArray = [];
this.addObject = function () { this.addObject = function () {
this.objectArray.push({ key: "", type: "absolute", value: 0.8 }); this.objectArray.push({ key: "", type: "absolute", value: 0.8 });
this.container.appendChild(createInputRow(this.objectArray.length - 1)); this.displayObjects();
keybindAddButton.scrollIntoView(false); keybindAddButton.scrollIntoView(false);
}; };
keybindAddButton.addEventListener("click", this.addObject.bind(this));
this.update = function (settings) { this.update = function (settings) {
this.objectArray = settings.attackPercentageKeybinds; this.objectArray = settings.attackPercentageKeybinds;
this.displayObjects(); this.displayObjects();
} }
keybindAddButton.addEventListener("click", this.addObject.bind(this));
this.displayObjects = function () { this.displayObjects = function () {
// Clear the content of the container // Clear the content of the container
this.container.innerHTML = ""; this.container.innerHTML = "";
if (this.objectArray.length === 0) return this.container.innerText = "No custom attack percentage keybinds added"; 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 // Loop through the array and display input fields for each object
for (var i = 0; i < this.objectArray.length; i++) { for (var i = 0; i < this.objectArray.length; i++) {
this.container.appendChild(createInputRow(i)); var objectDiv = document.createElement("div");
// Create input fields for each key
this.keys.forEach(function (key) {
let inputField = document.createElement(key === "type" ? "select" : "input");
if (key === "type") {
inputField.innerHTML = '<option value="absolute">Absolute</option><option value="relative">Relative</option>';
inputField.addEventListener("change", this.updateObject.bind(this, i, key));
} else if (key === "key") {
inputField.type = "text";
inputField.setAttribute("readonly", "");
inputField.setAttribute("placeholder", "No key set");
inputField.addEventListener("click", this.startKeyInput.bind(this, i, key));
} else { // key === "value"
const isAbsolute = this.objectArray[i].type === "absolute";
inputField.type = isAbsolute ? "text" : "number";
if (isAbsolute) inputField.addEventListener("click", this.convertIntoNumberInput.bind(this, i, key), { once: true });
else inputField.setAttribute("step", "0.1");
inputField.addEventListener("input", this.updateObject.bind(this, i, key));
}
if (key === "value" && this.objectArray[i].type === "absolute")
inputField.value = this.objectArray[i][key] * 100 + "%";
else inputField.value = this.objectArray[i][key];
// Append input field to the object div
objectDiv.appendChild(inputField);
}, this);
// Button to delete the object
var deleteButton = document.createElement("button");
deleteButton.textContent = "Delete";
deleteButton.addEventListener("click", this.deleteObject.bind(this, i));
// Append delete button to the object div
objectDiv.appendChild(deleteButton);
// Append the object div to the container
this.container.appendChild(objectDiv);
} }
}; };
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));
objectDiv.appendChild(deleteButton);
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) { this.startKeyInput = function (index, property, event) {
event.target.value = "Press any key"; event.target.value = "Press any key";
const handler = this.updateObject.bind(this, index, property); const handler = this.updateObject.bind(this, index, property);
@ -80,28 +68,25 @@ export function KeybindsInput(/** @type {HTMLElement} */ containerElement) {
event.target.addEventListener("blur", () => { event.target.addEventListener("blur", () => {
event.target.removeEventListener('keydown', handler); event.target.removeEventListener('keydown', handler);
event.target.value = this.objectArray[index][property]; event.target.value = this.objectArray[index][property];
//this.displayObjects();
}, { once: true }); }, { once: true });
}; };
this.convertIntoNumberInput = function (index, property, event) { this.convertIntoNumberInput = function (index, property, event) {
event.target.value = event.target.value.slice(0, -1); event.target.value = event.target.value.slice(0, -1);
event.target.type = "number"; event.target.type = "number";
event.target.addEventListener("blur", () => { event.target.addEventListener("blur", () => {
this.recreateInputField(index, property); //event.target.value = this.objectArray[index][property];
this.displayObjects();
}, { once: true }); }, { once: true });
}; };
this.updateObject = function (index, property, event) { this.updateObject = function (index, property, event) {
if (index >= this.objectArray.length) return; if (index >= this.objectArray.length) return;
// Update the corresponding property of the object in the array // Update the corresponding property of the object in the array
const value = property === "value" ? ( const value = property === "value" ? (
this.objectArray[index].type === "absolute" this.objectArray[index].type === "absolute" ? parseFloat(event.target.value) / 100 : parseFloat(event.target.value)
? truncate(parseFloat(event.target.value) / 100)
: parseFloat(event.target.value)
) : property === "key" ? event.key : event.target.value; ) : property === "key" ? event.key : event.target.value;
this.objectArray[index][property] = 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) { this.deleteObject = function (index) {
// Remove the object from the array // Remove the object from the array

View File

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

View File

@ -1,10 +1,7 @@
{ {
"version": "0.6.10", "version": "0.6.9.1",
"lastUpdated": "Jul 8", "lastUpdated": "Jul 3",
"changes": [ "changes": [
"Fixed floating point number imprecision when configuring keybinds", "Updated FX Client to the latest game version"
"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."
] ]
} }