Compare commits

...

5 Commits

Author SHA1 Message Date
peshomir ed94c65172 Mobile keybinds
- Attack percentage bar is now always redrawn after each keybind use
- UI changes: Slightly decreased the UI font size, adjusted the size of the inputs in the keybind settings and added a small margin under the keybind settings
2025-03-02 21:41:05 +02:00
peshomir 546d17c76b Improvements for modUtils.js
Added the ability to use predefined replacements for certain variables while matching and/or replacing code (name mappings)
2025-03-02 21:02:45 +02:00
peshomir 43b5b1b016 Fix for update v2.04.4 2025-02-28 09:01:37 +02:00
peshomir 3f3ad1a185 matchCode and insertCode functions 2025-02-20 22:32:06 +02:00
peshomir eb216fd0e4 Module system for patches 2025-02-18 12:04:03 +02:00
12 changed files with 228 additions and 32 deletions

View File

@ -5,7 +5,6 @@ import UglifyJS from 'uglify-js';
import fs from 'fs'; import fs from 'fs';
import webpack from 'webpack'; import webpack from 'webpack';
import path from 'path'; import path from 'path';
import applyPatches from './patches/patches.js';
import ModUtils, { minifyCode } from './modUtils.js'; import ModUtils, { minifyCode } from './modUtils.js';
if (!fs.existsSync("./build")) fs.mkdirSync("./build"); if (!fs.existsSync("./build")) fs.mkdirSync("./build");
@ -56,8 +55,9 @@ script = script.replace(/\bS\[(\d+)\]/g, (_match, index) => `"${stringArray[inde
const modUtils = new ModUtils(minifyCode(script)); const modUtils = new ModUtils(minifyCode(script));
import customLobbyPatches from './patches/customLobby.js'; import applyPatches from './patches/main.js';
customLobbyPatches(modUtils); console.log("Applying patches...");
applyPatches(modUtils);
// for versions ^1.99.5.2 // for versions ^1.99.5.2
const minificationResult = UglifyJS.minify(modUtils.script, { const minificationResult = UglifyJS.minify(modUtils.script, {
@ -101,9 +101,10 @@ rawCodeSegments.forEach(code => {
}); });
modUtils.executePostMinifyHandlers(); modUtils.executePostMinifyHandlers();
applyPatches(modUtils);
script = modUtils.script; script = modUtils.script;
console.log("Building client code...")
await buildClientCode(); await buildClientCode();
// the dictionary should maybe get embedded into one of the files in the bundle // the dictionary should maybe get embedded into one of the files in the bundle
fs.writeFileSync( fs.writeFileSync(

View File

@ -5,6 +5,8 @@ import UglifyJS from 'uglify-js';
const escapeRegExp = (/** @type {string} */ string) => string.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&'); const escapeRegExp = (/** @type {string} */ string) => string.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&');
export function minifyCode(/** @type {string} */ script) { export function minifyCode(/** @type {string} */ script) {
// "return" statements outside of a function throw a parse error
script = "()=>{" + script + "}";
const output = UglifyJS.minify(script, { const output = UglifyJS.minify(script, {
compress: false, compress: false,
mangle: false mangle: false
@ -12,7 +14,10 @@ export function minifyCode(/** @type {string} */ script) {
if (output.error) throw output.error; if (output.error) throw output.error;
if (output.warnings) throw (output.warnings); if (output.warnings) throw (output.warnings);
if (output.warnings) console.warn(output.warnings); if (output.warnings) console.warn(output.warnings);
return output.code; let code = output.code;
if (code.endsWith(";")) code = code.slice(0, -1);
code = code.slice(5, -1); // unwrap from function
return code;
} }
class ModUtils { class ModUtils {
@ -34,6 +39,8 @@ class ModUtils {
this.matchRawCode = this.matchRawCode.bind(this); this.matchRawCode = this.matchRawCode.bind(this);
this.replaceCode = this.replaceCode.bind(this); this.replaceCode = this.replaceCode.bind(this);
this.waitForMinification = this.waitForMinification.bind(this); this.waitForMinification = this.waitForMinification.bind(this);
this.matchCode = this.matchCode.bind(this);
this.insertCode = this.insertCode.bind(this);
} }
/** @param {RegExp} expression */ /** @param {RegExp} expression */
@ -71,11 +78,13 @@ class ModUtils {
// Return value example: // Return value example:
// When replaceRawCode or matchRawCode are called with "var1=var2+1;" as the code // 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" } // and this matches "a=b+1;", the returned value will be the object: { var1: "a", var2: "b" }
/** @param {{ [x: string]: string; }} [nameMappings] */
replaceRawCode(/** @type {string} */ raw, /** @type {string} */ result, nameMappings) { replaceRawCode(/** @type {string} */ raw, /** @type {string} */ result, nameMappings) {
const { expression, groups } = this.generateRegularExpression(raw, false, nameMappings); const { expression, groups } = this.generateRegularExpression(raw, false, nameMappings);
let localizerCount = 0; let localizerCount = 0;
let replacementString = result.replaceAll("$", "$$").replaceAll("__L()", "__L)").replaceAll("__L(", "__L,") let replacementString = result.replaceAll("$", "$$").replaceAll("__L()", "__L)").replaceAll("__L(", "__L,")
.replace(/\w+/g, match => { .replace(/\w+/g, match => {
if (nameMappings !== undefined && nameMappings.hasOwnProperty(match)) return nameMappings[match];
// these would get stored as "___localizer1", "___localizer2", ... // these would get stored as "___localizer1", "___localizer2", ...
if (match === "__L") match = "___localizer" + (++localizerCount); if (match === "__L") match = "___localizer" + (++localizerCount);
return groups.hasOwnProperty(match) ? "$" + groups[match] : match; return groups.hasOwnProperty(match) ? "$" + groups[match] : match;
@ -90,8 +99,14 @@ class ModUtils {
} }
matchRawCode(/** @type {string} */ raw, nameMappings) { matchRawCode(/** @type {string} */ raw, nameMappings) {
const { expression, groups } = this.generateRegularExpression(raw, false, nameMappings); const { expression, groups } = this.generateRegularExpression(raw, false, nameMappings);
const expressionMatchResult = this.matchOne(expression); try {
return Object.fromEntries(Object.entries(groups).map(([identifier, groupNumber]) => [identifier, expressionMatchResult[groupNumber]])); const expressionMatchResult = this.matchOne(expression);
return Object.fromEntries(Object.entries(groups).map(
([identifier, groupNumber]) => [identifier, expressionMatchResult[groupNumber]]
));
} catch (e) {
throw new Error("matchRawCode match error:\n\n" + e + "\n\nRaw code: " + raw + "\n");
}
} }
generateRegularExpression(/** @type {string} */ code, /** @type {boolean} */ isForDictionary, nameMappings) { generateRegularExpression(/** @type {string} */ code, /** @type {boolean} */ isForDictionary, nameMappings) {
const groups = {}; const groups = {};
@ -119,9 +134,38 @@ class ModUtils {
let expression = new RegExp(isForDictionary ? raw.replaceAll("@@", "@") : raw, "g"); let expression = new RegExp(isForDictionary ? raw.replaceAll("@@", "@") : raw, "g");
return { expression, groups }; return { expression, groups };
} }
replaceCode(code, replacement, options) {
return this.replaceRawCode(minifyCode(code), replacement); /**
* @typedef {{ dictionary?: { [x: string]: string } }} BaseOptions
* @typedef {BaseOptions & { addToDictionary?: string[] }} MatchCodeOptions
*/
matchCode(code, /** @type {MatchCodeOptions=} */ options) {
const result = this.matchRawCode(minifyCode(code));
if (options?.addToDictionary !== undefined) {
options.addToDictionary.forEach(varName => {
if (result[varName] === undefined)
throw new Error(`matchCode addToDictionary error: ${varName} was undefined in the match results`)
this.addToDictionary(varName, result[varName]);
});
}
return result;
} }
replaceCode(/** @type {string} */ code, /** @type {string} */ replacement, /** @type {BaseOptions=} */ options) {
return this.replaceRawCode(minifyCode(code), replacement, options?.dictionary);
}
/**
* @param {string} code
* @param {string} codeToInsert
* @param {BaseOptions} [options]
*/
insertCode(code, codeToInsert, options) {
const insertionPoint = "/* here */";
if (!code.includes(insertionPoint)) throw new Error("insertCode: No insertion point found");
return this.replaceCode(code.replace(insertionPoint, ""), code.replace(insertionPoint, codeToInsert), options);
}
waitForMinification(/** @type {Function} */ handler) { waitForMinification(/** @type {Function} */ handler) {
this.postMinifyHandlers.push(handler); this.postMinifyHandlers.push(handler);
} }

View File

@ -1,23 +1,17 @@
import ModUtils from '../modUtils.js'; import ModUtils from '../modUtils.js';
// Custom lobby patches // Custom lobby patches
export default (/** @type {ModUtils} */ { replaceCode, replaceRawCode, dictionary: dict, waitForMinification }) => { export default (/** @type {ModUtils} */ { insertCode, replaceRawCode, dictionary: dict, waitForMinification }) => {
// set player id correctly // set player id correctly
replaceCode(`function aBG(aBE) { insertCode(`function aBG(aBE) {
if (!Lobby.aAl) { return -1; } if (!Lobby.aAl) { return -1; }
/* here */
var s = aBE.length; var s = aBE.length;
var qu = Lobby.aAl.qu; var qu = Lobby.aAl.qu;
for (var i = 0; i < s; i++) { if (aBE[i].qu === qu) { return i; } } for (var i = 0; i < s; i++) { if (aBE[i].qu === qu) { return i; } }
return -1; return -1;
}`, `function aBG(aBE) { }`, `if (__fx.customLobby.isActive()) return __fx.customLobby.getPlayerId();`);
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(() => { waitForMinification(() => {
replaceRawCode("this.aHm=function(){i___.rX(),aM.a7U(0),aM.init()}", replaceRawCode("this.aHm=function(){i___.rX(),aM.a7U(0),aM.init()}",
@ -42,10 +36,10 @@ export default (/** @type {ModUtils} */ { replaceCode, replaceRawCode, dictionar
wR===1 && __fx.customLobby.isActive() && ${dict.MenuManager}.${dict.getState}() !== 6 && __fx.customLobby.setActive(false); 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);`) if(8===i.pz&&0===wR)if(4211===d)wS(d);`)
// when leaving a game // 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)}", 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),aN.setState(0),zs||bJ.df.show(),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()), `this.wl=function(zs){a1.gZ||az.oO.a11.length||(az.oO.a11=az.a12.vd()),
__fx.customLobby.isActive() === false && ap.ky.zt(), __fx.customLobby.isActive() === false && ap.ky.zt(),
this.vH=0,bU.zu(),m.n.setState(0),zs||bJ.df.show(),aN.setState(0); this.vH=0,bU.zu(),m.n.setState(0),aN.setState(0),zs||bJ.df.show();
if (__fx.customLobby.isActive()) __fx.customLobby.rejoinLobby(); else 2===this.a3D?i.ky.a3U():1===this.a3D?i.j(19):i.j(5,5)}`) 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 // do not display lobby UI
replaceRawCode(`(sV.style.backdropFilter="blur(4px)",sV.style.webkitBackdropFilter="blur(4px)"),`, replaceRawCode(`(sV.style.backdropFilter="blur(4px)",sV.style.webkitBackdropFilter="blur(4px)"),`,

22
patches/main.js 100644
View File

@ -0,0 +1,22 @@
import fs from 'fs'
import ModUtils from '../modUtils.js';
const modules = await Promise.all(fs.readdirSync("./patches").flatMap(fileName => {
if (fileName === "main.js") return [];
else return import("./" + fileName);
}));
const requiredVariables = new Set(modules.map(module => module.requiredVariables ?? []).flat());
export default function applyPatches(/** @type {ModUtils} */ modUtils) {
const dictionary = modUtils.dictionary;
requiredVariables.forEach(varName => {
if (!dictionary.hasOwnProperty(varName)) {
throw new Error(`"${varName}" is required by a module but not defined the dictionary`);
}
});
// apply patches (default exported function)
modules.forEach(module => module.default(modUtils))
}

View File

@ -0,0 +1,56 @@
export default (/** @type {import('../modUtils.js').default} */ { insertCode, replaceCode, matchCode }) => {
const { mainCanvas, x, y } = insertCode(`this.te = function() {
if (!this.b()) { return; }
mainCanvas.drawImage(canvas, x, this.y);
/* here */
}`, `if (__fx.settings.keybindButtons) __fx.mobileKeybinds.draw(mainCanvas, x, this.y);`)
const { h, redraw } = insertCode(`a6k = Math.floor(3 * this.h / 2);
a4M = c.pZ.rN(1, Math.floor(0.5 * this.h));
canvas = document.createElement("canvas");
canvas.width = w;
/* here */
canvas.height = this.h;
ctx = canvas.getContext("2d", { alpha: true });
ctx.font = a4M;
c.pZ.textBaseline(ctx, 1);
c.pZ.textAlign(ctx, 1);
this.a6m();
redraw();
`, `__fx.mobileKeybinds.setSize(w, this.h, mainCanvas)`, { dictionary: { mainCanvas } })
const { ba, gap } = matchCode(`this.h = Math.floor(0.066 * h___.pb); w = h___.w - 4 * ba.gap - this.h;`);
const { bd, requestRepaint } = insertCode(`this.gm = function(kt, ku) {
if (!this.b()) { return false; }
/* here */
if (!a.a0n(kt, ku)) { return false; }
aR.mC = false;
if (a6w(this, kt, ku)) { bd.requestRepaint = true; }
return true;
};`,
`if (__fx.settings.keybindButtons && ku > this.y - Math.floor(ba.gap / 4) - this.h && ku < this.y - Math.floor(ba.gap / 4) && __fx.mobileKeybinds.click(kt - x)) return true;`,
{ dictionary: { x, y, h, ba, gap } }
)
insertCode(
`var a6l = 11 / 12; /* here */`,
`__fx.keybindFunctions.repaintAttackPercentageBar = function() { redraw(); bd.requestRepaint = true; };`,
{ dictionary: { redraw, bd, requestRepaint } }
)
// fix to correctly display peace vote menu and game messages (prevent overlap with keybind buttons)
replaceCode(`if (a.a4y(aM.a4u())) {
if (au.b) { return a.y - a.h - 2 * a4a; }
else { return a.y - a4a; }
}`, `if (a.a4y(aM.a4u())) {
if (au.b) { return __fx.settings.keybindButtons ? a.y - 2 * a.h - 3 * a4a : a.y - a.h - 2 * a4a; }
else { return __fx.settings.keybindButtons ? a.y - a.h - 2 * a4a : a.y - a4a; }
}`)
insertCode(
`if (a.a4y(aM.a4u())) { return /* here */ a.y - h - ba.gap; }`,
`__fx.settings.keybindButtons ? a.y - 2 * (h + ba.gap) : `
)
}

View File

@ -1,6 +1,12 @@
import assets from '../assets.js'; import assets from '../assets.js';
import ModUtils from '../modUtils.js'; import ModUtils from '../modUtils.js';
export default (/** @type {ModUtils} */ { replace, replaceOne, replaceRawCode, dictionary, matchOne, matchRawCode, escapeRegExp }) => {
export default (/** @type {ModUtils} */ modUtils) => {
modUtils.waitForMinification(() => applyPatches(modUtils))
}
//export const requiredVariables = ["game", "playerId", "playerData", "rawPlayerNames", "gIsSingleplayer", "playerTerritories"];
function applyPatches(/** @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;

View File

@ -1,7 +1,10 @@
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]]
}; };
export const getUIGap = () => Math.floor(window[dictionary.uiSizes]?.[dictionary.gap] ?? 10);

View File

@ -1,10 +1,66 @@
import { getUIGap } from "./gameInterface.js";
import { getSettings } from "./settings.js"; import { getSettings } from "./settings.js";
export const keybindFunctions = { setAbsolute: () => {}, setRelative: () => {} }; export const keybindFunctions = {
setAbsolute: () => {},
setRelative: () => {},
repaintAttackPercentageBar: () => {}
};
export const keybindHandler = key => { export const keybindHandler = key => {
const keybindData = getSettings().attackPercentageKeybinds.find(keybind => keybind.key === key); const keybindData = getSettings().attackPercentageKeybinds.find(keybind => keybind.key === key);
if (keybindData === undefined) return false; if (keybindData === undefined) return false;
if (keybindData.type === "absolute") keybindFunctions.setAbsolute(keybindData.value); executeKeybind(keybindData);
else keybindFunctions.setRelative(keybindData.value);
return true; return true;
}; };
function executeKeybind(keybind) {
if (keybind.type === "absolute") keybindFunctions.setAbsolute(keybind.value);
else keybindFunctions.setRelative(keybind.value);
keybindFunctions.repaintAttackPercentageBar();
}
// mobile keybinds (keybind buttons)
let canvas;
let width = 0;
let height = 0;
const maxCount = 6;
export const mobileKeybinds = {
setSize: (w, h, mainCanvas) => {
if (getSettings().keybindButtons !== true) return;
width = w;
height = h;
// redraw
canvas = document.createElement("canvas");
canvas.width = w;
canvas.height = h;
const ctx = canvas.getContext("2d");
const fontName = mainCanvas.font.split("px ", 2)[1];
ctx.font = "bold " + h / 2 + "px " + fontName;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
const keybinds = getSettings().attackPercentageKeybinds.slice(0, maxCount);
const gap = getUIGap() / 4;
const buttonWidth = (w - gap * (maxCount - 1)) / maxCount;
keybinds.forEach((keybind, i) => {
ctx.fillStyle = "rgba(0, 0, 0, 0.8)";
ctx.fillRect(i * (buttonWidth + gap), 0, buttonWidth, h);
ctx.fillStyle = "white";
const label = keybind.type === "absolute" ? (keybind.value * 100).toFixed() + "%" : "x " + Math.round(keybind.value * 100) / 100;
ctx.fillText(label, (i + 0.5) * (buttonWidth + gap), h / 2);
});
},
click: (xRelative) => {
if (xRelative < 0 || xRelative > width) return false;
const keybinds = getSettings().attackPercentageKeybinds;
const index = Math.floor(xRelative / width * maxCount);
if (index >= keybinds.length) return false;
executeKeybind(keybinds[index]);
return true;
},
draw: (mainCanvas, x, y) => {
mainCanvas.drawImage(canvas, x, y - (height + getUIGap() / 4));
}
}

View File

@ -1,4 +1,4 @@
export function KeybindsInput(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");
@ -6,6 +6,7 @@ export function KeybindsInput(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";
this.container = keybindContainer; this.container = keybindContainer;
this.keys = [ "key", "type", "value" ]; this.keys = [ "key", "type", "value" ];
this.objectArray = []; this.objectArray = [];

View File

@ -1,5 +1,5 @@
const fx_version = '0.6.7.1'; // FX Client Version const fx_version = '0.6.7.2'; // FX Client Version
const fx_update = 'Feb 15'; // FX Client Last Updated const fx_update = 'Mar 2'; // FX Client Last Updated
if ("serviceWorker" in navigator) { if ("serviceWorker" in navigator) {
navigator.serviceWorker.addEventListener("message", (e) => { navigator.serviceWorker.addEventListener("message", (e) => {
@ -20,7 +20,7 @@ import winCounter from "./winCounter.js";
import playerList from "./playerList.js"; 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, mobileKeybinds } from "./keybinds.js";
import customLobby from './customLobby.js'; import customLobby from './customLobby.js';
window.__fx = window.__fx || {}; window.__fx = window.__fx || {};
@ -33,6 +33,7 @@ __fx.utils = gameScriptUtils;
__fx.WindowManager = WindowManager; __fx.WindowManager = WindowManager;
__fx.keybindFunctions = keybindFunctions; __fx.keybindFunctions = keybindFunctions;
__fx.keybindHandler = keybindHandler; __fx.keybindHandler = keybindHandler;
__fx.mobileKeybinds = mobileKeybinds;
__fx.donationsTracker = donationsTracker; __fx.donationsTracker = donationsTracker;
__fx.playerList = playerList; __fx.playerList = playerList;
__fx.hoveringTooltip = hoveringTooltip; __fx.hoveringTooltip = hoveringTooltip;

View File

@ -21,6 +21,7 @@ var settings = {
detailedTeamPercentage: false, detailedTeamPercentage: false,
//"customMapFileBtn": true //"customMapFileBtn": true
customBackgroundUrl: "", customBackgroundUrl: "",
keybindButtons: false,
attackPercentageKeybinds: [], attackPercentageKeybinds: [],
}; };
__fx.settings = settings; __fx.settings = settings;
@ -102,6 +103,10 @@ const settingsManager = new (function () {
"A custom image to be shown as the main menu background instead of the currently selected map.", "A custom image to be shown as the main menu background instead of the currently selected map.",
}, },
KeybindsInput, KeybindsInput,
{
for: "keybindButtons", type: "checkbox",
label: "Keybind buttons", note: "Show keybind buttons above the troop selector (max 6)"
}
]; ];
const settingsContainer = document.querySelector(".settings .scrollable"); const settingsContainer = document.querySelector(".settings .scrollable");
var inputFields = {}; // (includes select menus) var inputFields = {}; // (includes select menus)

View File

@ -28,7 +28,7 @@
border-width : 2px; border-width : 2px;
border-width : calc(0.15 * (1vw + 1vh)); border-width : calc(0.15 * (1vw + 1vh));
font-size : 20px; font-size : 20px;
font-size : calc(14px + ((0.5 * (1.1vw - 0.1vh)) + 0.14rem)); font-size : calc(13px + ((0.5 * (1.1vw - 0.1vh)) + 0.14rem));
max-height : 90%; max-height : 90%;
transition : 0.2s; transition : 0.2s;
z-index : 10; z-index : 10;
@ -59,6 +59,13 @@
margin: 0px; margin: 0px;
} }
.keybinds-input {
margin-bottom: 1em;
}
.keybinds-input input {
width: 10em;
}
.flex { .flex {
display: flex; display: flex;
} }