Modern UI/menu shell for Zyrox client
// ==UserScript==
// @name Zyrox client (gimkit)
// @namespace https://github.com/zyrox
// @version 1.4.5
// @description Modern UI/menu shell for Zyrox client
// @author Zyrox
// @match https://www.gimkit.com/join*
// @run-at document-start
// @icon https://raw.githubusercontent.com/Bob-alt-828100/zyrox-gimkit-client/refs/heads/main/images/logo.png
// @license MIT
// @grant none
// ==/UserScript==
(() => {
"use strict";
// Some userscript runtimes execute bundled code that expects a global `Module`.
// with `enable/disable` methods. Provide a minimal compatible fallback.
if (typeof globalThis.Module === "undefined") {
globalThis.Module = class Module {
constructor(name = "Module", options = {}) {
this.name = name;
this.enabled = false;
this.onEnable = typeof options.onEnable === "function" ? options.onEnable : () => {};
this.onDisable = typeof options.onDisable === "function" ? options.onDisable : () => {};
}
enable() {
if (this.enabled) return;
this.enabled = true;
this.onEnable();
}
disable() {
if (!this.enabled) return;
this.enabled = false;
this.onDisable();
}
};
}
if (window.__ZYROX_UI_MOUNTED__) return;
window.__ZYROX_UI_MOUNTED__ = true;
// ---------------------------------------------------------------------------
// AUTO-ANSWER PAGE-CONTEXT INJECTION
// Injected as a real <script> tag so it runs in page scope and patches
// window.WebSocket BEFORE Gimkit creates its connection.
// Mirrors autoanswer.js 1:1, but exposes window.__zyroxAutoAnswer.start/stop
// so the Zyrox module toggle controls it.
// ---------------------------------------------------------------------------
(function injectAutoAnswerPageContext() {
function pageMain() {
const LOG = "[AutoAnswer][page]";
const colyseusProtocol = { ROOM_DATA: 13 };
function msgpackEncode(value) {
const bytes = [];
const deferred = [];
const write = (input) => {
const type = typeof input;
if (type === "string") {
let len = 0;
for (let i = 0; i < input.length; i++) {
const code = input.charCodeAt(i);
if (code < 128) len++; else if (code < 2048) len += 2; else if (code < 55296 || code > 57343) len += 3; else { i++; len += 4; }
}
if (len < 32) bytes.push(160 | len); else if (len < 256) bytes.push(217, len); else bytes.push(218, len >> 8, len & 255);
deferred.push({ type: "string", value: input, offset: bytes.length });
bytes.length += len;
return;
}
if (type === "number") {
if (Number.isInteger(input) && input >= 0 && input < 128) { bytes.push(input); return; }
if (Number.isInteger(input) && input >= 0 && input < 65536) { bytes.push(205, input >> 8, input & 255); return; }
bytes.push(203); deferred.push({ type: "float64", value: input, offset: bytes.length }); bytes.length += 8; return;
}
if (type === "boolean") { bytes.push(input ? 195 : 194); return; }
if (input == null) { bytes.push(192); return; }
if (Array.isArray(input)) {
const len = input.length;
if (len < 16) bytes.push(144 | len); else bytes.push(220, len >> 8, len & 255);
for (const item of input) write(item);
return;
}
const keys = Object.keys(input);
const len = keys.length;
if (len < 16) bytes.push(128 | len); else bytes.push(222, len >> 8, len & 255);
for (const key of keys) { write(key); write(input[key]); }
};
write(value);
const view = new DataView(new ArrayBuffer(bytes.length));
for (let i = 0; i < bytes.length; i++) view.setUint8(i, bytes[i] & 255);
for (const part of deferred) {
if (part.type === "float64") { view.setFloat64(part.offset, part.value); continue; }
let offset = part.offset;
const s = part.value;
for (let i = 0; i < s.length; i++) {
let code = s.charCodeAt(i);
if (code < 128) view.setUint8(offset++, code);
else if (code < 2048) { view.setUint8(offset++, 192 | (code >> 6)); view.setUint8(offset++, 128 | (code & 63)); }
else { view.setUint8(offset++, 224 | (code >> 12)); view.setUint8(offset++, 128 | ((code >> 6) & 63)); view.setUint8(offset++, 128 | (code & 63)); }
}
}
return view.buffer;
}
function msgpackDecode(buffer, startOffset = 0) {
const view = new DataView(buffer);
let offset = startOffset;
const readString = (len) => {
let out = "";
const end = offset + len;
while (offset < end) {
const byte = view.getUint8(offset++);
if ((byte & 0x80) === 0) out += String.fromCharCode(byte);
else if ((byte & 0xe0) === 0xc0) out += String.fromCharCode(((byte & 0x1f) << 6) | (view.getUint8(offset++) & 0x3f));
else out += String.fromCharCode(((byte & 0x0f) << 12) | ((view.getUint8(offset++) & 0x3f) << 6) | (view.getUint8(offset++) & 0x3f));
}
return out;
};
const read = () => {
const token = view.getUint8(offset++);
if (token < 0x80) return token;
if (token < 0x90) { const size = token & 0x0f; const map = {}; for (let i = 0; i < size; i++) map[read()] = read(); return map; }
if (token < 0xa0) { const size = token & 0x0f; const arr = new Array(size); for (let i = 0; i < size; i++) arr[i] = read(); return arr; }
if (token < 0xc0) return readString(token & 0x1f);
if (token > 0xdf) return token - 256;
switch (token) {
case 192: return null;
case 194: return false;
case 195: return true;
case 202: { const n = view.getFloat32(offset); offset += 4; return n; }
case 203: { const n = view.getFloat64(offset); offset += 8; return n; }
case 204: { const n = view.getUint8(offset); offset += 1; return n; }
case 205: { const n = view.getUint16(offset); offset += 2; return n; }
case 206: { const n = view.getUint32(offset); offset += 4; return n; }
case 208: { const n = view.getInt8(offset); offset += 1; return n; }
case 209: { const n = view.getInt16(offset); offset += 2; return n; }
case 210: { const n = view.getInt32(offset); offset += 4; return n; }
case 217: { const n = view.getUint8(offset); offset += 1; return readString(n); }
case 218: { const n = view.getUint16(offset); offset += 2; return readString(n); }
case 220: { const size = view.getUint16(offset); offset += 2; const arr = new Array(size); for (let i = 0; i < size; i++) arr[i] = read(); return arr; }
case 222: { const size = view.getUint16(offset); offset += 2; const map = {}; for (let i = 0; i < size; i++) map[read()] = read(); return map; }
default: return null;
}
};
const value = read();
return { value, offset };
}
function parseChangePacket(packet) {
const out = [];
for (const change of packet?.changes || []) {
const data = {};
const keys = change[1].map((index) => packet.values[index]);
for (let i = 0; i < keys.length; i++) data[keys[i]] = change[2][i];
out.push({ id: change[0], data });
}
return out;
}
class LocalSocketManager extends EventTarget {
constructor() {
super();
this.socket = null;
this.transportType = "unknown";
this.playerId = null;
this.install();
}
install() {
const manager = this;
const NativeWebSocket = window.WebSocket;
window.WebSocket = class extends NativeWebSocket {
constructor(url, protocols) {
super(url, protocols);
if (String(url || "").includes("gimkitconnect.com")) manager.registerSocket(this);
}
send(data) {
super.send(data);
}
};
}
registerSocket(socket) {
this.socket = socket;
this.transportType = "colyseus";
console.log(LOG, "Registered WebSocket", socket.url);
socket.addEventListener("message", (e) => {
const decoded = this.decodeColyseus(e.data);
if (!decoded) return;
this.dispatchEvent(new CustomEvent("colyseusMessage", { detail: decoded }));
if (decoded.type === "AUTH_ID") {
this.playerId = decoded.message;
console.log(LOG, "Got player id", this.playerId);
}
if (decoded.type === "DEVICES_STATES_CHANGES") {
const parsed = parseChangePacket(decoded.message);
this.dispatchEvent(new CustomEvent("deviceChanges", { detail: parsed }));
}
});
}
decodeColyseus(data) {
const bytes = new Uint8Array(data);
if (bytes[0] !== colyseusProtocol.ROOM_DATA) return null;
const first = msgpackDecode(data, 1);
if (!first) return null;
let message;
if (bytes.byteLength > first.offset) {
const second = msgpackDecode(data, first.offset);
message = second?.value;
}
return { type: first.value, message };
}
sendMessage(channel, payload) {
if (!this.socket) return;
const header = new Uint8Array([colyseusProtocol.ROOM_DATA]);
const a = new Uint8Array(msgpackEncode(channel));
const b = new Uint8Array(msgpackEncode(payload));
const packet = new Uint8Array(header.length + a.length + b.length);
packet.set(header, 0);
packet.set(a, header.length);
packet.set(b, header.length + a.length);
this.socket.send(packet);
}
}
const socketManager = window.socketManager || new LocalSocketManager();
window.socketManager = socketManager;
const state = {
questions: [],
answerDeviceId: null,
currentQuestionId: null,
questionIdList: [],
currentQuestionIndex: -1,
};
function answerQuestion() {
if (socketManager.transportType === "colyseus") {
if (state.currentQuestionId == null || state.answerDeviceId == null) return;
const question = state.questions.find((q) => q._id == state.currentQuestionId);
if (!question) return;
const packet = { key: "answered", deviceId: state.answerDeviceId, data: {} };
if (question.type == "text") packet.data.answer = question.answers[0].text;
else packet.data.answer = question.answers.find((a) => a.correct)?._id;
if (!packet.data.answer) return;
socketManager.sendMessage("MESSAGE_FOR_DEVICE", packet);
console.log(LOG, "Answered colyseus", state.currentQuestionId);
} else {
const questionId = state.questionIdList[state.currentQuestionIndex];
const question = state.questions.find((q) => q._id == questionId);
if (!question) return;
const answer = question.type == "mc" ? question.answers.find((a) => a.correct)?._id : question.answers[0]?.text;
if (!answer) return;
socketManager.sendMessage("QUESTION_ANSWERED", { answer, questionId });
console.log(LOG, "Answered blueboat", questionId);
}
}
socketManager.addEventListener("deviceChanges", (event) => {
for (const { id, data } of event.detail || []) {
for (const key in data || {}) {
if (key === "GLOBAL_questions") {
state.questions = JSON.parse(data[key]);
state.answerDeviceId = id;
console.log(LOG, "Got questions", state.questions.length);
}
if (socketManager.playerId && key === `PLAYER_${socketManager.playerId}_currentQuestionId`) {
state.currentQuestionId = data[key];
}
}
}
});
socketManager.addEventListener("blueboatMessage", (event) => {
if (event.detail?.key !== "STATE_UPDATE") return;
switch (event.detail.data.type) {
case "GAME_QUESTIONS":
state.questions = event.detail.data.value;
break;
case "PLAYER_QUESTION_LIST":
state.questionIdList = event.detail.data.value.questionList;
state.currentQuestionIndex = event.detail.data.value.questionIndex;
break;
case "PLAYER_QUESTION_LIST_INDEX":
state.currentQuestionIndex = event.detail.data.value;
break;
}
});
// Expose start/stop so the Zyrox module toggle controls the interval
let _intervalId = null;
window.__zyroxAutoAnswer = {
start(speed = 1000) {
if (_intervalId) clearInterval(_intervalId);
_intervalId = setInterval(answerQuestion, speed);
},
stop() {
if (_intervalId) { clearInterval(_intervalId); _intervalId = null; }
},
};
console.log(LOG, "Page context ready, waiting for module toggle.");
}
const el = document.createElement("script");
el.textContent = `;(${pageMain.toString()})();`;
(document.head || document.documentElement).appendChild(el);
el.remove();
})();
(function injectEspPageContextBridge() {
function pageMain() {
const LOG = "[ESP][page]";
const shared = {
ready: false,
lastUpdate: 0,
localPlayerId: null,
localTeamId: null,
camera: null,
players: [],
};
window.__zyroxEspShared = shared;
function tick() {
const serializer = window?.serializer;
const characters = serializer?.state?.characters?.$items;
const camera = window?.stores?.phaser?.scene?.cameras?.cameras?.[0];
const localPlayerId = window?.socketManager?.playerId ?? null;
const localCharacter = localPlayerId != null ? characters?.get?.(localPlayerId) : null;
const localTeamId = localCharacter?.teamId ?? null;
if (!characters || typeof characters[Symbol.iterator] !== "function" || !camera || localPlayerId == null || localTeamId == null) {
shared.ready = false;
shared.lastUpdate = Date.now();
requestAnimationFrame(tick);
return;
}
const outPlayers = [];
for (const [id, character] of characters) {
const x = Number(character?.x);
const y = Number(character?.y);
if (!Number.isFinite(x) || !Number.isFinite(y)) continue;
outPlayers.push({
id: String(id ?? character?.id ?? "unknown"),
name: String(character?.name ?? character?.displayName ?? character?.username ?? id ?? "Unknown"),
teamId: character?.teamId ?? null,
x,
y,
});
}
shared.ready = true;
shared.lastUpdate = Date.now();
shared.localPlayerId = localPlayerId;
shared.localTeamId = localTeamId;
shared.camera = {
midX: Number(camera?.midPoint?.x ?? 0),
midY: Number(camera?.midPoint?.y ?? 0),
zoom: Number(camera?.zoom ?? 1),
};
shared.players = outPlayers;
requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
console.log(LOG, "Bridge ready");
}
const el = document.createElement("script");
el.textContent = `;(${pageMain.toString()})();`;
(document.head || document.documentElement).appendChild(el);
el.remove();
})();
function readUserscriptVersion() {
// Update this variable whenever you bump @version above.
const CLIENT_VERSION = "1.4.5";
return CLIENT_VERSION;
}
const CONFIG = {
toggleKey: "\\",
defaultToggleKey: "\\",
title: "Zyrox",
subtitle: "Client",
version: readUserscriptVersion(),
logoUrl: "https://raw.githubusercontent.com/Bob-alt-828100/zyrox-gimkit-client/refs/heads/main/images/logo.png",
};
// --- Core Utilities & Networking (Extracted from Gimkit Cheat) ---
const colyseusProtocol = {
HANDSHAKE: 9,
JOIN_ROOM: 10,
ERROR: 11,
LEAVE_ROOM: 12,
ROOM_DATA: 13,
ROOM_STATE: 14,
ROOM_STATE_PATCH: 15,
ROOM_DATA_SCHEMA: 16,
ROOM_DATA_BYTES: 17,
};
function utf8Read(view, offset) {
const length = view[offset++];
let string = "";
for (let i = offset, end = offset + length; i < end; i++) {
const byte = view[i];
if ((byte & 0x80) === 0x00) {
string += String.fromCharCode(byte);
} else if ((byte & 0xe0) === 0xc0) {
string += String.fromCharCode(((byte & 0x1f) << 6) | (view[++i] & 0x3f));
} else if ((byte & 0xf0) === 0xe0) {
string += String.fromCharCode(((byte & 0x0f) << 12) | ((view[++i] & 0x3f) << 6) | ((view[++i] & 0x3f) << 0));
}
}
return string;
}
function parseChangePacket(packet) {
const out = [];
for (const change of packet?.changes || []) {
const data = {};
const keys = change[1].map((index) => packet.values[index]);
for (let i = 0; i < keys.length; i++) data[keys[i]] = change[2][i];
out.push({ id: change[0], data });
}
return out;
}
function msgpackEncode(value) {
const bytes = [];
const deferred = [];
const write = (input) => {
const type = typeof input;
if (type === "string") {
let len = 0;
for (let i = 0; i < input.length; i++) {
const code = input.charCodeAt(i);
if (code < 128) len++;
else if (code < 2048) len += 2;
else if (code < 55296 || code > 57343) len += 3;
else {
i++;
len += 4;
}
}
if (len < 32) bytes.push(160 | len);
else if (len < 256) bytes.push(217, len);
else if (len < 65536) bytes.push(218, len >> 8, len & 255);
else bytes.push(219, len >> 24, (len >> 16) & 255, (len >> 8) & 255, len & 255);
deferred.push({ type: "string", value: input, offset: bytes.length });
bytes.length += len;
return;
}
if (type === "number") {
if (Number.isInteger(input) && Number.isFinite(input)) {
if (input >= 0) {
if (input < 128) bytes.push(input);
else if (input < 256) bytes.push(204, input);
else if (input < 65536) bytes.push(205, input >> 8, input & 255);
else if (input < 4294967296) bytes.push(206, input >> 24, (input >> 16) & 255, (input >> 8) & 255, input & 255);
else {
const hi = Math.floor(input / Math.pow(2, 32));
const lo = input >>> 0;
bytes.push(207, hi >> 24, (hi >> 16) & 255, (hi >> 8) & 255, hi & 255, lo >> 24, (lo >> 16) & 255, (lo >> 8) & 255, lo & 255);
}
} else if (input >= -32) bytes.push(input);
else if (input >= -128) bytes.push(208, input & 255);
else if (input >= -32768) bytes.push(209, (input >> 8) & 255, input & 255);
else if (input >= -2147483648) bytes.push(210, (input >> 24) & 255, (input >> 16) & 255, (input >> 8) & 255, input & 255);
else {
const hi = Math.floor(input / Math.pow(2, 32));
const lo = input >>> 0;
bytes.push(211, hi >> 24, (hi >> 16) & 255, (hi >> 8) & 255, hi & 255, lo >> 24, (lo >> 16) & 255, (lo >> 8) & 255, lo & 255);
}
return;
}
bytes.push(203);
deferred.push({ type: "float64", value: input, offset: bytes.length });
bytes.length += 8;
return;
}
if (type === "boolean") {
bytes.push(input ? 195 : 194);
return;
}
if (input == null) {
bytes.push(192);
return;
}
if (Array.isArray(input)) {
const len = input.length;
if (len < 16) bytes.push(144 | len);
else if (len < 65536) bytes.push(220, len >> 8, len & 255);
else bytes.push(221, len >> 24, (len >> 16) & 255, (len >> 8) & 255, len & 255);
for (const item of input) write(item);
return;
}
const keys = Object.keys(input).filter((k) => typeof input[k] !== "function");
const len = keys.length;
if (len < 16) bytes.push(128 | len);
else if (len < 65536) bytes.push(222, len >> 8, len & 255);
else bytes.push(223, len >> 24, (len >> 16) & 255, (len >> 8) & 255, len & 255);
for (const key of keys) {
write(key);
write(input[key]);
}
};
write(value);
const view = new DataView(new ArrayBuffer(bytes.length));
for (let i = 0; i < bytes.length; i++) view.setUint8(i, bytes[i] & 255);
for (const part of deferred) {
if (part.type === "float64") {
view.setFloat64(part.offset, part.value);
continue;
}
let offset = part.offset;
const value = part.value;
for (let i = 0; i < value.length; i++) {
let code = value.charCodeAt(i);
if (code < 128) view.setUint8(offset++, code);
else if (code < 2048) {
view.setUint8(offset++, 192 | (code >> 6));
view.setUint8(offset++, 128 | (code & 63));
} else if (code < 55296 || code > 57343) {
view.setUint8(offset++, 224 | (code >> 12));
view.setUint8(offset++, 128 | ((code >> 6) & 63));
view.setUint8(offset++, 128 | (code & 63));
} else {
i++;
code = 65536 + (((code & 1023) << 10) | (value.charCodeAt(i) & 1023));
view.setUint8(offset++, 240 | (code >> 18));
view.setUint8(offset++, 128 | ((code >> 12) & 63));
view.setUint8(offset++, 128 | ((code >> 6) & 63));
view.setUint8(offset++, 128 | (code & 63));
}
}
}
return view.buffer;
}
function msgpackDecode(buffer, startOffset = 0) {
const view = new DataView(buffer);
let offset = startOffset;
const readString = (len) => {
let out = "";
const end = offset + len;
while (offset < end) {
const byte = view.getUint8(offset++);
if ((byte & 0x80) === 0) out += String.fromCharCode(byte);
else if ((byte & 0xe0) === 0xc0) out += String.fromCharCode(((byte & 0x1f) << 6) | (view.getUint8(offset++) & 0x3f));
else if ((byte & 0xf0) === 0xe0) out += String.fromCharCode(((byte & 0x0f) << 12) | ((view.getUint8(offset++) & 0x3f) << 6) | (view.getUint8(offset++) & 0x3f));
else {
const codePoint = ((byte & 0x07) << 18) | ((view.getUint8(offset++) & 0x3f) << 12) | ((view.getUint8(offset++) & 0x3f) << 6) | (view.getUint8(offset++) & 0x3f);
const cp = codePoint - 0x10000;
out += String.fromCharCode((cp >> 10) + 0xd800, (cp & 1023) + 0xdc00);
}
}
return out;
};
const read = () => {
const token = view.getUint8(offset++);
if (token < 0x80) return token;
if (token < 0x90) {
const size = token & 0x0f;
const map = {};
for (let i = 0; i < size; i++) map[read()] = read();
return map;
}
if (token < 0xa0) {
const size = token & 0x0f;
const arr = new Array(size);
for (let i = 0; i < size; i++) arr[i] = read();
return arr;
}
if (token < 0xc0) return readString(token & 0x1f);
if (token > 0xdf) return token - 256;
switch (token) {
case 192: return null;
case 194: return false;
case 195: return true;
case 196: { const n = view.getUint8(offset); offset += 1; const b = buffer.slice(offset, offset + n); offset += n; return b; }
case 197: { const n = view.getUint16(offset); offset += 2; const b = buffer.slice(offset, offset + n); offset += n; return b; }
case 198: { const n = view.getUint32(offset); offset += 4; const b = buffer.slice(offset, offset + n); offset += n; return b; }
case 202: { const v = view.getFloat32(offset); offset += 4; return v; }
case 203: { const v = view.getFloat64(offset); offset += 8; return v; }
case 204: { const v = view.getUint8(offset); offset += 1; return v; }
case 205: { const v = view.getUint16(offset); offset += 2; return v; }
case 206: { const v = view.getUint32(offset); offset += 4; return v; }
case 207: { const hi = view.getUint32(offset); const lo = view.getUint32(offset + 4); offset += 8; return (hi * Math.pow(2, 32)) + lo; }
case 208: { const v = view.getInt8(offset); offset += 1; return v; }
case 209: { const v = view.getInt16(offset); offset += 2; return v; }
case 210: { const v = view.getInt32(offset); offset += 4; return v; }
case 211: { const hi = view.getInt32(offset); const lo = view.getUint32(offset + 4); offset += 8; return (hi * Math.pow(2, 32)) + lo; }
case 217: { const n = view.getUint8(offset); offset += 1; return readString(n); }
case 218: { const n = view.getUint16(offset); offset += 2; return readString(n); }
case 219: { const n = view.getUint32(offset); offset += 4; return readString(n); }
case 220: { const n = view.getUint16(offset); offset += 2; const arr = []; for (let i = 0; i < n; i++) arr.push(read()); return arr; }
case 221: { const n = view.getUint32(offset); offset += 4; const arr = []; for (let i = 0; i < n; i++) arr.push(read()); return arr; }
case 222: { const n = view.getUint16(offset); offset += 2; const map = {}; for (let i = 0; i < n; i++) map[read()] = read(); return map; }
case 223: { const n = view.getUint32(offset); offset += 4; const map = {}; for (let i = 0; i < n; i++) map[read()] = read(); return map; }
default: return null;
}
};
return { value: read(), offset };
}
// Simplified msgpack-like encoding/decoding for Blueboat
const blueboat = (() => {
function encode(t, e, s) {
let o = Array.isArray(t) ? { type: 2, data: t, options: { compress: !0 }, nsp: "/" } : { type: 2, data: ["blueboat_SEND_MESSAGE", { room: s, key: t, data: e }], options: { compress: !0 }, nsp: "/" };
return (function(t) {
let e = [], i = [], s = function t(e, n, i) {
let s = typeof i, o = 0, r = 0, a = 0, c = 0, l = 0, u = 0;
if ("string" === s) {
l = (function(t) {
let e = 0, n = 0, i = 0, s = t.length;
for (i = 0; i < s; i++) (e = t.charCodeAt(i)) < 128 ? n += 1 : e < 2048 ? n += 2 : e < 55296 || 57344 <= e ? n += 3 : (i++, n += 4);
return n;
})(i);
if (l < 32) e.push(160 | l), u = 1;
else if (l < 256) e.push(217, l), u = 2;
else if (l < 65536) e.push(218, l >> 8, l), u = 3;
else e.push(219, l >> 24, l >> 16, l >> 8, l), u = 5;
return n.push({ h: i, u: l, t: e.length }), u + l;
}
if ("number" === s) {
if (Math.floor(i) === i && isFinite(i)) {
if (i >= 0) {
if (i < 128) return e.push(i), 1;
if (i < 256) return e.push(204, i), 2;
if (i < 65536) return e.push(205, i >> 8, i), 3;
if (i < 4294967296) return e.push(206, i >> 24, i >> 16, i >> 8, i), 5;
a = i / Math.pow(2, 32) >> 0; c = i >>> 0; e.push(207, a >> 24, a >> 16, a >> 8, a, c >> 24, c >> 16, c >> 8, c); return 9;
} else {
if (i >= -32) return e.push(i), 1;
if (i >= -128) return e.push(208, i), 2;
if (i >= -32768) return e.push(209, i >> 8, i), 3;
if (i >= -2147483648) return e.push(210, i >> 24, i >> 16, i >> 8, i), 5;
a = Math.floor(i / Math.pow(2, 32)); c = i >>> 0; e.push(211, a >> 24, a >> 16, a >> 8, a, c >> 24, c >> 16, c >> 8, c); return 9;
}
} else {
e.push(203); n.push({ o: i, u: 8, t: e.length }); return 9;
}
}
if ("object" === s) {
if (null === i) return e.push(192), 1;
if (Array.isArray(i)) {
l = i.length;
if (l < 16) e.push(144 | l), u = 1;
else if (l < 65536) e.push(220, l >> 8, l), u = 3;
else e.push(221, l >> 24, l >> 16, l >> 8, l), u = 5;
for (o = 0; o < l; o++) u += t(e, n, i[o]);
return u;
}
let d = [], f = "", p = Object.keys(i);
for (o = 0, r = p.length; o < r; o++) "function" != typeof i[f = p[o]] && d.push(f);
l = d.length;
if (l < 16) e.push(128 | l), u = 1;
else if (l < 65536) e.push(222, l >> 8, l), u = 3;
else e.push(223, l >> 24, l >> 16, l >> 8, l), u = 5;
for (o = 0; o < l; o++) u += t(e, n, f = d[o]), u += t(e, n, i[f]);
return u;
}
if ("boolean" === s) return e.push(i ? 195 : 194), 1;
return 0;
}(e, i, t);
let o = new ArrayBuffer(s), r = new DataView(o), a = 0, c = 0, l = -1;
if (i.length > 0) l = i[0].t;
for (let u, h = 0, d = 0, f = 0, p = e.length; f < p; f++) {
r.setUint8(c + f, e[f]);
if (f + 1 === l) {
u = i[a]; h = u.u; d = c + l;
if (u.l) { let g = new Uint8Array(u.l); for (let E = 0; E < h; E++) r.setUint8(d + E, g[E]); }
else if (u.h) { (function(t, e, n) { for (let i = 0, s = 0, o = n.length; s < o; s++) (i = n.charCodeAt(s)) < 128 ? t.setUint8(e++, i) : (i < 2048 ? t.setUint8(e++, 192 | i >> 6) : (i < 55296 || 57344 <= i ? t.setUint8(e++, 224 | i >> 12) : (s++, i = 65536 + ((1023 & i) << 10 | 1023 & n.charCodeAt(s)), t.setUint8(e++, 240 | i >> 18), t.setUint8(e++, 128 | i >> 12 & 63)), t.setUint8(e++, 128 | i >> 6 & 63)), t.setUint8(e++, 128 | 63 & i)); })(r, d, u.h); }
else if (void 0 !== u.o) r.setFloat64(d, u.o);
c += h; if (i[++a]) l = i[a].t;
}
}
let y = Array.from(new Uint8Array(o)); y.unshift(4); return new Uint8Array(y).buffer;
})(o);
}
function decode(packet) {
function e(t) {
this.t = 0;
if (t instanceof ArrayBuffer) { this.i = t; this.s = new DataView(this.i); }
else { if (!ArrayBuffer.isView(t)) return null; this.i = t.buffer; this.s = new DataView(this.i, t.byteOffset, t.byteLength); }
}
e.prototype.g = function(t) { let e = new Array(t); for (let n = 0; n < t; n++) e[n] = this.v(); return e; };
e.prototype.M = function(t) { let e = {}; for (let n = 0; n < t; n++) e[this.v()] = this.v(); return e; };
e.prototype.h = function(t) {
let e = (function(t, e, n) {
let i = "", s = 0, o = e, r = e + n;
for (; o < r; o++) {
let a = t.getUint8(o);
if (0 != (128 & a)) {
if (192 != (224 & a)) {
if (224 != (240 & a)) {
s = (7 & a) << 18 | (63 & t.getUint8(++o)) << 12 | (63 & t.getUint8(++o)) << 6 | (63 & t.getUint8(++o)) << 0;
if (65536 <= s) { s -= 65536; i += String.fromCharCode(55296 + (s >>> 10), 56320 + (1023 & s)); }
else i += String.fromCharCode(s);
} else i += String.fromCharCode((15 & a) << 12 | (63 & t.getUint8(++o)) << 6 | (63 & t.getUint8(++o)) << 0);
} else i += String.fromCharCode((31 & a) << 6 | 63 & t.getUint8(++o));
} else i += String.fromCharCode(a);
}
return i;
})(this.s, this.t, t);
this.t += t; return e;
};
e.prototype.l = function(t) { let e = this.i.slice(this.t, this.t + t); this.t += t; return e; };
e.prototype.v = function() {
if (!this.s) return null;
let t, e = this.s.getUint8(this.t++), n = 0, i = 0, s = 0, o = 0;
if (e < 192) return e < 128 ? e : e < 144 ? this.M(15 & e) : e < 160 ? this.g(15 & e) : this.h(31 & e);
if (223 < e) return -1 * (255 - e + 1);
switch (e) {
case 192: return null;
case 194: return !1;
case 195: return !0;
case 196: n = this.s.getUint8(this.t); this.t += 1; return this.l(n);
case 197: n = this.s.getUint16(this.t); this.t += 2; return this.l(n);
case 198: n = this.s.getUint32(this.t); this.t += 4; return this.l(n);
case 202: t = this.s.getFloat32(this.t); this.t += 4; return t;
case 203: t = this.s.getFloat64(this.t); this.t += 8; return t;
case 204: t = this.s.getUint8(this.t); this.t += 1; return t;
case 205: t = this.s.getUint16(this.t); this.t += 2; return t;
case 206: t = this.s.getUint32(this.t); this.t += 4; return t;
case 207: s = this.s.getUint32(this.t) * Math.pow(2, 32); o = this.s.getUint32(this.t + 4); this.t += 8; return s + o;
case 208: t = this.s.getInt8(this.t); this.t += 1; return t;
case 209: t = this.s.getInt16(this.t); this.t += 2; return t;
case 210: t = this.s.getInt32(this.t); this.t += 4; return t;
case 211: s = this.s.getInt32(this.t) * Math.pow(2, 32); o = this.s.getUint32(this.t + 4); this.t += 8; return s + o;
case 217: n = this.s.getUint8(this.t); this.t += 1; return this.h(n);
case 218: n = this.s.getUint16(this.t); this.t += 2; return this.h(n);
case 219: n = this.s.getUint32(this.t); this.t += 4; return this.h(n);
case 220: n = this.s.getUint16(this.t); this.t += 2; return this.g(n);
case 221: n = this.s.getUint32(this.t); this.t += 4; return this.g(n);
case 222: n = this.s.getUint16(this.t); this.t += 2; this.M(n); break;
case 223: n = this.s.getUint32(this.t); this.t += 4; this.M(n); break;
}
return null;
};
let q = (function(t) { let n = new e(t = t.slice(1)), i = n.v(); if (n.t === t.byteLength) return i; return null; })(packet);
return q?.data?.[1];
}
return { encode, decode };
})();
class SocketManager extends EventTarget {
constructor() {
super();
this.socket = null;
this.transportType = "unknown";
this.blueboatRoomId = null;
this.playerId = null;
this.setup();
}
setup() {
const manager = this;
const shouldTrackSocketUrl = (url) => String(url || "").includes("gimkitconnect.com");
class NewWebSocket extends WebSocket {
constructor(url, params) {
super(url, params);
if (shouldTrackSocketUrl(url)) manager.registerSocket(this);
}
send(data) {
manager.onSend(data);
super.send(data);
}
}
const nativeXMLSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function() {
this.addEventListener("load", () => {
if (!this.responseURL.endsWith("/matchmaker/join")) return;
try {
const response = JSON.parse(this.responseText);
manager.blueboatRoomId = response.roomId;
} catch (_) {}
});
nativeXMLSend.apply(this, arguments);
};
window.WebSocket = NewWebSocket;
globalThis.socketManager = this;
}
registerSocket(socket) {
this.socket = socket;
const socketUrl = String(socket?.url || "");
const looksLikeColyseus = socketUrl.includes("gimkitconnect.com") && !socketUrl.includes("/socket.io/");
if (window.Phaser || looksLikeColyseus) {
this.transportType = "colyseus";
this.addEventListener("colyseusMessage", (e) => {
if (e.detail.type !== "DEVICES_STATES_CHANGES") return;
this.dispatchEvent(new CustomEvent("deviceChanges", { detail: parseChangePacket(e.detail.message) }));
});
} else {
this.transportType = "blueboat";
}
socket.addEventListener("message", (e) => {
const firstByte = (() => {
try {
return new Uint8Array(e.data)[0];
} catch (_) {
return null;
}
})();
if (this.transportType === "unknown" && firstByte != null) {
if (Object.values(colyseusProtocol).includes(firstByte)) this.transportType = "colyseus";
else this.transportType = "blueboat";
}
let decoded;
if (this.transportType === "colyseus") {
decoded = this.decodeColyseus(e);
if (decoded) {
this.dispatchEvent(new CustomEvent("colyseusMessage", { detail: decoded }));
if (decoded.type === "AUTH_ID") {
this.playerId = decoded.message;
}
}
} else {
decoded = blueboat.decode(e.data);
if (decoded) this.dispatchEvent(new CustomEvent("blueboatMessage", { detail: decoded }));
}
});
}
onSend(data) {
if (this.transportType === "blueboat" && !this.blueboatRoomId) {
const decoded = blueboat.decode(data);
if (decoded?.roomId) this.blueboatRoomId = decoded.roomId;
if (decoded?.room) this.blueboatRoomId = decoded.room;
}
}
sendMessage(channel, data) {
if (!this.socket) return;
if (!this.blueboatRoomId && this.transportType === "blueboat") return;
let encoded;
if (this.transportType === "colyseus") {
const header = new Uint8Array([colyseusProtocol.ROOM_DATA]);
const channelEncoded = msgpackEncode(channel);
const packetEncoded = msgpackEncode(data);
encoded = new Uint8Array(header.length + channelEncoded.byteLength + packetEncoded.byteLength);
encoded.set(header, 0);
encoded.set(new Uint8Array(channelEncoded), header.length);
encoded.set(new Uint8Array(packetEncoded), header.length + channelEncoded.byteLength);
this.socket.send(encoded);
} else {
encoded = blueboat.encode(channel, data, this.blueboatRoomId);
this.socket.send(encoded);
}
}
decodeColyseus(event) {
const bytes = new Uint8Array(event.data);
const code = bytes[0];
if (code === colyseusProtocol.ROOM_DATA) {
const first = msgpackDecode(event.data, 1);
if (!first) return null;
let message;
if (bytes.byteLength > first.offset) {
const second = msgpackDecode(event.data, first.offset);
message = second?.value;
}
return { type: first.value, message };
}
return null;
}
}
const socketManager = new SocketManager();
const autoAnswerState = {
questions: [],
answerDeviceId: null,
currentQuestionId: null,
questionIdList: [],
currentQuestionIndex: -1,
};
const AUTO_ANSWER_TICK = 1000;
let autoAnswerEnabled = false;
let answerInterval = null;
function answerQuestion() {
if (socketManager.transportType === "colyseus") {
if (autoAnswerState.currentQuestionId == null || autoAnswerState.answerDeviceId == null) return;
const question = autoAnswerState.questions.find((q) => q._id == autoAnswerState.currentQuestionId);
if (!question) return;
const packet = { key: "answered", deviceId: autoAnswerState.answerDeviceId, data: {} };
if (question.type == "text") packet.data.answer = question.answers[0].text;
else packet.data.answer = question.answers.find((a) => a.correct)?._id;
if (!packet.data.answer) return;
socketManager.sendMessage("MESSAGE_FOR_DEVICE", packet);
} else {
const questionId = autoAnswerState.questionIdList[autoAnswerState.currentQuestionIndex];
const question = autoAnswerState.questions.find((q) => q._id == questionId);
if (!question) return;
const answer = question.type == "mc" ? question.answers.find((a) => a.correct)?._id : question.answers[0]?.text;
if (!answer) return;
socketManager.sendMessage("QUESTION_ANSWERED", { answer, questionId });
}
}
const autoAnswerModule = new Module("Auto Answer", {
onEnable: () => {
console.log("Auto Answer enabled");
autoAnswerEnabled = true;
},
onDisable: () => {
console.log("Auto Answer disabled");
autoAnswerEnabled = false;
},
});
socketManager.addEventListener("deviceChanges", event => {
for (const { id, data } of event.detail || []) {
for (const key in data || {}) {
if (key === "GLOBAL_questions") {
autoAnswerState.questions = JSON.parse(data[key]);
autoAnswerState.answerDeviceId = id;
}
if (key === `PLAYER_${socketManager.playerId}_currentQuestionId`) {
autoAnswerState.currentQuestionId = data[key];
}
}
}
});
socketManager.addEventListener("blueboatMessage", event => {
if (event.detail?.key !== "STATE_UPDATE") return;
switch (event.detail.data.type) {
case "GAME_QUESTIONS":
autoAnswerState.questions = event.detail.data.value;
break;
case "PLAYER_QUESTION_LIST":
autoAnswerState.questionIdList = event.detail.data.value.questionList;
autoAnswerState.currentQuestionIndex = event.detail.data.value.questionIndex;
break;
case "PLAYER_QUESTION_LIST_INDEX":
autoAnswerState.currentQuestionIndex = event.detail.data.value;
break;
}
});
answerInterval = setInterval(() => {
if (!autoAnswerEnabled) return;
answerQuestion();
}, AUTO_ANSWER_TICK);
const ESP_LOG = "[ESP]";
const espState = {
enabled: false,
canvas: null,
ctx: null,
intervalId: null,
stores: null,
storesPromise: null,
seenPlayers: new Map(),
waitLogTick: 0,
};
function espLog(message, extra) {
if (extra !== undefined) console.log(`${ESP_LOG} ${message}`, extra);
else console.log(`${ESP_LOG} ${message}`);
}
function createEspCanvas() {
if (espState.canvas?.parentNode) {
espLog("Canvas already exists; reusing existing canvas.");
return;
}
const canvas = document.createElement("canvas");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
canvas.style.position = "fixed";
canvas.style.left = "0";
canvas.style.top = "0";
canvas.style.width = "100vw";
canvas.style.height = "100vh";
canvas.style.zIndex = "9999";
canvas.style.pointerEvents = "none";
canvas.style.userSelect = "none";
document.body.appendChild(canvas);
espState.canvas = canvas;
espState.ctx = canvas.getContext("2d");
if (!espState.ctx) {
espLog("Failed to get canvas 2D context");
canvas.remove();
espState.canvas = null;
return;
}
espLog("Canvas created");
}
function destroyEspCanvas() {
if (!espState.canvas) return;
espState.canvas.remove();
espState.canvas = null;
espState.ctx = null;
espLog("Canvas destroyed");
}
function resizeEspCanvas() {
if (!espState.canvas?.parentNode) return;
espState.canvas.width = window.innerWidth;
espState.canvas.height = window.innerHeight;
espLog(`Canvas resized to ${espState.canvas.width}x${espState.canvas.height}`);
}
async function resolveEspStores() {
if (espState.stores) return espState.stores;
if (espState.storesPromise) return espState.storesPromise;
espState.storesPromise = (async () => {
if (!document.body) {
await new Promise((resolve) => window.addEventListener("DOMContentLoaded", resolve, { once: true }));
}
const moduleScript = document.querySelector("script[src][type='module']");
if (!moduleScript?.src) throw new Error("Failed to find game module script");
const response = await fetch(moduleScript.src);
const text = await response.text();
const gameScriptUrl = text.match(/FixSpinePlugin-[^.]+\.js/)?.[0];
if (!gameScriptUrl) throw new Error("Failed to find game script URL");
const gameScript = await import(`/assets/${gameScriptUrl}`);
const stores = Object.values(gameScript).find((value) => value && value.assignment);
if (!stores) throw new Error("Failed to resolve stores export");
window.stores = stores;
espState.stores = stores;
espLog("Resolved stores via module import");
return stores;
})();
try {
return await espState.storesPromise;
} finally {
espState.storesPromise = null;
}
}
function primeSharedPlayerData() {
if (espState.stores || espState.storesPromise) return;
const attemptResolve = () => {
resolveEspStores().catch((error) => {
espLog("Shared stores resolve failed; retrying", error);
setTimeout(() => {
if (!espState.stores) attemptResolve();
}, 1500);
});
};
attemptResolve();
}
primeSharedPlayerData();
function getCharacterPosition(character) {
const x = Number(character?.x ?? character?.position?.x ?? character?.body?.x);
const y = Number(character?.y ?? character?.position?.y ?? character?.body?.y);
if (!Number.isFinite(x) || !Number.isFinite(y)) return null;
return { x, y };
}
function getCharacters(stores) {
const manager = stores?.phaser?.scene?.characterManager;
const map = manager?.characters;
if (!map) return [];
if (typeof map.values === "function") return Array.from(map.values());
if (Array.isArray(map)) return map;
return Object.values(map);
}
function getCharacterEntries(stores) {
const manager = stores?.phaser?.scene?.characterManager;
const map = manager?.characters;
if (!map) return [];
if (typeof map.entries === "function") {
return Array.from(map.entries(), ([id, character]) => ({ id, character }));
}
if (Array.isArray(map)) {
return map.map((character, index) => ({ id: character?.id ?? character?.characterId ?? index, character }));
}
return Object.entries(map).map(([id, character]) => ({ id, character }));
}
function getMainCharacter(stores) {
const mainId = stores?.phaser?.mainCharacter?.id;
const manager = stores?.phaser?.scene?.characterManager;
const map = manager?.characters;
if (!map) return null;
if (mainId != null && typeof map.get === "function") return map.get(mainId) || null;
return getCharacters(stores).find((character) => character?.id === mainId || character?.characterId === mainId) || null;
}
function getCharacterTeam(character) {
return character?.teamId ?? character?.team?.id ?? character?.state?.teamId ?? character?.data?.teamId ?? null;
}
function getCharacterId(character) {
return character?.id ?? character?.characterId ?? character?.playerId ?? character?.entityId ?? null;
}
function getSerializerCharacterById(id) {
if (id == null) return null;
const map = window?.serializer?.state?.characters?.$items;
if (!map || typeof map.get !== "function") return null;
return map.get(id) || map.get(String(id)) || null;
}
function findSerializerCharacterByPosition(character) {
const map = window?.serializer?.state?.characters?.$items;
if (!map || typeof map.values !== "function") return null;
const x = Number(character?.x ?? character?.position?.x);
const y = Number(character?.y ?? character?.position?.y);
if (!Number.isFinite(x) || !Number.isFinite(y)) return null;
for (const candidate of map.values()) {
const cx = Number(candidate?.x ?? candidate?.position?.x);
const cy = Number(candidate?.y ?? candidate?.position?.y);
if (!Number.isFinite(cx) || !Number.isFinite(cy)) continue;
if (Math.abs(cx - x) < 0.5 && Math.abs(cy - y) < 0.5) return candidate;
}
return null;
}
function getCharacterName(character, fallbackId = null) {
const id = getCharacterId(character) ?? fallbackId;
const serializerCharacter = getSerializerCharacterById(id) ?? findSerializerCharacterByPosition(character);
return character?.name
?? character?.displayName
?? character?.state?.name
?? character?.username
?? character?.playerName
?? character?.profile?.name
?? character?.meta?.name
?? character?.data?.name
?? serializerCharacter?.name
?? serializerCharacter?.displayName
?? serializerCharacter?.username
?? "Player";
}
function projectWorldToScreen(position, cameraSnapshot, viewportWidth, viewportHeight) {
const x = Number(position?.x);
const y = Number(position?.y);
const camX = Number(cameraSnapshot?.midX);
const camY = Number(cameraSnapshot?.midY);
const zoom = Number(cameraSnapshot?.zoom ?? 1) || 1;
if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(camX) || !Number.isFinite(camY)) return null;
return {
x: (x - camX) * zoom + viewportWidth / 2,
y: (y - camY) * zoom + viewportHeight / 2,
zoom,
};
}
function getEspRenderConfig() {
const defaults = {
hitbox: true,
hitboxSize: 150,
hitboxWidth: 3,
hitboxColor: "#ff4444",
names: false,
namesDistanceOnly: false,
nameSize: 20,
nameColor: "#ffffff",
offscreenStyle: "tracers",
offscreenTheme: "classic",
alwaysTracer: false,
tracerWidth: 3,
tracerColor: "#ff4444",
arrowSize: 14,
arrowColor: "#ff4444",
arrowStyle: "regular",
valueTextColor: window.__zyroxEspValueTextColor || "#ffffff",
};
const liveCfg = window.__zyroxEspConfig;
if (liveCfg && typeof liveCfg === "object") return { ...defaults, ...liveCfg };
return defaults;
}
function getHealthBarsConfig() {
const defaults = {
enabled: true,
width: 54,
height: 6,
yOffset: 32,
showText: true,
};
const liveCfg = window.__zyroxHealthBarsConfig;
return liveCfg && typeof liveCfg === "object" ? { ...defaults, ...liveCfg } : defaults;
}
function readNumericCandidate(source, paths) {
if (!source) return null;
for (const path of paths) {
const parts = path.split(".");
let node = source;
for (const part of parts) node = node?.[part];
const value = Number(node);
if (Number.isFinite(value)) return value;
}
return null;
}
function getCharacterHealthSnapshot(character, fallbackId = null) {
const cid = getCharacterId(character) ?? fallbackId;
const serializerCharacter = getSerializerCharacterById(cid) ?? findSerializerCharacterByPosition(character);
const candidates = [character, serializerCharacter];
let current = null;
let max = null;
for (const source of candidates) {
if (!source) continue;
if (current == null) {
current = readNumericCandidate(source, ["health", "hp", "currentHealth", "state.health", "stats.health", "data.health"]);
}
if (max == null) {
max = readNumericCandidate(source, ["maxHealth", "maxHp", "healthMax", "state.maxHealth", "stats.maxHealth", "data.maxHealth"]);
}
if (current != null && max != null) break;
}
if (current == null) return null;
if (max == null || max <= 0) {
if (current <= 100) max = 100;
else return null;
}
return { current: Math.max(0, current), max: Math.max(1, max) };
}
function renderEspPlayers(stores) {
const ctx = espState.ctx;
const canvas = espState.canvas;
if (!ctx || !canvas) {
espLog("Missing data: no canvas/context; rendering skip.");
return;
}
const camera = stores?.phaser?.scene?.cameras?.cameras?.[0];
const me = getMainCharacter(stores);
if (!camera || !me) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
return;
}
ctx.clearRect(0, 0, canvas.width, canvas.height);
const myTeam = getCharacterTeam(me);
const espCfg = getEspRenderConfig();
const healthCfg = getHealthBarsConfig();
const showHitbox = espCfg.hitbox !== false;
const showNames = espCfg.names !== false;
const showHealthBars = state.enabledModules?.has("Health Bars") && healthCfg.enabled !== false;
const namesDistanceOnly = espCfg.namesDistanceOnly === true;
const offscreenStyle = espCfg.offscreenStyle === "arrows" || espCfg.offscreenStyle === "none"
? espCfg.offscreenStyle
: "tracers";
const offscreenTheme = espCfg.offscreenTheme || "classic";
const alwaysTracer = espCfg.alwaysTracer === true;
const arrowStyle = ["regular", "dot", "modern"].includes(espCfg.arrowStyle) ? espCfg.arrowStyle : "regular";
const camX = Number(camera?.midPoint?.x);
const camY = Number(camera?.midPoint?.y);
const zoom = Number(camera?.zoom ?? 1) || 1;
if (!Number.isFinite(camX) || !Number.isFinite(camY)) return;
const activeIds = new Set();
const now = performance.now();
for (const entry of getCharacterEntries(stores)) {
const character = entry.character;
const characterId = entry.id ?? getCharacterId(character);
if (!character || character === me) continue;
const pos = getCharacterPosition(character);
if (!pos) continue;
const stableId = String(characterId ?? `${Math.round(pos.x)}:${Math.round(pos.y)}`);
activeIds.add(stableId);
const angle = Math.atan2(pos.y - camY, pos.x - camX);
const distance = Math.hypot(pos.x - camX, pos.y - camY) * zoom;
const rawX = (pos.x - camX) * zoom + canvas.width / 2;
const rawY = (pos.y - camY) * zoom + canvas.height / 2;
const prev = espState.seenPlayers.get(stableId);
let screenX = rawX;
let screenY = rawY;
if (prev) {
const delta = Math.hypot(rawX - prev.x, rawY - prev.y);
if (delta < 300) {
const blend = 0.38;
screenX = prev.x + (rawX - prev.x) * blend;
screenY = prev.y + (rawY - prev.y) * blend;
}
}
espState.seenPlayers.set(stableId, { x: screenX, y: screenY, t: now });
const onScreen = screenX >= 0 && screenX <= canvas.width && screenY >= 0 && screenY <= canvas.height;
const isTeammate = myTeam !== null && getCharacterTeam(character) === myTeam;
const hitboxColor = espCfg.hitboxColor || (isTeammate ? "green" : "red");
const tracerColor = espCfg.tracerColor || (isTeammate ? "green" : "red");
const arrowColor = espCfg.arrowColor || (isTeammate ? "green" : "red");
const nameColor = espCfg.nameColor || "#000000";
const hitboxSize = Math.max(12, Number(espCfg.hitboxSize) || 80);
const hitboxWidth = Math.max(1, Number(espCfg.hitboxWidth) || 3);
const nameSize = Math.max(8, Number(espCfg.nameSize) || 20);
const tracerWidth = Math.max(1, Number(espCfg.tracerWidth) || 3);
const arrowSize = Math.max(6, Number(espCfg.arrowSize) || 14);
if (onScreen && showHitbox) {
const boxSize = Math.max(24, hitboxSize / zoom);
ctx.beginPath();
ctx.lineWidth = hitboxWidth;
ctx.strokeStyle = hitboxColor;
ctx.strokeRect(screenX - boxSize / 2, screenY - boxSize / 2, boxSize, boxSize);
}
const shouldDrawOffscreen = !onScreen && offscreenStyle !== "none";
const shouldDrawTracer = offscreenStyle === "tracers" && (alwaysTracer || !onScreen);
if (shouldDrawOffscreen || shouldDrawTracer) {
const margin = 20;
const halfW = canvas.width / 2 - margin;
const halfH = canvas.height / 2 - margin;
const dx = Math.cos(angle);
const dy = Math.sin(angle);
const scale = Math.min(
Math.abs(halfW / (dx || 0.0001)),
Math.abs(halfH / (dy || 0.0001))
);
const endX = canvas.width / 2 + dx * scale;
const endY = canvas.height / 2 + dy * scale;
if (shouldDrawTracer) {
ctx.save();
ctx.beginPath();
ctx.moveTo(canvas.width / 2, canvas.height / 2);
ctx.lineTo(onScreen ? screenX : endX, onScreen ? screenY : endY);
ctx.lineWidth = tracerWidth;
ctx.strokeStyle = tracerColor;
if (offscreenTheme === "dashed") ctx.setLineDash([8, 6]);
if (offscreenTheme === "neon") {
ctx.shadowColor = tracerColor;
ctx.shadowBlur = 10;
}
ctx.stroke();
ctx.restore();
} else if (offscreenStyle === "arrows" && !onScreen) {
const headLength = arrowSize;
const headAngle = Math.PI / 6;
const a1 = angle - headAngle;
const a2 = angle + headAngle;
ctx.save();
ctx.beginPath();
if (arrowStyle === "dot") {
ctx.arc(endX, endY, Math.max(4, headLength * 0.35), 0, Math.PI * 2);
ctx.fillStyle = arrowColor;
} else if (arrowStyle === "modern") {
const tailX = endX - Math.cos(angle) * headLength;
const tailY = endY - Math.sin(angle) * headLength;
const perpX = Math.cos(angle + Math.PI / 2) * (headLength * 0.45);
const perpY = Math.sin(angle + Math.PI / 2) * (headLength * 0.45);
ctx.moveTo(endX, endY);
ctx.quadraticCurveTo(tailX + perpX, tailY + perpY, tailX, tailY);
ctx.quadraticCurveTo(tailX - perpX, tailY - perpY, endX, endY);
ctx.fillStyle = arrowColor;
} else {
ctx.moveTo(endX, endY);
ctx.lineTo(endX - Math.cos(a1) * headLength, endY - Math.sin(a1) * headLength);
ctx.moveTo(endX, endY);
ctx.lineTo(endX - Math.cos(a2) * headLength, endY - Math.sin(a2) * headLength);
}
ctx.lineWidth = tracerWidth;
ctx.strokeStyle = arrowColor;
if (offscreenTheme === "dashed") ctx.setLineDash([6, 5]);
if (offscreenTheme === "neon") {
ctx.shadowColor = arrowColor;
ctx.shadowBlur = 10;
}
if (arrowStyle === "dot" || arrowStyle === "modern") ctx.fill();
else ctx.stroke();
ctx.restore();
}
}
if (!showNames) continue;
ctx.fillStyle = nameColor;
ctx.font = `${nameSize}px ${espCfg.font || "Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif"}`;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
const labelX = onScreen ? screenX : Math.cos(angle) * Math.min(250, distance) + canvas.width / 2;
const labelY = onScreen ? (screenY - 18) : Math.sin(angle) * Math.min(250, distance) + canvas.height / 2;
const distanceText = `${Math.floor(distance)}`;
const labelText = namesDistanceOnly ? distanceText : `${getCharacterName(character, characterId)} (${distanceText})`;
ctx.fillText(labelText, labelX, labelY);
}
for (const [id, data] of espState.seenPlayers) {
if (!activeIds.has(id) && now - Number(data?.t ?? 0) > 900) {
espState.seenPlayers.delete(id);
}
}
}
function renderEspTick() {
if (!espState.enabled || !espState.ctx || !espState.canvas) return;
const stores = espState.stores ?? window.stores;
if (!stores) {
espState.waitLogTick += 1;
if (espState.waitLogTick % 60 === 0) espLog("Waiting for stores...");
espState.ctx.clearRect(0, 0, espState.canvas.width, espState.canvas.height);
return;
}
espState.waitLogTick = 0;
renderEspPlayers(stores);
}
function startEsp() {
if (espState.enabled) {
espLog("ESP already enabled; skipping duplicate start.");
return;
}
espState.enabled = true;
espLog("ESP initialized");
createEspCanvas();
resizeEspCanvas();
resolveEspStores().catch((error) => espLog("Failed to resolve stores", error));
if (espState.intervalId != null) {
clearInterval(espState.intervalId);
espState.intervalId = null;
}
espState.intervalId = setInterval(renderEspTick, 1000 / 30);
}
function stopEsp() {
if (!espState.enabled) {
espLog("ESP already disabled; skipping duplicate stop.");
return;
}
espState.enabled = false;
if (espState.intervalId != null) {
clearInterval(espState.intervalId);
espState.intervalId = null;
}
espState.seenPlayers.clear();
destroyEspCanvas();
espLog("ESP stopped and cleaned up");
}
window.addEventListener("resize", resizeEspCanvas);
// ---------------------------------------------------------------------------
// CROSSHAIR MODULE
// Renders a crosshair at the mouse cursor position and optionally a line
// from the center of the screen to the cursor.
// ---------------------------------------------------------------------------
const crosshairState = {
enabled: false,
canvas: null,
ctx: null,
mouseX: 0,
mouseY: 0,
rafId: null,
};
function getCrosshairConfig() {
const defaults = {
enabled: true,
style: "x",
color: "#ff3b3b",
crosshairSize: 25,
lineSize: 4,
showLine: false,
lineColor: "#ff3b3b",
tracerLineSize: 1.5,
hoverHighlight: true,
hoverColor: "#ffff00",
};
const stored = window.__zyroxCrosshairConfig;
return stored && typeof stored === "object" ? { ...defaults, ...stored } : defaults;
}
function createCrosshairCanvas() {
if (crosshairState.canvas?.parentNode) return;
const canvas = document.createElement("canvas");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
canvas.style.cssText = "position:fixed;left:0;top:0;width:100vw;height:100vh;z-index:10000;pointer-events:none;user-select:none;";
document.body.appendChild(canvas);
crosshairState.canvas = canvas;
crosshairState.ctx = canvas.getContext("2d");
}
function destroyCrosshairCanvas() {
if (crosshairState.rafId != null) { cancelAnimationFrame(crosshairState.rafId); crosshairState.rafId = null; }
crosshairState.canvas?.remove();
crosshairState.canvas = null;
crosshairState.ctx = null;
}
function resizeCrosshairCanvas() {
if (!crosshairState.canvas) return;
crosshairState.canvas.width = window.innerWidth;
crosshairState.canvas.height = window.innerHeight;
}
function renderCrosshairFrame() {
if (!crosshairState.enabled) return;
crosshairState.rafId = requestAnimationFrame(renderCrosshairFrame);
const ctx = crosshairState.ctx;
const canvas = crosshairState.canvas;
if (!ctx || !canvas) return;
ctx.clearRect(0, 0, canvas.width, canvas.height);
const cfg = getCrosshairConfig();
if (!cfg.enabled) return;
const mx = crosshairState.mouseX;
const my = crosshairState.mouseY;
const cx = canvas.width / 2;
const cy = canvas.height / 2;
const crosshairSize = typeof cfg.crosshairSize === "number" ? cfg.crosshairSize : 25;
const lineSize = typeof cfg.lineSize === "number" ? cfg.lineSize : 4;
const tracerSize = typeof cfg.tracerLineSize === "number" ? cfg.tracerLineSize : 1.5;
// --- Player hover detection ---
let hoveringPlayer = false;
if (cfg.hoverHighlight) {
try {
const stores = espState.stores ?? window.stores ?? null;
const camera = stores?.phaser?.scene?.cameras?.cameras?.[0];
const me = stores ? getMainCharacter(stores) : null;
if (camera && me) {
const camX = Number(camera?.midPoint?.x);
const camY = Number(camera?.midPoint?.y);
const zoom = Number(camera?.zoom ?? 1) || 1;
const hitRadius = (Math.max(20, 120 / zoom) / 2) * 3;
if (Number.isFinite(camX) && Number.isFinite(camY)) {
for (const { character } of getCharacterEntries(stores)) {
if (!character || character === me) continue;
const pos = getCharacterPosition(character);
if (!pos) continue;
const sx = (pos.x - camX) * zoom + canvas.width / 2;
const sy = (pos.y - camY) * zoom + canvas.height / 2;
if (Math.hypot(mx - sx, my - sy) <= hitRadius) {
hoveringPlayer = true;
break;
}
}
}
}
} catch (_) { /* stores not ready yet */ }
}
const col = hoveringPlayer ? (cfg.hoverColor || "#ffff00") : (cfg.color || "#ff3b3b");
// Draw line from center to cursor if enabled
if (cfg.showLine) {
ctx.save();
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.lineTo(mx, my);
ctx.lineWidth = tracerSize;
ctx.strokeStyle = cfg.lineColor || "#ff3b3b";
ctx.globalAlpha = 0.65;
ctx.stroke();
ctx.restore();
}
// Draw crosshair at cursor
ctx.save();
ctx.strokeStyle = col;
ctx.fillStyle = col;
ctx.lineWidth = lineSize;
ctx.globalAlpha = 0.92;
const style = cfg.style || "cross";
if (style === "dot") {
ctx.beginPath();
ctx.arc(mx, my, Math.max(1, crosshairSize * 0.35), 0, Math.PI * 2);
ctx.fill();
} else if (style === "solid") {
// Solid cross — lines go straight through the center with no gap
const arm = crosshairSize;
ctx.beginPath();
ctx.moveTo(mx - arm, my); ctx.lineTo(mx + arm, my);
ctx.moveTo(mx, my - arm); ctx.lineTo(mx, my + arm);
ctx.stroke();
} else if (style === "crossdot") {
// Cross with gap + filled center dot
const arm = crosshairSize;
const gap = Math.max(1, crosshairSize * 0.4);
ctx.beginPath();
ctx.moveTo(mx - arm, my); ctx.lineTo(mx - gap, my);
ctx.moveTo(mx + gap, my); ctx.lineTo(mx + arm, my);
ctx.moveTo(mx, my - arm); ctx.lineTo(mx, my - gap);
ctx.moveTo(mx, my + gap); ctx.lineTo(mx, my + arm);
ctx.stroke();
ctx.beginPath();
ctx.arc(mx, my, Math.max(1.5, lineSize * 1.2), 0, Math.PI * 2);
ctx.fill();
} else if (style === "circle") {
ctx.beginPath();
ctx.arc(mx, my, crosshairSize, 0, Math.PI * 2);
ctx.stroke();
ctx.beginPath();
ctx.arc(mx, my, Math.max(1, crosshairSize * 0.2), 0, Math.PI * 2);
ctx.fill();
} else if (style === "circlecross") {
// Circle with solid cross lines through the center
ctx.beginPath();
ctx.arc(mx, my, crosshairSize, 0, Math.PI * 2);
ctx.stroke();
const arm = crosshairSize;
ctx.beginPath();
ctx.moveTo(mx - arm, my); ctx.lineTo(mx + arm, my);
ctx.moveTo(mx, my - arm); ctx.lineTo(mx, my + arm);
ctx.stroke();
} else if (style === "plus") {
// Thick plus sign
ctx.lineWidth = lineSize * 1.5;
const arm = crosshairSize;
const gap = Math.max(1, crosshairSize * 0.3);
ctx.beginPath();
ctx.moveTo(mx - arm, my); ctx.lineTo(mx - gap, my);
ctx.moveTo(mx + gap, my); ctx.lineTo(mx + arm, my);
ctx.moveTo(mx, my - arm); ctx.lineTo(mx, my - gap);
ctx.moveTo(mx, my + gap); ctx.lineTo(mx, my + arm);
ctx.stroke();
} else if (style === "x") {
// Diagonal X crosshair
const arm = crosshairSize * 0.75;
const gap = Math.max(1, crosshairSize * 0.28);
ctx.beginPath();
ctx.moveTo(mx - arm, my - arm); ctx.lineTo(mx - gap, my - gap);
ctx.moveTo(mx + gap, my + gap); ctx.lineTo(mx + arm, my + arm);
ctx.moveTo(mx + arm, my - arm); ctx.lineTo(mx + gap, my - gap);
ctx.moveTo(mx - gap, my + gap); ctx.lineTo(mx - arm, my + arm);
ctx.stroke();
} else {
// Default "cross" — thin with center gap
const arm = crosshairSize;
const gap = Math.max(1, crosshairSize * 0.4);
ctx.beginPath();
ctx.moveTo(mx - arm, my); ctx.lineTo(mx - gap, my);
ctx.moveTo(mx + gap, my); ctx.lineTo(mx + arm, my);
ctx.moveTo(mx, my - arm); ctx.lineTo(mx, my - gap);
ctx.moveTo(mx, my + gap); ctx.lineTo(mx, my + arm);
ctx.stroke();
}
ctx.restore();
}
function startCrosshair() {
if (crosshairState.enabled) return;
primeSharedPlayerData();
crosshairState.enabled = true;
createCrosshairCanvas();
renderCrosshairFrame();
}
function stopCrosshair() {
if (!crosshairState.enabled) return;
crosshairState.enabled = false;
destroyCrosshairCanvas();
}
document.addEventListener("mousemove", (e) => {
const dx = e.clientX - crosshairState.mouseX;
const dy = e.clientY - crosshairState.mouseY;
const len = Math.hypot(dx, dy);
if (len > 0.0001) {
autoAimState.aimDirX = dx / len;
autoAimState.aimDirY = dy / len;
}
crosshairState.mouseX = e.clientX;
crosshairState.mouseY = e.clientY;
}, { passive: true });
window.addEventListener("resize", resizeCrosshairCanvas);
// ---------------------------------------------------------------------------
// TRIGGER ASSIST MODULE
// Uses shared ESP bridge data and cursor position to trigger fire when
// player targets are within a configurable cursor radius.
// ---------------------------------------------------------------------------
const triggerAssistState = {
enabled: false,
loopId: null,
canvas: null,
ctx: null,
lastFireAt: 0,
mouseHeld: false,
releaseTimeoutId: null,
target: null,
statusText: "Idle",
};
const autoAimState = {
enabled: false,
loopId: null,
canvas: null,
ctx: null,
target: null,
statusText: "Idle",
lastAimX: 0,
lastAimY: 0,
aimDirX: 1,
aimDirY: 0,
};
const autoAimInputState = {
leftMouseDown: false,
};
function getTriggerAssistConfig() {
const defaults = {
enabled: true,
teamCheck: true,
fovPx: 85,
holdToFire: false,
fireRateMs: 45,
requireLOS: false,
onlyWhenGameFocused: true,
showTargetRing: true,
};
const stored = window.__zyroxTriggerAssistConfig;
return stored && typeof stored === "object" ? { ...defaults, ...stored } : defaults;
}
function getAutoAimConfig() {
const defaults = {
enabled: true,
teamCheck: true,
fovDeg: 120,
smoothing: 0.2,
maxStepPx: 32,
stickToTarget: true,
onlyWhenGameFocused: true,
requireMouseDown: false,
showDebugDot: true,
};
const stored = window.__zyroxAutoAimConfig;
if (stored && typeof stored === "object") {
const merged = { ...defaults, ...stored };
if (merged.fovDeg == null && Number.isFinite(Number(stored.fovPx))) {
merged.fovDeg = Math.max(15, Math.min(180, Number(stored.fovPx)));
}
return merged;
}
return defaults;
}
function createTriggerAssistCanvas() {
if (triggerAssistState.canvas?.parentNode) return;
const canvas = document.createElement("canvas");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
canvas.style.cssText = "position:fixed;left:0;top:0;width:100vw;height:100vh;z-index:10001;pointer-events:none;user-select:none;";
document.body.appendChild(canvas);
triggerAssistState.canvas = canvas;
triggerAssistState.ctx = canvas.getContext("2d");
}
function destroyTriggerAssistCanvas() {
triggerAssistState.canvas?.remove();
triggerAssistState.canvas = null;
triggerAssistState.ctx = null;
}
function resizeTriggerAssistCanvas() {
if (!triggerAssistState.canvas) return;
triggerAssistState.canvas.width = window.innerWidth;
triggerAssistState.canvas.height = window.innerHeight;
}
function createAutoAimCanvas() {
if (autoAimState.canvas?.parentNode) return;
const canvas = document.createElement("canvas");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
canvas.style.cssText = "position:fixed;left:0;top:0;width:100vw;height:100vh;z-index:10002;pointer-events:none;user-select:none;";
document.body.appendChild(canvas);
autoAimState.canvas = canvas;
autoAimState.ctx = canvas.getContext("2d");
}
function destroyAutoAimCanvas() {
autoAimState.canvas?.remove();
autoAimState.canvas = null;
autoAimState.ctx = null;
}
function resizeAutoAimCanvas() {
if (!autoAimState.canvas) return;
autoAimState.canvas.width = window.innerWidth;
autoAimState.canvas.height = window.innerHeight;
}
function getGameCanvas() {
const stores = espState.stores ?? window.stores;
return stores?.phaser?.game?.canvas
?? stores?.phaser?.scene?.game?.canvas
?? document.querySelector("canvas");
}
function fireCanvasPointerEvent(type, canvas, x, y) {
if (!canvas) return;
const rect = canvas.getBoundingClientRect();
const clientX = rect.left + Math.max(0, Math.min(rect.width, x));
const clientY = rect.top + Math.max(0, Math.min(rect.height, y));
const init = {
bubbles: true,
cancelable: true,
button: 0,
buttons: type === "pointerup" || type === "mouseup" ? 0 : 1,
clientX,
clientY,
pointerId: 1,
pointerType: "mouse",
isPrimary: true,
};
canvas.dispatchEvent(new PointerEvent(type, init));
}
function fireCanvasMouseEvent(type, canvas, x, y, buttons = 0) {
if (!canvas) return;
const rect = canvas.getBoundingClientRect();
const clientX = rect.left + Math.max(0, Math.min(rect.width, x));
const clientY = rect.top + Math.max(0, Math.min(rect.height, y));
canvas.dispatchEvent(new MouseEvent(type, {
bubbles: true,
cancelable: true,
button: 0,
buttons,
clientX,
clientY,
}));
}
function syncAimPointer(canvas, x, y, buttons = 0) {
fireCanvasPointerEvent("pointermove", canvas, x, y);
fireCanvasMouseEvent("mousemove", canvas, x, y, buttons);
}
function releaseFireHold() {
if (!triggerAssistState.mouseHeld) return;
const canvas = getGameCanvas();
if (canvas) {
syncAimPointer(canvas, crosshairState.mouseX, crosshairState.mouseY, 0);
fireCanvasPointerEvent("pointerup", canvas, crosshairState.mouseX, crosshairState.mouseY);
fireCanvasMouseEvent("mouseup", canvas, crosshairState.mouseX, crosshairState.mouseY, 0);
}
triggerAssistState.mouseHeld = false;
}
function attemptFire(hold, forceRelease = false, point = null) {
const canvas = getGameCanvas();
if (!canvas) return false;
canvas.focus?.({ preventScroll: true });
const aimX = Number(point?.x ?? crosshairState.mouseX);
const aimY = Number(point?.y ?? crosshairState.mouseY);
if (forceRelease) {
releaseFireHold();
return true;
}
if (hold) {
syncAimPointer(canvas, aimX, aimY, 1);
if (!triggerAssistState.mouseHeld) {
fireCanvasPointerEvent("pointerdown", canvas, aimX, aimY);
fireCanvasMouseEvent("mousedown", canvas, aimX, aimY, 1);
triggerAssistState.mouseHeld = true;
}
return true;
}
syncAimPointer(canvas, aimX, aimY, 1);
fireCanvasPointerEvent("pointerdown", canvas, aimX, aimY);
fireCanvasMouseEvent("mousedown", canvas, aimX, aimY, 1);
setTimeout(() => {
syncAimPointer(canvas, aimX, aimY, 0);
fireCanvasPointerEvent("pointerup", canvas, aimX, aimY);
fireCanvasMouseEvent("mouseup", canvas, aimX, aimY, 0);
}, 12);
return true;
}
function findTriggerTarget(cfg) {
const snapshot = getAutoAimPlayerSnapshot();
if (!snapshot?.camera || !Array.isArray(snapshot.players)) return null;
const mx = crosshairState.mouseX;
const my = crosshairState.mouseY;
const espCfg = getEspRenderConfig();
const baseHitbox = Math.max(12, Number(espCfg.hitboxSize) || 150);
const width = window.innerWidth;
const height = window.innerHeight;
const margin = 80;
let best = null;
for (const player of snapshot.players) {
if (!player) continue;
const pid = String(player.id ?? "");
if (!pid || (snapshot.localPlayerId != null && pid === String(snapshot.localPlayerId))) continue;
if (cfg.teamCheck && snapshot.localTeamId != null && player.teamId === snapshot.localTeamId) continue;
const screen = projectWorldToScreen(player, snapshot.camera, width, height);
if (!screen) continue;
if (screen.x < -margin || screen.x > width + margin || screen.y < -margin || screen.y > height + margin) continue;
const boxSize = Math.max(24, baseHitbox / Math.max(0.01, Number(screen.zoom) || 1));
const half = boxSize * 0.5;
if (mx < screen.x - half || mx > screen.x + half || my < screen.y - half || my > screen.y + half) continue;
const dist = Math.hypot(mx - screen.x, my - screen.y);
if (!best || dist < best.distancePx) {
best = {
player,
screenX: screen.x,
screenY: screen.y,
distancePx: dist,
hitboxSizePx: boxSize,
};
}
}
return best;
}
function getAutoAimPlayerSnapshot() {
const shared = window.__zyroxEspShared;
if (shared?.ready && Array.isArray(shared.players) && shared.camera) {
return {
localPlayerId: shared.localPlayerId ?? null,
localTeamId: shared.localTeamId ?? null,
camera: shared.camera,
players: shared.players,
};
}
const stores = espState.stores ?? window.stores ?? null;
const me = stores ? getMainCharacter(stores) : null;
const cam = stores?.phaser?.scene?.cameras?.cameras?.[0];
if (!me || !cam) return null;
const mePos = getCharacterPosition(me);
const meId = String(getCharacterId(me) ?? stores?.phaser?.mainCharacter?.id ?? "");
const meTeam = getCharacterTeam(me);
const fallbackPlayers = [];
for (const { id, character } of getCharacterEntries(stores)) {
const pos = getCharacterPosition(character);
if (!pos) continue;
fallbackPlayers.push({
id: String(id ?? getCharacterId(character) ?? ""),
name: String(getCharacterName(character, id)),
teamId: getCharacterTeam(character),
x: pos.x,
y: pos.y,
});
}
return {
localPlayerId: meId || (mePos ? `${mePos.x}:${mePos.y}` : null),
localTeamId: meTeam ?? null,
camera: {
midX: Number(cam?.midPoint?.x ?? 0),
midY: Number(cam?.midPoint?.y ?? 0),
zoom: Number(cam?.zoom ?? 1),
},
players: fallbackPlayers,
};
}
function findAutoAimTarget(cfg) {
const snapshot = getAutoAimPlayerSnapshot();
if (!snapshot?.camera || !Array.isArray(snapshot.players)) return null;
const mx = crosshairState.mouseX;
const my = crosshairState.mouseY;
const width = window.innerWidth;
const height = window.innerHeight;
const margin = 80;
const fovDeg = Math.max(15, Math.min(180, Number(cfg.fovDeg) || 120));
const stickyFovDeg = Math.min(180, fovDeg * 1.15);
const aimDirX = Number(autoAimState.aimDirX) || 1;
const aimDirY = Number(autoAimState.aimDirY) || 0;
const angleToAimDir = (toX, toY) => {
const len = Math.hypot(toX, toY);
if (len <= 0.001) return 0;
const nx = toX / len;
const ny = toY / len;
const dot = Math.max(-1, Math.min(1, nx * aimDirX + ny * aimDirY));
return Math.acos(dot) * (180 / Math.PI);
};
const canUseSticky = cfg.stickToTarget && autoAimState.target?.player;
let stickyCandidate = null;
let best = null;
for (const player of snapshot.players) {
if (!player) continue;
const pid = String(player.id ?? "");
if (!pid || (snapshot.localPlayerId != null && pid === String(snapshot.localPlayerId))) continue;
if (cfg.teamCheck && snapshot.localTeamId != null && player.teamId === snapshot.localTeamId) continue;
const screen = projectWorldToScreen(player, snapshot.camera, width, height);
if (!screen) continue;
if (screen.x < -margin || screen.x > width + margin || screen.y < -margin || screen.y > height + margin) continue;
const dist = Math.hypot(mx - screen.x, my - screen.y);
const angleDelta = angleToAimDir(screen.x - mx, screen.y - my);
if (angleDelta <= fovDeg && (!best || dist < best.distancePx)) {
best = { player, playerId: pid, screenX: screen.x, screenY: screen.y, distancePx: dist, angleDelta };
}
if (canUseSticky && pid === String(autoAimState.target.playerId) && angleDelta <= stickyFovDeg) {
stickyCandidate = { player, playerId: pid, screenX: screen.x, screenY: screen.y, distancePx: dist, angleDelta };
}
}
return stickyCandidate || best;
}
function renderAutoAimOverlay(cfg) {
const ctx = autoAimState.ctx;
const canvas = autoAimState.canvas;
if (!ctx || !canvas) return;
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (!cfg.showDebugDot || !autoAimState.target) return;
const pulse = (Math.sin(performance.now() / 140) + 1) * 0.5;
const tx = autoAimState.target.screenX;
const ty = autoAimState.target.screenY;
ctx.save();
ctx.fillStyle = `rgba(255, 255, 255, ${0.55 + pulse * 0.2})`;
ctx.strokeStyle = `rgba(255, 92, 92, ${0.7 + pulse * 0.2})`;
ctx.lineWidth = 1.6;
ctx.beginPath();
ctx.arc(tx, ty, 2.8, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(tx, ty, 7 + pulse * 2.5, 0, Math.PI * 2);
ctx.stroke();
ctx.restore();
}
function autoAimTick() {
if (!autoAimState.enabled) return;
const cfg = getAutoAimConfig();
if (!cfg.enabled) {
autoAimState.target = null;
autoAimState.statusText = "Disabled in config";
renderAutoAimOverlay(cfg);
return;
}
if (cfg.onlyWhenGameFocused && (!document.hasFocus() || document.visibilityState !== "visible")) {
autoAimState.target = null;
autoAimState.statusText = "Waiting for focus";
renderAutoAimOverlay(cfg);
return;
}
if (cfg.requireMouseDown && !autoAimInputState.leftMouseDown) {
autoAimState.target = null;
autoAimState.statusText = "Waiting for mouse hold";
renderAutoAimOverlay(cfg);
return;
}
const target = findAutoAimTarget(cfg);
autoAimState.target = target;
if (!target) {
const hasShared = window.__zyroxEspShared?.ready;
const hasStores = Boolean((espState.stores ?? window.stores)?.phaser?.scene);
autoAimState.statusText = (!hasShared && !hasStores) ? "Waiting for match data" : "No target";
renderAutoAimOverlay(cfg);
return;
}
const canvas = getGameCanvas();
const smoothing = Math.max(0, Math.min(1, Number(cfg.smoothing) || 0.2));
const maxStep = Math.max(2, Number(cfg.maxStepPx) || 32);
const dx = target.screenX - crosshairState.mouseX;
const dy = target.screenY - crosshairState.mouseY;
const dist = Math.hypot(dx, dy);
if (dist > 0.01) {
const baseStep = dist * smoothing;
const step = Math.min(maxStep, Math.max(0.1, baseStep));
const ratio = Math.min(1, step / dist);
const nextX = crosshairState.mouseX + dx * ratio;
const nextY = crosshairState.mouseY + dy * ratio;
const moveX = nextX - crosshairState.mouseX;
const moveY = nextY - crosshairState.mouseY;
const moveLen = Math.hypot(moveX, moveY);
if (moveLen > 0.0001) {
autoAimState.aimDirX = moveX / moveLen;
autoAimState.aimDirY = moveY / moveLen;
}
crosshairState.mouseX = nextX;
crosshairState.mouseY = nextY;
autoAimState.lastAimX = nextX;
autoAimState.lastAimY = nextY;
if (canvas) syncAimPointer(canvas, nextX, nextY, autoAimInputState.leftMouseDown ? 1 : 0);
}
autoAimState.statusText = `Locked: ${target.player?.name ?? "Player"}`;
renderAutoAimOverlay(cfg);
}
function startAutoAim() {
if (autoAimState.enabled) return;
primeSharedPlayerData();
autoAimState.enabled = true;
autoAimState.target = null;
autoAimState.statusText = "Armed";
createAutoAimCanvas();
if (autoAimState.loopId != null) clearInterval(autoAimState.loopId);
autoAimState.loopId = setInterval(autoAimTick, 1000 / 60);
}
function stopAutoAim() {
if (!autoAimState.enabled) return;
autoAimState.enabled = false;
if (autoAimState.loopId != null) {
clearInterval(autoAimState.loopId);
autoAimState.loopId = null;
}
autoAimState.target = null;
autoAimState.statusText = "Idle";
destroyAutoAimCanvas();
}
function renderTriggerAssistOverlay(cfg) {
const ctx = triggerAssistState.ctx;
const canvas = triggerAssistState.canvas;
if (!ctx || !canvas) return;
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (!cfg.showTargetRing || !triggerAssistState.target) return;
const pulse = (Math.sin(performance.now() / 120) + 1) * 0.5;
const ringR = Math.max(10, Number(cfg.fovPx) || 85);
ctx.save();
const ringGradient = ctx.createRadialGradient(
crosshairState.mouseX,
crosshairState.mouseY,
Math.max(1, ringR * 0.1),
crosshairState.mouseX,
crosshairState.mouseY,
ringR
);
ringGradient.addColorStop(0, "rgba(255, 130, 130, 0.12)");
ringGradient.addColorStop(1, "rgba(255, 40, 40, 0.02)");
ctx.fillStyle = ringGradient;
ctx.beginPath();
ctx.arc(crosshairState.mouseX, crosshairState.mouseY, ringR, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = `rgba(255, 70, 70, ${0.7 + pulse * 0.25})`;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(crosshairState.mouseX, crosshairState.mouseY, ringR, 0, Math.PI * 2);
ctx.stroke();
ctx.setLineDash([5, 5]);
ctx.strokeStyle = `rgba(255, 225, 120, ${0.55 + pulse * 0.35})`;
ctx.beginPath();
ctx.moveTo(crosshairState.mouseX, crosshairState.mouseY);
ctx.lineTo(triggerAssistState.target.screenX, triggerAssistState.target.screenY);
ctx.stroke();
ctx.setLineDash([]);
ctx.strokeStyle = `rgba(255, 255, 120, ${0.8 + pulse * 0.2})`;
ctx.beginPath();
ctx.arc(triggerAssistState.target.screenX, triggerAssistState.target.screenY, 10 + pulse * 2.5, 0, Math.PI * 2);
ctx.stroke();
ctx.restore();
}
function triggerAssistTick() {
if (!triggerAssistState.enabled) return;
const cfg = getTriggerAssistConfig();
if (!cfg.enabled) {
triggerAssistState.statusText = "Disabled in config";
triggerAssistState.target = null;
releaseFireHold();
renderTriggerAssistOverlay(cfg);
return;
}
if (cfg.onlyWhenGameFocused && (!document.hasFocus() || document.visibilityState !== "visible")) {
triggerAssistState.statusText = "Waiting for focus";
triggerAssistState.target = null;
releaseFireHold();
renderTriggerAssistOverlay(cfg);
return;
}
const target = findTriggerTarget(cfg);
triggerAssistState.target = target;
if (!target) {
const hasShared = window.__zyroxEspShared?.ready;
const hasStores = Boolean((espState.stores ?? window.stores)?.phaser?.scene);
triggerAssistState.statusText = (!hasShared && !hasStores) ? "Waiting for match data" : "No target";
releaseFireHold();
renderTriggerAssistOverlay(cfg);
return;
}
triggerAssistState.statusText = `Inside Hitbox: ${target.player?.name ?? "Player"}`;
const now = Date.now();
const minDelay = Math.max(16, Number(cfg.fireRateMs) || 45);
if (cfg.holdToFire) {
attemptFire(true, false, null);
} else if (now - triggerAssistState.lastFireAt >= minDelay && attemptFire(false, false, null)) {
triggerAssistState.lastFireAt = now;
}
if (triggerAssistState.releaseTimeoutId != null) clearTimeout(triggerAssistState.releaseTimeoutId);
triggerAssistState.releaseTimeoutId = setTimeout(() => {
if (!document.hasFocus() || document.visibilityState !== "visible") releaseFireHold();
}, Math.max(160, minDelay * 2));
renderTriggerAssistOverlay(cfg);
}
function startTriggerAssist() {
if (triggerAssistState.enabled) return;
primeSharedPlayerData();
triggerAssistState.enabled = true;
createTriggerAssistCanvas();
triggerAssistState.statusText = "Armed";
if (triggerAssistState.loopId != null) clearInterval(triggerAssistState.loopId);
triggerAssistState.loopId = setInterval(triggerAssistTick, 1000 / 60);
}
function stopTriggerAssist() {
if (!triggerAssistState.enabled) return;
triggerAssistState.enabled = false;
if (triggerAssistState.loopId != null) {
clearInterval(triggerAssistState.loopId);
triggerAssistState.loopId = null;
}
if (triggerAssistState.releaseTimeoutId != null) {
clearTimeout(triggerAssistState.releaseTimeoutId);
triggerAssistState.releaseTimeoutId = null;
}
releaseFireHold();
triggerAssistState.target = null;
triggerAssistState.statusText = "Idle";
destroyTriggerAssistCanvas();
}
window.addEventListener("blur", () => {
autoAimInputState.leftMouseDown = false;
autoAimState.target = null;
releaseFireHold();
});
document.addEventListener("visibilitychange", () => {
if (document.visibilityState !== "visible") {
autoAimInputState.leftMouseDown = false;
autoAimState.target = null;
releaseFireHold();
}
});
window.addEventListener("resize", resizeTriggerAssistCanvas);
window.addEventListener("resize", resizeAutoAimCanvas);
window.addEventListener("mousedown", (event) => {
if (event.button === 0) autoAimInputState.leftMouseDown = true;
}, { passive: true });
window.addEventListener("mouseup", (event) => {
if (event.button === 0) autoAimInputState.leftMouseDown = false;
}, { passive: true });
const MODULE_BEHAVIORS = {
"ESP": {
onEnable: startEsp,
onDisable: stopEsp,
},
"Crosshair": {
onEnable: startCrosshair,
onDisable: stopCrosshair,
},
"Trigger Assist": {
onEnable: startTriggerAssist,
onDisable: stopTriggerAssist,
},
"Auto Aim": {
onEnable: startAutoAim,
onDisable: stopAutoAim,
},
};
const WORKING_MODULES = new Set(["Auto Answer", "ESP", "Crosshair", "Trigger Assist", "Auto Aim"]);
// --- End of Core Utilities ---
const MENU_LAYOUT = {
general: {
title: "General",
groups: [
{
name: "Core",
modules: [
{
name: "Auto Answer",
settings: [
{ id: "speed", label: "Answer Delay", type: "slider", min: 200, max: 3000, step: 50, default: 500 },
],
},
"Answer Streak",
"Question Preview",
"Skip Animation",
"Instant Continue",
],
},
{
name: "Visual",
modules: [
{
name: "ESP",
settings: [
{ id: "hitbox", label: "Hitbox", type: "checkbox", default: true },
{ id: "hitboxSize", label: "Hitbox Size", type: "slider", min: 24, max: 270, step: 2, default: 150, unit: "px" },
{ id: "hitboxWidth", label: "Hitbox Width", type: "slider", min: 1, max: 10, step: 1, default: 3, unit: "px" },
{ id: "hitboxColor", label: "Hitbox Color", type: "color", default: "#ff3b3b" },
{ id: "names", label: "Names", type: "checkbox", default: false },
{ id: "namesDistanceOnly", label: "Distance Only", type: "checkbox", default: false },
{ id: "nameSize", label: "Name Size", type: "slider", min: 10, max: 32, step: 1, default: 20, unit: "px" },
{ id: "nameColor", label: "Name Color", type: "color", default: "#000000" },
{
id: "offscreenStyle",
label: "Off-screen Indicator",
type: "select",
default: "tracers",
options: [
{ value: "none", label: "None" },
{ value: "tracers", label: "Tracers" },
{ value: "arrows", label: "Arrows" },
],
},
{
id: "offscreenTheme",
label: "Off-screen Theme",
type: "select",
default: "classic",
options: [
{ value: "classic", label: "Classic" },
{ value: "dashed", label: "Dashed" },
{ value: "neon", label: "Neon" },
],
},
{ id: "alwaysTracer", label: "Always Show Tracer", type: "checkbox", default: false },
{ id: "tracerWidth", label: "Tracer Width", type: "slider", min: 1, max: 8, step: 1, default: 3, unit: "px" },
{ id: "tracerColor", label: "Tracer Color", type: "color", default: "#ff3b3b" },
{ id: "arrowSize", label: "Arrow Size", type: "slider", min: 8, max: 30, step: 1, default: 14, unit: "px" },
{ id: "arrowColor", label: "Arrow Color", type: "color", default: "#ff3b3b" },
{
id: "arrowStyle",
label: "Arrow Style",
type: "select",
default: "regular",
options: [
{ value: "regular", label: "Regular Arrow" },
{ value: "dot", label: "Dot" },
{ value: "modern", label: "Modern Arrow" },
],
},
],
},
{
name: "Health Bars",
settings: [
{ id: "enabled", label: "Enabled", type: "checkbox", default: true },
{ id: "width", label: "Bar Width", type: "slider", min: 20, max: 120, step: 1, default: 54, unit: "px" },
{ id: "height", label: "Bar Height", type: "slider", min: 3, max: 18, step: 1, default: 6, unit: "px" },
{ id: "yOffset", label: "Vertical Offset", type: "slider", min: 8, max: 90, step: 1, default: 32, unit: "px" },
{ id: "showText", label: "Show HP Text", type: "checkbox", default: true },
],
},
"HUD",
"Overlay",
],
},
{
name: "Quality of Life",
modules: ["Notifications", "Session Timer", "Hotkeys", "Clipboard Tools"],
},
{
name: "Combat",
modules: [
{
name: "Crosshair",
settings: [
{ id: "enabled", label: "Show Crosshair", type: "checkbox", default: true },
{ id: "style", label: "Style", type: "select", default: "x",
options: [
{ value: "cross", label: "Cross (gap)" },
{ value: "solid", label: "Solid Cross" },
{ value: "crossdot", label: "Cross + Dot" },
{ value: "dot", label: "Dot" },
{ value: "circle", label: "Circle" },
{ value: "circlecross", label: "Circle + Cross" },
{ value: "plus", label: "Plus (thick)" },
{ value: "x", label: "X (diagonal)" },
],
},
{ id: "color", label: "Crosshair Color", type: "color", default: "#ff3b3b" },
{ id: "crosshairSize", label: "Crosshair Size", type: "slider", default: 25, min: 4, max: 40, step: 1, unit: "px" },
{ id: "lineSize", label: "Cursor Width", type: "slider", default: 4, min: 1, max: 6, step: 0.5, unit: "px" },
{ id: "showLine", label: "Show Line", type: "checkbox", default: false },
{ id: "lineColor", label: "Line Color", type: "color", default: "#ff3b3b" },
{ id: "tracerLineSize", label: "Tracer Thickness", type: "slider", default: 1.5, min: 0.5, max: 5, step: 0.5, unit: "px" },
{ id: "hoverHighlight", label: "Player Hover", type: "checkbox", default: true },
{ id: "hoverColor", label: "Hover Color", type: "color", default: "#ffff00" },
],
},
{
name: "Trigger Assist",
settings: [
{ id: "enabled", label: "Enabled", type: "checkbox", default: true },
{ id: "teamCheck", label: "Ignore Teammates", type: "checkbox", default: true },
{ id: "fovPx", label: "FOV Radius", type: "slider", default: 85, min: 8, max: 220, step: 1, unit: "px" },
{ id: "holdToFire", label: "Hold Fire While Targeted", type: "checkbox", default: false },
{ id: "fireRateMs", label: "Fire Rate Limit", type: "slider", default: 45, min: 16, max: 500, step: 1, unit: "ms" },
{ id: "requireLOS", label: "Require LOS (future)", type: "checkbox", default: false },
{ id: "onlyWhenGameFocused", label: "Only When Focused", type: "checkbox", default: true },
{ id: "showTargetRing", label: "Show Target Ring", type: "checkbox", default: true },
],
},
{
name: "Auto Aim",
settings: [
{ id: "enabled", label: "Enabled", type: "checkbox", default: true },
{ id: "teamCheck", label: "Ignore Teammates", type: "checkbox", default: true },
{ id: "fovDeg", label: "Aim FOV", type: "slider", default: 120, min: 15, max: 180, step: 1, unit: "°" },
{ id: "smoothing", label: "Smoothing", type: "slider", default: 0.2, min: 0, max: 1, step: 0.01 },
{ id: "maxStepPx", label: "Max Step", type: "slider", default: 32, min: 2, max: 120, step: 1, unit: "px" },
{ id: "stickToTarget", label: "Stick To Target", type: "checkbox", default: true },
{ id: "onlyWhenGameFocused", label: "Only When Focused", type: "checkbox", default: true },
{ id: "requireMouseDown", label: "Require Left Mouse", type: "checkbox", default: false },
{ id: "showDebugDot", label: "Show Debug Dot", type: "checkbox", default: true },
],
},
],
},
],
},
gamemodeSpecific: {
title: "Gamemode Specific",
groups: [
{
name: "Classic",
modules: ["Classic Auto Buy", "Classic Streak Manager", "Classic Speed Round"],
},
{
name: "Team Mode",
modules: ["Team Comms Overlay", "Team Upgrade Sync", "Team Split Strategy"],
},
{
name: "Capture The Flag",
modules: ["Flag Pathing", "Flag Return Alert", "Carrier Tracker"],
},
{
name: "Tag: Domination",
modules: ["Zone Priority", "Tag Timer Overlay", "Defense Rotation"],
},
{
name: "The Floor Is Lava",
modules: ["Safe Tile Highlight", "Lava Cycle Timer", "Route Assist"],
},
],
},
};
const state = {
visible: true,
searchQuery: "",
shellWidth: 1160,
shellHeight: 640,
enabledModules: new Set(),
moduleItems: new Map(),
modulePanels: new Map(),
moduleEntries: [],
moduleConfig: new Map(),
collapsedPanels: {},
listeningForBind: null,
listeningForMenuBind: false,
searchAutofocus: true,
hideBrokenModules: true,
displayMode: "loose",
looseInitialized: false,
loosePositions: {
topbar: { x: 12, y: 12 },
},
loosePanelPositions: {},
mergedRootPosition: { left: 20, top: 28 },
modules: new Map(),
};
// Bumped to v3 — includes display-mode and loose layout position persistence
const STORAGE_KEY = "zyrox_client_settings_v3";
const DEFAULT_FOOTER_HTML = () => `<span>Press <b>${CONFIG.toggleKey}</b> to show/hide menu</span><span>Right click modules for settings</span>`;
function debounce(fn, waitMs = 120) {
let timerId = null;
return (...args) => {
if (timerId) clearTimeout(timerId);
timerId = setTimeout(() => {
timerId = null;
fn(...args);
}, waitMs);
};
}
// Defer all DOM work — WebSocket is already patched above at document-start.
function initUi() {
const style = document.createElement("style");
style.textContent = `
.zyrox-root,
.zyrox-config-backdrop {
--zyx-border: #ff6f6f99;
--zyx-border-soft: rgba(255, 255, 255, 0.12);
--zyx-text: #d6d6df;
--zyx-text-strong: #fff;
--zyx-header-text: #fff;
--zyx-header-bg-start: rgba(255, 74, 74, 0.24);
--zyx-header-bg-end: rgba(60, 18, 18, 0.92);
--zyx-topbar-bg-start: rgba(255, 74, 74, 0.22);
--zyx-topbar-bg-end: rgba(56, 16, 16, 0.9);
--zyx-icon-color: #ffdada;
--zyx-outline-color: #ff5b5bcc;
--zyx-slider-color: #ff6b6b;
--zyx-panel-count-text: #ffd9d9;
--zyx-panel-count-border: rgba(255, 100, 100, 0.45);
--zyx-panel-count-bg: rgba(8, 8, 10, 0.6);
--zyx-settings-header-start: rgba(255, 61, 61, .3);
--zyx-settings-header-end: rgba(45, 12, 12, .95);
--zyx-settings-sidebar-bg: rgba(24, 24, 32, .22);
--zyx-settings-body-bg: linear-gradient(180deg, rgba(18, 18, 22, 0.97), rgba(8, 8, 10, 0.97));
--zyx-settings-text: #ffe5e5;
--zyx-settings-subtext: #c2c2ce;
--zyx-settings-card-bg: rgba(255,255,255,.03);
--zyx-settings-card-border: rgba(255,255,255,.08);
--zyx-select-bg: rgba(20, 20, 28, 0.9);
--zyx-select-text: #ffe5e5;
--zyx-accent-soft: #ffbdbd;
--zyx-search-text: #ffe6e6;
--zyx-checkmark-color: #ff6b6b;
--zyx-module-hover-bg: rgba(30, 30, 36, 0.9);
--zyx-module-hover-border: rgba(255, 255, 255, 0.14);
--zyx-module-active-start: rgba(255, 61, 61, 0.32);
--zyx-module-active-end: rgba(40, 10, 10, 0.8);
--zyx-module-active-border: rgba(255, 61, 61, 0.52);
--zyx-hover-shift: 2px;
--zyx-shell-blur: 10px;
--zyx-muted: #9b9bab;
--zyx-shadow: 0 18px 48px rgba(0, 0, 0, 0.55);
--zyx-radius-xl: 14px;
--zyx-radius-lg: 12px;
--zyx-radius-md: 10px;
--zyx-font: Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
/* FIX: button accent colours are now CSS variables, updated by applyAppearance() */
--zyx-btn-bg: rgba(255, 61, 61, 0.12);
--zyx-btn-hover-bg: rgba(255, 61, 61, 0.2);
}
.zyrox-root {
all: initial;
position: fixed;
top: 28px;
left: 20px;
z-index: 2147483647;
color: var(--zyx-text);
user-select: none;
font-family: var(--zyx-font);
}
.zyrox-root * { box-sizing: border-box; font-family: inherit; }
.zyrox-config-backdrop {
all: initial;
position: fixed;
inset: 0;
z-index: 2147483648;
background: rgba(0, 0, 0, 0.26);
backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
color: var(--zyx-settings-text);
font-family: var(--zyx-font);
}
.zyrox-config-backdrop * { box-sizing: border-box; font-family: inherit; }
.zyrox-hidden { display: none !important; }
.zyrox-shell {
position: relative;
display: inline-flex;
flex-direction: column;
gap: 10px;
padding: 10px;
width: 1160px;
height: 640px;
border-radius: var(--zyx-radius-xl);
border: 1px solid var(--zyx-border-soft);
background: linear-gradient(150deg, #ff3d3d22, rgba(0, 0, 0, 0.45));
backdrop-filter: blur(var(--zyx-shell-blur)) saturate(115%);
box-shadow: var(--zyx-shadow);
overflow: auto;
}
.zyrox-topbar {
min-height: 42px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 8px 12px;
border-radius: var(--zyx-radius-lg);
border: 1px solid var(--zyx-border);
background: linear-gradient(125deg, var(--zyx-topbar-bg-start), var(--zyx-topbar-bg-end));
cursor: move;
}
.zyrox-topbar-right {
display: flex;
align-items: center;
gap: 8px;
}
/* Hide legacy topbar category controls from older builds/state */
.zyrox-collapse-row,
.zyrox-collapse-btn {
display: none !important;
}
.zyrox-shell.loose-mode {
padding: 0;
width: auto !important;
height: auto !important;
min-width: 0;
min-height: 0;
border: none;
box-shadow: none;
background: transparent !important;
backdrop-filter: none !important;
overflow: visible;
}
.zyrox-shell.loose-mode .zyrox-footer,
.zyrox-shell.loose-mode .zyrox-resize-handle {
display: none;
}
.zyrox-shell.loose-mode .zyrox-topbar {
position: absolute;
top: 0;
left: 0;
width: fit-content;
min-height: 38px;
padding: 6px 10px;
z-index: 4;
}
.zyrox-shell.loose-mode .zyrox-section {
display: contents;
}
.zyrox-shell.loose-mode .zyrox-section-label {
display: none;
}
.zyrox-shell.loose-mode .zyrox-panels {
display: block;
overflow: visible;
max-height: none;
padding: 0;
}
.zyrox-shell.loose-mode .zyrox-panel {
position: absolute;
width: 212px;
z-index: 3;
}
.zyrox-shell.loose-mode .zyrox-panel-header {
cursor: move;
}
.zyrox-brand { display: flex; align-items: center; gap: 10px; color: var(--zyx-text-strong); }
.zyrox-logo {
width: 30px;
height: 30px;
border-radius: 6px;
object-fit: contain;
box-shadow: 0 0 0 1px rgba(255,255,255,.25), 0 0 18px rgba(255,61,61,.45);
outline: 1px solid var(--zyx-icon-color);
}
.zyrox-brand .title { font-size: 13px; font-weight: 700; line-height: 1; }
.zyrox-brand .subtitle { font-size: 11px; font-weight: 500; color: rgba(255,255,255,.7); }
.zyrox-chip {
font-size: 10px;
color: var(--zyx-settings-text);
background: rgba(0, 0, 0, 0.35);
border: 1px solid var(--zyx-outline-color);
border-radius: 999px;
padding: 4px 8px;
line-height: 1;
}
.zyrox-keybind-btn {
font-size: 11px;
color: var(--zyx-icon-color);
background: rgba(0, 0, 0, 0.35);
border: 1px solid var(--zyx-outline-color);
border-radius: 8px;
padding: 4px 8px;
line-height: 1;
cursor: pointer;
}
.zyrox-settings-btn {
width: 30px;
height: 30px;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 15px;
color: var(--zyx-icon-color);
background: rgba(0, 0, 0, 0.35);
border: 1px solid var(--zyx-outline-color);
border-radius: 8px;
line-height: 1;
cursor: pointer;
padding: 0;
}
.zyrox-search {
width: 190px;
height: 28px;
border-radius: 8px;
border: 1px solid var(--zyx-outline-color);
background: rgba(10, 8, 8, 0.72);
color: var(--zyx-search-text);
padding: 0 10px;
font-size: 12px;
outline: none;
}
.zyrox-search:focus {
background: rgba(15, 12, 12, 0.8);
border-color: rgba(255, 255, 255, 0.15);
}
.zyrox-section { display: flex; flex-direction: column; gap: 7px; }
.zyrox-section-label {
font-size: 11px;
letter-spacing: 0.25px;
color: var(--zyx-accent-soft);
padding-left: 2px;
text-transform: uppercase;
}
.zyrox-panels {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: flex-start;
align-content: flex-start;
overflow: auto;
max-width: 100%;
padding-bottom: 2px;
max-height: 38vh;
}
/* FIX: was hardcoded rgba(255, 61, 61, 0.3) — now follows theme */
.zyrox-panels::-webkit-scrollbar { width: 8px; height: 8px; }
.zyrox-panels::-webkit-scrollbar-thumb { background: var(--zyx-btn-hover-bg); border-radius: 999px; }
.zyrox-panel {
width: 212px;
border-radius: var(--zyx-radius-lg);
border: 1px solid var(--zyx-border-soft);
background: linear-gradient(180deg, rgba(24, 24, 30, 0.9), rgba(10, 10, 12, 0.9));
overflow: hidden;
}
.zyrox-panel-header {
min-height: 33px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 10px;
font-size: 12px;
font-weight: 600;
color: var(--zyx-header-text);
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
background: linear-gradient(90deg, var(--zyx-header-bg-start), var(--zyx-header-bg-end));
}
.zyrox-panel-collapse-btn {
font-size: 10px;
color: var(--zyx-panel-count-text);
background: var(--zyx-panel-count-bg);
border: 1px solid var(--zyx-panel-count-border);
border-radius: 999px;
padding: 3px 7px;
line-height: 1;
cursor: pointer;
}
.zyrox-panel-collapse-btn.collapsed {
opacity: 0.62;
}
.zyrox-module-list { margin: 0; padding: 7px; list-style: none; display: flex; flex-direction: column; gap: 5px; }
.zyrox-module {
min-height: 30px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 0 10px;
font-size: 13px;
font-weight: 500;
color: var(--zyx-text);
border: 1px solid transparent;
border-radius: var(--zyx-radius-md);
background: rgba(255, 255, 255, 0.03);
transition: transform .11s ease, background .11s ease, border-color .11s ease, color .11s ease;
cursor: pointer;
white-space: nowrap;
}
.zyrox-module:hover {
background: var(--zyx-module-hover-bg);
border-color: var(--zyx-module-hover-border);
color: var(--zyx-settings-text);
transform: translateX(var(--zyx-hover-shift));
}
.zyrox-module.active {
color: #fff;
background: linear-gradient(90deg, var(--zyx-module-active-start), var(--zyx-module-active-end));
border-color: var(--zyx-module-active-border);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.06);
}
.zyrox-bind-label {
font-size: 10px;
color: var(--zyx-muted);
border: 1px solid rgba(255, 255, 255, 0.14);
border-radius: 6px;
padding: 2px 5px;
line-height: 1;
background: rgba(0, 0, 0, 0.35);
}
.zyrox-footer {
display: flex;
justify-content: space-between;
align-items: center;
gap: 10px;
color: var(--zyx-muted);
font-size: 11px;
padding: 0 3px;
}
.zyrox-config {
position: relative;
z-index: 2147483649;
min-width: 340px;
border-radius: 11px;
border: 1px solid var(--zyx-border);
background: linear-gradient(180deg, rgba(18, 18, 22, 0.97), rgba(8, 8, 10, 0.97));
box-shadow: var(--zyx-shadow);
overflow: hidden;
}
.zyrox-config.hidden { display: none !important; }
/* FIX: config header now uses settings-header vars so it follows the theme */
.zyrox-config-header { padding: 11px 13px; border-bottom: 1px solid rgba(255,255,255,.09); background: linear-gradient(90deg, var(--zyx-settings-header-start), var(--zyx-settings-header-end)); }
.zyrox-config-title { color: var(--zyx-settings-text); font-size: 14px; font-weight: 700; margin-bottom: 3px; }
.zyrox-config-sub { color: var(--zyx-settings-subtext); font-size: 12px; }
.zyrox-config-body { padding: 13px; color: var(--zyx-settings-text); }
.zyrox-config-row { display:flex; justify-content:space-between; align-items:center; gap:8px; color:var(--zyx-settings-text); font-size:14px; }
.zyrox-config-actions { display: flex; align-items: center; gap: 6px; }
/* FIX: was hardcoded rgba(255, 61, 61, ...) — now reads CSS variables set by applyAppearance() */
.zyrox-btn {
border: 1px solid var(--zyx-outline-color);
background: var(--zyx-btn-bg);
color: var(--zyx-settings-text);
border-radius: 8px;
padding: 7px 10px;
font-size: 12px;
cursor: pointer;
}
.zyrox-btn:hover { background: var(--zyx-btn-hover-bg); color: #fff; }
.zyrox-btn-square {
width: 33px;
height: 33px;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
font-weight: 700;
line-height: 1;
font-size: 16px;
color: var(--zyx-icon-color);
}
.zyrox-config-backdrop.hidden { display: none !important; }
.zyrox-settings {
position: relative;
z-index: 2147483649;
width: min(760px, 92vw);
height: min(620px, 88vh);
border-radius: 12px;
border: 1px solid var(--zyx-border);
background: var(--zyx-settings-body-bg);
box-shadow: var(--zyx-shadow);
overflow: hidden;
color: var(--zyx-settings-text);
font-family: var(--zyx-font);
display: flex;
flex-direction: column;
}
.zyrox-config {
font-family: var(--zyx-font);
}
.esp-value-text {
font-family: var(--zyx-font);
font-size: 0.85em;
}
.zyrox-settings.hidden { display: none !important; }
.zyrox-settings-header { padding: 12px 14px; border-bottom: 1px solid rgba(255,255,255,.09); background: linear-gradient(90deg, var(--zyx-settings-header-start), var(--zyx-settings-header-end)); }
.zyrox-settings-title { font-size: 16px; font-weight: 700; margin-bottom: 4px; color: var(--zyx-settings-text); }
.zyrox-settings-sub { font-size: 12px; color: var(--zyx-settings-subtext); }
.zyrox-settings-layout { display: grid; grid-template-columns: 150px 1fr; min-height: 0; flex: 1; }
.zyrox-settings-sidebar {
border-right: 1px solid rgba(255,255,255,.08);
padding: 10px;
display: flex;
flex-direction: column;
gap: 8px;
background: var(--zyx-settings-sidebar-bg);
}
.zyrox-settings-tab {
border: 1px solid rgba(255,255,255,.12);
border-radius: 8px;
padding: 7px 8px;
font-size: 12px;
color: var(--zyx-settings-text);
background: rgba(0,0,0,.2);
text-align: left;
cursor: pointer;
}
.zyrox-settings-tab.active {
border-color: var(--zyx-outline-color);
background: color-mix(in srgb, var(--zyx-topbar-bg-start) 75%, transparent);
color: #fff;
}
.zyrox-settings-pane { min-height: 0; display: flex; }
.zyrox-settings-body { padding: 14px; display: flex; flex-direction: column; gap: 8px; overflow: auto; min-height: 0; width: 100%; }
.zyrox-settings-body::-webkit-scrollbar { width: 10px; }
.zyrox-settings-body::-webkit-scrollbar-thumb { background: color-mix(in srgb, var(--zyx-outline-color) 70%, transparent); border-radius: 999px; }
.zyrox-settings-pane.hidden { display: none !important; }
.zyrox-setting-card { border: 1px solid var(--zyx-settings-card-border); border-radius: 10px; padding: 8px 10px; background: var(--zyx-settings-card-bg); display:flex; align-items:center; justify-content:space-between; gap:10px; }
.zyrox-setting-card label { display:block; font-size: 12px; color: var(--zyx-settings-text); margin: 0; }
.zyrox-setting-card input[type='color'] {
width: 52px;
height: 30px;
border: 1px solid rgba(255,255,255,.2);
border-radius: 8px;
background: transparent;
cursor: pointer;
overflow: hidden;
padding: 0;
}
.zyrox-setting-card input[type='range'] { width: 190px; accent-color: var(--zyx-slider-color); }
.zyrox-setting-card input[type='checkbox'] { width: 16px; height: 16px; accent-color: var(--zyx-checkmark-color); }
.zyrox-setting-card select {
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
border: 1px solid var(--zyx-settings-card-border);
background: var(--zyx-select-bg);
background-image:
linear-gradient(45deg, transparent 50%, var(--zyx-select-text) 50%),
linear-gradient(135deg, var(--zyx-select-text) 50%, transparent 50%);
background-position:
calc(100% - 14px) calc(50% - 2px),
calc(100% - 8px) calc(50% - 2px);
background-size: 6px 6px, 6px 6px;
background-repeat: no-repeat;
color: var(--zyx-select-text);
border-radius: 8px;
padding: 6px 26px 6px 8px;
font-size: 12px;
min-height: 30px;
}
.zyrox-setting-card select:focus {
outline: 1px solid var(--zyx-outline-color);
outline-offset: 1px;
}
.zyrox-setting-card select option {
background: var(--zyx-select-bg);
color: var(--zyx-select-text);
}
.zyrox-gradient-pair { display: inline-flex; align-items: center; gap: 8px; }
.zyrox-preset-header { font-size: 11px; text-transform: uppercase; letter-spacing: .35px; color: var(--zyx-accent-soft); margin-bottom: 4px; }
.zyrox-preset-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 2px; }
.zyrox-preset-btn { border: 1px solid var(--zyx-outline-color); background: rgba(0,0,0,.26); color: var(--zyx-settings-text); border-radius: 8px; padding: 6px 10px; font-size: 11px; cursor: pointer; }
.zyrox-preset-btn .preset-swatch { display:inline-block; width:10px; height:10px; border-radius:999px; margin-right:6px; border:1px solid rgba(255,255,255,.3); vertical-align:-1px; }
.zyrox-preset-btn:hover { background: var(--zyx-btn-hover-bg); }
.zyrox-subheading {
grid-column: 1 / -1;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.25px;
color: var(--zyx-accent-soft);
margin-top: -2px;
margin-bottom: -4px;
}
.zyrox-about-content {
display: flex;
flex-direction: column;
gap: 10px;
font-size: 12px;
color: var(--zyx-settings-subtext);
line-height: 1.45;
user-select: text;
}
.zyrox-about-content b {
color: var(--zyx-settings-text);
font-weight: 700;
}
.zyrox-about-source-btn {
align-self: flex-start;
text-decoration: none;
margin-top: 4px;
}
.zyrox-settings-actions { display:flex; justify-content:space-between; align-items:flex-end; gap:8px; padding: 8px 14px 14px; }
.zyrox-settings-actions-group { display:flex; gap:8px; }
.zyrox-settings-action-btn {
min-height: 31px;
line-height: 1.1;
white-space: nowrap;
}
.zyrox-close-btn {
position: absolute;
top: 10px;
right: 10px;
width: 24px;
height: 24px;
border-radius: 6px;
border: 1px solid var(--zyx-outline-color);
background: rgba(0, 0, 0, 0.25);
color: var(--zyx-icon-color);
cursor: pointer;
line-height: 1;
font-size: 14px;
}
.zyrox-resize-handle {
position: absolute;
right: 2px;
bottom: 2px;
width: 14px;
height: 14px;
cursor: nwse-resize;
border-right: 2px solid rgba(255, 110, 110, 0.85);
border-bottom: 2px solid rgba(255, 110, 110, 0.85);
border-radius: 0 0 8px 0;
opacity: 0.9;
}
/* Theme layout styles */
.zyrox-theme-layout {
display: grid;
grid-template-columns: 180px 1fr;
min-height: 0;
height: 100%;
}
.zyrox-theme-sidebar {
border-right: 1px solid rgba(255,255,255,.08);
padding: 14px;
display: flex;
flex-direction: column;
align-items: flex-start;
background: var(--zyx-settings-sidebar-bg);
overflow-y: auto;
}
.zyrox-theme-sidebar::-webkit-scrollbar {
width: 6px;
}
.zyrox-theme-sidebar::-webkit-scrollbar-thumb {
background: color-mix(in srgb, var(--zyx-outline-color) 50%, transparent);
border-radius: 999px;
}
.zyrox-theme-categories {
display: flex;
flex-direction: column;
gap: 6px;
width: 100%;
}
.zyrox-theme-category {
border: 1px solid rgba(255,255,255,.12);
border-radius: 8px;
padding: 8px 10px;
font-size: 11px;
color: var(--zyx-settings-text);
background: rgba(0,0,0,.2);
text-align: left;
cursor: pointer;
transition: all 0.15s ease;
}
.zyrox-theme-category:hover {
background: var(--zyx-btn-hover-bg);
border-color: rgba(255,255,255,.2);
}
.zyrox-theme-category.active {
border-color: var(--zyx-outline-color);
background: color-mix(in srgb, var(--zyx-topbar-bg-start) 75%, transparent);
color: #fff;
}
.zyrox-theme-content {
padding: 14px;
overflow-y: auto;
min-height: 0;
}
.zyrox-theme-content::-webkit-scrollbar {
width: 10px;
}
.zyrox-theme-content::-webkit-scrollbar-thumb {
background: color-mix(in srgb, var(--zyx-outline-color) 70%, transparent);
border-radius: 999px;
}
.zyrox-theme-section {
display: none;
flex-direction: column;
gap: 8px;
}
.zyrox-theme-section.active {
display: flex;
}
`;
const root = document.createElement("div");
root.className = "zyrox-root";
const shell = document.createElement("div");
shell.className = "zyrox-shell";
const topbar = document.createElement("div");
topbar.className = "zyrox-topbar";
topbar.innerHTML = `
<div class="zyrox-brand">
<img class="zyrox-logo" src="${CONFIG.logoUrl}" alt="Zyrox logo" />
<div>
<div class="title">${CONFIG.title}</div>
<div class="subtitle">${CONFIG.subtitle}</div>
</div>
</div>
<div class="zyrox-collapse-row"></div>
<div class="zyrox-topbar-right">
<input class="zyrox-search" type="text" placeholder="Search utilities..." autocomplete="off" />
<button class="zyrox-settings-btn" type="button" title="Open client settings">⚙</button>
<span class="zyrox-chip">v${CONFIG.version}</span>
</div>
`;
const searchInput = topbar.querySelector(".zyrox-search");
const settingsBtn = topbar.querySelector(".zyrox-settings-btn");
const collapseRow = topbar.querySelector(".zyrox-collapse-row");
const generalSection = document.createElement("section");
generalSection.className = "zyrox-section";
generalSection.innerHTML = `<div class="zyrox-section-label">General</div>`;
const gamemodeSection = document.createElement("section");
gamemodeSection.className = "zyrox-section";
gamemodeSection.innerHTML = `<div class="zyrox-section-label">Gamemode Specific</div>`;
const footer = document.createElement("div");
footer.className = "zyrox-footer";
setFooterText();
const resizeHandle = document.createElement("div");
resizeHandle.className = "zyrox-resize-handle";
const configMenu = document.createElement("div");
configMenu.className = "zyrox-config hidden";
configMenu.innerHTML = `
<div class="zyrox-config-header">
<div class="zyrox-config-title">Module Config</div>
<div class="zyrox-config-sub">Edit settings</div>
</div>
<button class="zyrox-close-btn config-close-btn" type="button" title="Close">✕</button>
<div class="zyrox-config-body">
<div class="zyrox-config-row">
<span>Keybind</span>
<div class="zyrox-config-actions">
<button class="zyrox-btn zyrox-btn-square" type="button" title="Reset keybind">↺</button>
<button class="zyrox-btn" type="button">Set keybind</button>
</div>
</div>
</div>
`;
const configBackdrop = document.createElement("div");
configBackdrop.className = "zyrox-config-backdrop hidden";
configBackdrop.appendChild(configMenu);
const settingsMenu = document.createElement("div");
settingsMenu.className = "zyrox-settings hidden";
settingsMenu.innerHTML = `
<div class="zyrox-settings-header">
<div class="zyrox-settings-title">Client Settings</div>
<div class="zyrox-settings-sub">Customize colors and appearance</div>
</div>
<button class="zyrox-close-btn settings-close-top" type="button" title="Close">✕</button>
<div class="zyrox-settings-layout">
<div class="zyrox-settings-sidebar">
<button class="zyrox-settings-tab active" type="button" data-tab="controls">Controls</button>
<button class="zyrox-settings-tab" type="button" data-tab="theme">Theme</button>
<button class="zyrox-settings-tab" type="button" data-tab="appearance">Appearance</button>
<button class="zyrox-settings-tab" type="button" data-tab="about">About</button>
</div>
<div class="zyrox-settings-pane" data-pane="controls">
<div class="zyrox-settings-body">
<div class="zyrox-subheading">Menu</div>
<div class="zyrox-setting-card">
<label>Menu Toggle Key</label>
<button class="zyrox-keybind-btn settings-menu-key" type="button">Menu Key: ${CONFIG.toggleKey}</button>
<button class="zyrox-btn zyrox-btn-square settings-menu-key-reset" type="button" title="Reset menu key">↺</button>
</div>
<div class="zyrox-subheading">Search</div>
<div class="zyrox-setting-card">
<label>Auto Focus Search</label>
<input type="checkbox" class="set-search-autofocus" checked />
</div>
<div class="zyrox-setting-card">
<label>Hide Non-Working Modules</label>
<input type="checkbox" class="set-hide-broken-modules" checked />
</div>
</div>
</div>
<div class="zyrox-settings-pane hidden" data-pane="theme">
<div class="zyrox-settings-body">
<div class="zyrox-subheading">Presets</div>
<div class="zyrox-preset-row" style="margin-bottom: 14px;">
<button type="button" class="zyrox-preset-btn" data-preset="default"><span class="preset-swatch" style="background:#ff3d3d"></span>Default</button>
<button type="button" class="zyrox-preset-btn" data-preset="green"><span class="preset-swatch" style="background:#2dff75"></span>Green</button>
<button type="button" class="zyrox-preset-btn" data-preset="ice"><span class="preset-swatch" style="background:#6cd8ff"></span>Ice</button>
<button type="button" class="zyrox-preset-btn" data-preset="grayscale"><span class="preset-swatch" style="background:#bfbfbf"></span>Greyscale</button>
</div>
<div class="zyrox-subheading">Display Mode</div>
<div class="zyrox-settings-actions-group" style="margin-bottom: 14px; margin-top: 8px;">
<button class="zyrox-btn set-display-mode active" data-display-mode="merged" type="button">Merged</button>
<button class="zyrox-btn set-display-mode" data-display-mode="loose" type="button">Loose</button>
</div>
</div>
</div>
<div class="zyrox-settings-pane hidden" data-pane="appearance">
<div class="zyrox-settings-body">
<div class="zyrox-subheading">Layout & Sizing</div>
<div class="zyrox-setting-card">
<label>UI Scale</label>
<input type="range" class="set-scale" min="80" max="130" value="100" />
</div>
<div class="zyrox-setting-card">
<label>Corner Radius</label>
<input type="range" class="set-radius" min="6" max="20" value="14" />
</div>
<div class="zyrox-setting-card">
<label>Panel Blur</label>
<input type="range" class="set-blur" min="0" max="16" value="10" />
</div>
<div class="zyrox-subheading">Motion</div>
<div class="zyrox-setting-card">
<label>Module Hover Shift</label>
<input type="range" class="set-hover-shift" min="0" max="6" value="2" />
</div>
<div class="zyrox-subheading">Main Window</div>
<div class="zyrox-setting-card">
<label>Accent Color</label>
<input type="color" class="set-accent" value="#ff3d3d" />
</div>
<div class="zyrox-setting-card">
<label>Background Gradient</label>
<span class="zyrox-gradient-pair">
<input type="color" class="set-shell-bg-start" value="#ff3d3d" />
<input type="color" class="set-shell-bg-end" value="#000000" />
</span>
</div>
<div class="zyrox-setting-card">
<label>Top Bar Color</label>
<input type="color" class="set-topbar-color" value="#ff4a4a" />
</div>
<div class="zyrox-setting-card">
<label>Text Color</label>
<input type="color" class="set-text" value="#d6d6df" />
</div>
<div class="zyrox-setting-card">
<label>Panel Border</label>
<input type="color" class="set-border" value="#ff6f6f" />
</div>
<div class="zyrox-setting-card">
<label>Background Opacity</label>
<input type="range" class="set-opacity" min="20" max="100" value="45" />
</div>
<div class="zyrox-subheading">Buttons & Inputs</div>
<div class="zyrox-setting-card">
<label>Outline Color</label>
<input type="color" class="set-outline-color" value="#ff5b5b" />
</div>
<div class="zyrox-setting-card">
<label>Slider Color</label>
<input type="color" class="set-slider-color" value="#ff6b6b" />
</div>
<div class="zyrox-setting-card">
<label>Checkmark Color</label>
<input type="color" class="set-checkmark-color" value="#ff6b6b" />
</div>
<div class="zyrox-setting-card">
<label>Dropdown Background</label>
<input type="color" class="set-select-bg" value="#17171f" />
</div>
<div class="zyrox-setting-card">
<label>Dropdown Text</label>
<input type="color" class="set-select-text" value="#ffe5e5" />
</div>
<div class="zyrox-subheading">Typography</div>
<div class="zyrox-setting-card">
<label>Font Family</label>
<select class="set-font">
<option value="Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif" selected>Inter (Default)</option>
<option value="JetBrains Mono, 'Courier New', monospace">JetBrains Mono</option>
<option value="'Segoe UI', Tahoma, Geneva, Verdana, sans-serif">Segoe UI</option>
<option value="Roboto, 'Helvetica Neue', Arial, sans-serif">Roboto</option>
<option value="'Open Sans', 'Helvetica Neue', Arial, sans-serif">Open Sans</option>
<option value="'Fira Code', 'Courier New', monospace">Fira Code</option>
<option value="Poppins, 'Helvetica Neue', Arial, sans-serif">Poppins</option>
</select>
</div>
<div class="zyrox-setting-card">
<label>Muted Text</label>
<input type="color" class="set-muted-text" value="#9b9bab" />
</div>
<div class="zyrox-setting-card">
<label>Label Accent</label>
<input type="color" class="set-accent-soft" value="#ffbdbd" />
</div>
<div class="zyrox-setting-card">
<label>Search Text</label>
<input type="color" class="set-search-text" value="#ffe6e6" />
</div>
<div class="zyrox-subheading">Icons & Badges</div>
<div class="zyrox-setting-card">
<label>Icon Color</label>
<input type="color" class="set-icon-color" value="#ffdada" />
</div>
<div class="zyrox-setting-card">
<label>Panel Count Text</label>
<input type="color" class="set-panel-count-text" value="#ffd9d9" />
</div>
<div class="zyrox-setting-card">
<label>Panel Count Border</label>
<input type="color" class="set-panel-count-border" value="#ff6464" />
</div>
<div class="zyrox-setting-card">
<label>Panel Count Background</label>
<input type="color" class="set-panel-count-bg" value="#1a1a1e" />
</div>
<div class="zyrox-subheading">Panels & Modules</div>
<div class="zyrox-setting-card">
<label>Module Bar Gradient</label>
<span class="zyrox-gradient-pair">
<input type="color" class="set-header-start" value="#ff4a4a" />
<input type="color" class="set-header-end" value="#3c1212" />
</span>
</div>
<div class="zyrox-setting-card">
<label>Module Bar Text</label>
<input type="color" class="set-header-text" value="#ffffff" />
</div>
<div class="zyrox-subheading">Settings Menu</div>
<div class="zyrox-setting-card">
<label>Settings Header Gradient</label>
<span class="zyrox-gradient-pair">
<input type="color" class="set-settings-header-start" value="#ff3d3d" />
<input type="color" class="set-settings-header-end" value="#2d0c0c" />
</span>
</div>
<div class="zyrox-setting-card">
<label>Settings Sidebar Tint</label>
<input type="color" class="set-settings-sidebar" value="#181820" />
</div>
<div class="zyrox-setting-card">
<label>Settings Body Tint</label>
<input type="color" class="set-settings-body" value="#121216" />
</div>
<div class="zyrox-setting-card">
<label>Settings Text Color</label>
<input type="color" class="set-settings-text" value="#ffe5e5" />
</div>
<div class="zyrox-setting-card">
<label>Settings Subtext Color</label>
<input type="color" class="set-settings-subtext" value="#c2c2ce" />
</div>
<div class="zyrox-setting-card">
<label>Settings Card Border</label>
<input type="color" class="set-settings-card-border" value="#ffffff" />
</div>
<div class="zyrox-setting-card">
<label>Settings Card Background</label>
<input type="color" class="set-settings-card-bg" value="#ffffff" />
</div>
<div class="zyrox-setting-card">
<label>ESP Value Text Color</label>
<input type="color" class="set-esp-value-text-color" value="#ffffff" />
</div>
</div>
</div>
<div class="zyrox-settings-pane hidden" data-pane="about">
<div class="zyrox-settings-body">
<div class="zyrox-subheading">Client Info</div>
<div class="zyrox-setting-card">
<div class="zyrox-about-content">
<div><b>Zyrox Client</b> is a custom opensource userscript hacked client for Gimkit with module toggles, keybinds, and theming controls.</div>
<div>We are not responsible for any bans, account issues, data loss, or damages that may result from using this client. Use it at your own risk.</div>
<div>Version: ${CONFIG.version}</div>
<a
class="zyrox-btn zyrox-about-source-btn"
href="https://github.com/Bob-alt-828100/zyrox-gimkit-client"
target="_blank"
rel="noopener noreferrer"
>View Source Code</a>
</div>
</div>
</div>
</div>
</div>
<div class="zyrox-settings-actions">
<div class="zyrox-settings-actions-group" style="flex-direction:column;gap:5px;align-items:flex-start;">
<button class="zyrox-btn zyrox-settings-action-btn settings-reset" type="button">Reset Appearance</button>
<button class="zyrox-btn zyrox-settings-action-btn settings-reset-all" type="button" style="opacity:0.8;">Reset All</button>
</div>
<div class="zyrox-settings-actions-group">
<button class="zyrox-btn settings-save" type="button">Save</button>
<button class="zyrox-btn settings-close" type="button">Close</button>
</div>
</div>
`;
configBackdrop.appendChild(settingsMenu);
const configTitleEl = configMenu.querySelector(".zyrox-config-title");
const configSubEl = configMenu.querySelector(".zyrox-config-sub");
const configCloseBtn = configMenu.querySelector(".config-close-btn");
const settingsTabs = [...settingsMenu.querySelectorAll(".zyrox-settings-tab")];
const settingsPanes = [...settingsMenu.querySelectorAll(".zyrox-settings-pane")];
const configBody = configMenu.querySelector(".zyrox-config-body");
// Backward-compat alias for legacy code paths that still reference this identifier.
const setBindButtonEl = configMenu.querySelector(".set-bind-btn");
const settingsMenuKeyBtn = settingsMenu.querySelector(".settings-menu-key");
const settingsMenuKeyResetBtn = settingsMenu.querySelector(".settings-menu-key-reset");
const settingsTopCloseBtn = settingsMenu.querySelector(".settings-close-top");
const settingsSaveBtn = settingsMenu.querySelector(".settings-save");
const presetButtons = [...settingsMenu.querySelectorAll(".zyrox-preset-btn")];
const searchAutofocusInput = settingsMenu.querySelector(".set-search-autofocus");
const hideBrokenModulesInput = settingsMenu.querySelector(".set-hide-broken-modules");
const accentInput = settingsMenu.querySelector(".set-accent");
const shellBgStartInput = settingsMenu.querySelector(".set-shell-bg-start");
const shellBgEndInput = settingsMenu.querySelector(".set-shell-bg-end");
const topbarColorInput = settingsMenu.querySelector(".set-topbar-color");
const iconColorInput = settingsMenu.querySelector(".set-icon-color");
const outlineColorInput = settingsMenu.querySelector(".set-outline-color");
const panelCountTextInput = settingsMenu.querySelector(".set-panel-count-text");
const panelCountBorderInput = settingsMenu.querySelector(".set-panel-count-border");
const panelCountBgInput = settingsMenu.querySelector(".set-panel-count-bg");
const borderInput = settingsMenu.querySelector(".set-border");
const textInput = settingsMenu.querySelector(".set-text");
const opacityInput = settingsMenu.querySelector(".set-opacity");
const sliderColorInput = settingsMenu.querySelector(".set-slider-color");
const checkmarkColorInput = settingsMenu.querySelector(".set-checkmark-color");
const selectBgInput = settingsMenu.querySelector(".set-select-bg");
const selectTextInput = settingsMenu.querySelector(".set-select-text");
const mutedTextInput = settingsMenu.querySelector(".set-muted-text");
const accentSoftInput = settingsMenu.querySelector(".set-accent-soft");
const searchTextInput = settingsMenu.querySelector(".set-search-text");
const fontInput = settingsMenu.querySelector(".set-font");
const headerStartInput = settingsMenu.querySelector(".set-header-start");
const headerEndInput = settingsMenu.querySelector(".set-header-end");
const headerTextInput = settingsMenu.querySelector(".set-header-text");
const settingsHeaderStartInput = settingsMenu.querySelector(".set-settings-header-start");
const settingsHeaderEndInput = settingsMenu.querySelector(".set-settings-header-end");
const settingsSidebarInput = settingsMenu.querySelector(".set-settings-sidebar");
const settingsBodyInput = settingsMenu.querySelector(".set-settings-body");
const settingsTextInput = settingsMenu.querySelector(".set-settings-text");
const settingsSubtextInput = settingsMenu.querySelector(".set-settings-subtext");
const settingsCardBorderInput = settingsMenu.querySelector(".set-settings-card-border");
const settingsCardBgInput = settingsMenu.querySelector(".set-settings-card-bg");
const espValueTextColorInput = settingsMenu.querySelector(".set-esp-value-text-color");
const scaleInput = settingsMenu.querySelector(".set-scale");
const radiusInput = settingsMenu.querySelector(".set-radius");
const blurInput = settingsMenu.querySelector(".set-blur");
const hoverShiftInput = settingsMenu.querySelector(".set-hover-shift");
const displayModeButtons = [...settingsMenu.querySelectorAll(".set-display-mode")];
const settingsResetBtn = settingsMenu.querySelector(".settings-reset");
const settingsResetAllBtn = settingsMenu.querySelector(".settings-reset-all");
const settingsCloseBtn = settingsMenu.querySelector(".settings-close");
const panelByName = new Map();
const panelCollapseButtons = new Map();
let openConfigModule = null;
let currentSetBindBtn = null;
let currentResetBindBtn = null;
let currentBindTextEl = null;
function setBindButtonText(text) {
const bindButton = currentSetBindBtn || setBindButtonEl || configMenu.querySelector(".set-bind-btn");
if (bindButton) bindButton.textContent = text;
}
function setFooterText() {
footer.innerHTML = DEFAULT_FOOTER_HTML();
}
function setCurrentBindText(bind) {
if (!currentBindTextEl) return;
currentBindTextEl.textContent = bind ? `Keybind: ${bind}` : "Keybind: none";
}
function isModuleHiddenByWorkState(moduleName) {
return state.hideBrokenModules && !WORKING_MODULES.has(moduleName);
}
function getModuleLayoutConfig(moduleName) {
const allGroups = [...MENU_LAYOUT.general.groups, ...MENU_LAYOUT.gamemodeSpecific.groups];
const found = allGroups
.flatMap((group) => group.modules || [])
.find((mod) => typeof mod === "object" && mod && mod.name === moduleName);
return found || null;
}
function ensureModuleConfigStore() {
if (state.moduleConfig instanceof Map) return state.moduleConfig;
const recovered = new Map();
if (state.moduleConfig && typeof state.moduleConfig === "object") {
for (const [moduleName, cfg] of Object.entries(state.moduleConfig)) {
if (cfg && typeof cfg === "object") {
recovered.set(moduleName, { keybind: cfg.keybind || null });
}
}
}
state.moduleConfig = recovered;
return state.moduleConfig;
}
function moduleCfg(name) {
const store = ensureModuleConfigStore();
if (!store.has(name)) {
const layout = getModuleLayoutConfig(name);
const settings = {};
if (layout && Array.isArray(layout.settings)) {
for (const setting of layout.settings) {
settings[setting.id] = setting.default ?? setting.min ?? 0;
}
}
store.set(name, { keybind: null, ...settings });
}
const cfg = store.get(name);
if (name === "ESP") {
window.__zyroxEspConfig = { ...getEspRenderConfig(), ...cfg };
} else if (name === "Trigger Assist") {
window.__zyroxTriggerAssistConfig = { ...getTriggerAssistConfig(), ...cfg };
} else if (name === "Auto Aim") {
window.__zyroxAutoAimConfig = { ...getAutoAimConfig(), ...cfg };
}
return cfg;
}
function setBindLabel(item, moduleName) {
const label = item.querySelector(".zyrox-bind-label");
const bind = moduleCfg(moduleName).keybind;
label.textContent = bind || "";
label.style.display = bind ? "" : "none";
}
function toggleModule(moduleName) {
if (isModuleHiddenByWorkState(moduleName)) return;
const item = state.moduleItems.get(moduleName);
const moduleInstance = state.modules.get(moduleName);
if (!item || !moduleInstance) return;
if (moduleInstance.enabled) {
moduleInstance.disable();
item.classList.remove("active");
state.enabledModules.delete(moduleName);
if (moduleName === "Auto Answer") stopAutoAnswer();
} else {
moduleInstance.enable();
item.classList.add("active");
state.enabledModules.add(moduleName);
if (moduleName === "Auto Answer") startAutoAnswer();
}
}
// ---------------------------------------------------------------------------
// AUTO-ANSWER MODULE CONTROLS
// The actual logic runs in page context (injected above).
// These functions just start/stop the interval via window.__zyroxAutoAnswer.
// ---------------------------------------------------------------------------
function stopAutoAnswer() {
window.__zyroxAutoAnswer?.stop();
}
function startAutoAnswer() {
const cfg = moduleCfg("Auto Answer");
const speed = Math.max(200, Number(cfg.speed) || 1000);
window.__zyroxAutoAnswer?.start(speed);
}
function refreshAutoAnswerLoopIfEnabled() {
if (state.enabledModules.has("Auto Answer")) startAutoAnswer();
}
function closeConfig() {
configBackdrop.classList.add("hidden");
configMenu.classList.add("hidden");
settingsMenu.classList.add("hidden");
openConfigModule = null;
currentBindTextEl = null;
state.listeningForBind = null;
setBindButtonText("Set keybind");
}
function openConfig(moduleName) {
openConfigModule = moduleName;
const cfg = moduleCfg(moduleName);
const moduleLayout = getModuleLayoutConfig(moduleName);
configBody.innerHTML = `
<div class="zyrox-config-row">
<span class="zyrox-keybind-current">Keybind: ${cfg.keybind || "none"}</span>
<div class="zyrox-config-actions">
<button class="zyrox-btn zyrox-btn-square reset-bind-btn" type="button" title="Reset keybind">↺</button>
<button class="zyrox-btn set-bind-btn" type="button">Set keybind</button>
</div>
</div>
`;
currentResetBindBtn = configMenu.querySelector(".reset-bind-btn");
currentSetBindBtn = configMenu.querySelector(".set-bind-btn");
currentBindTextEl = configMenu.querySelector(".zyrox-keybind-current");
if (currentSetBindBtn) {
currentSetBindBtn.addEventListener("click", () => {
if (!openConfigModule) return;
state.listeningForBind = openConfigModule;
setBindButtonText("Press any key...");
});
}
if (currentResetBindBtn) {
currentResetBindBtn.addEventListener("click", () => {
if (!openConfigModule) return;
const activeCfg = moduleCfg(openConfigModule);
activeCfg.keybind = null;
const item = state.moduleItems.get(openConfigModule);
if (item) setBindLabel(item, openConfigModule);
setCurrentBindText(null);
state.listeningForBind = null;
setBindButtonText("Set keybind");
});
}
if (moduleName === "ESP") {
const defaults = getEspRenderConfig();
Object.assign(cfg, { ...defaults, ...cfg });
window.__zyroxEspConfig = { ...cfg };
const makeRow = (title, html) => {
const row = document.createElement("div");
row.className = "zyrox-setting-card";
row.innerHTML = `
<div style="display:flex;flex-direction:column;gap:8px;width:100%;">
<label style="font-weight:600;">${title}</label>
${html}
</div>
`;
configBody.appendChild(row);
return row;
};
const hitboxRow = makeRow("Hitbox", `
<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;">
<label style="display:flex;align-items:center;gap:6px;"><input type="checkbox" class="esp-hitbox-enabled" ${cfg.hitbox ? "checked" : ""} /> Enabled</label>
<label>Size <input type="range" class="esp-hitbox-size" min="24" max="270" step="2" value="${cfg.hitboxSize}" /></label>
<span class="esp-hitbox-size-value esp-value-text">${cfg.hitboxSize}px</span>
<label>Width <input type="range" class="esp-hitbox-width" min="1" max="10" step="1" value="${cfg.hitboxWidth}" /></label>
<span class="esp-hitbox-width-value esp-value-text">${cfg.hitboxWidth}px</span>
<input type="color" class="esp-hitbox-color" value="${cfg.hitboxColor}" />
</div>
`);
const namesRow = makeRow("Names", `
<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;">
<label style="display:flex;align-items:center;gap:6px;"><input type="checkbox" class="esp-names-enabled" ${cfg.names ? "checked" : ""} /> Enabled</label>
<label style="display:flex;align-items:center;gap:6px;"><input type="checkbox" class="esp-names-distance-only" ${cfg.namesDistanceOnly ? "checked" : ""} /> Distance Only</label>
<label>Size <input type="range" class="esp-name-size" min="10" max="32" step="1" value="${cfg.nameSize}" /></label>
<span class="esp-name-size-value esp-value-text">${cfg.nameSize}px</span>
<input type="color" class="esp-name-color" value="${cfg.nameColor}" />
</div>
`);
const offscreenRow = makeRow("Off-screen", `
<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;">
<label>Mode
<select class="esp-offscreen-style">
<option value="none" ${cfg.offscreenStyle === "none" ? "selected" : ""}>None</option>
<option value="tracers" ${cfg.offscreenStyle === "tracers" ? "selected" : ""}>Tracers</option>
<option value="arrows" ${cfg.offscreenStyle === "arrows" ? "selected" : ""}>Arrows</option>
</select>
</label>
<label style="display:flex;align-items:center;gap:6px;">
<input type="checkbox" class="esp-always-tracer" ${cfg.alwaysTracer ? "checked" : ""} />
Always Show Tracer
</label>
<label>Theme
<select class="esp-offscreen-theme">
<option value="classic" ${cfg.offscreenTheme === "classic" ? "selected" : ""}>Classic</option>
<option value="dashed" ${cfg.offscreenTheme === "dashed" ? "selected" : ""}>Dashed</option>
<option value="neon" ${cfg.offscreenTheme === "neon" ? "selected" : ""}>Neon</option>
</select>
</label>
<span class="esp-tracer-controls" style="display:flex;align-items:center;gap:10px;">
<label>Tracer Width <input type="range" class="esp-tracer-width" min="1" max="8" step="1" value="${cfg.tracerWidth}" /></label>
<span class="esp-tracer-width-value esp-value-text">${cfg.tracerWidth}px</span>
<input type="color" class="esp-tracer-color" value="${cfg.tracerColor}" />
</span>
<span class="esp-arrow-controls" style="display:flex;align-items:center;gap:10px;">
<label>Arrow Size <input type="range" class="esp-arrow-size" min="8" max="30" step="1" value="${cfg.arrowSize}" /></label>
<span class="esp-arrow-size-value esp-value-text">${cfg.arrowSize}px</span>
<input type="color" class="esp-arrow-color" value="${cfg.arrowColor}" />
<label>Arrow Style
<select class="esp-arrow-style">
<option value="regular" ${cfg.arrowStyle === "regular" ? "selected" : ""}>Regular Arrow</option>
<option value="dot" ${cfg.arrowStyle === "dot" ? "selected" : ""}>Dot</option>
<option value="modern" ${cfg.arrowStyle === "modern" ? "selected" : ""}>Modern Arrow</option>
</select>
</label>
</span>
</div>
`);
const syncEsp = () => {
window.__zyroxEspConfig = { ...cfg };
};
syncEsp();
const applyValueTextColor = () => {
for (const el of configBody.querySelectorAll(".esp-value-text")) {
el.style.color = cfg.valueTextColor || "#ffffff";
}
};
applyValueTextColor();
const bindCheckbox = (root, selector, key) => {
const input = root.querySelector(selector);
if (!input) return;
input.addEventListener("change", (event) => {
cfg[key] = Boolean(event.target.checked);
syncEsp();
});
};
const bindColor = (root, selector, key) => {
const input = root.querySelector(selector);
if (!input) return;
input.addEventListener("input", (event) => {
cfg[key] = String(event.target.value || "#ffffff");
syncEsp();
});
};
const bindSlider = (root, selector, key, labelSelector) => {
const input = root.querySelector(selector);
const label = root.querySelector(labelSelector);
if (!input) return;
input.addEventListener("input", (event) => {
const value = Number(event.target.value);
cfg[key] = value;
if (label) label.textContent = `${value}px`;
syncEsp();
});
};
bindCheckbox(hitboxRow, ".esp-hitbox-enabled", "hitbox");
bindSlider(hitboxRow, ".esp-hitbox-size", "hitboxSize", ".esp-hitbox-size-value");
bindSlider(hitboxRow, ".esp-hitbox-width", "hitboxWidth", ".esp-hitbox-width-value");
bindColor(hitboxRow, ".esp-hitbox-color", "hitboxColor");
bindCheckbox(namesRow, ".esp-names-enabled", "names");
bindCheckbox(namesRow, ".esp-names-distance-only", "namesDistanceOnly");
bindSlider(namesRow, ".esp-name-size", "nameSize", ".esp-name-size-value");
bindColor(namesRow, ".esp-name-color", "nameColor");
const styleInput = offscreenRow.querySelector(".esp-offscreen-style");
const tracerControls = offscreenRow.querySelector(".esp-tracer-controls");
const arrowControls = offscreenRow.querySelector(".esp-arrow-controls");
const alwaysTracerInput = offscreenRow.querySelector(".esp-always-tracer");
const refreshIndicatorModeVisibility = () => {
const mode = cfg.offscreenStyle === "arrows" || cfg.offscreenStyle === "none" ? cfg.offscreenStyle : "tracers";
if (tracerControls) tracerControls.style.display = mode === "tracers" ? "flex" : "none";
if (arrowControls) arrowControls.style.display = mode === "arrows" ? "flex" : "none";
};
if (styleInput) {
styleInput.addEventListener("change", (event) => {
cfg.offscreenStyle = String(event.target.value || "tracers");
refreshIndicatorModeVisibility();
syncEsp();
});
}
const themeInput = offscreenRow.querySelector(".esp-offscreen-theme");
if (themeInput) {
themeInput.addEventListener("change", (event) => {
cfg.offscreenTheme = String(event.target.value || "classic");
syncEsp();
});
}
if (alwaysTracerInput) {
alwaysTracerInput.addEventListener("change", (event) => {
cfg.alwaysTracer = Boolean(event.target.checked);
syncEsp();
});
}
bindSlider(offscreenRow, ".esp-tracer-width", "tracerWidth", ".esp-tracer-width-value");
bindColor(offscreenRow, ".esp-tracer-color", "tracerColor");
bindSlider(offscreenRow, ".esp-arrow-size", "arrowSize", ".esp-arrow-size-value");
bindColor(offscreenRow, ".esp-arrow-color", "arrowColor");
const arrowStyleInput = offscreenRow.querySelector(".esp-arrow-style");
if (arrowStyleInput) {
arrowStyleInput.addEventListener("change", (event) => {
cfg.arrowStyle = String(event.target.value || "regular");
syncEsp();
});
}
refreshIndicatorModeVisibility();
} else if (moduleName === "Crosshair") {
const defaults = getCrosshairConfig();
Object.assign(cfg, { ...defaults, ...cfg });
window.__zyroxCrosshairConfig = { ...cfg };
const syncCrosshair = () => { window.__zyroxCrosshairConfig = { ...cfg }; };
syncCrosshair();
const makeRow = (title, html) => {
const row = document.createElement("div");
row.className = "zyrox-setting-card";
row.innerHTML = `
<div style="display:flex;flex-direction:column;gap:8px;width:100%;">
<label style="font-weight:600;">${title}</label>
${html}
</div>
`;
configBody.appendChild(row);
return row;
};
const enabledRow = makeRow("Crosshair", `
<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;">
<label style="display:flex;align-items:center;gap:6px;">
<input type="checkbox" class="xh-enabled" ${cfg.enabled !== false ? "checked" : ""} />
Show Crosshair
</label>
<input type="color" class="xh-color" value="${cfg.color || "#ff3b3b"}" title="Crosshair color" />
</div>
`);
const styleRow = makeRow("Style", `
<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;">
<select class="xh-style">
<option value="cross" ${cfg.style === "cross" ? "selected" : ""}>Cross (gap)</option>
<option value="solid" ${cfg.style === "solid" ? "selected" : ""}>Solid Cross</option>
<option value="crossdot" ${cfg.style === "crossdot" ? "selected" : ""}>Cross + Dot</option>
<option value="dot" ${cfg.style === "dot" ? "selected" : ""}>Dot</option>
<option value="circle" ${cfg.style === "circle" ? "selected" : ""}>Circle</option>
<option value="circlecross" ${cfg.style === "circlecross" ? "selected" : ""}>Circle + Cross</option>
<option value="plus" ${cfg.style === "plus" ? "selected" : ""}>Plus (thick)</option>
<option value="x" ${cfg.style === "x" ? "selected" : ""}>X (diagonal)</option>
</select>
</div>
`);
const sizeRow = makeRow("Crosshair Size", `
<div style="display:flex;align-items:center;gap:10px;">
<input type="range" class="xh-crosshair-size" min="4" max="40" step="1" value="${cfg.crosshairSize ?? 25}" style="flex:1;" />
<span class="xh-crosshair-size-label" style="min-width:36px;text-align:right;font-size:0.85em;opacity:0.75;">${cfg.crosshairSize ?? 25}px</span>
</div>
`);
const lineSizeRow = makeRow("Cursor Width", `
<div style="display:flex;align-items:center;gap:10px;">
<input type="range" class="xh-line-size" min="0.5" max="6" step="0.5" value="${cfg.lineSize ?? 4}" style="flex:1;" />
<span class="xh-line-size-label" style="min-width:36px;text-align:right;font-size:0.85em;opacity:0.75;">${cfg.lineSize ?? 4}px</span>
</div>
`);
const lineRow = makeRow("Line to Cursor", `
<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;">
<label style="display:flex;align-items:center;gap:6px;">
<input type="checkbox" class="xh-show-line" ${cfg.showLine ? "checked" : ""} />
Show Line
</label>
<input type="color" class="xh-line-color" value="${cfg.lineColor || "#ff3b3b"}" title="Line color" />
</div>
`);
const tracerSizeRow = makeRow("Tracer Thickness", `
<div style="display:flex;align-items:center;gap:10px;">
<input type="range" class="xh-tracer-size" min="0.5" max="5" step="0.5" value="${cfg.tracerLineSize ?? 1.5}" style="flex:1;" />
<span class="xh-tracer-size-label" style="min-width:36px;text-align:right;font-size:0.85em;opacity:0.75;">${cfg.tracerLineSize ?? 1.5}px</span>
</div>
`);
const hoverRow = makeRow("Player Hover", `
<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;">
<label style="display:flex;align-items:center;gap:6px;">
<input type="checkbox" class="xh-hover-highlight" ${cfg.hoverHighlight ? "checked" : ""} />
Change color on player
</label>
<input type="color" class="xh-hover-color" value="${cfg.hoverColor || "#ffff00"}" title="Hover color" />
</div>
`);
enabledRow.querySelector(".xh-enabled").addEventListener("change", (e) => {
cfg.enabled = e.target.checked;
syncCrosshair();
});
enabledRow.querySelector(".xh-color").addEventListener("input", (e) => {
cfg.color = e.target.value;
syncCrosshair();
});
styleRow.querySelector(".xh-style").addEventListener("change", (e) => {
cfg.style = e.target.value;
syncCrosshair();
});
sizeRow.querySelector(".xh-crosshair-size").addEventListener("input", (e) => {
const v = Number(e.target.value);
cfg.crosshairSize = v;
sizeRow.querySelector(".xh-crosshair-size-label").textContent = `${v}px`;
syncCrosshair();
});
lineSizeRow.querySelector(".xh-line-size").addEventListener("input", (e) => {
const v = Number(e.target.value);
cfg.lineSize = v;
lineSizeRow.querySelector(".xh-line-size-label").textContent = `${v}px`;
syncCrosshair();
});
lineRow.querySelector(".xh-show-line").addEventListener("change", (e) => {
cfg.showLine = e.target.checked;
syncCrosshair();
});
lineRow.querySelector(".xh-line-color").addEventListener("input", (e) => {
cfg.lineColor = e.target.value;
syncCrosshair();
});
tracerSizeRow.querySelector(".xh-tracer-size").addEventListener("input", (e) => {
const v = Number(e.target.value);
cfg.tracerLineSize = v;
tracerSizeRow.querySelector(".xh-tracer-size-label").textContent = `${v}px`;
syncCrosshair();
});
hoverRow.querySelector(".xh-hover-highlight").addEventListener("change", (e) => {
cfg.hoverHighlight = e.target.checked;
syncCrosshair();
});
hoverRow.querySelector(".xh-hover-color").addEventListener("input", (e) => {
cfg.hoverColor = e.target.value;
syncCrosshair();
});
} else if (moduleName === "Trigger Assist") {
const defaults = getTriggerAssistConfig();
Object.assign(cfg, { ...defaults, ...cfg });
window.__zyroxTriggerAssistConfig = { ...cfg };
const syncTriggerAssist = () => { window.__zyroxTriggerAssistConfig = { ...cfg }; };
syncTriggerAssist();
for (const setting of moduleLayout?.settings || []) {
if (setting.type === "checkbox") {
if (cfg[setting.id] === undefined) cfg[setting.id] = Boolean(setting.default);
const checked = cfg[setting.id] ? "checked" : "";
const card = document.createElement("div");
card.className = "zyrox-setting-card";
card.innerHTML = `
<label>${setting.label}</label>
<input type="checkbox" class="set-module-setting-checkbox" data-setting-id="${setting.id}" ${checked} />
`;
configBody.appendChild(card);
const input = card.querySelector(".set-module-setting-checkbox");
input?.addEventListener("change", (event) => {
cfg[setting.id] = Boolean(event.target.checked);
syncTriggerAssist();
});
} else if (setting.type === "slider") {
const value = Number(cfg[setting.id] ?? setting.default ?? setting.min ?? 0);
const unit = setting.unit ?? "ms";
const card = document.createElement("div");
card.className = "zyrox-setting-card";
card.innerHTML = `
<label style="display:flex;justify-content:space-between;align-items:center;">
<span>${setting.label}</span>
<span class="zyrox-slider-value" style="font-size:0.85em;opacity:0.75;min-width:52px;text-align:right;">${value}${unit}</span>
</label>
<input type="range" class="set-module-setting" data-setting-id="${setting.id}" min="${setting.min}" max="${setting.max}" step="${setting.step}" value="${value}" />
`;
configBody.appendChild(card);
const slider = card.querySelector(".set-module-setting");
const valueLabel = card.querySelector(".zyrox-slider-value");
slider?.addEventListener("input", (event) => {
const next = Number(event.target.value);
cfg[setting.id] = next;
if (valueLabel) valueLabel.textContent = `${next}${unit}`;
syncTriggerAssist();
});
}
}
} else if (moduleName === "Auto Aim") {
const defaults = getAutoAimConfig();
Object.assign(cfg, { ...defaults, ...cfg });
window.__zyroxAutoAimConfig = { ...cfg };
const syncAutoAim = () => { window.__zyroxAutoAimConfig = { ...cfg }; };
syncAutoAim();
for (const setting of moduleLayout?.settings || []) {
if (setting.type === "checkbox") {
if (cfg[setting.id] === undefined) cfg[setting.id] = Boolean(setting.default);
const checked = cfg[setting.id] ? "checked" : "";
const card = document.createElement("div");
card.className = "zyrox-setting-card";
card.innerHTML = `
<label>${setting.label}</label>
<input type="checkbox" class="set-module-setting-checkbox" data-setting-id="${setting.id}" ${checked} />
`;
configBody.appendChild(card);
const input = card.querySelector(".set-module-setting-checkbox");
input?.addEventListener("change", (event) => {
cfg[setting.id] = Boolean(event.target.checked);
syncAutoAim();
});
} else if (setting.type === "slider") {
const value = Number(cfg[setting.id] ?? setting.default ?? setting.min ?? 0);
const unit = setting.unit ?? "";
const card = document.createElement("div");
card.className = "zyrox-setting-card";
card.innerHTML = `
<label style="display:flex;justify-content:space-between;align-items:center;">
<span>${setting.label}</span>
<span class="zyrox-slider-value" style="font-size:0.85em;opacity:0.75;min-width:52px;text-align:right;">${value}${unit}</span>
</label>
<input type="range" class="set-module-setting" data-setting-id="${setting.id}" min="${setting.min}" max="${setting.max}" step="${setting.step}" value="${value}" />
`;
configBody.appendChild(card);
const slider = card.querySelector(".set-module-setting");
const valueLabel = card.querySelector(".zyrox-slider-value");
slider?.addEventListener("input", (event) => {
const next = Number(event.target.value);
cfg[setting.id] = next;
if (valueLabel) valueLabel.textContent = `${next}${unit}`;
syncAutoAim();
});
}
}
} else if (moduleLayout && Array.isArray(moduleLayout.settings)) {
for (const setting of moduleLayout.settings) {
const settingCard = document.createElement("div");
settingCard.className = "zyrox-setting-card";
if (setting.type === "slider") {
if (cfg[setting.id] === undefined) cfg[setting.id] = setting.default ?? setting.min ?? 0;
const initialVal = cfg[setting.id];
const valueUnit = setting.unit ?? "ms";
settingCard.innerHTML = `
<label style="display:flex;justify-content:space-between;align-items:center;">
<span>${setting.label}</span>
<span class="zyrox-slider-value" style="font-size:0.85em;opacity:0.75;min-width:52px;text-align:right;">${initialVal}${valueUnit}</span>
</label>
<input type="range" class="set-module-setting" data-setting-id="${setting.id}" min="${setting.min}" max="${setting.max}" step="${setting.step}" value="${initialVal}" />
`;
const settingInput = settingCard.querySelector(".set-module-setting");
const valueLabel = settingCard.querySelector(".zyrox-slider-value");
if (settingInput) {
settingInput.addEventListener("input", (event) => {
const newVal = Number(event.target.value);
cfg[setting.id] = newVal;
if (valueLabel) valueLabel.textContent = `${newVal}${valueUnit}`;
if (moduleName === "Auto Answer" && setting.id === "speed") {
// Live-update the interval speed only while Auto Answer is enabled
if (state.enabledModules.has("Auto Answer")) {
window.__zyroxAutoAnswer?.start(newVal);
}
}
});
}
}
if (setting.type === "checkbox") {
if (cfg[setting.id] === undefined) cfg[setting.id] = Boolean(setting.default);
const checked = cfg[setting.id] ? "checked" : "";
settingCard.innerHTML = `
<label>${setting.label}</label>
<input type="checkbox" class="set-module-setting-checkbox" data-setting-id="${setting.id}" ${checked} />
`;
const settingInput = settingCard.querySelector(".set-module-setting-checkbox");
if (settingInput) {
settingInput.addEventListener("change", (event) => {
cfg[setting.id] = Boolean(event.target.checked);
});
}
}
if (setting.type === "select") {
if (cfg[setting.id] === undefined) cfg[setting.id] = setting.default ?? setting.options?.[0]?.value ?? "";
const options = Array.isArray(setting.options) ? setting.options : [];
const optionsHtml = options
.map((option) => {
const selected = String(option.value) === String(cfg[setting.id]) ? "selected" : "";
return `<option value="${option.value}" ${selected}>${option.label}</option>`;
})
.join("");
settingCard.innerHTML = `
<label>${setting.label}</label>
<select class="set-module-setting-select" data-setting-id="${setting.id}">${optionsHtml}</select>
`;
const settingInput = settingCard.querySelector(".set-module-setting-select");
if (settingInput) {
settingInput.addEventListener("change", (event) => {
cfg[setting.id] = String(event.target.value);
});
}
}
if (setting.type === "color") {
if (cfg[setting.id] === undefined) cfg[setting.id] = setting.default ?? "#ffffff";
settingCard.innerHTML = `
<label>${setting.label}</label>
<input type="color" class="set-module-setting-color" data-setting-id="${setting.id}" value="${cfg[setting.id]}" />
`;
const settingInput = settingCard.querySelector(".set-module-setting-color");
if (settingInput) {
settingInput.addEventListener("input", (event) => {
cfg[setting.id] = String(event.target.value || "#ffffff");
});
}
}
if (settingCard.innerHTML.trim()) configBody.appendChild(settingCard);
}
}
configTitleEl.textContent = moduleName;
configSubEl.textContent = "Edit settings";
setBindButtonText("Set keybind");
setCurrentBindText(cfg.keybind || null);
configBackdrop.classList.remove("hidden");
configMenu.classList.remove("hidden");
settingsMenu.classList.add("hidden");
}
function openSettings() {
configBackdrop.classList.remove("hidden");
settingsMenu.classList.remove("hidden");
configMenu.classList.add("hidden");
}
function collectSettings() {
return {
toggleKey: CONFIG.toggleKey,
searchAutofocus: searchAutofocusInput.checked,
hideBrokenModules: hideBrokenModulesInput.checked,
accent: accentInput.value,
shellBgStart: shellBgStartInput.value,
shellBgEnd: shellBgEndInput.value,
topbarColor: topbarColorInput.value,
iconColor: iconColorInput.value,
outlineColor: outlineColorInput.value,
panelCountText: panelCountTextInput.value,
panelCountBorder: panelCountBorderInput.value,
panelCountBg: panelCountBgInput.value,
border: borderInput.value,
text: textInput.value,
opacity: opacityInput.value,
sliderColor: sliderColorInput.value,
checkmarkColor: checkmarkColorInput.value,
selectBg: selectBgInput.value,
selectText: selectTextInput.value,
mutedText: mutedTextInput.value,
accentSoft: accentSoftInput.value,
searchText: searchTextInput.value,
font: fontInput.value,
headerStart: headerStartInput.value,
headerEnd: headerEndInput.value,
headerText: headerTextInput.value,
settingsHeaderStart: settingsHeaderStartInput.value,
settingsHeaderEnd: settingsHeaderEndInput.value,
settingsSidebar: settingsSidebarInput.value,
settingsBody: settingsBodyInput.value,
settingsText: settingsTextInput.value,
settingsSubtext: settingsSubtextInput.value,
settingsCardBorder: settingsCardBorderInput.value,
settingsCardBg: settingsCardBgInput.value,
espValueTextColor: espValueTextColorInput.value,
scale: scaleInput.value,
radius: radiusInput.value,
blur: blurInput.value,
hoverShift: hoverShiftInput.value,
displayMode: state.displayMode,
looseInitialized: state.looseInitialized,
loosePositions: state.loosePositions,
loosePanelPositions: state.loosePanelPositions,
collapsedPanels: state.collapsedPanels,
moduleConfig: Array.from(ensureModuleConfigStore().entries()),
};
}
function setPanelCollapsed(panelName, collapsed) {
const panel = panelByName.get(panelName);
if (!panel) return;
const list = panel.querySelector(".zyrox-module-list");
if (!list) return;
state.collapsedPanels[panelName] = collapsed;
list.style.display = collapsed ? "none" : "";
const button = panelCollapseButtons.get(panelName);
if (button) {
button.textContent = collapsed ? "▸" : "▾";
button.title = collapsed ? "Expand category" : "Collapse category";
button.setAttribute("aria-label", button.title);
button.classList.toggle("collapsed", collapsed);
}
}
function syncCollapseButtons() {
for (const [panelName, button] of panelCollapseButtons.entries()) {
const collapsed = !!state.collapsedPanels[panelName];
button.textContent = collapsed ? "▸" : "▾";
button.title = collapsed ? "Expand category" : "Collapse category";
button.setAttribute("aria-label", button.title);
button.classList.toggle("collapsed", collapsed);
}
}
function clampToViewport(x, y, el) {
const rect = el.getBoundingClientRect();
const maxX = Math.max(0, window.innerWidth - rect.width);
const maxY = Math.max(0, window.innerHeight - rect.height);
return {
x: Math.max(0, Math.min(x, maxX)),
y: Math.max(0, Math.min(y, maxY)),
};
}
function getShellScale() {
const transform = getComputedStyle(shell).transform;
if (!transform || transform === "none") return 1;
const matrix = transform.match(/^matrix\((.+)\)$/);
if (!matrix) return 1;
const values = matrix[1].split(",").map((v) => Number(v.trim()));
if (values.length < 4 || values.some((v) => !Number.isFinite(v))) return 1;
const [a, b] = values;
return Math.max(0.01, Math.hypot(a, b));
}
function clampLoosePosition(x, y, el, scale, shellRect) {
const rect = el.getBoundingClientRect();
const minX = -shellRect.left / scale;
const minY = -shellRect.top / scale;
const maxX = (window.innerWidth - shellRect.left - rect.width) / scale;
const maxY = (window.innerHeight - shellRect.top - rect.height) / scale;
return {
x: Math.max(minX, Math.min(x, maxX)),
y: Math.max(minY, Math.min(y, maxY)),
};
}
function captureLoosePanelPositionsFromMerged() {
const shellRect = shell.getBoundingClientRect();
for (const [name, panel] of panelByName.entries()) {
const rect = panel.getBoundingClientRect();
state.loosePanelPositions[name] = {
x: Math.round(rect.left - shellRect.left),
y: Math.round(rect.top - shellRect.top),
};
}
}
function setDisplayMode(mode) {
const nextMode = mode === "loose" ? "loose" : "merged";
if (nextMode === "loose" && !state.looseInitialized) {
// Capture while still in merged flow layout so the first loose layout mirrors merged positions.
shell.classList.remove("loose-mode");
captureLoosePanelPositionsFromMerged();
state.looseInitialized = true;
}
state.displayMode = nextMode;
shell.classList.toggle("loose-mode", state.displayMode === "loose");
for (const btn of displayModeButtons) {
btn.classList.toggle("active", btn.dataset.displayMode === state.displayMode);
}
if (state.displayMode === "loose") {
state.mergedRootPosition = {
left: parseInt(root.style.left || "20", 10),
top: parseInt(root.style.top || "28", 10),
};
root.style.left = "0px";
root.style.top = "0px";
const shellRect = shell.getBoundingClientRect();
const scale = getShellScale();
const clampedTopbar = clampLoosePosition(state.loosePositions.topbar.x, state.loosePositions.topbar.y, topbar, scale, shellRect);
state.loosePositions.topbar = clampedTopbar;
topbar.style.left = `${clampedTopbar.x}px`;
topbar.style.top = `${clampedTopbar.y}px`;
for (const [name, panel] of panelByName.entries()) {
const pos = state.loosePanelPositions[name] || { x: 0, y: 0 };
const clamped = clampLoosePosition(pos.x, pos.y, panel, scale, shellRect);
state.loosePanelPositions[name] = clamped;
panel.style.left = `${clamped.x}px`;
panel.style.top = `${clamped.y}px`;
}
} else {
root.style.left = `${state.mergedRootPosition.left}px`;
root.style.top = `${state.mergedRootPosition.top}px`;
topbar.style.left = "";
topbar.style.top = "";
for (const panel of panelByName.values()) {
panel.style.left = "";
panel.style.top = "";
}
shell.style.width = `${state.shellWidth}px`;
shell.style.height = `${state.shellHeight}px`;
}
}
function applyPreset(presetName) {
const preset = (() => {
if (presetName === "green") {
return {
accent: "#2dff75", shellStart: "#2dff75", shellEnd: "#03130a", topbar: "#35d96d", border: "#5dff9a",
outline: "#37d878", text: "#d7ffe6", muted: "#88b79b", soft: "#a8ffd0", search: "#e6fff0", icon: "#d7ffe9",
panelText: "#d9ffe8", panelBorder: "#5fff99", panelBg: "#04110a", slider: "#2dff75", checkmark: "#2dff75",
selectBg: "#111e16", selectText: "#d7ffe6",
headerStart: "#2dff75", headerEnd: "#0f2f1b", headerText: "#f0fff4",
settingsText: "#d7ffe6", settingsSubtext: "#a7cfb7", settingsSidebar: "#102016", settingsBody: "#0d1510",
settingsCardBorder: "#79d6a0", settingsCardBg: "#12301f",
settingsHeaderStart: "#2dff75", settingsHeaderEnd: "#0f2f1b", espValueTextColor: "#ffffff",
font: "Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif",
};
}
if (presetName === "ice") {
return {
accent: "#6cd8ff", shellStart: "#6cd8ff", shellEnd: "#07131a", topbar: "#58bff1", border: "#8ae4ff",
outline: "#6fbce8", text: "#d7edff", muted: "#8ea7bd", soft: "#b8e5ff", search: "#e7f5ff", icon: "#dff3ff",
panelText: "#e1f4ff", panelBorder: "#8fd7ff", panelBg: "#071019", slider: "#7bdfff", checkmark: "#7bdfff",
selectBg: "#0c1c26", selectText: "#d7edff",
headerStart: "#6cd8ff", headerEnd: "#133042", headerText: "#f4fbff",
settingsText: "#d7edff", settingsSubtext: "#9db4c6", settingsSidebar: "#10202c", settingsBody: "#0e141a",
settingsCardBorder: "#90cae8", settingsCardBg: "#173247",
settingsHeaderStart: "#6cd8ff", settingsHeaderEnd: "#133042", espValueTextColor: "#ffffff",
font: "Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif",
};
}
if (presetName === "grayscale") {
return {
accent: "#d3d3d3", shellStart: "#7a7a7a", shellEnd: "#0a0a0a", topbar: "#8d8d8d", border: "#b1b1b1",
outline: "#9a9a9a", text: "#dddddd", muted: "#9a9a9a", soft: "#c9c9c9", search: "#f1f1f1", icon: "#f5f5f5",
panelText: "#efefef", panelBorder: "#a0a0a0", panelBg: "#0f0f0f", slider: "#c4c4c4", checkmark: "#d0d0d0",
selectBg: "#1b1b1b", selectText: "#efefef",
headerStart: "#8f8f8f", headerEnd: "#1d1d1d", headerText: "#ffffff",
settingsText: "#efefef", settingsSubtext: "#b2b2b2", settingsSidebar: "#202020", settingsBody: "#181818",
settingsCardBorder: "#b7b7b7", settingsCardBg: "#313131",
settingsHeaderStart: "#8f8f8f", settingsHeaderEnd: "#1d1d1d", espValueTextColor: "#ffffff",
font: "Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif",
};
}
// Default (red)
return {
accent: "#ff3d3d", shellStart: "#ff3d3d", shellEnd: "#000000", topbar: "#ff4a4a", border: "#ff6f6f",
outline: "#ff5b5b", text: "#d6d6df", muted: "#9b9bab", soft: "#ffbdbd", search: "#ffe6e6", icon: "#ffdada",
panelText: "#ffd9d9", panelBorder: "#ff6464", panelBg: "#1a1a1e", slider: "#ff6b6b", checkmark: "#ff6b6b",
selectBg: "#17171f", selectText: "#ffe5e5",
headerStart: "#ff4a4a", headerEnd: "#3c1212", headerText: "#ffffff",
settingsText: "#ffe5e5", settingsSubtext: "#c2c2ce", settingsSidebar: "#181820", settingsBody: "#121216",
settingsCardBorder: "#ffffff", settingsCardBg: "#ffffff",
settingsHeaderStart: "#ff3d3d", settingsHeaderEnd: "#2d0c0c", espValueTextColor: "#ffffff",
font: "Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif",
};
})();
accentInput.value = preset.accent;
shellBgStartInput.value = preset.shellStart;
shellBgEndInput.value = preset.shellEnd;
topbarColorInput.value = preset.topbar;
borderInput.value = preset.border;
outlineColorInput.value = preset.outline;
textInput.value = preset.text;
mutedTextInput.value = preset.muted;
accentSoftInput.value = preset.soft;
searchTextInput.value = preset.search;
fontInput.value = preset.font || "Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif";
iconColorInput.value = preset.icon;
panelCountTextInput.value = preset.panelText;
panelCountBorderInput.value = preset.panelBorder;
panelCountBgInput.value = preset.panelBg;
sliderColorInput.value = preset.slider;
checkmarkColorInput.value = preset.checkmark;
selectBgInput.value = preset.selectBg;
selectTextInput.value = preset.selectText;
headerStartInput.value = preset.headerStart;
headerEndInput.value = preset.headerEnd;
headerTextInput.value = preset.headerText;
settingsHeaderStartInput.value = preset.settingsHeaderStart;
settingsHeaderEndInput.value = preset.settingsHeaderEnd;
settingsSidebarInput.value = preset.settingsSidebar;
settingsBodyInput.value = preset.settingsBody;
settingsTextInput.value = preset.settingsText;
settingsSubtextInput.value = preset.settingsSubtext;
settingsCardBorderInput.value = preset.settingsCardBorder;
settingsCardBgInput.value = preset.settingsCardBg;
espValueTextColorInput.value = preset.espValueTextColor;
applyAppearance();
}
function applyAppearance() {
const normalizeHex = (value, fallback) => {
const normalized = String(value || "").trim();
return /^#([0-9a-fA-F]{6})$/.test(normalized) ? normalized.toLowerCase() : fallback;
};
const clampNumber = (value, min, max, fallback) => {
const parsed = Number(value);
if (!Number.isFinite(parsed)) return fallback;
return Math.min(max, Math.max(min, parsed));
};
const toRgba = (hex, alpha) => {
const h = hex.replace("#", "");
const r = parseInt(h.slice(0, 2), 16);
const g = parseInt(h.slice(2, 4), 16);
const b = parseInt(h.slice(4, 6), 16);
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
};
const darken = (hex, factor) => {
const h = hex.replace("#", "");
const r = Math.max(0, Math.floor(parseInt(h.slice(0, 2), 16) * factor));
const g = Math.max(0, Math.floor(parseInt(h.slice(2, 4), 16) * factor));
const b = Math.max(0, Math.floor(parseInt(h.slice(4, 6), 16) * factor));
return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
};
const shellBgStart = normalizeHex(shellBgStartInput.value, "#ff3d3d");
const shellBgEnd = normalizeHex(shellBgEndInput.value, "#000000");
const topbarColor = normalizeHex(topbarColorInput.value, "#ff4a4a");
const iconColor = normalizeHex(iconColorInput.value, "#ffdada");
const outlineColor = normalizeHex(outlineColorInput.value, "#ff5b5b");
const panelCountText = normalizeHex(panelCountTextInput.value, "#ffd9d9");
const panelCountBorder = normalizeHex(panelCountBorderInput.value, "#ff6464");
const panelCountBg = normalizeHex(panelCountBgInput.value, "#1a1a1e");
const border = normalizeHex(borderInput.value, "#ff6f6f");
const text = normalizeHex(textInput.value, "#d6d6df");
const opacity = clampNumber(opacityInput.value, 10, 100, 45) / 100;
const sliderColor = normalizeHex(sliderColorInput.value, "#ff6b6b");
const checkmarkColor = normalizeHex(checkmarkColorInput.value, "#ff6b6b");
const selectBg = normalizeHex(selectBgInput.value, "#17171f");
const selectText = normalizeHex(selectTextInput.value, "#ffe5e5");
const mutedText = normalizeHex(mutedTextInput.value, "#9b9bab");
const accentSoft = normalizeHex(accentSoftInput.value, "#ffbdbd");
const searchText = normalizeHex(searchTextInput.value, "#ffe6e6");
const font = fontInput.value;
const headerStart = normalizeHex(headerStartInput.value, "#ff4a4a");
const headerEnd = normalizeHex(headerEndInput.value, "#3c1212");
const headerText = normalizeHex(headerTextInput.value, "#ffffff");
const settingsHeaderStart = normalizeHex(settingsHeaderStartInput.value, "#ff3d3d");
const settingsHeaderEnd = normalizeHex(settingsHeaderEndInput.value, "#2d0c0c");
const settingsSidebar = normalizeHex(settingsSidebarInput.value, "#181820");
const settingsBody = normalizeHex(settingsBodyInput.value, "#121216");
const settingsText = normalizeHex(settingsTextInput.value, "#ffe5e5");
const settingsSubtext = normalizeHex(settingsSubtextInput.value, "#c2c2ce");
const settingsCardBorder = normalizeHex(settingsCardBorderInput.value, "#ffffff");
const settingsCardBg = normalizeHex(settingsCardBgInput.value, "#ffffff");
const espValueTextColor = normalizeHex(espValueTextColorInput.value, "#ffffff");
const scale = clampNumber(scaleInput.value, 80, 130, 100) / 100;
const radius = clampNumber(radiusInput.value, 8, 22, 14);
const blur = clampNumber(blurInput.value, 0, 24, 10);
const hoverShift = clampNumber(hoverShiftInput.value, 0, 8, 2);
const themeTargets = [root.style, configBackdrop.style];
const setThemeVar = (name, value) => {
for (const target of themeTargets) target.setProperty(name, value);
};
setThemeVar("--zyx-border", `${border}99`);
setThemeVar("--zyx-text", text);
setThemeVar("--zyx-font", font);
setThemeVar("--zyx-muted", mutedText);
setThemeVar("--zyx-accent-soft", accentSoft);
setThemeVar("--zyx-search-text", searchText);
setThemeVar("--zyx-topbar-bg-start", toRgba(topbarColor, 0.22));
setThemeVar("--zyx-topbar-bg-end", toRgba(darken(topbarColor, 0.22), 0.9));
setThemeVar("--zyx-module-hover-bg", toRgba(topbarColor, 0.16));
setThemeVar("--zyx-module-hover-border", toRgba(topbarColor, 0.4));
setThemeVar("--zyx-module-active-start", toRgba(headerStart, 0.35));
setThemeVar("--zyx-module-active-end", toRgba(headerEnd, 0.82));
setThemeVar("--zyx-module-active-border", toRgba(headerStart, 0.55));
setThemeVar("--zyx-icon-color", iconColor);
setThemeVar("--zyx-outline-color", `${outlineColor}cc`);
setThemeVar("--zyx-panel-count-text", panelCountText);
setThemeVar("--zyx-panel-count-border", toRgba(panelCountBorder, 0.45));
setThemeVar("--zyx-panel-count-bg", toRgba(panelCountBg, 0.6));
setThemeVar("--zyx-header-bg-start", toRgba(headerStart, 0.24));
setThemeVar("--zyx-header-bg-end", toRgba(headerEnd, 0.92));
setThemeVar("--zyx-header-text", headerText);
setThemeVar("--zyx-settings-header-start", toRgba(settingsHeaderStart, 0.3));
setThemeVar("--zyx-settings-header-end", toRgba(settingsHeaderEnd, 0.95));
setThemeVar("--zyx-settings-sidebar-bg", toRgba(settingsSidebar, 0.22));
setThemeVar("--zyx-settings-body-bg", `linear-gradient(180deg, ${toRgba(settingsBody, 0.97)}, rgba(8, 8, 10, 0.97))`);
setThemeVar("--zyx-settings-text", settingsText);
setThemeVar("--zyx-settings-subtext", settingsSubtext);
setThemeVar("--zyx-settings-card-border", toRgba(settingsCardBorder, 0.18));
setThemeVar("--zyx-settings-card-bg", toRgba(settingsCardBg, 0.05));
setThemeVar("--zyx-slider-color", sliderColor);
setThemeVar("--zyx-checkmark-color", checkmarkColor);
setThemeVar("--zyx-select-bg", toRgba(selectBg, 0.9));
setThemeVar("--zyx-select-text", selectText);
window.__zyroxEspValueTextColor = espValueTextColor;
window.__zyroxEspConfig = { ...getEspRenderConfig(), valueTextColor: espValueTextColor, font: font };
setThemeVar("--zyx-radius-xl", `${radius}px`);
setThemeVar("--zyx-radius-lg", `${Math.max(4, radius - 2)}px`);
setThemeVar("--zyx-radius-md", `${Math.max(3, radius - 4)}px`);
setThemeVar("--zyx-hover-shift", `${hoverShift}px`);
shell.style.transform = `scale(${scale.toFixed(2)})`;
shell.style.transformOrigin = "top left";
shell.style.background = `linear-gradient(150deg, ${toRgba(shellBgStart, 0.22)}, ${toRgba(shellBgEnd, opacity.toFixed(2))})`;
setThemeVar("--zyx-shell-blur", `${blur}px`);
shell.style.backdropFilter = `blur(var(--zyx-shell-blur)) saturate(115%)`;
// FIX: derive button accent background from outlineColor so buttons always match the theme
setThemeVar("--zyx-btn-bg", toRgba(outlineColor, 0.12));
setThemeVar("--zyx-btn-hover-bg", toRgba(outlineColor, 0.2));
}
function applySearchFilter() {
const query = state.searchQuery.trim().toLowerCase();
for (const entry of state.moduleEntries) {
const hiddenByWorkState = isModuleHiddenByWorkState(entry.name);
const visibleByQuery = !query || entry.name.toLowerCase().includes(query);
const visible = !hiddenByWorkState && visibleByQuery;
entry.item.style.display = visible ? "" : "none";
}
for (const [panel, meta] of state.modulePanels.entries()) {
let visibleCount = 0;
for (const moduleName of meta.modules) {
const item = state.moduleItems.get(moduleName);
if (item && item.style.display !== "none") visibleCount += 1;
}
panel.style.display = visibleCount > 0 ? "" : "none";
}
}
function buildPanel(name, modules) {
const panel = document.createElement("section");
panel.className = "zyrox-panel";
panel.dataset.panelName = name;
const header = document.createElement("header");
header.className = "zyrox-panel-header";
const title = document.createElement("span");
title.textContent = name;
const collapseButton = document.createElement("button");
collapseButton.type = "button";
collapseButton.className = "zyrox-panel-collapse-btn";
collapseButton.textContent = "▾";
collapseButton.title = "Collapse category";
collapseButton.setAttribute("aria-label", "Collapse category");
collapseButton.addEventListener("click", (event) => {
event.stopPropagation();
const nextCollapsed = !state.collapsedPanels[name];
setPanelCollapsed(name, nextCollapsed);
});
header.appendChild(title);
header.appendChild(collapseButton);
const list = document.createElement("ul");
list.className = "zyrox-module-list";
const moduleNames = [];
for (const moduleDef of modules) {
const moduleName = typeof moduleDef === "string" ? moduleDef : moduleDef?.name;
if (!moduleName) continue;
if (state.moduleItems.has(moduleName)) continue;
moduleNames.push(moduleName);
const item = document.createElement("li");
item.className = "zyrox-module";
item.innerHTML = `<span>${moduleName}</span><span class="zyrox-bind-label"></span>`;
state.moduleItems.set(moduleName, item);
state.moduleEntries.push({ name: moduleName, item, panel });
const behavior = MODULE_BEHAVIORS[moduleName];
const moduleInstance = new Module(moduleName, {
onEnable: () => {
console.log(`${moduleName} enabled`);
if (behavior?.onEnable) behavior.onEnable();
},
onDisable: () => {
console.log(`${moduleName} disabled`);
if (behavior?.onDisable) behavior.onDisable();
},
});
state.modules.set(moduleName, moduleInstance);
moduleCfg(moduleName);
setBindLabel(item, moduleName);
item.addEventListener("click", () => {
toggleModule(moduleName);
});
item.addEventListener("contextmenu", (event) => {
event.preventDefault();
openConfig(moduleName);
});
list.appendChild(item);
}
panel.appendChild(header);
panel.appendChild(list);
panelByName.set(name, panel);
panelCollapseButtons.set(name, collapseButton);
state.modulePanels.set(panel, { modules: moduleNames });
return panel;
}
settingsMenuKeyBtn.addEventListener("click", () => {
state.listeningForMenuBind = true;
settingsMenuKeyBtn.textContent = "Press key...";
searchInput.blur();
});
settingsMenuKeyResetBtn.addEventListener("click", () => {
CONFIG.toggleKey = CONFIG.defaultToggleKey;
settingsMenuKeyBtn.textContent = `Menu Key: ${CONFIG.toggleKey}`;
setFooterText();
state.listeningForMenuBind = false;
});
presetButtons.forEach((btn) => {
btn.addEventListener("click", () => applyPreset(btn.dataset.preset || "default"));
});
settingsBtn.addEventListener("click", () => {
openSettings();
});
settingsTabs.forEach((tab) => {
tab.addEventListener("click", () => {
const target = tab.dataset.tab;
for (const t of settingsTabs) t.classList.toggle("active", t === tab);
for (const pane of settingsPanes) pane.classList.toggle("hidden", pane.dataset.pane !== target);
});
});
searchInput.addEventListener("keydown", (event) => {
event.stopPropagation();
if (event.key === CONFIG.toggleKey) {
event.preventDefault();
setVisible(false);
}
});
const applySearchFilterDebounced = debounce(applySearchFilter, 80);
searchInput.addEventListener("input", () => {
state.searchQuery = searchInput.value;
applySearchFilterDebounced();
});
accentInput.addEventListener("input", applyAppearance);
shellBgStartInput.addEventListener("input", applyAppearance);
shellBgEndInput.addEventListener("input", applyAppearance);
topbarColorInput.addEventListener("input", applyAppearance);
iconColorInput.addEventListener("input", applyAppearance);
outlineColorInput.addEventListener("input", applyAppearance);
panelCountTextInput.addEventListener("input", applyAppearance);
panelCountBorderInput.addEventListener("input", applyAppearance);
panelCountBgInput.addEventListener("input", applyAppearance);
borderInput.addEventListener("input", applyAppearance);
textInput.addEventListener("input", applyAppearance);
opacityInput.addEventListener("input", applyAppearance);
sliderColorInput.addEventListener("input", applyAppearance);
checkmarkColorInput.addEventListener("input", applyAppearance);
mutedTextInput.addEventListener("input", applyAppearance);
accentSoftInput.addEventListener("input", applyAppearance);
searchTextInput.addEventListener("input", applyAppearance);
fontInput.addEventListener("input", applyAppearance);
fontInput.addEventListener("change", applyAppearance);
headerStartInput.addEventListener("input", applyAppearance);
headerEndInput.addEventListener("input", applyAppearance);
headerTextInput.addEventListener("input", applyAppearance);
settingsHeaderStartInput.addEventListener("input", applyAppearance);
settingsHeaderEndInput.addEventListener("input", applyAppearance);
settingsSidebarInput.addEventListener("input", applyAppearance);
settingsBodyInput.addEventListener("input", applyAppearance);
settingsTextInput.addEventListener("input", applyAppearance);
settingsSubtextInput.addEventListener("input", applyAppearance);
settingsCardBorderInput.addEventListener("input", applyAppearance);
settingsCardBgInput.addEventListener("input", applyAppearance);
selectBgInput.addEventListener("input", applyAppearance);
selectTextInput.addEventListener("input", applyAppearance);
espValueTextColorInput.addEventListener("input", applyAppearance);
scaleInput.addEventListener("input", applyAppearance);
radiusInput.addEventListener("input", applyAppearance);
blurInput.addEventListener("input", applyAppearance);
hoverShiftInput.addEventListener("input", applyAppearance);
displayModeButtons.forEach((btn) => {
btn.addEventListener("click", () => setDisplayMode(btn.dataset.displayMode || "merged"));
});
searchAutofocusInput.addEventListener("change", () => {
state.searchAutofocus = searchAutofocusInput.checked;
});
hideBrokenModulesInput.addEventListener("change", () => {
state.hideBrokenModules = hideBrokenModulesInput.checked;
if (state.hideBrokenModules) {
for (const moduleName of [...state.enabledModules]) {
if (isModuleHiddenByWorkState(moduleName)) toggleModule(moduleName);
}
if (openConfigModule && isModuleHiddenByWorkState(openConfigModule)) closeConfig();
}
applySearchFilter();
});
settingsResetBtn.addEventListener("click", () => {
accentInput.value = "#ff3d3d";
shellBgStartInput.value = "#ff3d3d";
shellBgEndInput.value = "#000000";
topbarColorInput.value = "#ff4a4a";
iconColorInput.value = "#ffdada";
outlineColorInput.value = "#ff5b5b";
panelCountTextInput.value = "#ffd9d9";
panelCountBorderInput.value = "#ff6464";
panelCountBgInput.value = "#1a1a1e";
borderInput.value = "#ff6f6f";
textInput.value = "#d6d6df";
opacityInput.value = "45";
sliderColorInput.value = "#ff6b6b";
checkmarkColorInput.value = "#ff6b6b";
selectBgInput.value = "#17171f";
selectTextInput.value = "#ffe5e5";
mutedTextInput.value = "#9b9bab";
accentSoftInput.value = "#ffbdbd";
searchTextInput.value = "#ffe6e6";
fontInput.value = "Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif";
headerStartInput.value = "#ff4a4a";
headerEndInput.value = "#3c1212";
headerTextInput.value = "#ffffff";
settingsHeaderStartInput.value = "#ff3d3d";
settingsHeaderEndInput.value = "#2d0c0c";
settingsSidebarInput.value = "#181820";
settingsBodyInput.value = "#121216";
settingsTextInput.value = "#ffe5e5";
settingsSubtextInput.value = "#c2c2ce";
settingsCardBorderInput.value = "#ffffff";
settingsCardBgInput.value = "#ffffff";
espValueTextColorInput.value = "#ffffff";
searchAutofocusInput.checked = true;
state.searchAutofocus = true;
hideBrokenModulesInput.checked = true;
state.hideBrokenModules = true;
scaleInput.value = "100";
radiusInput.value = "14";
blurInput.value = "10";
hoverShiftInput.value = "2";
state.looseInitialized = false;
state.loosePositions = { topbar: { x: 12, y: 12 } };
state.loosePanelPositions = {};
state.collapsedPanels = {};
for (const panelName of panelByName.keys()) {
setPanelCollapsed(panelName, false);
}
syncCollapseButtons();
setDisplayMode("loose");
const themeTargets = [root.style, configBackdrop.style];
const removeThemeVar = (name) => {
for (const target of themeTargets) target.removeProperty(name);
};
removeThemeVar("--zyx-border");
removeThemeVar("--zyx-text");
removeThemeVar("--zyx-font");
removeThemeVar("--zyx-muted");
removeThemeVar("--zyx-accent-soft");
removeThemeVar("--zyx-search-text");
removeThemeVar("--zyx-topbar-bg-start");
removeThemeVar("--zyx-topbar-bg-end");
removeThemeVar("--zyx-module-hover-bg");
removeThemeVar("--zyx-module-hover-border");
removeThemeVar("--zyx-module-active-start");
removeThemeVar("--zyx-module-active-end");
removeThemeVar("--zyx-module-active-border");
removeThemeVar("--zyx-icon-color");
removeThemeVar("--zyx-outline-color");
removeThemeVar("--zyx-panel-count-text");
removeThemeVar("--zyx-panel-count-border");
removeThemeVar("--zyx-panel-count-bg");
removeThemeVar("--zyx-header-bg-start");
removeThemeVar("--zyx-header-bg-end");
removeThemeVar("--zyx-header-text");
removeThemeVar("--zyx-settings-header-start");
removeThemeVar("--zyx-settings-header-end");
removeThemeVar("--zyx-settings-sidebar-bg");
removeThemeVar("--zyx-settings-body-bg");
removeThemeVar("--zyx-settings-text");
removeThemeVar("--zyx-settings-subtext");
removeThemeVar("--zyx-settings-card-border");
removeThemeVar("--zyx-settings-card-bg");
removeThemeVar("--zyx-slider-color");
removeThemeVar("--zyx-checkmark-color");
removeThemeVar("--zyx-select-bg");
removeThemeVar("--zyx-select-text");
removeThemeVar("--zyx-radius-xl");
removeThemeVar("--zyx-radius-lg");
removeThemeVar("--zyx-radius-md");
removeThemeVar("--zyx-hover-shift");
removeThemeVar("--zyx-shell-blur");
removeThemeVar("--zyx-btn-bg");
removeThemeVar("--zyx-btn-hover-bg");
shell.style.background = "";
shell.style.transform = "";
shell.style.backdropFilter = "";
saveSettings();
});
settingsResetAllBtn.addEventListener("click", () => {
// Nuke localStorage
try { localStorage.removeItem(STORAGE_KEY); } catch (_) {}
// Reset module enabled state
for (const moduleName of [...state.enabledModules]) {
toggleModule(moduleName); // toggles off
}
state.enabledModules.clear();
for (const [, item] of state.moduleItems) item.classList.remove("active");
// Reset all module configs (keybinds + settings)
state.moduleConfig = new Map();
// Reset keybind labels
for (const [moduleName, item] of state.moduleItems) {
setBindLabel(item, moduleName);
}
// Reset menu keybind
CONFIG.toggleKey = CONFIG.defaultToggleKey;
settingsMenuKeyBtn.textContent = `Menu Key: ${CONFIG.toggleKey}`;
setFooterText();
// Reset search autofocus
state.searchAutofocus = true;
searchAutofocusInput.checked = true;
state.hideBrokenModules = true;
hideBrokenModulesInput.checked = true;
// Trigger the full appearance reset too
settingsResetBtn.click();
});
function saveSettings(showFeedback = false) {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(collectSettings()));
if (showFeedback) {
settingsSaveBtn.textContent = "Saved";
setTimeout(() => {
settingsSaveBtn.textContent = "Save";
}, 850);
}
} catch (_) {
if (showFeedback) {
settingsSaveBtn.textContent = "Save failed";
setTimeout(() => {
settingsSaveBtn.textContent = "Save";
}, 1200);
}
}
}
settingsSaveBtn.addEventListener("click", () => {
saveSettings(true);
});
settingsCloseBtn.addEventListener("click", () => {
closeConfig();
});
configCloseBtn.addEventListener("click", () => closeConfig());
settingsTopCloseBtn.addEventListener("click", () => closeConfig());
const generalPanels = document.createElement("div");
generalPanels.className = "zyrox-panels";
for (const generalGroup of MENU_LAYOUT.general.groups) {
generalPanels.appendChild(buildPanel(generalGroup.name, generalGroup.modules));
}
generalSection.appendChild(generalPanels);
const gamemodePanels = document.createElement("div");
gamemodePanels.className = "zyrox-panels";
for (const gm of MENU_LAYOUT.gamemodeSpecific.groups) {
gamemodePanels.appendChild(buildPanel(gm.name, gm.modules));
}
gamemodeSection.appendChild(gamemodePanels);
for (const [panelName] of panelByName.entries()) {
const btn = document.createElement("button");
btn.type = "button";
btn.className = "zyrox-collapse-btn";
btn.textContent = panelName;
btn.addEventListener("click", () => {
const nextCollapsed = !state.collapsedPanels[panelName];
setPanelCollapsed(panelName, nextCollapsed);
btn.classList.toggle("inactive", nextCollapsed);
});
collapseRow.appendChild(btn);
}
shell.appendChild(topbar);
shell.appendChild(generalSection);
shell.appendChild(gamemodeSection);
shell.appendChild(footer);
shell.appendChild(resizeHandle);
root.appendChild(shell);
document.head.appendChild(style);
document.body.appendChild(root);
document.body.appendChild(configBackdrop);
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (raw) {
const saved = JSON.parse(raw);
if (saved && typeof saved === "object") {
if (saved.toggleKey) CONFIG.toggleKey = saved.toggleKey;
if (typeof saved.searchAutofocus === "boolean") {
state.searchAutofocus = saved.searchAutofocus;
searchAutofocusInput.checked = saved.searchAutofocus;
}
if (typeof saved.hideBrokenModules === "boolean") {
state.hideBrokenModules = saved.hideBrokenModules;
hideBrokenModulesInput.checked = saved.hideBrokenModules;
}
const assign = (input, key) => {
if (saved[key] !== undefined && input) input.value = String(saved[key]);
};
assign(accentInput, "accent");
assign(shellBgStartInput, "shellBgStart");
assign(shellBgEndInput, "shellBgEnd");
assign(topbarColorInput, "topbarColor");
assign(iconColorInput, "iconColor");
assign(outlineColorInput, "outlineColor");
assign(panelCountTextInput, "panelCountText");
assign(panelCountBorderInput, "panelCountBorder");
assign(panelCountBgInput, "panelCountBg");
assign(borderInput, "border");
assign(textInput, "text");
assign(opacityInput, "opacity");
assign(sliderColorInput, "sliderColor");
assign(checkmarkColorInput, "checkmarkColor");
assign(selectBgInput, "selectBg");
assign(selectTextInput, "selectText");
assign(mutedTextInput, "mutedText");
assign(accentSoftInput, "accentSoft");
assign(searchTextInput, "searchText");
assign(fontInput, "font");
assign(headerStartInput, "headerStart");
assign(headerEndInput, "headerEnd");
assign(headerTextInput, "headerText");
assign(settingsHeaderStartInput, "settingsHeaderStart");
assign(settingsHeaderEndInput, "settingsHeaderEnd");
assign(settingsSidebarInput, "settingsSidebar");
assign(settingsBodyInput, "settingsBody");
assign(settingsTextInput, "settingsText");
assign(settingsSubtextInput, "settingsSubtext");
assign(settingsCardBorderInput, "settingsCardBorder");
assign(settingsCardBgInput, "settingsCardBg");
assign(espValueTextColorInput, "espValueTextColor");
assign(scaleInput, "scale");
assign(radiusInput, "radius");
assign(blurInput, "blur");
assign(hoverShiftInput, "hoverShift");
if (saved.displayMode) state.displayMode = saved.displayMode === "loose" ? "loose" : "merged";
if (typeof saved.looseInitialized === "boolean") state.looseInitialized = saved.looseInitialized;
if (saved.loosePositions && typeof saved.loosePositions === "object") {
state.loosePositions = {
topbar: saved.loosePositions.topbar || state.loosePositions.topbar,
};
}
if (saved.loosePanelPositions && typeof saved.loosePanelPositions === "object") {
state.loosePanelPositions = saved.loosePanelPositions;
}
if (saved.collapsedPanels && typeof saved.collapsedPanels === "object") {
state.collapsedPanels = saved.collapsedPanels;
}
const savedModuleConfig = Array.isArray(saved.moduleConfig)
? saved.moduleConfig
: (Array.isArray(saved.moduleSettings) ? saved.moduleSettings : null);
if (savedModuleConfig) {
state.moduleConfig = new Map(savedModuleConfig);
}
settingsMenuKeyBtn.textContent = `Menu Key: ${CONFIG.toggleKey}`;
setFooterText();
}
}
} catch (_) {}
for (const panelName of panelByName.keys()) {
setPanelCollapsed(panelName, !!state.collapsedPanels[panelName]);
}
syncCollapseButtons();
applyAppearance();
setDisplayMode(state.displayMode);
applySearchFilter();
const isTypingTarget = (target) => {
if (!(target instanceof Element)) return false;
return Boolean(target.closest("input, textarea, select, [contenteditable='true']"));
};
function setVisible(nextVisible) {
state.visible = nextVisible;
root.classList.toggle("zyrox-hidden", !nextVisible);
if (!nextVisible) closeConfig();
if (nextVisible && state.searchAutofocus) {
requestAnimationFrame(() => {
searchInput.focus();
if (searchInput.value === CONFIG.toggleKey) {
searchInput.value = "";
state.searchQuery = "";
applySearchFilter();
}
});
}
}
document.addEventListener("keydown", (event) => {
if (event.key === "Escape") {
if (!configBackdrop.classList.contains("hidden")) {
event.preventDefault();
closeConfig();
return;
}
}
if (state.listeningForMenuBind) {
event.preventDefault();
CONFIG.toggleKey = event.key;
settingsMenuKeyBtn.textContent = `Menu Key: ${CONFIG.toggleKey}`;
setFooterText();
state.listeningForMenuBind = false;
return;
}
if (state.listeningForBind && openConfigModule === state.listeningForBind) {
event.preventDefault();
const cfg = moduleCfg(openConfigModule);
cfg.keybind = event.key;
const item = state.moduleItems.get(openConfigModule);
if (item) setBindLabel(item, openConfigModule);
setCurrentBindText(cfg.keybind);
setBindButtonText("Set keybind");
state.listeningForBind = null;
return;
}
if (event.key === CONFIG.toggleKey) {
if (isTypingTarget(event.target)) return;
event.preventDefault();
setVisible(!state.visible);
return;
}
if (isTypingTarget(event.target)) return;
for (const [moduleName, cfg] of ensureModuleConfigStore()) {
if (cfg.keybind && cfg.keybind === event.key) {
toggleModule(moduleName);
}
}
});
// Intentionally no backdrop click-to-close; menus close only via explicit close buttons.
let dragState = null;
let resizeState = null;
const panelDragState = { panelName: null, offsetX: 0, offsetY: 0, shellLeft: 0, shellTop: 0, scale: 1 };
topbar.addEventListener("mousedown", (event) => {
const interactiveTarget = event.target instanceof Element
? event.target.closest("input, button")
: null;
if (interactiveTarget) return;
const rootBox = root.getBoundingClientRect();
if (state.displayMode === "loose") {
const box = topbar.getBoundingClientRect();
const shellRect = shell.getBoundingClientRect();
const scale = getShellScale();
dragState = {
mode: "topbar",
offsetX: event.clientX - box.left,
offsetY: event.clientY - box.top,
shellLeft: shellRect.left,
shellTop: shellRect.top,
scale,
};
} else {
dragState = {
mode: "root",
offsetX: event.clientX - rootBox.left,
offsetY: event.clientY - rootBox.top,
};
}
event.preventDefault();
});
panelByName.forEach((panel, panelName) => {
const header = panel.querySelector(".zyrox-panel-header");
header.addEventListener("mousedown", (event) => {
if (state.displayMode !== "loose") return;
const box = panel.getBoundingClientRect();
const shellRect = shell.getBoundingClientRect();
const scale = getShellScale();
panelDragState.panelName = panelName;
panelDragState.offsetX = event.clientX - box.left;
panelDragState.offsetY = event.clientY - box.top;
panelDragState.shellLeft = shellRect.left;
panelDragState.shellTop = shellRect.top;
panelDragState.scale = scale;
event.preventDefault();
event.stopPropagation();
});
});
document.addEventListener("mousemove", (event) => {
if (dragState?.mode === "root") {
const clamped = clampToViewport(event.clientX - dragState.offsetX, event.clientY - dragState.offsetY, root);
root.style.left = `${clamped.x}px`;
root.style.top = `${clamped.y}px`;
}
if (dragState?.mode === "topbar") {
const scale = dragState.scale || 1;
const unclampedX = (event.clientX - dragState.offsetX - dragState.shellLeft) / scale;
const unclampedY = (event.clientY - dragState.offsetY - dragState.shellTop) / scale;
const clamped = clampLoosePosition(unclampedX, unclampedY, topbar, scale, {
left: dragState.shellLeft,
top: dragState.shellTop,
});
state.loosePositions.topbar = clamped;
topbar.style.left = `${clamped.x}px`;
topbar.style.top = `${clamped.y}px`;
}
if (panelDragState.panelName) {
const panel = panelByName.get(panelDragState.panelName);
if (panel) {
const scale = panelDragState.scale || 1;
const unclampedX = (event.clientX - panelDragState.offsetX - panelDragState.shellLeft) / scale;
const unclampedY = (event.clientY - panelDragState.offsetY - panelDragState.shellTop) / scale;
const clamped = clampLoosePosition(unclampedX, unclampedY, panel, scale, {
left: panelDragState.shellLeft,
top: panelDragState.shellTop,
});
state.loosePanelPositions[panelDragState.panelName] = clamped;
panel.style.left = `${clamped.x}px`;
panel.style.top = `${clamped.y}px`;
}
}
});
document.addEventListener("mouseup", () => {
dragState = null;
resizeState = null;
panelDragState.panelName = null;
panelDragState.shellLeft = 0;
panelDragState.shellTop = 0;
panelDragState.scale = 1;
});
resizeHandle.addEventListener("mousedown", (event) => {
if (state.displayMode === "loose") return;
resizeState = {
startX: event.clientX,
startY: event.clientY,
startWidth: state.shellWidth,
startHeight: state.shellHeight,
};
event.preventDefault();
event.stopPropagation();
});
document.addEventListener("mousemove", (event) => {
if (!resizeState || state.displayMode === "loose") return;
const width = Math.max(760, resizeState.startWidth + (event.clientX - resizeState.startX));
const height = Math.max(420, resizeState.startHeight + (event.clientY - resizeState.startY));
state.shellWidth = width;
state.shellHeight = height;
shell.style.width = `${width}px`;
shell.style.height = `${height}px`;
});
// Theme category switching functionality
const themeCategories = [...settingsMenu.querySelectorAll(".zyrox-theme-category")];
const themeSections = [...settingsMenu.querySelectorAll(".zyrox-theme-section")];
themeCategories.forEach((category) => {
category.addEventListener("click", () => {
const targetCategory = category.dataset.category;
// Update active category
themeCategories.forEach((cat) => cat.classList.toggle("active", cat === category));
// Show corresponding section
themeSections.forEach((section) => {
section.classList.toggle("active", section.dataset.section === targetCategory);
});
});
});
} // end initUi
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initUi, { once: true });
} else {
initUi();
}
})();