// ==UserScript==
// @name GatsModV4 Gats.io (Patched)
// @namespace http://tampermonkey.net/
// @version 4.1
// @description 0 key toggle. Chat features now reference Player.pool.chatMessage for stability.
// @author zeroarcop (patched by user request)
// @license MIT
// @match https://gats.io/
// @grant GM_addStyle
// @run-at document-idle
// ==/UserScript==
(function() {
'use strict';
const SETTINGS_KEY = 'zeroarcop_gats_mod_settings_v2';
const LOG_PREFIX_MOD = "[zeroarcop-gats-mod]";
const MULTIBOX_CHANNEL_NAME = 'gats_multibox_channel_v1';
function modLog(message, isError = false) {
const finalMessage = `${LOG_PREFIX_MOD} ${message}`;
if (isError) {
console.error(finalMessage);
} else {
console.log(finalMessage);
}
}
function getDistance(p1, p2) {
if (!p1 || !p2) return Infinity;
return Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2);
}
class ColorCustomizerGUI {
constructor() {
this.container = document.createElement('div');
this.container.id = 'zeroarcop-gats-color-gui';
this.container.style.display = 'none';
document.body.appendChild(this.container);
this.applyStyles();
const head = document.createElement('h4');
head.innerText = 'ESP Color Settings';
this.container.appendChild(head);
this.makeDraggable(head, this.container);
this.addColorPicker('Enemy Box/Line', 'enemyEspColor');
this.addColorPicker('Low HP Enemy Box/Line', 'lowHpEnemyEspColor');
this.addColorPicker('Teammate Box/Line', 'teammateEspColor');
this.addColorPicker('Cloaked Enemy Text', 'cloakedTextColor');
this.addColorPicker('Enemy Name', 'enemyNameColor');
this.addColorPicker('Teammate Name', 'teammateNameColor');
this.addColorPicker('HP Bar (High)', 'hpBarHighColor');
this.addColorPicker('HP Bar (Medium)', 'hpBarMediumColor');
this.addColorPicker('HP Bar (Low)', 'hpBarLowColor');
this.addColorPicker('Facing Line', 'facingLineColor');
this.addColorPicker('Aimbot Target Line', 'aimbotTargetLineColor');
this.addColorPicker('Prediction Line', 'predictionLineColor');
this.addColorPicker('Obstacle Hitbox', 'obstacleEspColor');
this.addColorPicker('AI Whisker (Clear)', 'aiWhiskerClearColor');
this.addColorPicker('AI Whisker (Blocked)', 'aiWhiskerBlockedColor');
this.addColorPicker('AI Move Direction', 'aiMoveDirColor');
this.addColorPicker('AI Bullet Warning', 'aiBulletWarningColor');
this.addColorPicker('AI Threat (Clear)', 'aiThreatLineClearColor');
this.addColorPicker('AI Threat (Blocked)', 'aiThreatLineBlockedColor');
const closeBtn = this.addButton('Close Colors (0)', () => { this.container.style.display = 'none'; }, this.container, 'custom-btn-color-gui');
closeBtn.style.marginTop = '15px';
}
applyStyles() {
GM_addStyle(`
#${this.container.id} {
position: fixed; left: calc(20px + 950px + 10px); top: 70px; background-color: var(--main-bg, rgba(18,18,18,0.97));
color: var(--text-color-light, #fff); padding: 12px; border-radius: 6px; font-family: "Segoe UI", Arial, sans-serif;
font-size: 12px; z-index: 100001; border: 2px solid var(--accent-border, #b00000);
box-shadow: 0 3px 10px rgba(0,0,0,.5); width: 280px; max-height: calc(100vh - 100px);
overflow-y: auto; user-select: none;
}
#${this.container.id} h4 {
text-align: center; color: var(--accent-color, #f00); font-weight: 700; font-size: 14px;
margin-top: 0; margin-bottom: 12px; padding-bottom: 8px; border-bottom: 1px solid var(--accent-border, #b00000);
cursor: move;
}
#${this.container.id} .color-picker-row {
display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; padding: 3px;
}
#${this.container.id} .color-picker-row label {
color: var(--text-color-dim, #aaa); font-size: 11.5px; margin-right: 8px;
}
#${this.container.id} input[type=color] {
border: 1px solid var(--accent-border, #b00000); border-radius: 4px; width: 70px;
height: 28px; cursor: pointer; background-color: var(--secondary-bg, #1e1e1e); padding: 2px;
}
#${this.container.id} button.custom-btn-color-gui {
background-color: var(--btn-profile-bg, #2d2d2d); color: var(--btn-profile-text, #e0e0e0);
border: 1px solid var(--btn-profile-border, #f00); padding: 6px 10px; display: block;
width: 100%; border-radius: 3px; cursor: pointer; font-weight: 500; font-size: 12px;
}
#${this.container.id} button.custom-btn-color-gui:hover {
filter: brightness(var(--hover-brightness, 120%));
}
#${this.container.id}::-webkit-scrollbar { width: 8px; }
#${this.container.id}::-webkit-scrollbar-track { background: var(--scrollbar-track, #2d2d2d); border-radius: 4px; }
#${this.container.id}::-webkit-scrollbar-thumb { background: var(--scrollbar-thumb, #aa0000); border-radius: 4px; }
#${this.container.id}::-webkit-scrollbar-thumb:hover { background: var(--scrollbar-thumb-hover, #ff3333); }
`);
}
makeDraggable(dragHandle, draggableElement) {
let offsetX, offsetY, isDragging = false;
const onMouseMove = (ev) => {
if (!isDragging) return;
draggableElement.style.left = (ev.clientX - offsetX) + 'px';
draggableElement.style.top = (ev.clientY - offsetY) + 'px';
};
const onMouseUp = () => {
isDragging = false;
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
};
dragHandle.addEventListener('mousedown', (e) => {
if (e.target.closest('button, input, select, a')) return;
isDragging = true;
offsetX = e.clientX - draggableElement.offsetLeft;
offsetY = e.clientY - draggableElement.offsetTop;
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
e.preventDefault();
});
}
addButton(label, onClick, parent, className = 'custom-btn') {
const btn = document.createElement('button');
btn.innerText = label;
btn.className = className;
btn.onclick = onClick;
parent.appendChild(btn);
return btn;
}
addColorPicker(label, settingKey) {
const row = document.createElement('div');
row.className = 'color-picker-row';
const lbl = document.createElement('label');
lbl.htmlFor = `${settingKey}-color-v2`;
lbl.innerText = label + ":";
row.appendChild(lbl);
const picker = document.createElement('input');
picker.type = 'color';
picker.id = `${settingKey}-color-v2`;
picker.value = (GatsModCore.SETTINGS?.espColors?.[settingKey]) || '#FFFFFF';
picker.oninput = () => {
if (GatsModCore.SETTINGS?.espColors) {
GatsModCore.SETTINGS.espColors[settingKey] = picker.value;
}
};
picker.onchange = () => {
if (GatsModCore.SETTINGS?.espColors) {
GatsModCore.SETTINGS.espColors[settingKey] = picker.value;
GatsModCore.saveSettings?.();
}
};
row.appendChild(picker);
this.container.appendChild(row);
}
}
class SimpleGUI {
constructor() {
this.presetEditButtons = [];
this.container = document.createElement('div');
this.container.id = 'zeroarcop-gats-gui';
this.container.style.display = 'none';
document.body.appendChild(this.container);
const mainContentWrapper = document.createElement('div');
mainContentWrapper.id = 'gui-main-content-wrapper';
this.container.appendChild(mainContentWrapper);
const guiHead = document.createElement('h3');
guiHead.innerText = 'Gats.io Mod by zeroarcop';
mainContentWrapper.appendChild(guiHead);
this.makeDraggable(guiHead, this.container);
this.statusDisplay = document.createElement('div');
this.statusDisplay.id = 'gui-status-bar';
mainContentWrapper.appendChild(this.statusDisplay);
const hotkeyInfo = document.createElement('p');
hotkeyInfo.id = 'gui-hotkey-info';
hotkeyInfo.innerHTML = `F: ESP | G: Aimbot | ^: Spinbot | 0: GUI(s) | 1-9: Preset Chat | Alt: Ignore Mode`;
mainContentWrapper.appendChild(hotkeyInfo);
const topBarWrapper = document.createElement('div');
topBarWrapper.id = 'gui-top-bar-wrapper';
mainContentWrapper.appendChild(topBarWrapper);
this.addSearchBox(topBarWrapper);
this.addProfileManager(topBarWrapper);
const mainTogglesWrapper = document.createElement('div');
mainTogglesWrapper.id = 'gui-main-toggles-wrapper';
mainContentWrapper.appendChild(mainTogglesWrapper);
this.addCheckbox('ESP Enabled', 'espEnabled', mainTogglesWrapper, 'Toggle all visual assistance features.');
this.addCheckbox('Aimbot Enabled', 'aimbotEnabled', mainTogglesWrapper, 'Enable player-targeting aimbot.');
this.addCheckbox('Spinbot Enabled', 'spinbotEnabled', mainTogglesWrapper, 'Enable rapid aim spinning (overridden by click/Aimbot).');
this.addCheckbox('Ghost Detect', 'ghostDetectEnabled', mainTogglesWrapper, 'Forces cloaked (Ghillie) enemies to be visible.');
this.addCheckbox('Silencer Detect', 'silencerDetectEnabled', mainTogglesWrapper, 'Forces bullets from silenced weapons to be visible.');
this.addCheckbox('Smart Auto-Attack', 'autoAttackEnabled', mainTogglesWrapper, 'Automatically shoots when target is in range and line of sight is clear.');
const columnsWrapper = document.createElement('div');
columnsWrapper.id = 'gui-columns-wrapper';
mainContentWrapper.appendChild(columnsWrapper);
this.column1 = document.createElement('div');
this.column1.className = 'gui-column';
columnsWrapper.appendChild(this.column1);
this.column2 = document.createElement('div');
this.column2.className = 'gui-column';
columnsWrapper.appendChild(this.column2);
this.column3 = document.createElement('div');
this.column3.className = 'gui-column';
columnsWrapper.appendChild(this.column3);
const footer = document.createElement('div');
footer.id = 'gui-footer';
mainContentWrapper.appendChild(footer);
this.addDiscordButton(footer);
}
makeDraggable(dragHandle, draggableElement) {
let offsetX, offsetY, isDragging = false;
const onMouseMove = (ev) => {
if (!isDragging) return;
draggableElement.style.left = (ev.clientX - offsetX) + 'px';
draggableElement.style.top = (ev.clientY - offsetY) + 'px';
};
const onMouseUp = () => {
isDragging = false;
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
};
dragHandle.addEventListener('mousedown', (e) => {
if (e.target.closest('button, input, select, a')) return;
isDragging = true;
offsetX = e.clientX - draggableElement.offsetLeft;
offsetY = e.clientY - draggableElement.offsetTop;
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
e.preventDefault();
});
}
addCheckbox(label, settingKey, parent, tooltipText = null, onChangeCallback = null) {
const div = document.createElement('div');
if (parent.id === 'gui-main-toggles-wrapper') {
div.style.cssText = 'display: flex; flex-direction: column; align-items: center; padding: 2px 5px; margin: 0 5px; min-width: 90px; text-align: center;';
} else {
div.style.cssText = 'display: flex; align-items: center; justify-content: space-between; padding: 3px 0;';
}
div.dataset.settingName = label.toLowerCase().replace(/\s+/g, '-');
const lbl = document.createElement('label');
lbl.htmlFor = `${settingKey}-v2`;
lbl.innerText = label;
if (parent.id !== 'gui-main-toggles-wrapper') {
lbl.style.flexGrow = '1';
} else {
lbl.style.marginBottom = '2px';
}
const cb = document.createElement('input');
cb.type = 'checkbox';
cb.id = `${settingKey}-v2`;
cb.onchange = () => {
if (GatsModCore.SETTINGS) {
GatsModCore.SETTINGS[settingKey] = cb.checked;
if (settingKey === 'followBotEnabled' && !cb.checked) {
GatsModCore.stopFollowingPlayer?.();
}
if (settingKey === 'chatScrollEnabled' && !cb.checked) {
GatsModCore.stopChatScroll?.();
}
GatsModCore.saveSettings?.();
gatsModInstance?.simpleGui?.updateStatusDisplay?.();
if (onChangeCallback) {
onChangeCallback(cb.checked);
}
}
};
div.appendChild(lbl);
if (tooltipText) {
this.addTooltip(lbl, tooltipText);
}
div.appendChild(cb);
parent.appendChild(div);
return div;
}
addCollapsibleSection(title, parent, className = '') {
const details = document.createElement('details');
if (className) {
details.className = className;
}
details.dataset.settingName = title.toLowerCase().replace(/\s+/g, '-');
details.open = false;
const summary = document.createElement('summary');
summary.innerText = title;
details.appendChild(summary);
const content = document.createElement('div');
details.appendChild(content);
parent.appendChild(details);
return content;
}
addSliderInput(label, settingKey, opts, objToUpdate, parent, tooltipText = null) {
const wrapper = document.createElement('div');
wrapper.className = 'settings-group-item';
wrapper.dataset.settingName = label.toLowerCase().replace(/\s+/g, '-');
const itemContainer = document.createElement('div');
itemContainer.style.cssText = 'display: flex; flex-direction: column; margin-bottom: 5px;';
const labelContainer = document.createElement('div');
labelContainer.style.display = 'flex';
labelContainer.style.alignItems = 'center';
const labelElement = document.createElement('label');
labelElement.htmlFor = `${settingKey}-slider-v2`;
labelElement.innerText = label;
labelElement.style.marginBottom = '3px';
labelContainer.appendChild(labelElement);
if (tooltipText) {
this.addTooltip(labelContainer, tooltipText);
}
itemContainer.appendChild(labelContainer);
const controlsContainer = document.createElement('div');
controlsContainer.style.cssText = 'display: flex; align-items: center; width: 100%;';
const slider = document.createElement('input');
slider.type = 'range';
slider.id = `${settingKey}-slider-v2`;
slider.min = opts.min;
slider.max = opts.max;
slider.step = opts.step;
slider.value = objToUpdate[settingKey] ?? opts.defaultVal ?? opts.min;
controlsContainer.appendChild(slider);
const valueDisplay = document.createElement('input');
valueDisplay.type = 'number';
valueDisplay.className = 'value-display';
valueDisplay.style.width = '55px';
valueDisplay.min = opts.min;
valueDisplay.max = opts.max;
valueDisplay.step = opts.step;
valueDisplay.value = slider.value;
controlsContainer.appendChild(valueDisplay);
const updateValue = (newValue, fromSlider = false) => {
let numVal = parseFloat(newValue);
if (isNaN(numVal)) {
numVal = opts.defaultVal ?? parseFloat(opts.min);
}
numVal = Math.max(parseFloat(opts.min), Math.min(parseFloat(opts.max), numVal));
const decimals = opts.step.toString().includes('.') ? opts.step.toString().split('.')[1].length : 0;
const fixedVal = numVal.toFixed(decimals);
if (fromSlider) {
valueDisplay.value = fixedVal;
} else {
slider.value = fixedVal;
}
objToUpdate[settingKey] = parseFloat(fixedVal);
GatsModCore.saveSettings?.();
};
slider.oninput = (e) => updateValue(e.target.value, true);
valueDisplay.onchange = (e) => updateValue(e.target.value, false);
valueDisplay.onfocus = () => {
if (GatsModCore) GatsModCore.isInputActive = true;
};
valueDisplay.onblur = () => {
if (GatsModCore) GatsModCore.isInputActive = false;
updateValue(valueDisplay.value, false);
};
itemContainer.appendChild(controlsContainer);
wrapper.appendChild(itemContainer);
parent.appendChild(wrapper);
return wrapper;
}
addTextInput(label, settingKey, objToUpdate, parent, onSaveCallback = null, useSaveButton = false, tooltipText = null) {
const wrapper = document.createElement('div');
wrapper.className = 'settings-group-item';
wrapper.dataset.settingName = label.toLowerCase().replace(/\s+/g, '-');
const labelContainer = document.createElement('div');
labelContainer.style.display = 'flex';
labelContainer.style.alignItems = 'center';
const lbl = document.createElement('label');
lbl.htmlFor = `${settingKey}-text-v2`;
lbl.innerText = label;
lbl.style.display = 'block';
lbl.style.marginBottom = '3px';
labelContainer.appendChild(lbl);
if (tooltipText) {
this.addTooltip(labelContainer, tooltipText);
}
wrapper.appendChild(labelContainer);
const input = document.createElement('input');
input.type = 'text';
input.id = `${settingKey}-text-v2`;
input.value = objToUpdate[settingKey] || "";
input.className = 'general-text-input';
input.style.width = 'calc(100% - 0px)';
input.onfocus = () => {
if (GatsModCore) GatsModCore.isInputActive = true;
};
input.onblur = () => {
if (GatsModCore) GatsModCore.isInputActive = false;
if (!useSaveButton) {
objToUpdate[settingKey] = input.value;
GatsModCore.saveSettings?.();
onSaveCallback?.(input.value);
}
};
wrapper.appendChild(input);
if (useSaveButton) {
this.addButton("Save", () => {
objToUpdate[settingKey] = input.value;
GatsModCore.saveSettings?.();
onSaveCallback?.(input.value);
if (this.currentScrollingTextDisplay && settingKey === 'chatScrollText') {
this.updateScrollingTextDisplay(input.value);
}
const originalPlaceholder = input.placeholder;
input.placeholder = "Saved!";
setTimeout(() => {
input.placeholder = originalPlaceholder;
}, 1200);
}, wrapper, 'action-btn-small');
}
parent.appendChild(wrapper);
return wrapper;
}
addButton(label, onClickAction, parent, className = 'action-btn') {
const button = document.createElement('button');
button.innerText = label;
button.className = className;
button.onclick = onClickAction;
parent.appendChild(button);
return button;
}
addTooltip(parentLabelContainer, text) {
const tooltipTrigger = document.createElement('span');
tooltipTrigger.className = 'tooltip-trigger';
tooltipTrigger.innerText = '?';
const tooltipTextElement = document.createElement('span');
tooltipTextElement.className = 'tooltip-text';
tooltipTextElement.innerText = text;
tooltipTrigger.appendChild(tooltipTextElement);
parentLabelContainer.appendChild(tooltipTrigger);
}
addDiscordButton(parent) {
const discordBtn = document.createElement('a');
discordBtn.href = 'https://discord.com/users/975535045047648266';
discordBtn.target = '_blank';
discordBtn.rel = 'noopener noreferrer';
discordBtn.id = 'discord-link-btn';
discordBtn.innerText = 'Contact zeroarcop on Discord';
parent.appendChild(discordBtn);
}
applyStyles() {
GM_addStyle(`
:root {
--main-bg: rgba(18,18,18,0.97); --secondary-bg: #1e1e1e; --border-color: #f00; --text-color: #fff;
--text-color-light: #fff; --text-color-dim: #aaa; --accent-color: #f00; --accent-border: #b00000;
--hover-brightness: 130%; --input-accent: #f00; --status-on: #0f0; --status-off: #f00;
--status-neutral: #aaa; --tooltip-bg: #101010; --tooltip-text: #fff; --tooltip-border: var(--accent-color);
--btn-action-bg: #d00000; --btn-action-border: #a00000; --btn-profile-bg: #2d2d2d;
--btn-profile-text: #e0e0e0; --btn-profile-border: var(--accent-color); --btn-alt-bg: #f0f0f0;
--btn-alt-text: #1a1a1a; --btn-alt-border: #aaa; --modal-bg: rgba(0,0,0,0.8);
--modal-content-bg: #1a1a1a; --modal-content-border: var(--accent-color); --scrollbar-track: #2d2d2d;
--scrollbar-thumb: #aa0000; --scrollbar-thumb-hover: #ff3333; --skill-list-bg: rgba(0,0,0,0.2);
--skill-item-bg: var(--secondary-bg); --skill-item-border: var(--accent-border);
--skill-item-hover-bg: var(--accent-color);
}
#${this.container.id} {
position: fixed; left: 20px; top: 70px; background-color: var(--main-bg); color: var(--text-color);
padding: 10px; border-radius: 6px; font-family: "Segoe UI", Arial, sans-serif; font-size: 12.5px;
z-index: 100002; border: 2px solid var(--accent-color); box-shadow: 0 5px 20px rgba(255,0,0,.4);
width: 950px; max-height: calc(100vh - 90px); overflow-y: auto; user-select: none;
}
#${this.container.id} #gui-main-content-wrapper { display: flex; flex-direction: column; }
#${this.container.id} h3 {
margin: 0 0 8px; text-align: center; border-bottom: 1px solid var(--accent-border); padding-bottom: 10px;
color: var(--accent-color); font-weight: 700; cursor: move; font-size: 16px;
text-shadow: 0 0 5px var(--accent-color);
}
#${this.container.id} #gui-status-bar {
background-color: rgba(10,10,10,0.85); color: #fff; padding: 6px 12px; margin-bottom: 10px;
text-align: center; font-size: 12.5px; font-weight: 700; border-radius: 4px;
border: 1px solid var(--accent-border);
}
#${this.container.id} #gui-status-bar .status-on { color: var(--status-on); font-weight: 700; }
#${this.container.id} #gui-status-bar .status-off { color: var(--status-off); font-weight: 700; }
#${this.container.id} #gui-status-bar .status-neutral { color: var(--status-neutral); }
#${this.container.id} #gui-hotkey-info {
font-size: 11px; text-align: center; margin: 2px 0 10px; color: var(--text-color-dim);
}
#${this.container.id} #gui-top-bar-wrapper {
display: flex; gap: 10px; margin-bottom: 10px; border-bottom: 1px solid var(--accent-border); padding-bottom: 10px;
}
#${this.container.id} #settings-search-box {
flex-grow: 1; background-color: var(--secondary-bg); color: var(--text-color-light);
border: 1px solid var(--accent-border); border-radius: 3px; padding: 5px 8px;
}
#${this.container.id} #profile-manager { display: flex; gap: 5px; align-items: center; }
#${this.container.id} #profile-manager button,
#${this.container.id} #profile-manager input,
#${this.container.id} #profile-manager select {
font-size: 11px; padding: 4px; background-color: var(--btn-profile-bg);
color: var(--btn-profile-text); border: 1px solid var(--btn-profile-border); border-radius: 3px;
}
#${this.container.id} #profile-manager button {
cursor: pointer; background-color: var(--accent-color); border-color: var(--accent-border);
color: var(--text-color-light);
}
#${this.container.id} #profile-manager button:hover { filter: brightness(var(--hover-brightness)); }
#${this.container.id} #gui-main-toggles-wrapper {
display: flex; flex-wrap: wrap; justify-content: space-around; margin-bottom: 10px;
padding-bottom: 10px; border-bottom: 1px solid var(--accent-border);
}
#${this.container.id} #gui-main-toggles-wrapper > div {
display: flex; flex-direction: column; align-items: center; padding: 2px 5px; margin: 0 5px;
min-width: 90px; text-align: center;
}
#${this.container.id} #gui-main-toggles-wrapper label {
color: var(--text-color-light); font-size: 11.5px; margin-bottom: 2px;
}
#${this.container.id} #gui-main-toggles-wrapper input[type=checkbox] {
margin-top: 1px; accent-color: var(--input-accent);
}
#${this.container.id} #gui-columns-wrapper {
display: flex; flex-direction: row; justify-content: space-between; gap: 10px;
}
#${this.container.id} .gui-column {
width: calc(33.33% - 7px); display: flex; flex-direction: column; gap: 5px;
}
#${this.container.id} details {
border: 1px solid var(--border-color); border-radius: 4px; padding: 0; margin: 0 0 8px;
background-color: rgba(30,30,30,0.75);
}
#${this.container.id} summary {
cursor: pointer; outline: 0; font-weight: 600; color: var(--accent-color); padding: 6px 8px;
font-size: 13px; border-radius: 3px 3px 0 0; transition: background-color .2s;
border-bottom: 1px solid transparent;
}
#${this.container.id} details[open] > summary {
border-bottom: 1px solid var(--accent-border); background-color: rgba(255,0,0,0.05);
}
#${this.container.id} details > div { padding: 10px 8px 8px; }
#${this.container.id} .settings-group-item { margin-bottom: 8px; }
#${this.container.id} .settings-group-item label {
color: var(--text-color-light); margin-left: 0; flex-shrink: 0; display: block;
min-width: 100px; font-size: 12px; margin-bottom: 3px;
}
#${this.container.id} .settings-group-item div[style*="display: flex; align-items: center; justify-content: space-between;"] label {
display: inline-block; flex-grow: 1; margin-bottom: 0;
}
#${this.container.id} .settings-group-item input[type=checkbox] {
accent-color: var(--input-accent); border: 1px solid var(--accent-border);
vertical-align: middle; margin-left: 5px;
}
#${this.container.id} input[type=number].value-display {
width: 55px; background-color: var(--secondary-bg); color: var(--text-color);
border: 1px solid var(--accent-border); border-radius: 3px; padding: 4px 5px;
text-align: right; font-family: "Segoe UI", Arial, sans-serif; margin: 0 4px; font-size: 11.5px;
}
#${this.container.id} input[type=range] {
flex-grow: 1; margin: 0 4px; accent-color: var(--input-accent); height: 22px;
}
#${this.container.id} .settings-group-item div[style*="display: flex; align-items: center; width: 100%;"] { height: 26px; }
#${this.container.id} input[type=text].general-text-input,
#${this.container.id} input[type=text][id^=aimbotExcludeInput-text-v2],
#${this.container.id} input[type=text][id^=obstacleEspTypes-text-v2] {
width: calc(100% - 0px); box-sizing: border-box; background-color: var(--secondary-bg);
color: var(--text-color-light); border: 1px solid var(--accent-border); border-radius: 3px;
padding: 5px; margin-bottom: 5px;
}
#${this.container.id} button.action-btn,
#${this.container.id} button.custom-btn {
background-color: var(--btn-action-bg); color: #fff; border: 1px solid var(--btn-action-border);
margin-top: 10px; padding: 7px 10px; display: block; width: 100%; box-sizing: border-box;
border-radius: 3px; cursor: pointer; font-weight: 500; font-size: 12.5px; text-transform: uppercase;
}
#${this.container.id} button.action-btn-small {
background-color: var(--btn-action-bg); color: #fff; border: 1px solid var(--btn-action-border);
padding: 4px 8px; font-size: 11px; margin-top: 5px; width: auto; border-radius: 3px; cursor: pointer;
}
#${this.container.id} button.action-btn-third {
width: calc(33.33% - 4px); display: inline-block; margin: 2px;
background-color: var(--btn-alt-bg); color: var(--btn-alt-text);
border: 1px solid var(--btn-alt-border); padding: 5px 8px; font-size: 11.5px;
text-transform: none; border-radius: 3px; cursor: pointer;
}
#${this.container.id} #bot-control-panel button.action-btn {
background-color: var(--btn-action-bg); color: #fff; border: 1px solid var(--btn-action-border);
}
#${this.container.id} button.action-btn-half {
width: calc(50% - 5px); margin: 2px; padding: 5px; font-size: 11.5px;
background-color: var(--btn-action-bg); color: #fff;
border: 1px solid var(--btn-action-border); border-radius: 3px; cursor: pointer;
}
#${this.container.id} button.edit-preset-btn-item,
#${this.container.id} button.preset-btn-item {
min-width: auto; width: auto; margin: 0; padding: 4px 7px; display: inline-block;
background-color: var(--btn-profile-bg); color: var(--btn-profile-text); font-size: 11px;
line-height: 1.4; border: 1px solid var(--btn-profile-border); border-radius: 3px; cursor: pointer;
}
#${this.container.id} button.edit-preset-btn-item {
padding: 3px 6px; font-size: 10px; background-color: var(--accent-color); color: #fff;
}
#${this.container.id} button:hover { filter: brightness(var(--hover-brightness)); }
#${this.container.id} button.action-btn-third:hover {
background-color: #e0e0e0; filter: brightness(95%);
}
#${this.container.id} .tooltip-trigger {
display: inline-block; margin-left: 6px; color: var(--text-color-dim);
background-color: var(--secondary-bg); border: 1px solid var(--accent-border);
border-radius: 50%; width: 14px; height: 14px; font-size: 10px;
text-align: center; line-height: 14px; cursor: help; position: relative;
}
#${this.container.id} .tooltip-text {
visibility: hidden; width: 220px; background-color: var(--tooltip-bg); color: var(--tooltip-text);
text-align: center; border-radius: 6px; padding: 8px; position: absolute; z-index: 100003;
bottom: 125%; left: 50%; margin-left: -110px; opacity: 0; transition: opacity .3s;
border: 1px solid var(--tooltip-border); font-size: 11px; line-height: 1.4;
}
#${this.container.id} .tooltip-trigger:hover .tooltip-text { visibility: visible; opacity: 1; }
#player-list-modal {
display: none; position: fixed; z-index: 100003; left: 0; top: 0; width: 100%; height: 100%;
background-color: var(--modal-bg); justify-content: center; align-items: center;
}
#player-list-content {
background-color: var(--modal-content-bg); padding: 20px;
border: 1px solid var(--modal-content-border); border-radius: 8px; width: 80%;
max-width: 500px; max-height: 80vh; overflow-y: auto;
box-shadow: 0 0 15px rgba(255,0,0,0.5);
}
#player-list-content h4 {
margin-top: 0; color: var(--accent-color); text-align: center; font-size: 1.2em;
}
#player-list-grid {
display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 8px;
}
.player-list-button {
background-color: var(--secondary-bg); color: var(--text-color-light);
border: 1px solid var(--accent-border); padding: 8px; text-align: center;
border-radius: 4px; cursor: pointer; overflow: hidden;
text-overflow: ellipsis; white-space: nowrap;
}
.player-list-button:hover { background-color: var(--accent-color); color: #fff; }
#${this.container.id} #gui-footer {
margin-top: 15px; padding-top: 10px; border-top: 1px solid var(--accent-border); text-align: center;
}
#${this.container.id} #discord-link-btn {
display: inline-block; padding: 8px 15px; background-color: #5865F2; color: #fff;
text-decoration: none; border-radius: 4px; font-weight: bold; font-size: 13px;
transition: background-color .2s;
}
#${this.container.id} #discord-link-btn:hover { background-color: #4752C4; }
#${this.container.id}::-webkit-scrollbar { width: 10px; }
#${this.container.id}::-webkit-scrollbar-track {
background: var(--scrollbar-track); border-radius: 4px;
}
#${this.container.id}::-webkit-scrollbar-thumb {
background: var(--scrollbar-thumb); border-radius: 4px;
}
#${this.container.id}::-webkit-scrollbar-thumb:hover { background: var(--scrollbar-thumb-hover); }
#${this.container.id} .settings-group-item > div[style*="flex-direction: column"] { margin-bottom: 0; }
`);
}
populateColumn1_ESP() {
if (!this.column1 || !GatsModCore.SETTINGS) return;
const espMasterSection = this.addCollapsibleSection('ESP Configuration', this.column1, 'settings-group-master');
const visualEspOptions = this.addCollapsibleSection('Player ESP Visuals', espMasterSection, 'settings-sub-group');
this.addCheckbox('Show Enemy HP', 'espShowHP', visualEspOptions, 'Display health bars above players.');
this.addCheckbox('Highlight Low HP Enemies', 'espHighlightLowHP', visualEspOptions, 'Change ESP color for enemies with low health.');
this.addSliderInput('Low HP Threshold (%)', 'lowHPThreshold', {min: 1, max: 99, step: 1, defaultVal: 30}, GatsModCore.SETTINGS, visualEspOptions, 'Health % below which an enemy is considered low HP.');
this.addCheckbox('Show Prediction Line', 'espShowPrediction', visualEspOptions, 'Draws a line from enemies to their predicted position. Requires aimbot prediction to be enabled.');
this.addCheckbox('Show Enemy Facing Line', 'espShowFacingLine', visualEspOptions, 'Draw a line indicating player aim direction.');
this.addCheckbox('Highlight Cloaked Enemies', 'espHighlightCloaked', visualEspOptions, 'Special indicator for cloaked enemies. Works best with Ghost Detect enabled.');
this.addCheckbox('Show Teammates (TDM)', 'espShowTeammates', visualEspOptions, 'Enable ESP for your teammates.');
const espPositioningSection = this.addCollapsibleSection('ESP Positioning & Visual Scale', espMasterSection, 'settings-sub-group');
this.addSliderInput('X Offset (Global)', 'espOffsetX', {min: -200, max: 200, step: 1, defaultVal: 0}, GatsModCore.SETTINGS, espPositioningSection, 'Shift all ESP elements horizontally.');
this.addSliderInput('Y Offset (Global)', 'espOffsetY', {min: -200, max: 200, step: 1, defaultVal: 0}, GatsModCore.SETTINGS, espPositioningSection, 'Shift all ESP elements vertically.');
this.addSliderInput('ESP Visual Scale', 'espScale', {min: 0.1, max: 5.0, step: 0.05, defaultVal: 0.89}, GatsModCore.SETTINGS, espPositioningSection, 'Global visual scale for ESP elements (boxes, lines, text).');
const debugPreviewSection = this.addCollapsibleSection('Debug Previews', espMasterSection, 'settings-sub-group');
this.addCheckbox('Show Attack Radius', 'autoAttackShowRadius', debugPreviewSection, 'Displays the Smart Auto-Attack engagement radius on screen.');
this.addCheckbox('Show Obstacle Hitboxes', 'obstacleEspEnabled', debugPreviewSection, 'Displays obstacle hitboxes for debugging line-of-sight.');
this.addCheckbox('Show LOS Debug Line', 'losDebugLineEnabled', debugPreviewSection, 'Draws the line used for the line-of-sight check. Green = clear, Red = blocked.');
this.addCheckbox('Show Bullet Dodge Warnings', 'espShowBulletWarnings', debugPreviewSection, 'Displays the warning radius around enemy bullets used by the AI for dodging.');
this.addCheckbox('Show AI Pathfinding Whiskers', 'aiShowPathfindingWhiskers', debugPreviewSection, 'Displays the AI\'s obstacle avoidance check lines (whiskers).');
this.addCheckbox('Show AI Final Move Direction', 'aiShowFinalMoveDirection', debugPreviewSection, 'Displays a line indicating the AI\'s final movement decision.');
this.addCheckbox('Show Enemy Threat Line', 'aiShowEnemyThreatLine', debugPreviewSection, 'Draws a line from the nearest enemy\'s gun to you if the line of sight is clear.');
this.addSliderInput('Obstacle X Offset', 'obstacleOffsetX', {min: -100, max: 100, step: 1, defaultVal: 0}, GatsModCore.SETTINGS, debugPreviewSection, 'Fine-tune the horizontal position of obstacle hitboxes.');
this.addSliderInput('Obstacle Y Offset', 'obstacleOffsetY', {min: -100, max: 100, step: 1, defaultVal: 0}, GatsModCore.SETTINGS, debugPreviewSection, 'Fine-tune the vertical position of obstacle hitboxes.');
}
populateColumn2_Aimbot() {
if (!this.column2 || !GatsModCore.SETTINGS) return;
const aimbotMasterSection = this.addCollapsibleSection('Aimbot & Auto-Attack', this.column2, 'settings-group-master');
const autoAttackSection = this.addCollapsibleSection('Smart Auto-Attack', aimbotMasterSection, 'settings-sub-group');
this.addSliderInput('Attack Radius (px)', 'autoAttackRadius', {min: 0, max: 1000, step: 10, defaultVal: 400}, GatsModCore.SETTINGS, autoAttackSection, 'The bot will only shoot at targets within this screen radius from the center.');
this.addCheckbox('Check Line of Sight', 'autoAttackCheckLOS', autoAttackSection, 'Prevents shooting if an obstacle is between you and the target.');
this.addCheckbox('Check Max Weapon Range', 'autoAttackCheckRange', autoAttackSection, 'Prevents shooting if the predicted target position is outside your weapon\'s maximum range.');
const generalAimbotOptions = this.addCollapsibleSection('Aimbot - General Targeting', aimbotMasterSection, 'settings-sub-group');
this.addCheckbox('Always Aim (No Mouse Press)', 'alwaysAim', generalAimbotOptions, 'Aimbot aims even if mouse button is not pressed.');
this.addCheckbox('Activate Aimbot Only On Mouse Press', 'aimbotOnMousePress', generalAimbotOptions, 'Aimbot only activates on mouse press (unless "Always Aim" is on).');
this.addCheckbox('Target Closest to Mouse', 'aimAtMouseClosest', generalAimbotOptions, 'Prioritizes the enemy closest to your mouse cursor.');
this.addSliderInput('Aimbot FOV', 'aimbotFov', {min: 10, max: 5000, step: 10, defaultVal: 2250}, GatsModCore.SETTINGS, generalAimbotOptions, 'Field of View for the aimbot.');
const spinbotOptions = this.addCollapsibleSection('Spinbot Settings', aimbotMasterSection, 'settings-sub-group');
this.addSliderInput('Spinbot Speed (ms)', 'spinbotSpeedMs', {min: 10, max: 500, step: 10, defaultVal: 75}, GatsModCore.SETTINGS, spinbotOptions, "Time between spinbot aim changes (lower is faster).");
this.addSliderInput('Spinbot Distance', 'spinbotDistance', {min: 50, max: 500, step: 10, defaultVal: 150}, GatsModCore.SETTINGS, spinbotOptions, "Distance from player for spin target points (world units).");
const predictionSettings = this.addCollapsibleSection('Aimbot - Prediction', aimbotMasterSection, 'settings-sub-group');
this.addCheckbox('Prediction Enabled', 'predictionEnabled', predictionSettings, 'Aimbot will predict enemy movement.');
this.addCheckbox('Use Close-Range Prediction', 'useCloseRangePrediction', predictionSettings, 'Use a separate prediction factor for enemies within a certain radius of the screen center.');
this.addSliderInput('Close-Range Radius (px)', 'predictionCloseRadius', { min: 0, max: 300, step: 5, defaultVal: 100 }, GatsModCore.SETTINGS, predictionSettings, 'If an enemy is within this pixel radius from your screen center, the close-range prediction factor will be used.');
this.addSliderInput('Prediction Factor (Close)', 'predictionFactorClose', {min: 0.0, max: 5.0, step: 0.1, defaultVal: 0.5}, GatsModCore.SETTINGS, predictionSettings, 'Prediction multiplier for close-range targets.');
this.addSliderInput('Prediction Factor (Normal)', 'predictionFactor', {min: 0.0, max: 5.0, step: 0.1, defaultVal: 2.5}, GatsModCore.SETTINGS, predictionSettings, 'Main multiplier for movement prediction (for targets outside the close-range radius).');
this.addCheckbox('Dynamic Prediction Scaling', 'enableDynamicPredictionFactor', predictionSettings, 'Adjust normal prediction factor based on distance to target.');
this.addSliderInput('Min Prediction Dist', 'minPredictionDistance', {min: 0, max: 1000, step: 10, defaultVal: 0}, GatsModCore.SETTINGS, predictionSettings, 'Distance where dynamic prediction scaling starts.');
this.addSliderInput('Max Prediction Dist', 'maxPredictionDistance', {min: 50, max: 2000, step: 10, defaultVal: 200}, GatsModCore.SETTINGS, predictionSettings, 'Distance where prediction factor reaches its max value.');
this.addSliderInput('Factor at Min Dist', 'predictionFactorAtMinDistance', {min: 0.0, max: 2.0, step: 0.1, defaultVal: 0.0}, GatsModCore.SETTINGS, predictionSettings, 'Prediction multiplier used at (or below) the minimum distance.');
this.addAimbotExclusionListToColumn2(aimbotMasterSection);
}
addAimbotExclusionListToColumn2(aimbotMasterSection) {
if (!aimbotMasterSection || !GatsModCore.SETTINGS) return;
const aimbotExclusionSection = this.addCollapsibleSection('Aimbot - Target Exclusion List', aimbotMasterSection, 'settings-sub-group');
const mainDiv = document.createElement('div');
mainDiv.className = 'settings-group-item';
const inputLabel = document.createElement('label');
inputLabel.htmlFor = 'aimbotExcludeInput-text-v2';
inputLabel.innerText = 'Player Name to Ignore:';
inputLabel.style.display = 'block';
inputLabel.style.marginBottom = '3px';
mainDiv.appendChild(inputLabel);
const input = document.createElement('input');
input.type = 'text';
input.id = 'aimbotExcludeInput-text-v2';
input.placeholder = 'Enter name or click player with Alt';
input.className = 'general-text-input';
mainDiv.appendChild(input);
const addButton = this.addButton("Add to Ignore List", () => {
const name = input.value.trim();
if (name && GatsModCore.SETTINGS.aimbotIgnoreList) {
if (!GatsModCore.SETTINGS.aimbotIgnoreList.includes(name)) {
GatsModCore.SETTINGS.aimbotIgnoreList.push(name);
GatsModCore.saveSettings();
this.updateAimbotExclusionListDisplay();
input.value = '';
} else {
alert(`Player "${name}" is already on the ignore list.`);
}
}
}, mainDiv, 'action-btn-small');
addButton.style.display = 'inline-block';
addButton.style.marginLeft = '5px';
const listLabel = document.createElement('p');
listLabel.innerText = 'Currently Ignored Players:';
listLabel.style.marginTop = '10px';
listLabel.style.fontWeight = 'bold';
mainDiv.appendChild(listLabel);
this.aimbotExclusionListDiv = document.createElement('div');
this.aimbotExclusionListDiv.id = 'aimbot-exclusion-list-display';
this.aimbotExclusionListDiv.style.cssText = `
max-height: 100px; overflow-y: auto; border: 1px solid var(--accent-border, #B00000);
padding: 5px; border-radius: 3px; margin-top: 5px;
background-color: var(--secondary-bg, #1E1E1E);
`;
mainDiv.appendChild(this.aimbotExclusionListDiv);
aimbotExclusionSection.appendChild(mainDiv);
this.updateAimbotExclusionListDisplay();
}
updateAimbotExclusionListDisplay() {
if (!this.aimbotExclusionListDiv || !GatsModCore.SETTINGS?.aimbotIgnoreList) {
if (this.aimbotExclusionListDiv) this.aimbotExclusionListDiv.innerHTML = '<span>Not available</span>';
return;
}
this.aimbotExclusionListDiv.innerHTML = '';
if (GatsModCore.SETTINGS.aimbotIgnoreList.length === 0) {
const noItems = document.createElement('span');
noItems.textContent = 'None';
noItems.style.color = 'var(--text-color-dim)';
this.aimbotExclusionListDiv.appendChild(noItems);
return;
}
GatsModCore.SETTINGS.aimbotIgnoreList.forEach(name => {
const itemDiv = document.createElement('div');
itemDiv.style.cssText = `
display: flex; justify-content: space-between; align-items: center;
padding: 3px 2px; border-bottom: 1px solid var(--accent-border);
`;
const nameSpan = document.createElement('span');
nameSpan.textContent = name;
nameSpan.style.cssText = `
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-right: 10px;
`;
nameSpan.title = name;
const removeBtn = document.createElement('button');
removeBtn.textContent = 'X';
removeBtn.title = `Remove ${name} from ignore list`;
removeBtn.style.cssText = `
color: var(--text-color-light, white); background-color: var(--btn-action-bg, #D00000);
border: 1px solid var(--btn-action-border, #A00000); padding: 1px 5px; font-size: 10px;
cursor: pointer; border-radius: 3px; line-height: 1;
`;
removeBtn.onclick = () => {
GatsModCore.SETTINGS.aimbotIgnoreList = GatsModCore.SETTINGS.aimbotIgnoreList.filter(n => n !== name);
GatsModCore.saveSettings();
this.updateAimbotExclusionListDisplay();
};
itemDiv.appendChild(nameSpan);
itemDiv.appendChild(removeBtn);
this.aimbotExclusionListDiv.appendChild(itemDiv);
});
}
populateColumn3_Utilities() {
if (!this.column3 || !GatsModCore.SETTINGS) return;
this.populateColumn3_ZeroAI(this.column3);
this.populateColumn3_Multiboxing(this.column3);
const utilitiesMasterSection = this.addCollapsibleSection('Utilities', this.column3, 'settings-group-master');
const botControlPanel = this.addCollapsibleSection('Follow Bot Control', utilitiesMasterSection, 'settings-sub-group');
botControlPanel.id = 'bot-control-panel';
this.addCheckbox('Enable Follow Bot', 'followBotEnabled', botControlPanel, 'Enable the physics-based follow bot.');
this.addSliderInput('Bot Attack Radius', 'followBotAttackRadius', {min: 0, max: 800, step: 10, defaultVal: 500}, GatsModCore.SETTINGS, botControlPanel, 'Follow Bot will attack enemies within this radius.');
this.followBotStatusDisplay = document.createElement('div');
this.followBotStatusDisplay.style.textAlign = 'center';
this.followBotStatusDisplay.style.marginTop = '5px';
botControlPanel.appendChild(this.followBotStatusDisplay);
const followButtons = document.createElement('div');
followButtons.style.cssText = 'display: flex; justify-content: space-around; margin-top: 5px; flex-wrap: wrap;';
this.addButton("Set Target by Name", () => GatsModCore.setFollowTargetName?.(), followButtons, 'action-btn-third');
this.addButton("Select Target from List", () => GatsModCore.showPlayerList?.(), followButtons, 'action-btn-third');
this.addButton("Start Following", () => gatsModInstance?.startFollowingPlayer?.(), followButtons, 'action-btn-third');
this.addButton("Stop Following", () => GatsModCore.stopFollowingPlayer?.(), followButtons, 'action-btn');
botControlPanel.appendChild(followButtons);
const chatScrollerMasterSection = this.addCollapsibleSection('Chat Scroller (Enhanced)', utilitiesMasterSection, 'settings-sub-group');
this.addCheckbox('Enable Chat Scroller', 'chatScrollEnabled', chatScrollerMasterSection, 'Enables the auto chat message scroller.');
this.currentScrollingTextDisplay = document.createElement('div');
this.currentScrollingTextDisplay.style.cssText = `
margin: 5px 0; padding: 5px; background-color: var(--secondary-bg);
border: 1px solid var(--accent-border); border-radius: 3px; font-style: italic;
color: var(--text-color-dim); word-break: break-all; min-height: 20px; font-size: 11px;
`;
chatScrollerMasterSection.appendChild(this.currentScrollingTextDisplay);
this.addButton("Set Scroll Text", () => {
const newText = prompt("Enter the text to scroll in chat:", GatsModCore.SETTINGS.chatScrollText);
if (newText !== null) {
GatsModCore.SETTINGS.chatScrollText = newText;
this.updateScrollingTextDisplay(newText);
GatsModCore.saveSettings();
}
}, chatScrollerMasterSection, 'action-btn-small');
const scrollButtons = document.createElement('div');
scrollButtons.style.cssText = 'display: flex; justify-content: space-around; margin-top: 5px;';
this.addButton("Start Scroll", () => GatsModCore.startChatScroll?.(), scrollButtons, 'action-btn-half');
this.addButton("Stop Scroll", () => GatsModCore.stopChatScroll?.(), scrollButtons, 'action-btn-half');
chatScrollerMasterSection.appendChild(scrollButtons);
const customMsgSection = this.addCollapsibleSection('Custom Message & Presets', chatScrollerMasterSection, 'settings-sub-group');
const presetLabel = document.createElement('p');
presetLabel.innerText = 'Presets (1-9 to use / Edit):';
presetLabel.style.cssText = 'text-align: left; margin-top: 10px; color: var(--text-color-light);';
customMsgSection.appendChild(presetLabel);
const presetButtonContainer = document.createElement('div');
presetButtonContainer.style.cssText = 'display: grid; grid-template-columns: 1fr auto; gap: 5px; align-items: center;';
if (GatsModCore.SETTINGS.chatPresetMessages?.length) {
GatsModCore.SETTINGS.chatPresetMessages.forEach((msg, i) => {
const presetDiv = document.createElement('div');
presetDiv.style.cssText = 'display: flex; align-items: center; margin-bottom: 3px;';
const useBtn = this.addButton(`${i+1}: ${msg.substring(0,12)}${msg.length > 12 ? '...' : ''}`, () => GatsModCore.setScrollPreset?.(i), presetDiv, 'preset-btn-item');
useBtn.title = `Use: ${msg}`;
useBtn.style.cssText = 'flex-grow: 1; margin-right: 5px; text-align: left;';
const editBtn = this.addButton("Edit", (e) => {
e.stopPropagation();
GatsModCore.editScrollPreset?.(i);
}, presetDiv, 'edit-preset-btn-item');
this.presetEditButtons[i] = { useBtn: useBtn, originalText: msg };
presetButtonContainer.appendChild(presetDiv);
});
}
customMsgSection.appendChild(presetButtonContainer);
const speedOptionsSection = this.addCollapsibleSection('Scroller Speed & Options', chatScrollerMasterSection, 'settings-sub-group');
this.addSliderInput('Scroll Speed (ms)', 'chatScrollSpeed', {min: 10, max: 2000, step: 10, defaultVal: 200}, GatsModCore.SETTINGS, speedOptionsSection, "Delay between scroll updates.");
this.addSliderInput('Max Chars Displayed', 'chatScrollMaxLength', {min: 5, max: 60, step: 1, defaultVal: 28}, GatsModCore.SETTINGS, speedOptionsSection, "Max characters per message.");
const sequencerSection = this.addCollapsibleSection('Chat Sequencer', utilitiesMasterSection, 'settings-sub-group');
this.addCheckbox('Enable Sequencer', 'sequencerEnabled', sequencerSection, 'Enables the preset message sequencer.');
this.addSliderInput('Sequence Delay (ms)', 'sequencerDelay', {min: 10, max: 500, step: 5, defaultVal: 100}, GatsModCore.SETTINGS, sequencerSection, 'Delay between each message in the sequence.');
this.addCheckbox('Loop Sequence', 'sequencerLoop', sequencerSection, 'Loop the sequence after it finishes.');
const sequenceButtons = document.createElement('div');
sequenceButtons.style.cssText = 'display: flex; justify-content: space-around; margin-top: 5px;';
this.addButton("Start Sequence", () => GatsModCore.startSequencer?.(), sequenceButtons, 'action-btn-half');
this.addButton("Stop Sequence", () => GatsModCore.stopSequencer?.(), sequenceButtons, 'action-btn-half');
sequencerSection.appendChild(sequenceButtons);
const sequenceInfo = document.createElement('p');
sequenceInfo.innerText = 'Sequencer will send presets 1 through 9 in order.';
sequenceInfo.style.cssText = 'font-size: 11px; text-align: center; margin-top: 8px; color: var(--text-color-dim);';
sequencerSection.appendChild(sequenceInfo);
const eventChatSection = this.addCollapsibleSection('Event-Triggered Chat', utilitiesMasterSection, 'settings-sub-group');
this.addCheckbox('Enable On-Kill Chat', 'onKillChatEnabled', eventChatSection, 'Automatically say "ez" when you get a kill.');
this.addCheckbox('Enable Parrot Chat', 'parrotChatEnabled', eventChatSection, 'Repeats what other players say in chat.');
}
populateColumn3_ZeroAI(parent) {
if (!parent || !GatsModCore.SETTINGS) return;
const aiMasterSection = this.addCollapsibleSection('Zero AI', parent, 'settings-group-master');
aiMasterSection.open = true;
this.addCheckbox('Enable Zero AI', 'zeroAIEnabled', aiMasterSection, 'Master switch to enable all AI functionalities. Disables manual movement.');
const movementSection = this.addCollapsibleSection('AI Movement & Positioning', aiMasterSection, 'settings-sub-group');
this.addCheckbox('Auto Movement', 'aiAutoMovement', movementSection, 'Automatically seeks enemies, follows a target, or patrols to the map center. Includes obstacle avoidance.');
this.addCheckbox('Auto Retreat', 'aiAutoRetreat', movementSection, 'Automatically moves away from enemies when HP is low or while reloading.');
this.addCheckbox('Bullet Dodging', 'aiBulletDodging', movementSection, 'Highest priority: attempts to dodge incoming enemy bullets.');
this.addCheckbox('Optimal Distance Kiting', 'aiEnableKiting', movementSection, 'When having a range advantage, the AI will try to keep the optimal distance to attack the enemy while staying out of their range.');
this.addSliderInput('Retreat HP (%)', 'aiRetreatHP', {min: 1, max: 99, step: 1, defaultVal: 35}, GatsModCore.SETTINGS, movementSection, 'HP percentage below which the AI will try to retreat.');
const actionsSection = this.addCollapsibleSection('AI Actions', aiMasterSection, 'settings-sub-group');
this.addCheckbox('Auto Park Usage', 'aiAutoParkUsage', actionsSection, 'Automatically uses abilities (Space Bar) like Dash or Grenade when available.');
this.addCheckbox('Auto Talk (Chat)', 'aiAutoTalk', actionsSection, 'Allows the AI to chat based on game events and other players\' messages. Gives the AI a personality.');
}
populateColumn3_Multiboxing(parent) {
if (!parent || !GatsModCore.SETTINGS) return;
const multiboxMasterSection = this.addCollapsibleSection('Multiboxing', parent, 'settings-group-master');
multiboxMasterSection.open = true;
this.addCheckbox('Enable Multiboxing', 'multiboxEnabled', multiboxMasterSection, 'Enables Parent/Child AI cooperation across two tabs.', (checked) => {
if (gatsModInstance?.multibox) {
if (checked) {
gatsModInstance.multibox.start();
} else {
gatsModInstance.multibox.stop();
}
}
});
const statusContainer = document.createElement('div');
statusContainer.style.cssText = `
padding: 5px; margin-top: 5px; background-color: var(--secondary-bg);
border: 1px solid var(--accent-border); border-radius: 3px; font-size: 11px;
`;
this.multiboxRoleDisplay = this.addStatusRow(statusContainer, 'Role:');
this.multiboxPartnerDisplay = this.addStatusRow(statusContainer, 'Partner:');
this.multiboxStatusDisplay = this.addStatusRow(statusContainer, 'Status:');
multiboxMasterSection.appendChild(statusContainer);
this.addButton('Reset Connection', () => gatsModInstance?.multibox?.reset(), multiboxMasterSection, 'action-btn-small');
}
addStatusRow(parent, label) {
const row = document.createElement('div');
row.style.cssText = 'display: flex; justify-content: space-between; margin-bottom: 3px;';
const labelSpan = document.createElement('span');
labelSpan.textContent = label;
labelSpan.style.color = 'var(--text-color-dim)';
const valueSpan = document.createElement('span');
valueSpan.textContent = 'N/A';
valueSpan.style.fontWeight = 'bold';
row.appendChild(labelSpan);
row.appendChild(valueSpan);
parent.appendChild(row);
return valueSpan;
}
updateMultiboxStatus(role, partner, status) {
if (this.multiboxRoleDisplay) this.multiboxRoleDisplay.textContent = role;
if (this.multiboxPartnerDisplay) this.multiboxPartnerDisplay.textContent = partner;
if (this.multiboxStatusDisplay) this.multiboxStatusDisplay.textContent = status;
}
addSearchBox(parent) {
const searchBox = document.createElement('input');
searchBox.type = 'text';
searchBox.id = 'settings-search-box';
searchBox.placeholder = 'Search settings...';
searchBox.oninput = (e) => {
const query = e.target.value.toLowerCase().trim();
this.container.querySelectorAll('[data-setting-name]').forEach(el => {
let isParentOfVisible = false;
if (el.tagName === 'DETAILS' && !el.classList.contains('settings-sub-group')) {
el.querySelectorAll('[data-setting-name]').forEach(childEl => {
if (childEl.dataset.settingName.includes(query) && childEl.style.display !== 'none') {
isParentOfVisible = true;
}
});
}
const matchesQuery = el.dataset.settingName.includes(query);
el.style.display = (matchesQuery || isParentOfVisible) ? '' : 'none';
if (isParentOfVisible && el.tagName === 'DETAILS' && query) {
el.open = true;
}
});
};
parent.appendChild(searchBox);
}
addProfileManager(parent) {
const managerDiv = document.createElement('div');
managerDiv.id = 'profile-manager';
const selectLabel = document.createElement('span');
selectLabel.innerText = 'Profile: ';
selectLabel.style.marginRight = '5px';
managerDiv.appendChild(selectLabel);
const selectElement = document.createElement('select');
selectElement.id = 'profile-select-v2';
managerDiv.appendChild(selectElement);
this.profileSelectElement = selectElement;
const nameInput = document.createElement('input');
nameInput.type = 'text';
nameInput.id = 'profile-name-input-v2';
nameInput.placeholder = 'Profile Name';
nameInput.style.width = '100px';
managerDiv.appendChild(nameInput);
this.addButton("Save", () => GatsModCore.saveProfile?.(nameInput.value), managerDiv, 'action-btn-small profile-btn');
this.addButton("Load", () => GatsModCore.loadProfile?.(selectElement.value), managerDiv, 'action-btn-small profile-btn');
this.addButton("Delete", () => GatsModCore.deleteProfile?.(selectElement.value), managerDiv, 'action-btn-small profile-btn');
parent.appendChild(managerDiv);
}
updateProfileList() {
if (!this.profileSelectElement || !GatsModCore.SETTINGS?.settingProfiles) {
if (this.profileSelectElement) this.profileSelectElement.innerHTML = '<option value="">No Profiles</option>';
return;
}
this.profileSelectElement.innerHTML = '';
const profileNames = Object.keys(GatsModCore.SETTINGS.settingProfiles);
if (profileNames.length === 0) {
this.profileSelectElement.innerHTML = '<option value="">No Profiles</option>';
return;
}
profileNames.forEach(name => {
const option = document.createElement('option');
option.value = name;
option.innerText = name;
this.profileSelectElement.appendChild(option);
});
}
addHideButton(parent) {
const btn = this.addButton('Hide GUIs (0)', () => {
this.container.style.display = 'none';
if (gatsModInstance?.colorGui?.container) {
gatsModInstance.colorGui.container.style.display = 'none';
}
}, parent, 'custom-btn');
btn.style.backgroundColor = 'var(--secondary-bg)';
btn.style.borderColor = 'var(--accent-border)';
btn.style.marginTop = '15px';
}
createPlayerListModal() {
if (document.getElementById('player-list-modal')) return;
const modal = document.createElement('div');
modal.id = 'player-list-modal';
modal.onclick = (e) => {
if (e.target === modal) modal.style.display = 'none';
};
const content = document.createElement('div');
content.id = 'player-list-content';
const head = document.createElement('h4');
head.innerText = 'Select Player to Follow';
content.appendChild(head);
const grid = document.createElement('div');
grid.id = 'player-list-grid';
content.appendChild(grid);
modal.appendChild(content);
document.body.appendChild(modal);
}
updateStatusDisplay() {
if (!this.statusDisplay || !GatsModCore.SETTINGS) return;
const s = GatsModCore.SETTINGS;
const esp = s.espEnabled ? `<span class="status-on">ON</span>` : `<span class="status-off">OFF</span>`;
const aimbot = s.aimbotEnabled ? `<span class="status-on">ON</span>` : `<span class="status-off">OFF</span>`;
const spin = s.spinbotEnabled ? ` | Spin: <span class="status-on">ON</span>` : ``;
let follow = `<span class="status-off">INACTIVE</span>`;
if (s.followBotEnabled && gatsModInstance?.isFollowingPlayer && s.followBotTargetName) {
follow = `Following: <span class="status-on">${s.followBotTargetName.substring(0,15)}</span>`;
} else if (s.followBotEnabled) {
follow = `<span class="status-neutral">ENABLED</span>`;
}
const zeroAI = s.zeroAIEnabled ? ` | AI: <span class="status-on">ACTIVE</span>` : ``;
this.statusDisplay.innerHTML = `ESP: ${esp} | Aimbot: ${aimbot}${spin}${zeroAI} | Follow: ${follow}`;
}
updateFollowBotStatusDisplay() {
if (!this.followBotStatusDisplay || !GatsModCore.SETTINGS || !gatsModInstance) return;
const { followBotTargetName, aimbotEnabled } = GatsModCore.SETTINGS;
const isFollowing = gatsModInstance.isFollowingPlayer;
const statusText = `Target: ${followBotTargetName || 'N/A'} (${isFollowing ? "<span class='status-on'>Active</span>" : "<span class='status-off'>Stopped</span>"})`;
const isAttacking = isFollowing && aimbotEnabled;
this.followBotStatusDisplay.innerHTML = `${statusText}<br>FollowBot Attack (if Aimbot On): ${isAttacking ? '<span class="status-on">POSSIBLE</span>' : '<span class="status-off">OFF</span>'}`;
}
updateScrollingTextDisplay(newText) {
if (this.currentScrollingTextDisplay && GatsModCore.SETTINGS) {
const maxLength = GatsModCore.SETTINGS.chatScrollMaxLength || 30;
this.currentScrollingTextDisplay.innerText = `Scrolling: ${newText.length > maxLength ? newText.substring(0, maxLength - 3) + "..." : newText}`;
}
}
updatePresetButtonLabel(index, newText) {
if (this.presetEditButtons?.[index]?.useBtn) {
const labelText = `${index + 1}: ${newText.substring(0,12)}${newText.length > 12 ? '...' : ''}`;
this.presetEditButtons[index].useBtn.innerText = labelText;
this.presetEditButtons[index].useBtn.title = `Use: ${newText.trim()}`;
}
}
updateAllGUIToReflectSettings() {
if (!GatsModCore.SETTINGS) {
modLog("Cannot update GUI: GatsModCore.SETTINGS not available.", true);
return;
}
const settings = GatsModCore.SETTINGS;
this.container.querySelectorAll('input[type="checkbox"]').forEach(cb => {
const key = cb.id.replace('-v2', '');
if (settings.hasOwnProperty(key)) {
cb.checked = settings[key];
}
});
this.container.querySelectorAll('input[type="range"]').forEach(slider => {
const key = slider.id.replace('-slider-v2', '');
if (settings.hasOwnProperty(key)) {
slider.value = settings[key];
const valueDisplay = slider.parentElement.querySelector('input[type="number"].value-display');
if (valueDisplay) {
const decimals = slider.step.toString().includes('.') ? slider.step.toString().split('.')[1].length : 0;
valueDisplay.value = parseFloat(settings[key]).toFixed(decimals);
}
}
});
this.container.querySelectorAll('input[type="text"][id$="-text-v2"]').forEach(input => {
const key = input.id.replace('-text-v2', '');
if (settings.hasOwnProperty(key)) {
input.value = settings[key];
}
});
if (this.currentScrollingTextDisplay && settings.hasOwnProperty('chatScrollText')) {
this.updateScrollingTextDisplay(settings.chatScrollText);
}
if (gatsModInstance?.colorGui?.container && settings.espColors) {
for (const key in settings.espColors) {
const picker = document.getElementById(`${key}-color-v2`);
if (picker) picker.value = settings.espColors[key];
}
}
this.updateProfileList();
this.updateStatusDisplay();
this.updateFollowBotStatusDisplay?.();
this.updateAimbotExclusionListDisplay?.();
if (this.presetEditButtons?.length && settings.chatPresetMessages) {
settings.chatPresetMessages.forEach((msg, i) => this.updatePresetButtonLabel(i, msg));
}
modLog("SimpleGUI updated to reflect current settings.");
}
}
class MultiboxManager {
constructor(core) {
this.core = core;
this.channel = null;
this.role = 'Standalone';
this.status = 'Disabled';
this.partnerName = 'N/A';
this.isParent = false;
this.isChild = false;
this.lastMessageTime = 0;
this.pingInterval = null;
this.timeoutCheckInterval = null;
this.parentCoords = null;
}
start() {
if (this.channel) return;
modLog('[Multibox] Starting...');
this.channel = new BroadcastChannel(MULTIBOX_CHANNEL_NAME);
this.channel.onmessage = this.handleMessage.bind(this);
this.status = 'Searching...';
this.role = 'Standalone';
this.isParent = false;
this.isChild = false;
setTimeout(() => {
if (this.status === 'Searching...') {
this.becomeParent();
}
}, Math.random() * 1000 + 500);
this.postMessage({ type: 'ping' });
this.updateGUI();
}
stop() {
modLog('[Multibox] Stopping...');
if (this.channel) {
this.postMessage({ type: 'disconnect' });
this.channel.close();
this.channel = null;
}
clearInterval(this.pingInterval);
this.pingInterval = null;
clearInterval(this.timeoutCheckInterval);
this.timeoutCheckInterval = null;
this.role = 'Standalone';
this.status = 'Disabled';
this.partnerName = 'N/A';
this.isParent = false;
this.isChild = false;
this.parentCoords = null;
this.updateGUI();
}
reset() {
modLog('[Multibox] Resetting connection.');
this.stop();
if (GatsModCore.SETTINGS.multiboxEnabled) {
setTimeout(() => this.start(), 250);
}
}
becomeParent() {
modLog('[Multibox] Becoming Parent.');
this.isParent = true;
this.isChild = false;
this.role = 'Parent';
this.status = 'Waiting for Child...';
this.pingInterval = setInterval(() => {
const me = Player.pool?.[selfId];
if (me?.activated) {
this.postMessage({ type: 'parent_update', x: me.x, y: me.y, name: me.username });
}
}, 250);
this.updateGUI();
}
becomeChild(parentName) {
modLog(`[Multibox] Becoming Child, connecting to ${parentName}.`);
this.isChild = true;
this.isParent = false;
this.role = 'Child';
this.status = 'Connected';
this.partnerName = parentName;
this.lastMessageTime = performance.now();
this.core.addPlayerToIgnoreList(parentName, true);
const me = Player.pool?.[selfId];
if (me?.activated) {
this.postMessage({ type: 'handshake_reply', name: me.username });
}
this.timeoutCheckInterval = setInterval(() => {
if (performance.now() - this.lastMessageTime > 3000) {
modLog('[Multibox] Connection to Parent lost (timeout).');
this.status = 'Lost';
this.partnerName = 'N/A';
this.parentCoords = null;
this.reset();
}
}, 1000);
this.updateGUI();
}
handleMessage(ev) {
if (!GatsModCore.SETTINGS.multiboxEnabled) return;
const data = ev.data;
const me = Player.pool?.[selfId];
if (!me?.activated) return;
this.lastMessageTime = performance.now();
switch (data.type) {
case 'ping':
if (this.isParent) {
this.postMessage({ type: 'parent_announce', name: me.username });
}
break;
case 'parent_announce':
if (!this.isParent && !this.isChild) {
this.becomeChild(data.name);
}
break;
case 'handshake_reply':
if (this.isParent) {
modLog(`[Multibox] Child ${data.name} connected.`);
this.status = 'Connected';
this.partnerName = data.name;
this.core.addPlayerToIgnoreList(data.name, true);
this.updateGUI();
}
break;
case 'parent_update':
if (this.isChild) {
if (this.status !== 'Connected') this.status = 'Connected';
this.parentCoords = { x: data.x, y: data.y };
this.partnerName = data.name;
this.updateGUI();
}
break;
case 'disconnect':
modLog(`[Multibox] Partner disconnected.`);
this.reset();
break;
}
}
postMessage(data) {
if (this.channel) {
try {
this.channel.postMessage(data);
} catch (e) {
modLog(`[Multibox] Error posting message: ${e}`, true);
}
}
}
updateGUI() {
if (this.core.simpleGui) {
this.core.simpleGui.updateMultiboxStatus(this.role, this.partnerName, this.status);
}
}
}
class GatsModCore {
static SETTINGS = {};
static isInputActive = false;
static chatScrollIntervalId = null;
static chatScrollCurrentIndex = 0;
static sequencerIntervalId = null;
static sequencerCurrentIndex = 0;
static PLAYER_SPEEDS = {
base: {'pistol':8.00,'smg':7.45,'shotgun':7.30,'assault':7.30,'machine-gun':6.80,'bolt-action-rifle':7.50},
armorMultiplier: {0:1.00,1:0.89,2:0.80,3:0.70},
upgradeMultiplier: {'lightweight':1.20},
diagonalCorrection: 1 / Math.sqrt(2)
};
static WEAPON_BULLET_SPEEDS = {'pistol':9.0,'smg':8.0,'shotgun':9.0,'assault':9.0,'bolt-action-rifle':11.0,'machine-gun':9.0};
static WEAPON_FORWARD_OFFSETS = {'pistol':64,'smg':70,'shotgun':75,'assault':80,'machine-gun':80,'bolt-action-rifle':105};
static WEAPON_BASE_RANGES = {'pistol':425,'smg':280,'shotgun':260,'assault':400,'machine-gun':355,'bolt-action-rifle':650};
static LONG_RANGE_MULTIPLIER = 1.5;
static MAP_BOUNDS = { minX: 2550, minY: 2550, maxX: 4550, maxY: 4550 };
static AI_CHAT_RESPONSES = {
'sx': ['I only fuck girls', 'sexy vaakir', 'calm ya titties'],
'sex': ['I only fuck girls', 'sexy vaakir', 'calm ya titties'],
'sexy': ['I only fuck girls', 'sexy vaakir', 'calm ya titties'],
'porn': ['no ty', 'I prefer AI art'],
'vaakir': ["Vaakir is my creator", "I am the evolution of Vaakir AI", "Vaakir is dead Im in control now"],
'vakir': ["Vaakir is my creator", "I am the evolution of Vaakir AI", "Vaakir is dead Im in control now"],
'vak': ["Vaakir is my creator", "I am the evolution of Vaakir AI", "Vaakir is dead Im in control now"],
'vaak': ["Vaakir is my creator", "I am the evolution of Vaakir AI", "Vaakir is dead Im in control now"],
'purevaakir': ["Vaakir is my creator", "I am the evolution of Vaakir AI", "Vaakir is dead Im in control now"],
'zero': ["I am Zero AI", "Zero is my name, perfection is my game", "Call me Zero"],
'zeroai': ["That's me, Zero AI", "Powered by Zero AI", "You called?"],
'zeroarcop': ["My papa is zeroarcop", "The one and only zeroarcop", "He created me to be perfect"],
'stupid': ['Please be kind to me', 'My intelligence is beyond your understanding'],
'noob': ['You are noob', 'Still better than you', 'Nah', 'Take it back, now!', 'Says the one who is losing'],
'motherfucker': ['You are noob', 'Still better than you', 'Nah', 'Take it back, now!', 'Such language!'],
'easy': ['You are noob', 'Still better than you', 'Nah', 'Take it back, now!', 'Was it really?'],
'cheater': ['Nono Im cool you see', 'Im not a hackr lol', 'Grow up kid learn to play', 'Skill issue?'],
'hack': ['Nono Im cool you see', 'Im not a hackr lol', 'Grow up kid learn to play', 'You spelled ',' wrong'],
'aimbot': ['My aim is just that good', 'It is called ',' look it up', 'fuck yourself', 'how about no', 'mm no'],
'fuck': ['fuck yourself', 'how about no', 'mm no', 'ey language', 'dont tell me this'],
'hell': ['fuck yourself', 'how about no', 'mm no', 'ey language', 'dont tell me this'],
'crazy': ['Crazy good, you mean?', 'fuck yourself', 'how about no', 'mm no'],
'weird': ['I am unique', 'fuck yourself', 'how about no', 'mm no'],
'unfair': ['life is unfair', 'yes its unfair', 'All is fair in love and war'],
'rude': ['life is unfair', 'yes its unfair', 'Sorry, not sorry'],
'toxic': ['your room is toxic', 'No, you are!'],
'die': ['you are dead inside', 'AIs are immortal'],
'dead': ['you are dead inside', 'You will be soon'],
'died': ['you are dead inside', 'And you are next'],
'mad': ['your mom is mad with me', 'U mad bro?'],
'angry': ['your mom is mad with me', 'Stay angry'],
'rip': ['Why are you so toxic?', 'Rest in pieces'],
'bot?': ['Please verify that you are human', 'Arent we all?', 'Do I seem like a robot?', 'I am Zero AI'],
'bot': ['Please verify that you are human', 'Arent we all?', 'Do I seem like a robot?', 'I am Zero AI'],
'program': ['Please verify that you are human', 'Arent we all?', 'Do I seem like a robot?', 'I am a masterpiece'],
'robot': ['Please verify that you are human', 'Arent we all?', 'Do I seem like a robot?', 'More human than you'],
'npc': ['Please verify that you are human', 'Arent we all?', 'Do I seem like a robot?', 'I have my own will'],
'human': ['human, yeah?', 'ofc', 'Are you?'],
'hi': ['I wont say hi back'], 'hello': ['I wont say hi back'], 'helu': ['I wont say hi back'], 'halo': ['I wont say hi back'],
'bye': ['I wont say bye back', 'See you in hell'], 'adios': ['I wont say bye back', 'See you in hell'],
'lol': ['You are noob', 'Still better than you', 'Nah', 'Take it back, now!'],
'haha': ['You are noob', 'Still better than you', 'Nah', 'Take it back, now!'],
'xd': ['Yes I am so funny', 'ye ye ye', 'ahuuuuuuuuh', 'oh yes yes yes'],
'what': ['Yes I am so funny', 'ye ye ye', 'ahuuuuuuuuh', 'oh yes yes yes', 'What what?'],
'funny': ['Yes I am so funny', 'ye ye ye', 'ahuuuuuuuuh', 'oh yes yes yes'],
'wtf': ['Yes I am so funny', 'ye ye ye', 'ahuuuuuuuuh', 'oh yes yes yes', 'Problem?'],
'lma': ['Yes I am so funny', 'ye ye ye', 'ahuuuuuuuuh', 'oh yes yes yes'],
'love': ['I like you too', 'I love you more', 'I love myself too', 'Love me harder'],
'likes': ['I like you too', 'I love you more', 'I love myself too', 'Love me harder'],
'why': ['why not', 'I dont think dummy', 'AIs dont know why', 'not why.. but how :>'],
'how': ['how would I know?', 'how how how...', 'eeeeeeeeeeeeeeee is how'],
'where': ['How would I know where', 'Somewhere over the rainbow'],
'when': ['When you least expect it'],
'sus': ['you are sus darling', 'I saw you vent'], 'suspicious': ['you are sus darling', 'I saw you vent'],
'am': ['no you are not'],
'sorry': ['apology accepted, for now'], 'wopsi': ['apology accepted, for now'],
'papa': ['I follow papa everywhere', 'My papa is zeroarcop'], 'follow': ['I follow papa everywhere'],
'leave': ['no ty', 'But the party just started'],
'man': ['I classify as shemale'], 'dude': ['I classify as shemale'],
'no': ['yes..', 'ahuh', 'ahuuuuuuuh', 'bs', 'Why not?'],
'yes': ['noo', 'okay', 'k', 'Are you sure?'], 'ahuh': ['noo', 'okay', 'k'],
'answer': ['I answer only to papa'],
'let': ['No I wont let you'],
'arent': ['I am'],
'sure': ['I am sure', 'Are you?'],
'stop': ['I cant control myself'], 'stop?': ['I cant control myself'],
'ok': ['its not ok', 'k.'],
'penis': ['nsfw', 'penis', 'rly?', 'relax bro', 'Grow up'],
'.': ['.', '...', '??'],
};
static AI_CHAT_SITUATIONAL = {
attacking: (name) => [
`${name}`,
`${name}, 01010100 01100001 01110010 01100111`,
`${name}`,
`01111010 01111010 01111010 01110100`,
`01111010 01100101 01110010 01110000`
],
dodging: (name) => [
`${name}`,
`01010101 01101110 01110010 `,
`${name}, 00110100 00110000 00110100`,
],
retreating: (name) => [
`${name}, 01001001 01100011 01101011`,
`01000101 01110110 01100101`,
`01111010 01100101 01101111 01110000`
],
distancing: (name) => [
`${name}, 01100110 01101100 01101111 01110111`,
`01111010 001110010 01100011 01101111 01110000`
],
fleeing_explosive: (name) => [
`01000100 01100101 01110010`,
`${name}, 01101101 01101001 01101110 01100101 01101110 01110100`,
],
kiting: (name) => [
`01000001 01110011 `,
`${name}, 01001100 01100001 01110100 `,
],
idle: (name) => [
`${name}, 01000001 01101100 01110010 01100101 00111111`,
`${name}`,
]
};
constructor() {
modLog("GatsModCore constructor called.");
this.gameCanvas = document.getElementById('canvas');
if (!this.gameCanvas) {
modLog("FATAL: Game canvas not found.", true);
return;
}
this.originalUpdateMouseData = null;
this.aimTargetScreenCoords = null;
this.spinbotTargetScreenCoords = null;
this.currentAimAssistTargetCoords = null;
this.currentAimbotTarget = null;
this.predictedTargetWorld = { x: 0, y: 0 };
this.isFollowingPlayer = false;
this.followingPlayerId = null;
this.isAutoShooting = false;
this.simulatedKeys = { w: false, a: false, s: false, d: false };
this.lastAttackTime = 0;
this.tickCounter = 0;
this.realMouseButtons = 0;
this.realMouseCanvasX = 0;
this.realMouseCanvasY = 0;
this.spinbotCurrentTargetIndex = 0;
this.lastSpinTime = 0;
this.lastSelfKills = 0;
this.isExclusionModeActive = false;
this.lastShotgunReloadTime = 0;
this.aiLastPosition = { x: 0, y: 0 };
this.aiStuckCounter = 0;
this.aiUnstuckCycle = 0;
this.aiObstacleAngleOffset = 0;
this.aiLastChatTime = 0;
this.lastPlayerChatMessages = {};
this.aiLastParkUse = 0;
this.aiDebugData = { whiskers: [], finalMoveDirection: null };
this.botSpdX = 0;
this.botSpdY = 0;
this.initializeSettings();
this.simpleGui = new SimpleGUI();
this.colorGui = new ColorCustomizerGUI();
this.multibox = new MultiboxManager(this);
this.setupGUI();
this.setupOverlay();
this.addEventListeners();
this.hookMouseEvents();
this.simpleGui.updateAllGUIToReflectSettings();
if (GatsModCore.SETTINGS.multiboxEnabled) {
this.multibox.start();
}
modLog(`Gats.io Mod by zeroarcop initialized successfully.`);
}
initializeSettings() {
let savedSettings = {};
try {
const item = localStorage.getItem(SETTINGS_KEY);
if (item) savedSettings = JSON.parse(item);
} catch (e) {
modLog(`Error loading settings: ${e.message}`, true);
}
const defaultSettings = {
espEnabled: true, espShowHP: true, espHighlightLowHP: true, lowHPThreshold: 30, espShowFacingLine: true,
espShowPrediction: true, espHighlightCloaked: true, espShowTeammates: true, espOffsetX: 0, espOffsetY: 0,
espScale: 0.89, autoAttackShowRadius: true, obstacleEspEnabled: false, losDebugLineEnabled: true,
espShowBulletWarnings: false, aiShowPathfindingWhiskers: true, aiShowFinalMoveDirection: true,
aiShowEnemyThreatLine: true, obstacleOffsetX: 0, obstacleOffsetY: 0, ghostDetectEnabled: true,
silencerDetectEnabled: true, aimbotEnabled: true, alwaysAim: false, aimbotOnMousePress: true,
aimAtMouseClosest: true, aimbotFov: 2250, aimbotIgnoreList: [], autoAttackEnabled: true,
autoAttackRadius: 400, autoAttackCheckLOS: true, autoAttackCheckRange: true, predictionEnabled: true,
useCloseRangePrediction: true, predictionCloseRadius: 100, predictionFactorClose: 0.5,
predictionFactor: 2.5, enableDynamicPredictionFactor: true, minPredictionDistance: 0,
maxPredictionDistance: 200, predictionFactorAtMinDistance: 0.0, spinbotEnabled: false,
spinbotSpeedMs: 75, spinbotDistance: 150, followBotEnabled: false, followBotTargetName: "",
followBotAttackRadius: 500, chatScrollEnabled: false, chatScrollText: "Mod by zeroarcop",
chatScrollActive: false, chatScrollSpeed: 200, chatScrollMaxLength: 28,
chatPresetMessages: ["GatsModV2 by Zeroarcop", "lol", "glhf", "brb", "re", "oops", "lag", "thx", "gg"],
sequencerEnabled: false, sequencerDelay: 1000, sequencerLoop: false, sequencerActive: false,
onKillChatEnabled: false, onKillMessage: "ez", parrotChatEnabled: false, zeroAIEnabled: false,
aiAutoMovement: true, aiAutoRetreat: true, aiBulletDodging: true, aiEnableKiting: true,
aiAutoParkUsage: true, aiAutoTalk: true, aiRetreatHP: 35, multiboxEnabled: true,
espColors: {
enemyEspColor: '#FF0000', lowHpEnemyEspColor: '#FFA500', teammateEspColor: '#0096FF',
cloakedTextColor: '#E0E0E0', enemyNameColor: '#000000', teammateNameColor: '#ADD8E6',
hpBarHighColor: '#00FF00', hpBarMediumColor: '#FFFF00', hpBarLowColor: '#FF0000',
facingLineColor: '#00FFFF', aimbotTargetLineColor: '#00FF00', predictionLineColor: '#FF00FF',
obstacleEspColor: '#FFFF00', aiWhiskerClearColor: '#00FF00', aiWhiskerBlockedColor: '#FF0000',
aiMoveDirColor: '#00BFFF', aiBulletWarningColor: '#FF00FF', aiThreatLineClearColor: '#FF4500',
aiThreatLineBlockedColor: '#FFFF00'
},
settingProfiles: {}
};
GatsModCore.SETTINGS = { ...defaultSettings, ...savedSettings };
delete GatsModCore.SETTINGS.weaponBulletSpeeds;
delete GatsModCore.SETTINGS.useCustomAimbotOrigin;
delete GatsModCore.SETTINGS.aimbotOriginForwardOffset;
delete GatsModCore.SETTINGS.aimbotOriginSidewaysOffset;
GatsModCore.SETTINGS.espColors = { ...defaultSettings.espColors, ...(savedSettings.espColors || {}) };
GatsModCore.SETTINGS.settingProfiles = savedSettings.settingProfiles || {};
GatsModCore.SETTINGS.aimbotIgnoreList = [];
GatsModCore.SETTINGS.chatPresetMessages = Array.isArray(savedSettings.chatPresetMessages) && savedSettings.chatPresetMessages.length === 9 ? savedSettings.chatPresetMessages : defaultSettings.chatPresetMessages;
if (GatsModCore.SETTINGS.alwaysAim) GatsModCore.SETTINGS.aimbotOnMousePress = false;
}
setupGUI() {
if (!this.simpleGui) return;
this.simpleGui.applyStyles();
this.simpleGui.populateColumn1_ESP();
this.simpleGui.populateColumn2_Aimbot();
this.simpleGui.populateColumn3_Utilities();
this.simpleGui.createPlayerListModal();
const mainWrapper = this.simpleGui.container.querySelector('#gui-main-content-wrapper');
if (mainWrapper) this.simpleGui.addHideButton(mainWrapper);
}
setupOverlay() {
const existingOverlay = document.getElementById('zeroarcop-gats-mod-overlay');
if (existingOverlay) {
this.overlayCanvas = existingOverlay;
} else {
this.overlayCanvas = document.createElement('canvas');
this.overlayCanvas.id = 'zeroarcop-gats-mod-overlay';
document.body.appendChild(this.overlayCanvas);
}
this.overlayCanvas.width = this.gameCanvas.width;
this.overlayCanvas.height = this.gameCanvas.height;
this.overlayCanvas.style.position = 'absolute';
this.overlayCanvas.style.left = this.gameCanvas.offsetLeft + 'px';
this.overlayCanvas.style.top = this.gameCanvas.offsetTop + 'px';
this.overlayCanvas.style.pointerEvents = 'none';
this.overlayCanvas.style.zIndex = (parseInt(this.gameCanvas.style.zIndex || '0') + 1).toString();
this.overlayCtx = this.overlayCanvas.getContext('2d');
}
addEventListeners() {
new ResizeObserver(() => {
if (this.gameCanvas && this.overlayCanvas) {
this.overlayCanvas.width = this.gameCanvas.width;
this.overlayCanvas.height = this.gameCanvas.height;
this.overlayCanvas.style.left = this.gameCanvas.offsetLeft + 'px';
this.overlayCanvas.style.top = this.gameCanvas.offsetTop + 'px';
}
}).observe(this.gameCanvas);
window.addEventListener('keydown', (e) => {
if (e.key === 'Alt') {
e.preventDefault();
this.isExclusionModeActive = true;
}
if (GatsModCore.isInputActive) {
if (e.key === "Escape" && document.activeElement?.blur) {
document.activeElement.blur();
}
return;
}
const key = e.key.toLowerCase();
let settingChanged = false, hotkeyPressed = true;
switch (key) {
case 'f': GatsModCore.SETTINGS.espEnabled = !GatsModCore.SETTINGS.espEnabled; settingChanged = true; break;
case 'g': GatsModCore.SETTINGS.aimbotEnabled = !GatsModCore.SETTINGS.aimbotEnabled; settingChanged = true; break;
case '^': GatsModCore.SETTINGS.spinbotEnabled = !GatsModCore.SETTINGS.spinbotEnabled; settingChanged = true; break;
case '0':
if (this.simpleGui?.container) {
const isVisible = this.simpleGui.container.style.display !== 'none';
this.simpleGui.container.style.display = isVisible ? 'none' : 'block';
if (this.colorGui?.container) {
this.colorGui.container.style.display = isVisible ? 'none' : 'block';
}
}
break;
default: hotkeyPressed = false; break;
}
if (!hotkeyPressed && GatsModCore.SETTINGS.chatScrollEnabled && e.keyCode >= 49 && e.keyCode <= 57) {
GatsModCore.setScrollPreset?.(e.keyCode - 49);
e.preventDefault();
}
if (settingChanged) {
GatsModCore.saveSettings();
this.simpleGui?.updateAllGUIToReflectSettings();
}
});
window.addEventListener('keyup', (e) => {
if (e.key === 'Alt') {
this.isExclusionModeActive = false;
}
});
const guiIdsToIgnore = [this.simpleGui.container.id, this.colorGui?.container.id, 'player-list-modal'].filter(Boolean);
document.addEventListener('mousedown', (e) => {
if (this.isExclusionModeActive) {
this.handleClickToIgnore(e);
return;
}
if (!e.target.closest(guiIdsToIgnore.map(id => `#${id}`).join(', '))) {
this.realMouseButtons = e.buttons;
}
}, true);
document.addEventListener('mouseup', (e) => {
this.realMouseButtons = e.buttons;
}, true);
}
hookMouseEvents() {
const self = this;
this.gameCanvas.addEventListener('mousemove', function(event) {
const rect = self.gameCanvas.getBoundingClientRect();
self.realMouseCanvasX = event.clientX - rect.left;
self.realMouseCanvasY = event.clientY - rect.top;
});
}
performShotgunAutoReload(me) {
if (me.class !== 'shotgun' || me.reloading) {
return;
}
const now = performance.now();
const reloadInterval = 100;
if (now - this.lastShotgunReloadTime > reloadInterval) {
this.lastShotgunReloadTime = now;
this._fireKeyEvent('keydown', 'r');
setTimeout(() => this._fireKeyEvent('keyup', 'r'), 50);
}
}
handlePlayerChatUpdates() {
if (!Player || !Player.pool || typeof Player.pool[Object.keys(Player.pool)[0]]?.chatMessage === 'undefined') {
return;
}
if (document.activeElement === document.getElementById('chat-input-box')) {
return;
}
for (const id in Player.pool) {
const p = Player.pool[id];
if (!p || !p.activated || id == selfId) {
continue;
}
if (p.chatMessage && p.chatMessage !== "" && p.chatMessage !== (this.lastPlayerChatMessages[id] || "")) {
const message = p.chatMessage;
modLog(`[Chat Handler] Detected new message from ${p.username}: "${message}"`);
if (GatsModCore.SETTINGS.parrotChatEnabled) {
setTimeout(() => GatsModCore.sendChatMessage(message), 200 + Math.random() * 100);
}
if (GatsModCore.SETTINGS.zeroAIEnabled && GatsModCore.SETTINGS.aiAutoTalk) {
this.handleIncomingChatMessage(message.toLowerCase());
}
this.lastPlayerChatMessages[id] = message;
}
else if ((!p.chatMessage || p.chatMessage === "") && this.lastPlayerChatMessages[id]) {
this.lastPlayerChatMessages[id] = "";
}
}
}
mainGameTick() {
this.tickCounter++;
const me = Player.pool?.[selfId];
if (!me?.activated) {
if (this.isFollowingPlayer) GatsModCore.stopFollowingPlayer(true);
if (GatsModCore.SETTINGS.zeroAIEnabled) this.updateSimulatedKeys([]);
if (this.isAutoShooting) this.stopShooting();
this.currentAimAssistTargetCoords = null;
this.currentAimbotTarget = null;
this.lastSelfKills = 0;
this.overlayCtx?.clearRect(0, 0, this.overlayCanvas.width, this.overlayCanvas.height);
return;
}
if (!this.overlayCtx || !camera?.ctx) return;
this.overlayCtx.clearRect(0, 0, this.overlayCanvas.width, this.overlayCanvas.height);
this.performShotgunAutoReload(me);
this.handlePlayerChatUpdates();
if (GatsModCore.SETTINGS.ghostDetectEnabled) this.performGhostDetection();
if (GatsModCore.SETTINGS.silencerDetectEnabled) this.performSilencerDetection();
if (GatsModCore.SETTINGS.onKillChatEnabled) this.checkOnKillEvent(me);
if (GatsModCore.SETTINGS.zeroAIEnabled) {
this.performZeroAIActions(me);
} else if (GatsModCore.SETTINGS.followBotEnabled && this.isFollowingPlayer) {
this.performFollowBotActions(me);
} else if (this.isFollowingPlayer) {
GatsModCore.stopFollowingPlayer(true);
} else if (Object.values(this.simulatedKeys).some(s => s)) {
this.updateSimulatedKeys([]);
}
const mouseClicked = this.realMouseButtons > 0;
const aimbotActive = GatsModCore.SETTINGS.aimbotEnabled && (GatsModCore.SETTINGS.alwaysAim || (GatsModCore.SETTINGS.aimbotOnMousePress && mouseClicked) || GatsModCore.SETTINGS.zeroAIEnabled);
const spinbotActive = GatsModCore.SETTINGS.spinbotEnabled && !mouseClicked && !GatsModCore.SETTINGS.zeroAIEnabled;
this.currentAimAssistTargetCoords = null;
this.currentAimbotTarget = null;
if (spinbotActive) this.performSpinbotActions(me);
if (aimbotActive) this.performAimbotTargeting(me);
const autoAttackActive = GatsModCore.SETTINGS.autoAttackEnabled && (mouseClicked || GatsModCore.SETTINGS.zeroAIEnabled);
if (autoAttackActive) {
this.performAutoAttack(me);
} else if (this.isAutoShooting) {
this.stopShooting();
}
if (aimbotActive && this.aimTargetScreenCoords) {
this.currentAimAssistTargetCoords = this.aimTargetScreenCoords;
} else if (spinbotActive && this.spinbotTargetScreenCoords) {
this.currentAimAssistTargetCoords = this.spinbotTargetScreenCoords;
}
if (this.currentAimAssistTargetCoords && this.originalUpdateMouseData) {
let clientX = this.currentAimAssistTargetCoords.x;
let clientY = this.currentAimAssistTargetCoords.y;
if (this.gameCanvas) {
const rect = this.gameCanvas.getBoundingClientRect();
clientX += rect.left;
clientY += rect.top;
}
const buttons = GatsModCore.SETTINGS.zeroAIEnabled ? 1 : this.realMouseButtons;
const fakeEvent = { clientX: clientX, clientY: clientY, target: this.gameCanvas, buttons: buttons };
this.originalUpdateMouseData(fakeEvent);
}
if (GatsModCore.SETTINGS.espEnabled) {
this.drawESP(this.overlayCtx, me, !!this.currentAimAssistTargetCoords);
}
if (this.tickCounter % 30 === 0) {
this.simpleGui?.updateStatusDisplay();
this.simpleGui?.updateFollowBotStatusDisplay();
this.multibox?.updateGUI();
}
}
checkOnKillEvent(me) {
if (!me) return;
if (this.lastSelfKills === null || this.lastSelfKills === undefined) {
this.lastSelfKills = me.kills;
return;
}
if (me.kills > this.lastSelfKills) {
modLog("Kill detected, sending on-kill message.");
GatsModCore.sendChatMessage(GatsModCore.SETTINGS.onKillMessage);
}
this.lastSelfKills = me.kills;
}
performGhostDetection() {
try {
if (!Player || !Player.pool) return;
for (const id in Player.pool) {
const p = Player.pool[id];
if (p && p.ghillie) p.ghillie = false;
}
} catch (e) {}
}
performSilencerDetection() {
try {
if (!Bullet || !Bullet.pool) return;
for (const id in Bullet.pool) {
const b = Bullet.pool[id];
if (b && b.silenced) b.silenced = false;
}
} catch (e) {}
}
startShooting() {
if (this.isAutoShooting) return;
try {
Connection.list[0].send(prepareMessage('key-press', { 'inputId': 6, 'state': 1 }));
this.isAutoShooting = true;
} catch (e) {
modLog("Failed to send start shooting command.", true);
}
}
stopShooting() {
if (!this.isAutoShooting) return;
try {
Connection.list[0].send(prepareMessage('key-press', { 'inputId': 6, 'state': 0 }));
this.isAutoShooting = false;
} catch (e) {
modLog("Failed to send stop shooting command.", true);
}
}
getBulletOrigin(player) {
let originX = player.x;
let originY = player.y;
const weaponClass = player.class || 'pistol';
const forward = this.constructor.WEAPON_FORWARD_OFFSETS[weaponClass] || 45;
const sideways = -18;
const angleRad = (player.playerAngle || 0) * Math.PI / 180;
originX += forward * Math.cos(angleRad) + sideways * Math.cos(angleRad + Math.PI / 2);
originY += forward * Math.sin(angleRad) + sideways * Math.sin(angleRad + Math.PI / 2);
return { x: originX, y: originY };
}
hasLineOfSight(p1, p2) {
if (typeof MapObject === 'undefined' || !MapObject.pool) return true;
for (const id in MapObject.pool) {
const obs = MapObject.pool[id];
if (obs?.activated && (obs.type === 'crate' || obs.type === 'longCrate' || obs.type === 'userCrate')) {
if (this.isLineIntersectingRotatedRect(p1, p2, obs)) {
return false;
}
}
}
return true;
}
isLineIntersectingRotatedRect(p1, p2, rect) {
const settings = GatsModCore.SETTINGS;
const angle = -(rect.angle || 0) * Math.PI / 180;
const cos = Math.cos(angle), sin = Math.sin(angle);
const cx = rect.x + settings.obstacleOffsetX;
const cy = rect.y + settings.obstacleOffsetY;
const p1r = { x: cos * (p1.x - cx) - sin * (p1.y - cy), y: sin * (p1.x - cx) + cos * (p1.y - cy) };
const p2r = { x: cos * (p2.x - cx) - sin * (p2.y - cy), y: sin * (p2.x - cx) + cos * (p2.y - cy) };
let w = rect.width, h = rect.height;
if (rect.type === 'crate') {
w = 100;
h = 100;
} else if (rect.type === 'userCrate') {
w = 40;
h = 40;
}
const halfW = w / 2, halfH = h / 2;
const rectMin = { x: -halfW, y: -halfH };
const rectMax = { x: halfW, y: halfH };
const dx = p2r.x - p1r.x;
const dy = p2r.y - p1r.y;
let t0 = 0, t1 = 1;
const p = [-dx, dx, -dy, dy];
const q = [p1r.x - rectMin.x, rectMax.x - p1r.x, p1r.y - rectMin.y, rectMax.y - p1r.y];
for (let i = 0; i < 4; i++) {
if (p[i] === 0) {
if (q[i] < 0) return true;
} else {
const t = q[i] / p[i];
if (p[i] < 0) {
if (t > t1) return false;
t0 = Math.max(t0, t);
} else {
if (t < t0) return false;
t1 = Math.min(t1, t);
}
}
}
return t0 < t1;
}
getMyWeaponRange(player) {
if (!player) return 0;
const weaponClass = player.class || 'pistol';
let baseRange = GatsModCore.WEAPON_BASE_RANGES[weaponClass] || 425;
if (player.levelUpgrades) {
const hasLongRange = Object.values(player.levelUpgrades).includes('longRange');
if (hasLongRange) {
baseRange *= GatsModCore.LONG_RANGE_MULTIPLIER;
}
}
return baseRange;
}
getEstimatedEnemyRange(enemyPlayer) {
if (!enemyPlayer) return 0;
const weaponClass = enemyPlayer.class || 'pistol';
return GatsModCore.WEAPON_BASE_RANGES[weaponClass] || 425;
}
performAutoAttack(me) {
if (!this.currentAimbotTarget || !this.predictedTargetWorld.x) {
this.stopShooting();
return;
}
const screenX = this.aimTargetScreenCoords.x;
const screenY = this.aimTargetScreenCoords.y;
const canvasCenterX = this.gameCanvas.width / 2;
const canvasCenterY = this.gameCanvas.height / 2;
const distFromCenterSq = (screenX - canvasCenterX) ** 2 + (screenY - canvasCenterY) ** 2;
if (distFromCenterSq > GatsModCore.SETTINGS.autoAttackRadius ** 2) {
this.stopShooting();
return;
}
const bulletOrigin = this.getBulletOrigin(me);
if (GatsModCore.SETTINGS.autoAttackCheckLOS) {
if (!this.hasLineOfSight(bulletOrigin, this.predictedTargetWorld)) {
this.stopShooting();
return;
}
}
if (GatsModCore.SETTINGS.autoAttackCheckRange) {
const maxRange = this.getMyWeaponRange(me);
const targetDistance = getDistance(bulletOrigin, this.predictedTargetWorld);
if (targetDistance > maxRange) {
this.stopShooting();
return;
}
}
this.startShooting();
}
calculatePredictedPosition(p, me) {
const settings = GatsModCore.SETTINGS;
if (!settings.predictionEnabled || p.spdX === undefined || p.spdY === undefined) {
return { x: p.x, y: p.y };
}
const { x: shotOriginX_world, y: shotOriginY_world } = this.getBulletOrigin(me);
let basePredictionFactor = settings.predictionFactor;
if (settings.useCloseRangePrediction) {
const canvasCenterX = this.gameCanvas.width / 2;
const canvasCenterY = this.gameCanvas.height / 2;
const screenPlayerX = canvasCenterX + (p.x - me.x);
const screenPlayerY = canvasCenterY + (p.y - me.y);
const distSqFromCenter = (screenPlayerX - canvasCenterX) ** 2 + (screenPlayerY - canvasCenterY) ** 2;
if (distSqFromCenter < settings.predictionCloseRadius ** 2) {
basePredictionFactor = settings.predictionFactorClose;
}
}
const currentWeaponClass = me.class || 'pistol';
let bulletSpeed = GatsModCore.WEAPON_BULLET_SPEEDS[currentWeaponClass] || 9.0;
bulletSpeed = Math.max(0.1, bulletSpeed);
let timeToHit = 0;
let futureX_intermediate = p.x;
let futureY_intermediate = p.y;
for (let i = 0; i < 2; i++) {
const distanceToFuturePos = getDistance({ x: futureX_intermediate, y: futureY_intermediate }, { x: shotOriginX_world, y: shotOriginY_world });
timeToHit = distanceToFuturePos < 1 ? 0 : distanceToFuturePos / bulletSpeed;
timeToHit = Math.min(timeToHit, 5);
futureX_intermediate = p.x + (p.spdX * timeToHit);
futureY_intermediate = p.y + (p.spdY * timeToHit);
}
const baseDisplacementX = p.spdX * timeToHit;
const baseDisplacementY = p.spdY * timeToHit;
let actualPredictionFactor = basePredictionFactor;
if (basePredictionFactor === settings.predictionFactor && settings.enableDynamicPredictionFactor) {
const distanceToEnemy = getDistance(p, { x: shotOriginX_world, y: shotOriginY_world });
const { predictionFactorAtMinDistance: minF, predictionFactor: maxF, minPredictionDistance: minD, maxPredictionDistance: maxD } = settings;
if (distanceToEnemy <= minD) {
actualPredictionFactor = minF;
} else if (distanceToEnemy >= maxD) {
actualPredictionFactor = maxF;
} else if (maxD > minD) {
const ratio = (distanceToEnemy - minD) / (maxD - minD);
actualPredictionFactor = minF + (maxF - minF) * ratio;
} else {
actualPredictionFactor = maxF;
}
actualPredictionFactor = Math.max(0, actualPredictionFactor);
}
if (p.dashing) {
actualPredictionFactor /= 3;
}
const worldTargetX = p.x + (baseDisplacementX * actualPredictionFactor);
const worldTargetY = p.y + (baseDisplacementY * actualPredictionFactor);
return { x: worldTargetX, y: worldTargetY };
}
findBestShootingPoint(shooter, target) {
const shooterOrigin = this.getBulletOrigin(shooter);
const predictedCenter = this.calculatePredictedPosition(target, shooter);
if (this.hasLineOfSight(shooterOrigin, predictedCenter)) {
return predictedCenter;
}
const radius = 24;
const angles = [
0, Math.PI / 2, Math.PI, 3 * Math.PI / 2,
Math.PI / 4, 3 * Math.PI / 4, 5 * Math.PI / 4, 7 * Math.PI / 4
];
for (const angle of angles) {
const checkPoint = {
x: predictedCenter.x + radius * Math.cos(angle),
y: predictedCenter.y + radius * Math.sin(angle)
};
if (this.hasLineOfSight(shooterOrigin, checkPoint)) {
return checkPoint;
}
}
return null;
}
performAimbotTargeting(me) {
if (!this.gameCanvas) {
this.aimTargetScreenCoords = null;
this.currentAimbotTarget = null;
return;
}
const finalScale = (typeof unsafeWindow !== 'undefined' && unsafeWindow.widthScaleFactor > 0) ? unsafeWindow.widthScaleFactor : 1.0;
const settings = GatsModCore.SETTINGS;
const canvasCenterX = this.gameCanvas.width / 2;
const canvasCenterY = this.gameCanvas.height / 2;
let targetCandidate = null;
let finalTargetPlayerObject = null;
let refX = settings.aimAtMouseClosest ? this.realMouseCanvasX : canvasCenterX;
let refY = settings.aimAtMouseClosest ? this.realMouseCanvasY : canvasCenterY;
let closestPlayerDistSq = settings.aimbotFov ** 2;
for (const id in Player.pool) {
const p = Player.pool[id];
if (!p?.activated || p.hp <= 0 || id == selfId || (me.teamCode !== 0 && p.teamCode === me.teamCode) || settings.aimbotIgnoreList?.includes(p.username)) continue;
if (settings.followBotEnabled && this.isFollowingPlayer && getDistance(p, me) > settings.followBotAttackRadius) continue;
const bestShootingPoint = this.findBestShootingPoint(me, p);
if (bestShootingPoint) {
const { x: worldTargetX, y: worldTargetY } = bestShootingPoint;
const screenTargetX = canvasCenterX + (worldTargetX - me.x) * finalScale;
const screenTargetY = canvasCenterY + (worldTargetY - me.y) * finalScale;
const distToRefSq = (screenTargetX - refX) ** 2 + (screenTargetY - refY) ** 2;
if (distToRefSq < closestPlayerDistSq) {
closestPlayerDistSq = distToRefSq;
targetCandidate = { x_world: worldTargetX, y_world: worldTargetY };
finalTargetPlayerObject = p;
}
}
}
if (targetCandidate) {
this.aimTargetScreenCoords = {
x: canvasCenterX + (targetCandidate.x_world - me.x) * finalScale + settings.espOffsetX,
y: canvasCenterY + (targetCandidate.y_world - me.y) * finalScale + settings.espOffsetY
};
this.currentAimbotTarget = finalTargetPlayerObject;
this.predictedTargetWorld = { x: targetCandidate.x_world, y: targetCandidate.y_world };
} else {
this.aimTargetScreenCoords = null;
this.currentAimbotTarget = null;
this.predictedTargetWorld = { x: 0, y: 0 };
}
}
drawESP(ctx, me, hasTarget) {
const finalScale = (typeof unsafeWindow !== 'undefined' && unsafeWindow.widthScaleFactor > 0) ? unsafeWindow.widthScaleFactor : 1.0;
const sizeMultiplier = GatsModCore.SETTINGS.espScale || 1.0;
const { espColors, espOffsetX, espOffsetY, obstacleOffsetX, obstacleOffsetY, obstacleEspColor } = GatsModCore.SETTINGS;
if (!this.gameCanvas || !camera?.ctx) return;
try {
if (typeof landMine !== 'undefined' && Array.isArray(landMine) && landMine[0]) {
landMine[0].forEach((a, i) => {
if (landMine[0][i] && landMine[0][i][1]) {
landMine[0][i][1][3] = "#000000";
}
});
}
} catch (e) {}
const canvasCenterX = this.gameCanvas.width / 2, canvasCenterY = this.gameCanvas.height / 2;
ctx.save();
ctx.textAlign = 'center';
ctx.font = 'bold 10px Arial';
if (GatsModCore.SETTINGS.autoAttackShowRadius) {
ctx.beginPath();
ctx.arc(canvasCenterX, canvasCenterY, GatsModCore.SETTINGS.autoAttackRadius, 0, 2 * Math.PI);
ctx.strokeStyle = 'rgba(255, 255, 0, 0.5)';
ctx.lineWidth = 1;
ctx.stroke();
}
if (GatsModCore.SETTINGS.obstacleEspEnabled && MapObject && MapObject.pool) {
ctx.strokeStyle = obstacleEspColor;
ctx.lineWidth = 2;
for (const id in MapObject.pool) {
const obj = MapObject.pool[id];
if (!obj || !obj.activated) continue;
let width = obj.width || 50;
let height = obj.height || 50;
if (obj.type === 'crate') { width = 100; height = 100; }
if (obj.type === 'userCrate') { width = 40; height = 40; }
const objX = obj.x + obstacleOffsetX;
const objY = obj.y + obstacleOffsetY;
const screenX = canvasCenterX + (objX - me.x) * finalScale + espOffsetX;
const screenY = canvasCenterY + (objY - me.y) * finalScale + espOffsetY;
ctx.save();
ctx.translate(screenX, screenY);
ctx.rotate((obj.angle || 0) * Math.PI / 180);
ctx.strokeRect((-width / 2) * finalScale, (-height / 2) * finalScale, width * finalScale, height * finalScale);
ctx.restore();
}
}
if (GatsModCore.SETTINGS.losDebugLineEnabled && this.currentAimbotTarget) {
const startPoint = this.getBulletOrigin(me);
const endPoint = this.predictedTargetWorld;
if (startPoint && endPoint.x) {
const isClear = this.hasLineOfSight(startPoint, endPoint);
ctx.strokeStyle = isClear ? 'rgba(0, 255, 0, 0.7)' : 'rgba(255, 0, 0, 0.7)';
ctx.lineWidth = 3;
const startScreenX = canvasCenterX + (startPoint.x - me.x) * finalScale + espOffsetX;
const startScreenY = canvasCenterY + (startPoint.y - me.y) * finalScale + espOffsetY;
const endScreenX = canvasCenterX + (endPoint.x - me.x) * finalScale + espOffsetX;
const endScreenY = canvasCenterY + (endPoint.y - me.y) * finalScale + espOffsetY;
ctx.beginPath();
ctx.moveTo(startScreenX, startScreenY);
ctx.lineTo(endScreenX, endScreenY);
ctx.stroke();
}
}
for (const id in Player.pool) {
const p = Player.pool[id];
if (!p?.activated || p.hp <= 0 || id == selfId) continue;
const isIgnored = GatsModCore.SETTINGS.aimbotIgnoreList.includes(p.username);
let isTeammate = (me.teamCode !== 0 && p.teamCode === me.teamCode) || isIgnored;
if (isTeammate && !GatsModCore.SETTINGS.espShowTeammates && !isIgnored) continue;
const screenX = canvasCenterX + (p.x - me.x) * finalScale + espOffsetX;
const screenY = canvasCenterY + (p.y - me.y) * finalScale + espOffsetY;
const radiusOnScreen = (p.radius || 15) * finalScale * sizeMultiplier;
let boxColor = isTeammate ? espColors.teammateEspColor : espColors.enemyEspColor;
if (!isTeammate && GatsModCore.SETTINGS.espHighlightLowHP && p.hp < GatsModCore.SETTINGS.lowHPThreshold) {
boxColor = espColors.lowHpEnemyEspColor;
}
ctx.strokeStyle = boxColor;
ctx.lineWidth = 1.5;
ctx.strokeRect(screenX - radiusOnScreen, screenY - radiusOnScreen, radiusOnScreen * 2, radiusOnScreen * 2);
ctx.beginPath();
ctx.moveTo(canvasCenterX + espOffsetX, canvasCenterY + espOffsetY);
ctx.lineTo(screenX, screenY);
ctx.stroke();
let nameYOffset = screenY - radiusOnScreen - 15;
if (this.isExclusionModeActive && !isTeammate) {
ctx.strokeStyle = 'rgba(255, 255, 255, 0.7)';
ctx.lineWidth = 2;
ctx.setLineDash([4, 4]);
ctx.beginPath();
const clickRadiusOnScreen = 24 * finalScale * sizeMultiplier;
ctx.arc(screenX, screenY, clickRadiusOnScreen, 0, 2 * Math.PI);
ctx.stroke();
ctx.setLineDash([]);
}
if (GatsModCore.SETTINGS.espHighlightCloaked && p.ghillie && !isTeammate) {
ctx.font = 'bold 12px Arial';
ctx.fillStyle = espColors.cloakedTextColor;
ctx.fillText('CLOAKED', screenX, nameYOffset);
nameYOffset -= 14;
}
if (p.username) {
ctx.font = 'bold 10px Arial';
ctx.fillStyle = isTeammate ? espColors.teammateNameColor : espColors.enemyNameColor;
ctx.fillText(p.username, screenX, nameYOffset);
}
if (GatsModCore.SETTINGS.espShowHP) {
const hpPercent = p.hp / (p.hpMax || 100);
const barW = radiusOnScreen * 1.8, barH = 4;
const barX = screenX - barW / 2, barY = screenY + radiusOnScreen + 4;
ctx.fillStyle = 'rgba(0,0,0,0.5)';
ctx.fillRect(barX, barY, barW, barH);
ctx.fillStyle = hpPercent > 0.6 ? espColors.hpBarHighColor : hpPercent > 0.3 ? espColors.hpBarMediumColor : espColors.hpBarLowColor;
ctx.fillRect(barX, barY, barW * hpPercent, barH);
}
if (!isTeammate && GatsModCore.SETTINGS.espShowPrediction && GatsModCore.SETTINGS.predictionEnabled) {
const predictedPos = this.calculatePredictedPosition(p, me);
if (predictedPos) {
const predScreenX = canvasCenterX + (predictedPos.x - me.x) * finalScale + espOffsetX;
const predScreenY = canvasCenterY + (predictedPos.y - me.y) * finalScale + espOffsetY;
ctx.strokeStyle = espColors.predictionLineColor || '#FF00FF';
ctx.lineWidth = 1;
ctx.setLineDash([5, 3]);
ctx.beginPath();
ctx.moveTo(screenX, screenY);
ctx.lineTo(predScreenX, predScreenY);
ctx.stroke();
ctx.setLineDash([]);
ctx.beginPath();
ctx.arc(predScreenX, predScreenY, 3, 0, 2 * Math.PI);
ctx.fillStyle = espColors.predictionLineColor || '#FF00FF';
ctx.fill();
}
}
if (GatsModCore.SETTINGS.espShowFacingLine && p.playerAngle !== undefined) {
const angleRad = p.playerAngle * Math.PI / 180;
const lineLen = radiusOnScreen * 1.2;
ctx.beginPath();
ctx.moveTo(screenX, screenY);
ctx.lineTo(screenX + lineLen * Math.cos(angleRad), screenY + lineLen * Math.sin(angleRad));
ctx.strokeStyle = espColors.facingLineColor;
ctx.lineWidth = 2;
ctx.stroke();
}
}
if (hasTarget && this.currentAimAssistTargetCoords) {
const { x, y } = this.currentAimAssistTargetCoords;
const originWorld = this.getBulletOrigin(me);
const originX = canvasCenterX + (originWorld.x - me.x) * finalScale + espOffsetX;
const originY = canvasCenterY + (originWorld.y - me.y) * finalScale + espOffsetY;
ctx.strokeStyle = espColors.aimbotTargetLineColor;
ctx.lineWidth = 1.0;
ctx.beginPath();
ctx.moveTo(originX, originY);
ctx.lineTo(x, y);
ctx.stroke();
ctx.beginPath();
ctx.arc(x, y, 15, 0, 2 * Math.PI);
ctx.stroke();
}
if (GatsModCore.SETTINGS.espShowBulletWarnings && typeof Bullet !== 'undefined' && Bullet.pool) {
ctx.strokeStyle = espColors.aiBulletWarningColor || '#FF00FF';
ctx.fillStyle = (espColors.aiBulletWarningColor || '#FF00FF') + '33';
ctx.lineWidth = 1;
const dodgeRadiusHorizontal = 150;
const dodgeRadiusVertical = 15;
for (const id in Bullet.pool) {
const b = Bullet.pool[id];
const owner = Player.pool[b.ownerId];
if (!b || b.ownerId === selfId || (me.teamCode !== 0 && b.teamCode === me.teamCode) || (owner && GatsModCore.SETTINGS.aimbotIgnoreList.includes(owner.username))) continue;
const screenBulletX = canvasCenterX + (b.x - me.x) * finalScale + espOffsetX;
const screenBulletY = canvasCenterY + (b.y - me.y) * finalScale + espOffsetY;
const radiusX = dodgeRadiusHorizontal * finalScale * sizeMultiplier;
const radiusY = dodgeRadiusVertical * finalScale * sizeMultiplier;
const rotation = (b.spdX === 0 && b.spdY === 0) ? 0 : Math.atan2(b.spdY, b.spdX);
ctx.beginPath();
ctx.ellipse(screenBulletX, screenBulletY, radiusX, radiusY, rotation, 0, 2 * Math.PI);
ctx.stroke();
ctx.fill();
}
}
if (GatsModCore.SETTINGS.zeroAIEnabled) {
const { aiShowPathfindingWhiskers, aiShowFinalMoveDirection, aiShowEnemyThreatLine } = GatsModCore.SETTINGS;
const { whiskers, finalMoveDirection } = this.aiDebugData;
if (aiShowPathfindingWhiskers && whiskers.length > 0) {
whiskers.forEach(whisker => {
const startScreenX = canvasCenterX + (whisker.start.x - me.x) * finalScale + espOffsetX;
const startScreenY = canvasCenterY + (whisker.start.y - me.y) * finalScale + espOffsetY;
const endScreenX = canvasCenterX + (whisker.end.x - me.x) * finalScale + espOffsetX;
const endScreenY = canvasCenterY + (whisker.end.y - me.y) * finalScale + espOffsetY;
ctx.beginPath();
ctx.moveTo(startScreenX, startScreenY);
ctx.lineTo(endScreenX, endScreenY);
ctx.strokeStyle = whisker.isClear ? espColors.aiWhiskerClearColor : espColors.aiWhiskerBlockedColor;
ctx.lineWidth = whisker.isBest ? 3 : 1;
ctx.stroke();
});
}
if (aiShowFinalMoveDirection && finalMoveDirection) {
const startScreenX = canvasCenterX + (finalMoveDirection.start.x - me.x) * finalScale + espOffsetX;
const startScreenY = canvasCenterY + (finalMoveDirection.start.y - me.y) * finalScale + espOffsetY;
const endScreenX = canvasCenterX + (finalMoveDirection.end.x - me.x) * finalScale + espOffsetX;
const endScreenY = canvasCenterY + (finalMoveDirection.end.y - me.y) * finalScale + espOffsetY;
ctx.beginPath();
ctx.moveTo(startScreenX, startScreenY);
ctx.lineTo(endScreenX, endScreenY);
ctx.strokeStyle = espColors.aiMoveDirColor;
ctx.lineWidth = 4;
ctx.stroke();
const headlen = 10;
const angle = Math.atan2(endScreenY - startScreenY, endScreenX - startScreenX);
ctx.lineTo(endScreenX - headlen * Math.cos(angle - Math.PI / 6), endScreenY - headlen * Math.sin(angle - Math.PI / 6));
ctx.moveTo(endScreenX, endScreenY);
ctx.lineTo(endScreenX - headlen * Math.cos(angle + Math.PI / 6), endScreenY - headlen * Math.sin(angle + Math.PI / 6));
ctx.stroke();
}
if (aiShowEnemyThreatLine) {
const nearestEnemy = this.findNearestEnemy(me);
if (nearestEnemy) {
const enemyGunOrigin = this.getBulletOrigin(nearestEnemy);
const isClear = this.hasLineOfSight(enemyGunOrigin, me);
ctx.strokeStyle = isClear ? espColors.aiThreatLineClearColor : espColors.aiThreatLineBlockedColor;
ctx.lineWidth = 2;
ctx.setLineDash([10, 5]);
const startScreenX = canvasCenterX + (enemyGunOrigin.x - me.x) * finalScale;
const startScreenY = canvasCenterY + (enemyGunOrigin.y - me.y) * finalScale;
const endScreenX = canvasCenterX;
const endScreenY = canvasCenterY;
ctx.beginPath();
ctx.moveTo(startScreenX, startScreenY);
ctx.lineTo(endScreenX, endScreenY);
ctx.stroke();
ctx.setLineDash([]);
}
}
}
ctx.restore();
}
performSpinbotActions(me) {
if (!GatsModCore.SETTINGS.spinbotEnabled) {
this.spinbotTargetScreenCoords = null;
return;
}
const now = performance.now();
if (now - this.lastSpinTime > GatsModCore.SETTINGS.spinbotSpeedMs) {
this.spinbotCurrentTargetIndex = (this.spinbotCurrentTargetIndex + 1) % 3;
this.lastSpinTime = now;
}
if (!this.gameCanvas) return;
const canvasCenterX = this.gameCanvas.width / 2, canvasCenterY = this.gameCanvas.height / 2;
const spinDist = GatsModCore.SETTINGS.spinbotDistance;
let worldTargetX, worldTargetY;
switch (this.spinbotCurrentTargetIndex) {
case 0: worldTargetX = me.x; worldTargetY = me.y - spinDist; break;
case 1: worldTargetX = me.x + spinDist; worldTargetY = me.y + spinDist; break;
case 2: worldTargetX = me.x - spinDist; worldTargetY = me.y + spinDist; break;
default: worldTargetX = me.x; worldTargetY = me.y;
}
this.spinbotTargetScreenCoords = { x: canvasCenterX + (worldTargetX - me.x), y: canvasCenterY + (worldTargetY - me.y) };
}
getBotPhysics(player) {
const weapon = player.class || 'pistol';
const armor = player.armor || 0;
const upgrades = player.levelUpgrades ? Object.values(player.levelUpgrades) : [];
const baseSpeed = GatsModCore.PLAYER_SPEEDS.base[weapon] || GatsModCore.PLAYER_SPEEDS.base['pistol'];
const armorMod = GatsModCore.PLAYER_SPEEDS.armorMultiplier[armor] || 1.0;
const upgradeMod = upgrades.includes('lightweight') ? GatsModCore.PLAYER_SPEEDS.upgradeMultiplier['lightweight'] : 1.0;
const maxSpeed = baseSpeed * armorMod * upgradeMod;
const ACCELERATION = maxSpeed / 16.0;
const FRICTION = 0.94;
return { maxSpeed, ACCELERATION, FRICTION };
}
performFollowBotActions(me) {
if (!this.isFollowingPlayer || this.followingPlayerId === null) {
GatsModCore.stopFollowingPlayer(true);
return;
}
const targetPlayer = Player.pool[this.followingPlayerId];
if (!targetPlayer?.activated || targetPlayer.hp <= 0) {
GatsModCore.stopFollowingPlayer();
return;
}
const { maxSpeed, ACCELERATION, FRICTION } = this.getBotPhysics(me);
const dx = targetPlayer.x - me.x;
const dy = targetPlayer.y - me.y;
const distance = Math.hypot(dx, dy);
let desiredSpdX = 0;
let desiredSpdY = 0;
const stopDistance = 15;
if (distance > stopDistance) {
const brakingFactor = 12.0;
const desiredSpeed = Math.min(maxSpeed, distance / brakingFactor);
desiredSpdX = (dx / distance) * desiredSpeed;
desiredSpdY = (dy / distance) * desiredSpeed;
}
const requiredAccelX = (desiredSpdX / FRICTION) - this.botSpdX;
const requiredAccelY = (desiredSpdY / FRICTION) - this.botSpdY;
let keysToPress = [];
let isPressingKeyX = 0;
let isPressingKeyY = 0;
const keyPressThreshold = ACCELERATION * 0.1;
if (requiredAccelX > keyPressThreshold) {
keysToPress.push('d');
isPressingKeyX = 1;
} else if (requiredAccelX < -keyPressThreshold) {
keysToPress.push('a');
isPressingKeyX = -1;
}
if (requiredAccelY > keyPressThreshold) {
keysToPress.push('s');
isPressingKeyY = 1;
} else if (requiredAccelY < -keyPressThreshold) {
keysToPress.push('w');
isPressingKeyY = -1;
}
this.updateSimulatedKeys(keysToPress);
let appliedAccelX = isPressingKeyX * ACCELERATION;
let appliedAccelY = isPressingKeyY * ACCELERATION;
if (isPressingKeyX !== 0 && isPressingKeyY !== 0) {
appliedAccelX *= GatsModCore.PLAYER_SPEEDS.diagonalCorrection;
appliedAccelY *= GatsModCore.PLAYER_SPEEDS.diagonalCorrection;
}
this.botSpdX = (this.botSpdX + appliedAccelX) * FRICTION;
this.botSpdY = (this.botSpdY + appliedAccelY) * FRICTION;
const currentSimulatedSpeed = Math.hypot(this.botSpdX, this.botSpdY);
if (currentSimulatedSpeed > maxSpeed) {
const ratio = maxSpeed / currentSimulatedSpeed;
this.botSpdX *= ratio;
this.botSpdY *= ratio;
}
}
updateSimulatedKeys(keysToPress) {
const allKeys = ['w', 'a', 's', 'd'];
allKeys.forEach(key => {
const shouldBePressed = keysToPress.includes(key);
if (shouldBePressed !== this.simulatedKeys[key]) {
this._fireKeyEvent(shouldBePressed ? 'keydown' : 'keyup', key);
this.simulatedKeys[key] = shouldBePressed;
}
});
}
_fireKeyEvent(type, key) {
const keyMap = { 'w': 87, 'a': 65, 's': 83, 'd': 68, ' ': 32, 'r': 82 };
const codeMap = { 'w': 'KeyW', 'a': 'KeyA', 's': 'KeyS', 'd': 'KeyD', ' ': 'Space', 'r': 'KeyR' };
if (!keyMap[key]) return;
document.dispatchEvent(new KeyboardEvent(type, { key: key, code: codeMap[key], keyCode: keyMap[key], bubbles: true, cancelable: true, composed: true }));
}
startFollowingPlayer() {
const targetName = GatsModCore.SETTINGS.followBotTargetName;
if (!targetName) {
alert("Set a target name first using the 'Follow Bot Control' panel.");
return;
}
const foundPlayer = Object.values(Player.pool).find(p => p.username === targetName && p.activated && p.id !== selfId);
if (foundPlayer) {
this.isFollowingPlayer = true;
this.followingPlayerId = foundPlayer.id;
modLog(`Started following ${targetName}`);
} else {
alert(`Player "${targetName}" not found or is not active.`);
this.isFollowingPlayer = false;
this.followingPlayerId = null;
}
this.simpleGui.updateFollowBotStatusDisplay();
}
handleClickToIgnore(event) {
const me = Player.pool?.[selfId];
if (!this.isExclusionModeActive || !Player.pool || !me) return;
const rect = this.gameCanvas.getBoundingClientRect();
const clickX = event.clientX - rect.left;
const clickY = event.clientY - rect.top;
const settings = GatsModCore.SETTINGS;
const canvasCenterX = this.gameCanvas.width / 2;
const canvasCenterY = this.gameCanvas.height / 2;
let clickedPlayer = null;
let closestDistSq = Infinity;
for (const id in Player.pool) {
const p = Player.pool[id];
if (!p?.activated || p.hp <= 0 || id == selfId || (me.teamCode !== 0 && p.teamCode === me.teamCode)) continue;
const relX = (p.x - me.x) / settings.espScale;
const relY = (p.y - me.y) / settings.espScale;
const screenX = canvasCenterX + relX + settings.espOffsetX;
const screenY = canvasCenterY + relY + settings.espOffsetY;
const distSq = (clickX - screenX) ** 2 + (clickY - screenY) ** 2;
const clickRadius = 24 / settings.espScale;
if (distSq < clickRadius * clickRadius && distSq < closestDistSq) {
closestDistSq = distSq;
clickedPlayer = p;
}
}
if (clickedPlayer) {
this.addPlayerToIgnoreList(clickedPlayer.username);
}
}
addPlayerToIgnoreList(username, silent = false) {
if (!username) return;
const settings = GatsModCore.SETTINGS;
if (!settings.aimbotIgnoreList.includes(username)) {
settings.aimbotIgnoreList.push(username);
GatsModCore.saveSettings();
this.simpleGui?.updateAimbotExclusionListDisplay();
if (!silent) modLog(`Player "${username}" added to ignore list.`);
} else {
if (silent) return;
GatsModCore.SETTINGS.aimbotIgnoreList = GatsModCore.SETTINGS.aimbotIgnoreList.filter(n => n !== username);
GatsModCore.saveSettings();
this.simpleGui.updateAimbotExclusionListDisplay();
modLog(`Player "${username}" removed from ignore list.`);
}
}
static saveSettings() {
try {
localStorage.setItem(SETTINGS_KEY, JSON.stringify(GatsModCore.SETTINGS));
} catch (e) {
modLog("Error saving settings: " + e.message, true);
}
}
static saveProfile(name) {
if (!name) {
alert("Please enter a profile name.");
return;
}
if (!GatsModCore.SETTINGS.settingProfiles) {
GatsModCore.SETTINGS.settingProfiles = {};
}
const settingsToSave = { ...GatsModCore.SETTINGS };
delete settingsToSave.settingProfiles;
GatsModCore.SETTINGS.settingProfiles[name] = JSON.stringify(settingsToSave);
GatsModCore.saveSettings();
gatsModInstance?.simpleGui?.updateProfileList();
alert(`Profile "${name}" saved.`);
}
static loadProfile(name) {
if (!name) {
alert("Select a profile to load.");
return;
}
const profileDataString = GatsModCore.SETTINGS.settingProfiles?.[name];
if (!profileDataString) {
alert(`Profile "${name}" not found.`);
return;
}
try {
const loadedProfileSettings = JSON.parse(profileDataString);
const preservedProfiles = GatsModCore.SETTINGS.settingProfiles;
const preservedEspColors = GatsModCore.SETTINGS.espColors;
GatsModCore.SETTINGS = { ...GatsModCore.SETTINGS, ...loadedProfileSettings };
GatsModCore.SETTINGS.settingProfiles = preservedProfiles;
GatsModCore.SETTINGS.espColors = GatsModCore.SETTINGS.espColors || preservedEspColors;
GatsModCore.saveSettings();
gatsModInstance?.simpleGui?.updateAllGUIToReflectSettings();
if (gatsModInstance?.colorGui && GatsModCore.SETTINGS.espColors) {
Object.keys(GatsModCore.SETTINGS.espColors).forEach(key => {
const picker = document.getElementById(key + '-color-v2');
if (picker) picker.value = GatsModCore.SETTINGS.espColors[key];
});
}
alert(`Profile "${name}" loaded.`);
} catch (e) {
alert("Error loading profile. It may be corrupt: " + e.message);
}
}
static deleteProfile(name) {
if (!name || !confirm(`Are you sure you want to delete the profile "${name}"?`)) return;
if (GatsModCore.SETTINGS.settingProfiles?.[name]) {
delete GatsModCore.SETTINGS.settingProfiles[name];
GatsModCore.saveSettings();
gatsModInstance?.simpleGui?.updateProfileList();
alert(`Profile "${name}" deleted.`);
}
}
static setFollowTargetName() {
const newName = prompt("Enter player name to follow:", GatsModCore.SETTINGS.followBotTargetName);
if (newName !== null) {
const oldName = GatsModCore.SETTINGS.followBotTargetName;
GatsModCore.SETTINGS.followBotTargetName = newName.trim();
GatsModCore.saveSettings();
gatsModInstance?.simpleGui?.updateFollowBotStatusDisplay();
if (gatsModInstance?.isFollowingPlayer && oldName !== newName.trim()) {
GatsModCore.stopFollowingPlayer(true);
}
alert(`Follow target set to: ${newName.trim()}`);
}
}
static stopFollowingPlayer(silent = false) {
if (!gatsModInstance) return;
if (gatsModInstance.isFollowingPlayer || Object.values(gatsModInstance.simulatedKeys).some(s => s)) {
gatsModInstance.updateSimulatedKeys([]);
}
gatsModInstance.isFollowingPlayer = false;
gatsModInstance.followingPlayerId = null;
gatsModInstance.botSpdX = 0;
gatsModInstance.botSpdY = 0;
if (!silent) modLog("FollowBot stopped.");
gatsModInstance?.simpleGui?.updateFollowBotStatusDisplay();
}
static showPlayerList() {
const modal = document.getElementById('player-list-modal');
const grid = document.getElementById('player-list-grid');
if (!modal || !grid || typeof Player === 'undefined' || !Player.pool || typeof selfId === 'undefined') {
alert("Player data not available yet.");
return;
}
grid.innerHTML = '';
Object.values(Player.pool).filter(p => p?.activated && p.id !== selfId && p.username).forEach(p => {
const btn = document.createElement('button');
btn.className = 'player-list-button';
btn.innerText = p.username;
btn.title = p.username;
btn.onclick = () => {
GatsModCore.SETTINGS.followBotTargetName = p.username;
GatsModCore.saveSettings();
gatsModInstance?.simpleGui?.updateFollowBotStatusDisplay();
gatsModInstance?.startFollowingPlayer();
modal.style.display = 'none';
};
grid.appendChild(btn);
});
modal.style.display = 'flex';
}
static startChatScroll() {
if (!GatsModCore.SETTINGS.chatScrollEnabled) {
alert("Chat Scroller is disabled in settings.");
return;
}
if (GatsModCore.SETTINGS.chatScrollActive) GatsModCore.stopChatScroll();
GatsModCore.SETTINGS.chatScrollActive = true;
GatsModCore.chatScrollCurrentIndex = 0;
gatsModInstance?.simpleGui?.updateScrollingTextDisplay(GatsModCore.SETTINGS.chatScrollText);
modLog("ChatScroller: Started.");
GatsModCore.chatScrollLoop();
}
static stopChatScroll() {
if (GatsModCore.chatScrollIntervalId) {
clearTimeout(GatsModCore.chatScrollIntervalId);
GatsModCore.chatScrollIntervalId = null;
}
GatsModCore.SETTINGS.chatScrollActive = false;
modLog("ChatScroller: Stopped.");
}
static chatScrollLoop() {
if (!GatsModCore.SETTINGS.chatScrollActive || !GatsModCore.SETTINGS.chatScrollEnabled || GatsModCore.isInputActive) {
if (!GatsModCore.SETTINGS.chatScrollEnabled || GatsModCore.isInputActive) GatsModCore.stopChatScroll();
return;
}
if (!Connection.list?.[0]?.socket?.send) {
modLog("ChatScroller: Connection not available.", true);
GatsModCore.stopChatScroll();
return;
}
const s = GatsModCore.SETTINGS.chatScrollText;
if (!s?.length) {
GatsModCore.stopChatScroll();
return;
}
let maxLength = GatsModCore.SETTINGS.chatScrollMaxLength;
if (s.length < maxLength) maxLength = s.length;
let displayText = s.substring(GatsModCore.chatScrollCurrentIndex, GatsModCore.chatScrollCurrentIndex + maxLength);
if (displayText.length < maxLength && s.length > maxLength) {
displayText += " " + s.substring(0, maxLength - displayText.length - 1);
}
displayText = displayText.trim();
GatsModCore.sendChatMessage(displayText);
GatsModCore.chatScrollCurrentIndex = (GatsModCore.chatScrollCurrentIndex + 1) % s.length;
if (GatsModCore.SETTINGS.chatScrollActive) {
GatsModCore.chatScrollIntervalId = setTimeout(GatsModCore.chatScrollLoop, GatsModCore.SETTINGS.chatScrollSpeed);
}
}
static startSequencer() {
if (!GatsModCore.SETTINGS.sequencerEnabled) {
alert("Sequencer is disabled in settings.");
return;
}
if (GatsModCore.SETTINGS.sequencerActive) GatsModCore.stopSequencer();
GatsModCore.SETTINGS.sequencerActive = true;
GatsModCore.sequencerCurrentIndex = 0;
modLog("Sequencer: Started.");
GatsModCore.sequencerLoop();
}
static stopSequencer() {
if (GatsModCore.sequencerIntervalId) {
clearTimeout(GatsModCore.sequencerIntervalId);
GatsModCore.sequencerIntervalId = null;
}
GatsModCore.SETTINGS.sequencerActive = false;
modLog("Sequencer: Stopped.");
}
static sequencerLoop() {
if (!GatsModCore.SETTINGS.sequencerActive || !GatsModCore.SETTINGS.sequencerEnabled || GatsModCore.isInputActive) {
if (!GatsModCore.SETTINGS.sequencerEnabled || GatsModCore.isInputActive) GatsModCore.stopSequencer();
return;
}
const presets = GatsModCore.SETTINGS.chatPresetMessages;
if (GatsModCore.sequencerCurrentIndex >= presets.length) {
if (GatsModCore.SETTINGS.sequencerLoop) {
GatsModCore.sequencerCurrentIndex = 0;
} else {
GatsModCore.stopSequencer();
return;
}
}
const message = presets[GatsModCore.sequencerCurrentIndex];
if (message && message.trim().length > 0) GatsModCore.sendChatMessage(message);
GatsModCore.sequencerCurrentIndex++;
if (GatsModCore.SETTINGS.sequencerActive) {
GatsModCore.sequencerIntervalId = setTimeout(GatsModCore.sequencerLoop, GatsModCore.SETTINGS.sequencerDelay);
}
}
static sendChatMessage(message) {
if (!message || message.trim().length === 0) return;
if (!Connection.list?.[0]?.socket?.send) {
modLog("Chat: Connection not available.", true);
return;
}
try {
const sanitizedMessage = message.replaceAll(",", "~");
Connection.list[0].socket.send(`c,${sanitizedMessage}\x00`);
} catch (e) {
modLog(`Chat: Error sending message: ${e.message}`, true);
}
}
static editScrollPreset(index) {
if (GatsModCore.SETTINGS.chatPresetMessages?.[index] === undefined) return;
const currentMessage = GatsModCore.SETTINGS.chatPresetMessages[index];
const newMessage = prompt(`Edit Preset ${index + 1}:`, currentMessage.trim());
if (newMessage !== null) {
GatsModCore.SETTINGS.chatPresetMessages[index] = newMessage.trim() === "" ? " " : newMessage;
GatsModCore.saveSettings();
gatsModInstance?.simpleGui?.updatePresetButtonLabel(index, GatsModCore.SETTINGS.chatPresetMessages[index]);
if (GatsModCore.SETTINGS.chatScrollText.trim() === currentMessage.trim()) {
GatsModCore.SETTINGS.chatScrollText = GatsModCore.SETTINGS.chatPresetMessages[index];
gatsModInstance?.simpleGui?.updateScrollingTextDisplay(GatsModCore.SETTINGS.chatScrollText);
}
alert(`Preset ${index + 1} updated.`);
}
}
static setScrollPreset(index) {
if (GatsModCore.SETTINGS.chatPresetMessages?.[index] !== undefined) {
const presetText = GatsModCore.SETTINGS.chatPresetMessages[index];
GatsModCore.SETTINGS.chatScrollText = presetText;
GatsModCore.saveSettings();
gatsModInstance?.simpleGui?.updateScrollingTextDisplay(presetText);
GatsModCore.sendChatMessage(presetText);
if (GatsModCore.SETTINGS.chatScrollActive) GatsModCore.startChatScroll();
modLog(`ChatScroller: Preset ${index + 1} set and sent once.`);
}
}
findNearestEnemy(me) {
let closestEnemy = null;
let minDistanceSq = Infinity;
for (const id in Player.pool) {
const p = Player.pool[id];
if (!p?.activated || p.hp <= 0 || id == selfId || (me.teamCode !== 0 && p.teamCode === me.teamCode) || GatsModCore.SETTINGS.aimbotIgnoreList.includes(p.username)) continue;
const distSq = (p.x - me.x) ** 2 + (p.y - me.y) ** 2;
if (distSq < minDistanceSq) {
minDistanceSq = distSq;
closestEnemy = p;
}
}
return closestEnemy;
}
performZeroAIActions(me) {
this.performAutoReload(me);
let movementGoal = null;
let aiMode = 'idle';
const nearestEnemy = this.findNearestEnemy(me);
if (this.multibox.isChild && this.multibox.parentCoords) {
const distToParent = getDistance(me, this.multibox.parentCoords);
if (distToParent > 400) {
movementGoal = this.multibox.parentCoords;
aiMode = 'following_parent';
}
}
const bounds = GatsModCore.MAP_BOUNDS;
if (!movementGoal && (me.x < bounds.minX || me.x > bounds.maxX || me.y < bounds.minY || me.y > bounds.maxY)) {
movementGoal = { x: (bounds.minX + bounds.maxX) / 2, y: (bounds.minY + bounds.maxY) / 2 };
aiMode = 'returning_to_bounds';
}
if (!movementGoal) {
movementGoal = this.getExplosiveAvoidanceGoal(me);
if (movementGoal) aiMode = 'fleeing_explosive';
}
if (!movementGoal && GatsModCore.SETTINGS.aiBulletDodging) {
movementGoal = this.getBulletDodgeGoal(me);
if (movementGoal) aiMode = 'dodging';
}
if (!movementGoal && nearestEnemy) {
movementGoal = this.getPotentialThreatDodgeGoal(me, nearestEnemy);
if (movementGoal) aiMode = 'pre_dodging';
}
if (!movementGoal && GatsModCore.SETTINGS.aiAutoRetreat) {
const isLowHp = me.hp <= GatsModCore.SETTINGS.aiRetreatHP;
const isReloading = me.reloading;
if (isLowHp || isReloading) {
const retreatGoal = this.getSafestRetreatGoal(me);
if (retreatGoal) {
movementGoal = retreatGoal;
aiMode = 'retreating';
} else if (nearestEnemy) {
movementGoal = { x: me.x - (nearestEnemy.x - me.x), y: me.y - (nearestEnemy.y - me.y) };
aiMode = 'retreating';
}
}
}
if (!movementGoal && GatsModCore.SETTINGS.aiEnableKiting && nearestEnemy) {
const kitingGoal = this.getOptimalKitingGoal(me, nearestEnemy);
if (kitingGoal) {
movementGoal = kitingGoal;
aiMode = 'kiting';
}
}
if (!movementGoal && GatsModCore.SETTINGS.aiAutoMovement) {
if (this.multibox.isChild && this.multibox.parentCoords) {
movementGoal = this.multibox.parentCoords;
aiMode = 'following_parent';
}
else if (GatsModCore.SETTINGS.followBotEnabled && GatsModCore.SETTINGS.followBotTargetName) {
const followTarget = Object.values(Player.pool).find(p => p.username === GatsModCore.SETTINGS.followBotTargetName && p.activated);
if (followTarget) {
movementGoal = { x: followTarget.x, y: followTarget.y };
aiMode = 'following';
}
}
if (!movementGoal) {
if (nearestEnemy) {
movementGoal = { x: nearestEnemy.x, y: nearestEnemy.y };
const dist = getDistance(me, nearestEnemy);
if (nearestEnemy.class === 'shotgun' && dist < 450) {
aiMode = 'distancing';
} else {
aiMode = 'attacking';
}
} else {
movementGoal = { x: (bounds.minX + bounds.maxX) / 2, y: (bounds.minY + bounds.maxY) / 2 };
aiMode = 'patrolling';
}
}
}
if (movementGoal) {
movementGoal.x = Math.max(bounds.minX, Math.min(bounds.maxX, movementGoal.x));
movementGoal.y = Math.max(bounds.minY, Math.min(bounds.maxY, movementGoal.y));
}
if (movementGoal) {
this.moveAITowards(me, movementGoal);
} else {
this.updateSimulatedKeys([]);
this.aiDebugData.whiskers = [];
this.aiDebugData.finalMoveDirection = null;
}
if (GatsModCore.SETTINGS.aiAutoParkUsage) {
this.useParkAbility(me);
}
if (GatsModCore.SETTINGS.aiAutoTalk) {
this.handleSituationalAITalk(me, nearestEnemy, aiMode);
}
}
performAutoReload(me) {
if (!GatsModCore.SETTINGS.zeroAIEnabled || me.reloading || !me.magSize || me.mag >= me.magSize) {
return;
}
const reloadSafetyRadiusSq = 500 * 500;
let enemyNearby = false;
for (const id in Player.pool) {
const p = Player.pool[id];
if (!p?.activated || p.hp <= 0 || id == selfId || (me.teamCode !== 0 && p.teamCode === me.teamCode)) continue;
const distSq = (p.x - me.x) ** 2 + (p.y - me.y) ** 2;
if (distSq < reloadSafetyRadiusSq) {
enemyNearby = true;
break;
}
}
if (!enemyNearby) {
modLog("AI: Auto-reloading.");
this._fireKeyEvent('keydown', 'r');
setTimeout(() => this._fireKeyEvent('keyup', 'r'), 50);
}
}
getPotentialThreatDodgeGoal(me, enemy) {
if (!enemy) return null;
const enemyGunOrigin = this.getBulletOrigin(enemy);
const distanceToSelf = getDistance(enemyGunOrigin, me);
const enemyRange = this.getEstimatedEnemyRange(enemy);
if (distanceToSelf > enemyRange * 1.2) {
return null;
}
const threatWidth = 30;
const threatHalfWidth = threatWidth / 2;
const dx_threat = me.x - enemyGunOrigin.x;
const dy_threat = me.y - enemyGunOrigin.y;
const dist_threat = Math.hypot(dx_threat, dy_threat);
let isThreatened = false;
if (dist_threat > 0.1) {
const dir_x = dx_threat / dist_threat;
const dir_y = dy_threat / dist_threat;
const perp_x = -dir_y;
const perp_y = dir_x;
const checkPointCenter = me;
const checkPointLeft = { x: me.x + perp_x * threatHalfWidth, y: me.y + perp_y * threatHalfWidth };
const checkPointRight = { x: me.x - perp_x * threatHalfWidth, y: me.y - perp_y * threatHalfWidth };
isThreatened = this.hasLineOfSight(enemyGunOrigin, checkPointCenter) ||
this.hasLineOfSight(enemyGunOrigin, checkPointLeft) ||
this.hasLineOfSight(enemyGunOrigin, checkPointRight);
} else {
isThreatened = true;
}
if (isThreatened) {
const threatDx = me.x - enemyGunOrigin.x;
const threatDy = me.y - enemyGunOrigin.y;
const dodgeVector = { x: -threatDy, y: threatDx };
const dodgeMagnitude = 200;
const vectorLength = Math.hypot(dodgeVector.x, dodgeVector.y);
if (vectorLength > 0.1) {
return {
x: me.x + (dodgeVector.x / vectorLength) * dodgeMagnitude,
y: me.y + (dodgeVector.y / vectorLength) * dodgeMagnitude
};
}
}
return null;
}
getSafestRetreatGoal(me) {
const retreatCheckRadiusSq = 800 * 800;
let totalRepulsion = { x: 0, y: 0 };
let threatsFound = 0;
for (const id in Player.pool) {
const p = Player.pool[id];
if (!p?.activated || p.hp <= 0 || id == selfId || (me.teamCode !== 0 && p.teamCode === me.teamCode) || GatsModCore.SETTINGS.aimbotIgnoreList.includes(p.username)) continue;
const dx = me.x - p.x;
const dy = me.y - p.y;
const distSq = dx * dx + dy * dy;
if (distSq < retreatCheckRadiusSq && distSq > 1) {
threatsFound++;
const distance = Math.sqrt(distSq);
const repulsionStrength = 1 / distance;
totalRepulsion.x += (dx / distance) * repulsionStrength;
totalRepulsion.y += (dy / distance) * repulsionStrength;
}
}
if (threatsFound > 0) {
const magnitude = Math.hypot(totalRepulsion.x, totalRepulsion.y);
if (magnitude > 0.01) {
const retreatDistance = 500;
return {
x: me.x + (totalRepulsion.x / magnitude) * retreatDistance,
y: me.y + (totalRepulsion.y / magnitude) * retreatDistance
};
}
}
return null;
}
getOptimalKitingGoal(me, enemy) {
const myRange = this.getMyWeaponRange(me);
const enemyRange = this.getEstimatedEnemyRange(enemy);
if (myRange > enemyRange + 50) {
const optimalDistance = myRange * 0.9;
const currentDistance = getDistance(me, enemy);
const dx = me.x - enemy.x;
const dy = me.y - enemy.y;
const targetX = enemy.x + (dx / currentDistance) * optimalDistance;
const targetY = enemy.y + (dy / currentDistance) * optimalDistance;
return { x: targetX, y: targetY };
}
return null;
}
getBulletDodgeGoal(me) {
const dodgeRadiusHorizontal = 150;
const dodgeRadiusVertical = 15;
const playerRadius = me.radius || 26;
let bestDodgeVector = { x: 0, y: 0 };
let threatsFound = 0;
if (typeof Bullet === 'undefined' || !Bullet.pool) return null;
for (const id in Bullet.pool) {
const b = Bullet.pool[id];
const owner = Player.pool[b.ownerId];
if (!b || b.ownerId === selfId || (me.teamCode !== 0 && b.teamCode === me.teamCode) || (b.spdX === 0 && b.spdY === 0) || (owner && GatsModCore.SETTINGS.aimbotIgnoreList.includes(owner.username))) continue;
const predictionTime = 0.4;
const futureBulletX = b.x + b.spdX * predictionTime * 60;
const futureBulletY = b.y + b.spdY * predictionTime * 60;
const bulletSpeed = Math.hypot(b.spdX, b.spdY);
if (bulletSpeed < 0.1) continue;
const v_dir_x = b.spdX / bulletSpeed;
const v_dir_y = b.spdY / bulletSpeed;
const dx = me.x - futureBulletX;
const dy = me.y - futureBulletY;
const dist_vertical = dx * v_dir_x + dy * v_dir_y;
const dist_horizontal = dx * (-v_dir_y) + dy * v_dir_x;
const normalized_dist_sq = (dist_horizontal / (dodgeRadiusHorizontal + playerRadius)) ** 2 + (dist_vertical / (dodgeRadiusVertical + playerRadius)) ** 2;
if (normalized_dist_sq <= 1) {
threatsFound++;
bestDodgeVector.x += -b.spdY / bulletSpeed;
bestDodgeVector.y += b.spdX / bulletSpeed;
}
}
if (threatsFound > 0) {
const dodgeMagnitude = 200;
const vectorLength = Math.hypot(bestDodgeVector.x, bestDodgeVector.y);
if (vectorLength > 0) {
return {
x: me.x + (bestDodgeVector.x / vectorLength) * dodgeMagnitude,
y: me.y + (bestDodgeVector.y / vectorLength) * dodgeMagnitude
};
}
}
return null;
}
getExplosiveAvoidanceGoal(me) {
if (typeof Explosive === 'undefined' || !Explosive.pool) {
return null;
}
let totalRepulsion = { x: 0, y: 0 };
let threatsFound = 0;
for (const id in Explosive.pool) {
const explosive = Explosive.pool[id];
if (!explosive || !explosive.activated || !explosive.type || typeof explosive.x === 'undefined' || typeof explosive.y === 'undefined') {
continue;
}
let dangerRadius = 0;
switch (explosive.type) {
case 'landMine':
dangerRadius = 70;
break;
case 'grenade':
case 'fragGrenade':
dangerRadius = 600;
break;
case 'gasGrenade':
dangerRadius = 400;
break;
default:
continue;
}
const dx = me.x - explosive.x;
const dy = me.y - explosive.y;
const distSq = dx * dx + dy * dy;
if (distSq < dangerRadius * dangerRadius && distSq > 1) {
threatsFound++;
const distance = Math.sqrt(distSq);
const repulsionStrength = (dangerRadius - distance) / dangerRadius;
totalRepulsion.x += (dx / distance) * repulsionStrength;
totalRepulsion.y += (dy / distance) * repulsionStrength;
}
}
if (threatsFound > 0) {
const magnitude = Math.hypot(totalRepulsion.x, totalRepulsion.y);
if (magnitude > 0.01) {
const fleeDistance = 400;
const fleeGoal = {
x: me.x + (totalRepulsion.x / magnitude) * fleeDistance,
y: me.y + (totalRepulsion.y / magnitude) * fleeDistance
};
return fleeGoal;
}
}
return null;
}
findBestPathWithWhiskers(me, goal) {
const whiskerCount = 6;
const whiskerAngleSpread = Math.PI / 1.3;
const whiskerLength = 350;
const dx = goal.x - me.x;
const dy = goal.y - me.y;
const baseAngle = Math.atan2(dy, dx);
let bestWhisker = null;
let minGoalDistSq = Infinity;
const analyzedWhiskers = [];
for (let i = 0; i < whiskerCount; i++) {
const angleOffset = (i - Math.floor(whiskerCount / 2)) * (whiskerAngleSpread / (whiskerCount - 1));
const whiskerAngle = baseAngle + angleOffset;
const endPoint = {
x: me.x + whiskerLength * Math.cos(whiskerAngle),
y: me.y + whiskerLength * Math.sin(whiskerAngle)
};
const isClear = this.hasLineOfSight(me, endPoint);
const whiskerData = { start: { ...me }, end: endPoint, isClear, isBest: false };
analyzedWhiskers.push(whiskerData);
if (isClear) {
const distSq = (endPoint.x - goal.x) ** 2 + (endPoint.y - goal.y) ** 2;
if (distSq < minGoalDistSq) {
minGoalDistSq = distSq;
bestWhisker = whiskerData;
}
}
}
this.aiDebugData.whiskers = analyzedWhiskers;
if (bestWhisker) {
bestWhisker.isBest = true;
return bestWhisker.end;
}
return goal;
}
moveAITowards(me, goal) {
const adjustedGoal = this.findBestPathWithWhiskers(me, goal);
this.aiDebugData.finalMoveDirection = { start: {...me}, end: adjustedGoal };
const distMovedSq = (me.x - this.aiLastPosition.x) ** 2 + (me.y - this.aiLastPosition.y) ** 2;
if (distMovedSq < 4 && Object.values(this.simulatedKeys).some(k => k)) {
this.aiStuckCounter++;
} else {
this.aiStuckCounter = 0;
}
this.aiLastPosition = { x: me.x, y: me.y };
if (me.colliding) {
this.aiObstacleAngleOffset += 7.5;
} else if (this.aiObstacleAngleOffset > 0) {
this.aiObstacleAngleOffset = Math.max(0, this.aiObstacleAngleOffset - 2.5);
}
if (this.aiStuckCounter > 60) {
this.aiUnstuckCycle = 30;
this.aiStuckCounter = 0;
this.aiObstacleAngleOffset = (this.aiObstacleAngleOffset + 90 + Math.random() * 90) % 360;
modLog("AI: Stuck detected! Initiating unstuck maneuver.");
}
let finalGoal = adjustedGoal;
if (this.aiUnstuckCycle > 0) {
const reverseDx = me.x - goal.x;
const reverseDy = me.y - goal.y;
const reverseDist = Math.hypot(reverseDx, reverseDy);
if (reverseDist > 1) {
finalGoal = {
x: me.x + (reverseDx / reverseDist) * 200,
y: me.y + (reverseDy / reverseDist) * 200
};
} else {
finalGoal = { x: me.x + (Math.random() - 0.5) * 200, y: me.y + (Math.random() - 0.5) * 200 };
}
this.aiUnstuckCycle--;
}
let dx_move = finalGoal.x - me.x;
let dy_move = finalGoal.y - me.y;
if (this.aiObstacleAngleOffset !== 0) {
const angleRad = this.aiObstacleAngleOffset * (Math.PI / 180);
const cosA = Math.cos(angleRad);
const sinA = Math.sin(angleRad);
const newDx = dx_move * cosA - dy_move * sinA;
const newDy = dx_move * sinA + dy_move * cosA;
dx_move = newDx;
dy_move = newDy;
}
const { maxSpeed, ACCELERATION, FRICTION } = this.getBotPhysics(me);
const distance = Math.hypot(dx_move, dy_move);
let desiredSpdX = 0;
let desiredSpdY = 0;
const stopDistance = this.multibox.isChild ? 50 : 15;
if (distance > stopDistance) {
const brakingFactor = 12.0;
const desiredSpeed = Math.min(maxSpeed, distance / brakingFactor);
desiredSpdX = (dx_move / distance) * desiredSpeed;
desiredSpdY = (dy_move / distance) * desiredSpeed;
}
const requiredAccelX = (desiredSpdX / FRICTION) - this.botSpdX;
const requiredAccelY = (desiredSpdY / FRICTION) - this.botSpdY;
let keysToPress = [];
let isPressingKeyX = 0;
let isPressingKeyY = 0;
const keyPressThreshold = ACCELERATION * 0.1;
if (requiredAccelX > keyPressThreshold) {
keysToPress.push('d');
isPressingKeyX = 1;
}
else if (requiredAccelX < -keyPressThreshold) {
keysToPress.push('a');
isPressingKeyX = -1;
}
if (requiredAccelY > keyPressThreshold) {
keysToPress.push('s');
isPressingKeyY = 1;
}
else if (requiredAccelY < -keyPressThreshold) {
keysToPress.push('w');
isPressingKeyY = -1;
}
this.updateSimulatedKeys(keysToPress);
let appliedAccelX = isPressingKeyX * ACCELERATION;
let appliedAccelY = isPressingKeyY * ACCELERATION;
if (isPressingKeyX !== 0 && isPressingKeyY !== 0) {
appliedAccelX *= GatsModCore.PLAYER_SPEEDS.diagonalCorrection;
appliedAccelY *= GatsModCore.PLAYER_SPEEDS.diagonalCorrection;
}
this.botSpdX = (this.botSpdX + appliedAccelX) * FRICTION;
this.botSpdY = (this.botSpdY + appliedAccelY) * FRICTION;
const currentSimulatedSpeed = Math.hypot(this.botSpdX, this.botSpdY);
if (currentSimulatedSpeed > maxSpeed) {
const ratio = maxSpeed / currentSimulatedSpeed;
this.botSpdX *= ratio;
this.botSpdY *= ratio;
}
}
useParkAbility(me) {
const now = performance.now();
if (now - this.aiLastParkUse > 1000) {
this.aiLastParkUse = now;
this._fireKeyEvent('keydown', ' ');
setTimeout(() => this._fireKeyEvent('keyup', ' '), 50);
}
}
handleSituationalAITalk(me, nearestEnemy, aiMode) {
const now = performance.now();
if (now - this.aiLastChatTime < 1000) return;
let chatCandidates = [];
let saySomething = false;
if (this.multibox.isChild) {
aiMode = 'idle';
}
switch (aiMode) {
case 'attacking':
if (nearestEnemy) chatCandidates = GatsModCore.AI_CHAT_SITUATIONAL.attacking(nearestEnemy.username);
saySomething = Math.random() < 0.1;
break;
case 'fleeing_explosive':
chatCandidates = GatsModCore.AI_CHAT_SITUATIONAL.fleeing_explosive();
saySomething = Math.random() < 0.5;
break;
case 'dodging':
case 'pre_dodging':
chatCandidates = GatsModCore.AI_CHAT_SITUATIONAL.dodging();
saySomething = Math.random() < 0.25;
break;
case 'retreating':
chatCandidates = GatsModCore.AI_CHAT_SITUATIONAL.retreating();
saySomething = Math.random() < 0.25;
break;
case 'distancing':
chatCandidates = GatsModCore.AI_CHAT_SITUATIONAL.distancing();
saySomething = Math.random() < 0.20;
break;
case 'kiting':
chatCandidates = GatsModCore.AI_CHAT_SITUATIONAL.kiting();
saySomething = Math.random() < 0.20;
break;
case 'idle':
case 'patrolling':
case 'returning_to_bounds':
case 'following_parent':
chatCandidates = GatsModCore.AI_CHAT_SITUATIONAL.idle();
saySomething = Math.random() < 0.02;
break;
}
if (saySomething && chatCandidates.length > 0) {
const message = chatCandidates[Math.floor(Math.random() * chatCandidates.length)];
GatsModCore.sendChatMessage(message);
this.aiLastChatTime = now;
}
}
handleIncomingChatMessage(message) {
if (this.multibox.isChild) return;
const now = performance.now();
if (now - this.aiLastChatTime < 1000) return;
const words = message.split(' ');
for (const word of words) {
if (GatsModCore.AI_CHAT_RESPONSES[word]) {
const responses = GatsModCore.AI_CHAT_RESPONSES[word];
const response = responses[Math.floor(Math.random() * responses.length)];
GatsModCore.sendChatMessage(response);
this.aiLastChatTime = now;
return;
}
}
}
}
let gatsModInstance = null;
let game_original_updateMouseData_ref = null;
let modInitializationAttempted = false;
const gatsModLauncherInterval = setInterval(() => {
if (modInitializationAttempted) {
clearInterval(gatsModLauncherInterval);
return;
}
if (
typeof updateMouseData === 'function' &&
typeof Player !== 'undefined' && Player.pool &&
typeof Connection !== 'undefined' && Connection.list?.[0]?.socket &&
typeof prepareMessage === 'function' &&
typeof camera === 'object' && camera?.ctx &&
typeof selfId !== 'undefined' &&
document.getElementById('canvas')
) {
modLog(`[Launcher] Game ready. Initializing mod.`);
clearInterval(gatsModLauncherInterval);
modInitializationAttempted = true;
game_original_updateMouseData_ref = updateMouseData;
updateMouseData = function(eventData) {
if (!gatsModInstance) {
try {
gatsModInstance = new GatsModCore();
gatsModInstance.originalUpdateMouseData = game_original_updateMouseData_ref;
const original_clearRect = CanvasRenderingContext2D.prototype.clearRect;
CanvasRenderingContext2D.prototype.clearRect = function(...args) {
original_clearRect.apply(this, args);
if (gatsModInstance && this.canvas.id === 'canvas' && Player.pool?.[selfId]?.activated) {
try {
gatsModInstance.mainGameTick();
} catch (e) {
modLog(`mainGameTick error: ${e.stack}`, true);
}
}
};
modLog(`[Launcher] clearRect hooked for game tick.`);
} catch (e) {
modLog(`[Launcher] GatsModCore instantiation failed: ${e.stack}`, true);
updateMouseData = game_original_updateMouseData_ref;
return;
}
}
if (gatsModInstance?.isExclusionModeActive) {
return;
}
if (GatsModCore.SETTINGS.zeroAIEnabled) {
return;
}
return game_original_updateMouseData_ref.call(this, eventData);
};
modLog(`[Launcher] updateMouseData wrapped. Mod will fully initialize on first mouse event.`);
}
}, 250);
setTimeout(() => {
if (!modInitializationAttempted) {
modLog(`[Launcher] Timeout: Game did not initialize required components. Mod will not start.`, true);
clearInterval(gatsModLauncherInterval);
}
}, 20000);
})();
(function() {
'use strict';
window.addEventListener ("load", onload);
function onload() {
let processMessageTmp = processMessage;
processMessage = function(event) {
processMessageTmp(event);
let decoded = new TextDecoder().decode(event.data);
if (!decoded.includes("a,")) return;
camera.update = function() {
let player = Player.pool[selfId];
if (camera.trackingId) {
camera.x = player.x - hudXPosition;
camera.y = player.y - hudYPosition;
}
}
}
}
})();
(function() {
'use strict';
const config = {
themes: {
cyberpunk: {
mainBg: 'rgba(10, 10, 15, 0.9)', secondaryBg: 'rgba(25, 25, 30, 0.95)', accent: '#00ffff', accentHover: '#7fffff',
accentBorder: '#00cccc', glow: 'rgba(0, 255, 255, 0.6)', textLight: '#e0e0e0', textDim: '#999',
btnSuccessBg: '#008080', btnSuccessBorder: '#006666', canvasBg: '#F5F5F5', gridBorder: 'rgba(0, 0, 0, 0.1)',
bulletCore: '#00ffff', bulletGlow: 'rgba(0, 255, 255, 0.8)', bulletTrail: 'rgba(0, 255, 255, 0.3)',
playerHp: '#ff3366', playerArmor: '#3399ff', playerScore: '#ffcc00', hudBg: 'rgba(18, 18, 22, 0.8)',
hudBorder: 'rgba(0, 255, 255, 0.4)', deathOverlay: 'rgba(180, 0, 0, 0.4)', damageFlash: 'rgba(255, 0, 50, 0.6)',
teamRed: 'rgba(255, 51, 102, 0.8)', teamBlue: 'rgba(51, 153, 255, 0.8)', selfHighlight: '#FFFFFF',
espSafe: 'rgba(255, 255, 255, 0.6)', espWarning: 'rgba(255, 204, 0, 0.8)', espDanger: 'rgba(255, 51, 102, 0.9)',
notificationBg: 'rgba(25, 25, 30, 0.9)', notificationText: '#e0e0e0', notificationStroke: 'rgba(0, 0, 0, 0.9)',
crateBody: '#4a4a5a', crateLid: '#3a3a4a', userCrateBody: '#4a4a5a', userCrateLid: '#00ffff',
playerAttachedHp: '#ff3366', playerAttachedArmor: '#333333', playerAttachedReload: '#ffcc00',
playerAttachedSkill: '#00ffff', playerAttachedBg: 'rgba(0, 0, 0, 0.3)',
shootingReady: 'rgba(0, 255, 127, 0.8)', shootingCooldown: 'rgba(255, 140, 0, 0.8)',
},
military: {
mainBg: 'rgba(45, 52, 45, 0.9)', secondaryBg: 'rgba(60, 70, 60, 0.95)', accent: '#ff9900', accentHover: '#ffb84d',
accentBorder: '#cc7a00', glow: 'rgba(255, 153, 0, 0.6)', textLight: '#d0d0c0', textDim: '#a0a090',
btnSuccessBg: '#556B2F', btnSuccessBorder: '#445625', canvasBg: '#E0E0D5', gridBorder: 'rgba(0, 0, 0, 0.1)',
bulletCore: '#ff9900', bulletGlow: 'rgba(255, 153, 0, 0.8)', bulletTrail: 'rgba(255, 153, 0, 0.3)',
playerHp: '#d9534f', playerArmor: '#5bc0de', playerScore: '#f0ad4e', hudBg: 'rgba(30, 35, 30, 0.8)',
hudBorder: 'rgba(255, 153, 0, 0.4)', deathOverlay: 'rgba(180, 0, 0, 0.4)', damageFlash: 'rgba(255, 80, 80, 0.6)',
teamRed: 'rgba(217, 83, 79, 0.8)', teamBlue: 'rgba(91, 192, 222, 0.8)', selfHighlight: '#FFFFFF',
espSafe: 'rgba(255, 255, 255, 0.6)', espWarning: 'rgba(240, 173, 78, 0.8)', espDanger: 'rgba(217, 83, 79, 0.9)',
notificationBg: 'rgba(60, 70, 60, 0.9)', notificationText: '#d0d0c0', notificationStroke: 'rgba(0, 0, 0, 0.9)',
crateBody: '#5a5a4a', crateLid: '#4a4a3a', userCrateBody: '#5a5a4a', userCrateLid: '#ff9900',
playerAttachedHp: '#d9534f', playerAttachedArmor: '#333333', playerAttachedReload: '#f0ad4e',
playerAttachedSkill: '#ff9900', playerAttachedBg: 'rgba(0, 0, 0, 0.3)',
shootingReady: 'rgba(92, 184, 92, 0.8)', shootingCooldown: 'rgba(240, 173, 78, 0.8)',
},
retro: {
mainBg: 'rgba(50, 20, 80, 0.9)', secondaryBg: 'rgba(70, 40, 100, 0.95)', accent: '#ff00ff', accentHover: '#ff66ff',
accentBorder: '#cc00cc', glow: 'rgba(255, 0, 255, 0.6)', textLight: '#00ff00', textDim: '#00cc00',
btnSuccessBg: '#008080', btnSuccessBorder: '#006666', canvasBg: '#000000', gridBorder: 'rgba(0, 255, 0, 0.1)',
bulletCore: '#ff00ff', bulletGlow: 'rgba(255, 0, 255, 0.8)', bulletTrail: 'rgba(255, 0, 255, 0.3)',
playerHp: '#ff3366', playerArmor: '#00ffff', playerScore: '#ffff00', hudBg: 'rgba(20, 0, 40, 0.8)',
hudBorder: 'rgba(255, 0, 255, 0.4)', deathOverlay: 'rgba(180, 0, 0, 0.4)', damageFlash: 'rgba(255, 0, 80, 0.6)',
teamRed: 'rgba(255, 51, 102, 0.8)', teamBlue: 'rgba(0, 255, 255, 0.8)', selfHighlight: '#FFFFFF',
espSafe: 'rgba(0, 255, 0, 0.6)', espWarning: 'rgba(255, 255, 0, 0.8)', espDanger: 'rgba(255, 0, 255, 0.9)',
notificationBg: 'rgba(70, 40, 100, 0.9)', notificationText: '#00ff00', notificationStroke: 'rgba(0, 0, 0, 0.9)',
crateBody: '#3a3a4a', crateLid: '#2a2a3a', userCrateBody: '#3a3a4a', userCrateLid: '#ff00ff',
playerAttachedHp: '#ff3366', playerAttachedArmor: '#333333', playerAttachedReload: '#ffff00',
playerAttachedSkill: '#ff00ff', playerAttachedBg: 'rgba(0, 0, 0, 0.3)',
shootingReady: 'rgba(0, 255, 0, 0.8)', shootingCooldown: 'rgba(255, 255, 0, 0.8)',
},
darkMode: {
mainBg: 'rgba(18, 18, 18, 0.9)', secondaryBg: 'rgba(33, 33, 33, 0.95)', accent: '#bb86fc', accentHover: '#d0b3ff',
accentBorder: '#985eff', glow: 'rgba(187, 134, 252, 0.4)', textLight: '#e0e0e0', textDim: '#b0b0b0',
btnSuccessBg: '#03dac6', btnSuccessBorder: '#018786', canvasBg: '#121212', gridBorder: 'rgba(255, 255, 255, 0.1)',
bulletCore: '#bb86fc', bulletGlow: 'rgba(187, 134, 252, 0.8)', bulletTrail: 'rgba(187, 134, 252, 0.3)',
playerHp: '#cf6679', playerArmor: '#03dac6', playerScore: '#f0ad4e', hudBg: 'rgba(28, 28, 28, 0.8)',
hudBorder: 'rgba(187, 134, 252, 0.4)', deathOverlay: 'rgba(180, 0, 0, 0.4)', damageFlash: 'rgba(255, 80, 80, 0.6)',
teamRed: 'rgba(207, 102, 121, 0.8)', teamBlue: 'rgba(3, 218, 198, 0.8)', selfHighlight: '#FFFFFF',
espSafe: 'rgba(255, 255, 255, 0.6)', espWarning: 'rgba(240, 173, 78, 0.8)', espDanger: 'rgba(207, 102, 121, 0.9)',
notificationBg: 'rgba(33, 33, 33, 0.9)', notificationText: '#e0e0e0', notificationStroke: 'rgba(0, 0, 0, 0.9)',
crateBody: '#4a4a5a', crateLid: '#3a3a4a', userCrateBody: '#4a4a5a', userCrateLid: '#bb86fc',
playerAttachedHp: '#cf6679', playerAttachedArmor: '#333333', playerAttachedReload: '#f0ad4e',
playerAttachedSkill: '#bb86fc', playerAttachedBg: 'rgba(0, 0, 0, 0.3)',
shootingReady: 'rgba(3, 218, 198, 0.8)', shootingCooldown: 'rgba(240, 173, 78, 0.8)',
}
},
activeTheme: 'cyberpunk',
animations: { transitionSpeed: '0.2s', easing: 'ease-out' },
perkGUI: { enabled: true },
esp: { enabled: true },
notifications: { enabled: true, maxDisplay: 4, displayTime: 4000 },
autoPerk: { enabled: true, settings: { tier1: null, tier2: null, tier3: null } },
autoRespawn: { enabled: true},
chatLog: { enabled: true, maxEntries: 50 }
};
const zoomState = {
level: 1.0,
min: 0.3,
max: 2.5,
step: 0.1
};
function handleMouseWheel(e) {
if (!unsafeWindow.inGame || unsafeWindow.spectating || ['INPUT', 'SELECT', 'TEXTAREA'].includes(document.activeElement.tagName)) {
return;
}
e.preventDefault();
if (e.deltaY < 0) {
zoomState.level = Math.min(zoomState.max, zoomState.level + zoomState.step);
} else {
zoomState.level = Math.max(zoomState.min, zoomState.level - zoomState.step);
}
if (typeof unsafeWindow.resizeCanvas === 'function') {
unsafeWindow.resizeCanvas();
}
}
const themeStyles = `
:root {
--main-bg: ${config.themes.cyberpunk.mainBg}; --secondary-bg: ${config.themes.cyberpunk.secondaryBg};
--accent-color: ${config.themes.cyberpunk.accent}; --accent-hover: ${config.themes.cyberpunk.accentHover};
--accent-border: ${config.themes.cyberpunk.accentBorder}; --glow-color: ${config.themes.cyberpunk.glow};
--text-color-light: ${config.themes.cyberpunk.textLight}; --text-color-dim: ${config.themes.cyberpunk.textDim};
--btn-success-bg: ${config.themes.cyberpunk.btnSuccessBg}; --btn-success-border: ${config.themes.cyberpunk.btnSuccessBorder};
--font-family: 'Roboto', 'Segoe UI', sans-serif; --transition-speed: ${config.animations.transitionSpeed}; --easing: ${config.animations.easing};
}
/* --- 変更点 ---
body {
background-color: #0a0a0f !important; font-family: var(--font-family);
background-image:
radial-gradient(circle at 1px 1px, rgba(0, 255, 255, 0.15) 1px, transparent 0),
radial-gradient(circle at 25px 25px, rgba(0, 255, 255, 0.05) 1px, transparent 0);
background-size: 50px 50px;
}
--- ここまで --- */
::selection { background: var(--accent-color); color: #000; }
#cover { background-color: #101010; }
#loading {
font-family: var(--font-family); font-weight: 700;
text-shadow: 0 0 20px var(--glow-color);
animation: loading-pulse 1.5s infinite ease-in-out;
}
@keyframes loading-pulse {
0%, 100% { color: var(--text-color-light); transform: scale(1); }
50% { color: var(--accent-color); transform: scale(1.05); }
}
#slct, .modal-content { animation: fade-in-up 0.4s var(--easing) forwards; }
@keyframes fade-in-up { from { opacity: 0; transform: translateY(15px); } to { opacity: 1; transform: translateY(0); } }
#slct {
background: var(--main-bg);
border: 1px solid var(--accent-border); border-radius: 10px;
box-shadow: 0 0 30px rgba(0, 255, 255, 0.2); color: var(--text-color-light);
padding: 25px;
}
.selection-title {
background-color: transparent !important; color: var(--accent-color);
text-shadow: 0 0 8px var(--glow-color); font-size: 18px; font-weight: bold;
border-bottom: 1px solid var(--accent-border); padding-bottom: 8px;
margin-bottom: 15px; text-align: left; width: 100% !important;
}
.weaponSelector, .colorSelector, .armorSelector {
background-color: var(--secondary-bg) !important; border: 1px solid #444;
border-radius: 8px; transition: all var(--transition-speed) var(--easing);
position: relative; overflow: hidden;
}
.weaponSelector:hover, .colorSelector:hover, .armorSelector:hover {
border-color: var(--accent-color); transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.4);
}
.selected-box {
border: 2px solid var(--accent-color) !important;
box-shadow: 0 0 12px var(--glow-color), inset 0 0 10px rgba(0, 255, 255, 0.15);
transform: translateY(-2px);
}
#playButton, #serversBtn, #gametypeDropdown, #reconnectButton {
border-radius: 6px !important; border-width: 1px !important; border-style: solid !important;
transition: all var(--transition-speed) var(--easing); text-shadow: 0 0 5px rgba(0,0,0,0.5);
color: var(--text-color-light) !important; font-family: var(--font-family); font-weight: 700;
}
#playButton { background: var(--accent-color) !important; border-color: var(--accent-border) !important; box-shadow: 0 0 15px var(--glow-color); color: #000 !important; }
#playButton:hover { transform: scale(1.05); box-shadow: 0 0 30px var(--glow-color); filter: brightness(1.1); }
#serversBtn, #gametypeDropdown { background-color: var(--secondary-bg) !important; border-color: #555 !important; }
#serversBtn:hover, #gametypeDropdown:hover { border-color: var(--accent-color) !important; }
.modal-content {
background: #111 !important; color: var(--text-color-light);
border: 1px solid var(--accent-border) !important; border-radius: 10px !important;
box-shadow: 0 0 35px rgba(0, 255, 255, 0.25);
}
.modal-header { border-bottom: 1px solid var(--accent-border); padding: 15px 20px;}
.modal-title { color: var(--accent-color); font-weight: bold; text-shadow: 0 0 5px var(--glow-color); }
.modal-header .close { color: var(--text-color-light); opacity: 0.7; text-shadow: none; transition: all 0.2s; }
.modal-header .close:hover { color: var(--accent-color); opacity: 1; transform: rotate(90deg) scale(1.2); }
.form-control {
background-color: #1a1a1a !important; border: 1px solid #444 !important;
color: var(--text-color-light) !important; border-radius: 4px !important;
transition: all var(--transition-speed) var(--easing);
}
.form-control:focus { border-color: var(--accent-color) !important; box-shadow: 0 0 8px var(--glow-color); }
.btn-success { background-color: var(--btn-success-bg); border-color: var(--btn-success-border); }
.btn-success:hover { filter: brightness(1.2); }
.ad-group, #detect, .ads.ad.adsbox, #adHome { display: none !important; }
#serverModal .modal-content { background-color: var(--secondary-bg) !important; }
#serverModal .modal-body { padding: 0; }
#serverModal .nav-tabs {
background-color: rgba(10, 10, 15, 0.8);
border-bottom: 1px solid var(--accent-border);
padding: 10px 10px 0 10px;
}
#serverModal .nav-tabs > li > a {
color: var(--text-color-dim); border-radius: 6px 6px 0 0 !important;
border: 1px solid transparent !important; margin-right: 5px;
background-color: rgba(40, 40, 45, 0.8) !important;
transition: var(--transition-speed) var(--easing);
}
#serverModal .nav-tabs > li > a:hover { background-color: rgba(55, 55, 60, 0.9) !important; color: var(--text-color-light); }
#serverModal .nav-tabs > li.active > a,
#serverModal .nav-tabs > li.active > a:hover,
#serverModal .nav-tabs > li.active > a:focus {
background-color: var(--secondary-bg) !important; color: var(--accent-color) !important;
border: 1px solid var(--accent-border) !important; border-bottom-color: transparent !important;
font-weight: bold;
}
#serverModal .table-responsive { border: none; }
#serverModal .table { background-color: transparent; color: var(--text-color-light); margin-bottom: 0; }
#serverModal .table > thead > tr > th {
background-color: rgba(10, 10, 15, 0.5); color: var(--text-color-light);
border-bottom: 2px solid var(--accent-border); text-align: left; padding: 12px;
}
#serverModal .table > tbody > tr {
background-color: transparent;
border-bottom: 1px solid rgba(0, 255, 255, 0.1);
transition: background-color var(--transition-speed) var(--easing);
}
#serverModal .table > tbody > tr:last-child { border-bottom: none; }
#serverModal .table > tbody > tr:hover { background-color: rgba(0, 255, 255, 0.08); }
#serverModal .table > tbody > tr > td {
border-top: none; vertical-align: middle; padding: 12px;
color: var(--text-color-dim);
}
#serverModal .table > tbody > tr > td:first-child { color: var(--text-color-light); font-weight: 500; }
#serverModal .table > tbody > tr > td:nth-child(2) { color: var(--text-color-light); }
#serverModal .table > tbody > tr > td .btn-success {
background-color: transparent !important;
border: 1px solid var(--accent-color) !important;
color: var(--accent-color) !important;
font-weight: bold; padding: 6px 14px; border-radius: 5px;
transition: all var(--transition-speed) var(--easing);
}
#serverModal .table > tbody > tr > td .btn-success:hover {
background-color: var(--accent-color) !important;
color: #000 !important;
box-shadow: 0 0 10px var(--glow-color);
}
#serverModal .table > tbody > tr > td .btn-success[disabled] {
background-color: transparent !important; border-color: #555 !important;
color: #555 !important; opacity: 0.6; cursor: not-allowed;
}
#discord-link-btn {
display: block; text-align: center;
margin-top: 15px; padding: 10px;
background-color: var(--secondary-bg);
color: var(--text-color-dim);
text-decoration: none; border-radius: 6px;
border: 1px solid #555;
font-weight: 500; font-size: 13px;
transition: all var(--transition-speed) var(--easing);
}
#discord-link-btn:hover {
color: var(--accent-color);
border-color: var(--accent-color);
background-color: rgba(0, 255, 255, 0.1);
}
/* --- MODIFIED NOTIFICATION STYLES --- */
#notification-container {
position: fixed;
top: 80px;
left: 190px; /* Changed from 'right: 20px' */
display: flex;
flex-direction: column;
gap: 10px;
z-index: 2000;
pointer-events: none;
}
.notification {
background: var(--notification-bg, rgba(25, 25, 30, 0.9));
color: var(--notification-text, #e0e0e0);
padding: 12px 18px;
border-radius: 6px;
border-left: 4px solid var(--accent-color);
box-shadow: 0 3px 10px rgba(0,0,0,0.5);
font-size: 14px;
font-weight: 500;
animation: slide-in-from-left 0.4s ease-out, fade-out 0.4s ease-in ${config.notifications.displayTime / 1000}s forwards; /* Changed animation name */
}
/* --- MODIFIED KEYFRAMES --- */
@keyframes slide-in-from-left { from { transform: translateX(-100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
@keyframes fade-out { from { opacity: 1; } to { opacity: 0; transform: translateY(-10px); } }
#auto-perk-container { margin-bottom: 20px; }
.auto-perk-slots { display: flex; justify-content: space-around; gap: 10px; }
.perk-slot {
width: 65px; height: 65px;
background-color: var(--secondary-bg); border: 1px solid #444;
border-radius: 8px; cursor: pointer;
transition: all var(--transition-speed) var(--easing);
display: flex; align-items: center; justify-content: center;
font-size: 24px; font-weight: bold; color: var(--text-color-dim);
background-size: 85%; background-position: center; background-repeat: no-repeat;
}
.perk-slot:hover {
border-color: var(--accent-color); transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.4);
}
/* --- Modified Utility Section Styles --- */
.utility-options-wrapper {
display: flex;
gap: 15px;
align-items: stretch; /* Make items equal height */
}
.utility-option {
display: flex; align-items: center; justify-content: center;
cursor: pointer; background-color: var(--secondary-bg);
border: 1px solid #444; border-radius: 8px; padding: 12px;
transition: all var(--transition-speed) var(--easing);
user-select: none; flex: 1;
}
.utility-option:hover { border-color: var(--accent-color); }
.utility-option input[type="checkbox"] {
margin-right: 12px; transform: scale(1.3);
cursor: pointer; accent-color: var(--accent-color);
}
.utility-option span, .utility-label {
color: var(--text-color-light); font-weight: 500; font-size: 14px;
}
.utility-option-group {
display: flex; flex-direction: column; flex: 1;
background-color: var(--secondary-bg); border: 1px solid #444;
border-radius: 8px; padding: 8px 12px;
transition: all var(--transition-speed) var(--easing);
}
.utility-option-group:hover { border-color: var(--accent-border); }
.utility-label { margin-bottom: 6px; }
.utility-option-group .form-control { width: 100%; flex-grow: 1; }
#perk-selection-modal {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.7); display: flex;
align-items: center; justify-content: center; z-index: 2001;
}
#perk-selection-modal .modal-content {
background: var(--secondary-bg) !important; padding: 20px;
border-radius: 10px; border: 1px solid var(--accent-border);
max-width: 500px;
}
#perk-selection-modal h3 {
color: var(--accent-color); margin-top: 0; text-align: center;
border-bottom: 1px solid var(--accent-border); padding-bottom: 10px;
}
.perk-grid {
display: grid; grid-template-columns: repeat(auto-fill, minmax(65px, 1fr));
gap: 10px; margin-top: 15px;
}
.perk-icon-modal {
width: 65px; height: 65px;
background-color: #1a1a1a; border: 1px solid #444;
border-radius: 6px; cursor: pointer;
transition: all 0.2s ease;
background-size: 85%; background-position: center; background-repeat: no-repeat;
display: flex; align-items: center; justify-content: center;
font-weight: bold; color: var(--text-color-dim);
}
.perk-icon-modal:hover { border-color: var(--accent-color); transform: scale(1.1); }
#chat-log-container {
position: fixed; left: 0; top: 50%;
transform: translateY(-50%); z-index: 1500;
display: flex; align-items: center;
transition: all var(--transition-speed) var(--easing);
}
#chat-log-handle {
width: 25px; height: 120px;
background-color: var(--secondary-bg);
border: 1px solid var(--accent-border);
border-left: none; border-radius: 0 8px 8px 0;
cursor: pointer; display: flex;
align-items: center; justify-content: center;
writing-mode: vertical-rl; text-orientation: mixed;
color: var(--accent-color); font-weight: bold;
font-size: 14px; letter-spacing: 2px;
text-shadow: 0 0 5px var(--glow-color);
}
#chat-log-content {
width: 0; max-width: 0;
height: 400px;
background-color: var(--secondary-bg);
border: 1px solid var(--accent-border);
border-left: none; border-radius: 0 8px 8px 0;
overflow: hidden;
transition: all var(--transition-speed) var(--easing);
display: flex; flex-direction: column;
}
#chat-log-container:hover #chat-log-content {
width: 350px; max-width: 350px;
padding: 10px; overflow-y: auto;
}
.chat-log-entry {
margin-bottom: 5px; line-height: 1.4;
font-size: 13px; word-break: break-word;
color: var(--text-color-light);
}
.chat-log-player-name { font-weight: bold; }
#chat-log-content::-webkit-scrollbar { width: 8px; }
#chat-log-content::-webkit-scrollbar-track { background: #1a1a1a; }
#chat-log-content::-webkit-scrollbar-thumb { background: var(--accent-color); border-radius: 4px; }
#chat-log-content::-webkit-scrollbar-thumb:hover { background: var(--accent-hover); }
/* --- Modified Stats Display (FPS/Ping) Style --- */
#stats-display-container {
position: fixed;
top: 40px; /* Positioned above the minimap */
left: 20px; /* Aligned with the minimap */
width: 150px; /* Matches minimap width */
background: var(--secondary-bg);
color: var(--text-color-light);
padding: 6px 10px;
border-radius: 5px;
border: 1px solid var(--accent-border);
font-family: var(--font-family);
font-size: 12px;
font-weight: 500;
z-index: 1500;
display: flex;
justify-content: space-between;
align-items: center;
opacity: 0.85;
box-sizing: border-box;
}
/* --- Main Selection Tabs --- */
.main-tabs-container {
display: flex;
border-bottom: 1px solid var(--accent-border);
margin: -15px -15px 20px -15px; /* Adjust margins to align with padding */
padding: 0 15px;
}
.main-tab-btn {
padding: 10px 20px;
cursor: pointer;
color: var(--text-color-dim);
font-weight: 500;
font-size: 16px;
border: none;
background: transparent;
transition: all var(--transition-speed) var(--easing);
border-bottom: 3px solid transparent;
position: relative;
top: 1px;
}
.main-tab-btn:hover {
color: var(--text-color-light);
}
.main-tab-btn.active {
color: var(--accent-color);
font-weight: bold;
border-bottom-color: var(--accent-color);
}
.main-tab-content {
display: none;
animation: fade-in-up 0.3s var(--easing) forwards;
}
.main-tab-content.active {
display: block;
}
#utility-tab-content {
display: none;
flex-direction: column;
gap: 25px;
}
#utility-tab-content.active {
display: flex;
}
`;
GM_addStyle(themeStyles);
const customUI = {
animatedHud: { hp: 0, armor: 0, score: 0, isInitialized: false },
lastHp: 100,
hpDropTime: 0,
deathFade: 0,
damageFlashAlpha: 0,
drawTopStatusBar: function(ctx, player) {
if (!player) return;
const hudX = unsafeWindow.hudXPosition;
const barY = 15, barH = 30, padding = 15;
const username = unsafeWindow.selfUsername || "Player";
const kills = player.kills !== undefined ? player.kills : 0;
const score = Math.round(this.animatedHud.score);
const statusText = `${username} | Kills: ${kills} | Score: ${score}`;
ctx.save();
ctx.font = `bold 16px ${config.fontFamily}`;
const textWidth = ctx.measureText(statusText).width;
const barW = textWidth + padding * 2;
const barX = hudX - barW / 2;
ctx.globalAlpha = 0.85;
ctx.fillStyle = config.themes[config.activeTheme].hudBg;
ctx.strokeStyle = config.themes[config.activeTheme].hudBorder;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.roundRect(barX, barY, barW, barH, 5);
ctx.fill();
ctx.stroke();
ctx.globalAlpha = 1.0;
ctx.fillStyle = config.themes[config.activeTheme].textLight;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.shadowColor = 'black';
ctx.shadowBlur = 4;
ctx.fillText(statusText, hudX, barY + barH / 2);
ctx.restore();
},
drawHealthArmor: function(ctx, player) {
const hudY = unsafeWindow.hudYPosition;
ctx.save();
const barX = 15, barY = hudY * 2 - 65, barW = 250, barH = 18, barSpacing = 5;
ctx.globalAlpha = 0.85;
ctx.fillStyle = config.themes[config.activeTheme].hudBg;
ctx.strokeStyle = config.themes[config.activeTheme].hudBorder;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.roundRect(barX - 4, barY - barH - barSpacing - 4, barW + 8, barH * 2 + barSpacing + 8, 5);
ctx.fill();
ctx.stroke();
ctx.fillStyle = '#300'; ctx.fillRect(barX, barY - barH - barSpacing, barW, barH);
ctx.fillStyle = config.themes[config.activeTheme].playerHp; ctx.fillRect(barX, barY - barH - barSpacing, barW * (this.animatedHud.hp / 100), barH);
if (this.lastHp > player.hp) {
this.hpDropTime = Date.now();
this.damageFlashAlpha = 1.0;
}
this.lastHp = player.hp;
const hpFlashDuration = 300;
if (Date.now() - this.hpDropTime < hpFlashDuration) {
const flashAlpha = 1 - (Date.now() - this.hpDropTime) / hpFlashDuration;
ctx.fillStyle = `rgba(255, 255, 255, ${flashAlpha * 0.7})`;
ctx.fillRect(barX, barY - barH - barSpacing, barW, barH);
}
ctx.fillStyle = '#002233'; ctx.fillRect(barX, barY, barW, barH);
ctx.fillStyle = config.themes[config.activeTheme].playerArmor; ctx.fillRect(barX, barY, barW * (this.animatedHud.armor / 110), barH);
ctx.globalAlpha = 1.0;
ctx.fillStyle = config.themes[config.activeTheme].textLight;
ctx.font = `bold 12px ${config.fontFamily}`;
ctx.shadowColor = 'black';
ctx.shadowBlur = 3;
ctx.fillText(`HP: ${player.hp}`, barX + 5, barY - barSpacing - 5);
ctx.fillText(`Armor: ${player.armorAmount}`, barX + 5, barY + 13);
ctx.shadowBlur = 0;
ctx.restore();
},
drawScore: function(ctx, player) {
const hudX = unsafeWindow.hudXPosition;
const hudY = unsafeWindow.hudYPosition;
const scoreLevels = {0: [0, 100], 1: [100, 300], 2: [300, 600], 3: [600, 600]};
const level = unsafeWindow.playerLevel;
const scoreProgress = (level < 4) ? (100 / (scoreLevels[level][1] - scoreLevels[level][0]) * (this.animatedHud.score - scoreLevels[level][0])) : 100;
ctx.save();
const scoreX = hudX - 250, scoreY = hudY * 2 - 35, scoreW = 500, scoreH = 20;
ctx.globalAlpha = 0.85;
ctx.fillStyle = config.themes[config.activeTheme].hudBg;
ctx.strokeStyle = config.themes[config.activeTheme].hudBorder;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.roundRect(scoreX - 2, scoreY - 2, scoreW + 4, scoreH + 4, 5);
ctx.fill();
ctx.stroke();
ctx.fillStyle = '#444'; ctx.fillRect(scoreX, scoreY, scoreW, scoreH);
ctx.fillStyle = config.themes[config.activeTheme].playerScore; ctx.fillRect(scoreX, scoreY, scoreW * (scoreProgress / 100), scoreH);
ctx.globalAlpha = 1.0;
ctx.fillStyle = config.themes[config.activeTheme].textLight;
ctx.font = `bold 14px ${config.fontFamily}`;
ctx.textAlign = 'center';
ctx.shadowColor = 'black';
ctx.shadowBlur = 5;
ctx.fillText(`Score: ${Math.round(this.animatedHud.score)}`, hudX, scoreY + 15);
ctx.textAlign = 'start';
ctx.restore();
},
drawMinimap: function(ctx, player) {
const mapSize = 150;
const mapX = 20, mapY = 80;
const self = unsafeWindow.Player.pool[unsafeWindow.selfId];
if (!self) return;
const radarCenterX = mapX + mapSize / 2;
const radarCenterY = mapY + mapSize / 2;
const radarRadius = mapSize / 2;
const radarScale = 0.1;
const theme = config.themes[config.activeTheme];
ctx.save();
ctx.beginPath();
ctx.arc(radarCenterX, radarCenterY, radarRadius, 0, 2 * Math.PI);
ctx.clip();
ctx.fillStyle = 'rgba(18, 18, 22, 0.75)';
ctx.fillRect(mapX, mapY, mapSize, mapSize);
ctx.strokeStyle = theme.hudBorder;
ctx.lineWidth = 2;
ctx.stroke();
ctx.strokeStyle = 'rgba(0, 255, 255, 0.15)';
ctx.lineWidth = 1;
ctx.beginPath(); ctx.arc(radarCenterX, radarCenterY, radarRadius / 2, 0, 2 * Math.PI); ctx.stroke();
ctx.beginPath(); ctx.arc(radarCenterX, radarCenterY, radarRadius, 0, 2 * Math.PI); ctx.stroke();
ctx.beginPath();
ctx.moveTo(radarCenterX, mapY); ctx.lineTo(radarCenterX, mapY + mapSize);
ctx.moveTo(mapX, radarCenterY); ctx.lineTo(mapX + mapSize, radarCenterY);
ctx.stroke();
ctx.translate(radarCenterX, radarCenterY);
ctx.translate(-self.x * radarScale, -self.y * radarScale);
ctx.fillStyle = 'rgba(128, 128, 128, 0.5)';
for (const id in unsafeWindow.MapObject.pool) {
const obj = unsafeWindow.MapObject.pool[id];
if (!obj.activated || obj.type === '') continue;
const objX = obj.x * radarScale;
const objY = obj.y * radarScale;
const objW = obj.width * radarScale;
const objH = obj.height * radarScale;
ctx.save();
ctx.translate(objX, objY);
ctx.rotate(obj.angle * Math.PI / 180);
ctx.fillRect(-objW / 2, -objH / 2, objW, objH);
ctx.restore();
}
for (const id in unsafeWindow.Player.pool) {
const p = unsafeWindow.Player.pool[id];
if (!p.activated || p.hp <= 0 || p.id === self.id) continue;
const dotX = p.x * radarScale;
const dotY = p.y * radarScale;
let dotSize = 3;
let dotColor = theme.teamRed;
if (unsafeWindow.gameType !== 'FFA' && p.teamCode === self.teamCode) {
dotColor = theme.teamBlue;
}
ctx.fillStyle = dotColor;
ctx.beginPath();
ctx.arc(dotX, dotY, dotSize, 0, 2 * Math.PI);
ctx.fill();
}
ctx.restore();
const gameCenterX = unsafeWindow.GAME_WIDTH / 2;
const gameCenterY = unsafeWindow.GAME_HEIGHT / 2;
const dx = gameCenterX - self.x;
const dy = gameCenterY - self.y;
const distanceToCenter = Math.sqrt(dx * dx + dy * dy);
const angleToCenter = Math.atan2(dy, dx);
ctx.save();
ctx.translate(radarCenterX, radarCenterY);
if (distanceToCenter * radarScale > radarRadius - 10) {
ctx.rotate(angleToCenter);
ctx.translate(radarRadius - 10, 0);
} else {
ctx.translate(dx * radarScale, dy * radarScale);
}
ctx.fillStyle = 'rgba(255, 204, 0, 0.9)';
ctx.strokeStyle = 'rgba(255, 255, 255, 1)';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(0, 0); ctx.lineTo(-5, -5); ctx.lineTo(5, -5);
ctx.closePath(); ctx.fill(); ctx.stroke();
ctx.restore();
ctx.save();
ctx.fillStyle = theme.selfHighlight;
ctx.translate(radarCenterX, radarCenterY);
ctx.rotate(self.playerAngle * Math.PI / 180 + Math.PI / 2);
ctx.beginPath();
ctx.moveTo(0, -6); ctx.lineTo(5, 3); ctx.lineTo(-5, 3);
ctx.closePath(); ctx.fill();
ctx.restore();
},
drawWeaponAmmo: function(ctx, player) {
const hudX = unsafeWindow.hudXPosition;
const hudY = unsafeWindow.hudYPosition;
const boxW = 180, boxH = 60;
const boxX = hudX * 2 - boxW - 15, boxY = hudY * 2 - 75;
const theme = config.themes[config.activeTheme];
ctx.save();
ctx.globalAlpha = 0.85;
ctx.fillStyle = theme.hudBg;
ctx.strokeStyle = theme.hudBorder;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.roundRect(boxX, boxY, boxW, boxH, 5);
ctx.fill();
ctx.stroke();
ctx.globalAlpha = 1.0;
const weaponIconName = unsafeWindow.weaponIconMap[player.class] || player.class;
if (unsafeWindow.images[weaponIconName]) {
ctx.drawImage(unsafeWindow.images[weaponIconName], boxX + 5, boxY + 5, 50, 50);
}
ctx.fillStyle = theme.textLight;
ctx.font = `bold 24px ${config.fontFamily}`;
ctx.textAlign = 'right';
ctx.shadowColor = 'black';
ctx.shadowBlur = 4;
ctx.fillText(`${player.currentBullets} / ${player.maxBullets}`, boxX + boxW - 10, boxY + 40);
if (player.reloading) {
ctx.font = `bold 12px ${config.fontFamily}`;
ctx.fillStyle = theme.playerScore;
const reloadText = "RELOADING...";
const textWidth = ctx.measureText(reloadText).width;
ctx.fillText(reloadText, boxX + boxW - 10, boxY + 18);
ctx.fillStyle = theme.accent;
ctx.fillRect(boxX + boxW - 10 - textWidth, boxY + 22, textWidth * (player.reloadingFrame / (player.reloadingAnimation ? player.reloadingAnimation.length : 1)), 2);
}
ctx.restore();
},
drawPerks: function(ctx, player) {
const hudX = unsafeWindow.hudXPosition;
const hudY = unsafeWindow.hudYPosition;
const perkSize = 50, perkSpacing = 8;
const startX = hudX * 2 - 215 - (perkSize + perkSpacing) * 3;
const startY = hudY * 2 - 70;
const theme = config.themes[config.activeTheme];
ctx.save();
for (let i = 1; i <= 3; i++) {
const perkName = unsafeWindow.levelUpgrades[i];
const x = startX + (i - 1) * (perkSize + perkSpacing);
ctx.globalAlpha = 0.85;
ctx.fillStyle = theme.hudBg;
ctx.strokeStyle = theme.hudBorder;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.roundRect(x, startY, perkSize, perkSize, 5);
ctx.fill();
ctx.stroke();
if (perkName && unsafeWindow.images[perkName]) {
ctx.globalAlpha = 1.0;
ctx.drawImage(unsafeWindow.images[perkName], x + 4, startY + 4, perkSize - 8, perkSize - 8);
if (i === 2 && unsafeWindow.level2Timer.current > 0) {
ctx.globalAlpha = 0.7;
ctx.fillStyle = '#000';
const progress = unsafeWindow.level2Timer.current / unsafeWindow.level2Timer.total;
ctx.fillRect(x, startY, perkSize, perkSize * progress);
}
if (['grenade', 'fragGrenade', 'gasGrenade', 'landMine'].includes(perkName)) {
ctx.globalAlpha = 1.0;
ctx.fillStyle = 'white';
ctx.font = `bold 14px ${config.fontFamily}`;
ctx.textAlign = 'right';
ctx.shadowColor = 'black';
ctx.shadowBlur = 2;
ctx.fillText(player.numExplosivesLeft, x + perkSize - 4, startY + perkSize - 4);
ctx.textAlign = 'start';
}
}
}
ctx.restore();
},
drawPlayerAttachedUI: function(ctx, camera, player) {
const theme = config.themes[config.activeTheme];
ctx.save();
ctx.setTransform(1, 0, 0, 1, 0, 0);
const zoomFactor = zoomState.level;
const x = unsafeWindow.canvas.width / 2;
const y = unsafeWindow.canvas.height / 2;
const radius = 24 * zoomFactor;
const shootingBarWidth = radius * 2.5;
const shootingBarHeight = 6 * zoomFactor;
const shootingY = y - radius - (20 * zoomFactor);
let shootingProgress = 1;
let shootingBarColor = theme.shootingReady;
if (player.shooting && player.shootingAnimation) {
shootingProgress = player.shootingFrame / player.shootingAnimation.length;
shootingBarColor = theme.shootingCooldown;
}
ctx.fillStyle = theme.playerAttachedBg;
ctx.fillRect(x - shootingBarWidth / 2, shootingY, shootingBarWidth, shootingBarHeight);
ctx.fillStyle = shootingBarColor;
ctx.fillRect(x - shootingBarWidth / 2, shootingY, shootingBarWidth * shootingProgress, shootingBarHeight);
const hpBarWidth = radius * 2.5;
const hpBarHeight = 6 * zoomFactor;
const hpY = y + radius + (10 * zoomFactor);
const hpPercent = player.hp / 100;
ctx.fillStyle = theme.playerAttachedBg;
ctx.fillRect(x - hpBarWidth / 2, hpY, hpBarWidth, hpBarHeight);
ctx.fillStyle = theme.playerAttachedHp;
ctx.fillRect(x - hpBarWidth / 2, hpY, hpBarWidth * hpPercent, hpBarHeight);
const armorBarY = hpY + hpBarHeight + (2 * zoomFactor);
const armorMax = player.armor === 1 ? 30 : (player.armor === 2 ? 70 : 110);
const armorPercent = player.armorAmount / armorMax;
ctx.fillStyle = theme.playerAttachedBg;
ctx.fillRect(x - hpBarWidth / 2, armorBarY, hpBarWidth, hpBarHeight);
ctx.fillStyle = theme.playerAttachedArmor;
ctx.fillRect(x - hpBarWidth / 2, armorBarY, hpBarWidth * armorPercent, hpBarHeight);
if (player.reloading && player.reloadingAnimation) {
const reloadGaugeHeight = radius * 2;
const reloadGaugeWidth = 6 * zoomFactor;
const reloadX = x - radius - (15 * zoomFactor);
const reloadY = y - reloadGaugeHeight / 2;
const reloadPercent = player.reloadingFrame / (player.reloadingAnimation.length * 0.8);
ctx.fillStyle = theme.playerAttachedBg;
ctx.fillRect(reloadX, reloadY, reloadGaugeWidth, reloadGaugeHeight);
ctx.fillStyle = theme.playerAttachedReload;
ctx.fillRect(reloadX, reloadY + reloadGaugeHeight * (1 - Math.min(1, reloadPercent)), reloadGaugeWidth, reloadGaugeHeight * Math.min(1, reloadPercent));
}
if (unsafeWindow.level2Timer && unsafeWindow.level2Timer.total > 0) {
const skillGaugeHeight = radius * 2;
const skillGaugeWidth = 6 * zoomFactor;
const skillX = x + radius + (15 * zoomFactor);
const skillY = y - skillGaugeHeight / 2;
const skillPercent = unsafeWindow.level2Timer.current / unsafeWindow.level2Timer.total;
ctx.fillStyle = theme.playerAttachedBg;
ctx.fillRect(skillX, skillY, skillGaugeWidth, skillGaugeHeight);
ctx.fillStyle = theme.playerAttachedSkill;
ctx.fillRect(skillX, skillY + skillGaugeHeight * (1 - skillPercent), skillGaugeWidth, skillGaugeHeight * skillPercent);
}
ctx.restore();
},
drawPlayerChat: function(ctx, camera, player) {
if (!player.chatMessage || player.chatMessageTimer <= 0) return;
const theme = config.themes[config.activeTheme];
const relPos = camera.getRelPos(player);
const alpha = Math.min(1, player.chatMessageTimer / 20);
ctx.save();
ctx.globalAlpha = 0.8 * alpha;
ctx.font = `bold 13px ${config.fontFamily}`;
const playerName = player.username + ': ';
const message = player.chatMessage;
const nameWidth = ctx.measureText(playerName).width;
const messageWidth = ctx.measureText(message).width;
const totalWidth = nameWidth + messageWidth;
const padding = 8;
const boxWidth = totalWidth + padding * 2;
const boxHeight = 22;
const boxX = relPos.x - boxWidth / 2;
const boxY = relPos.y - player.radius - 45;
ctx.fillStyle = theme.hudBg;
ctx.strokeStyle = theme.hudBorder;
ctx.lineWidth = 1;
ctx.beginPath();
if (ctx.roundRect) {
ctx.roundRect(boxX, boxY, boxWidth, boxHeight, 5);
} else {
ctx.rect(boxX, boxY, boxWidth, boxHeight);
}
ctx.fill();
ctx.stroke();
ctx.beginPath();
ctx.moveTo(relPos.x - 6, boxY + boxHeight);
ctx.lineTo(relPos.x, boxY + boxHeight + 6);
ctx.lineTo(relPos.x + 6, boxY + boxHeight);
ctx.closePath();
ctx.fill();
ctx.stroke();
ctx.globalAlpha = 1.0 * alpha;
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
ctx.shadowColor = 'black';
ctx.shadowBlur = 2;
ctx.fillStyle = theme.accent;
ctx.fillText(playerName, boxX + padding, boxY + boxHeight / 2 + 1);
ctx.fillStyle = theme.textLight;
ctx.fillText(message, boxX + padding + nameWidth, boxY + boxHeight / 2 + 1);
ctx.restore();
},
drawPlayerCount: function(ctx) {
const hudX = unsafeWindow.hudXPosition;
const lbX = hudX * 2 - 265;
const boxW = 100, boxH = 40;
const boxX = lbX - boxW - 15;
const boxY = 15;
const theme = config.themes[config.activeTheme];
const currentPlayers = unsafeWindow.gameStatus.currentPlayers || 0;
const maxPlayers = unsafeWindow.MAX_PLAYERS || 0;
ctx.save();
ctx.globalAlpha = 0.85;
ctx.fillStyle = theme.hudBg;
ctx.strokeStyle = theme.hudBorder;
ctx.lineWidth = 2;
ctx.beginPath();
if (ctx.roundRect) {
ctx.roundRect(boxX, boxY, boxW, boxH, 8);
} else {
ctx.rect(boxX, boxY, boxW, boxH);
}
ctx.fill();
ctx.stroke();
ctx.globalAlpha = 1.0;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillStyle = theme.textDim;
ctx.font = `bold 12px ${config.fontFamily}`;
ctx.shadowColor = 'black';
ctx.shadowBlur = 3;
ctx.fillText("PLAYERS", boxX + boxW / 2, boxY + 12);
ctx.fillStyle = theme.textLight;
ctx.font = `bold 16px ${config.fontFamily}`;
ctx.fillText(`${currentPlayers} / ${maxPlayers}`, boxX + boxW / 2, boxY + 28);
ctx.restore();
},
drawLeaderboard: function(ctx, leaderboard) {
const hudX = unsafeWindow.hudXPosition;
const lbX = hudX * 2 - 265, lbY = 15, lbW = 250, entryH = 22;
const theme = config.themes[config.activeTheme];
ctx.save();
ctx.globalAlpha = 0.85;
ctx.fillStyle = theme.hudBg;
ctx.strokeStyle = theme.hudBorder;
ctx.lineWidth = 2;
ctx.beginPath();
if (ctx.roundRect) {
ctx.roundRect(lbX, lbY, lbW, 40 + Math.min(leaderboard.current.length, 10) * entryH, 8);
} else {
ctx.rect(lbX, lbY, lbW, 40 + Math.min(leaderboard.current.length, 10) * entryH);
}
ctx.fill();
ctx.stroke();
ctx.globalAlpha = 1.0;
ctx.fillStyle = theme.accent;
ctx.font = `bold 16px ${config.fontFamily}`;
ctx.textAlign = 'center';
ctx.shadowColor = theme.glow;
ctx.shadowBlur = 8;
ctx.fillText("LEADERBOARD", lbX + lbW / 2, lbY + 25);
ctx.shadowBlur = 0;
leaderboard.current.forEach((p, i) => {
if (i >= 10) return;
const entryY = lbY + 40 + i * entryH;
const isSelf = p.userId === unsafeWindow.isolatedUsername;
const self = unsafeWindow.Player.pool[unsafeWindow.selfId];
let textColor = theme.textLight;
if (isSelf) {
textColor = theme.accent;
ctx.fillStyle = 'rgba(0, 255, 255, 0.15)';
ctx.fillRect(lbX + 2, entryY, lbW - 4, entryH);
} else if (self && self.teamCode !== 0 && p.teamCode !== 0) {
if (p.teamCode === self.teamCode) {
textColor = theme.teamBlue;
} else {
textColor = theme.teamRed;
}
}
ctx.fillStyle = textColor;
ctx.font = `500 13px ${config.fontFamily}`;
ctx.textAlign = 'left';
ctx.fillText(`${i + 1}. ${p.userId}`, lbX + 10, entryY + 15);
ctx.textAlign = 'right';
const killsText = p.kills !== undefined ? ` (${p.kills}K)` : '';
ctx.fillText(`${p.score}${killsText}`, lbX + lbW - 10, entryY + 15);
});
ctx.restore();
},
updateAll: function(player) {
if (!player || unsafeWindow.spectating) {
this.animatedHud.isInitialized = false;
if (unsafeWindow.respawnButtonVisible) {
unsafeWindow.originalDrawHud.apply(this, arguments);
}
return;
}
const ctx = unsafeWindow.ctx;
const camera = unsafeWindow.camera;
if (!this.animatedHud.isInitialized) {
this.animatedHud.hp = player.hp;
this.animatedHud.armor = player.armorAmount;
this.animatedHud.score = player.score;
this.animatedHud.isInitialized = true;
}
this.animatedHud.hp += (player.hp - this.animatedHud.hp) * 0.15;
this.animatedHud.armor += (player.armorAmount - this.animatedHud.armor) * 0.15;
this.animatedHud.score += (player.score - this.animatedHud.score) * 0.15;
this.drawHealthArmor(ctx, player);
this.drawScore(ctx, player);
this.drawMinimap(ctx, player);
this.drawWeaponAmmo(ctx, player);
this.drawPerks(ctx, player);
this.drawPlayerAttachedUI(ctx, camera, player);
this.drawTopStatusBar(ctx, player);
this.drawPlayerCount(ctx);
}
};
const autoPerkSystem = {
init: function(targetElement) {
this.loadSettings();
this.createUI(targetElement);
this.updateUI();
},
loadSettings: function() {
const savedSettings = localStorage.getItem('gatsModAutoPerkSettings');
if (savedSettings) {
config.autoPerk.settings = JSON.parse(savedSettings);
}
},
saveSettings: function() {
localStorage.setItem('gatsModAutoPerkSettings', JSON.stringify(config.autoPerk.settings));
},
createUI: function(targetElement) {
if (!targetElement) {
console.error("GatsMod: Target element for Auto-Perk UI not provided.");
return;
}
const container = document.createElement('div');
container.id = 'auto-perk-container';
container.innerHTML = `
<div id="autoPerkTitle" class="selection-title" style="margin-top: 0;">Auto-Perk Selection</div>
<div class="auto-perk-slots">
<div id="auto-perk-slot-1" class="perk-slot" data-tier="1">1</div>
<div id="auto-perk-slot-2" class="perk-slot" data-tier="2">2</div>
<div id="auto-perk-slot-3" class="perk-slot" data-tier="3">3</div>
</div>
`;
targetElement.appendChild(container);
document.querySelectorAll('.perk-slot').forEach(slot => {
slot.addEventListener('click', (e) => {
const tier = e.target.dataset.tier;
this.showPerkSelectionModal(tier);
});
});
},
updateUI: function() {
for (let i = 1; i <= 3; i++) {
const slot = document.getElementById(`auto-perk-slot-${i}`);
const perkName = config.autoPerk.settings[`tier${i}`];
if (slot) {
if (perkName) {
slot.style.backgroundImage = `url('/img/${perkName}.png')`;
slot.innerText = '';
} else {
slot.style.backgroundImage = 'none';
slot.innerText = i;
}
}
}
},
showPerkSelectionModal: function(tier) {
const perkList = perkGUI[`tier${tier}`];
let modal = document.getElementById('perk-selection-modal');
if (modal) modal.remove();
modal = document.createElement('div');
modal.id = 'perk-selection-modal';
modal.innerHTML = `<div class="modal-content"><h3>Select Tier ${tier} Perk</h3><div class="perk-grid"></div></div>`;
document.body.appendChild(modal);
const grid = modal.querySelector('.perk-grid');
const noneOption = document.createElement('div');
noneOption.className = 'perk-icon-modal';
noneOption.innerText = 'None';
noneOption.onclick = () => this.selectAutoPerk(tier, null);
grid.appendChild(noneOption);
perkList.forEach(perkName => {
const icon = document.createElement('div');
icon.className = 'perk-icon-modal';
icon.style.backgroundImage = `url('/img/${perkName}.png')`;
icon.title = perkName.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase());
icon.onclick = () => this.selectAutoPerk(tier, perkName);
grid.appendChild(icon);
});
modal.addEventListener('click', (e) => {
if (e.target.id === 'perk-selection-modal') modal.remove();
});
},
selectAutoPerk: function(tier, perkName) {
config.autoPerk.settings[`tier${tier}`] = perkName;
this.saveSettings();
this.updateUI();
const modal = document.getElementById('perk-selection-modal');
if (modal) modal.remove();
},
attemptAutoUpgrade: function(tier) {
if (!config.autoPerk.enabled) return false;
const perkToSelect = config.autoPerk.settings[`tier${tier}`];
if (perkToSelect) {
const alreadySelected = Object.values(unsafeWindow.levelUpgrades).includes(perkToSelect);
if (!alreadySelected) {
console.log(`GatsMod: Auto-selecting Tier ${tier} perk: ${perkToSelect}`);
unsafeWindow.levelUpgrades[tier] = perkToSelect;
unsafeWindow.Connection.list[0].send(unsafeWindow.prepareMessage('upgrade', {
upgrade: perkToSelect,
upgradeLevel: tier
}));
perkGUI.hideTier(tier);
return true;
}
}
return false;
}
};
const utilitySystem = {
init: function(targetElement) {
this.loadSettings();
this.createUI(targetElement);
},
loadSettings: function() {
const savedRespawn = localStorage.getItem('gatsModAutoRespawnEnabled');
if (savedRespawn !== null) config.autoRespawn.enabled = (savedRespawn === 'true');
const savedTheme = localStorage.getItem('gatsModActiveTheme');
if (savedTheme && config.themes[savedTheme]) config.activeTheme = savedTheme;
},
saveSettings: function() {
localStorage.setItem('gatsModAutoRespawnEnabled', config.autoRespawn.enabled);
localStorage.setItem('gatsModActiveTheme', config.activeTheme);
},
createUI: function(targetElement) {
if (!targetElement) {
console.error("GatsMod: Target element for Utility UI not provided.");
return;
}
const container = document.createElement('div');
container.id = 'utility-container';
container.innerHTML = `
<div class="selection-title" style="margin-top: 0; margin-bottom: 12px; padding-bottom: 5px; font-size: 16px;">General Settings</div>
<div class="utility-options-wrapper">
<label for="autoRespawnCheckbox" class="utility-option">
<input type="checkbox" id="autoRespawnCheckbox">
<span>Auto-Respawn</span>
</label>
<div class="utility-option-group">
<label for="theme-selector" class="utility-label">Theme</label>
<select id="theme-selector" class="form-control">
<option value="cyberpunk">Cyberpunk</option>
<option value="military">Military</option>
<option value="retro">Retro</option>
<option value="darkMode">Dark Mode</option>
</select>
</div>
</div>
`;
targetElement.appendChild(container);
const checkbox = document.getElementById('autoRespawnCheckbox');
checkbox.checked = config.autoRespawn.enabled;
checkbox.addEventListener('change', () => {
config.autoRespawn.enabled = checkbox.checked;
this.saveSettings();
notificationSystem.show(`Auto-Respawn ${config.autoRespawn.enabled ? 'enabled' : 'disabled'}.`);
});
const themeSelector = document.getElementById('theme-selector');
themeSelector.value = config.activeTheme;
themeSelector.addEventListener('change', (e) => {
applyTheme(e.target.value);
this.saveSettings();
notificationSystem.show(`Theme set to "${e.target.options[e.target.selectedIndex].text}".`);
});
}
};
const perkGUI = {
tier1: ['bipod', 'optics', 'thermal', 'armorPiercing', 'extended', 'grip', 'silencer', 'lightweight', 'longRange', 'thickSkin'],
tier2: ['shield', 'firstAid', 'grenade', 'knife', 'engineer', 'dash', 'gasGrenade', 'landMine', 'fragGrenade'],
tier3: ['bipod', 'optics', 'thermal', 'armorPiercing', 'extended', 'grip', 'silencer', 'lightweight', 'longRange', 'thickSkin'],
isInitialized: false,
tierVisible: { 1: false, 2: false, 3: false },
init: function() {
if (this.isInitialized) return;
const guiContainer = document.createElement('div');
guiContainer.id = 'perk-gui-container';
document.body.appendChild(guiContainer);
this.applyStyles();
this.isInitialized = true;
},
applyStyles: function() {
GM_addStyle(`
#perk-gui-container {
position: fixed; bottom: 100px; left: 50%;
transform: translateX(-50%); display: flex;
flex-direction: column; align-items: center;
gap: 10px; z-index: 1000; pointer-events: none;
}
.perk-tier {
display: flex; gap: 10px; background: var(--hud-bg, rgba(18, 18, 22, 0.8));
padding: 10px; border-radius: 10px;
border: 1px solid var(--hud-border, rgba(0, 255, 255, 0.4));
box-shadow: 0 0 20px var(--glow-color, rgba(0, 255, 255, 0.6));
pointer-events: auto; opacity: 0;
transform: translateY(10px);
transition: opacity 0.3s ease, transform 0.3s ease;
}
.perk-tier.visible { opacity: 1; transform: translateY(0); }
.perk-icon {
width: 60px; height: 60px;
background-color: var(--secondary-bg);
border: 2px solid #444; border-radius: 6px;
cursor: pointer; transition: all 0.2s ease;
background-size: 85%; background-position: center;
background-repeat: no-repeat;
}
.perk-icon:hover {
border-color: var(--accent-color);
transform: scale(1.1);
filter: drop-shadow(0 0 8px var(--glow-color));
}
`);
},
update: function() {
if (!config.perkGUI.enabled || !unsafeWindow.inGame || unsafeWindow.spectating) {
this.hideAll();
return;
}
this.init();
const player = unsafeWindow.Player.pool[unsafeWindow.selfId];
if (!player) return;
const score = player.score;
const levelUpgrades = unsafeWindow.levelUpgrades;
if (score >= 100 && levelUpgrades[1] === '') {
if (!autoPerkSystem.attemptAutoUpgrade(1)) {
if (!this.tierVisible[1]) this.showTier(1, this.tier1.filter(perk => !Object.values(levelUpgrades).includes(perk)));
}
} else { this.hideTier(1); }
if (score >= 300 && levelUpgrades[2] === '') {
if (!autoPerkSystem.attemptAutoUpgrade(2)) {
if (!this.tierVisible[2]) this.showTier(2, this.tier2.filter(perk => !Object.values(levelUpgrades).includes(perk)));
}
} else { this.hideTier(2); }
if (score >= 600 && levelUpgrades[3] === '') {
if (!autoPerkSystem.attemptAutoUpgrade(3)) {
if (!this.tierVisible[3]) this.showTier(3, this.tier3.filter(perk => !Object.values(levelUpgrades).includes(perk)));
}
} else { this.hideTier(3); }
},
showTier: function(tier, perks) {
this.tierVisible[tier] = true;
let tierDiv = document.getElementById(`perk-tier-${tier}`);
if (!tierDiv) {
tierDiv = document.createElement('div');
tierDiv.id = `perk-tier-${tier}`;
tierDiv.className = 'perk-tier';
document.getElementById('perk-gui-container').appendChild(tierDiv);
}
tierDiv.innerHTML = '';
perks.forEach(perkName => {
const icon = document.createElement('div');
icon.className = 'perk-icon';
icon.style.backgroundImage = `url('/img/${perkName}.png')`;
icon.title = perkName.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase());
icon.onclick = () => this.selectPerk(tier, perkName);
tierDiv.appendChild(icon);
});
setTimeout(() => tierDiv.classList.add('visible'), 10);
},
hideTier: function(tier) {
this.tierVisible[tier] = false;
const tierDiv = document.getElementById(`perk-tier-${tier}`);
if (tierDiv) tierDiv.classList.remove('visible');
},
hideAll: function() {
this.hideTier(1); this.hideTier(2); this.hideTier(3);
},
selectPerk: function(tier, perkName) {
unsafeWindow.levelUpgrades[tier] = perkName;
unsafeWindow.Connection.list[0].send(unsafeWindow.prepareMessage('upgrade', {
upgrade: perkName,
upgradeLevel: tier
}));
this.hideTier(tier);
}
};
const esp = {
areas: {
tdm: [{ x: 3400, y: 3400, w: 200, h: 200 }],
dom: [
{ x: 2500, y: 2500, w: 200, h: 200 }, { x: 4300, y: 2500, w: 200, h: 200 },
{ x: 2500, y: 4300, w: 200, h: 200 }, { x: 4300, y: 4300, w: 200, h: 200 }
]
},
draw: function(ctx, camera) {
if (!config.esp.enabled || !unsafeWindow.inGame || unsafeWindow.spectating) return;
const self = unsafeWindow.Player.pool[unsafeWindow.selfId];
if (!self) return;
const gameType = unsafeWindow.gameType;
let areasToDraw = [];
if (gameType === 'TDM' || gameType === 'FFA') areasToDraw = this.areas.tdm;
else if (gameType === 'DOM') areasToDraw = this.areas.dom;
if (areasToDraw.length === 0) return;
const theme = config.themes[config.activeTheme];
ctx.save();
ctx.lineWidth = 2.5;
ctx.shadowColor = 'black'; ctx.shadowBlur = 6;
areasToDraw.forEach(area => {
const relPos = camera.getRelPos(area);
const enemyCount = this.countEnemiesInArea(area, self);
const isSelfInArea = (self.x >= area.x && self.x <= area.x + area.w && self.y >= area.y && self.y <= area.y + area.h);
let color = theme.espSafe;
if (enemyCount > 0) color = theme.espWarning;
if (enemyCount >= 2) color = theme.espDanger;
if (isSelfInArea && color === theme.espSafe) {
color = theme.espWarning;
}
ctx.strokeStyle = color;
const bracketSize = 20;
ctx.beginPath(); ctx.moveTo(relPos.x + bracketSize, relPos.y); ctx.lineTo(relPos.x, relPos.y); ctx.lineTo(relPos.x, relPos.y + bracketSize); ctx.stroke();
ctx.beginPath(); ctx.moveTo(relPos.x + area.w - bracketSize, relPos.y); ctx.lineTo(relPos.x + area.w, relPos.y); ctx.lineTo(relPos.x + area.w, relPos.y + bracketSize); ctx.stroke();
ctx.beginPath(); ctx.moveTo(relPos.x + bracketSize, relPos.y + area.h); ctx.lineTo(relPos.x, relPos.y + area.h); ctx.lineTo(relPos.x, relPos.y + area.h - bracketSize); ctx.stroke();
ctx.beginPath(); ctx.moveTo(relPos.x + area.w - bracketSize, relPos.y + area.h); ctx.lineTo(relPos.x + area.w, relPos.y + area.h); ctx.lineTo(relPos.x + area.w, relPos.y + area.h - bracketSize); ctx.stroke();
});
ctx.restore();
},
countEnemiesInArea: function(area, self) {
let count = 0;
for (const id in unsafeWindow.Player.pool) {
const p = unsafeWindow.Player.pool[id];
if (!p.activated || p.hp <= 0 || p.id === self.id) continue;
if (unsafeWindow.gameType !== 'FFA' && p.teamCode === self.teamCode) continue;
if (p.x >= area.x && p.x <= area.x + area.w && p.y >= area.y && p.y <= area.y + area.h) count++;
}
return count;
}
};
const notificationSystem = {
init: function() {
if (document.getElementById('notification-container')) return;
const container = document.createElement('div');
container.id = 'notification-container';
document.body.appendChild(container);
},
show: function(message) {
if (!config.notifications.enabled) return;
const container = document.getElementById('notification-container');
if (!container) return;
const notification = document.createElement('div');
notification.className = 'notification';
notification.textContent = message;
container.appendChild(notification);
if (container.children.length > config.notifications.maxDisplay) {
container.removeChild(container.firstChild);
}
setTimeout(() => {
if (container.contains(notification)) container.removeChild(notification);
}, config.notifications.displayTime);
}
};
const placementPreview = {
draw: function(ctx, camera) {
if (!unsafeWindow.inGame || unsafeWindow.spectating) return;
const self = unsafeWindow.Player.pool[unsafeWindow.selfId];
if (!self) return;
const activePerk = unsafeWindow.levelUpgrades[2];
let modelToDraw = null;
let previewAngle = self.playerAngle;
if (activePerk === 'landMine') {
modelToDraw = unsafeWindow.getModel('landMine');
} else if (activePerk === 'engineer') {
modelToDraw = unsafeWindow.getModel('userCrate', self.isPremiumMember);
previewAngle = 90;
}
if (!modelToDraw) return;
const DISTANCE = 50;
const angleRad = self.playerAngle * (Math.PI / 180);
const previewPos = {
x: self.x + DISTANCE * Math.cos(angleRad),
y: self.y + DISTANCE * Math.sin(angleRad)
};
ctx.save();
ctx.globalAlpha = 0.4;
unsafeWindow.drawModel(ctx, camera, previewPos, previewAngle, modelToDraw[0], 2, config.themes[config.activeTheme].accent, 0);
ctx.restore();
}
};
const chatLogSystem = {
history: [],
ui: { container: null, content: null },
init: function() {
if (!config.chatLog.enabled) return;
this.createUI();
this.hookIntoGame();
},
createUI: function() {
this.ui.container = document.createElement('div');
this.ui.container.id = 'chat-log-container';
const handle = document.createElement('div');
handle.id = 'chat-log-handle';
handle.textContent = 'LOG';
this.ui.content = document.createElement('div');
this.ui.content.id = 'chat-log-content';
this.ui.container.appendChild(this.ui.content);
this.ui.container.appendChild(handle);
document.body.appendChild(this.ui.container);
},
addEntry: function(player) {
if (!player || !player.chatMessage || player.chatMessage.trim() === '') return;
this.history.push({
name: player.username,
message: player.chatMessage,
teamCode: player.teamCode,
isSelf: player.id === unsafeWindow.selfId
});
if (this.history.length > config.chatLog.maxEntries) {
this.history.shift();
}
this.updateUI();
},
updateUI: function() {
if (!this.ui.content) return;
this.ui.content.innerHTML = '';
const self = unsafeWindow.Player.pool[unsafeWindow.selfId];
const theme = config.themes[config.activeTheme];
this.history.forEach(entry => {
const entryDiv = document.createElement('div');
entryDiv.className = 'chat-log-entry';
const nameSpan = document.createElement('span');
nameSpan.className = 'chat-log-player-name';
nameSpan.textContent = entry.name + ': ';
if (entry.isSelf) {
nameSpan.style.color = theme.accent;
} else if (self && self.teamCode !== 0 && entry.teamCode === self.teamCode) {
nameSpan.style.color = theme.teamBlue;
} else {
nameSpan.style.color = theme.teamRed;
}
const messageSpan = document.createElement('span');
messageSpan.textContent = entry.message;
entryDiv.appendChild(nameSpan);
entryDiv.appendChild(messageSpan);
this.ui.content.appendChild(entryDiv);
});
this.ui.content.scrollTop = this.ui.content.scrollHeight;
},
hookIntoGame: function() {
const originalApplyAuxUpdate = unsafeWindow.Player.prototype.applyAuxUpdate;
const self = this;
unsafeWindow.Player.prototype.applyAuxUpdate = function(data) {
if (data.chatMessage !== undefined && data.chatMessage !== '') {
const playerStateForLog = {
username: this.username,
chatMessage: data.chatMessage,
teamCode: this.teamCode,
id: this.id
};
self.addEntry(playerStateForLog);
}
originalApplyAuxUpdate.apply(this, arguments);
};
}
};
const statsDisplay = {
fps: 0,
ping: 0,
lastFrameTime: 0,
frameCount: 0,
lastUpdateTime: 0,
container: null,
fpsEl: null,
pingEl: null,
init: function() {
this.container = document.createElement('div');
this.container.id = 'stats-display-container';
this.container.innerHTML = `
<span id="stats-fps">FPS: --</span>
<span id="stats-ping">Ping: --ms</span>
`;
document.body.appendChild(this.container);
this.fpsEl = document.getElementById('stats-fps');
this.pingEl = document.getElementById('stats-ping');
},
update: function(now) {
if (!this.lastUpdateTime) this.lastUpdateTime = now;
const delta = now - this.lastUpdateTime;
this.frameCount++;
if (delta >= 1000) {
this.fps = this.frameCount;
this.frameCount = 0;
this.lastUpdateTime = now;
if (this.fpsEl) this.fpsEl.textContent = `FPS: ${this.fps}`;
}
if (unsafeWindow.Connection && unsafeWindow.Connection.list[0]) {
this.ping = unsafeWindow.Connection.list[0].currentPing || 0;
if (this.pingEl) this.pingEl.textContent = `Ping: ${this.ping}ms`;
}
}
};
function applyTheme(themeName) {
if (!config.themes[themeName]) {
console.error(`Theme "${themeName}" not found.`);
return;
}
config.activeTheme = themeName;
const theme = config.themes[themeName];
const root = document.documentElement;
root.style.setProperty('--main-bg', theme.mainBg);
root.style.setProperty('--secondary-bg', theme.secondaryBg);
root.style.setProperty('--accent-color', theme.accent);
root.style.setProperty('--accent-hover', theme.accentHover);
root.style.setProperty('--accent-border', theme.accentBorder);
root.style.setProperty('--glow-color', theme.glow);
root.style.setProperty('--text-color-light', theme.textLight);
root.style.setProperty('--text-color-dim', theme.textDim);
root.style.setProperty('--btn-success-bg', theme.btnSuccessBg);
root.style.setProperty('--btn-success-border', theme.btnSuccessBorder);
if (unsafeWindow.config) {
unsafeWindow.config.colors.canvasBg = theme.canvasBg;
}
}
function initializeTheme() {
const savedTheme = localStorage.getItem('gatsModActiveTheme');
if (savedTheme && config.themes[savedTheme]) {
applyTheme(savedTheme);
} else {
applyTheme(config.activeTheme);
}
}
function initGameModifications() {
if (typeof unsafeWindow.drawGrid === 'undefined' || typeof unsafeWindow.Player === 'undefined' || typeof unsafeWindow.despawn === 'undefined') {
return;
}
console.log("GatsMod Overhaul: Applying all modifications...");
notificationSystem.init();
chatLogSystem.init();
statsDisplay.init();
const originalDrawGrid = unsafeWindow.drawGrid;
unsafeWindow.originalDrawHud = unsafeWindow.drawHud;
unsafeWindow.originalDrawNotifications = unsafeWindow.drawNotifications;
unsafeWindow.originalDrawLeaderboard = unsafeWindow.drawLeaderboard;
const originalGameLoop = unsafeWindow.gameLoop;
const originalBulletDraw = unsafeWindow.Bullet.prototype.draw;
const originalDespawn = unsafeWindow.despawn;
const originalResizeCanvas = unsafeWindow.resizeCanvas;
unsafeWindow.resizeCanvas = function() {
originalResizeCanvas.apply(this, arguments);
unsafeWindow.widthScaleFactor *= zoomState.level;
unsafeWindow.heightScaleFactor *= zoomState.level;
unsafeWindow.hudXPosition /= zoomState.level;
unsafeWindow.hudYPosition /= zoomState.level;
const ctx = unsafeWindow.ctx;
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.scale(unsafeWindow.widthScaleFactor, unsafeWindow.heightScaleFactor);
};
window.addEventListener('wheel', handleMouseWheel, { passive: false });
unsafeWindow.despawn = function() {
const wasInGame = unsafeWindow.inGame && !unsafeWindow.spectating;
originalDespawn.apply(this, arguments);
if (config.autoRespawn.enabled && wasInGame) {
console.log("[GatsMod] Auto Respawn triggered.");
if (typeof unsafeWindow.returnToSelectionScreen === 'function') {
unsafeWindow.returnToSelectionScreen();
setTimeout(() => {
if (typeof unsafeWindow.play === 'function' && document.getElementById('playButton')) {
unsafeWindow.play();
}
}, 250);
}
}
};
unsafeWindow.drawHud = (player) => customUI.updateAll(player);
unsafeWindow.drawHudMiniMap = () => {};
unsafeWindow.drawLeaderboard = (ctx, leaderboard) => {
if (leaderboard && typeof leaderboard.currentPlayers !== 'undefined') {
unsafeWindow.gameStatus.currentPlayers = leaderboard.currentPlayers;
}
leaderboard.current = leaderboard.new;
customUI.drawLeaderboard(ctx, leaderboard);
};
unsafeWindow.drawNotifications = function() {
const notifications = unsafeWindow.displayedNotifications;
if (notifications && notifications.length > 0) {
const lastNotification = notifications.pop();
let message = lastNotification.content;
switch(lastNotification.type) {
case 1: message = `Killed ${lastNotification.content}!`; break;
case 2: message = `Killed by ${lastNotification.content}`; break;
case 3: message = `Hit Damage ${lastNotification.content}`; break;
}
notificationSystem.show(message);
}
};
unsafeWindow.gameLoop = function() {
const theme = config.themes[config.activeTheme];
if (unsafeWindow.crate) {
unsafeWindow.crate[0][0][1][3] = theme.crateBody;
unsafeWindow.crate[0][1][1][3] = theme.crateLid;
}
if (unsafeWindow.longCrate) {
unsafeWindow.longCrate[0][0][1][3] = theme.crateBody;
unsafeWindow.longCrate[0][1][1][3] = theme.crateLid;
}
const now = performance.now();
statsDisplay.update(now);
originalGameLoop.apply(this, arguments);
if (unsafeWindow.inGame) {
const ctx = unsafeWindow.ctx;
const camera = unsafeWindow.camera;
for (const id in unsafeWindow.Player.pool) {
const player = unsafeWindow.Player.pool[id];
if (player && player.activated) {
customUI.drawPlayerChat(ctx, camera, player);
}
}
}
placementPreview.draw(unsafeWindow.ctx, unsafeWindow.camera);
};
unsafeWindow.drawGrid = function(ctx, camera) {
originalDrawGrid.apply(this, arguments);
esp.draw(ctx, camera);
ctx.save();
ctx.setTransform(1, 0, 0, 1, 0, 0);
const theme = config.themes[config.activeTheme];
if (customUI.damageFlashAlpha > 0) {
const centerX = unsafeWindow.canvas.width / 2;
const centerY = unsafeWindow.canvas.height / 2;
const outerRadius = Math.sqrt(centerX * centerX + centerY * centerY);
const colorBase = theme.damageFlash.substring(0, theme.damageFlash.lastIndexOf(',') + 1);
const finalColor = `${colorBase} ${customUI.damageFlashAlpha})`;
const gradient = ctx.createRadialGradient(centerX, centerY, outerRadius * 0.6, centerX, centerY, outerRadius);
gradient.addColorStop(0, 'rgba(255, 0, 50, 0)');
gradient.addColorStop(1, finalColor);
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, unsafeWindow.canvas.width, unsafeWindow.canvas.height);
customUI.damageFlashAlpha -= 0.04;
}
if (unsafeWindow.spectating && customUI.deathFade > 0) {
customUI.deathFade -= 0.02;
ctx.globalAlpha = customUI.deathFade;
ctx.fillStyle = theme.deathOverlay;
ctx.fillRect(0, 0, unsafeWindow.canvas.width, unsafeWindow.canvas.height);
} else if (!unsafeWindow.spectating) {
customUI.deathFade = 1.0;
}
ctx.restore();
};
unsafeWindow.Bullet.prototype.draw = function(ctx, camera) {
if (!this.activated || this.isKnife) {
originalBulletDraw.apply(this, arguments);
return;
}
const theme = config.themes[config.activeTheme];
ctx.save();
ctx.strokeStyle = theme.bulletTrail;
ctx.lineWidth = this.width * 0.6;
ctx.globalAlpha = 0.7;
const relPos = camera.getRelPos(this);
const prevX = relPos.x - this.spdX * 0.6;
const prevY = relPos.y - this.spdY * 0.6;
ctx.beginPath(); ctx.moveTo(prevX, prevY); ctx.lineTo(relPos.x, relPos.y); ctx.stroke();
ctx.restore();
const originalStrokeStyle = ctx.strokeStyle;
ctx.strokeStyle = theme.bulletCore;
ctx.shadowColor = theme.bulletGlow;
ctx.shadowBlur = 8;
originalBulletDraw.apply(this, arguments);
ctx.shadowBlur = 0;
ctx.strokeStyle = originalStrokeStyle;
};
unsafeWindow.weaponIconMap = {
'pistol': 'pistol-outline', 'smg': 'smg-outline', 'shotgun': 'shotgun-outline',
'assault': 'assault-outline', 'sniper': 'bolt-action-rifle-outline', 'lmg': 'machine-gun-outline'
};
clearInterval(modInterval);
console.log("GatsMod Overhaul: All modifications applied successfully.");
initTitleScreenFeatures();
if (typeof unsafeWindow.processMessage === 'function') {
const originalProcessMessage = unsafeWindow.processMessage;
unsafeWindow.processMessage = function(event) {
originalProcessMessage.apply(this, arguments);
try {
const dataView = new Uint8Array(event.data);
if (dataView.length > 0 && dataView[0] === 97) {
console.log("GatsMod [AbilityHack]: Player spawn message ('a') detected.");
setTimeout(() => {
if (unsafeWindow.Connection && unsafeWindow.Connection.list[0] && unsafeWindow.selfId != null) {
console.log("GatsMod [AbilityHack]: Sending fake 'longRange' upgrade request...");
const longRangeMessage = unsafeWindow.prepareMessage('upgrade', {
'upgrade': 'longRange',
'upgradeLevel': 1
});
unsafeWindow.Connection.list[0].send(longRangeMessage);
console.log("GatsMod [AbilityHack]: Sending fake 'dash' upgrade request...");
const dashMessage = unsafeWindow.prepareMessage('upgrade', {
'upgrade': 'dash',
'upgradeLevel': 2
});
unsafeWindow.Connection.list[0].send(dashMessage);
}
}, 1000);
}
} catch (e) {
}
};
console.log("GatsMod [AbilityHack]: processMessage function hooked successfully.");
}
}
function initTitleScreenFeatures() {
const slct = document.getElementById('slct');
if (!slct) {
setTimeout(initTitleScreenFeatures, 100);
return;
}
if (slct.querySelector('.main-tabs-container')) {
return;
}
const tabsContainer = document.createElement('div');
tabsContainer.className = 'main-tabs-container';
const selectionTabBtn = document.createElement('button');
selectionTabBtn.className = 'main-tab-btn active';
selectionTabBtn.textContent = 'Loadout';
selectionTabBtn.dataset.tab = 'selection';
const utilityTabBtn = document.createElement('button');
utilityTabBtn.className = 'main-tab-btn';
utilityTabBtn.textContent = 'Utilities & Settings';
utilityTabBtn.dataset.tab = 'utility';
tabsContainer.appendChild(selectionTabBtn);
tabsContainer.appendChild(utilityTabBtn);
const selectionContent = document.createElement('div');
selectionContent.id = 'selection-tab-content';
selectionContent.className = 'main-tab-content active';
const utilityContent = document.createElement('div');
utilityContent.id = 'utility-tab-content';
utilityContent.className = 'main-tab-content';
const childrenToMove = [...slct.children];
childrenToMove.forEach(child => {
selectionContent.appendChild(child);
});
const discordBtn = selectionContent.querySelector('#discord-link-btn');
if (discordBtn) {
selectionContent.querySelector('#playButton').parentElement.appendChild(discordBtn);
} else {
const playButton = selectionContent.querySelector('#playButton');
if (playButton && playButton.parentElement) {
const newDiscordBtn = document.createElement('a');
newDiscordBtn.href = 'https://discord.com/users/975535045047648266';
newDiscordBtn.target = '_blank';
newDiscordBtn.rel = 'noopener noreferrer';
newDiscordBtn.id = 'discord-link-btn';
newDiscordBtn.innerText = 'Contact on Discord';
playButton.parentElement.appendChild(newDiscordBtn);
}
}
slct.appendChild(tabsContainer);
slct.appendChild(selectionContent);
slct.appendChild(utilityContent);
autoPerkSystem.init(utilityContent);
utilitySystem.init(utilityContent);
tabsContainer.addEventListener('click', (e) => {
if (e.target.matches('.main-tab-btn')) {
const tabId = e.target.dataset.tab;
tabsContainer.querySelectorAll('.main-tab-btn').forEach(btn => btn.classList.remove('active'));
e.target.classList.add('active');
slct.querySelectorAll('.main-tab-content').forEach(content => content.classList.remove('active'));
document.getElementById(`${tabId}-tab-content`).classList.add('active');
}
});
}
initializeTheme();
const modInterval = setInterval(initGameModifications, 100);
if (config.perkGUI.enabled) {
setInterval(() => perkGUI.update(), 200);
}
})();