// ==UserScript==
// @name GatsModV4 Gats.io
// @namespace http://tampermonkey.net/
// @version 4.3
// @description 0 key toggle GUI. Updated to work with gats.io version 2.1.3.
// @author zeroarcop (updated by AI)
// @license MIT
// @match https://gats.io/
// @grant GM_addStyle
// @run-at document-idle
// ==/UserScript==
(function() {
'use strict';
// Fallback for unsafeWindow in different userscript managers
const unsafeWindow = window.unsafeWindow || window;
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 = unsafeWindow.Player.pool?.[unsafeWindow.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 = unsafeWindow.Player.pool?.[unsafeWindow.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 = unsafeWindow.Player.pool?.[unsafeWindow.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}`,
`${name}`,
],
dodging: (name) => [
`${name}`,
],
retreating: (name) => [
`${name}`,
],
distancing: (name) => [
`${name}1`,
],
fleeing_explosive: (name) => [
`${name}`,
],
kiting: (name) => [
`${name}`,
],
idle: (name) => [
`${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.isShieldModeActive = false;
this.shieldTargetScreenCoords = null;
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 (e.key === ' ') {
this.isShieldModeActive = 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;
}
if (e.key === ' ') {
this.isShieldModeActive = 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;
});
}
_prepareMessage(type, data) {
try {
const compressed = unsafeWindow.compressMessage(type, data);
return unsafeWindow.encodeMessage(compressed);
} catch(e) {
modLog(`Error in _prepareMessage for type ${type}: ${e.message}`, true);
return null;
}
}
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() {
const Player = unsafeWindow.Player;
const selfId = unsafeWindow.selfId;
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] = "";
}
}
}
findClosestIncomingBullet(me) {
let closestBullet = null;
let minDistanceSq = Infinity;
const playerHitboxRadius = 24;
const Bullet = unsafeWindow.Bullet;
const selfId = unsafeWindow.selfId;
if (typeof Bullet === 'undefined' || !Bullet.pool) {
return null;
}
for (const id in Bullet.pool) {
const b = Bullet.pool[id];
if (!b || !b.activated || b.ownerId === selfId || (me.teamCode !== 0 && b.teamCode === me.teamCode)) {
continue;
}
if (b.spdX === 0 && b.spdY === 0) {
continue;
}
const dx = me.x - b.x;
const dy = me.y - b.y;
const bulletSpeed = Math.hypot(b.spdX, b.spdY);
const dirX = b.spdX / bulletSpeed;
const dirY = b.spdY / bulletSpeed;
const projection = dx * dirX + dy * dirY;
if (projection < 0 || projection > 1000) {
continue;
}
const closestDistOnTrajectory = Math.abs(dx * dirY - dy * dirX);
if (closestDistOnTrajectory > playerHitboxRadius) {
continue;
}
const distSq = dx * dx + dy * dy;
if (distSq < minDistanceSq) {
minDistanceSq = distSq;
closestBullet = b;
}
}
return closestBullet;
}
mainGameTick() {
this.tickCounter++;
const Player = unsafeWindow.Player;
const selfId = unsafeWindow.selfId;
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.shieldTargetScreenCoords = null;
this.lastSelfKills = 0;
this.overlayCtx?.clearRect(0, 0, this.overlayCanvas.width, this.overlayCanvas.height);
return;
}
const camera = unsafeWindow.camera;
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 isShieldEquipped = Object.values(me.levelUpgrades || {}).includes('shield');
const shieldModeActive = isShieldEquipped && this.isShieldModeActive;
const aimbotActive = !shieldModeActive && GatsModCore.SETTINGS.aimbotEnabled && (GatsModCore.SETTINGS.alwaysAim || (GatsModCore.SETTINGS.aimbotOnMousePress && mouseClicked) || GatsModCore.SETTINGS.zeroAIEnabled);
const spinbotActive = !shieldModeActive && GatsModCore.SETTINGS.spinbotEnabled && !mouseClicked;
this.currentAimAssistTargetCoords = null;
this.currentAimbotTarget = null;
this.shieldTargetScreenCoords = null;
this.aimTargetScreenCoords = null;
this.spinbotTargetScreenCoords = null;
if (shieldModeActive) {
const targetBullet = this.findClosestIncomingBullet(me);
if (targetBullet) {
const finalScale = (typeof unsafeWindow !== 'undefined' && unsafeWindow.widthScaleFactor > 0) ? unsafeWindow.widthScaleFactor : 1.0;
const canvasCenterX = this.gameCanvas.width / 2;
const canvasCenterY = this.gameCanvas.height / 2;
this.shieldTargetScreenCoords = {
x: canvasCenterX + (targetBullet.x - me.x) * finalScale,
y: canvasCenterY + (targetBullet.y - me.y) * finalScale
};
}
}
if (spinbotActive) {
this.performSpinbotActions(me);
}
if (aimbotActive) {
this.performAimbotTargeting(me);
}
if (shieldModeActive && this.shieldTargetScreenCoords) {
this.currentAimAssistTargetCoords = this.shieldTargetScreenCoords;
} else if (aimbotActive && this.aimTargetScreenCoords) {
this.currentAimAssistTargetCoords = this.aimTargetScreenCoords;
} else if (spinbotActive && this.spinbotTargetScreenCoords) {
this.currentAimAssistTargetCoords = this.spinbotTargetScreenCoords;
}
const autoAttackActive = !shieldModeActive && GatsModCore.SETTINGS.autoAttackEnabled && (mouseClicked || GatsModCore.SETTINGS.zeroAIEnabled);
if (autoAttackActive) {
this.performAutoAttack(me);
} else if (this.isAutoShooting) {
this.stopShooting();
}
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 && !shieldModeActive) ? 1 : (shieldModeActive ? 0 : 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 {
const Player = unsafeWindow.Player;
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 {
const Bullet = unsafeWindow.Bullet;
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 {
unsafeWindow.Connection.list[0].send(this._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 {
unsafeWindow.Connection.list[0].send(this._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) {
const MapObject = unsafeWindow.MapObject;
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 || 50, h = rect.height || 50;
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 Player = unsafeWindow.Player;
const selfId = unsafeWindow.selfId;
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 Player = unsafeWindow.Player;
const selfId = unsafeWindow.selfId;
const MapObject = unsafeWindow.MapObject;
const Bullet = unsafeWindow.Bullet;
const camera = unsafeWindow.camera;
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 unsafeWindow.landMine !== 'undefined' && Array.isArray(unsafeWindow.landMine) && unsafeWindow.landMine[0]) {
unsafeWindow.landMine[0].forEach((a, i) => {
if (unsafeWindow.landMine[0][i] && unsafeWindow.landMine[0][i][1]) {
unsafeWindow.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 (this.shieldTargetScreenCoords) {
const { x, y } = this.shieldTargetScreenCoords;
ctx.save();
ctx.strokeStyle = '#00FFFF';
ctx.lineWidth = 2.0;
ctx.setLineDash([5, 5]);
ctx.beginPath();
ctx.moveTo(canvasCenterX, canvasCenterY);
ctx.lineTo(x, y);
ctx.stroke();
ctx.beginPath();
ctx.arc(x, y, 10, 0, 2 * Math.PI);
ctx.stroke();
ctx.restore();
}
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.activated || 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 Player = unsafeWindow.Player;
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 Player = unsafeWindow.Player;
const selfId = unsafeWindow.selfId;
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 Player = unsafeWindow.Player;
const selfId = unsafeWindow.selfId;
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 finalScale = (typeof unsafeWindow !== 'undefined' && unsafeWindow.widthScaleFactor > 0) ? unsafeWindow.widthScaleFactor : 1.0;
const relX = (p.x - me.x) * finalScale;
const relY = (p.y - me.y) * finalScale;
const screenX = canvasCenterX + relX + settings.espOffsetX;
const screenY = canvasCenterY + relY + settings.espOffsetY;
const distSq = (clickX - screenX) ** 2 + (clickY - screenY) ** 2;
const clickRadius = 24 * finalScale * 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');
const Player = unsafeWindow.Player;
const selfId = unsafeWindow.selfId;
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 (!unsafeWindow.Connection?.list?.[0]) {
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 (!unsafeWindow.Connection?.list?.[0]) {
modLog("Chat: Connection not available.", true);
return;
}
try {
const sanitizedMessage = message.replaceAll(",", "~");
const msg = GatsModCore._prepareMessage('message', { 'message': sanitizedMessage });
if (msg) unsafeWindow.Connection.list[0].send(msg);
} 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) {
const Player = unsafeWindow.Player;
const selfId = unsafeWindow.selfId;
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(unsafeWindow.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 unsafeWindow.Player.pool) {
const p = unsafeWindow.Player.pool[id];
if (!p?.activated || p.hp <= 0 || id == unsafeWindow.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 unsafeWindow.Player.pool) {
const p = unsafeWindow.Player.pool[id];
if (!p?.activated || p.hp <= 0 || id == unsafeWindow.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 fleeDistance = 500;
return {
x: me.x + (totalRepulsion.x / magnitude) * fleeDistance,
y: me.y + (totalRepulsion.y / magnitude) * fleeDistance
};
}
}
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 Bullet = unsafeWindow.Bullet;
const Player = unsafeWindow.Player;
const selfId = unsafeWindow.selfId;
if (typeof Bullet === 'undefined' || !Bullet.pool) {
return null;
}
const dodgeRadiusHorizontal = 150;
const dodgeRadiusVertical = 15;
const playerRadius = me.radius || 26;
let bestDodgeVector = { x: 0, y: 0 };
let threatsFound = 0;
for (const id in Bullet.pool) {
const b = Bullet.pool[id];
const owner = Player.pool[b.ownerId];
if (!b || !b.activated || 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) {
const Explosive = unsafeWindow.Explosive;
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 unsafeWindow.updateMouseData === 'function' &&
typeof unsafeWindow.Player !== 'undefined' && unsafeWindow.Player.pool &&
typeof unsafeWindow.Connection !== 'undefined' &&
typeof unsafeWindow.compressMessage === 'function' &&
typeof unsafeWindow.encodeMessage === 'function' &&
typeof unsafeWindow.camera === 'object' &&
document.getElementById('canvas')
) {
modLog(`[Launcher] Game ready. Initializing mod.`);
clearInterval(gatsModLauncherInterval);
modInitializationAttempted = true;
game_original_updateMouseData_ref = unsafeWindow.updateMouseData;
unsafeWindow.updateMouseData = function(eventData) {
if (!gatsModInstance) {
modLog("[Launcher] First mouse event, creating GatsModCore instance.", false);
try {
gatsModInstance = new GatsModCore();
gatsModInstance.originalUpdateMouseData = game_original_updateMouseData_ref;
const original_clearRect = unsafeWindow.CanvasRenderingContext2D.prototype.clearRect;
unsafeWindow.CanvasRenderingContext2D.prototype.clearRect = function(...args) {
original_clearRect.apply(this, args);
if (gatsModInstance && this.canvas.id === 'canvas' && unsafeWindow.Player?.pool?.[unsafeWindow.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);
unsafeWindow.updateMouseData = game_original_updateMouseData_ref;
return;
}
}
if (gatsModInstance?.isExclusionModeActive) {
return;
}
if (GatsModCore.SETTINGS.zeroAIEnabled || gatsModInstance?.currentAimAssistTargetCoords) {
// AI or Aimbot is handling mouse, so don't call original function with real mouse data yet.
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);
})();