Hitbox Room Commander – full control over game settings via chat commands.
// ==UserScript==
// @name Hitbox Room Commander
// @namespace http://tampermonkey.net/
// @version 3.9
// @description Hitbox Room Commander – full control over game settings via chat commands.
// @author Mr-FaZ3a
// @match https://heav.io/game.html
// @match https://hitbox.io/game.html
// @match https://heav.io/game2.html
// @match https://hitbox.io/game2.html
// @match https://hitbox.io/game-beta.html
// @grant none
// @run-at document-idle
// ==/UserScript==
(function () {
'use strict';
class HitboxCommander {
constructor() {
this.ws = null;
this.myid = -1;
this.hostId = -2;
this.gameSettings = null; // current live settings
this.roomDefaults = null; // original room defaults (never changes)
this.nativeSend = window.WebSocket.prototype.send;
this.users = {};
this.myName = '';
this.gameSettingsObject = null;
this.settingsMap = this.buildSettingsMap();
this.nameToAbbr = {};
this.abbrToName = new Map();
this.categoryNames = ['bat', 'rk', 'push', 'eg', 'dash', 'others', 'unknown'];
this.categoryOrder = ['Bat', 'RK', 'Push', 'EG', 'Dash', 'Others', 'Unknown'];
this.buildMaps();
this.chatHidden = localStorage.getItem('chatHidden') === 'true';
this.tempChatVisible = false;
this.chatMessages = [];
this.chatDialog = null;
this.userScrolled = false;
this.init();
this.initChatToggle();
this.createChatDialog();
}
buildSettingsMap() {
return [
// Bat
{ name: 'bbRange', abbr: 'st', category: 'Bat' },
{ name: 'bbPower', abbr: 'it', category: 'Bat' },
{ name: 'bbHoldAmmoCost', abbr: 'rt', category: 'Bat' },
{ name: 'bbFireFramesLength', abbr: 'at', category: 'Bat' },
{ name: 'bbHideAfterFireFrames', abbr: 'lt', category: 'Bat' },
{ name: 'bbResetOn', abbr: 'ut', category: 'Bat' },
{ name: 'bbEnabled', abbr: 'ct', category: 'Bat' },
{ name: 'bbHoldUntil', abbr: 'et', category: 'Bat' },
{ name: 'bbFireOn', abbr: 'nt', category: 'Bat' },
{ name: 'bbAngleVariance', abbr: 'ht', category: 'Bat' },
// RK
{ name: 'rkAmmoNeeded', abbr: 'kt', category: 'RK' },
{ name: 'rkVelocity', abbr: 'St', category: 'RK' },
{ name: 'rkAimRate', abbr: 'Nt', category: 'RK' },
{ name: 'rkBulletRound', abbr: 'Mt', category: 'RK' },
{ name: 'rkBulletRadius', abbr: 'Ct', category: 'RK' },
{ name: 'rkBulletAge', abbr: 'Tt', category: 'RK' },
{ name: 'rkBulletTurnRate', abbr: 'xt', category: 'RK' },
{ name: 'rkBulletGravityScale', abbr: '_t', category: 'RK' },
{ name: 'rkBulletBounces', abbr: 'Pt', category: 'RK' },
{ name: 'rkBulletRestitution', abbr: 'Et', category: 'RK' },
{ name: 'rkBulletBounceCountLimit', abbr: 'Bt', category: 'RK' },
{ name: 'rkSelfBatScale', abbr: 'It', category: 'RK' },
{ name: 'rkExplodeRadius', abbr: 'Ft', category: 'RK' },
{ name: 'rkExplodePush', abbr: 'At', category: 'RK' },
// Push
{ name: 'pushPlayers', abbr: 'Y', category: 'Push' },
{ name: 'pushRange', abbr: 'q', category: 'Push' },
{ name: 'pushDelay', abbr: 'V', category: 'Push' },
{ name: 'pushMinAmmoToStartPush', abbr: 'X', category: 'Push' },
{ name: 'pushAmmoSpendIfPushing', abbr: 'Z', category: 'Push' },
{ name: 'pushAmmoSpendAtFirstPushFrame', abbr: '$', category: 'Push' },
// EG
{ name: 'egEnabled', abbr: 'vt', category: 'EG' },
{ name: 'egSize', abbr: 'Rt', category: 'EG' },
{ name: 'egAge', abbr: 'Dt', category: 'EG' },
{ name: 'egGravityScale', abbr: 'Lt', category: 'EG' },
{ name: 'egRestitution', abbr: 'Ut', category: 'EG' },
{ name: 'egStartSpin', abbr: 'Wt', category: 'EG' },
{ name: 'egMaxThrowPower', abbr: 'Jt', category: 'EG' },
{ name: 'egAimRate', abbr: 'qt', category: 'EG' },
{ name: 'egAmmoNeeded', abbr: 'Gt', category: 'EG' },
{ name: 'egDelay1', abbr: 'Ht', category: 'EG' },
{ name: 'egDelay2', abbr: 'zt', category: 'EG' },
{ name: 'egDelayBeforeAmmoUse', abbr: 'Yt', category: 'EG' },
{ name: 'egShape', abbr: 'Vt', category: 'EG' },
// Dash
{ name: 'dashInitDelay', abbr: 'dt', category: 'Dash' },
{ name: 'dashActiveLength', abbr: 'wt', category: 'Dash' },
{ name: 'dashCooldownAfterLength', abbr: 'ft', category: 'Dash' },
{ name: 'dashBoostSpeed', abbr: 'gt', category: 'Dash' },
{ name: 'dashAmmoCost', abbr: 'yt', category: 'Dash' },
{ name: 'dashEnabled', abbr: 'bt', category: 'Dash' },
// Others
{ name: 'timeScale', abbr: 'J', category: 'Others' },
{ name: 'gravityOffset', abbr: 'G', category: 'Others' },
{ name: 'gameMode', abbr: 'H', category: 'Others' },
{ name: 'maxStamina', abbr: 'K', category: 'Others' },
{ name: 'parachuteEnabled', abbr: 'Ot', category: 'Others' },
{ name: 'playerSize', abbr: 'Kt', category: 'Others' },
{ name: 'objectiveLimit', abbr: 'Zt', category: 'Others' },
{ name: 'pushAmmoReplenish', abbr: 'tt', category: 'Others' },
// Unknown
{ name: 'ht', abbr: 'ht', category: 'Unknown' },
{ name: 'et', abbr: 'et', category: 'Unknown' },
{ name: 'nt', abbr: 'nt', category: 'Unknown' },
{ name: 'ot', abbr: 'ot', category: 'Unknown' },
{ name: 'jt', abbr: 'jt', category: 'Unknown' },
{ name: 'Qt', abbr: 'Qt', category: 'Unknown' },
{ name: '$t', abbr: '$t', category: 'Unknown' },
{ name: 'ti', abbr: 'ti', category: 'Unknown' },
{ name: 'ii', abbr: 'ii', category: 'Unknown' },
{ name: 'si', abbr: 'si', category: 'Unknown' },
{ name: 'hi', abbr: 'hi', category: 'Unknown' },
{ name: 'ei', abbr: 'ei', category: 'Unknown' },
{ name: 'ni', abbr: 'ni', category: 'Unknown' },
{ name: 'oi', abbr: 'oi', category: 'Unknown' },
{ name: 'ri', abbr: 'ri', category: 'Unknown' },
{ name: 'Xt', abbr: 'Xt', category: 'Unknown' },
{ name: 'gsFightersCollide', abbr: 'gsFightersCollide', category: 'Unknown' },
{ name: 'recordMode', abbr: 'recordMode', category: 'Unknown' },
];
}
buildMaps() {
this.settingsMap.forEach(item => {
this.nameToAbbr[item.name.toLowerCase()] = item.abbr;
this.abbrToName.set(item.abbr, item.name);
});
}
// Get a setting value: current if exists, else default (roomDefaults)
getSettingValue(abbr) {
if (this.gameSettings && this.gameSettings[abbr] !== undefined) {
return this.gameSettings[abbr];
}
if (this.roomDefaults && this.roomDefaults[abbr] !== undefined) {
return this.roomDefaults[abbr];
}
return undefined;
}
init() {
this.hookWebSocket();
const findSettings = () => {
if (window.multiplayerSession) {
for (let key in window.multiplayerSession) {
let obj = window.multiplayerSession[key];
if (obj && obj.$L && typeof obj.$L === 'object') {
this.gameSettings = obj.$L;
this.gameSettingsObject = obj.$L;
if (!this.roomDefaults) {
this.roomDefaults = JSON.parse(JSON.stringify(this.gameSettings));
//console.log('[Commander] Room defaults captured:', this.roomDefaults);
}
break;
}
}
}
if (!this.myName) {
try {
const localName = localStorage.getItem('playerName');
if (localName) this.myName = localName;
} catch (e) { }
}
};
findSettings();
const interval = setInterval(() => {
if (this.gameSettings) {
clearInterval(interval);
} else {
findSettings();
}
}, 500);
}
// Re‑apply current team assignments to all players
restoreTeams() {
for (let id in this.users) {
let user = this.users[id];
if (user && user.team !== undefined && user.team !== -1) {
this.ws.send(`42[1,[47,{"i":${user.id},"t":${user.team}}]]`);
}
}
}
hookWebSocket() {
const self = this;
window.WebSocket.prototype.send = function (args) {
if (this.url && this.url.includes('/socket.io/')) {
if (!self.ws || self.ws !== this) {
self.ws = this;
this.addEventListener('message', (event) => self.processIncoming(event));
this.addEventListener('close', () => {
if (self.ws === this) {
self.myid = -1;
self.hostId = -2;
self.ws = null;
self.clearMessages();
}
});
}
try {
if (typeof args === 'string' && args.startsWith('42[1,[')) {
let packet = JSON.parse(args.slice(5, -1));
if (packet[0] === 28) {
let msg = packet[1].trim();
if (msg.startsWith('!')) {
if (self.processCommand(null, msg, true)) {
return;
}
} else {
let name = self.myName || 'You';
let user = self.users[self.myid];
if (user) name = user.name;
let nameColor = user ? self.getFontColor(user) : '#FFFFFF';
let borderColor = user ? self.getBorderColor(user) : '#FFFFFF';
let nameSpan = `<span style="color:${nameColor}; text-shadow:0.5px 0 0 ${borderColor}, -0.5px 0 0 ${borderColor}, 0 0.5px 0 ${borderColor}, 0 -0.5px 0 ${borderColor};">${self.escapeHtml(name)}</span>`;
let html = `${nameSpan}: <span>${self.escapeHtml(msg)}</span>`;
self.addMessage(html, true);
}
}
}
} catch (e) { }
}
return self.nativeSend.call(this, args);
};
}
processIncoming(event) {
if (typeof event.data !== 'string' || !event.data.startsWith('42[')) return;
try {
let packet = JSON.parse(event.data.slice(2));
let type = packet[0];
//console.log(`[Commander] Packet type ${type}`, packet);
if (type === 7) {
//console.log('[Commander] 🔵 FULL PLAYER LIST RECEIVED');
this.clearMessages();
this.myid = packet[1][0];
this.hostId = packet[1][1];
this.users = {};
for (let p of packet[1][3]) {
let userId = p[4];
let customColor = p[7] ? (p[7][1] || p[7][0]) : 0xFFFFFF;
let team = p[2];
this.users[userId] = { id: userId, name: p[0], customColor: customColor, team: team };
if (userId === this.myid) this.myName = p[0];
//console.log(`[Commander] Player ${p[0]} (ID ${userId}) team ${team} color 0x${customColor.toString(16)}`);
}
// Do NOT reset roomDefaults here
} else if (type === 8) {
let userId = packet[1][4];
let userName = packet[1][0];
let customColor = packet[7] ? (packet[7][1] || packet[7][0]) : 0xFFFFFF;
let team = packet[1][2];
this.users[userId] = { id: userId, name: userName, customColor: customColor, team: team };
//console.log(`[Commander] Player joined: ${userName} (team ${team})`);
let html = `<span style="color:#AAAAAA">${this.escapeHtml(userName)} joined the game</span>`;
this.addMessage(html, false);
} else if (type === 9) {
let userId = packet[1];
let userName = this.users[userId]?.name || 'Unknown';
this.hostId = packet[2];
delete this.users[userId];
//console.log(`[Commander] Player left: ${userName}`);
let html = `<span style="color:#AAAAAA">${this.escapeHtml(userName)} left the game</span>`;
this.addMessage(html, false);
} else if (type === 25) {
let userId = packet[1];
let newTeam = packet[2];
if (this.users[userId]) {
let oldTeam = this.users[userId].team;
this.users[userId].team = newTeam;
//console.log(`[Commander] 🔄 Team change: ${this.users[userId].name} from ${oldTeam} to ${newTeam}`);
}
} else if (type === 32) {
let data = packet[1];
let kickedUserId = data.id;
let userName = this.users[kickedUserId]?.name || 'Unknown';
let action = (data.ban === 1) ? 'banned' : 'kicked';
//console.log(`[Commander] Player ${userName} ${action}`);
let html = `<span style="color:#AAAAAA">${this.escapeHtml(userName)} was ${action}</span>`;
this.addMessage(html, false);
} else if (type === 42) {
let mapId = packet[1];
//console.log(`[Commander] Map changed to ${mapId}`);
let html = `<span style="color:#AAAAAA">Game starting on new map (ID: ${mapId})</span>`;
this.addMessage(html, false);
} else if (type === 43) {
//console.log(`[Commander] Game start`);
let html = `<span style="color:#AAAAAA">Game has started!</span>`;
this.addMessage(html, false);
} else if (type === 45) {
this.hostId = packet[1];
//console.log(`[Commander] New host ID: ${this.hostId}`);
} else if (type === 41) {
let userId = packet[1];
let newColor = packet[2]["1"];
if (this.users[userId]) {
let oldColor = this.users[userId].customColor;
this.users[userId].customColor = newColor;
//console.log(`[Commander] 🎨 Color change: ${this.users[userId].name} from 0x${oldColor.toString(16)} to 0x${newColor.toString(16)}`);
}
} else if (type === 63) {
this.gameSettings = packet[1];
this.gameSettingsObject = packet[1];
if (!this.roomDefaults) {
this.roomDefaults = JSON.parse(JSON.stringify(this.gameSettings));
//console.log('[Commander] Room defaults captured from type 63:', this.roomDefaults);
}
if (this.gameSettings && this.gameSettings.H !== undefined) {
//console.log(`[Commander] ⚙️ Game mode changed to: ${this.gameSettings.H}`);
}
} else if (type === 74) {
let teamsEnabled = packet[1];
let assignments = packet[2] || [];
//console.log(`[Commander] 🏁 Team mode toggle: enabled=${teamsEnabled}, assignments=`, assignments);
for (let assignment of assignments) {
let playerId = assignment[0];
let team = assignment[1];
if (this.users[playerId]) {
let oldTeam = this.users[playerId].team;
this.users[playerId].team = team;
//console.log(`[Commander] Player ${this.users[playerId].name} team ${oldTeam} -> ${team}`);
}
}
}
if (type === 29) {
let userId = packet[1];
if (userId === this.myid) return;
let msg = packet[2].trim();
let user = this.users[userId];
if (!user) return;
let name = user.name;
let nameColor = this.getFontColor(user);
let borderColor = this.getBorderColor(user);
let nameSpan = `<span style="color:${nameColor}; text-shadow:0.5px 0 0 ${borderColor}, -0.5px 0 0 ${borderColor}, 0 0.5px 0 ${borderColor}, 0 -0.5px 0 ${borderColor};">${this.escapeHtml(name)}</span>`;
let html = `${nameSpan}: <span>${this.escapeHtml(msg)}</span>`;
this.addMessage(html, false);
}
} catch (e) {
console.error('[Commander] Error processing packet', e);
}
}
getFontColor(user) {
if (user.team === 0) return '#888888';
if (user.team === 1) return this.rgbNumberToHex(user.customColor);
let baseColor = (user.team === 2) ? '#FF0000' : '#0000FF';
let luminance = this.getLuminance(user.customColor);
let factor = Math.max(0.2, luminance);
let baseR = parseInt(baseColor.slice(1, 3), 16);
let baseG = parseInt(baseColor.slice(3, 5), 16);
let baseB = parseInt(baseColor.slice(5, 7), 16);
let newR = Math.round(baseR * factor);
let newG = Math.round(baseG * factor);
let newB = Math.round(baseB * factor);
return '#' + ((1 << 24) + (newR << 16) + (newG << 8) + newB).toString(16).slice(1);
}
getBorderColor(user) {
if (user.team === 2) return '#FF0000';
if (user.team === 3) return '#0000FF';
if (user.team === 0) return '#000000';
return this.brightenColor(user.customColor);
}
getLuminance(customColorNum) {
let hex = this.rgbNumberToHex(customColorNum);
let r = parseInt(hex.slice(1, 3), 16);
let g = parseInt(hex.slice(3, 5), 16);
let b = parseInt(hex.slice(5, 7), 16);
return (0.299 * r + 0.587 * g + 0.114 * b) / 255;
}
brightenColor(customColorNum) {
let hex = this.rgbNumberToHex(customColorNum);
if (hex === '#000000') return '#FFFFFF';
let r = parseInt(hex.slice(1, 3), 16);
let g = parseInt(hex.slice(3, 5), 16);
let b = parseInt(hex.slice(5, 7), 16);
let max = Math.max(r, g, b);
if (max < 128) {
let factor = 255 / max;
r = Math.min(255, Math.round(r * factor));
g = Math.min(255, Math.round(g * factor));
b = Math.min(255, Math.round(b * factor));
}
return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
}
rgbNumberToHex(rgb) {
return '#' + ('000000' + ((rgb & 0xFFFFFF) >>> 0).toString(16)).slice(-6);
}
addMessage(html, isOwn = false) {
this.chatMessages.push({ html, isOwn });
if (this.chatDialog && this.chatDialog.style.display !== 'none') {
this.updateChatDialog();
}
}
clearMessages() {
this.chatMessages = [];
if (this.chatDialog && this.chatDialog.style.display !== 'none') {
this.updateChatDialog();
}
}
escapeHtml(unsafe) {
return unsafe.replace(/[&<>"']/g, function (m) {
if (m === '&') return '&';
if (m === '<') return '<';
if (m === '>') return '>';
if (m === '"') return '"';
if (m === "'") return ''';
return m;
});
}
isValidValue(str) {
if (str.toLowerCase() === 'true' || str.toLowerCase() === 'false') return true;
return /^-?\d+(\.\d+)?$/.test(str);
}
findSettingByInput(input) {
if (input.startsWith('@')) {
let abbr = input.substring(1);
if (this.abbrToName.has(abbr)) {
return { type: 'setting', abbr: abbr, name: this.abbrToName.get(abbr) };
}
if (this.gameSettings && this.gameSettings.hasOwnProperty(abbr)) {
return { type: 'setting', abbr: abbr, name: abbr };
}
return null;
}
let lowerInput = input.toLowerCase();
if (this.nameToAbbr.hasOwnProperty(lowerInput)) {
let abbr = this.nameToAbbr[lowerInput];
return { type: 'setting', abbr: abbr, name: this.abbrToName.get(abbr) || abbr };
}
if (this.abbrToName.has(input)) {
return { type: 'setting', abbr: input, name: this.abbrToName.get(input) };
}
const matchedCat = this.categoryNames.find(cat => cat === lowerInput);
if (matchedCat) {
let category = matchedCat.charAt(0).toUpperCase() + matchedCat.slice(1);
return { type: 'category', category: category };
}
if (this.gameSettings && this.gameSettings.hasOwnProperty(input)) {
return { type: 'setting', abbr: input, name: input };
}
return null;
}
processCommand(user, msg, isOutgoing) {
if (!msg.startsWith('!')) return false;
let parts = msg.slice(1).split(' ');
let cmd = parts[0].toLowerCase();
let args = parts.slice(1).join(' ').trim();
if (cmd === 'help') {
const helpText = [
'<span style="color:#00FF00">!get <setting|@abbr|category></span> – Show current value of a setting, or list all settings in a category.',
'<span style="color:#00FF00">!set <setting|@abbr> <value></span> – Change a setting (host only).',
'<span style="color:#00FF00">!reset <setting|@abbr></span> – Reset a specific setting to its initial value (host only).',
'<span style="color:#00FF00">!getall</span> – List all known settings, showing current value (or default if not changed).',
'<span style="color:#00FF00">!sync</span> – Adjust settings based on current timeScale.',
'<span style="color:#00FF00">!dumpjo</span> – Log full settings object to console (F12).',
'<span style="color:#AAAAAA">!togglechat</span> – Hide/show the in‑game message area. Hold Shift to see last messages.',
'<span style="color:#00FF00">!help</span> – Show this message.'
];
helpText.forEach(line => this.display(line, '#FFFFFF', true));
return true;
}
if (cmd === 'set' || cmd === 'sync' || cmd === 'reset') {
if (this.myid === -1 || this.hostId === -2) {
this.display('Host status unknown. Try again in a moment.', '#FFA500');
return true;
}
if (this.myid !== this.hostId) {
this.display('Only the host can use this command.', '#FF0000');
return true;
}
}
if (cmd === 'get') {
if (!args) {
this.display('Usage: !get <setting|@abbr|category> (e.g., !get rkAimRate, !get @Nt, !get bat)', '#FFA500');
return true;
}
let result = this.findSettingByInput(args);
if (!result) return true;
if (result.type === 'category') {
let category = result.category;
let items = this.settingsMap.filter(item => item.category.toLowerCase() === category.toLowerCase());
if (items.length === 0) {
this.display(`No settings found in category ${category}.`, '#FF0000');
return true;
}
let lines = [];
lines.push(`<span style="color:#FFA500; font-weight:bold;">--- ${category} ---</span>`);
items.sort((a, b) => a.name.localeCompare(b.name));
for (let item of items) {
let val = this.getSettingValue(item.abbr);
if (val === undefined) continue;
if (typeof val === 'boolean') val = val ? 'true' : 'false';
lines.push(`<span style="color:#00FF00">${item.name} (${item.abbr})</span>=<span style="color:#FFFF00">${val}</span>`);
}
this.sendChunked(lines);
return true;
} else {
let value = this.getSettingValue(result.abbr);
if (value === undefined) return true;
if (typeof value === 'boolean') value = value ? 'true' : 'false';
let html = `<span style="color:#00FF00">${result.name} (${result.abbr})</span> = <span style="color:#FFFF00">${value}</span>`;
this.display(html, '#00FF00', true);
return true;
}
}
if (cmd === 'dumpjo') {
if (this.gameSettings) {
//console.log('=== Full game settings ===', this.gameSettings);
this.display('Full settings object logged to console (F12).', '#AAAAAA');
} else {
this.display('gameSettings not available.', '#FF0000');
}
return true;
}
if (cmd === 'getall') {
if (!this.gameSettings && !this.roomDefaults) {
this.display('Settings not available yet. Try again in a moment.', '#FF0000');
return true;
}
let lines = [];
for (let cat of this.categoryOrder) {
const items = this.settingsMap.filter(item => item.category.toLowerCase() === cat.toLowerCase());
if (items.length === 0) continue;
lines.push(`<span style="color:#FFA500; font-weight:bold;">--- ${cat} ---</span>`);
items.sort((a, b) => a.name.localeCompare(b.name));
for (let item of items) {
let val = this.getSettingValue(item.abbr);
let displayVal = (val !== undefined) ? (typeof val === 'boolean' ? (val ? 'true' : 'false') : val) : 'N/A';
lines.push(`<span style="color:#00FF00">${item.name} (${item.abbr})</span>=<span style="color:#FFFF00">${displayVal}</span>`);
}
}
if (lines.length === 0) {
this.display('No settings found.', '#FF0000');
} else {
this.sendChunked(lines);
}
return true;
}
if (cmd === 'set') {
let setParts = args.split(' ');
if (setParts.length < 2) {
this.display('Usage: !set <setting|@abbr> <value>', '#FFA500');
return true;
}
let settingInput = setParts[0];
let settingValue = setParts.slice(1).join(' ').trim();
if (!this.isValidValue(settingValue)) return false;
let result = this.findSettingByInput(settingInput);
if (!result || result.type !== 'setting') return false;
let numValue = parseFloat(settingValue);
let finalValue = isNaN(numValue) ? settingValue : numValue;
if (settingValue.toLowerCase() === 'true') finalValue = true;
if (settingValue.toLowerCase() === 'false') finalValue = false;
let update = {};
update[result.abbr] = finalValue;
if (this.ws) {
this.ws.send('42' + JSON.stringify([1, [62, update]]));
setTimeout(() => this.restoreTeams(), 50);
} else {
this.display('WebSocket not available.', '#FF0000');
return true;
}
if (this.gameSettings) this.gameSettings[result.abbr] = finalValue;
if (this.gameSettingsObject) this.gameSettingsObject[result.abbr] = finalValue;
this.display(`Setting ${result.name} changed to ${finalValue}.`, '#00FF00');
if (result.name.toLowerCase() === 'timescale') {
setTimeout(() => {
this.display('TimeScale changed. Use !sync to adjust related settings.', '#AAAAAA');
}, 500);
}
return true;
}
if (cmd === 'reset') {
if (!args) {
this.display('Usage: !reset <setting|@abbr>', '#FFA500');
return true;
}
let result = this.findSettingByInput(args);
if (!result || result.type !== 'setting') return false;
if (!this.roomDefaults) {
this.display('Room defaults not available. Use !getall to populate.', '#FF0000');
return true;
}
let initialValue = this.roomDefaults[result.abbr];
if (initialValue === undefined) {
this.display(`No default value found for ${result.name}.`, '#FF0000');
return true;
}
let update = {};
update[result.abbr] = initialValue;
if (this.ws) {
this.ws.send('42' + JSON.stringify([1, [62, update]]));
setTimeout(() => this.restoreTeams(), 50);
} else {
this.display('WebSocket not available.', '#FF0000');
return true;
}
if (this.gameSettings) this.gameSettings[result.abbr] = initialValue;
if (this.gameSettingsObject) this.gameSettingsObject[result.abbr] = initialValue;
this.display(`Setting ${result.name} reset to ${initialValue}.`, '#00FF00');
return true;
}
if (cmd === 'sync') {
if (!this.gameSettings) {
this.display('gameSettings not fully loaded. Try again in a moment.', '#FF0000');
return true;
}
let baseline = this.roomDefaults;
if (!baseline) {
this.display('Room defaults not available. Use !getall to populate.', '#FF0000');
return true;
}
let currentTimeScale = this.gameSettings.J;
if (currentTimeScale === undefined) {
this.display('TimeScale not found.', '#FF0000');
return true;
}
let factorUp = 30 / currentTimeScale;
let factorDown = currentTimeScale / 30;
let baseRkAimRate = baseline.Nt;
let basePushReplenish = baseline.tt;
let baseBbFireFramesLength = baseline.at;
let baseBbHideAfterFireFrames = baseline.lt;
let baseBbHoldAmmoCost = baseline.rt;
let baseBbResetOn = baseline.ut;
let baseBbFireOn = baseline.nt;
const missing = [];
if (baseRkAimRate === undefined) missing.push('rkAimRate (Nt)');
if (basePushReplenish === undefined) missing.push('pushAmmoReplenish (tt)');
if (baseBbFireFramesLength === undefined) missing.push('bbFireFramesLength (at)');
if (baseBbHideAfterFireFrames === undefined) missing.push('bbHideAfterFireFrames (lt)');
if (baseBbHoldAmmoCost === undefined) missing.push('bbHoldAmmoCost (rt)');
if (baseBbResetOn === undefined) missing.push('bbResetOn (ut)');
if (baseBbFireOn === undefined) missing.push('bbFireOn (nt)');
if (missing.length > 0) {
this.display(`Missing baseline settings: ${missing.join(', ')}. They may not exist in this game mode.`, '#FF0000');
return true;
}
let newRkAimRate = baseRkAimRate * factorUp;
let newPushReplenish = basePushReplenish * factorUp;
let newBbFireFramesLength = baseBbFireFramesLength * factorDown;
let newBbHideAfterFireFrames = baseBbHideAfterFireFrames * factorDown;
let newBbHoldAmmoCost = baseBbHoldAmmoCost * factorUp;
let newBbResetOn = Math.round(baseBbResetOn * factorDown);
let newBbFireOn = Math.round(baseBbFireOn * factorDown);
let updates = {
Nt: newRkAimRate,
tt: newPushReplenish,
at: newBbFireFramesLength,
lt: newBbHideAfterFireFrames,
rt: newBbHoldAmmoCost,
ut: newBbResetOn,
nt: newBbFireOn
};
//console.log('[Commander] Sending sync updates:', updates);
if (this.ws) {
this.ws.send('42' + JSON.stringify([1, [62, updates]]));
setTimeout(() => this.restoreTeams(), 50);
} else {
this.display('WebSocket not available.', '#FF0000');
return true;
}
if (this.gameSettings) {
this.gameSettings.Nt = newRkAimRate;
this.gameSettings.tt = newPushReplenish;
this.gameSettings.at = newBbFireFramesLength;
this.gameSettings.lt = newBbHideAfterFireFrames;
this.gameSettings.rt = newBbHoldAmmoCost;
this.gameSettings.ut = newBbResetOn;
this.gameSettings.nt = newBbFireOn;
}
if (this.gameSettingsObject) {
this.gameSettingsObject.Nt = newRkAimRate;
this.gameSettingsObject.tt = newPushReplenish;
this.gameSettingsObject.at = newBbFireFramesLength;
this.gameSettingsObject.lt = newBbHideAfterFireFrames;
this.gameSettingsObject.rt = newBbHoldAmmoCost;
this.gameSettingsObject.ut = newBbResetOn;
this.gameSettingsObject.nt = newBbFireOn;
}
this.display(`Synced: rkAimRate = ${newRkAimRate.toFixed(2)}, pushAmmoReplenish = ${newPushReplenish.toFixed(2)}, bbFireFramesLength = ${newBbFireFramesLength.toFixed(2)}, bbHideAfterFireFrames = ${newBbHideAfterFireFrames.toFixed(2)}, bbHoldAmmoCost = ${newBbHoldAmmoCost.toFixed(2)}, bbResetOn = ${newBbResetOn}, bbFireOn = ${newBbFireOn} (timeScale = ${currentTimeScale})`, '#00FF00');
return true;
}
if (cmd === 'togglechat') {
this.chatHidden = !this.chatHidden;
localStorage.setItem('chatHidden', this.chatHidden);
this.applyChatHidden();
this.display(`In-game message area ${this.chatHidden ? 'hidden' : 'visible'}. Input always visible.`, '#AAAAAA');
return true;
}
return false;
}
sendChunked(lines) {
const CHUNK_SIZE = 8;
for (let i = 0; i < lines.length; i += CHUNK_SIZE) {
let chunk = lines.slice(i, i + CHUNK_SIZE).join('<br>');
this.display(chunk, '#00FF00', true);
}
}
display(text, color, isHtml = false) {
let chatBoxes = document.querySelectorAll('.chatBox .content, .inGameChat .content');
if (!chatBoxes.length) return;
let div = document.createElement('div');
div.classList.add('statusContainer');
let span = document.createElement('span');
span.classList.add('status');
span.style.backgroundColor = 'rgba(37, 38, 42, 0.768627451)';
span.style.borderRadius = '7px';
span.style.padding = '2px 5px';
span.style.display = 'inline-block';
span.style.margin = '2px 0';
span.style.whiteSpace = 'pre-wrap';
span.style.wordBreak = 'break-word';
if (isHtml) {
span.innerHTML = `<span style="color:purple">[Bot]</span> ${text}`;
} else {
span.innerHTML = `<span style="color:purple">[Bot]</span> <span style="color:${color}">${this.escapeHtml(text)}</span>`;
}
div.appendChild(span);
let botHtml = span.innerHTML;
this.addMessage(botHtml, false);
for (let box of chatBoxes) {
let clone = div.cloneNode(true);
box.appendChild(clone);
box.scrollTop = box.scrollHeight;
if (box.closest('.inGameChat')) {
setTimeout(() => {
if (clone.parentNode) clone.remove();
}, 5000);
}
}
}
initChatToggle() {
//console.log('[Commander] initChatToggle - chatHidden =', this.chatHidden);
this.applyChatHidden();
setInterval(() => {
if (this.chatHidden && !this.tempChatVisible) {
this.applyChatHidden();
}
}, 1000);
window.addEventListener('keydown', (e) => {
if (e.key === 'Shift') {
//console.log('[Commander] Shift down');
this.showChatDialog();
}
});
window.addEventListener('keyup', (e) => {
if (e.key === 'Shift') {
//console.log('[Commander] Shift up');
this.hideChatDialog();
}
});
}
applyChatHidden() {
const chatContent = document.querySelector('.inGameChat .content');
if (!chatContent) {
//console.log('[Commander] Chat content not found');
return;
}
if (this.chatHidden && !this.tempChatVisible) {
chatContent.style.setProperty('display', 'none', 'important');
//console.log('[Commander] Messages hidden');
} else {
chatContent.style.display = '';
//console.log('[Commander] Messages visible');
}
}
createChatDialog() {
this.chatDialog = document.createElement('div');
this.chatDialog.id = 'commander-chat-dialog';
this.chatDialog.style.position = 'fixed';
this.chatDialog.style.bottom = '60px';
this.chatDialog.style.left = '10px';
this.chatDialog.style.width = '300px';
this.chatDialog.style.maxHeight = '200px';
this.chatDialog.style.overflowY = 'auto';
this.chatDialog.style.backgroundColor = 'rgba(0, 0, 0, 0.3)';
this.chatDialog.style.color = 'white';
this.chatDialog.style.fontFamily = 'Arial, sans-serif';
this.chatDialog.style.fontSize = '14px';
this.chatDialog.style.padding = '8px';
this.chatDialog.style.borderRadius = '8px';
this.chatDialog.style.backdropFilter = 'blur(2px)';
this.chatDialog.style.zIndex = '10000';
this.chatDialog.style.display = 'none';
this.chatDialog.style.pointerEvents = 'auto';
this.chatDialog.addEventListener('scroll', () => {
if (this.chatDialog) {
const atBottom = this.chatDialog.scrollHeight - this.chatDialog.scrollTop <= this.chatDialog.clientHeight + 5;
this.userScrolled = !atBottom;
}
});
document.body.appendChild(this.chatDialog);
}
updateChatDialog() {
if (!this.chatDialog) return;
let html = '';
for (let m of this.chatMessages) {
let extraStyle = m.isOwn ? 'border-left: 2px solid gold; background-color: rgba(255,215,0,0.1); padding-left: 4px;' : '';
html += `<div style="margin-bottom: 2px; ${extraStyle}">${m.html}</div>`;
}
this.chatDialog.innerHTML = html || '<i>No messages yet</i>';
if (!this.userScrolled) {
setTimeout(() => {
if (this.chatDialog) {
this.chatDialog.scrollTop = this.chatDialog.scrollHeight;
}
}, 0);
}
}
showChatDialog() {
if (!this.chatDialog) return;
this.userScrolled = false;
this.updateChatDialog();
this.chatDialog.style.display = 'block';
}
hideChatDialog() {
if (!this.chatDialog) return;
this.chatDialog.style.display = 'none';
}
}
// ---- Modified keydown handler to allow non-mod keys even when input is focused ----
const originalAddEventListener = window.addEventListener;
window.addEventListener = function (type, listener, options) {
if (type === 'keydown') {
const wrappedListener = function (e) {
const isModKey = (e.which == 65 && e.shiftKey) || (e.which == 83 && e.shiftKey) || (e.which == 70 && e.shiftKey) ||
(() => {
if (typeof macro_scripts !== 'undefined' && macro_scripts) {
let comb = ((e.shiftKey ? 'shift+' : '') + (e.altKey ? 'alt+' : '') + e.key.toLowerCase()).split('+');
for (let i in macro_scripts) {
let keycomb = i.toLowerCase().split('+');
let kc = true;
for (let y of keycomb) if (!comb.includes(y)) kc = false;
for (let y of comb) if (!keycomb.includes(y)) kc = false;
if (kc) return true;
}
}
return false;
})();
if (document.activeElement && document.activeElement.tagName == "INPUT" && !isModKey && e.which != 13) {
e.stopImmediatePropagation();
return;
}
listener(e);
};
return originalAddEventListener.call(this, type, wrappedListener, options);
}
return originalAddEventListener.call(this, type, listener, options);
};
if (document.readyState === 'complete') {
new HitboxCommander();
} else {
window.addEventListener('load', () => new HitboxCommander());
}
})();