// ==UserScript==
// @name GatsModV2
// @namespace https://tempermonkey.net/
// @version 4.2
// @description 0 key toggle GUI, Map Rader, Auto upGrade Added. Gats.io
// @author zeroarcop
// @license MIT
// @match https://gats.io/
// @grant GM_addStyle
// @run-at document-idle
// ==/UserScript==
(function() {
'use strict';
const SETTINGS_KEY = 'zeroarcop_gats_mod_settings_v2';
const LOG_PREFIX_MOD = "[zeroarcop-gats-mod]";
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');
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 | M: Spinbot | 0: GUI(s) | 1-9: Preset Chat`; 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) { 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?.(); } }; 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}#${this.container.id} details{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.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 exact player name'; 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;
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.'); // Tooltip updated
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.");
// ### NEW: Preset Sequencer ###
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);
// ### NEW: Event-Triggered Chat ###
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.');
}
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'; 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.model-"،"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>`; } this.statusDisplay.innerHTML = `ESP: ${esp} | Aimbot: ${aimbot}${spin} | 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 GatsModCore {
static SETTINGS = {};
static isInputActive = false;
static chatScrollIntervalId = null;
static chatScrollCurrentIndex = 0;
static sequencerIntervalId = null; // ### NEW
static sequencerCurrentIndex = 0; // ### NEW
// ### NEW ### Physics parameters from the report
static PLAYER_SPEEDS = {
// 1. 各武器の基本速度 (No Armor, No Upgrades)
base: {
'pistol': 8.00,
'smg': 7.45,
'shotgun': 7.30,
'assault': 7.30,
'machine-gun': 6.80,
'lmg': 6.80,
'sniper': 7.50,
'bolt-action-rifle': 7.50
},
// 2. アーマーによる速度乗数(ペナルティ)
armorMultiplier: {
0: 1.00, // noArmor
1: 0.89, // lightArmor
2: 0.80, // mediumArmor
3: 0.70 // heavyArmor
},
// 3. アップグレードによる速度乗数(ボーナス)
upgradeMultiplier: {
'lightweight': 1.20
},
// 4. 斜め移動時の各軸への補正係数
diagonalCorrection: 1 / Math.sqrt(2) // Approx 0.707
};
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, 'sniper': 11.0, 'lmg': 9.0
};
static WEAPON_FORWARD_OFFSETS = {
'pistol': 65, 'smg': 70, 'shotgun': 75, 'assault': 84,
'machine-gun': 80, 'lmg': 80, 'bolt-action-rifle': 95, 'sniper': 95
};
static WEAPON_BASE_RANGES = {
'pistol': 425, 'smg': 280, 'shotgun': 280, 'assault': 400,
'machine-gun': 355, 'lmg': 355, 'bolt-action-rifle': 650, 'sniper': 650
};
static LONG_RANGE_MULTIPLIER = 1.5;
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; // ### NEW for On-Kill event
// ### NEW ### State for the physics-based follow bot
this.botSpdX = 0;
this.botSpdY = 0;
this.initializeSettings();
this.simpleGui = new SimpleGUI();
this.colorGui = new ColorCustomizerGUI();
this.setupGUI();
this.setupOverlay();
this.addEventListeners();
this.hookMouseEvents();
this.simpleGui.updateAllGUIToReflectSettings();
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: false,
espHighlightCloaked: true, espShowTeammates: true, espOffsetX: 0, espOffsetY: 0, espScale: 0.89,
autoAttackShowRadius: true,
obstacleEspEnabled: false,
losDebugLineEnabled: false,
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,
// ### MODIFIED: Default presets
chatPresetMessages: [ "GatsModV2 by Zeroarcop", "lol", "glhf", "brb", "re", "oops", "lag", "thx", "gg" ],
// ### NEW: Sequencer and Event Chat settings
sequencerEnabled: false,
sequencerDelay: 1000,
sequencerLoop: false,
sequencerActive: false,
onKillChatEnabled: false,
onKillMessage: "ez",
// ### END NEW
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' },
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 = Array.isArray(savedSettings.aimbotIgnoreList) ? savedSettings.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 (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 'm': 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(); } });
const guiIdsToIgnore = [this.simpleGui.container.id, this.colorGui?.container.id, 'player-list-modal'].filter(Boolean);
document.addEventListener('mousedown', (e) => { 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; }); }
mainGameTick() {
this.tickCounter++;
const me = Player.pool?.[selfId];
if (!me?.activated) {
if (this.isFollowingPlayer) GatsModCore.stopFollowingPlayer(true);
if (this.isAutoShooting) this.stopShooting();
this.currentAimAssistTargetCoords = null;
this.currentAimbotTarget = null;
this.lastSelfKills = 0; // Reset kills on death/disconnect
this.overlayCtx?.clearRect(0, 0, this.overlayCanvas.width, this.overlayCanvas.height);
return;
}
if (!this.overlayCtx || !camera?.ctx) return;
this.overlayCtx.clearRect(0, 0, this.overlayCanvas.width, this.overlayCanvas.height);
if (GatsModCore.SETTINGS.ghostDetectEnabled) this.performGhostDetection();
if (GatsModCore.SETTINGS.silencerDetectEnabled) this.performSilencerDetection();
if (GatsModCore.SETTINGS.onKillChatEnabled) this.checkOnKillEvent(me); // ### NEW
if (GatsModCore.SETTINGS.followBotEnabled && this.isFollowingPlayer) this.performFollowBotActions(me);
else if (this.isFollowingPlayer) GatsModCore.stopFollowingPlayer(true);
else if (Object.values(this.simulatedKeys).some(s => s)) this.updateSimulatedKeys([]);
const mouseClicked = this.realMouseButtons > 0;
const aimbotActive = GatsModCore.SETTINGS.aimbotEnabled && (GatsModCore.SETTINGS.alwaysAim || (GatsModCore.SETTINGS.aimbotOnMousePress && mouseClicked));
const spinbotActive = GatsModCore.SETTINGS.spinbotEnabled && !mouseClicked;
this.currentAimAssistTargetCoords = null;
this.currentAimbotTarget = null;
if (spinbotActive) this.performSpinbotActions(me);
if (aimbotActive) this.performAimbotTargeting(me);
if (GatsModCore.SETTINGS.autoAttackEnabled) {
this.performAutoAttack(me);
} else if (this.isAutoShooting) {
this.stopShooting();
}
if (aimbotActive && this.aimTargetScreenCoords) this.currentAimAssistTargetCoords = this.aimTargetScreenCoords;
else if (spinbotActive && this.spinbotTargetScreenCoords) this.currentAimAssistTargetCoords = this.spinbotTargetScreenCoords;
if (this.currentAimAssistTargetCoords && this.originalUpdateMouseData) {
let clientX = this.currentAimAssistTargetCoords.x, clientY = this.currentAimAssistTargetCoords.y;
if (this.gameCanvas) { const rect = this.gameCanvas.getBoundingClientRect(); clientX += rect.left; clientY += rect.top; }
const fakeEvent = { clientX: clientX, clientY: clientY, target: this.gameCanvas, buttons: this.realMouseButtons };
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();
}
}
// ### NEW: On-Kill event checker ###
checkOnKillEvent(me) {
if (!me) return;
// Initialize lastSelfKills if it hasn't been set yet
if (this.lastSelfKills === null || this.lastSelfKills === undefined) {
this.lastSelfKills = me.kills;
return;
}
if (me.kills > this.lastSelfKills) {
modLog("Kill detected, sending on-kill message.");
GatsModCore.sendChatMessage(GatsModCore.SETTINGS.onKillMessage);
}
this.lastSelfKills = me.kills;
}
performGhostDetection() { try { if (!Player || !Player.pool) return; for (const id in Player.pool) { const p = Player.pool[id]; if (p && p.ghillie) p.ghillie = false; } } catch (e) {} }
performSilencerDetection() { try { if (!Bullet || !Bullet.pool) return; for (const id in Bullet.pool) { const b = Bullet.pool[id]; if (b && b.silenced) b.silenced = false; } } catch (e) {} }
startShooting() {
if (this.isAutoShooting) return;
try {
Connection.list[0].send(prepareMessage('key-press', { 'inputId': 6, 'state': 1 }));
this.isAutoShooting = true;
} catch(e) { modLog("Failed to send start shooting command.", true); }
}
stopShooting() {
if (!this.isAutoShooting) return;
try {
Connection.list[0].send(prepareMessage('key-press', { 'inputId': 6, 'state': 0 }));
this.isAutoShooting = false;
} catch(e) { modLog("Failed to send stop shooting command.", true); }
}
getBulletOrigin(player) {
let originX = player.x;
let originY = player.y;
const weaponClass = player.class || 'pistol';
const forward = this.constructor.WEAPON_FORWARD_OFFSETS[weaponClass] || 45;
const sideways = -18;
const angleRad = (player.playerAngle || 0) * Math.PI / 180;
originX += forward * Math.cos(angleRad) + sideways * Math.cos(angleRad + Math.PI / 2);
originY += forward * Math.sin(angleRad) + sideways * Math.sin(angleRad + Math.PI / 2);
return { x: originX, y: originY };
}
hasLineOfSight(p1, p2) {
if (typeof MapObject === 'undefined' || !MapObject.pool) return true;
for (const id in MapObject.pool) {
const obs = MapObject.pool[id];
if (obs?.activated && (obs.type === 'crate' || obs.type === 'longCrate' || obs.type === 'userCrate')) {
if (this.isLineIntersectingRotatedRect(p1, p2, obs)) {
return false;
}
}
}
return true;
}
isLineIntersectingRotatedRect(p1, p2, rect) {
const settings = GatsModCore.SETTINGS;
const angle = -(rect.angle || 0) * Math.PI / 180;
const cos = Math.cos(angle), sin = Math.sin(angle);
const cx = rect.x + settings.obstacleOffsetX;
const cy = rect.y + settings.obstacleOffsetY;
const p1r = { x: cos * (p1.x - cx) - sin * (p1.y - cy), y: sin * (p1.x - cx) + cos * (p1.y - cy) };
const p2r = { x: cos * (p2.x - cx) - sin * (p2.y - cy), y: sin * (p2.x - cx) + cos * (p2.y - cy) };
let w = rect.width, h = rect.height;
if (rect.type === 'crate') { w = 100; h = 100; }
else if (rect.type === 'userCrate') { w = 40; h = 40; }
const halfW = w / 2, halfH = h / 2;
const rectMin = { x: -halfW, y: -halfH };
const rectMax = { x: halfW, y: halfH };
const dx = p2r.x - p1r.x;
const dy = p2r.y - p1r.y;
let t0 = 0, t1 = 1;
const p = [-dx, dx, -dy, dy];
const q = [p1r.x - rectMin.x, rectMax.x - p1r.x, p1r.y - rectMin.y, rectMax.y - p1r.y];
for(let i = 0; i < 4; i++) {
if (p[i] === 0) {
if (q[i] < 0) return true;
} else {
const t = q[i] / p[i];
if (p[i] < 0) {
if (t > t1) return false;
t0 = Math.max(t0, t);
} else {
if (t < t0) return false;
t1 = Math.min(t1, t);
}
}
}
return t0 < t1;
}
getCurrentMaxRange(player) {
if (!player) return 0;
const weaponClass = player.class || 'pistol';
let baseRange = GatsModCore.WEAPON_BASE_RANGES[weaponClass] || 425; // Default to pistol range
if (player.levelUpgrades) {
const hasLongRange = Object.values(player.levelUpgrades).includes('longRange');
if (hasLongRange) {
baseRange *= GatsModCore.LONG_RANGE_MULTIPLIER;
}
}
return baseRange;
}
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.getCurrentMaxRange(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);
}
// ### 変更点 ###
// 敵がダッシュ中の場合、予測係数を1/3に減らす
if (p.dashing) {
actualPredictionFactor /= 3;
}
// ### 変更ここまで ###
const worldTargetX = p.x + (baseDisplacementX * actualPredictionFactor);
const worldTargetY = p.y + (baseDisplacementY * actualPredictionFactor);
return { x: worldTargetX, y: worldTargetY };
}
performAimbotTargeting(me) {
if (!this.gameCanvas) { this.aimTargetScreenCoords = null; this.currentAimbotTarget = null; return; }
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 / (settings.aimAtMouseClosest ? 1 : (settings.espScale || 1)))**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 { x: worldTargetX, y: worldTargetY } = this.calculatePredictedPosition(p, me);
const screenTargetXUnscaled = canvasCenterX + (worldTargetX - me.x);
const screenTargetYUnscaled = canvasCenterY + (worldTargetY - me.y);
const distToRefSq = (screenTargetXUnscaled - refX)**2 + (screenTargetYUnscaled - 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) / (settings.espScale || 1) + settings.espOffsetX,
y: canvasCenterY + (targetCandidate.y_world - me.y) / (settings.espScale || 1) + 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 { espColors, espScale, espOffsetX, espOffsetY, obstacleOffsetX, obstacleOffsetY, obstacleEspColor } = GatsModCore.SETTINGS;
if (!this.gameCanvas || !camera?.ctx) return;
try {
if (typeof landMine !== 'undefined' && Array.isArray(landMine) && landMine[0]) {
landMine[0].forEach((a, i) => {
if (landMine[0][i] && landMine[0][i][1]) {
landMine[0][i][1][3] = "#000000";
}
});
}
} catch (e) {}
const canvasCenterX = this.gameCanvas.width / 2, canvasCenterY = this.gameCanvas.height / 2;
ctx.save();
ctx.textAlign = 'center';
ctx.font = 'bold 10px Arial';
if (GatsModCore.SETTINGS.autoAttackShowRadius) {
ctx.beginPath();
ctx.arc(canvasCenterX, canvasCenterY, GatsModCore.SETTINGS.autoAttackRadius, 0, 2 * Math.PI);
ctx.strokeStyle = 'rgba(255, 255, 0, 0.5)';
ctx.lineWidth = 1;
ctx.stroke();
}
if (GatsModCore.SETTINGS.obstacleEspEnabled && MapObject && MapObject.pool) {
ctx.strokeStyle = obstacleEspColor;
ctx.lineWidth = 2;
for (const id in MapObject.pool) {
const obj = MapObject.pool[id];
if (!obj || !obj.activated) continue;
let width = obj.width || 50;
let height = obj.height || 50;
if (obj.type === 'crate') { width = 100; height = 100; }
if (obj.type === 'userCrate') { width = 40; height = 40; }
const objX = obj.x + obstacleOffsetX;
const objY = obj.y + obstacleOffsetY;
const screenX = canvasCenterX + (objX - me.x) / espScale + espOffsetX;
const screenY = canvasCenterY + (objY - me.y) / espScale + espOffsetY;
ctx.save();
ctx.translate(screenX, screenY);
ctx.rotate((obj.angle || 0) * Math.PI / 180);
ctx.strokeRect(-width / 2 / espScale, -height / 2 / espScale, width / espScale, height / espScale);
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) / espScale + espOffsetX;
const startScreenY = canvasCenterY + (startPoint.y - me.y) / espScale + espOffsetY;
const endScreenX = canvasCenterX + (endPoint.x - me.x) / espScale + espOffsetX;
const endScreenY = canvasCenterY + (endPoint.y - me.y) / espScale + 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 isTeammate = (me.teamCode !== 0 && p.teamCode === me.teamCode);
if (isTeammate && !GatsModCore.SETTINGS.espShowTeammates) continue;
const relX = (p.x - me.x) / espScale, relY = (p.y - me.y) / espScale;
const screenX = canvasCenterX + relX + espOffsetX, screenY = canvasCenterY + relY + espOffsetY;
const radiusOnScreen = (p.radius || 15) / espScale;
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 (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 predRelX = (predictedPos.x - me.x) / espScale;
const predRelY = (predictedPos.y - me.y) / espScale;
const predScreenX = canvasCenterX + predRelX + espOffsetX;
const predScreenY = canvasCenterY + predRelY + 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) / espScale + espOffsetX;
const originY = canvasCenterY + (originWorld.y - me.y) / espScale + 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();
}
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) }; }
// ### NEW ### Helper function to calculate physics parameters for the bot
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;
// Conceptual model parameters from the report
const ACCELERATION = maxSpeed / 16.0;
const FRICTION = 0.94;
return { maxSpeed, ACCELERATION, FRICTION };
}
// ### MODIFIED ### Complete overhaul of the follow bot logic
performFollowBotActions(me) {
if (!this.isFollowingPlayer || this.followingPlayerId === null) {
GatsModCore.stopFollowingPlayer(true);
return;
}
const targetPlayer = Player.pool[this.followingPlayerId];
if (!targetPlayer?.activated || targetPlayer.hp <= 0) {
GatsModCore.stopFollowingPlayer(); // Stop and show message
return;
}
// 1. Get current player's physics properties
const { maxSpeed, ACCELERATION, FRICTION } = this.getBotPhysics(me);
// 2. Calculate vector to target
const dx = targetPlayer.x - me.x;
const dy = targetPlayer.y - me.y;
const distance = Math.hypot(dx, dy);
// 3. Calculate Desired Velocity (the "Goal")
let desiredSpdX = 0;
let desiredSpdY = 0;
const stopDistance = 15; // Distance at which to aim for zero velocity
if (distance > stopDistance) {
// The "brakingFactor" determines how early the bot starts to slow down.
// A larger value means it brakes more gently from further away.
const brakingFactor = 12.0;
const desiredSpeed = Math.min(maxSpeed, distance / brakingFactor);
desiredSpdX = (dx / distance) * desiredSpeed;
desiredSpdY = (dy / distance) * desiredSpeed;
}
// 4. Determine Required Acceleration to reach desired velocity
// We calculate the acceleration needed to counteract friction and move our current
// velocity (this.botSpdX/Y) towards the desired velocity.
const requiredAccelX = (desiredSpdX / FRICTION) - this.botSpdX;
const requiredAccelY = (desiredSpdY / FRICTION) - this.botSpdY;
// 5. Convert Required Acceleration to Key Presses
let keysToPress = [];
let isPressingKeyX = 0;
let isPressingKeyY = 0;
const keyPressThreshold = ACCELERATION * 0.1; // Press key if required accel is > 10% of one-frame-accel
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);
// 6. Update our internal simulation of velocity for the next frame
let appliedAccelX = isPressingKeyX * ACCELERATION;
let appliedAccelY = isPressingKeyY * ACCELERATION;
if (isPressingKeyX !== 0 && isPressingKeyY !== 0) {
appliedAccelX *= GatsModCore.PLAYER_SPEEDS.diagonalCorrection;
appliedAccelY *= GatsModCore.PLAYER_SPEEDS.diagonalCorrection;
}
// Apply acceleration and then friction
this.botSpdX = (this.botSpdX + appliedAccelX) * FRICTION;
this.botSpdY = (this.botSpdY + appliedAccelY) * FRICTION;
// Clamp the simulated velocity vector to its maxSpeed
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 }; const codeMap = { 'w': 'KeyW', 'a': 'KeyA', 's': 'KeyS', 'd': 'KeyD' }; if (!keyMap[key]) return; document.dispatchEvent(new KeyboardEvent(type, { key: key, code: codeMap[key], keyCode: keyMap[key], bubbles: true, cancelable: true, composed: true })); }
startFollowingPlayer() { const targetName = GatsModCore.SETTINGS.followBotTargetName; if (!targetName) { alert("Set a target name first using the 'Follow Bot Control' panel."); return; } const foundPlayer = Object.values(Player.pool).find(p => p.username === targetName && p.activated && p.id !== selfId); if (foundPlayer) { this.isFollowingPlayer = true; this.followingPlayerId = foundPlayer.id; modLog(`Started following ${targetName}`); } else { alert(`Player "${targetName}" not found or is not active.`); this.isFollowingPlayer = false; this.followingPlayerId = null; } this.simpleGui.updateFollowBotStatusDisplay(); }
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()}`); } };
// ### MODIFIED ### Also resets the bot's internal velocity state.
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;
// Reset simulated velocity
gatsModInstance.botSpdX = 0;
gatsModInstance.botSpdY = 0;
if (!silent) modLog("FollowBot stopped.");
gatsModInstance?.simpleGui?.updateFollowBotStatusDisplay();
};
static showPlayerList() { const modal = document.getElementById('player-list-modal'); const grid = document.getElementById('player-list-grid'); if (!modal || !grid || typeof Player === 'undefined' || !Player.pool || typeof selfId === 'undefined') { alert("Player data not available yet."); return; } grid.innerHTML = ''; Object.values(Player.pool).filter(p => p?.activated && p.id !== selfId && p.username).forEach(p => { const btn = document.createElement('button'); btn.className = 'player-list-button'; btn.innerText = p.username; btn.title = p.username; btn.onclick = () => { GatsModCore.SETTINGS.followBotTargetName = p.username; GatsModCore.saveSettings(); gatsModInstance?.simpleGui?.updateFollowBotStatusDisplay(); gatsModInstance?.startFollowingPlayer(); modal.style.display = 'none'; }; grid.appendChild(btn); }); modal.style.display = 'flex'; };
static startChatScroll() { if (!GatsModCore.SETTINGS.chatScrollEnabled) { alert("Chat Scroller is disabled in settings."); return; } if (GatsModCore.SETTINGS.chatScrollActive) GatsModCore.stopChatScroll(); GatsModCore.SETTINGS.chatScrollActive = true; GatsModCore.chatScrollCurrentIndex = 0; gatsModInstance?.simpleGui?.updateScrollingTextDisplay(GatsModCore.SETTINGS.chatScrollText); modLog("ChatScroller: Started."); GatsModCore.chatScrollLoop(); };
static stopChatScroll() { if (GatsModCore.chatScrollIntervalId) { clearTimeout(GatsModCore.chatScrollIntervalId); GatsModCore.chatScrollIntervalId = null; } GatsModCore.SETTINGS.chatScrollActive = false; modLog("ChatScroller: Stopped."); };
static chatScrollLoop() { if (!GatsModCore.SETTINGS.chatScrollActive || !GatsModCore.SETTINGS.chatScrollEnabled || GatsModCore.isInputActive) { if (!GatsModCore.SETTINGS.chatScrollEnabled || GatsModCore.isInputActive) GatsModCore.stopChatScroll(); return; } if (!Connection.list?.[0]?.socket?.send) { modLog("ChatScroller: Connection not available.", true); GatsModCore.stopChatScroll(); return; } const s = GatsModCore.SETTINGS.chatScrollText; if (!s?.length) { GatsModCore.stopChatScroll(); return; } let maxLength = GatsModCore.SETTINGS.chatScrollMaxLength; if (s.length < maxLength) maxLength = s.length; let displayText = s.substring(GatsModCore.chatScrollCurrentIndex, GatsModCore.chatScrollCurrentIndex + maxLength); if (displayText.length < maxLength && s.length > maxLength) displayText += " " + s.substring(0, maxLength - displayText.length -1); displayText = displayText.trim(); GatsModCore.sendChatMessage(displayText); GatsModCore.chatScrollCurrentIndex = (GatsModCore.chatScrollCurrentIndex + 1) % s.length; if (GatsModCore.SETTINGS.chatScrollActive) GatsModCore.chatScrollIntervalId = setTimeout(GatsModCore.chatScrollLoop, GatsModCore.SETTINGS.chatScrollSpeed); };
// ### NEW: Sequencer functions ###
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); } }
// ### NEW: Central chat sending function ###
static sendChatMessage(message) {
if (!message || message.trim().length === 0) return;
if (!Connection.list?.[0]?.socket?.send) { modLog("Chat: Connection not available.", true); return; }
try {
const sanitizedMessage = message.replaceAll(",", "~");
Connection.list[0].socket.send(`c,${sanitizedMessage}\x00`);
} catch (e) {
modLog(`Chat: Error sending message: ${e.message}`, true);
}
}
static editScrollPreset(index) { if (GatsModCore.SETTINGS.chatPresetMessages?.[index] === undefined) return; const currentMessage = GatsModCore.SETTINGS.chatPresetMessages[index]; const newMessage = prompt(`Edit Preset ${index + 1}:`, currentMessage.trim()); if (newMessage !== null ) { GatsModCore.SETTINGS.chatPresetMessages[index] = newMessage.trim() === "" ? " " : newMessage; GatsModCore.saveSettings(); gatsModInstance?.simpleGui?.updatePresetButtonLabel(index, GatsModCore.SETTINGS.chatPresetMessages[index]); if(GatsModCore.SETTINGS.chatScrollText.trim() === currentMessage.trim()) { GatsModCore.SETTINGS.chatScrollText = GatsModCore.SETTINGS.chatPresetMessages[index]; gatsModInstance?.simpleGui?.updateScrollingTextDisplay(GatsModCore.SETTINGS.chatScrollText); } alert(`Preset ${index + 1} updated.`);}};
static setScrollPreset(index) { if (GatsModCore.SETTINGS.chatPresetMessages?.[index] !== undefined) { const presetText = GatsModCore.SETTINGS.chatPresetMessages[index]; GatsModCore.SETTINGS.chatScrollText = presetText; GatsModCore.saveSettings(); gatsModInstance?.simpleGui?.updateScrollingTextDisplay(presetText); GatsModCore.sendChatMessage(presetText); if (GatsModCore.SETTINGS.chatScrollActive) GatsModCore.startChatScroll(); modLog(`ChatScroller: Preset ${index + 1} set and sent once.`);}};
}
let gatsModInstance = null;
let game_original_updateMouseData_ref = null;
let modInitializationAttempted = false;
const gatsModLauncherInterval = setInterval(() => {
if (modInitializationAttempted) { clearInterval(gatsModLauncherInterval); return; }
if (typeof updateMouseData === 'function' && typeof Player !== 'undefined' && Player.pool && typeof Connection !== 'undefined' && Connection.list?.[0]?.socket && typeof prepareMessage === 'function' && typeof camera === 'object' && camera?.ctx && typeof selfId !== 'undefined' && document.getElementById('canvas')) {
modLog(`[Launcher] Game ready. Initializing mod.`);
clearInterval(gatsModLauncherInterval);
modInitializationAttempted = true;
game_original_updateMouseData_ref = updateMouseData;
updateMouseData = function(eventData) {
if (!gatsModInstance) {
try {
gatsModInstance = new GatsModCore();
gatsModInstance.originalUpdateMouseData = game_original_updateMouseData_ref;
const original_clearRect = CanvasRenderingContext2D.prototype.clearRect;
CanvasRenderingContext2D.prototype.clearRect = function(...args) {
original_clearRect.apply(this, args);
if (gatsModInstance && this.canvas.id === 'canvas' && Player.pool?.[selfId]?.activated) {
try { gatsModInstance.mainGameTick(); } catch (e) { modLog(`mainGameTick error: ${e.stack}`, true); }
}
};
modLog(`[Launcher] clearRect hooked for game tick.`);
} catch (e) {
modLog(`[Launcher] GatsModCore instantiation failed: ${e.stack}`, true);
updateMouseData = game_original_updateMouseData_ref;
return;
}
}
return game_original_updateMouseData_ref.call(this, eventData);
};
modLog(`[Launcher] updateMouseData wrapped. Mod will fully initialize on first mouse event.`);
}
}, 250);
setTimeout(() => {
if (!modInitializationAttempted) {
modLog(`[Launcher] Timeout: Game did not initialize required components. Mod will not start.`, true);
clearInterval(gatsModLauncherInterval);
}
}, 20000);
})();
(function() {
'use strict';
window.addEventListener ("load", onload);
function onload() {
let processMessageTmp = processMessage;
processMessage = function(event) {
processMessageTmp(event);
let decoded = new TextDecoder().decode(event.data);
if (!decoded.includes("a,")) return;
camera.update = function() {
let player = Player.pool[selfId];
if (camera.trackingId) {
camera.x = player.x - hudXPosition;
camera.y = player.y - hudYPosition;
}
}
}
}
})();
(function() {
'use strict';
// --- 1. 設定オブジェクト ---
const config = {
colors: {
mainBg: 'rgba(10, 10, 15, 0.9)',
secondaryBg: 'rgba(25, 25, 30, 0.95)',
accent: '#00ffff', // Cyan
accentHover: '#7fffff',
accentBorder: '#00cccc',
glow: 'rgba(0, 255, 255, 0.6)',
textLight: '#e0e0e0',
textDim: '#999',
btnSuccessBg: '#008080',
btnSuccessBorder: '#006666',
canvasBg: '#F5F5F5',
gridBorder: 'rgba(0, 0, 0, 0.1)',
bulletCore: '#00ffff',
bulletGlow: 'rgba(0, 255, 255, 0.8)',
bulletTrail: 'rgba(0, 255, 255, 0.3)',
playerHp: '#ff3366', // Hot Pink
playerArmor: '#3399ff', // Bright Blue
playerScore: '#ffcc00', // Gold
hudBg: 'rgba(18, 18, 22, 0.8)',
hudBorder: 'rgba(0, 255, 255, 0.4)',
deathOverlay: 'rgba(180, 0, 0, 0.4)',
damageFlash: 'rgba(255, 0, 50, 0.6)', // 被ダメージエフェクトの色
teamRed: 'rgba(255, 51, 102, 0.8)',
teamBlue: 'rgba(51, 153, 255, 0.8)',
selfHighlight: '#FFFFFF', // 白色に変更
espSafe: 'rgba(255, 255, 255, 0.6)',
espWarning: 'rgba(255, 204, 0, 0.8)',
espDanger: 'rgba(255, 51, 102, 0.9)',
notificationBg: 'rgba(25, 25, 30, 0.9)',
notificationText: '#e0e0e0',
notificationStroke: 'rgba(0, 0, 0, 0.9)',
crateBody: '#4a4a5a',
crateLid: '#3a3a4a',
userCrateBody: '#4a4a5a',
userCrateLid: '#00ffff',
playerAttachedHp: '#ff3366',
playerAttachedArmor: '#333333',
playerAttachedReload: '#ffcc00',
playerAttachedSkill: '#00ffff',
playerAttachedBg: 'rgba(0, 0, 0, 0.3)',
},
animations: {
transitionSpeed: '0.2s',
easing: 'ease-out',
},
perkGUI: {
enabled: true,
},
esp: {
enabled: true,
},
notifications: {
enabled: true,
maxDisplay: 4,
displayTime: 4000, // ms
},
// --- スキル自動取得機能の設定 ---
autoPerk: {
enabled: true,
settings: {
tier1: null,
tier2: null,
tier3: null
}
}
};
// --- 2. CSSスタイルの注入 ---
const themeStyles = `
:root {
--main-bg: ${config.colors.mainBg};
--secondary-bg: ${config.colors.secondaryBg};
--accent-color: ${config.colors.accent};
--accent-hover: ${config.colors.accentHover};
--accent-border: ${config.colors.accentBorder};
--text-color-light: ${config.colors.textLight};
--text-color-dim: ${config.colors.textDim};
--btn-success-bg: ${config.colors.btnSuccessBg};
--btn-success-border: ${config.colors.btnSuccessBorder};
--glow-color: ${config.colors.glow};
--font-family: 'Roboto', 'Segoe UI', sans-serif;
--transition-speed: ${config.animations.transitionSpeed};
--easing: ${config.animations.easing};
}
body {
background-color: #0a0a0f !important; font-family: var(--font-family);
background-image:
radial-gradient(circle at 1px 1px, rgba(0, 255, 255, 0.15) 1px, transparent 0),
radial-gradient(circle at 25px 25px, rgba(0, 255, 255, 0.05) 1px, transparent 0);
background-size: 50px 50px;
}
::selection { background: var(--accent-color); color: #000; }
#cover { background-color: #101010; }
#loading {
font-family: var(--font-family); font-weight: 700;
text-shadow: 0 0 20px var(--glow-color);
animation: loading-pulse 1.5s infinite ease-in-out;
}
@keyframes loading-pulse {
0%, 100% { color: var(--text-color-light); transform: scale(1); }
50% { color: var(--accent-color); transform: scale(1.05); }
}
#slct, .modal-content { animation: fade-in-up 0.4s var(--easing) forwards; }
@keyframes fade-in-up { from { opacity: 0; transform: translateY(15px); } to { opacity: 1; transform: translateY(0); } }
#slct {
background: var(--main-bg);
border: 1px solid var(--accent-border); border-radius: 10px;
box-shadow: 0 0 30px rgba(0, 255, 255, 0.2); color: var(--text-color-light);
padding: 25px;
}
.selection-title {
background-color: transparent !important; color: var(--accent-color);
text-shadow: 0 0 8px var(--glow-color); font-size: 18px; font-weight: bold;
border-bottom: 1px solid var(--accent-border); padding-bottom: 8px;
margin-bottom: 15px; text-align: left; width: 100% !important;
}
.weaponSelector, .colorSelector, .armorSelector {
background-color: var(--secondary-bg) !important; border: 1px solid #444;
border-radius: 8px; transition: all var(--transition-speed) var(--easing);
position: relative; overflow: hidden;
}
.weaponSelector:hover, .colorSelector:hover, .armorSelector:hover {
border-color: var(--accent-color); transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.4);
}
.selected-box {
border: 2px solid var(--accent-color) !important;
box-shadow: 0 0 12px var(--glow-color), inset 0 0 10px rgba(0, 255, 255, 0.15);
transform: translateY(-2px);
}
#playButton, #serversBtn, #gametypeDropdown, #reconnectButton {
border-radius: 6px !important; border-width: 1px !important; border-style: solid !important;
transition: all var(--transition-speed) var(--easing); text-shadow: 0 0 5px rgba(0,0,0,0.5);
color: var(--text-color-light) !important; font-family: var(--font-family); font-weight: 700;
}
#playButton { background: var(--accent-color) !important; border-color: var(--accent-border) !important; box-shadow: 0 0 15px var(--glow-color); color: #000 !important; }
#playButton:hover { transform: scale(1.05); box-shadow: 0 0 30px var(--glow-color); filter: brightness(1.1); }
#serversBtn, #gametypeDropdown { background-color: var(--secondary-bg) !important; border-color: #555 !important; }
#serversBtn:hover, #gametypeDropdown:hover { border-color: var(--accent-color) !important; }
.modal-content {
background: #111 !important; color: var(--text-color-light);
border: 1px solid var(--accent-border) !important; border-radius: 10px !important;
box-shadow: 0 0 35px rgba(0, 255, 255, 0.25);
}
.modal-header { border-bottom: 1px solid var(--accent-border); padding: 15px 20px;}
.modal-title { color: var(--accent-color); font-weight: bold; text-shadow: 0 0 5px var(--glow-color); }
.modal-header .close { color: var(--text-color-light); opacity: 0.7; text-shadow: none; transition: all 0.2s; }
.modal-header .close:hover { color: var(--accent-color); opacity: 1; transform: rotate(90deg) scale(1.2); }
.form-control {
background-color: #1a1a1a !important; border: 1px solid #444 !important;
color: var(--text-color-light) !important; border-radius: 4px !important;
transition: all var(--transition-speed) var(--easing);
}
.form-control:focus { border-color: var(--accent-color) !important; box-shadow: 0 0 8px var(--glow-color); }
.btn-success { background-color: var(--btn-success-bg); border-color: var(--btn-success-border); }
.btn-success:hover { filter: brightness(1.2); }
.ad-group, #detect, .ads.ad.adsbox, #adHome { display: none !important; }
/* --- サーバー選択画面のスタイル改善 --- */
#serverModal .modal-content { background-color: var(--secondary-bg) !important; }
#serverModal .modal-body { padding: 0; }
#serverModal .nav-tabs {
background-color: rgba(10, 10, 15, 0.8);
border-bottom: 1px solid var(--accent-border);
padding: 10px 10px 0 10px;
}
#serverModal .nav-tabs > li > a {
color: var(--text-color-dim); border-radius: 6px 6px 0 0 !important;
border: 1px solid transparent !important; margin-right: 5px;
background-color: rgba(40, 40, 45, 0.8) !important;
transition: var(--transition-speed) var(--easing);
}
#serverModal .nav-tabs > li > a:hover { background-color: rgba(55, 55, 60, 0.9) !important; color: var(--text-color-light); }
#serverModal .nav-tabs > li.active > a,
#serverModal .nav-tabs > li.active > a:hover,
#serverModal .nav-tabs > li.active > a:focus {
background-color: var(--secondary-bg) !important; color: var(--accent-color) !important;
border: 1px solid var(--accent-border) !important; border-bottom-color: transparent !important;
font-weight: bold;
}
#serverModal .table-responsive { border: none; }
#serverModal .table { background-color: transparent; color: var(--text-color-light); margin-bottom: 0; }
#serverModal .table > thead > tr > th {
background-color: rgba(10, 10, 15, 0.5); color: var(--text-color-light);
border-bottom: 2px solid var(--accent-border); text-align: left; padding: 12px;
}
#serverModal .table > tbody > tr {
background-color: transparent;
border-bottom: 1px solid rgba(0, 255, 255, 0.1);
transition: background-color var(--transition-speed) var(--easing);
}
#serverModal .table > tbody > tr:last-child { border-bottom: none; }
#serverModal .table > tbody > tr:hover { background-color: rgba(0, 255, 255, 0.08); }
#serverModal .table > tbody > tr > td {
border-top: none; vertical-align: middle; padding: 12px;
color: var(--text-color-dim);
}
#serverModal .table > tbody > tr > td:first-child { color: var(--text-color-light); font-weight: 500; }
#serverModal .table > tbody > tr > td:nth-child(2) { color: var(--text-color-light); }
#serverModal .btn-success {
background-color: transparent !important;
border: 1px solid var(--accent-color) !important;
color: var(--accent-color) !important;
font-weight: bold; padding: 6px 14px; border-radius: 5px;
transition: all var(--transition-speed) var(--easing);
}
#serverModal .btn-success:hover {
background-color: var(--accent-color) !important;
color: #000 !important;
box-shadow: 0 0 10px var(--glow-color);
}
#serverModal .btn-success[disabled] {
background-color: transparent !important; border-color: #555 !important;
color: #555 !important; opacity: 0.6; cursor: not-allowed;
}
/* --- Discordボタンのスタイル --- */
#discord-link-btn {
display: block; text-align: center;
margin-top: 15px; padding: 10px;
background-color: var(--secondary-bg);
color: var(--text-color-dim);
text-decoration: none; border-radius: 6px;
border: 1px solid #555;
font-weight: 500; font-size: 13px;
transition: all var(--transition-speed) var(--easing);
}
#discord-link-btn:hover {
color: var(--accent-color);
border-color: var(--accent-color);
background-color: rgba(0, 255, 255, 0.1);
}
/* --- 通知システムのスタイル --- */
#notification-container {
position: fixed; top: 20px; left: 150px;
display: flex; flex-direction: column;
gap: 10px; z-index: 2000; pointer-events: none;
}
.notification {
background: ${config.colors.notificationBg};
color: ${config.colors.notificationText};
padding: 12px 18px; border-radius: 6px;
border-left: 4px solid var(--accent-color);
box-shadow: 0 3px 10px rgba(0,0,0,0.5);
font-size: 14px; font-weight: 500;
animation: slide-in-right 0.4s ease-out, fade-out 0.4s ease-in ${config.notifications.displayTime / 1000}s forwards;
}
@keyframes slide-in-right { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
@keyframes fade-out { from { opacity: 1; } to { opacity: 0; transform: translateY(-10px); } }
/* --- スキル自動取得UIのスタイル --- */
#auto-perk-container { margin-top: 10px; }
.auto-perk-slots {
display: flex; justify-content: space-around; gap: 10px;
}
.perk-slot {
width: 65px; height: 65px;
background-color: var(--secondary-bg); border: 1px solid #444;
border-radius: 8px; cursor: pointer;
transition: all var(--transition-speed) var(--easing);
display: flex; align-items: center; justify-content: center;
font-size: 24px; font-weight: bold; color: var(--text-color-dim);
background-size: 85%; background-position: center; background-repeat: no-repeat;
}
.perk-slot:hover {
border-color: var(--accent-color); transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.4);
}
#perk-selection-modal {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.7); display: flex;
align-items: center; justify-content: center; z-index: 2001;
}
#perk-selection-modal .modal-content {
background: var(--secondary-bg) !important; padding: 20px;
border-radius: 10px; border: 1px solid var(--accent-border);
max-width: 500px;
}
#perk-selection-modal h3 {
color: var(--accent-color); margin-top: 0; text-align: center;
border-bottom: 1px solid var(--accent-border); padding-bottom: 10px;
}
.perk-grid {
display: grid; grid-template-columns: repeat(auto-fill, minmax(65px, 1fr));
gap: 10px; margin-top: 15px;
}
.perk-icon-modal {
width: 65px; height: 65px;
background-color: #1a1a1a; border: 1px solid #444;
border-radius: 6px; cursor: pointer;
transition: all 0.2s ease;
background-size: 85%; background-position: center; background-repeat: no-repeat;
display: flex; align-items: center; justify-content: center;
font-weight: bold; color: var(--text-color-dim);
}
.perk-icon-modal:hover {
border-color: var(--accent-color);
transform: scale(1.1);
}
`;
GM_addStyle(themeStyles);
const notificationContainer = document.createElement('div');
notificationContainer.id = 'notification-container';
document.body.appendChild(notificationContainer);
// --- 3. 新しい統合UI描画ロジック ---
const customUI = {
animatedHud: { hp: 0, armor: 0, score: 0, isInitialized: false },
lastHp: 100,
hpDropTime: 0,
deathFade: 0,
damageFlashAlpha: 0, // ダメージエフェクトの透明度を管理
drawHealthArmor: function(ctx, player) {
const hudY = unsafeWindow.hudYPosition;
ctx.save();
const barX = 15, barY = hudY * 2 - 65, barW = 250, barH = 18, barSpacing = 5;
ctx.globalAlpha = 0.85;
ctx.fillStyle = config.colors.hudBg;
ctx.strokeStyle = config.colors.hudBorder;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.roundRect(barX - 4, barY - barH - barSpacing - 4, barW + 8, barH * 2 + barSpacing + 8, 5);
ctx.fill();
ctx.stroke();
ctx.fillStyle = '#300'; ctx.fillRect(barX, barY - barH - barSpacing, barW, barH);
ctx.fillStyle = config.colors.playerHp; ctx.fillRect(barX, barY - barH - barSpacing, barW * (this.animatedHud.hp / 100), barH);
if (this.lastHp > player.hp) {
this.hpDropTime = Date.now();
this.damageFlashAlpha = 1.0;
}
this.lastHp = player.hp;
const hpFlashDuration = 300;
if (Date.now() - this.hpDropTime < hpFlashDuration) {
const flashAlpha = 1 - (Date.now() - this.hpDropTime) / hpFlashDuration;
ctx.fillStyle = `rgba(255, 255, 255, ${flashAlpha * 0.7})`;
ctx.fillRect(barX, barY - barH - barSpacing, barW, barH);
}
ctx.fillStyle = '#002233'; ctx.fillRect(barX, barY, barW, barH);
ctx.fillStyle = config.colors.playerArmor; ctx.fillRect(barX, barY, barW * (this.animatedHud.armor / 110), barH);
ctx.globalAlpha = 1.0;
ctx.fillStyle = config.colors.textLight;
ctx.font = `bold 12px ${config.fontFamily}`;
ctx.shadowColor = 'black'; ctx.shadowBlur = 3;
ctx.fillText(`HP: ${player.hp}`, barX + 5, barY - barSpacing - 5);
ctx.fillText(`Armor: ${player.armorAmount}`, barX + 5, barY + 13);
ctx.shadowBlur = 0;
ctx.restore();
},
drawScore: function(ctx, player) {
const hudX = unsafeWindow.hudXPosition;
const hudY = unsafeWindow.hudYPosition;
const scoreLevels = {0: [0, 100], 1: [100, 300], 2: [300, 600], 3: [600, 600]};
const level = unsafeWindow.playerLevel;
const scoreProgress = (level < 4) ? (100 / (scoreLevels[level][1] - scoreLevels[level][0]) * (this.animatedHud.score - scoreLevels[level][0])) : 100;
ctx.save();
const scoreX = hudX - 250, scoreY = hudY * 2 - 35, scoreW = 500, scoreH = 20;
ctx.globalAlpha = 0.85;
ctx.fillStyle = config.colors.hudBg;
ctx.strokeStyle = config.colors.hudBorder;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.roundRect(scoreX - 2, scoreY - 2, scoreW + 4, scoreH + 4, 5);
ctx.fill();
ctx.stroke();
ctx.fillStyle = '#444'; ctx.fillRect(scoreX, scoreY, scoreW, scoreH);
ctx.fillStyle = config.colors.playerScore; ctx.fillRect(scoreX, scoreY, scoreW * (scoreProgress / 100), scoreH);
ctx.globalAlpha = 1.0;
ctx.fillStyle = config.colors.textLight;
ctx.font = `bold 14px ${config.fontFamily}`;
ctx.textAlign = 'center';
ctx.shadowColor = 'black';
ctx.shadowBlur = 5;
ctx.fillText(`Score: ${Math.round(this.animatedHud.score)}`, hudX, scoreY + 15);
ctx.textAlign = 'start';
ctx.restore();
},
drawMinimap: function(ctx, player) {
const mapSize = 120;
const mapX = 20, mapY = 20;
const scale = mapSize / unsafeWindow.GAME_WIDTH;
const gameType = unsafeWindow.gameType;
const self = unsafeWindow.Player.pool[unsafeWindow.selfId];
ctx.save();
ctx.beginPath();
ctx.arc(mapX + mapSize / 2, mapY + mapSize / 2, mapSize / 2 + 3, 0, 2 * Math.PI);
ctx.fillStyle = config.colors.hudBg;
ctx.globalAlpha = 0.85;
ctx.fill();
ctx.strokeStyle = config.colors.hudBorder;
ctx.lineWidth = 2;
ctx.stroke();
ctx.clip();
ctx.fillStyle = config.colors.canvasBg;
ctx.fillRect(mapX, mapY, mapSize, mapSize);
ctx.strokeStyle = 'rgba(0, 255, 255, 0.2)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(mapX + mapSize / 2, mapY); ctx.lineTo(mapX + mapSize / 2, mapY + mapSize);
ctx.moveTo(mapX, mapY + mapSize / 2); ctx.lineTo(mapX + mapSize, mapY + mapSize / 2);
ctx.stroke();
for (const id in unsafeWindow.Player.pool) {
const p = unsafeWindow.Player.pool[id];
if (!p.activated || p.hp <= 0) continue;
const dotX = mapX + p.x * scale;
const dotY = mapY + p.y * scale;
let dotSize = 3;
let dotColor = config.colors.teamRed;
if (p.id === unsafeWindow.selfId) {
dotColor = config.colors.selfHighlight;
dotSize = 4;
} else if (gameType !== 'FFA' && p.teamCode === self.teamCode) {
dotColor = config.colors.teamBlue;
}
ctx.fillStyle = dotColor;
ctx.beginPath();
ctx.arc(dotX, dotY, dotSize, 0, 2 * Math.PI);
ctx.fill();
}
ctx.restore();
},
drawWeaponAmmo: function(ctx, player) {
const hudX = unsafeWindow.hudXPosition;
const hudY = unsafeWindow.hudYPosition;
const boxW = 180, boxH = 60;
const boxX = hudX * 2 - boxW - 15, boxY = hudY * 2 - 75;
ctx.save();
ctx.globalAlpha = 0.85;
ctx.fillStyle = config.colors.hudBg;
ctx.strokeStyle = config.colors.hudBorder;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.roundRect(boxX, boxY, boxW, boxH, 5);
ctx.fill();
ctx.stroke();
ctx.globalAlpha = 1.0;
const weaponIconName = unsafeWindow.weaponIconMap[player.class] || player.class;
if (unsafeWindow.images[weaponIconName]) {
ctx.drawImage(unsafeWindow.images[weaponIconName], boxX + 5, boxY + 5, 50, 50);
}
ctx.fillStyle = config.colors.textLight;
ctx.font = `bold 24px ${config.fontFamily}`;
ctx.textAlign = 'right';
ctx.shadowColor = 'black';
ctx.shadowBlur = 4;
ctx.fillText(`${player.currentBullets} / ${player.maxBullets}`, boxX + boxW - 10, boxY + 40);
if (player.reloading) {
ctx.font = `bold 12px ${config.fontFamily}`;
ctx.fillStyle = config.colors.playerScore;
const reloadText = "RELOADING...";
const textWidth = ctx.measureText(reloadText).width;
ctx.fillText(reloadText, boxX + boxW - 10, boxY + 18);
ctx.fillStyle = config.colors.accent;
ctx.fillRect(boxX + boxW - 10 - textWidth, boxY + 22, textWidth * (player.reloadingFrame / (player.reloadingAnimation ? player.reloadingAnimation.length : 1)), 2);
}
ctx.restore();
},
drawPerks: function(ctx, player) {
const hudX = unsafeWindow.hudXPosition;
const hudY = unsafeWindow.hudYPosition;
const perkSize = 50, perkSpacing = 8;
const startX = hudX * 2 - 215 - (perkSize + perkSpacing) * 3;
const startY = hudY * 2 - 70;
ctx.save();
for (let i = 1; i <= 3; i++) {
const perkName = unsafeWindow.levelUpgrades[i];
const x = startX + (i - 1) * (perkSize + perkSpacing);
ctx.globalAlpha = 0.85;
ctx.fillStyle = config.colors.hudBg;
ctx.strokeStyle = config.colors.hudBorder;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.roundRect(x, startY, perkSize, perkSize, 5);
ctx.fill();
ctx.stroke();
if (perkName && unsafeWindow.images[perkName]) {
ctx.globalAlpha = 1.0;
ctx.drawImage(unsafeWindow.images[perkName], x + 4, startY + 4, perkSize - 8, perkSize - 8);
if (i === 2 && unsafeWindow.level2Timer.current > 0) {
ctx.globalAlpha = 0.7;
ctx.fillStyle = '#000';
const progress = unsafeWindow.level2Timer.current / unsafeWindow.level2Timer.total;
ctx.fillRect(x, startY, perkSize, perkSize * progress);
}
if (['grenade', 'fragGrenade', 'gasGrenade', 'landMine'].includes(perkName)) {
ctx.globalAlpha = 1.0;
ctx.fillStyle = 'white';
ctx.font = `bold 14px ${config.fontFamily}`;
ctx.textAlign = 'right';
ctx.shadowColor = 'black';
ctx.shadowBlur = 2;
ctx.fillText(player.numExplosivesLeft, x + perkSize - 4, startY + perkSize - 4);
ctx.textAlign = 'start';
}
}
}
ctx.restore();
},
drawPlayerAttachedUI: function(ctx, camera, player) {
ctx.save();
ctx.setTransform(1, 0, 0, 1, 0, 0);
const x = unsafeWindow.canvas.width / 2;
const y = unsafeWindow.canvas.height / 2;
const radius = 24;
const hpBarWidth = radius * 2.5;
const hpBarHeight = 6;
const hpY = y + radius + 10;
const hpPercent = player.hp / 100;
ctx.fillStyle = config.colors.playerAttachedBg;
ctx.fillRect(x - hpBarWidth / 2, hpY, hpBarWidth, hpBarHeight);
ctx.fillStyle = config.colors.playerAttachedHp;
ctx.fillRect(x - hpBarWidth / 2, hpY, hpBarWidth * hpPercent, hpBarHeight);
const armorBarY = hpY + hpBarHeight + 2;
const armorMax = player.armor === 1 ? 30 : (player.armor === 2 ? 70 : 110);
const armorPercent = player.armorAmount / armorMax;
ctx.fillStyle = config.colors.playerAttachedBg;
ctx.fillRect(x - hpBarWidth / 2, armorBarY, hpBarWidth, hpBarHeight);
ctx.fillStyle = config.colors.playerAttachedArmor;
ctx.fillRect(x - hpBarWidth / 2, armorBarY, hpBarWidth * armorPercent, hpBarHeight);
if (player.reloading) {
const reloadGaugeHeight = radius * 2;
const reloadGaugeWidth = 6;
const reloadX = x - radius - 15;
const reloadY = y - reloadGaugeHeight / 2;
const reloadPercent = player.reloadingFrame / (player.reloadingAnimation.length * 0.8);
ctx.fillStyle = config.colors.playerAttachedBg;
ctx.fillRect(reloadX, reloadY, reloadGaugeWidth, reloadGaugeHeight);
ctx.fillStyle = config.colors.playerAttachedReload;
ctx.fillRect(reloadX, reloadY + reloadGaugeHeight * (1 - Math.min(1, reloadPercent)), reloadGaugeWidth, reloadGaugeHeight * Math.min(1, reloadPercent));
}
if (unsafeWindow.level2Timer && unsafeWindow.level2Timer.total > 0) {
const skillGaugeHeight = radius * 2;
const skillGaugeWidth = 6;
const skillX = x + radius + 15;
const skillY = y - skillGaugeHeight / 2;
const skillPercent = unsafeWindow.level2Timer.current / unsafeWindow.level2Timer.total;
ctx.fillStyle = config.colors.playerAttachedBg;
ctx.fillRect(skillX, skillY, skillGaugeWidth, skillGaugeHeight);
ctx.fillStyle = config.colors.playerAttachedSkill;
ctx.fillRect(skillX, skillY + skillGaugeHeight * (1 - skillPercent), skillGaugeWidth, skillGaugeHeight * skillPercent);
}
ctx.restore();
},
updateAll: function(player) {
if (!player || unsafeWindow.spectating) {
this.animatedHud.isInitialized = false;
if (unsafeWindow.respawnButtonVisible) {
unsafeWindow.originalDrawHud.apply(this, arguments);
}
return;
}
const ctx = unsafeWindow.ctx;
const camera = unsafeWindow.camera;
if (!this.animatedHud.isInitialized) {
this.animatedHud.hp = player.hp;
this.animatedHud.armor = player.armorAmount;
this.animatedHud.score = player.score;
this.animatedHud.isInitialized = true;
}
this.animatedHud.hp += (player.hp - this.animatedHud.hp) * 0.15;
this.animatedHud.armor += (player.armorAmount - this.animatedHud.armor) * 0.15;
this.animatedHud.score += (player.score - this.animatedHud.score) * 0.15;
this.drawHealthArmor(ctx, player);
this.drawScore(ctx, player);
this.drawMinimap(ctx, player);
this.drawWeaponAmmo(ctx, player);
this.drawPerks(ctx, player);
this.drawPlayerAttachedUI(ctx, camera, player);
}
};
// --- 4. スキル自動取得システム ---
const autoPerkSystem = {
init: function() {
this.loadSettings();
this.createUI();
this.updateUI();
},
loadSettings: function() {
const savedSettings = localStorage.getItem('gatsModAutoPerkSettings');
if (savedSettings) {
config.autoPerk.settings = JSON.parse(savedSettings);
}
},
saveSettings: function() {
localStorage.setItem('gatsModAutoPerkSettings', JSON.stringify(config.autoPerk.settings));
},
createUI: function() {
const armorTitle = document.getElementById('armorTitle');
if (!armorTitle || !armorTitle.parentElement) return;
const container = document.createElement('div');
container.id = 'auto-perk-container';
container.innerHTML = `
<div id="autoPerkTitle" class="selection-title">スキル自動取得</div>
<div class="auto-perk-slots">
<div id="auto-perk-slot-1" class="perk-slot" data-tier="1">1</div>
<div id="auto-perk-slot-2" class="perk-slot" data-tier="2">2</div>
<div id="auto-perk-slot-3" class="perk-slot" data-tier="3">3</div>
</div>
`;
armorTitle.parentElement.insertAdjacentElement('afterend', container);
document.querySelectorAll('.perk-slot').forEach(slot => {
slot.addEventListener('click', (e) => {
const tier = e.target.dataset.tier;
this.showPerkSelectionModal(tier);
});
});
},
updateUI: function() {
for (let i = 1; i <= 3; i++) {
const slot = document.getElementById(`auto-perk-slot-${i}`);
const perkName = config.autoPerk.settings[`tier${i}`];
if (slot) {
if (perkName) {
slot.style.backgroundImage = `url('/img/${perkName}.png')`;
slot.innerText = '';
} else {
slot.style.backgroundImage = 'none';
slot.innerText = i;
}
}
}
},
showPerkSelectionModal: function(tier) {
const perkList = perkGUI[`tier${tier}`];
let modal = document.getElementById('perk-selection-modal');
if (modal) modal.remove();
modal = document.createElement('div');
modal.id = 'perk-selection-modal';
modal.innerHTML = `<div class="modal-content"><h3>Tier ${tier} スキルを選択</h3><div class="perk-grid"></div></div>`;
document.body.appendChild(modal);
const grid = modal.querySelector('.perk-grid');
const noneOption = document.createElement('div');
noneOption.className = 'perk-icon-modal';
noneOption.innerText = 'なし';
noneOption.onclick = () => this.selectAutoPerk(tier, null);
grid.appendChild(noneOption);
perkList.forEach(perkName => {
const icon = document.createElement('div');
icon.className = 'perk-icon-modal';
icon.style.backgroundImage = `url('/img/${perkName}.png')`;
icon.title = perkName.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase());
icon.onclick = () => this.selectAutoPerk(tier, perkName);
grid.appendChild(icon);
});
modal.addEventListener('click', (e) => {
if (e.target.id === 'perk-selection-modal') modal.remove();
});
},
selectAutoPerk: function(tier, perkName) {
config.autoPerk.settings[`tier${tier}`] = perkName;
this.saveSettings();
this.updateUI();
const modal = document.getElementById('perk-selection-modal');
if (modal) modal.remove();
},
attemptAutoUpgrade: function(tier) {
if (!config.autoPerk.enabled) return false;
const perkToSelect = config.autoPerk.settings[`tier${tier}`];
if (perkToSelect) {
const alreadySelected = Object.values(unsafeWindow.levelUpgrades).includes(perkToSelect);
if (!alreadySelected) {
console.log(`GatsModV2: Auto-selecting Tier ${tier} perk: ${perkToSelect}`);
unsafeWindow.levelUpgrades[tier] = perkToSelect;
unsafeWindow.Connection.list[0].send(unsafeWindow.prepareMessage('upgrade', {
upgrade: perkToSelect,
upgradeLevel: tier
}));
perkGUI.hideTier(tier);
return true;
}
}
return false;
}
};
// --- 5. スキル選択GUI機能 ---
const perkGUI = {
tier1: ['bipod', 'optics', 'thermal', 'armorPiercing', 'extended', 'grip', 'silencer', 'lightweight', 'longRange', 'thickSkin'],
tier2: ['shield', 'firstAid', 'grenade', 'knife', 'engineer', 'dash', 'gasGrenade', 'landMine', 'fragGrenade'],
tier3: ['bipod', 'optics', 'thermal', 'armorPiercing', 'extended', 'grip', 'silencer', 'lightweight', 'longRange', 'thickSkin'],
isInitialized: false,
tierVisible: { 1: false, 2: false, 3: false },
init: function() {
if (this.isInitialized) return;
const guiContainer = document.createElement('div');
guiContainer.id = 'perk-gui-container';
document.body.appendChild(guiContainer);
this.applyStyles();
this.isInitialized = true;
},
applyStyles: function() {
GM_addStyle(`
#perk-gui-container {
position: fixed; bottom: 100px; left: 50%;
transform: translateX(-50%); display: flex;
flex-direction: column; align-items: center;
gap: 10px; z-index: 1000; pointer-events: none;
}
.perk-tier {
display: flex; gap: 10px; background: ${config.colors.hudBg};
padding: 10px; border-radius: 10px;
border: 1px solid ${config.colors.hudBorder};
box-shadow: 0 0 20px ${config.colors.glow};
pointer-events: auto; opacity: 0;
transform: translateY(10px);
transition: opacity 0.3s ease, transform 0.3s ease;
}
.perk-tier.visible { opacity: 1; transform: translateY(0); }
.perk-icon {
width: 60px; height: 60px;
background-color: ${config.colors.secondaryBg};
border: 2px solid #444; border-radius: 6px;
cursor: pointer; transition: all 0.2s ease;
background-size: 85%; background-position: center;
background-repeat: no-repeat;
}
.perk-icon:hover {
border-color: ${config.colors.accent};
transform: scale(1.1);
filter: drop-shadow(0 0 8px ${config.colors.glow});
}
`);
},
update: function() {
if (!config.perkGUI.enabled || !unsafeWindow.inGame || unsafeWindow.spectating) {
this.hideAll();
return;
}
this.init();
const player = unsafeWindow.Player.pool[unsafeWindow.selfId];
if (!player) return;
const score = player.score;
const levelUpgrades = unsafeWindow.levelUpgrades;
if (score >= 100 && levelUpgrades[1] === '') {
if (!autoPerkSystem.attemptAutoUpgrade(1)) {
if (!this.tierVisible[1]) this.showTier(1, this.tier1.filter(perk => !Object.values(levelUpgrades).includes(perk)));
}
} else { this.hideTier(1); }
if (score >= 300 && levelUpgrades[2] === '') {
if (!autoPerkSystem.attemptAutoUpgrade(2)) {
if (!this.tierVisible[2]) this.showTier(2, this.tier2.filter(perk => !Object.values(levelUpgrades).includes(perk)));
}
} else { this.hideTier(2); }
if (score >= 600 && levelUpgrades[3] === '') {
if (!autoPerkSystem.attemptAutoUpgrade(3)) {
if (!this.tierVisible[3]) this.showTier(3, this.tier3.filter(perk => !Object.values(levelUpgrades).includes(perk)));
}
} else { this.hideTier(3); }
},
showTier: function(tier, perks) {
this.tierVisible[tier] = true;
let tierDiv = document.getElementById(`perk-tier-${tier}`);
if (!tierDiv) {
tierDiv = document.createElement('div');
tierDiv.id = `perk-tier-${tier}`;
tierDiv.className = 'perk-tier';
document.getElementById('perk-gui-container').appendChild(tierDiv);
}
tierDiv.innerHTML = '';
perks.forEach(perkName => {
const icon = document.createElement('div');
icon.className = 'perk-icon';
icon.style.backgroundImage = `url('/img/${perkName}.png')`;
icon.title = perkName.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase());
icon.onclick = () => this.selectPerk(tier, perkName);
tierDiv.appendChild(icon);
});
setTimeout(() => tierDiv.classList.add('visible'), 10);
},
hideTier: function(tier) {
this.tierVisible[tier] = false;
const tierDiv = document.getElementById(`perk-tier-${tier}`);
if (tierDiv) tierDiv.classList.remove('visible');
},
hideAll: function() {
this.hideTier(1); this.hideTier(2); this.hideTier(3);
},
selectPerk: function(tier, perkName) {
unsafeWindow.levelUpgrades[tier] = perkName;
unsafeWindow.Connection.list[0].send(unsafeWindow.prepareMessage('upgrade', {
upgrade: perkName,
upgradeLevel: tier
}));
this.hideTier(tier);
}
};
// --- 6. ESP機能 ---
const esp = {
areas: {
tdm: [{ x: 3400, y: 3400, w: 200, h: 200 }],
dom: [
{ x: 2500, y: 2500, w: 200, h: 200 }, { x: 4300, y: 2500, w: 200, h: 200 },
{ x: 2500, y: 4300, w: 200, h: 200 }, { x: 4300, y: 4300, w: 200, h: 200 }
]
},
draw: function(ctx, camera) {
if (!config.esp.enabled || !unsafeWindow.inGame || unsafeWindow.spectating) return;
const self = unsafeWindow.Player.pool[unsafeWindow.selfId];
if (!self) return;
const gameType = unsafeWindow.gameType;
let areasToDraw = [];
if (gameType === 'TDM' || gameType === 'FFA') areasToDraw = this.areas.tdm;
else if (gameType === 'DOM') areasToDraw = this.areas.dom;
if (areasToDraw.length === 0) return;
ctx.save();
ctx.lineWidth = 2.5;
ctx.shadowColor = 'black'; ctx.shadowBlur = 6;
areasToDraw.forEach(area => {
const relPos = camera.getRelPos(area);
const enemyCount = this.countEnemiesInArea(area, self);
const isSelfInArea = (self.x >= area.x && self.x <= area.x + area.w && self.y >= area.y && self.y <= area.y + area.h);
let color = config.colors.espSafe;
if (enemyCount > 0) color = config.colors.espWarning;
if (enemyCount >= 2) color = config.colors.espDanger;
if (isSelfInArea && color === config.colors.espSafe) {
color = config.colors.espWarning;
}
ctx.strokeStyle = color;
const bracketSize = 20;
ctx.beginPath(); ctx.moveTo(relPos.x + bracketSize, relPos.y); ctx.lineTo(relPos.x, relPos.y); ctx.lineTo(relPos.x, relPos.y + bracketSize); ctx.stroke();
ctx.beginPath(); ctx.moveTo(relPos.x + area.w - bracketSize, relPos.y); ctx.lineTo(relPos.x + area.w, relPos.y); ctx.lineTo(relPos.x + area.w, relPos.y + bracketSize); ctx.stroke();
ctx.beginPath(); ctx.moveTo(relPos.x + bracketSize, relPos.y + area.h); ctx.lineTo(relPos.x, relPos.y + area.h); ctx.lineTo(relPos.x, relPos.y + area.h - bracketSize); ctx.stroke();
ctx.beginPath(); ctx.moveTo(relPos.x + area.w - bracketSize, relPos.y + area.h); ctx.lineTo(relPos.x + area.w, relPos.y + area.h); ctx.lineTo(relPos.x + area.w, relPos.y + area.h - bracketSize); ctx.stroke();
});
ctx.restore();
},
countEnemiesInArea: function(area, self) {
let count = 0;
for (const id in unsafeWindow.Player.pool) {
const p = unsafeWindow.Player.pool[id];
if (!p.activated || p.hp <= 0 || p.id === self.id) continue;
if (unsafeWindow.gameType !== 'FFA' && p.teamCode === self.teamCode) continue;
if (p.x >= area.x && p.x <= area.x + area.w && p.y >= area.y && p.y <= area.y + area.h) count++;
}
return count;
}
};
// --- 7. 通知システム ---
const notificationSystem = {
show: function(message) {
if (!config.notifications.enabled) return;
const container = document.getElementById('notification-container');
const notification = document.createElement('div');
notification.className = 'notification';
notification.textContent = message;
container.appendChild(notification);
if (container.children.length > config.notifications.maxDisplay) {
container.removeChild(container.firstChild);
}
setTimeout(() => {
if (container.contains(notification)) container.removeChild(notification);
}, config.notifications.displayTime);
}
};
// --- 8. メインループと初期化 ---
function initGameModifications() {
if (typeof unsafeWindow.drawGrid === 'undefined' || typeof unsafeWindow.Player === 'undefined') {
return;
}
console.log("GatsModV2 Fusion: Applying canvas modifications...");
unsafeWindow.originalDrawHud = unsafeWindow.drawHud;
unsafeWindow.originalDrawNotifications = unsafeWindow.drawNotifications;
const originalGameLoop = unsafeWindow.gameLoop;
const originalBulletDraw = unsafeWindow.Bullet.prototype.draw;
unsafeWindow.drawHud = (player) => customUI.updateAll(player);
unsafeWindow.drawHudMiniMap = () => {};
unsafeWindow.drawNotifications = function() {
const notifications = unsafeWindow.displayedNotifications;
if (notifications && notifications.length > 0) {
const lastNotification = notifications.pop();
let message = lastNotification.content;
switch(lastNotification.type) {
case 1: message = `Killed ${lastNotification.content}!`; break;
case 2: message = `Killed by ${lastNotification.content}`; break;
case 3: message = `Hit Damage ${lastNotification.content}`; break;
}
notificationSystem.show(message);
}
};
unsafeWindow.gameLoop = function() {
if (unsafeWindow.crate) {
unsafeWindow.crate[0][0][1][3] = config.colors.crateBody;
unsafeWindow.crate[0][1][1][3] = config.colors.crateLid;
}
if (unsafeWindow.longCrate) {
unsafeWindow.longCrate[0][0][1][3] = config.colors.crateBody;
unsafeWindow.longCrate[0][1][1][3] = config.colors.crateLid;
}
originalGameLoop.apply(this, arguments);
};
unsafeWindow.drawGrid = function(ctx, camera) {
ctx.fillStyle = config.colors.canvasBg;
ctx.fillRect(0, 0, unsafeWindow.canvas.width, unsafeWindow.canvas.height);
const relPos = camera.getRelPos({x:0,y:0});
ctx.strokeStyle = 'rgba(0, 0, 0, 0.3)';
ctx.lineWidth = 2;
ctx.strokeRect(relPos.x, relPos.y, unsafeWindow.GAME_WIDTH, unsafeWindow.GAME_HEIGHT);
if (customUI.damageFlashAlpha > 0) {
ctx.save();
const centerX = unsafeWindow.canvas.width / 2;
const centerY = unsafeWindow.canvas.height / 2;
const outerRadius = Math.sqrt(centerX * centerX + centerY * centerY);
const gradient = ctx.createRadialGradient(centerX, centerY, outerRadius * 0.6, centerX, centerY, outerRadius);
gradient.addColorStop(0, 'rgba(255, 0, 50, 0)');
gradient.addColorStop(1, `rgba(255, 0, 50, ${customUI.damageFlashAlpha * 0.7})`);
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, unsafeWindow.canvas.width, unsafeWindow.canvas.height);
ctx.restore();
customUI.damageFlashAlpha -= 0.04;
}
esp.draw(ctx, camera);
if (unsafeWindow.spectating && customUI.deathFade > 0) {
customUI.deathFade -= 0.02;
ctx.globalAlpha = customUI.deathFade;
ctx.fillStyle = config.colors.deathOverlay;
ctx.fillRect(0, 0, unsafeWindow.canvas.width, unsafeWindow.canvas.height);
ctx.globalAlpha = 1.0;
} else if (!unsafeWindow.spectating) {
customUI.deathFade = 1.0;
}
};
unsafeWindow.Bullet.prototype.draw = function(ctx, camera) {
if (!this.activated || this.isKnife) {
originalBulletDraw.apply(this, arguments);
return;
}
ctx.save();
ctx.strokeStyle = config.colors.bulletTrail;
ctx.lineWidth = this.width * 0.6;
ctx.globalAlpha = 0.7;
const relPos = camera.getRelPos(this);
const prevX = relPos.x - this.spdX * 0.6;
const prevY = relPos.y - this.spdY * 0.6;
ctx.beginPath(); ctx.moveTo(prevX, prevY); ctx.lineTo(relPos.x, relPos.y); ctx.stroke();
ctx.restore();
const originalStrokeStyle = ctx.strokeStyle;
ctx.strokeStyle = config.colors.bulletCore;
ctx.shadowColor = config.colors.bulletGlow;
ctx.shadowBlur = 8;
originalBulletDraw.apply(this, arguments);
ctx.shadowBlur = 0;
ctx.strokeStyle = originalStrokeStyle;
};
unsafeWindow.weaponIconMap = {
'pistol': 'pistol-outline', 'smg': 'smg-outline', 'shotgun': 'shotgun-outline',
'assault': 'assault-outline', 'sniper': 'bolt-action-rifle-outline', 'lmg': 'machine-gun-outline'
};
clearInterval(modInterval);
console.log("GatsModV2 Fusion: All modifications applied successfully.");
const playButton = document.getElementById('playButton');
if (playButton && playButton.parentElement) {
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 on Discord';
playButton.parentElement.appendChild(discordBtn);
}
initAutoPerkFeature();
}
function initAutoPerkFeature() {
if (document.getElementById('slct')) {
autoPerkSystem.init();
} else {
setTimeout(initAutoPerkFeature, 100);
}
}
// --- 9. 初期化処理 ---
const modInterval = setInterval(initGameModifications, 100);
if (config.perkGUI.enabled) {
setInterval(() => perkGUI.update(), 200);
}
})();