您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Ultimate Slither.io Mod Menu with Chat & Custom UI - Fixed chat toggle and simplify - Enhanced Visuals
// ==UserScript== // @name SLITHER.IO MOD MENU - DSC.GG/143X CHAT, PROFILES, +REP, ZOOM, LEADERBOARD, GIFS & MORE *UPDATED* // @namespace http://tampermonkey.net/ // @version X7 - GreasyFork // @description Ultimate Slither.io Mod Menu with Chat & Custom UI - Fixed chat toggle and simplify - Enhanced Visuals // @author GITHUB.COM/DXXTHLY - HTTPS://DSC.GG/143X by: dxxthly. & waynesg on Discord (UI Enhanced by AI) // @icon https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQUNcRl2Rh40pZLhgffYGFDRLbYJ4qfMNwddQ&s.png // @match http://slither.io/ // @match https://slither.io/ // @match http://slither.com/io // @match https://slither.com/io // @grant none // ==/UserScript== (function () { 'use strict'; // --- Custom BG Patch (MUST be first!) --- window.__customBgUrlCurrent = 'https://slither.io/s2/bg54.jpg'; // Default if (!window.__customBgPatched) { window.__customBgPatched = true; const originalDrawImage = CanvasRenderingContext2D.prototype.drawImage; CanvasRenderingContext2D.prototype.drawImage = function(img, ...args) { if ( img && img.src && img.src.includes('bg54.jpg') && window.__customBgUrlCurrent ) { const customImg = new window.Image(); customImg.crossOrigin = "anonymous"; customImg.src = window.__customBgUrlCurrent; return originalDrawImage.call(this, customImg, ...args); } return originalDrawImage.apply(this, arguments); }; } // === NEW HELPER FUNCTION for color manipulation === function adjustColor(hex, percent) { let r = parseInt(hex.slice(1, 3), 16), g = parseInt(hex.slice(3, 5), 16), b = parseInt(hex.slice(5, 7), 16); r = Math.min(255, Math.max(0, r + (r * percent / 100))); g = Math.min(255, Math.max(0, g + (g * percent / 100))); b = Math.min(255, Math.max(0, b + (b * percent / 100))); return `#${Math.round(r).toString(16).padStart(2, '0')}${Math.round(g).toString(16).padStart(2, '0')}${Math.round(b).toString(16).padStart(2, '0')}`; } // === CONFIG === const config = { currentVersion: 'VX7 - GreasyFork', menuPosition: 'right', defaultCircleRadius: 150, circleRadiusStep: 20, minCircleRadius: 50, maxCircleRadius: 300, // --- NEW: GIPHY API Key for GIF feature --- giphyApiKey: 'xWBhUx8jBtCxxjPHvtUzHLZlPGYBUTFq', // This is a public key from GIPHY's examples deathSoundURL: 'https://audio.jukehost.co.uk/WwASzZ0a1wJDKubIcoZzin8J7kycCt5l.mp3', godModeVideoURL: 'https://youtu.be/ghAap5IWu1Y', defaultMenuName: 'DSC.GG/143X', defaultMenuColor: '#4CAF50', // Main accent color chatMaxMessages: 50, chatMaxMessageLength: 100, chatProfanityFilter: true, chatProfanityList: ['fuck', 'shit', 'asshole', 'bitch', 'cunt', 'nigger', 'fag', 'retard'], // vvv PASTE THE NEW RANK SYSTEM HERE vvv repMilestones: { 0: { name: 'Unranked', icon: '🌱' }, 100: { name: 'Bronze Slither', icon: '🥉' }, 500: { name: 'Silver Snake', icon: '🥈' }, 1000: { name: 'Gold Serpent', icon: '🥇' }, 2500: { name: 'Platinum Python', icon: '💠' }, 5000: { name: 'Diamond Drake', icon: '💎' }, 10000: { name: 'Master Mamba', icon: '🏆' }, 25000: { name: 'Grandmaster Naga', icon: '⚜️' }, 50000: { name: 'Apex Anaconda', icon: '🐍' }, 100000: { name: 'Mythic Ouroboros', icon: '🌀' }, 500000: { name: 'Slither Titan', icon: '☄️' }, 1000000: { name: 'Slither God', icon: '👑' } } }; // === STATE === const state = { versionStatus: 'Checking...', keybinds: JSON.parse(localStorage.getItem('modKeybinds')) || { toggleMenu: 'm', toggleKeybinds: '-', circleRestriction: 'k', circleSmaller: 'j', circleLarger: 'l', autoCircle: 'a', autoBoost: 'b', fpsDisplay: 'f', autoRespawn: 's', neonLine: 'e', deathSound: 'v', showServer: 't', chatEnabled: '/', zoomIn: 'z', zoomOut: 'x', zoomReset: 'c', screenshot: 'p', github: 'g', discord: 'd', godMode: 'y', reddit: 'r', dreamwave: 'n' }, features: { circleRestriction: false, autoCircle: false, performanceMode: 1, deathSound: true, snakeTrail: false, snakeTrailColor: '#FFD700', fpsDisplay: false, autoBoost: false, neonLine: false, neonLineColor: '#00ffff', chatFocus: false, showServer: false, autoRespawn: false, chatVisible: true, chatEnabled: true, chatProfanityFilter: config.chatProfanityFilter, keybindsEnabled: true, blackBg: false // This makes the background default at the start. }, menuVisible: true, zoomFactor: 1.0, circleRadius: config.defaultCircleRadius, fps: 0, fpsFrames: 0, fpsLastCheck: Date.now(), deathSound: new Audio(config.deathSoundURL), isInGame: false, boosting: false, autoCircleAngle: 0, ping: 0, server: '', lastSnakeAlive: true, boostingInterval: null, menuName: localStorage.getItem('modMenuName') || config.defaultMenuName, menuColor: localStorage.getItem('modMenuColor') || config.defaultMenuColor, showCustomization: sessionStorage.getItem('showCustomization') === 'false' ? false : true, simplified: sessionStorage.getItem('modMenuSimplified') === 'true', chatMessages: [], uiLayout: JSON.parse(localStorage.getItem('modMenuUILayout')) || { menu: { x: null, y: null, width: null, height: null }, // Width/Height for menu might not be needed if content dictates it chat: { x: 20, y: 100, width: 320, height: 250 }, // Adjusted default chat size minimap: { x: null, y: null, width: null, height: null } }, draggingElement: null, resizingElement: null, dragStartX: 0, dragStartY: 0, elementStartX: 0, elementStartY: 0, elementStartWidth: 0, elementStartHeight: 0, uiScale: parseFloat(localStorage.getItem('modMenuUIScale')) || 1.0 // <<< ADD THIS LINE }; // Ensure all default keybinds are present in state.keybinds const defaultKeybinds = { toggleMenu: 'm', toggleKeybinds: '-', circleRestriction: 'k', circleSmaller: 'j', circleLarger: 'l', autoCircle: 'a', autoBoost: 'b', neonLine: "e", fpsDisplay: 'f', autoRespawn: 's', deathSound: 'v', showServer: 't', chatEnabled: 'enter', // Changed from / to enter to align with original user expectation. Can be rebound. zoomIn: 'z', zoomOut: 'x', zoomReset: 'c', screenshot: 'p', github: 'g', discord: 'd', godMode: 'y', reddit: 'r', dreamwave: 'n' }; Object.entries(defaultKeybinds).forEach(([action, key]) => { if (!state.keybinds.hasOwnProperty(action)) { state.keybinds[action] = key; } }); function buttonStyle(bgColor = state.menuColor, textColor = '#fff') { return `padding:8px 15px; border-radius:6px; border:none; color:${textColor}; font-size:14px; font-weight:500; cursor:pointer; transition:background-color 0.2s, box-shadow 0.2s; background-color:${bgColor};`; } function buttonHoverStyle(bgColor = state.menuColor) { return `this.style.backgroundColor='${adjustColor(bgColor, -15)}'; this.style.boxShadow='0 2px 8px rgba(0,0,0,0.15)';`; } function buttonLeaveStyle(bgColor = state.menuColor) { return `this.style.backgroundColor='${bgColor}'; this.style.boxShadow='none';`; } let waitingForKeybind = false; let currentKeybindAction = null; function openKeybindModal(action) { const overlay = document.getElementById('keybind-modal-overlay'); const actionLabel = document.getElementById('keybind-modal-action'); if (!overlay || !actionLabel) return; overlay.style.display = 'flex'; actionLabel.textContent = `Action: ${action.replace(/([A-Z])/g, ' $1')}`; waitingForKeybind = true; currentKeybindAction = action; } function closeKeybindModal() { const overlay = document.getElementById('keybind-modal-overlay'); if (overlay) overlay.style.display = 'none'; waitingForKeybind = false; currentKeybindAction = null; } document.addEventListener('keydown', function(e) { if (!waitingForKeybind) return; e.preventDefault(); e.stopPropagation(); if (e.key === "Escape" || e.key === "Enter") { closeKeybindModal(); return; } const key = e.key.length === 1 ? e.key.toLowerCase() : e.key; state.keybinds[currentKeybindAction] = key; localStorage.setItem('modKeybinds', JSON.stringify(state.keybinds)); closeKeybindModal(); if (typeof updateMenu === "function") updateMenu(); }); document.addEventListener('wheel', function(e) { if (!waitingForKeybind) return; e.preventDefault(); e.stopPropagation(); let key; if (e.deltaY < 0) key = "wheelup"; else if (e.deltaY > 0) key = "wheeldown"; else return; state.keybinds[currentKeybindAction] = key; localStorage.setItem('modKeybinds', JSON.stringify(state.keybinds)); closeKeybindModal(); if (typeof updateMenu === "function") updateMenu(); }, { passive: false }); function loadSavedServers() { try { return JSON.parse(localStorage.getItem('customServerList') || '[]'); } catch { return []; } } function saveServers(list) { localStorage.setItem('customServerList', JSON.stringify(list)); } function updateServerDropdown() { const selectSrv = document.getElementById('select-srv'); if (!selectSrv) return; selectSrv.innerHTML = '<option value="">Select a Server</option>'; const servers = loadSavedServers(); servers.forEach((ip, i) => { const opt = document.createElement('option'); opt.value = ip; opt.text = `${i+1}. ${ip}`; selectSrv.appendChild(opt); }); } (function(){ // This bridge ensures ALL ArrowLeft/ArrowRight KeyboardEvents set window.l/window.r, even if preventDefault is called elsewhere window.addEventListener('keydown', function(e) { if (e.key === 'ArrowLeft') window.l = true; if (e.key === 'ArrowRight') window.r = true; }, true); // Use capture phase to run before other handlers window.addEventListener('keyup', function(e) { if (e.key === 'ArrowLeft') window.l = false; if (e.key === 'ArrowRight') window.r = false; }, true); })(); // update server ip loop wayne function updateServerIpLoop() { let ip = null, port = null; if (window.bso && window.bso.ip && window.bso.po) { ip = window.bso.ip; port = window.bso.po; } if (ip && port) { state.server = `${ip}:${port}`; } else { state.server = ''; } setTimeout(updateServerIpLoop, 1000); // Check every second } updateServerIpLoop(); // --- PASTE THIS CODE RIGHT BEFORE THE VIP MEMBERS SECTION --- // --- PASTE THIS NEW HELP MODAL CODE --- if (!document.getElementById('rep-help-modal')) { const helpModal = document.createElement('div'); helpModal.id = 'rep-help-modal'; helpModal.style.cssText = ` display: none; position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; z-index: 10015; background: rgba(0,0,0,0.75); align-items: center; justify-content: center; font-family: 'Segoe UI', Arial, sans-serif; `; // Dynamically generate the rank list from the config object const rankListHTML = Object.entries(config.repMilestones).map(([rep, rank]) => `<li><span style="font-size: 1.2em; width: 25px; display: inline-block;">${rank.icon}</span> <b>${rank.name}:</b> ${parseInt(rep).toLocaleString()} REP</li>` ).join(''); helpModal.innerHTML = ` <div style="background: #23232a; border-radius: 12px; padding: 25px 35px; min-width: 450px; max-width: 90%; max-height: 80vh; display: flex; flex-direction: column; box-shadow:0 6px 25px rgba(0,0,0,0.4); border: 1px solid var(--menu-color, #4CAF50); position:relative;"> <button id="rep-help-close" style="position:absolute; top:10px; right:10px; font-size:1.5em; background:none; border:none; color:#aaa; cursor:pointer; line-height:1;">×</button> <h2 style="color:var(--menu-color, #4CAF50); margin-top:0; text-align:center; padding-bottom: 10px; border-bottom: 1px solid #444;">REP & Ranking System</h2> <div style="margin-top:15px; overflow-y: auto; padding-right: 15px; color: #ccc; line-height: 1.6;"> <h3 style="color: #FFD700; margin-top: 5px;">How to Gain REP</h3> <ul style="margin-left: 20px; padding-left: 0;"> <li><b>Stay Active:</b> Earn 1 REP for every 15 minutes of gameplay.</li> <li><b>Be Social:</b> Earn 1 REP every 5 minutes you send a message in chat.</li> </ul> <h3 style="color: #FFD700;">Ranks & Milestones</h3> <p>Ranks are automatically awarded as you reach REP milestones.</p> <ul style="margin-left: 20px; padding-left: 0; list-style-type: none;"> ${rankListHTML} </ul> </div> </div> `; document.body.appendChild(helpModal); // Attach listeners for the modal document.getElementById('rep-help-close').onclick = () => { helpModal.style.display = 'none'; }; helpModal.onclick = (e) => { if (e.target.id === 'rep-help-modal') { helpModal.style.display = 'none'; } }; } // === VIP MEMBERS // DISCORD === const vipMembers = [ { uid: "crcOY9hoRrfayStCxMVm7Zdx2W92", name: "stevao" }, { uid: "DhGhICAZwkRa7wuMsyquM9a5uO92", name: "LUANBLAYNER" }, { uid: "EWhWsb2veZPzvSyBq4xM5f4r5Ng2", name: "stevao" }, { uid: "CiOpgh1RLBg3l5oXn0SAho66Po93", name: "dxxthly"}, // DXXTHLY VIP { uid: "P75eMwh756Rb6h1W6iqQfHN2Dm92", name: "wayne"}, // WAYNE VIP { uid: "VIP_UID_4", name: "Another2VIP" }, { uid: "VIP_UID_5", name: "Another3VIP" }, ]; const devList = [ { uid: "CiOpgh1RLBg3l5oXn0SAho66Po93", name: "dxxthly" }, { uid: "PZA5qgKWsPTXc278pyx7NwROf313", name: "dxxthly" }, // <-- Add your new UID here { uid: "P75eMwh756Rb6h1W6iqQfHN2Dm92", name: "wayne" } ]; function isVip(uid, name) { return vipMembers.some(vip => vip.uid === uid && vip.name.toLowerCase() === (name || '').toLowerCase() ); } // --- THIS IS THE NEW, CORRECTED FUNCTION --- function isDev(uid) { return devList.some(dev => dev.uid === uid); } function vipGlowStyle(name, color) { const vipColor = color || state.menuColor; // Fallback to menu color if specific VIP color not provided return `<span style=" color:#fff; font-weight:bold; text-shadow:0 0 5px #fff, 0 0 10px ${vipColor}, 0 0 15px ${vipColor}; ">${name}</span>`; } // --- NEW: Helper lists and functions for roles --- const adminMembers = [ // { uid: "ADMIN_UID_1", name: "AdminName1" }, ]; const supporterMembers = [ // { uid: "SUPPORTER_UID_1", name: "SupporterName1" }, ]; function isAdmin(uid) { return adminMembers.some(admin => admin.uid === uid); } function isSupporter(uid) { return supporterMembers.some(supporter => supporter.uid === uid); } function isSystemAccount(uid) { return systemAccounts.includes(uid); } const systemAccounts = [ "system", "discord_bot" ]; // List of UIDs to hide from leaderboards (e.g., bots) // List of UIDs to hide from leaderboards (e.g., bots) const leaderboardBlockedUIDs = [ "discord_bot", "n4P6uCFzhFO11xsUYge1nQQSpcL2", // Add first UID to block "pk4p3FkLFVShqX8pD3dBtb4CJbB3" // Add second UID to block ]; function isBlockedFromLeaderboard(uid) { return leaderboardBlockedUIDs.includes(uid); } let chatMessagesArray = []; let forcedServer = null; let chatHistory = []; let autoCircleRAF = null; let autoRespawnDead = false; let autoRespawnSpam = null; let deathCheckInterval = null; let afkOn = false; let afkInterval = null; let realMouseX = window.innerWidth / 2; let realMouseY = window.innerHeight / 2; document.addEventListener('mousemove', function(e) { realMouseX = e.clientX; realMouseY = e.clientY; }); function syncServerBoxWithMenu() { const box = document.getElementById('custom-server-box'); const nameSpan = document.getElementById('custom-server-box-name'); const serverListBtn = document.getElementById('server-list-btn'); const connectBtn = document.getElementById('connect-btn'); const saveIpBtn = document.getElementById('save-ip-btn'); if (!box || !nameSpan) return; const menuColor = state.menuColor; const hoverColor = adjustColor(menuColor, -15); // Darker for hover nameSpan.textContent = state.menuName; nameSpan.style.color = menuColor; nameSpan.style.textShadow = `0 0 6px ${menuColor}, 0 0 12px ${menuColor}`; box.style.borderColor = menuColor; box.style.boxShadow = `0 0 12px ${hexToRgba(menuColor, 0.4)}`; [connectBtn, saveIpBtn, serverListBtn].forEach(btn => { if (btn) { btn.style.background = menuColor; btn.style.boxShadow = `0 0 8px ${hexToRgba(menuColor, 0.4)}`; // Add hover effect directly if not using CSS classes btn.onmouseenter = () => btn.style.background = hoverColor; btn.onmouseleave = () => btn.style.background = menuColor; } }); const serverIpInput = document.getElementById('server-ip'); const selectSrv = document.getElementById('select-srv'); if(serverIpInput) { serverIpInput.onfocus = () => { serverIpInput.style.borderColor = menuColor; serverIpInput.style.boxShadow = `0 0 5px ${hexToRgba(menuColor, 0.5)}`;}; serverIpInput.onblur = () => { serverIpInput.style.borderColor = '#555'; serverIpInput.style.boxShadow = 'none';}; } if(selectSrv) { selectSrv.onfocus = () => { selectSrv.style.borderColor = menuColor; selectSrv.style.boxShadow = `0 0 5px ${hexToRgba(menuColor, 0.5)}`;}; selectSrv.onblur = () => { selectSrv.style.borderColor = '#555'; selectSrv.style.boxShadow = 'none';}; } } const zoomSteps = [ 0.1, 0.125, 0.15, 0.175, 0.2, 0.225, 0.25, 0.275, 0.3, 0.325, 0.35, 0.375, 0.4, 0.425, 0.45, 0.475, 0.5, 0.525, 0.55, 0.575, 0.6, 0.625, 0.65, 0.675, 0.7, 0.725, 0.75, 0.775, 0.8, 0.825, 0.85, 0.875, 0.9, 0.925, 0.95, 0.975, 1.0, 1.025, 1.05, 1.075, 1.1, 1.125, 1.15, 1.175, 1.2, 1.225, 1.25, 1.275, 1.3, 1.325, 1.35, 1.375, 1.4, 1.425, 1.45, 1.475, 1.5, 1.525, 1.55, 1.575, 1.6, 1.625, 1.65, 1.675, 1.7, 1.725, 1.75, 1.775, 1.8, 1.825, 1.85, 1.875, 1.9, 1.925, 1.95, 1.975, 2.0, 2.25, 2.5, 2.75, 3.0, 3.25, 3.5, 3.75, 4.0, 4.25, 4.5, 4.75, 5.0 ]; // Reduced max zoom for sanity function addServerBox() { const check = setInterval(() => { const login = document.getElementById('login'); const nickInput = document.getElementById('nick'); if (login && nickInput) { clearInterval(check); if (document.getElementById('custom-server-box')) return; const box = document.createElement('div'); box.id = 'custom-server-box'; // --- ENHANCED SERVER BOX STYLES --- box.style.cssText = ` margin: 28px auto 0 auto; max-width: 360px; /* Slightly wider */ background: rgba(28, 28, 32, 0.97); /* Darker, more opaque */ border: 2px solid ${state.menuColor}; border-radius: 12px; /* Smoother radius */ box-shadow: 0 4px 20px ${hexToRgba(state.menuColor, 0.25)}, 0 0 0 1px rgba(0,0,0,0.1); padding: 22px; text-align: center; font-family: 'Arial', 'Helvetica Neue', Helvetica, sans-serif; /* Modern font stack */ color: #e0e0e0; /* Softer white */ position: relative; transition: border-color 0.3s, box-shadow 0.3s; `; // --- ENHANCED SERVER BOX INNER HTML --- box.innerHTML = ` <div style="margin-bottom:12px;"> <span id="custom-server-box-name" style=" color:${state.menuColor}; font-size:1.6em; /* Larger title */ font-family: 'Segoe UI', 'Arial', sans-serif; /* Title font */ text-shadow:0 0 6px ${state.menuColor}, 0 0 12px ${state.menuColor}; font-weight:600; /* Bolder */ letter-spacing:0.5px; transition:color 0.3s, text-shadow 0.3s; "> ${state.menuName} </span> </div> <div style="display:flex; flex-direction:column; gap:12px; margin-bottom:15px;"> <input id="server-ip" type="text" placeholder="Server Address (IP:Port)" style="width:100%; padding:10px 12px; background:rgba(255,255,255,0.05); color:#e0e0ff; border:1px solid #555; border-radius:6px; outline:none; font-size:1em; box-sizing:border-box; transition: border-color 0.2s, box-shadow 0.2s;"> <div style="display:flex; gap:10px;"> <input id="save-ip-btn" type="button" value="Save" style="flex:1; height:40px; border-radius:6px; color:#FFF; background: ${state.menuColor}; border:none; outline:none; cursor:pointer; font-weight:bold; font-size: 0.95em; transition: background-color 0.2s;"> <input id="connect-btn" type="button" value="Play" style="flex:2; height:40px; border-radius:6px; color:#FFF; background: ${state.menuColor}; border:none; outline:none; cursor:pointer; font-weight:bold; font-size: 1.05em; transition: background-color 0.2s;"> </div> </div> <select id="select-srv" style="display:block; margin:0 auto 15px auto; width:100%; background:rgba(255,255,255,0.05); border:1px solid #555; border-radius:6px; padding:10px 12px; font-size:1em; color: #e0e0e0; text-align:center; box-sizing:border-box; transition: border-color 0.2s, box-shadow 0.2s;"> <option value="">Select a Saved Server</option> </select> <a id="server-list-btn" href="https://ntl-slither.com/ss/?reg=na" target="_blank" style=" display: block; width: 100%; background: ${state.menuColor}; color: #fff; border: none; border-radius: 6px; padding: 12px 0; font-size: 1.1em; font-family: inherit; font-weight: bold; cursor: pointer; box-shadow: 0 2px 5px ${hexToRgba(state.menuColor, 0.3)}; text-align: center; text-decoration: none; transition: background-color 0.2s, box-shadow 0.2s; box-sizing:border-box; " > Browse Server List </a> `; let parent = nickInput.parentElement; if (parent && parent.nextSibling) { parent.parentNode.insertBefore(box, parent.nextSibling.nextSibling); } else { login.appendChild(box); } updateServerDropdown(); syncServerBoxWithMenu(); // Apply dynamic styles const selectSrv = document.getElementById('select-srv'); selectSrv.onchange = function() { document.getElementById('server-ip').value = this.value; }; document.getElementById('save-ip-btn').onclick = function() { const ipInput = document.getElementById('server-ip'); if (!ipInput || !ipInput.value.trim()) return; const ip = ipInput.value.trim(); if (!ip.includes(':') || ip.split(':')[0].trim() === '' || ip.split(':')[1].trim() === '') { alert("Please enter a valid IP:Port (e.g., 15.204.212.200:444 or server.domain.com:444)"); return; } let servers = loadSavedServers(); const normalized = ip.toLowerCase().replace(/\s+/g, ''); const isDuplicate = servers.some(s => s.toLowerCase().replace(/\s+/g, '') === normalized); if (!isDuplicate) { servers.push(ip); saveServers(servers); updateServerDropdown(); if (selectSrv) selectSrv.value = ip; } else { alert("This server is already in your list!"); } }; document.getElementById('connect-btn').onclick = function() { const ipInput = document.getElementById('server-ip'); if (!ipInput || !ipInput.value.trim()) return; const ip = ipInput.value.trim(); const parts = ip.split(':'); const ipPart = parts[0]; const portPart = parts[1] || "444"; forcedServer = { ip: ipPart, port: portPart }; localStorage.setItem('forcedServer', JSON.stringify(forcedServer)); if (typeof window.forceServer === "function") { window.forceServer(ipPart, portPart); } window.forcing = true; if (!window.bso) window.bso = {}; window.bso.ip = ipPart; window.bso.po = portPart; if (typeof window.connect === "function") { window.connect(); } const playBtn = document.getElementById('playh') || document.querySelector('.btn.btn-primary.btn-play-guest'); if (playBtn) playBtn.click(); if (typeof connectionStatus === "function") setTimeout(connectionStatus, 1000); }; } }, 100); } addServerBox(); let retry = 0; function connectionStatus() { if (!window.connecting || retry == 10) { window.forcing = false; retry = 0; return; } retry++; setTimeout(connectionStatus, 1000); } // vvv PASTE THESE TWO NEW FUNCTIONS HERE vvv function awardTimeBasedRep() { const uid = firebase.auth().currentUser?.uid; if (!uid) return; // Not logged in yet. const userRef = firebase.database().ref(`playerData/${uid}`); const now = Date.now(); const TEN_MINUTES = 15 * 60 * 1000; // <-- CHANGED FROM 30 userRef.once('value', async (snapshot) => { if (!snapshot.exists()) return; // Data not created yet. const data = snapshot.val(); const lastAwardTime = data.lastRepAwardTime || 0; if (now - lastAwardTime > TEN_MINUTES) { // <-- USES THE NEW VALUE await userRef.child('rep').transaction(currentRep => (currentRep || 0) + 1); await userRef.child('lastRepAwardTime').set(now); console.log("Awarded 1 REP for 10 minutes of activity."); // <-- Updated log } }); } function processEndOfGame(score) { // This is a placeholder for now. You can add point logic here later. // For the REP system, we only care about awarding for time and chat. } // ^^^ END OF PASTED FUNCTIONS ^^^ function autoRespawnCheck() { if (!state.features.autoRespawn) { autoRespawnDead = false; stopAutoRespawnSpam(); return; } const isDead = ( (window.snake && !window.snake.alive) || (window.dead_mtm !== undefined && window.dead_mtm !== -1) || (document.getElementById('died')?.style.display !== 'none') || (document.querySelector('.playagain')?.offsetParent !== null) ); if (isDead && !autoRespawnDead) { autoRespawnDead = true; startAutoRespawnSpam(); } else if (!isDead && autoRespawnDead) { autoRespawnDead = false; stopAutoRespawnSpam(); } } function startAutoRespawnSpam() { if (autoRespawnSpam) return; attemptAutoRespawn(); autoRespawnSpam = setInterval(attemptAutoRespawn, 50); } function attemptAutoRespawn() { if (!autoRespawnDead || !state.features.autoRespawn) { stopAutoRespawnSpam(); return; } const nickInput = document.getElementById('nick'); if (nickInput && !nickInput.value.trim()) { nickInput.value = localStorage.getItem("nickname") || "Anon"; nickInput.dispatchEvent(new Event('input', { bubbles: true })); } if (nickInput) nickInput.focus(); document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true })); } function stopAutoRespawnSpam() { if (autoRespawnSpam) { clearInterval(autoRespawnSpam); autoRespawnSpam = null; } } function enableAutoRespawn() { if (!deathCheckInterval) { deathCheckInterval = setInterval(autoRespawnCheck, 100); } } function disableAutoRespawn() { if (deathCheckInterval) { clearInterval(deathCheckInterval); deathCheckInterval = null; } autoRespawnDead = false; stopAutoRespawnSpam(); } if (state.features.autoRespawn) enableAutoRespawn(); const primeAudio = () => { state.deathSound.volume = 0.01; state.deathSound.play().then(() => { state.deathSound.pause(); state.deathSound.currentTime = 0; state.deathSound.volume = 1; }).catch(console.error); document.removeEventListener('click', primeAudio); document.removeEventListener('keydown', primeAudio); }; document.addEventListener('click', primeAudio); document.addEventListener('keydown', primeAudio); // --- ENHANCED GLOBAL STYLES (PROFILE POPUP) --- const style = document.createElement('style'); style.textContent = ` .profile-popup { position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%) scale(1); min-width: 280px; /* Slightly wider */ max-width: 90vw; background: linear-gradient(145deg, #38383E, #2A2A2F); /* Darker gradient */ color: #e0e0e0; /* Softer white */ border-radius: 12px; /* Smoother radius */ border: 1px solid var(--menu-color-transparent, rgba(76, 175, 80, 0.5)); /* Use menu color for border */ box-shadow: 0 8px 32px rgba(0,0,0,0.35), 0 2px 8px rgba(0,0,0,0.2); padding: 24px 30px 20px 30px; z-index: 10001; /* Ensure it's above chat */ font-family: 'Segoe UI', 'Arial', sans-serif; /* Modern font */ font-size: 1.05em; animation: fadeInProfile 0.25s cubic-bezier(.17,.67,.6,1.04); display: flex; flex-direction: column; align-items: center; } @keyframes fadeInProfile { from { opacity: 0; transform: translate(-50%, -55%) scale(0.92);} to { opacity: 1; transform: translate(-50%, -50%) scale(1);} } .profile-popup .close-btn { position: absolute; top: 12px; right: 12px; background: none; border: none; color: #aaa; /* Softer color */ font-size: 1.6em; line-height: 1; cursor: pointer; opacity: 0.7; transition: opacity 0.2s, color 0.2s; } .profile-popup .close-btn:hover { opacity: 1; color: #fff; } .profile-popup .avatar { width: 72px; height: 72px; /* Larger avatar */ border-radius: 50%; background: #444; /* Darker placeholder */ margin-bottom: 16px; object-fit: cover; border: 3px solid var(--menu-color, #4CAF50); /* Menu color border */ box-shadow: 0 0 10px rgba(0,0,0,0.2); } .profile-popup .status-dot { display: inline-block; width: 11px; height: 11px; border-radius: 50%; margin-right: 7px; vertical-align: middle; border: 1px solid rgba(0,0,0,0.2); /* Subtle border for dot */ } .profile-popup_action_button { background-color: var(--menu-color, #4CAF50); color: white; border: none; border-radius: 5px; padding: 6px 12px; font-size: 0.9em; cursor: pointer; margin: 2px; transition: background-color 0.2s ease; } .profile-popup_action_button:hover { /* Use JS to set darker color based on --menu-color */ } /* General button styles for menu - can be used via classes if preferred */ .mod-menu-button { padding: 8px 15px; border-radius: 6px; border: none; color: #fff; font-size: 14px; font-weight: 500; cursor: pointer; transition: background-color 0.2s, box-shadow 0.2s; background-color: ${state.menuColor}; /* Default */ } .mod-menu-button:hover { background-color: ${adjustColor(state.menuColor, -15)}; /* Darken on hover */ box-shadow: 0 2px 8px rgba(0,0,0,0.15); } .mod-menu-button-secondary { background-color: #4a4a4e; /* Dark gray for secondary actions */ } .mod-menu-button-secondary:hover { background-color: #5a5a5e; /* Slightly lighter gray */ } /* Styling for input fields */ .mod-menu-input { padding: 8px 10px; border-radius: 5px; border: 1px solid #454548; /* Darker border */ background-color: #2c2c30; /* Dark input background */ color: #e0e0e0; font-size: 14px; transition: border-color 0.2s, box-shadow 0.2s; box-sizing: border-box; } .mod-menu-input:focus { outline: none; border-color: ${state.menuColor}; /* Accent color on focus */ box-shadow: 0 0 0 2px ${hexToRgba(state.menuColor, 0.3)}; } /* Keybind modal specific improvements */ #keybind-modal { background: #2E2E34 !important; /* Darker background */ border-radius: 10px !important; padding: 30px 35px !important; box-shadow: 0 6px 25px rgba(0,0,0,0.3) !important; border: 1px solid rgba(255,255,255,0.1); } #keybind-modal-action { color: #b0b0b0 !important; /* Softer text */ font-size: 1.15em !important; margin-bottom: 16px !important; } #keybind-modal-cancel { background: #555 !important; /* Darker cancel button */ padding: 9px 25px !important; border-radius: 5px !important; font-size: 0.95em !important; transition: background-color 0.2s !important; } #keybind-modal-cancel:hover { background: #666 !important; } /* --- NEW LEADERBOARD STYLES --- */ #rep-leaderboard-content { display: flex; flex-direction: column; gap: 8px; /* Spacing between rows */ } .leaderboard-row { display: flex; align-items: center; border-radius: 8px; padding: 10px; transition: all 0.2s ease-in-out; cursor: pointer; /* Make it look clickable */ } .leaderboard-row:hover { transform: scale(1.02); /* Pop out on hover */ box-shadow: 0 4px 20px rgba(0,0,0,0.25); } .leaderboard-rank { font-size: 1.4em; font-weight: 700; width: 40px; text-align: center; margin-right: 15px; } .leaderboard-avatar { width: 48px; height: 48px; border-radius: 50%; object-fit: cover; margin-right: 15px; border: 2px solid #555; } .leaderboard-info { flex-grow: 1; } .leaderboard-name { font-weight: bold; font-size: 1.1em; color: #fff; } .leaderboard-rep { color: #aaa; font-size: 0.95em; } /* --- NEW: Styles for Chat Buttons and Pop-ups --- */ .chat-action-btn { padding: 0 12px; height: 100%; background: transparent; border: none; border-left: 1px solid rgba(255,255,255,0.1); color: #aaa; font-size: 16px; cursor: pointer; transition: color 0.2s, background-color 0.2s; } .chat-action-btn:hover { color: #fff; background-color: rgba(255,255,255,0.1); } .emoji-picker, .gif-picker-modal { display: none; position: absolute; bottom: 50px; /* Position above the input bar */ right: 10px; background: #2E2E34; border: 1px solid var(--menu-color, #4CAF50); border-radius: 8px; box-shadow: 0 5px 20px rgba(0,0,0,0.4); z-index: 10001; flex-direction: column; overflow: hidden; } .emoji-picker { padding: 10px; width: 280px; max-height: 200px; overflow-y: auto; } .emoji-picker span { font-size: 22px; cursor: pointer; padding: 5px; border-radius: 4px; display: inline-block; } .emoji-picker span:hover { background: #444; } .gif-picker-modal { width: 300px; height: 320px; } .gif-picker-header { display: flex; padding: 8px; border-bottom: 1px solid #444; background: rgba(0,0,0,0.2); } #gif-search-input { flex-grow: 1; background: #222; border: 1px solid #555; color: #eee; border-radius: 4px; padding: 6px 8px; outline: none; } #gif-search-input:focus { border-color: var(--menu-color, #4CAF50); } .gif-results-container { flex-grow: 1; overflow-y: auto; padding: 8px; display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; } .gif-results-container img { width: 100%; height: 120px; object-fit: cover; cursor: pointer; border-radius: 4px; border: 2px solid transparent; transition: border-color 0.2s; } .gif-results-container img:hover { border-color: var(--menu-color, #4CAF50); } .chat-image-preview { max-width: 90%; max-height: 150px; border-radius: 6px; margin-top: 8px; cursor: pointer; border: 1px solid #555; } /* --- NEW: One-Time Info Popup Styles --- */ .info-popup-overlay { display: none; position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; z-index: 10050; /* High z-index to be on top of everything */ background: rgba(0,0,0,0.8); align-items: center; justify-content: center; font-family: 'Segoe UI', Arial, sans-serif; } .info-popup-content { background: #2E2E34; border-radius: 12px; padding: 30px 40px; box-shadow: 0 8px 35px rgba(0,0,0,0.5); border: 1px solid var(--menu-color, #4CAF50); max-width: 500px; text-align: center; color: #e0e0e0; animation: fadeInProfile 0.3s cubic-bezier(.17,.67,.6,1.04); } .info-popup-content h2 { color: var(--menu-color, #4CAF50); margin-top: 0; margin-bottom: 15px; font-size: 1.6em; } .info-popup-content p { font-size: 1.1em; line-height: 1.6; margin-bottom: 25px; } .info-popup-content a { color: #3498db; /* A nice link blue */ text-decoration: none; font-weight: bold; } .info-popup-content a:hover { text-decoration: underline; } .info-popup-button { padding: 10px 30px; border-radius: 6px; border: none; color: #fff; font-size: 1.1em; font-weight: bold; cursor: pointer; transition: background-color 0.2s; background-color: var(--menu-color, #4CAF50); } .info-popup-button:hover { background-color: var(--menu-color-darker, #3e8e41); } `; document.head.appendChild(style); // Function to update --menu-color CSS variable for profile popup border etc. function updateCSSVariables() { document.documentElement.style.setProperty('--menu-color', state.menuColor); document.documentElement.style.setProperty('--menu-color-transparent', hexToRgba(state.menuColor, 0.5)); document.documentElement.style.setProperty('--menu-color-darker', adjustColor(state.menuColor, -15)); } updateCSSVariables(); // Initial call function hexToRgba(hex, alpha = 1) { let c = hex.replace('#', ''); if (c.length === 3) c = c[0]+c[0]+c[1]+c[1]+c[2]+c[2]; const num = parseInt(c, 16); return `rgba(${(num>>16)&255},${(num>>8)&255},${num&255},${alpha})`; } let lastChatMessageTime = 0; const chatCooldown = 10000; function filterProfanity(text) { if (!state.features.chatProfanityFilter) return text; // Use the config.chatProfanityList defined at the top return text.split(/\b/).map(word => { const lowerWord = word.toLowerCase(); if (config.chatProfanityList.some(profanity => lowerWord.includes(profanity))) { return '*'.repeat(word.length); } return word; }).join(''); } function replaceLinksWithDiscord(text) { const urlRegex = /https?:\/\/[^\s]+|www\.[^\s]+/gi; return text.replace(urlRegex, 'https://dsc.gg/143X'); } document.addEventListener('pointerdown', function primeDeathSoundOnce() { state.deathSound.volume = 0.01; // Very low volume state.deathSound.play().then(() => { state.deathSound.pause(); state.deathSound.currentTime = 0; state.deathSound.volume = 1; // Reset to full volume }).catch(()=>{/* User hasn't interacted yet */}); document.removeEventListener('pointerdown', primeDeathSoundOnce); document.removeEventListener('keydown', primeDeathSoundOnce); // Also remove keydown listener }); // Add keydown listener as well for priming document.addEventListener('keydown', function primeDeathSoundOnceKey() { // This is the same function as above, effectively. // It ensures that either click or keydown will prime the audio. // The removeEventListener calls will handle removing both. state.deathSound.volume = 0.01; state.deathSound.play().then(() => { state.deathSound.pause(); state.deathSound.currentTime = 0; state.deathSound.volume = 1; }).catch(()=>{}); document.removeEventListener('pointerdown', primeDeathSoundOnce); // Remove the click listener document.removeEventListener('keydown', primeDeathSoundOnceKey); // Remove this keydown listener }); function createChatSystem() { const chatContainer = document.createElement('div'); chatContainer.id = 'mod-menu-chat-container'; // --- ENHANCED CHAT CONTAINER STYLES --- chatContainer.style.cssText = ` position: fixed; left: ${state.uiLayout.chat.x}px; top: ${state.uiLayout.chat.y}px; width: ${state.uiLayout.chat.width}px; height: ${state.uiLayout.chat.height}px; z-index: 9999; display: ${state.features.chatVisible ? 'flex' : 'none'}; flex-direction: column; background: rgba(28, 28, 32, 0.97); /* Darker, more opaque */ border: 1px solid ${hexToRgba(state.menuColor, 0.5)}; border-radius: 8px; /* Smoother radius */ box-shadow: 0 5px 20px rgba(0,0,0,0.3); overflow: hidden; /* Important for border-radius */ user-select: none; /* Prevent text selection during drag */ font-family: 'Segoe UI', Arial, sans-serif; /* Modern font */ `; // --- ENHANCED CHAT TABS --- const chatTabs = document.createElement('div'); chatTabs.style.display = 'flex'; chatTabs.style.borderBottom = `1px solid ${hexToRgba(state.menuColor, 0.3)}`; chatTabs.style.background = `rgba(0,0,0,0.1)`; // Subtle background for tab bar const chatTab = document.createElement('div'); chatTab.textContent = '143X Chat'; chatTab.id = 'chat-tab-main'; chatTab.style.cssText = ` flex: 1; padding: 10px 12px; text-align: center; cursor: pointer; font-weight: 500; color: #fff; background: ${hexToRgba(state.menuColor, 0.25)}; /* Active by default */ transition: background 0.2s, color 0.2s; border-right: 1px solid ${hexToRgba(state.menuColor, 0.2)}; `; const usersTab = document.createElement('div'); usersTab.textContent = 'Online Users'; usersTab.id = 'chat-tab-users'; usersTab.style.cssText = ` flex: 1; padding: 10px 12px; text-align: center; cursor: pointer; font-weight: 500; color: #ccc; background: transparent; transition: background 0.2s, color 0.2s; `; chatTab.onclick = () => { document.getElementById('mod-menu-chat-body').style.display = 'flex'; document.getElementById('mod-menu-online-users').style.display = 'none'; chatTab.style.background = hexToRgba(state.menuColor, 0.25); chatTab.style.color = '#fff'; usersTab.style.background = 'transparent'; usersTab.style.color = '#ccc'; }; usersTab.onclick = () => { document.getElementById('mod-menu-chat-body').style.display = 'none'; document.getElementById('mod-menu-online-users').style.display = 'flex'; chatTab.style.background = 'transparent'; chatTab.style.color = '#ccc'; usersTab.style.background = hexToRgba(state.menuColor, 0.25); usersTab.style.color = '#fff'; }; chatTabs.appendChild(chatTab); chatTabs.appendChild(usersTab); // chatContainer.appendChild(chatTabs); // Tabs will be part of header now // --- ENHANCED CHAT HEADER (for dragging and close button) --- const chatHeader = document.createElement('div'); chatHeader.style.cssText = ` /* height: 38px; Combined with tabs */ /* padding: 0 12px; */ /* Padding will be within tabs */ background: rgba(0,0,0,0.1); /* Match tabs bar */ display: flex; /* justify-content: space-between; */ /* Tabs handle this */ align-items: center; cursor: move; /* Draggable handle */ border-bottom: 1px solid ${hexToRgba(state.menuColor, 0.3)}; `; chatHeader.dataset.draggable = 'true'; chatHeader.appendChild(chatTabs); // Tabs are inside header const chatToggle = document.createElement('div'); chatToggle.innerHTML = '×'; // HTML entity for X chatToggle.style.cssText = ` cursor: pointer; font-size: 22px; padding: 0 15px; color: #aaa; line-height: 1; transition: color 0.2s; position: absolute; right: 0; top: 0; height: 38px; /* Align with tab height */ display: flex; align-items: center; `; chatToggle.title = state.features.chatVisible ? 'Hide chat' : 'Show chat'; chatToggle.onclick = (e) => { e.stopPropagation(); toggleChatVisible(); }; chatToggle.onmouseenter = () => chatToggle.style.color = '#fff'; chatToggle.onmouseleave = () => chatToggle.style.color = '#aaa'; chatHeader.appendChild(chatToggle); // Add close button to the header chatContainer.appendChild(chatHeader); // --- ENHANCED CHAT AREA (messages and input) --- const chatArea = document.createElement('div'); chatArea.style.cssText = ` flex: 1; display: flex; flex-direction: column; overflow: hidden; /* For child elements */ background: rgba(20,20,24,0.9); /* Slightly different dark for content */ /* Border is now on main chatContainer */ `; const chatBody = document.createElement('div'); chatBody.id = 'mod-menu-chat-body'; chatBody.style.cssText = ` flex: 1; padding: 10px 15px; overflow-y: auto; display: flex; flex-direction: column; scrollbar-width: thin; scrollbar-color: ${state.menuColor} rgba(0,0,0,0.2); `; // Added scrollbar styling const onlineUsers = document.createElement('div'); onlineUsers.id = 'mod-menu-online-users'; onlineUsers.style.cssText = chatBody.style.cssText; // Same styling as chatBody onlineUsers.style.display = 'none'; // Hidden by default onlineUsers.innerHTML = '<div style="text-align:center;color:#888;margin-top:10px;">Loading users...</div>'; chatArea.appendChild(chatBody); chatArea.appendChild(onlineUsers); // --- MODIFIED: Create a container for the input and new buttons --- const chatInputContainer = document.createElement('div'); chatInputContainer.style.cssText = ` display: flex; align-items: center; border-top: 1px solid ${hexToRgba(state.menuColor, 0.3)}; background: rgba(0,0,0,0.25); `; const slowModeIndicator = document.createElement('span'); slowModeIndicator.id = 'slow-mode-indicator'; slowModeIndicator.style.cssText = ` display: none; /* Hidden by default */ position: absolute; left: 15px; /* Aligns with input padding */ top: 50%; transform: translateY(-50%); color: #ffc107; /* A nice warning yellow */ font-size: 13px; font-weight: bold; pointer-events: none; /* Allows you to click through it */ text-shadow: 1px 1px 2px rgba(0,0,0,0.5); `; const chatInput = document.createElement('input'); chatInput.id = 'mod-menu-chat-input'; chatInput.type = 'text'; chatInput.placeholder = `Press '${state.keybinds.chatEnabled.toUpperCase()}' to type...`; chatInput.style.cssText = ` flex-grow: 1; /* Allow input to take up most space */ padding: 12px 15px; border: none; background: transparent; /* Make background transparent */ color: #e0e0e0; outline: none; font-size: 14px; box-sizing: border-box; `; // --- NEW: Create the GIF and Emoji buttons --- const gifBtn = document.createElement('button'); gifBtn.id = 'gif-btn'; gifBtn.textContent = 'GIF'; gifBtn.title = 'Send a GIF'; gifBtn.className = 'chat-action-btn'; // We will style this class later const emojiBtn = document.createElement('button'); emojiBtn.id = 'emoji-btn'; emojiBtn.textContent = '🙂'; emojiBtn.title = 'Send an Emoji'; emojiBtn.className = 'chat-action-btn'; // Add elements to the container chatInputContainer.appendChild(chatInput); chatInputContainer.appendChild(slowModeIndicator); chatInputContainer.appendChild(gifBtn); chatInputContainer.appendChild(emojiBtn); // Add the new container to the chat area chatArea.appendChild(chatInputContainer); chatContainer.appendChild(chatArea); // --- ENHANCED RESIZE HANDLE --- const resizeHandle = document.createElement('div'); resizeHandle.style.cssText = ` position: absolute; right: 0; bottom: 0; width: 15px; height: 15px; cursor: nwse-resize; background-color: ${hexToRgba(state.menuColor, 0.4)}; /* Subtler handle */ opacity: 0.7; transition: opacity 0.2s, background-color 0.2s; border-top-left-radius: 5px; /* Rounded corner for handle */ `; resizeHandle.dataset.resizable = 'true'; resizeHandle.onmouseenter = () => { resizeHandle.style.opacity = '1'; resizeHandle.style.backgroundColor = hexToRgba(state.menuColor, 0.6); }; resizeHandle.onmouseleave = () => { resizeHandle.style.opacity = '0.7'; resizeHandle.style.backgroundColor = hexToRgba(state.menuColor, 0.4); }; chatContainer.appendChild(resizeHandle); // --- NEW: Add the pop-up modal HTML to the page --- const popupsHTML = ` <div id="emoji-picker" class="emoji-picker"></div> <div id="gif-picker-modal" class="gif-picker-modal"> <div class="gif-picker-header"> <input type="text" id="gif-search-input" placeholder="Search GIPHY..."> </div> <div id="gif-results-container" class="gif-results-container"></div> </div> `; const tempDiv = document.createElement('div'); tempDiv.innerHTML = popupsHTML; while(tempDiv.firstChild) { chatContainer.appendChild(tempDiv.firstChild); } // --- NEW: Event listeners for the new chat buttons and pickers --- // We get the buttons we created earlier in this function const emojiBtnEl = emojiBtn; const gifBtnEl = gifBtn; const emojiPicker = chatContainer.querySelector('#emoji-picker'); const gifPicker = chatContainer.querySelector('#gif-picker-modal'); // Emoji Picker Logic const emojis = "😀 😂 😊 😍 😎 😭 👍 👎 ❤️ 🔥 💀 👑 🏆 🎉 🙏 GG L".split(' '); emojiPicker.innerHTML = emojis.map(e => `<span>${e}</span>`).join(''); emojiBtnEl.onclick = (e) => { e.stopPropagation(); gifPicker.style.display = 'none'; emojiPicker.style.display = emojiPicker.style.display === 'block' ? 'none' : 'block'; }; emojiPicker.addEventListener('click', (e) => { if (e.target.tagName === 'SPAN') { document.getElementById('mod-menu-chat-input').value += e.target.textContent; document.getElementById('mod-menu-chat-input').focus(); } }); // GIF Picker Logic gifBtnEl.onclick = (e) => { e.stopPropagation(); emojiPicker.style.display = 'none'; // Note the change to 'flex' display for the GIF picker if (gifPicker.style.display === 'flex') { gifPicker.style.display = 'none'; } else { openGifPicker(); } }; chatContainer.querySelector('#gif-search-input').addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); searchGifs(); } }); chatContainer.querySelector('#gif-results-container').addEventListener('click', (e) => { if (e.target.tagName === 'IMG' && e.target.dataset.directUrl) { sendGifToChat(e.target.dataset.directUrl); } }); // Universal 'click outside' to close popups document.addEventListener('click', (e) => { if (emojiPicker.style.display === 'block' && !emojiBtnEl.contains(e.target) && !emojiPicker.contains(e.target)) { emojiPicker.style.display = 'none'; } if (gifPicker.style.display === 'flex' && !gifBtnEl.contains(e.target) && !gifPicker.contains(e.target)) { gifPicker.style.display = 'none'; } }); document.body.appendChild(chatContainer); makeDraggable(chatContainer, chatHeader); makeResizable(chatContainer, resizeHandle); } function syncChatBoxWithMenu() { const chatContainer = document.getElementById('mod-menu-chat-container'); if (!chatContainer) return; const menuColor = state.menuColor; const lighterMenuColor = hexToRgba(menuColor, 0.25); const borderColor = hexToRgba(menuColor, 0.5); const borderTopColor = hexToRgba(menuColor, 0.3); chatContainer.style.border = `1px solid ${borderColor}`; const chatHeader = chatContainer.querySelector('div[style*="cursor: move"]'); // The draggable header if (chatHeader) { chatHeader.style.borderBottom = `1px solid ${borderTopColor}`; } const chatTabMain = document.getElementById('chat-tab-main'); const chatTabUsers = document.getElementById('chat-tab-users'); if (chatTabMain && chatTabUsers) { // Determine which tab is active to reapply styles correctly const chatBodyVisible = document.getElementById('mod-menu-chat-body')?.style.display !== 'none'; if (chatBodyVisible) { chatTabMain.style.background = lighterMenuColor; chatTabMain.style.color = '#fff'; chatTabUsers.style.background = 'transparent'; chatTabUsers.style.color = '#ccc'; } else { chatTabMain.style.background = 'transparent'; chatTabMain.style.color = '#ccc'; chatTabUsers.style.background = lighterMenuColor; chatTabUsers.style.color = '#fff'; } chatTabMain.style.borderRight = `1px solid ${hexToRgba(menuColor, 0.2)}`; } const chatInput = document.getElementById('mod-menu-chat-input'); if (chatInput) { chatInput.style.borderTop = `1px solid ${borderTopColor}`; chatInput.placeholder = `Press '${state.keybinds.chatEnabled.toUpperCase()}' to type...`; // Update placeholder if keybind changes } const resizeHandle = chatContainer.querySelector('div[style*="cursor: nwse-resize"]'); if (resizeHandle) { const baseHandleColor = hexToRgba(menuColor, 0.4); const hoverHandleColor = hexToRgba(menuColor, 0.6); resizeHandle.style.backgroundColor = baseHandleColor; resizeHandle.onmouseenter = () => { resizeHandle.style.opacity = '1'; resizeHandle.style.backgroundColor = hoverHandleColor; }; resizeHandle.onmouseleave = () => { resizeHandle.style.opacity = '0.7'; resizeHandle.style.backgroundColor = baseHandleColor; }; } const chatBody = document.getElementById('mod-menu-chat-body'); if (chatBody) { chatBody.style.scrollbarColor = `${menuColor} rgba(0,0,0,0.2)`; } const onlineUsers = document.getElementById('mod-menu-online-users'); if (onlineUsers) { onlineUsers.style.scrollbarColor = `${menuColor} rgba(0,0,0,0.2)`; } } function rainbowTextStyle(name) { const rainbowColors = ["#ef3550","#f48fb1","#7e57c2","#2196f3","#26c6da","#43a047","#eeff41","#f9a825","#ff5722"]; return name.split('').map((char, i) => `<span style="color:${rainbowColors[i % rainbowColors.length]}; font-weight: bold; text-shadow: 0 0 3px ${rainbowColors[i % rainbowColors.length]}66;">${char}</span>` ).join(''); } // --- NEW: GIPHY and Emoji System Functions --- // Function to open the GIF picker and load trending GIFs async function openGifPicker() { const gifPicker = document.getElementById('gif-picker-modal'); const resultsContainer = document.getElementById('gif-results-container'); if (!gifPicker || !resultsContainer) return; gifPicker.style.display = 'flex'; resultsContainer.innerHTML = '<div style="color:#888; text-align:center; grid-column: 1 / -1;">Loading trending GIFs...</div>'; try { const response = await fetch(`https://api.giphy.com/v1/gifs/trending?api_key=${config.giphyApiKey}&limit=20&rating=pg-13`); const json = await response.json(); displayGifs(json.data); } catch (error) { resultsContainer.innerHTML = '<div style="color:#f77; text-align:center; grid-column: 1 / -1;">Could not load GIFs.</div>'; console.error("Giphy Trending Error:", error); } } // Function to search GIPHY async function searchGifs() { const searchInput = document.getElementById('gif-search-input'); const resultsContainer = document.getElementById('gif-results-container'); const query = searchInput.value.trim(); if (!query) return; resultsContainer.innerHTML = `<div style="color:#888; text-align:center; grid-column: 1 / -1;">Searching for "${escapeHTML(query)}"...</div>`; try { const response = await fetch(`https://api.giphy.com/v1/gifs/search?api_key=${config.giphyApiKey}&q=${encodeURIComponent(query)}&limit=30&rating=pg-13`); const json = await response.json(); displayGifs(json.data); } catch (error) { resultsContainer.innerHTML = '<div style="color:#f77; text-align:center; grid-column: 1 / -1;">Search failed.</div>'; console.error("Giphy Search Error:", error); } } // Function to display the GIFs in the picker function displayGifs(gifData) { const resultsContainer = document.getElementById('gif-results-container'); resultsContainer.innerHTML = ''; // Clear previous results if (!gifData || gifData.length === 0) { resultsContainer.innerHTML = '<div style="color:#888; text-align:center; grid-column: 1 / -1;">No results found.</div>'; return; } gifData.forEach(gif => { const img = document.createElement('img'); img.src = gif.images.fixed_height_small.url; img.dataset.directUrl = gif.images.original.url; img.alt = gif.title; resultsContainer.appendChild(img); }); } // Function to send a chosen GIF to the chat function sendGifToChat(gifUrl) { const auth = firebase.auth(); const currentUser = auth.currentUser; if (!currentUser || !gifUrl) return; const messagePayload = { uid: currentUser.uid, name: localStorage.getItem("nickname") || "Anon", text: gifUrl, // The message is just the URL time: firebase.database.ServerValue.TIMESTAMP, chatNameColor: localStorage.getItem("chatNameColor") || "#FFD700" }; firebase.database().ref("slitherChat").push(messagePayload); firebase.database().ref("discordBridge").push(messagePayload); document.getElementById('gif-picker-modal').style.display = 'none'; lastChatMessageTime = Date.now(); // Reset slow mode timer after sending a GIF } // --- NEW: Helper function to escape HTML characters for security --- function escapeHTML(str) { const p = document.createElement('p'); p.appendChild(document.createTextNode(str)); return p.innerHTML; } // --- REPLACEMENT: Corrected Firebase Chat Loading and Rendering System --- function loadFirebaseChat() { const script1 = document.createElement('script'); script1.src = 'https://www.gstatic.com/firebasejs/8.10.0/firebase-app.js'; script1.onload = () => { const script2 = document.createElement('script'); script2.src = 'https://www.gstatic.com/firebasejs/8.10.0/firebase-database.js'; script2.onload = () => { const script3 = document.createElement('script'); script3.src = 'https://www.gstatic.com/firebasejs/8.10.0/firebase-auth.js'; script3.onload = () => { const firebaseConfig = { // KEEP YOUR CONFIG PRIVATE apiKey: "AIzaSyCtTloqGNdhmI3Xt0ta11vF0MQJHiKpO7Q", authDomain: "chatforslither.firebaseapp.com", databaseURL: "https://chatforslither-default-rtdb.firebaseio.com", projectId: "chatforslither", storageBucket: "chatforslither.appspot.com", messagingSenderId: "1045559625491", appId: "1:1045559625491:web:79eb8200eb87edac00bce6" }; if (!firebase.apps.length) firebase.initializeApp(firebaseConfig); firebase.database().ref('/modInfo/latestVersion').once('value') .then(snapshot => { if (snapshot.exists()) { const latestVersion = snapshot.val(); state.versionStatus = (latestVersion === config.currentVersion) ? 'Current' : `Outdated! (v${latestVersion} is available)`; } else { state.versionStatus = 'Unknown'; } }).catch(error => { console.error("Firebase version check failed:", error); state.versionStatus = 'Check Failed'; }).finally(() => { if (typeof updateMenu === "function") updateMenu(); }); const auth = firebase.auth(); auth.signInAnonymously().then(async (userCredential) => { const uid = userCredential.user.uid; const playerDataRef = firebase.database().ref(`playerData/${uid}`); playerDataRef.once('value', (snapshot) => { if (!snapshot.exists()) { playerDataRef.set({ rep: 0, lastRepAwardTime: 0, lastChatRepTime: 0, }); } }); const nickname = localStorage.getItem("nickname") || "Anon"; const userRef = firebase.database().ref("onlineUsers/" + uid); let snapshot; try { snapshot = await userRef.once('value'); } catch (err) { console.error("Failed to fetch profile from Firebase:", err); snapshot = null; } if (snapshot && snapshot.exists()) { const cloudData = snapshot.val(); if (cloudData.profileAvatar) localStorage.setItem("profileAvatar", cloudData.profileAvatar); if (cloudData.profileMotto) localStorage.setItem("profileMotto", cloudData.profileMotto); } const localAvatar = localStorage.getItem("profileAvatar"); const localMotto = localStorage.getItem("profileMotto"); let needsUpdate = false; const updates = {}; if (localAvatar && (!snapshot?.val()?.profileAvatar || snapshot.val().profileAvatar !== localAvatar)) { updates.profileAvatar = localAvatar; needsUpdate = true; } if (localMotto && (!snapshot?.val()?.profileMotto || snapshot.val().profileMotto !== localMotto)) { updates.profileMotto = localMotto; needsUpdate = true; } if (needsUpdate) await userRef.update(updates); userRef.onDisconnect().remove(); await userRef.update({ name: nickname, uid: uid, lastActive: Date.now(), chatNameColor: localStorage.getItem("chatNameColor") || "#FFD700" }); setInterval(() => { userRef.update({ lastActive: Date.now() }); }, 30000); }).catch(err => { console.error("Firebase sign-in error:", err); }); firebase.database().ref("onlineUsers").on("value", snapshot => { const users = snapshot.val() || {}; const onlineUsersEl = document.getElementById('mod-menu-online-users'); if (onlineUsersEl) { const now = Date.now(); const usersList = Object.entries(users) .filter(([_, user]) => now - (user.lastActive || 0) < 300000) .map(([userUid, user]) => { let displayName = filterProfanity(user.name || 'Anon'); let nameColor = user.chatNameColor || '#FFD700'; let userIdentifier = (auth.currentUser && userUid === auth.currentUser.uid) ? ' <span style="color: #8f8; font-size:0.9em;">(You)</span>' : ''; if (isDev(user.uid, user.name)) displayName = rainbowTextStyle(displayName); else if (isVip(user.uid, user.name)) displayName = vipGlowStyle(displayName, nameColor); return `<div style="padding: 5px 2px; border-bottom: 1px solid rgba(255,255,255,0.05); display: flex; align-items: center;"><span style="width: 10px; height: 10px; border-radius: 50%; background-color: lime; margin-right: 8px;"></span><span class="online-username" data-uid="${user.uid}" style="color:${nameColor};font-weight:bold; flex-grow: 1; cursor:pointer;text-decoration:underline dotted;">${displayName}</span>${userIdentifier}</div>`; }).join(''); onlineUsersEl.innerHTML = usersList || '<div style="text-align:center;color:#888;margin-top:10px;">No other users online.</div>'; } }); // --- START OF FIXED CHAT LOADING LOGIC --- let chatMessagesArray = []; window.chatMessagesArray = chatMessagesArray; let latestTimeLoaded = 0; const chatBody = document.getElementById('mod-menu-chat-body'); // Step 1: Load initial messages firebase.database().ref("slitherChat").orderByChild("time").limitToLast(config.chatMaxMessages).once("value", async (snapshot) => { if (!snapshot.exists()) return; snapshot.forEach(child => { chatMessagesArray.push({ key: child.key, ...child.val() }); }); chatMessagesArray.sort((a, b) => a.time - b.time); if (chatMessagesArray.length > 0) { latestTimeLoaded = chatMessagesArray[chatMessagesArray.length - 1].time; } if (chatBody) { chatBody.innerHTML = ''; for (const msg of chatMessagesArray) { await renderChatMessage(msg, chatBody, auth.currentUser?.uid); } chatBody.scrollTop = chatBody.scrollHeight; } // Step 2: Listen for ONLY new messages from now on firebase.database().ref("slitherChat").orderByChild("time").startAt(latestTimeLoaded + 1).on("child_added", async (newSnapshot) => { const newMsg = { key: newSnapshot.key, ...newSnapshot.val() }; if (chatMessagesArray.some(m => m.key === newMsg.key)) return; chatMessagesArray.push(newMsg); if (chatMessagesArray.length > config.chatMaxMessages) chatMessagesArray.shift(); if (chatBody) { while (chatBody.children.length >= config.chatMaxMessages) { chatBody.removeChild(chatBody.firstChild); } await renderChatMessage(newMsg, chatBody, auth.currentUser?.uid, true); } }); }); // --- END OF FIXED CHAT LOADING LOGIC --- const chatInput = document.getElementById('mod-menu-chat-input'); chatInput.addEventListener('keydown', async function (e) { if (e.key === 'Enter') { const now = Date.now(); const timeSinceLastMessage = now - lastChatMessageTime; // NEW: Enforce slow mode with a better alert if (timeSinceLastMessage < chatCooldown) { const timeLeft = Math.ceil((chatCooldown - timeSinceLastMessage) / 1000); alert(`Slow mode is active. Please wait ${timeLeft} more second(s).`); e.preventDefault(); return; } e.preventDefault(); e.stopPropagation(); const currentUID = auth.currentUser.uid; try { const punishSnap = await firebase.database().ref(`chatPunishments/${currentUID}`).once('value'); if (punishSnap.exists()) { const punishment = punishSnap.val(); if (punishment.type === "timeout" && Date.now() < punishment.until) { let mins = Math.ceil((punishment.until - Date.now()) / 60000); alert(`You are timed out from chat for ${mins} more minute(s).`); chatInput.value = ''; chatInput.blur(); return; } } } catch (err) { console.error("Error checking punishment:", err); } if (chatInput.value.trim()) { const nickname = localStorage.getItem("nickname") || "Anon"; const messagePayload = { uid: currentUID, name: nickname, text: chatInput.value.trim(), time: firebase.database.ServerValue.TIMESTAMP, // Use server time for accuracy chatNameColor: localStorage.getItem("chatNameColor") || "#FFD700" }; firebase.database().ref("slitherChat").push(messagePayload); firebase.database().ref("discordBridge").push(messagePayload); lastChatMessageTime = Date.now(); chatInput.value = ''; // NEW: Start the visual countdown timer loop if (slowModeInterval) clearInterval(slowModeInterval); slowModeInterval = setInterval(updateSlowModeIndicator, 250); updateSlowModeIndicator(); // Run once immediately to hide it } chatInput.blur(); const userRef = firebase.database().ref(`playerData/${currentUID}`); const CHAT_REP_COOLDOWN = 5 * 60 * 1000; const snapshot = await userRef.once('value'); if (snapshot.exists()) { const lastChatTime = snapshot.val().lastChatRepTime || 0; if (now - lastChatTime > CHAT_REP_COOLDOWN) { await userRef.child('rep').transaction(currentRep => (currentRep || 0) + 1); await userRef.child('lastChatRepTime').set(now); console.log("Awarded 1 REP for chatting."); } } } }); // This is the closing of the keydown listener // vvv PASTE THIS NEW LISTENER vvv chatInput.addEventListener('input', function() { // When the user types, check if a cooldown is active if (Date.now() - lastChatMessageTime < chatCooldown) { // If it is, and the timer isn't already running, start it if (!slowModeInterval) { slowModeInterval = setInterval(updateSlowModeIndicator, 250); } } // Always run the update function on input to correctly show/hide the timer updateSlowModeIndicator(); }); // ^^^ END OF NEW LISTENER ^^^ }; document.head.appendChild(script3); }; document.head.appendChild(script2); }; document.head.appendChild(script1); } // --- Advanced renderChatMessage function with role and Discord bot support --- async function renderChatMessage(msg, chatBodyElement, currentUid, shouldScroll = false) { if (!msg || !msg.uid) return; // Add a check to ensure message data is valid let nameHtml; const userColor = msg.chatNameColor || '#FFD700'; const isSystemMessage = isSystemAccount(msg.uid); const isDiscordBot = msg.uid === 'discord_bot'; let displayName = isSystemMessage ? 'System' : escapeHTML(msg.name || 'Anon'); if (isDiscordBot) { const nameMatch = displayName.match(/^Discord\((.*)\)$/); if (nameMatch && nameMatch[1]) { displayName = escapeHTML(nameMatch[1]); } } else { displayName = filterProfanity(displayName); } let roleTagHTML = ''; if (isDiscordBot) { roleTagHTML = ` <span style="background: #7289DA; color: #fff; padding: 2px 7px; border-radius: 4px; font-size: 0.8em; font-weight: 700; vertical-align:middle;">DISCORD</span>`; nameHtml = `<span class="chat-username" data-uid="${msg.uid}" style="color:${userColor};font-weight:bold;cursor:pointer;text-decoration:underline dotted;">${displayName}</span>`; } else if (isDev(msg.uid)) { nameHtml = `<span class="chat-username" data-uid="${msg.uid}" style="cursor:pointer;text-decoration:underline dotted;">${rainbowTextStyle(displayName)}</span>`; roleTagHTML = ` <span style="background: #E91E63; color: #fff; padding: 2px 7px; border-radius: 4px; font-size: 0.8em; font-weight: 700; vertical-align:middle;">DEV</span>`; } else if (isVip(msg.uid, msg.name)) { nameHtml = `<span class="chat-username" data-uid="${msg.uid}" style="cursor:pointer;text-decoration:underline dotted;">${vipGlowStyle(displayName, userColor)}</span>`; roleTagHTML = ` <span style="background: #9C27B0; color: #fff; padding: 2px 7px; border-radius: 4px; font-size: 0.8em; font-weight: 700; vertical-align:middle;">VIP</span>`; } else if (isSystemMessage) { nameHtml = `<span style="color:${userColor};font-weight:bold;">${displayName}</span>`; } else { nameHtml = `<span class="chat-username" data-uid="${msg.uid}" style="color:${userColor};font-weight:bold;cursor:pointer;text-decoration:underline dotted;">${displayName}</span>`; } const el = document.createElement('div'); el.style.cssText = `margin-bottom: 8px; word-break: break-word; background: ${msg.uid === currentUid ? hexToRgba(state.menuColor, 0.12) : 'rgba(255,255,255,0.04)'}; padding: 8px 12px; border-radius: 6px; color: #ddd; font-family: inherit; font-size: 14px; line-height: 1.5; border-left: 3px solid ${msg.uid === currentUid ? state.menuColor : userColor};`; // --- MODIFIED: Add image/GIF rendering logic --- let messageText; let imageHTML = ''; const imageRegex = /(https?:\/\/[^\s]+\.(?:png|jpg|jpeg|gif|webp))/i; const imageMatch = msg.text.match(imageRegex); if (imageMatch) { // If the message is a direct image link const imageUrl = imageMatch[0]; messageText = escapeHTML(msg.text.replace(imageRegex, '').trim()); // Show any text besides the link imageHTML = `<br><img src="${imageUrl}" class="chat-image-preview" onclick="window.open('${imageUrl}', '_blank')">`; } else { // If it's a regular text message messageText = (isSystemMessage || isDiscordBot) ? msg.text : escapeHTML(filterProfanity(replaceLinksWithDiscord(msg.text))); } el.innerHTML = `<b>${nameHtml}${roleTagHTML}:</b> ${messageText}${imageHTML}`; chatBodyElement.appendChild(el); if (shouldScroll || chatBodyElement.scrollTop >= chatBodyElement.scrollHeight - chatBodyElement.clientHeight - 150) { chatBodyElement.scrollTop = chatBodyElement.scrollHeight; } } function createTrailOverlayCanvas() { let overlay = document.getElementById('snake-trail-overlay'); if (overlay) { overlay.style.display = 'block'; // <-- Always show overlay when trail is ON return overlay; } const gameCanvas = document.querySelector('canvas'); if (!gameCanvas) return null; overlay = document.createElement('canvas'); overlay.id = 'snake-trail-overlay'; overlay.style.position = 'fixed'; overlay.style.left = gameCanvas.style.left || '0px'; overlay.style.top = gameCanvas.style.top || '0px'; overlay.style.pointerEvents = 'none'; overlay.style.zIndex = '9000'; overlay.width = window.innerWidth; // Match window size as per your original overlay.height = window.innerHeight; // Match window size overlay.style.display = 'block'; // <-- Make sure it's visible when created document.body.appendChild(overlay); // Adjust overlay size on resize window.addEventListener('resize', () => { if (overlay) { // Check if overlay still exists overlay.width = window.innerWidth; overlay.height = window.innerHeight; } }); return overlay; } function toggleChatVisible() { state.features.chatVisible = !state.features.chatVisible; const chatContainer = document.getElementById('mod-menu-chat-container'); if (chatContainer) { chatContainer.style.display = state.features.chatVisible ? 'flex' : 'none'; } if (typeof updateMenu === "function") updateMenu(); // Ensure updateMenu is called } function addChatMessage(messageContent, isSystemMessage = false) { // This function is largely for local messages if ever needed. // Firebase handles the main chat display. console.log("Local addChatMessage called (primarily for debug/local system messages):", messageContent); // If you want to display these in the chat UI, you would add DOM manipulation here // similar to renderChatMessage but without Firebase specific data. } function updateChatDisplay() { // This function is effectively replaced by the real-time updates // from Firebase handled by renderChatMessage. // console.log("updateChatDisplay called (mostly deprecated)."); } function makeDraggable(element, handle) { handle.addEventListener('mousedown', function(e) { // Check if the event target is the handle itself or a child that shouldn't prevent dragging (e.g. text in header) // And ensure it's a left-click if ((e.target.dataset.draggable === 'true' || handle.contains(e.target)) && e.button === 0) { // Prevent dragging if mousedown on an interactive element within the handle (e.g., a button in chat header) if (e.target.tagName === 'BUTTON' || e.target.tagName === 'INPUT' || e.target.tagName === 'A' || e.target.closest('button, input, a')) { return; } e.preventDefault(); // Prevent text selection state.draggingElement = element; state.dragStartX = e.clientX; state.dragStartY = e.clientY; state.elementStartX = parseInt(element.style.left, 10) || 0; // Use current position state.elementStartY = parseInt(element.style.top, 10) || 0; } }); } function makeResizable(element, handle) { handle.addEventListener('mousedown', function(e) { if (e.target.dataset.resizable === 'true' && e.button === 0) { e.preventDefault(); state.resizingElement = element; state.dragStartX = e.clientX; state.dragStartY = e.clientY; state.elementStartWidth = parseInt(element.style.width, 10) || 300; state.elementStartHeight = parseInt(element.style.height, 10) || 200; } }); } // --- START: Slow Mode Indicator Logic --- let slowModeInterval = null; // This will hold our timer function updateSlowModeIndicator() { const indicator = document.getElementById('slow-mode-indicator'); const chatInput = document.getElementById('mod-menu-chat-input'); if (!indicator || !chatInput) return; // Exit if elements don't exist const now = Date.now(); const timeSinceLastMessage = now - lastChatMessageTime; if (timeSinceLastMessage < chatCooldown && chatInput.value.length > 0) { // If in cooldown and user is typing, show the timer const timeLeft = Math.ceil((chatCooldown - timeSinceLastMessage) / 1000); indicator.textContent = `Slow Mode: ${timeLeft}s`; indicator.style.display = 'block'; chatInput.style.textIndent = '115px'; // Push the typed text to the right } else { // Otherwise, hide the timer indicator.style.display = 'none'; chatInput.style.textIndent = '0px'; // Reset text position if (slowModeInterval) { clearInterval(slowModeInterval); // Stop the timer loop slowModeInterval = null; } } } // --- END: Slow Mode Indicator Logic --- // PASTE THIS ENTIRE NEW FUNCTION HERE function applyUIScale() { const menu = document.getElementById('mod-menu'); const chat = document.getElementById('mod-menu-chat-container'); const serverBox = document.getElementById('custom-server-box'); // Get the server box const scaleValue = state.uiScale; // Apply scaling to the main menu if (menu) { if (menu.style.right && menu.style.right !== 'auto') { menu.style.transformOrigin = 'top right'; } else { menu.style.transformOrigin = 'top left'; } menu.style.transform = `scale(${scaleValue})`; } // Apply scaling to the chat window if (chat) { chat.style.transformOrigin = 'top left'; chat.style.transform = `scale(${scaleValue})`; } // Apply scaling to the server IP box if (serverBox) { serverBox.style.transformOrigin = 'center top'; // Scales from the top center serverBox.style.transform = `scale(${scaleValue})`; } } document.addEventListener('mousemove', function(e) { if (state.draggingElement) { const dx = e.clientX - state.dragStartX; const dy = e.clientY - state.dragStartY; let newX = state.elementStartX + dx; let newY = state.elementStartY + dy; // Boundary checks (optional, but good for usability) const eleRect = state.draggingElement.getBoundingClientRect(); newX = Math.max(0, Math.min(newX, window.innerWidth - eleRect.width)); newY = Math.max(0, Math.min(newY, window.innerHeight - eleRect.height)); state.draggingElement.style.left = `${newX}px`; state.draggingElement.style.top = `${newY}px`; const id = state.draggingElement.id; if (id === 'mod-menu') { state.uiLayout.menu.x = newX; state.uiLayout.menu.y = newY; } else if (id === 'mod-menu-chat-container') { state.uiLayout.chat.x = newX; state.uiLayout.chat.y = newY; } } if (state.resizingElement) { const dx = e.clientX - state.dragStartX; const dy = e.clientY - state.dragStartY; const newWidth = Math.max(250, state.elementStartWidth + dx); // Min width const newHeight = Math.max(200, state.elementStartHeight + dy); // Min height state.resizingElement.style.width = `${newWidth}px`; state.resizingElement.style.height = `${newHeight}px`; const id = state.resizingElement.id; if (id === 'mod-menu') { state.uiLayout.menu.width = newWidth; state.uiLayout.menu.height = newHeight; } else if (id === 'mod-menu-chat-container') { state.uiLayout.chat.width = newWidth; state.uiLayout.chat.height = newHeight; } } }); document.addEventListener('mouseup', function() { if (state.draggingElement || state.resizingElement) { localStorage.setItem('modMenuUILayout', JSON.stringify(state.uiLayout)); } state.draggingElement = null; state.resizingElement = null; }); // === MENU CREATION (Structural Change) === const menu = document.createElement('div'); menu.id = 'mod-menu'; menu.style.position = 'fixed'; menu.style.top = state.uiLayout.menu.y !== null ? `${state.uiLayout.menu.y}px` : '50px'; menu.style.left = state.uiLayout.menu.x !== null ? `${state.uiLayout.menu.x}px` : (config.menuPosition === 'left' ? '50px' : (config.menuPosition === 'center' ? '50%' : 'auto')); if (config.menuPosition === 'center' && state.uiLayout.menu.x === null) { menu.style.transform = 'translateX(-50%)'; } menu.style.right = state.uiLayout.menu.x !== null ? 'auto' : (config.menuPosition === 'right' ? '50px' : 'auto'); // --- ENHANCED MENU STYLES --- menu.style.background = 'rgba(28, 28, 32, 0.97)'; // Darker, more opaque menu.style.border = `1px solid ${hexToRgba(state.menuColor, 0.6)}`; // Thinner, but distinct border menu.style.borderRadius = '10px'; // Consistent radius // Padding is handled by header and content area menu.style.zIndex = '9999'; menu.style.color = '#e0e0e0'; // Softer white menu.style.fontFamily = "'Segoe UI', Arial, sans-serif"; // Modern font menu.style.fontSize = '14px'; // Width is set in updateMenu based on simplified state menu.style.boxShadow = '0 6px 25px rgba(0,0,0,0.3)'; // Softer, larger shadow menu.style.backdropFilter = 'blur(8px)'; // Stronger blur if supported menu.style.transition = 'border-color 0.3s, box-shadow 0.3s'; // For color changes menu.style.userSelect = "none"; // Prevent text selection on menu body menu.style.overflow = 'hidden'; // Crucial for border-radius on children document.body.appendChild(menu); // Persistent Draggable Header for Main Menu const menuDraggableHeader = document.createElement('div'); menuDraggableHeader.id = 'mod-menu-draggable-header'; menuDraggableHeader.dataset.draggable = 'true'; // For makeDraggable // Styles for this header will be set in updateMenu to react to state.menuColor and state.menuName menu.appendChild(menuDraggableHeader); // Persistent Content Area for Main Menu const menuContentArea = document.createElement('div'); menuContentArea.id = 'mod-menu-content-area'; menuContentArea.style.padding = '0 20px 15px 20px'; // Padding for content below header menuContentArea.style.maxHeight = 'calc(90vh - 80px)'; // Max height with some margin menuContentArea.style.overflowY = 'auto'; // Scrollable content menuContentArea.style.overflowX = 'hidden'; // Custom scrollbar for content area menuContentArea.style.scrollbarWidth = 'thin'; menuContentArea.style.scrollbarColor = `${state.menuColor} rgba(0,0,0,0.2)`; menu.appendChild(menuContentArea); makeDraggable(menu, menuDraggableHeader); // Initialize dragging // --- START: Add Resize Handle for Main Menu --- const menuResizeHandle = document.createElement('div'); menuResizeHandle.id = 'mod-menu-resize-handle'; menuResizeHandle.style.cssText = ` position: absolute; right: 0; bottom: 0; width: 18px; /* A good size for grabbing */ height: 18px; cursor: nwse-resize; /* The diagonal resize cursor */ z-index: 10000; /* Ensures it's on top of menu content */ opacity: 0.6; transition: opacity 0.2s, border-color 0.3s; `; menuResizeHandle.dataset.resizable = 'true'; menuResizeHandle.onmouseenter = () => { menuResizeHandle.style.opacity = '1'; }; menuResizeHandle.onmouseleave = () => { menuResizeHandle.style.opacity = '0.6'; }; menu.appendChild(menuResizeHandle); // Now, connect the handle to the resizing logic function makeResizable(menu, menuResizeHandle); // --- END: Add Resize Handle --- // (modal injection): // REPLACE the old block with this NEW, FIXED block // (modal injection): if (!document.getElementById('keybind-modal-overlay')) { const modalHTML = ` <div id="keybind-modal-overlay" style="display:none; position:fixed; top:0; left:0; width:100vw; height:100vh; z-index:10002; background:rgba(0,0,0,0.75); align-items:center; justify-content:center; font-family: 'Segoe UI', Arial, sans-serif;"> <div id="keybind-modal" style="background:#2E2E34; border-radius:10px; padding:30px 35px; box-shadow:0 6px 25px rgba(0,0,0,0.4); display:flex; flex-direction:column; align-items:center; min-width:320px; border: 1px solid rgba(255,255,255,0.1);"> <div style="color:#fff; font-size:1.4em; font-weight:600; margin-bottom:12px;">Rebind Key</div> <div id="keybind-modal-action" style="color:#b0b0b0; font-size:1.15em; margin-bottom:18px;">Action: Placeholder</div> <div style="color:#fff; font-size:1.1em; margin-bottom:24px;">Press a key to assign... <br><small>(Or scroll mouse wheel)</small></div> <button id="keybind-modal-cancel" style="background:#555; color:#fff; border:none; padding:9px 25px; border-radius:5px; font-size:0.95em; cursor:pointer; transition: background-color 0.2s;">Cancel</button> </div> </div> `; const tempDiv = document.createElement('div'); tempDiv.innerHTML = modalHTML; document.body.appendChild(tempDiv.firstElementChild); // Attach the click listener for the cancel button RIGHT HERE <<< FIX // This guarantees the button exists before we try to find it. const cancelBtn = document.getElementById('keybind-modal-cancel'); if (cancelBtn) { // We can reuse the existing `closeKeybindModal` function. <<< FIX // It already does everything we need (hides the modal and resets the state). cancelBtn.onclick = closeKeybindModal; } } // START OF CODE TO PASTE // --- NEW: Edit Profile Modal --- if (!document.getElementById('edit-profile-modal-overlay')) { const profileModalStyle = document.createElement('style'); profileModalStyle.textContent = ` #edit-profile-modal-overlay { display: none; position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; z-index: 10003; background: rgba(0,0,0,0.75); align-items: center; justify-content: center; font-family: 'Segoe UI', Arial, sans-serif; } #edit-profile-modal { background: #2E2E34; border-radius: 10px; padding: 25px 30px; box-shadow: 0 6px 25px rgba(0,0,0,0.4); display:flex; flex-direction:column; min-width: 400px; border: 1px solid rgba(255,255,255,0.1); } .profile-modal-title { color: #fff; font-size: 1.4em; font-weight: 600; margin-bottom: 20px; text-align: center; } .profile-modal-label { color: #bbb; font-size: 0.9em; margin-bottom: 5px; } .profile-modal-input { width: 100%; padding: 10px; margin-bottom: 15px; background: #222; border: 1px solid #555; border-radius: 5px; color: #eee; font-size: 1em; box-sizing: border-box; transition: border-color 0.2s; } .profile-modal-input:focus { outline: none; border-color: ${state.menuColor}; } .profile-modal-buttons { display: flex; justify-content: flex-end; gap: 10px; margin-top: 10px; } #profile-modal-save { background: ${state.menuColor}; color: #fff; } #profile-modal-cancel { background: #555; color: #fff; } .profile-modal-button { border: none; padding: 9px 20px; border-radius: 5px; font-size: 0.95em; cursor: pointer; transition: background-color 0.2s; } #profile-modal-save:hover { background: ${adjustColor(state.menuColor, -15)}; } #profile-modal-cancel:hover { background: #666; } `; document.head.appendChild(profileModalStyle); const profileModalHTML = ` <div id="edit-profile-modal-overlay"> <div id="edit-profile-modal"> <div class="profile-modal-title">Edit Your Profile</div> <label for="profile-avatar-input" class="profile-modal-label">Avatar URL (.png, .jpg, .gif)</label> <input id="profile-avatar-input" type="text" class="profile-modal-input" placeholder="https://i.imgur.com/example.png"> <label for="profile-motto-input" class="profile-modal-label">Motto / Status</label> <input id="profile-motto-input" type="text" class="profile-modal-input" placeholder="The best slither player!" maxlength="60"> <div id="profile-modal-status" style="color: #ffc107; text-align: center; height: 18px; margin-top: 5px; font-size: 0.9em;"></div> <div class="profile-modal-buttons"> <button id="profile-modal-cancel" class="profile-modal-button">Cancel</button> <button id="profile-modal-save" class="profile-modal-button">Save</button> </div> </div> </div> `; const tempDiv = document.createElement('div'); tempDiv.innerHTML = profileModalHTML; document.body.appendChild(tempDiv.firstElementChild); // Attach listeners for the modal's buttons RIGHT AWAY // Attach listeners for the modal's buttons RIGHT AWAY document.getElementById('profile-modal-cancel').onclick = () => { const editModal = document.getElementById('edit-profile-modal-overlay'); if (editModal) { editModal.style.display = 'none'; delete editModal.dataset.targetUid; // Add this line to clear the target } }; // The 'Save' button logic will be handled later. } // END OF CODE TO PASTE // PASTE THIS ENTIRE BLOCK // --- NEW: Timeout Modal --- if (!document.getElementById('timeout-modal-overlay')) { const timeoutModal = document.createElement('div'); timeoutModal.id = 'timeout-modal-overlay'; timeoutModal.style = ` display:none; position:fixed; top:0; left:0; width:100vw; height:100vh; z-index:10005; background:rgba(0,0,0,0.7); align-items:center; justify-content:center; font-family: 'Segoe UI', Arial, sans-serif;`; timeoutModal.innerHTML = ` <div style="background:#2E2E34;padding:28px 33px;border-radius:10px;box-shadow:0 6px 25px rgba(0,0,0,0.4);min-width:320px;display:flex;flex-direction:column;align-items:center; border: 1px solid rgba(255,255,255,0.1);"> <div style="color:#fff;font-size:1.3em;font-weight:600;margin-bottom:20px;">Timeout User</div> <div style="margin-bottom:15px; display:flex; align-items:center; gap: 8px;"> <input id="timeout-value" type="number" min="1" max="9999" value="5" style="width:70px;padding:8px 10px;border-radius:5px;border:1px solid #555;background:#222;color:#eee;font-size:1em;text-align:center;"> <select id="timeout-unit" style="padding:8px 10px;border-radius:5px;border:1px solid #555;background:#222;color:#eee;font-size:1em;"> <option value="minutes">Minutes</option> <option value="hours">Hours</option> <option value="days">Days</option> </select> </div> <div style="display:flex;gap:10px; margin-top:10px;"> <button id="timeout-cancel-btn" style="padding:9px 20px;border-radius:5px;background:#555;color:#fff;border:none;cursor:pointer; font-size: 0.95em; transition: background 0.2s;">Cancel</button> <button id="timeout-confirm-btn" style="padding:9px 20px;border-radius:5px;background:#c9302c;color:#fff;border:none;cursor:pointer; font-size: 0.95em; transition: background 0.2s;">Confirm Timeout</button> </div> </div> `; document.body.appendChild(timeoutModal); // --- Add Listeners for the Timeout Modal --- document.getElementById('timeout-cancel-btn').onclick = () => { document.getElementById('timeout-modal-overlay').style.display = 'none'; }; document.getElementById('timeout-confirm-btn').onclick = async () => { const overlay = document.getElementById('timeout-modal-overlay'); const uid = overlay.dataset.targetUid; const username = overlay.dataset.targetName; const value = parseInt(document.getElementById('timeout-value').value, 10); const unit = document.getElementById('timeout-unit').value; if (!uid || !username || !value || value < 1) { alert("Invalid timeout value."); return; } let mins = value; if (unit === 'hours') mins *= 60; if (unit === 'days') mins *= 1440; // 60 * 24 const until = Date.now() + mins * 60 * 1000; try { await firebase.database().ref(`chatPunishments/${uid}`).set({ type: "timeout", until, by: firebase.auth().currentUser.uid, name: username }); await firebase.database().ref("slitherChat").push({ uid: "system", name: "System", text: `${username} was timed out from chat for ${value} ${unit}.`, time: Date.now(), chatNameColor: "#e91e63" }); alert(`${username} timed out successfully.`); overlay.style.display = 'none'; document.getElementById('profile-popup')?.remove(); } catch(err) { alert(`Failed to timeout user: ${err.message}`); } }; } // --- NEW: REP Leaderboard Modal --- if (!document.getElementById('rep-leaderboard-modal')) { const leaderboardModal = document.createElement('div'); leaderboardModal.id = 'rep-leaderboard-modal'; // We set the border color here using a CSS variable that our other code already updates leaderboardModal.style = ` display:none; position:fixed; top:0; left:0; width:100vw; height:100vh; z-index:10010; background:rgba(0,0,0,0.75); align-items:center; justify-content:center; font-family: 'Segoe UI', Arial, sans-serif;`; leaderboardModal.innerHTML = ` <div style="background:#23232a; border-radius:12px; padding:28px 35px; min-width:380px; max-height: 80vh; display: flex; flex-direction: column; box-shadow:0 6px 25px rgba(0,0,0,0.4); border: 1px solid var(--menu-color, #4CAF50); position:relative;"> <button id="rep-leaderboard-close" style="position:absolute;top:10px;right:10px;font-size:1.5em;background:none;border:none;color:#aaa;cursor:pointer;line-height:1;">×</button> <h2 style="color:#FFD700; margin-top:0; text-align:center; padding-bottom: 10px; border-bottom: 1px solid #444;">REP Leaderboard</h2> <div id="rep-leaderboard-content" style="margin-top:15px; overflow-y: auto; padding-right: 10px;"></div> </div> `; document.body.appendChild(leaderboardModal); // Attach the listener to the close button right away document.getElementById('rep-leaderboard-close').onclick = () => { leaderboardModal.style.display = 'none'; }; } // --- NEW: Cleanup for Expired Punishments --- // This runs in the background to remove old timeouts from the database. setInterval(async () => { try { const punishRef = firebase.database().ref("chatPunishments"); const snap = await punishRef.orderByChild('until').endAt(Date.now()).once('value'); if (snap.exists()) { const updates = {}; snap.forEach(child => { updates[child.key] = null; // Mark for deletion }); punishRef.update(updates); console.log('Cleaned up expired punishments.'); } } catch (err) { console.error('Error during punishment cleanup:', err); } }, 5 * 60 * 1000); // Check every 5 minutes // --- ENHANCED FPS/PING DISPLAYS --- const fpsDisplay = document.createElement('div'); fpsDisplay.id = 'fps-display'; fpsDisplay.style.cssText = ` position: fixed; bottom: 10px; right: 10px; color: #e0e0e0; font-family: 'Segoe UI', Arial, sans-serif; font-size: 13px; z-index: 10000; display: ${state.features.fpsDisplay ? 'block' : 'none'}; background: rgba(15,15,18,0.85); padding: 5px 10px; border-radius: 5px; border: 1px solid rgba(255,255,255,0.1); backdrop-filter: blur(4px); box-shadow: 0 1px 3px rgba(0,0,0,0.3); `; document.body.appendChild(fpsDisplay); // Ping display removed from here, integrated into simplified menu if needed, or can be added back similarly // const pingDisplay = document.createElement('div'); ... // --- ENHANCED CIRCLE VISUAL --- const circleVisual = document.createElement('div'); circleVisual.id = 'circle-visual'; circleVisual.style.cssText = ` position: fixed; border: 2px dashed ${hexToRgba(state.menuColor, 0.7)}; border-radius: 50%; pointer-events: none; transform: translate(-50%, -50%); z-index: 9998; display: none; transition: all 0.2s ease; box-shadow: 0 0 12px ${hexToRgba(state.menuColor, 0.3)}, inset 0 0 8px ${hexToRgba(state.menuColor, 0.2)}; `; document.body.appendChild(circleVisual); // Chat overlay (for updates/maintenance) - style slightly enhanced const chatOverlay = document.createElement('div'); chatOverlay.id = 'mod-menu-chat-overlay'; chatOverlay.style.cssText = ` position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%); background: rgba(20,20,24,0.95); border: 1px solid ${state.menuColor}; border-radius: 8px; padding: 25px 30px; z-index: 10000; color: #e0e0e0; font-family: 'Segoe UI', Arial, sans-serif; font-size: 18px; text-align: center; display: none; box-shadow: 0 5px 20px rgba(0,0,0,0.4); `; chatOverlay.textContent = 'Chat feature is currently under maintenance.'; document.body.appendChild(chatOverlay); async function promptForUniqueNickname() { let nickname; let isValidNickname = false; while (!isValidNickname) { nickname = prompt("Enter a nickname for chat (1-20 chars, letters, numbers, and underscores_ only):"); if (nickname === null) { nickname = "Anon"; isValidNickname = true; break; } nickname = nickname.trim(); if (nickname === "") { nickname = "Anon"; isValidNickname = true; break; } if (nickname.length < 1 || nickname.length > 20) { alert("Nickname must be between 1 and 20 characters long."); continue; } if (!/^[a-zA-Z0-9_]+$/.test(nickname)) { alert("Nickname can only contain letters, numbers, and underscores (_)."); continue; } if (nickname.toLowerCase() === "anon") { isValidNickname = true; break; } let exists = false; if (typeof firebase !== "undefined" && firebase.database) { try { const snapshot = await firebase.database().ref("onlineUsers") .orderByChild("name_lowercase") .equalTo(nickname.toLowerCase()) .once('value'); exists = snapshot.exists(); } catch (e) { console.warn("Firebase nickname uniqueness check failed during prompt:", e); exists = false; } } else { console.warn("promptForUniqueNickname: Firebase not available to check uniqueness."); } if (exists) { alert("That nickname is already in use. Please choose another."); } else { isValidNickname = true; } } localStorage.setItem("nickname", nickname); if (firebase && firebase.auth && firebase.auth().currentUser) { const userRef = firebase.database().ref(`onlineUsers/${firebase.auth().currentUser.uid}`); userRef.update({ name: nickname, name_lowercase: nickname.toLowerCase() }) .catch(err => console.error("Error updating nickname in Firebase during prompt:", err)); } return nickname; } (async function ensureUniqueNickname() { if (!localStorage.getItem("nickname")) { await promptForUniqueNickname(); } else { const nickname = localStorage.getItem("nickname"); // Basic check if Firebase is likely loaded - more robust checks are in loadFirebaseChat if (typeof firebase !== "undefined" && firebase.database) { try { const snapshot = await firebase.database().ref("onlineUsers").once('value'); const users = snapshot.val() || {}; const currentUserUid = firebase.auth().currentUser ? firebase.auth().currentUser.uid : null; // If someone else (not current user) is using this nickname, prompt again const isTakenByOther = Object.entries(users).some(([uid, user]) => uid !== currentUserUid && user.name && user.name.toLowerCase() === nickname.toLowerCase() ); if (isTakenByOther) { alert("That nickname is already in use by another player. Please choose another."); await promptForUniqueNickname(); } } catch (e) { console.warn("Firebase check for nickname failed, proceeding:", e); } } } createChatSystem(); loadFirebaseChat(); syncChatBoxWithMenu(); // Initial sync after creation })(); // --- THOROUGHLY REVAMPED updateMenu FUNCTION --- function updateMenu() { const menuColor = state.menuColor; const menuDraggableHeader = document.getElementById('mod-menu-draggable-header'); const menuContentArea = document.getElementById('mod-menu-content-area'); // Update persistent draggable header styles if (menuDraggableHeader) { menuDraggableHeader.style.cssText = ` padding: 12px 20px; /* Consistent padding */ margin-bottom: 10px; /* Space before content */ background: linear-gradient(to bottom, ${hexToRgba(menuColor, 0.3)}, ${hexToRgba(menuColor, 0.2)}); border-bottom: 1px solid ${hexToRgba(menuColor, 0.4)}; cursor: move; display: flex; justify-content: space-between; align-items: center; user-select: none; `; // Content of the header (title + version/buttons) menuDraggableHeader.innerHTML = ` <h2 id="mod-menu-title" style="margin:0; color:#fff; font-size:1.4em; font-weight:600; text-shadow: 1px 1px 2px rgba(0,0,0,0.3);"> ${state.menuName} </h2> <div style="color:#ccc; font-size:0.9em;">VX7 - GreasyFork</div> `; } // Update main menu border and shadow menu.style.border = `1px solid ${hexToRgba(menuColor, 0.6)}`; menu.style.boxShadow = `0 6px 25px ${hexToRgba(menuColor, 0.15)}`; circleVisual.style.border = `2px dashed ${hexToRgba(menuColor, 0.7)}`; circleVisual.style.boxShadow = `0 0 12px ${hexToRgba(menuColor, 0.3)}, inset 0 0 8px ${hexToRgba(menuColor, 0.2)}`; // --- START: Update Menu Resize Handle Style --- const menuResizeHandle = document.getElementById('mod-menu-resize-handle'); if (menuResizeHandle) { // Creates a visual triangle in the corner using borders menuResizeHandle.style.borderRight = `2px solid ${hexToRgba(menuColor, 0.8)}`; menuResizeHandle.style.borderBottom = `2px solid ${hexToRgba(menuColor, 0.8)}`; } // --- END: Update Menu Resize Handle Style --- // Update scrollbar color for content area if (menuContentArea) { menuContentArea.style.scrollbarColor = `${menuColor} rgba(0,0,0,0.2)`; } const arrow = state.showCustomization ? '▼' : '▶'; const inputStyle = `padding:8px 10px; border-radius:5px; border:1px solid #454548; background-color:#2c2c30; color:#e0e0e0; font-size:14px; box-sizing:border-box; transition: border-color 0.2s, box-shadow 0.2s;`; const focusStyle = `this.style.borderColor='${menuColor}'; this.style.boxShadow='0 0 0 2px ${hexToRgba(menuColor, 0.3)}';`; const blurStyle = `this.style.borderColor='#454548'; this.style.boxShadow='none';`; const buttonStyle = (bgColor = menuColor, textColor = '#fff') => `padding:8px 15px; border-radius:6px; border:none; color:${textColor}; font-size:14px; font-weight:500; cursor:pointer; transition:background-color 0.2s, box-shadow 0.2s; background-color:${bgColor};`; const buttonHoverStyle = (bgColor = menuColor) => `this.style.backgroundColor='${adjustColor(bgColor, -15)}'; this.style.boxShadow='0 2px 8px rgba(0,0,0,0.15)';`; const buttonLeaveStyle = (bgColor = menuColor) => `this.style.backgroundColor='${bgColor}'; this.style.boxShadow='none';`; if (state.simplified) { menu.style.width = state.uiLayout.menu.width !== null ? `${state.uiLayout.menu.width}px` : '340px'; // Slightly wider simplified menu menuContentArea.innerHTML = ` <div style="display:flex; justify-content:flex-end; margin-bottom:10px;"> <button id="default-menu-btn" title="Expand menu" style="${buttonStyle(menuColor)}; padding: 6px 12px; font-size: 13px;" onmouseover="${buttonHoverStyle(menuColor)}" onmouseout="${buttonLeaveStyle(menuColor)}"> Full Menu </button> </div> <div style="background:${hexToRgba(menuColor,0.08)}; padding:12px 15px; border-radius:8px; margin-bottom:15px; border: 1px solid ${hexToRgba(menuColor,0.2)};"> <div style="font-size:1.1em; margin-bottom:10px; color:${menuColor}; font-weight:600; text-align:center; padding-bottom:8px; border-bottom: 1px solid ${hexToRgba(menuColor,0.2)};"> Quick Status </div> <div style="display: grid; grid-template-columns: auto 1fr; gap: 8px 15px; font-size:14px; line-height:1.8;"> <span><b>Perf Mode:</b></span> <span style="color:#87CEEB; text-align:right;">Low (Optimized)</span> <span><b>Zoom:</b></span> <span style="text-align:right;">${Math.round(100 / state.zoomFactor)}%</span> <span><b>FPS:</b></span> <span style="color:#90EE90; text-align:right;">${state.fps}</span> <span><b>Server:</b></span> <span style="color:#FFD700; text-align:right;">${state.features.showServer ? (state.server || 'N/A') : 'Hidden'}</span> <span><b>Chat:</b></span> <span style="color:${state.features.chatVisible ? '#90EE90' : '#FF7F7F'}; text-align:right;">${state.features.chatVisible ? 'ON' : 'OFF'}</span> <span><b>Keybinds:</b></span> <span style="color:${state.features.keybindsEnabled ? '#90EE90' : '#FF7F7F'}; text-align:right;">${state.features.keybindsEnabled ? 'ON' : 'OFF'}</span> <span><b>Ping:</b></span> <span id="ping-value-simplified" style="color:#FFD700; text-align:right;">${state.ping} ms</span> </div> </div> <div style="text-align:center; font-size:12px; color:#888; margin-top:15px; padding-top:10px; border-top:1px solid #444; line-height:1.6;"> Press <strong>${state.keybinds.toggleMenu.toUpperCase()}</strong> to toggle menu </div> `; // Logic for simplified menu button setTimeout(() => { const btn = document.getElementById('default-menu-btn'); if (btn) { btn.onclick = () => { state.simplified = false; sessionStorage.setItem('modMenuSimplified', 'false'); state.features.performanceMode = parseInt(localStorage.getItem('prevPerformanceMode')) || 2; // Restore applyPerformanceMode(); updateMenu(); }; } // Update ping display in simplified menu const pingValueDisplay = document.getElementById("ping-value-simplified"); if (pingValueDisplay) pingValueDisplay.textContent = `${state.ping} ms`; }, 0); if (state.features.performanceMode !== 1) { state.features.performanceMode = 1; applyPerformanceMode(); } return; // End simplified menu update } // vvv PASTE THE COLOR-PICKING LOGIC HERE vvv let versionColor = '#FFD700'; // Yellow for 'Checking...' if (state.versionStatus === 'Current') { versionColor = '#90EE90'; // Green } else if (state.versionStatus.startsWith('Outdated')) { versionColor = '#FF7F7F'; // Red } else if (state.versionStatus === 'Unknown' || state.versionStatus === 'Check Failed') { versionColor = '#aaa'; // Gray for errors } // ^^^ END OF PASTED CODE ^^^ // Full Menu menu.style.width = state.uiLayout.menu.width !== null ? `${state.uiLayout.menu.width}px` : '480px'; // Wider full menu let menuHtml = ` <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:15px;"> <div> <span id="customization-toggle" style="cursor:pointer; user-select:none; color:${menuColor}; font-weight:bold; font-size:1.05em;"> ${arrow} Menu Customization </span> </div> <div style="display:flex; align-items:center; gap:8px;"> <button id="simplify-menu-btn" title="Simplify menu" style="${buttonStyle('#5a5a5e')}; padding: 6px 12px; font-size: 13px;" onmouseover="${buttonHoverStyle('#5a5a5e')}" onmouseout="${buttonLeaveStyle('#5a5a5e')}">Simplify</button> <button id="open-keybinds-menu-btn" style="${buttonStyle()}; padding: 6px 12px; font-size: 13px;" onmouseover="${buttonHoverStyle()}" onmouseout="${buttonLeaveStyle()}">Keybinds</button> </div> </div> <div id="customization-section" style="display:${state.showCustomization ? 'block' : 'none'}; background:${hexToRgba(menuColor,0.08)}; padding:15px; border-radius:8px; margin-bottom:20px; border: 1px solid ${hexToRgba(menuColor,0.2)};"> <div style="display:grid; grid-template-columns: 1fr auto; gap:10px; align-items:center; margin-bottom:12px;"> <input id="mod-menu-name-input" type="text" placeholder="Menu Name..." value="${state.menuName.replace(/"/g,'"')}" style="${inputStyle} width:100%;" onfocus="${focusStyle}" onblur="${blurStyle}"> <button id="mod-menu-name-btn" style="${buttonStyle(menuColor)}; padding: 8px 12px;" onmouseover="${buttonHoverStyle()}" onmouseout="${buttonLeaveStyle()}">Set Name</button> </div> <div style="display:flex; gap:15px; align-items:center; justify-content:start;"> <div style="display:flex; align-items:center; gap:5px;"> <label for="mod-menu-color-input" style="color:${menuColor}; font-size:14px; cursor:pointer;">Theme:</label> <input id="mod-menu-color-input" type="color" value="${state.menuColor}" style="width:28px; height:28px; border:none; outline:2px solid ${menuColor}; border-radius:5px; cursor:pointer; background:transparent;"> </div> <div style="display:flex; align-items:center; gap:5px;"> <label for="chat-name-color-input" style="color:${menuColor}; font-size:14px; cursor:pointer;">Chat Name:</label> <input id="chat-name-color-input" type="color" value="${localStorage.getItem("chatNameColor") || "#FFD700"}" style="width:28px; height:28px; border:none; outline:2px solid ${localStorage.getItem("chatNameColor") || "#FFD700"}; border-radius:5px; cursor:pointer; background:transparent;"> </div> </div> </div> `; menuHtml += ` <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom:20px"> <div> <h3 style="color:${menuColor}; border-bottom:1px solid ${hexToRgba(menuColor,0.3)}; padding-bottom:6px; margin-top:0; margin-bottom:12px; font-size:1.15em; font-weight:600;">Movement</h3> <p><strong>${state.keybinds.circleRestriction.toUpperCase()}: Circle Restrict:</strong> <span style="color:${state.features.circleRestriction ? '#90EE90' : '#FF7F7F'}; float:right;">${state.features.circleRestriction ? 'ON' : 'OFF'}</span></p> <p><strong>${state.keybinds.circleSmaller.toUpperCase()}/${state.keybinds.circleLarger.toUpperCase()}: Circle Size:</strong> <span style="float:right;">${state.circleRadius}px</span></p> <p><strong>${state.keybinds.autoCircle.toUpperCase()}: Bot Movement:</strong> <span style="color:${state.features.autoCircle ? '#90EE90' : '#FF7F7F'}; float:right;">${state.features.autoCircle ? 'ON' : 'OFF'}</span></p> <p><strong>${state.keybinds.autoBoost.toUpperCase()}: Auto Boost:</strong> <span style="color:${state.features.autoBoost ? '#90EE90' : '#FF7F7F'}; float:right;">${state.features.autoBoost ? 'ON' : 'OFF'}</span></p> <h3 style="color:${menuColor}; border-bottom:1px solid ${hexToRgba(menuColor,0.3)}; padding-bottom:6px; margin-top:20px; margin-bottom:12px; font-size:1.15em; font-weight:600;">Zoom</h3> <p><strong>${state.keybinds.zoomIn.toUpperCase()}: Zoom Out</strong></p> <p><strong>${state.keybinds.zoomOut.toUpperCase()}: Zoom In</strong></p> <p><strong>${state.keybinds.zoomReset.toUpperCase()}: Reset Zoom</strong></p> <h3 style="color:${menuColor}; border-bottom:1px solid ${hexToRgba(menuColor,0.3)}; padding-bottom:6px; margin-top:20px; margin-bottom:12px; font-size:1.15em; font-weight:600;">Utilities</h3> <p><strong>${(state.keybinds.autoRespawn || 'S').toUpperCase()}: Auto Respawn:</strong> <span style="color:${state.features.autoRespawn ? '#90EE90' : '#FF7F7F'}; float:right;">${state.features.autoRespawn ? 'ON' : 'OFF'}</span></p> <div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:8px;"> <span><strong>${(state.keybinds.neonLine || 'E').toUpperCase()}: Neon Line:</strong> <span style="color:${state.features.neonLine ? '#90EE90' : '#FF7F7F'};">${state.features.neonLine ? 'ON' : 'OFF'}</span></span> <input id="neon-line-color-input" type="color" value="${state.features.neonLineColor}" style="width:24px;height:24px;border:none;outline:2px solid ${state.features.neonLineColor};border-radius:4px;cursor:pointer;background:transparent;"> </div> <button id="help-info-btn" style="${buttonStyle('#4a4a4e')}; width:100%; margin-top:10px; padding: 7px 0;" onmouseover="${buttonHoverStyle('#4a4a4e')}" onmouseout="${buttonLeaveStyle('#4a4a4e')}"> ❔ Level Info </button> <button id="rep-leaderboard-btn" style="${buttonStyle('#4a4a4e')}; width:100%; margin-top:10px; padding: 7px 0;"onmouseover="${buttonHoverStyle('#4a4a4e')}" onmouseout="${buttonLeaveStyle('#4a4a4e')}">🏆 Rep Leaderboard</button> </div> <div> <h3 style="color:${menuColor}; border-bottom:1px solid ${hexToRgba(menuColor,0.3)}; padding-bottom:6px; margin-top:0; margin-bottom:12px; font-size:1.15em; font-weight:600;">Visuals & Audio</h3> <p><strong>1-3: Performance:</strong> <span style="color:${['#90EE90','#87CEEB','#FFA07A'][state.features.performanceMode-1] || '#aaa'}; float:right;">${['Low','Medium','High'][state.features.performanceMode-1] || 'N/A'}</span></p> <p><strong>${state.keybinds.fpsDisplay.toUpperCase()}: FPS Display:</strong> <span style="color:${state.features.fpsDisplay ? '#90EE90' : '#FF7F7F'}; float:right;">${state.features.fpsDisplay ? 'ON' : 'OFF'}</span></p> <p><strong>${state.keybinds.deathSound.toUpperCase()}: Death Sound:</strong> <span style="color:${state.features.deathSound ? '#90EE90' : '#FF7F7F'}; float:right;">${state.features.deathSound ? 'ON' : 'OFF'}</span></p> <p><strong>${state.keybinds.showServer.toUpperCase()}: Show Server IP:</strong> <span style="color:${state.features.showServer ? '#90EE90' : '#FF7F7F'}; float:right;">${state.features.showServer ? 'ON' : 'OFF'}</span></p> <div style="display:flex; align-items:center; justify-content:space-between; margin:10px 0;"> <button id="trail-toggle-btn" style="${buttonStyle('#4a4a4e')}; flex-grow:1; margin-right:10px;" onmouseover="${buttonHoverStyle('#4a4a4e')}" onmouseout="${buttonLeaveStyle('#4a4a4e')}"> Trail: <span style="color:${state.features.snakeTrail ? '#90EE90' : '#FF7F7F'};">${state.features.snakeTrail ? 'ON' : 'OFF'}</span> </button> <input id="trail-color-input" type="color" value="${state.features.snakeTrailColor}" style="width:28px;height:28px;border:none;outline:2px solid ${state.features.snakeTrailColor};border-radius:5px;cursor:pointer;background:transparent;"> </div> <button id="afk-btn" style="${buttonStyle('#4a4a4e')}; width:100%; margin-bottom:10px;" onmouseover="${buttonHoverStyle('#4a4a4e')}" onmouseout="${buttonLeaveStyle('#4a4a4e')}"> AFK Mode: <span id="afk-status" style="color:${afkOn ? '#90EE90' : '#FF7F7F'};">${afkOn ? 'ON' : 'OFF'}</span> </button> <!-- START OF NEW BUTTONS --> <div style="display:flex; gap:10px; margin-bottom:10px;"> <button id="ui-scale-down-btn" style="${buttonStyle('#4a4a4e')}; flex:1; padding:7px 0;" onmouseover="${buttonHoverStyle('#4a4a4e')}" onmouseout="${buttonLeaveStyle('#4a4a4e')}"> UI Scale - </button> <button id="ui-scale-up-btn" style="${buttonStyle('#4a4a4e')}; flex:1; padding:7px 0;" onmouseover="${buttonHoverStyle('#4a4a4e')}" onmouseout="${buttonLeaveStyle('#4a4a4e')}"> UI Scale + </button> </div> <!-- END OF NEW BUTTONS --> <h3 style="color:${menuColor}; border-bottom:1px solid ${hexToRgba(menuColor,0.3)}; padding-bottom:6px; margin-top:20px; margin-bottom:12px; font-size:1.15em; font-weight:600;">Links</h3> <p><strong>${state.keybinds.github.toUpperCase()}: Dev GitHub</strong> <span style="float:right; opacity:0.7;">🔗</span></p> <p><strong>${state.keybinds.discord.toUpperCase()}: 143X Discord</strong> <span style="float:right; opacity:0.7;">🔗</span></p> <p><strong>${state.keybinds.godMode.toUpperCase()}: GodMode</strong> <span style="float:right; opacity:0.7;">🔗</span></p> <p><strong>${(state.keybinds.reddit || 'R').toUpperCase()}: Slither Reddit</strong> <span style="float:right; opacity:0.7;">🔗</span></p> <p><strong>${state.keybinds.dreamwave.toUpperCase()}: DreamWave Extension</strong> <span style="float:right; opacity:0.7;">🔗</span></p> </div> </div> <div style="background:${hexToRgba(menuColor,0.08)}; padding:15px; border-radius:8px; margin-bottom:15px; border: 1px solid ${hexToRgba(menuColor,0.2)};"> <div style="display:grid; grid-template-columns:1fr 1fr; gap:15px;"> <div> <h3 style="color:${menuColor}; margin-top:0; margin-bottom:10px; font-size:1.1em; font-weight:600;">Status</h3> <p><strong>Game State:</strong> <span style="float:right;">${state.isInGame ? 'In Game' : 'Menu'}</span></p> <p><strong>Zoom:</strong> <span style="float:right;">${Math.round(100 / state.zoomFactor)}%</span></p> <p><strong>FPS:</strong> <span style="float:right;">${state.fps}</span></p> <p><strong>Keybinds:</strong> <span style="color:${state.features.keybindsEnabled ? '#90EE90' : '#FF7F7F'}; float:right;">${state.features.keybindsEnabled ? 'ON' : 'OFF'}</span></p> <p><strong>Version:</strong> <span style="color:${versionColor}; font-weight:bold; float:right;">${state.versionStatus}</span></p> </div> <div> <h3 style="color:${menuColor}; margin-top:0; margin-bottom:10px; font-size:1.1em; font-weight:600;">Extras</h3> <p><strong>Server:</strong> <span style="color:#FFD700; float:right;">${state.features.showServer ? (state.server || 'N/A') : 'Hidden'}</span></p> <div style="margin-top:8px;"> <button id="chat-toggle-btn" style="${buttonStyle('#4a4a4e')}; width:100%; margin-bottom:8px; text-align:left; padding-left:15px;" onmouseover="${buttonHoverStyle('#4a4a4e')}" onmouseout="${buttonLeaveStyle('#4a4a4e')}"> Chat: <span id="chat-toggle-status" style="color:${state.features.chatVisible ? '#90EE90' : '#FF7F7F'}; font-weight:bold; float:right; padding-right:5px;">${state.features.chatVisible ? 'ON' : 'OFF'}</span> </button> <button id="change-nickname-btn" style="${buttonStyle('#4a4a4e')}; width:100%; margin-bottom:8px;" onmouseover="${buttonHoverStyle('#4a4a4e')}" onmouseout="${buttonLeaveStyle('#4a4a4e')}">Change Nickname</button> <button id="donate-btn" style="${buttonStyle('#E6B43E', '#000')}; width:100%;" onmouseover="${buttonHoverStyle('#E6B43E')}" onmouseout="${buttonLeaveStyle('#E6B43E')}">💖 Donate</button> </div> </div> </div> </div> <div style="text-align:center; font-size:12px; color:#888; margin-top:15px; padding-top:10px; border-top:1px solid #444; line-height:1.7;"> <span style="color:#ff6b6b; font-weight:bold;">(Developers will NEVER ask for money in chat. Beware of Scammers.)</span><br> Press <strong>${state.keybinds.toggleMenu.toUpperCase()}</strong> to hide/show menu | <b>DSC.GG/143X</b> | <strong>${state.keybinds.screenshot.toUpperCase()}</strong> Screenshot<br> Made by: <b>dxxthly.</b> & <b>waynesg</b> on Discord </div> `; if (menuContentArea) menuContentArea.innerHTML = menuHtml; // Event listeners for newly created elements setTimeout(() => { // Customization Toggle const custToggle = document.getElementById('customization-toggle'); if (custToggle) { custToggle.onclick = () => { state.showCustomization = !state.showCustomization; sessionStorage.setItem('showCustomization', state.showCustomization.toString()); updateMenu(); }; } // Simplify Button const simplifyBtn = document.getElementById('simplify-menu-btn'); if (simplifyBtn) { simplifyBtn.onclick = () => { localStorage.setItem('prevPerformanceMode', state.features.performanceMode.toString()); state.simplified = true; state.features.performanceMode = 1; applyPerformanceMode(); sessionStorage.setItem('modMenuSimplified', 'true'); updateMenu(); }; } // Keybinds Button const keybindsBtn = document.getElementById('open-keybinds-menu-btn'); if (keybindsBtn) keybindsBtn.onclick = showKeybindsMenu; // Menu Name and Color Inputs const nameInput = document.getElementById('mod-menu-name-input'); const nameBtn = document.getElementById('mod-menu-name-btn'); const colorIn = document.getElementById('mod-menu-color-input'); if (nameBtn && nameInput) { nameBtn.onclick = () => { const val = nameInput.value.trim(); if (val.length > 0) { state.menuName = val; localStorage.setItem('modMenuName', val); updateMenu(); // Update menu title in draggable header syncServerBoxWithMenu(); // Update server box title } }; nameInput.onkeydown = (e) => { if (e.key === 'Enter') nameBtn.click(); }; } if (colorIn) { colorIn.oninput = () => { state.menuColor = colorIn.value; localStorage.setItem('modMenuColor', state.menuColor); updateCSSVariables(); // Update global CSS vars updateMenu(); // Re-style elements based on new color syncServerBoxWithMenu(); syncChatBoxWithMenu(); }; colorIn.style.outlineColor = state.menuColor; // Sync outline with current color } const chatNameColorIn = document.getElementById('chat-name-color-input'); if (chatNameColorIn) { chatNameColorIn.oninput = () => { localStorage.setItem('chatNameColor', chatNameColorIn.value); // No full updateMenu needed, but if there's a live preview, update it. // For now, color picker outline updates itself. chatNameColorIn.style.outlineColor = chatNameColorIn.value; }; chatNameColorIn.style.outlineColor = localStorage.getItem("chatNameColor") || "#FFD700"; } // Neon Line Color Input const neonLineColorInput = document.getElementById('neon-line-color-input'); if (neonLineColorInput) { neonLineColorInput.value = state.features.neonLineColor; neonLineColorInput.oninput = () => { state.features.neonLineColor = neonLineColorInput.value; neonLineColor = neonLineColorInput.value; // Assuming global var for drawing if (neonCtx) neonCtx.shadowColor = neonLineColor; neonLineColorInput.style.outlineColor = state.features.neonLineColor; }; neonLineColorInput.style.outlineColor = state.features.neonLineColor; } // AFK Button const afkBtnEl = document.getElementById('afk-btn'); if (afkBtnEl) afkBtnEl.onclick = () => setAfk(!afkOn); // setAfk updates its own status text // UI Scale Buttons const uiScaleDownBtn = document.getElementById('ui-scale-down-btn'); const uiScaleUpBtn = document.getElementById('ui-scale-up-btn'); const scaleStep = 0.05; if (uiScaleDownBtn) { uiScaleDownBtn.onclick = () => { state.uiScale = Math.max(0.6, state.uiScale - scaleStep); // Min 60% localStorage.setItem('modMenuUIScale', state.uiScale.toString()); applyUIScale(); }; } if (uiScaleUpBtn) { uiScaleUpBtn.onclick = () => { state.uiScale = Math.min(1.5, state.uiScale + scaleStep); // Max 150% localStorage.setItem('modMenuUIScale', state.uiScale.toString()); applyUIScale(); }; } // Toggle Background Button const helpInfoBtn = document.getElementById('help-info-btn'); if (helpInfoBtn) { helpInfoBtn.onclick = () => { const helpModal = document.getElementById('rep-help-modal'); if (helpModal) { helpModal.style.display = 'flex'; } }; } // Leaderboard for Rep const repLeaderboardBtn = document.getElementById('rep-leaderboard-btn'); if (repLeaderboardBtn) { // Block to Copy and Paste // --- PASTE THIS CORRECTED VERSION IN ITS PLACE --- // --- PASTE THIS NEW, CORRECTED VERSION --- repLeaderboardBtn.onclick = async () => { const modal = document.getElementById('rep-leaderboard-modal'); const content = document.getElementById('rep-leaderboard-content'); if (!modal || !content) return; modal.style.display = 'flex'; content.innerHTML = '<div style="color:#aaa;text-align:center;">Loading leaderboard...</div>'; try { // First, get the top 10 players by REP score. This is always correct. const repSnapshot = await firebase.database().ref('playerData').orderByChild('rep').limitToLast(10).once('value'); if (!repSnapshot.exists()) { content.innerHTML = '<div style="color:#aaa;text-align:center;">No players with REP found yet.</div>'; return; } const players = []; const userPromises = []; repSnapshot.forEach(child => { const playerData = child.val(); const uid = child.key; // For each top player, TRY to get their profile from the working `onlineUsers` list. const userPromise = firebase.database().ref(`onlineUsers/${uid}`).once('value') .then(userSnap => { let userProfile = {}; if (userSnap.exists()) { // If they are online, we have their info! userProfile = userSnap.val(); } else { // If they are OFFLINE, create a placeholder profile. THIS PREVENTS THE CRASH. userProfile = { name: 'Offline User', profileAvatar: null }; } players.push({ uid: uid, rep: playerData.rep || 0, name: userProfile.name || 'Offline User', avatar: userProfile.profileAvatar || `https://api.dicebear.com/7.x/identicon/svg?seed=${encodeURIComponent(uid)}` }); }); userPromises.push(userPromise); }); // Wait for all lookups to finish await Promise.all(userPromises); // Sort players by REP score, descending players.sort((a, b) => b.rep - a.rep); // --- NEW: Filter out devs and system accounts from the leaderboard --- const filteredPlayers = players.filter(player => !isDev(player.uid) && !isSystemAccount(player.uid) ); // --- End of new filter --- // Build and display the leaderboard HTML content.innerHTML = filteredPlayers.map((p, i) => { const rank = i + 1; let rankStyle = 'color: #ddd;'; let rowStyle = 'background: rgba(255, 255, 255, 0.04); border-left: 4px solid #555;'; if (rank === 1) { rankStyle = 'color: #FFD700; text-shadow: 0 0 5px #FFD700;'; rowStyle = 'background: linear-gradient(90deg, rgba(255,215,0,0.15) 0%, rgba(255,215,0,0) 60%); border-left: 4px solid #FFD700;'; } else if (rank === 2) { rankStyle = 'color: #C0C0C0; text-shadow: 0 0 5px #C0C0C0;'; rowStyle = 'background: linear-gradient(90deg, rgba(192,192,192,0.1) 0%, rgba(192,192,192,0) 60%); border-left: 4px solid #C0C0C0;'; } else if (rank === 3) { rankStyle = 'color: #CD7F32; text-shadow: 0 0 5px #CD7F32;'; rowStyle = 'background: linear-gradient(90deg, rgba(205,127,50,0.1) 0%, rgba(205,127,50,0) 60%); border-left: 4px solid #CD7F32;'; } let displayName = filterProfanity(p.name); if (p.name === 'Offline User') { displayName = `<i style="color:#999;">${displayName}</i>`; } if (isDev(p.uid)) { displayName = rainbowTextStyle(filterProfanity(p.name)); } else if (isVip(p.uid, p.name)) { displayName = vipGlowStyle(filterProfanity(p.name), '#FFD700'); } return `<div class="leaderboard-row leaderboard-clickable-row" data-uid="${p.uid}" style="${rowStyle}"> <div class="leaderboard-rank" style="${rankStyle}">#${rank}</div> <img class="leaderboard-avatar" src="${p.avatar}" onerror="this.src='https://api.dicebear.com/7.x/identicon/svg?seed=${encodeURIComponent(p.uid)}';"> <div class="leaderboard-info"> <div class="leaderboard-name">${displayName}</div> <div class="leaderboard-rep"><b style="color:var(--menu-color, #4CAF50); font-weight:900;">${p.rep.toLocaleString()}</b> REP</div> </div> </div>`; }).join(''); } catch (err) { console.error("Error loading rep leaderboard:", err); content.innerHTML = `<div style="color:#f77;text-align:center;">Error loading leaderboard. Please check console.</div>`; } }; } // Trail Toggle and Color const trailToggleBtn = document.getElementById('trail-toggle-btn'); if (trailToggleBtn) { trailToggleBtn.onclick = () => { state.features.snakeTrail = !state.features.snakeTrail; if (!state.features.snakeTrail) { state.snakeTrailPoints = []; clearTrailOverlay(); } updateMenu(); }; } const trailColorInput = document.getElementById('trail-color-input'); if (trailColorInput) { trailColorInput.oninput = () => { state.features.snakeTrailColor = trailColorInput.value; trailColorInput.style.outlineColor = state.features.snakeTrailColor; // No full updateMenu needed unless other elements depend on this color live. }; trailColorInput.style.outlineColor = state.features.snakeTrailColor; } // Chat Toggle Button (in main menu) const chatToggleBtn = document.getElementById('chat-toggle-btn'); if (chatToggleBtn) chatToggleBtn.onclick = toggleChatVisible; // Change Nickname Button const changeNickBtn = document.getElementById('change-nickname-btn'); if (changeNickBtn) { changeNickBtn.onclick = async () => { localStorage.removeItem("nickname"); // Clear old one await promptForUniqueNickname(); // Prompt for new // Consider re-initializing chat or parts of it if needed, or simply reload window.location.reload(); }; } // Donate Button const donateBtn = document.getElementById('donate-btn'); if (donateBtn) { donateBtn.onclick = () => window.open("https://www.paypal.com/donate/?business=SC3RFTW5QDZJ4&no_recurring=0¤cy_code=USD", "_blank", "toolbar=no,scrollbars=yes,resizable=yes,top=200,left=200,width=520,height=700"); } }, 0); // End setTimeout for event listeners syncServerBoxWithMenu(); syncChatBoxWithMenu(); updateCSSVariables(); // Ensure CSS variables are current } let lastWheelTime = 0; document.addEventListener('wheel', function(e) { const now = Date.now(); if (now - lastWheelTime < 100) return; lastWheelTime = now; if (!state.features.keybindsEnabled) return; const activeEl = document.activeElement; if (activeEl && (activeEl.tagName === 'INPUT' || activeEl.tagName === 'TEXTAREA' || activeEl.isContentEditable || state.features.chatFocus)) { return; } if (!state.isInGame) return; const binds = state.keybinds; let currentZoomIdx = zoomSteps.findIndex(z => Math.abs(z - state.zoomFactor) < 1e-5); if (currentZoomIdx === -1) { currentZoomIdx = zoomSteps.reduce((bestIdx, currentStep, idx) => Math.abs(currentStep - state.zoomFactor) < Math.abs(zoomSteps[bestIdx] - state.zoomFactor) ? idx : bestIdx, 0); } let actionTaken = false; if (e.deltaY < 0 && binds.zoomIn === "wheelup") { if (currentZoomIdx > 0) { state.zoomFactor = zoomSteps[--currentZoomIdx]; actionTaken = true; } } else if (e.deltaY > 0 && binds.zoomOut === "wheeldown") { if (currentZoomIdx < zoomSteps.length - 1) { state.zoomFactor = zoomSteps[++currentZoomIdx]; actionTaken = true; } } if (actionTaken) { if (typeof updateMenu === "function") updateMenu(); e.preventDefault(); } }, { passive: false }); function displayKey(key) { if (!key) return 'N/A'; if (key.toLowerCase() === " ") return "SPACE"; if (key.toLowerCase() === "wheelup") return "Wheel Up"; if (key.toLowerCase() === "wheeldown") return "Wheel Down"; return key.toUpperCase(); } // The IIFE for Keybind Modal Logic is already defined within the // `if (!document.getElementById('keybind-modal-overlay'))` block. // No need to repeat it here if you followed SECTION 4 instructions. // If you didn't, ensure the IIFE from SECTION 4 is correctly placed. function showKeybindsMenu() { const menuColor = state.menuColor; const menuContentArea = document.getElementById('mod-menu-content-area'); if (!menuContentArea) return; // Update draggable header for keybinds menu const menuDraggableHeader = document.getElementById('mod-menu-draggable-header'); if (menuDraggableHeader) { menuDraggableHeader.innerHTML = ` <h2 style="margin:0; color:#fff; font-size:1.4em; font-weight:600; text-shadow: 1px 1px 2px rgba(0,0,0,0.3);"> Keybind Settings </h2> <button id="back-to-main-menu-btn" style="${buttonStyle(menuColor)}; padding: 6px 12px; font-size: 13px;" onmouseover="${buttonHoverStyle(menuColor)}" onmouseout="${buttonLeaveStyle(menuColor)}"> Back </button> `; // Attach listener for the new back button setTimeout(() => { const backBtn = document.getElementById('back-to-main-menu-btn'); if (backBtn) backBtn.onclick = updateMenu; },0); } menuContentArea.innerHTML = ` <table style="width:100%; font-size:14px; margin-top:5px; border-collapse:collapse; background:rgba(0,0,0,0.1); border-radius:8px; overflow:hidden;"> <thead> <tr> <th style="text-align:left; color:${menuColor}; padding:10px 12px; border-bottom: 1px solid ${hexToRgba(menuColor, 0.4)}; font-weight:600;">Action</th> <th style="text-align:left; color:${menuColor}; padding:10px 12px; border-bottom: 1px solid ${hexToRgba(menuColor, 0.4)}; font-weight:600;">Key</th> <th style="text-align:right; padding:10px 12px; border-bottom: 1px solid ${hexToRgba(menuColor, 0.4)};"></th> </tr> </thead> <tbody> ${Object.entries(state.keybinds).map(([action, key], index, arr) => ` <tr style="${index === arr.length - 1 ? '' : 'border-bottom: 1px solid rgba(255,255,255,0.08);'}"> <td style="color:#ccc; padding:9px 12px; text-transform: capitalize;">${action.replace(/([A-Z])/g, ' $1')}</td> <td style="color:#FFD700; font-weight:bold; padding:9px 12px;">${displayKey(key)}</td> <td style="text-align:right; padding:9px 12px;"> <button data-action="${action}" class="set-keybind-btn" style="${buttonStyle(menuColor)};" onmouseover="${buttonHoverStyle(menuColor)}" onmouseout="${buttonLeaveStyle(menuColor)}">Set</button> </td> </tr> `).join('')} </tbody> </table> <div style="font-size:12px; color:#999; margin-top:15px; text-align:center;"> Click "Set" to rebind. Press <strong>${(state.keybinds.toggleKeybinds || '-').toUpperCase()}</strong> to toggle all mod keybinds. </div> `; setTimeout(() => { // Back button listener is set when header is created above document.querySelectorAll('.set-keybind-btn').forEach(btn => { btn.onclick = () => openKeybindModal(btn.dataset.action); }); }, 0); } function applyBackground() { const defaultBgUrl = 'https://slither.io/s2/bg54.jpg'; const blackBgDataUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='; window.__customBgUrlCurrent = state.features.blackBg ? blackBgDataUrl : defaultBgUrl; if (window.resize) { window.resize(); } } // === GAME STATE DETECTION === // === GAME STATE DETECTION === function checkGameState() { const gameCanvas = document.querySelector('canvas'); const loginForm = document.getElementById('login'); state.isInGame = !!(gameCanvas && gameCanvas.style.display !== 'none' && (!loginForm || loginForm.style.display === 'none')); setTimeout(checkGameState, 1000); } // === CIRCLE RESTRICTION VISUAL === // === CIRCLE RESTRICTION VISUAL === function drawCircleRestriction() { if (state.features.circleRestriction && state.isInGame) { const centerX = window.innerWidth / 2; const centerY = window.innerHeight / 2; // Ensure circleVisual is defined and accessible (it's created globally in your script) if (circleVisual) { circleVisual.style.left = `${centerX}px`; circleVisual.style.top = `${centerY}px`; circleVisual.style.width = `${state.circleRadius * 2}px`; circleVisual.style.height = `${state.circleRadius * 2}px`; circleVisual.style.display = 'block'; } } else { if (circleVisual) { circleVisual.style.display = 'none'; } } requestAnimationFrame(drawCircleRestriction); } // REMOVE the standalone drawCircleRestriction(); call from here if it exists. It will be called once at the end. document.addEventListener('keydown', function (e) { const activeEl = document.activeElement; // --- THIS IS THE FIX --- // Check if the user is currently focused on ANY input, textarea, // or if the keybind rebinding modal is active. if ( (activeEl && (activeEl.tagName === 'INPUT' || activeEl.tagName === 'TEXTAREA' || activeEl.isContentEditable)) || waitingForKeybind ) { // If they are typing, we do nothing and let the browser handle the key press. // We only make an exception for the 'Escape' key to allow blurring the input. if (e.key === 'Escape' && activeEl) { activeEl.blur(); } return; // This is the most important part: it stops the function right here. } // --- END OF FIX --- // If we get past the check above, it means the user is NOT typing in an input box, // so we can now safely process our mod's keybinds. // Handle arrow keys first (for AFK mode) if (e.key === 'ArrowLeft') window.l = true; if (e.key === 'ArrowRight') window.r = true; const key = e.key.toLowerCase() === " " ? "space" : e.key.toLowerCase(); const binds = state.keybinds; // Universal toggles (these should work even if other keybinds are off) if (key === binds.toggleMenu) { state.menuVisible = !state.menuVisible; menu.style.display = state.menuVisible ? 'block' : 'none'; if (state.menuVisible && typeof updateMenu === "function") updateMenu(); e.preventDefault(); return; } if (key === binds.toggleKeybinds) { state.features.keybindsEnabled = !state.features.keybindsEnabled; if (typeof updateMenu === "function") updateMenu(); e.preventDefault(); return; } if (!state.features.keybindsEnabled) return; if (key === binds.chatEnabled && state.features.chatVisible) { const chatInput = document.getElementById('mod-menu-chat-input'); if (chatInput) { chatInput.focus(); e.preventDefault(); } return; } let actionTaken = false; switch (key) { case '=': state.features.blackBg = !state.features.blackBg; // This toggles the switch ON/OFF applyBackground(); // This tells the game to update the background actionTaken = true; break; case binds.circleRestriction: state.features.circleRestriction = !state.features.circleRestriction; actionTaken = true; break; case binds.circleSmaller: state.circleRadius = Math.max(config.minCircleRadius, state.circleRadius - config.circleRadiusStep); actionTaken = true; break; case binds.circleLarger: state.circleRadius = Math.min(config.maxCircleRadius, state.circleRadius + config.circleRadiusStep); actionTaken = true; break; case binds.autoCircle: state.features.autoCircle = !state.features.autoCircle; if (state.features.autoCircle && !autoCircleRAF) { autoCircleRAF = requestAnimationFrame(autoCircle); } else if (autoCircleRAF) { cancelAnimationFrame(autoCircleRAF); autoCircleRAF = null; } if (typeof updateMenu === "function") updateMenu(); break; case binds.autoBoost: state.features.autoBoost = !state.features.autoBoost; if (typeof updateMenu === "function") updateMenu(); break; case binds.fpsDisplay: state.features.fpsDisplay = !state.features.fpsDisplay; if (fpsDisplay) fpsDisplay.style.display = state.features.fpsDisplay ? 'block' : 'none'; actionTaken = true; break; case binds.deathSound: state.features.deathSound = !state.features.deathSound; actionTaken = true; break; case binds.showServer: state.features.showServer = !state.features.showServer; actionTaken = true; break; case binds.neonLine: state.features.neonLine = !state.features.neonLine; if (state.features.neonLine) { neonLineActive = true; createNeonLineCanvas(); window.addEventListener('mousemove', neonLineDraw); } else { neonLineActive = false; if (neonCtx && neonCanvas) neonCtx.clearRect(0,0,neonCanvas.width, neonCanvas.height); window.removeEventListener('mousemove', neonLineDraw); } actionTaken = true; break; case binds.zoomIn: case binds.zoomOut: if (state.isInGame) { let idx = zoomSteps.findIndex(z => Math.abs(z - state.zoomFactor) < 1e-5); if (idx === -1) idx = zoomSteps.reduce((best, z_1, i) => Math.abs(z_1 - state.zoomFactor) < Math.abs(zoomSteps[best] - state.zoomFactor) ? i : best, 0); if (key === binds.zoomIn && idx > 0) idx--; else if (key === binds.zoomOut && idx < zoomSteps.length - 1) idx++; state.zoomFactor = zoomSteps[idx]; actionTaken = true; } break; case binds.zoomReset: if (state.isInGame) { state.zoomFactor = 1.0; actionTaken = true; } break; case binds.autoRespawn: state.features.autoRespawn = !state.features.autoRespawn; if (state.features.autoRespawn) enableAutoRespawn(); else disableAutoRespawn(); actionTaken = true; break; case binds.screenshot: if (state.isInGame) { try { const canvas = document.querySelector('canvas'); if (canvas) { const dataURL = canvas.toDataURL(); const link = document.createElement('a'); link.href = dataURL; link.download = `slither_screenshot_${Date.now()}.png`; document.body.appendChild(link); link.click(); document.body.removeChild(link); } } catch (err) { alert('Screenshot failed: ' + err); } } actionTaken = true; break; case binds.dreamwave: window.open("https://www.deathly.info", "_blank"); actionTaken = true; break; case binds.github: window.open('https://github.com/dxxthly', '_blank'); actionTaken = true; break; case binds.discord: window.open('https://dsc.gg/143x', '_blank'); actionTaken = true; break; case binds.godMode: window.open(config.godModeVideoURL, '_blank'); actionTaken = true; break; case binds.reddit: if (binds.reddit) window.open('https://www.reddit.com/r/Slitherio/', '_blank'); actionTaken = true; break; case '1': case '2': case '3': state.features.performanceMode = parseInt(key); applyPerformanceMode(); actionTaken = true; break; } if (actionTaken) { if (typeof updateMenu === "function") { updateMenu(); } e.preventDefault(); } }); document.addEventListener('keyup', function(e) { if (e.key === 'ArrowLeft') window.l = false; if (e.key === 'ArrowRight') window.r = false; }); // === FORCED SERVER LOGIC === function applyForcedServer() { try { const savedForcedServer = localStorage.getItem('forcedServer'); if (!savedForcedServer) return; const serverDetails = JSON.parse(savedForcedServer); if (!serverDetails.ip || !serverDetails.port) { localStorage.removeItem('forcedServer'); return; } window.forcing = true; if (!window.bso) window.bso = {}; window.bso.ip = serverDetails.ip; window.bso.po = parseInt(serverDetails.port, 10); } catch (e) { console.error("Error applying forced server:", e); localStorage.removeItem('forcedServer'); } } function patchPlayButtons() { const mainPlayBtn = document.getElementById('playh') || document.querySelector('.btn-play-guest') || document.querySelector('form .btn.btn-primary'); if (mainPlayBtn && !mainPlayBtn._patchedForceServer) { mainPlayBtn._patchedForceServer = true; mainPlayBtn.addEventListener('click', () => { setTimeout(applyForcedServer, 0); }, true); } document.querySelectorAll('.btn-play-again, #play-again, .play_btn').forEach(playAgainBtn => { if (playAgainBtn && !playAgainBtn._patchedForceServer) { playAgainBtn._patchedForceServer = true; playAgainBtn.addEventListener('click', () => { setTimeout(applyForcedServer, 0); }, true); } }); } // These calls below this function are important for its operation: setInterval(patchPlayButtons, 1000); applyForcedServer(); // Apply on load // === AUTO CIRCLE (Bot Movement) === // autoCircleRAF is already declared globally // autoCircleRAF is declared in the global-like scope of your IIFE // === AUTO CIRCLE === function autoCircle() { if (!state.features.autoCircle || !state.isInGame) { // Check isInGame from your state if (autoCircleRAF) { // Ensure autoCircleRAF is declared in the script's scope cancelAnimationFrame(autoCircleRAF); autoCircleRAF = null; } // If the feature is ON in UI but conditions not met, update UI if (state.features.autoCircle) { state.features.autoCircle = false; // Correct the state if (typeof updateMenu === "function") updateMenu(); } return; } try { state.autoCircleAngle += 0.025; // Your original speed const centerX = window.innerWidth / 2; const centerY = window.innerHeight / 2; const radius = Math.min(Math.max(state.circleRadius, 80), 180); // Your original radius logic const moveX = centerX + Math.cos(state.autoCircleAngle) * radius; const moveY = centerY + Math.sin(state.autoCircleAngle) * radius; const canvas = document.querySelector('canvas'); if (canvas) { const event = new MouseEvent('mousemove', { clientX: moveX, clientY: moveY, bubbles: true }); canvas.dispatchEvent(event); } } catch (err) { console.error("Auto Circle error:", err); // Good to keep error logging } if (state.features.autoCircle) { // Keep requesting frame if feature is still on autoCircleRAF = requestAnimationFrame(autoCircle); } else { // Explicitly clear if toggled off elsewhere if (autoCircleRAF) { cancelAnimationFrame(autoCircleRAF); autoCircleRAF = null; } } } // === SNAKE TRAIL DRAWING === function drawSnakeTrail() { if (!state.features.snakeTrail || !state.snakeTrailPoints || !state.snakeTrailPoints.length) { // Removed isInGame check here, trail can be drawn if points exist if (typeof clearTrailOverlay === "function") clearTrailOverlay(); // Call your clear function return; } const overlay = createTrailOverlayCanvas(); // Use your (now updated) createTrailOverlayCanvas if (!overlay) return; const ctx = overlay.getContext('2d'); ctx.clearRect(0, 0, overlay.width, overlay.height); const TRAIL_MAX_AGE = 1500; const now = Date.now(); const viewX = window.snake ? window.snake.xx || 0 : 0; const viewY = window.snake ? window.snake.yy || 0 : 0; const viewZoom = window.gsc || 1; // Use overlay center if trail is aligned to game canvas, else window center const screenCenterX = overlay.width / 2; const screenCenterY = overlay.height / 2; ctx.save(); ctx.lineJoin = 'round'; ctx.lineCap = 'round'; ctx.lineWidth = 8; // Your original lineWidth ctx.shadowBlur = 12; // Your original shadowBlur ctx.shadowColor = state.features.snakeTrailColor; for (let i = 1; i < state.snakeTrailPoints.length; i++) { const p1 = state.snakeTrailPoints[i-1]; const p2 = state.snakeTrailPoints[i]; const age = now - ((p1.time + p2.time) / 2); const alpha = Math.max(0, 1 - age / TRAIL_MAX_AGE); if (alpha <= 0) continue; const deltaX1 = p1.x - viewX; const deltaY1 = p1.y - viewY; const screenX1 = screenCenterX + deltaX1 * viewZoom; const screenY1 = screenCenterY + deltaY1 * viewZoom; const deltaX2 = p2.x - viewX; const deltaY2 = p2.y - viewY; const screenX2 = screenCenterX + deltaX2 * viewZoom; const screenY2 = screenCenterY + deltaY2 * viewZoom; ctx.strokeStyle = hexToRgba(state.features.snakeTrailColor, alpha * 0.7); ctx.beginPath(); ctx.moveTo(screenX1, screenY1); ctx.lineTo(screenX2, screenY2); ctx.stroke(); } ctx.restore(); } // === AUTO BOOST === function autoBoost() { if (!state.features.autoBoost || !state.isInGame) { if (state.boosting) { state.boosting = false; if (typeof window.setAcceleration === 'function') window.setAcceleration(0); document.dispatchEvent(new KeyboardEvent('keyup', { key: ' ' })); } return; } if (!state.boosting) { state.boosting = true; if (typeof window.setAcceleration === 'function') window.setAcceleration(1); document.dispatchEvent(new KeyboardEvent('keydown', { key: ' ' })); } } function autoBoostLoop() { autoBoost(); setTimeout(autoBoostLoop, 100); } autoBoostLoop(); function fpsCounter() { state.fpsFrames++; const now = Date.now(); if (now - state.fpsLastCheck >= 1000) { state.fps = state.fpsFrames; state.fpsFrames = 0; state.fpsLastCheck = now; if (state.features.fpsDisplay && fpsDisplay) { // Check if fpsDisplay exists fpsDisplay.textContent = `FPS: ${state.fps}`; } // Update ping in simplified menu status if visible if (state.simplified && state.menuVisible) { const pingValueDisplay = document.getElementById("ping-value-simplified"); if (pingValueDisplay) pingValueDisplay.textContent = `${state.ping} ms`; } } requestAnimationFrame(fpsCounter); } fpsCounter(); function deathSoundObserver() { /* ... unchanged ... */ } state.deathSound.preload = 'auto'; state.deathSound.load(); state.deathSound.addEventListener('ended', () => { state.deathSound.currentTime = 0; }); deathSoundObserver(); function applyPerformanceMode() { if (typeof window !== "undefined") { switch (state.features.performanceMode) { case 1: window.want_quality = 0; window.high_quality = false; window.render_mode = 1; break; case 2: window.want_quality = 1; window.high_quality = false; window.render_mode = 2; break; case 3: window.want_quality = 2; window.high_quality = true; window.render_mode = 2; break; } } updateMenu(); // Update menu to reflect change } applyPerformanceMode(); // Initial call // === ZOOM LOCK === // === ZOOM LOCK === function zoomLockLoop() { if (typeof window.gsc !== 'undefined' && state.isInGame) { // Check isInGame if (Math.abs(window.gsc - state.zoomFactor) > 0.001) { // Avoid tiny floating point updates window.gsc = state.zoomFactor; } } requestAnimationFrame(zoomLockLoop); } // REMOVE the standalone zoomLockLoop(); call from here. It will be called once at the end. function pingLoop() { // Simplified ping display, mainly for simplified menu status let currentPing = 0; if (window.lagging && typeof window.lagging === "number") currentPing = Math.round(window.lagging); else if (window.lag && typeof window.lag === "number") currentPing = Math.round(window.lag); state.ping = currentPing; // Ping display element outside menu is removed, rely on status in menu // If you want it back, recreate it and update here: // const pingDisplayEl = document.getElementById('ping-display'); // if (pingDisplayEl) pingDisplayEl.textContent = `Ping: ${currentPing} ms`; // This is now handled in fpsCounter to reduce DOM updates // if (state.simplified && state.menuVisible) { // const pingValueDisplay = document.getElementById("ping-value-simplified"); // if (pingValueDisplay) pingValueDisplay.textContent = `${currentPing} ms`; // } setTimeout(pingLoop, 500); } pingLoop(); function clearTrailOverlay() { const overlay = document.getElementById('snake-trail-overlay'); if (overlay) { const ctx = overlay.getContext('2d'); ctx.clearRect(0, 0, overlay.width, overlay.height); overlay.style.display = 'none'; // <--- Hide the overlay when trail is off } } menu.style.display = state.menuVisible ? 'block' : 'none'; if (fpsDisplay) fpsDisplay.style.display = state.features.fpsDisplay ? 'block' : 'none'; // Check fpsDisplay existence if (circleVisual) circleVisual.style.border = `2px dashed ${hexToRgba(state.menuColor, 0.7)}`; // Check existence function snakeTrailAnimationLoop() { requestAnimationFrame(snakeTrailAnimationLoop); drawSnakeTrail(); } setInterval(() => { if (!state.features.snakeTrail) { state.snakeTrailPoints = []; return; } // Get mouse screen position const mouseX = realMouseX; const mouseY = realMouseY; // Convert screen position to world (game) coordinates const viewX = window.snake ? window.snake.xx || 0 : 0; const viewY = window.snake ? window.snake.yy || 0 : 0; const viewZoom = window.gsc || 1; const screenCenterX = window.innerWidth / 2; const screenCenterY = window.innerHeight / 2; // This formula converts screen (mouse) to world coordinates const worldX = viewX + (mouseX - screenCenterX) / viewZoom; const worldY = viewY + (mouseY - screenCenterY) / viewZoom; if ( state.snakeTrailPoints.length === 0 || Math.abs(state.snakeTrailPoints[state.snakeTrailPoints.length-1].x - worldX) > 1 || Math.abs(state.snakeTrailPoints[state.snakeTrailPoints.length-1].y - worldY) > 1 ) { state.snakeTrailPoints.push({ x: worldX, y: worldY, time: Date.now() }); // Limit trail length if (state.snakeTrailPoints.length > 100) state.snakeTrailPoints.shift(); } }, 30); // START OF CODE TO PASTE // PASTE THIS NEW CODE BLOCK IN ITS PLACE: // PASTE THIS FINAL, CORRECTED BLOCK async function showUserProfile(uid) { if (!uid) return; // Close any existing popups first document.getElementById('profile-popup')?.remove(); let avatarUrl = `https://api.dicebear.com/7.x/identicon/svg?seed=${encodeURIComponent(uid)}`, motto = '<i>No motto set.</i>', isOnline = false, userChatColor = '#FFD700', username = 'Anon', playerRep = 0, unlockedBadges = {}; try { const userSnap = await firebase.database().ref("onlineUsers/" + uid).once('value'); if (userSnap.exists()) { const userData = userSnap.val(); username = filterProfanity(userData.name || 'Anon'); if (userData.profileAvatar) avatarUrl = userData.profileAvatar; if (userData.profileMotto) motto = filterProfanity(userData.profileMotto); if (!motto.trim() || motto === '<i></i>') motto = '<i>No motto set.</i>'; isOnline = (Date.now() - (userData.lastActive || 0) < 300000); userChatColor = userData.chatNameColor || '#FFD700'; } const playerDataSnap = await firebase.database().ref("playerData/" + uid).once('value'); if (playerDataSnap.exists()) { const pData = playerDataSnap.val(); playerRep = pData.rep || 0; unlockedBadges = pData.badges || {}; } } catch (err) { console.error(`Failed to fetch profile for UID ${uid}:`, err); } const popup = document.createElement('div'); popup.className = 'profile-popup'; popup.id = 'profile-popup'; popup.dataset.targetUid = uid; popup.dataset.targetName = username; popup.style.setProperty('--menu-color', state.menuColor); let highestRank = { name: 'Unranked', icon: '🌱', level: 0 }; if (config.repMilestones) { for (const level in config.repMilestones) { const repNeeded = parseInt(level); if (playerRep >= repNeeded && repNeeded >= highestRank.level) { highestRank = { ...config.repMilestones[level], level: repNeeded }; } } } const sortedBadgeKeys = Object.keys(unlockedBadges).sort((a, b) => parseInt(a) - parseInt(b)); const badgesHTML = `<div style="margin-top: 5px; margin-bottom: 8px; display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; max-width: 220px;"> ${sortedBadgeKeys.map(level => { const badge = unlockedBadges[level]; return `<span title="${badge.name} (Unlocked at ${level} REP)" style="font-size: 1.5em; cursor: help;">${badge.icon}</span>`; }).join('')} </div>`; let nameDisplay, devTagHTML = '', selfEditButtonHTML = '', adminButtonsHTML = '', giveRepButtonHTML = ''; const isDeveloper = isDev(uid); const currentUser = firebase.auth()?.currentUser; if (isDeveloper) { nameDisplay = rainbowTextStyle(username); devTagHTML = `<span style="background: #e91e63; color: #fff; padding: 2px 7px; border-radius: 4px; font-size: 0.8em; margin-left: 8px; font-weight: 700; vertical-align:middle;">DEV</span>`; } else if (isVip(uid, username)) { nameDisplay = vipGlowStyle(username, userChatColor); } else { nameDisplay = username; } if (currentUser && uid === currentUser.uid) { selfEditButtonHTML = `<button id="profile-edit-btn" title="Edit Your Profile" style="position: absolute; top: 10px; left: 10px; background: rgba(0,0,0,0.3); border: 1px solid #888; color: #fff; width: 30px; height: 30px; border-radius: 50%; font-size: 16px; cursor: pointer; line-height: 1; display: flex; align-items: center; justify-content: center;">✎</button>`; } if (currentUser && isDev(currentUser.uid) && uid !== currentUser.uid) { adminButtonsHTML = `<div style="margin-top:10px; display:flex; gap: 8px;"><button id="admin-edit-profile-btn" class="profile-popup_action_button" style="background-color:${state.menuColor};">🛠️ Edit Profile</button><button id="timeout-chat-btn" class="profile-popup_action_button" style="background-color:#c9302c;">⏰ Timeout</button></div>`; } if (currentUser && uid !== currentUser.uid) { giveRepButtonHTML = `<div style="display: flex; gap: 10px; align-items: center; margin: 15px 0 0 0; width: 90%;"><input id="rep-amount-input" type="number" min="1" placeholder="Amt" style="width: 70px; padding: 8px; border-radius: 5px; border: 1px solid #555; background: #222; color: #eee; text-align: center; font-size: 1em;"><button id="give-rep-btn" data-target-uid="${uid}" style="padding:8px 15px; border-radius:6px; border:none; color:#fff; font-size:14px; font-weight:500; cursor:pointer; background-color:#3F51B5; flex-grow: 1;">👍 Give REP</button></div>`; } const defaultAvatar = `https://api.dicebear.com/7.x/identicon/svg?seed=${encodeURIComponent(uid)}`; // --- THIS IS THE PART WITH THE CORRECTED LAYOUT --- popup.innerHTML = ` ${selfEditButtonHTML} <button class="close-btn" title="Close" onclick="this.parentElement.remove();">×</button> <img class="avatar" src="${avatarUrl}" alt="Avatar" style="border-color:${userChatColor};" onerror="this.src='${defaultAvatar}';"> <div style="font-size:1.23em;font-weight:bold;margin-bottom:2px;">${nameDisplay} ${devTagHTML}</div> <div title="${highestRank.name} - Unlocked at ${highestRank.level} REP" style="color: #ccc; cursor: help; margin-bottom: 8px;"> <span style="font-size: 1.2em; vertical-align: middle;">${highestRank.icon}</span> ${highestRank.name} </div> <div style="margin-bottom:10px;"><span class="status-dot" style="background:${isOnline ? '#0f0':'#888'}"></span><span style="font-size:1.04em;">${isOnline ? 'Online':'Offline'}</span></div> ${badgesHTML} <div style="width: 90%; text-align: left; margin-bottom: 15px;"> <div style="font-size: 0.9em; color: #ccc; margin-bottom: 4px; display: flex; justify-content: space-between;"> <span>REP Level: ${Math.floor(playerRep/100)}</span><span style="font-weight: bold;">${playerRep.toLocaleString()}</span> </div> <div style="background: #222; border-radius: 5px; height: 12px; border: 1px solid #444; padding: 1px;"> <div style="width: ${playerRep%100}%; height: 100%; background: linear-gradient(to right, #4CAF50, #8BC34A); border-radius: 3px;"></div> </div> </div> <div style="margin:8px 0 0 0; color:#ccc; font-style: italic; background: rgba(0,0,0,0.2); padding: 8px 12px; border-radius: 6px; text-align: center; word-break: break-word;"> ${motto} </div> ${giveRepButtonHTML} ${adminButtonsHTML} <div style="margin-top: 15px; font-size: 0.8em; color: #aaa; font-family: 'Courier New', monospace; word-break: break-all;"> UID: <span style="color: #fff; user-select: text; cursor: text;">${uid}</span> </div> `; document.body.appendChild(popup); } // =================================================================== // === START: FINAL ADMIN PROFILE/MODERATION CLICK HANDLING SYSTEM === // =================================================================== // This function handles opening the "Edit Profile" modal FOR YOURSELF function openEditProfileModal() { document.getElementById('profile-avatar-input').value = localStorage.getItem("profileAvatar") || ''; document.getElementById('profile-motto-input').value = localStorage.getItem("profileMotto") || ''; const editModal = document.getElementById('edit-profile-modal-overlay'); if (editModal) { editModal.style.display = 'flex'; } const statusEl = document.getElementById('profile-modal-status'); if (statusEl) { statusEl.textContent = ''; } } // =================================================================== // === START: COMBINED PROFILE & MODERATION SYSTEM (FINAL FIX) ===== // =================================================================== // This function handles opening the "Edit Profile" modal FOR YOURSELF. function openEditProfileModal() { const editModal = document.getElementById('edit-profile-modal-overlay'); document.getElementById('profile-avatar-input').value = localStorage.getItem("profileAvatar") || ''; document.getElementById('profile-motto-input').value = localStorage.getItem("profileMotto") || ''; // IMPORTANT: Clear any leftover target UID from a previous admin edit delete editModal.dataset.targetUid; if (editModal) { editModal.style.display = 'flex'; } const statusEl = document.getElementById('profile-modal-status'); if (statusEl) { statusEl.textContent = ''; } } // This function handles saving profile data for YOURSELF or for OTHERS (if you're an admin). async function saveProfile() { const statusEl = document.getElementById('profile-modal-status'); const saveButton = document.getElementById('profile-modal-save'); const editModal = document.getElementById('edit-profile-modal-overlay'); const targetUid = editModal.dataset.targetUid; const currentUser = firebase.auth().currentUser; const uidToSave = targetUid || currentUser?.uid; if (!uidToSave) { if (statusEl) statusEl.textContent = 'Error: No user to save for!'; return; } if (saveButton) saveButton.disabled = true; if (statusEl) statusEl.textContent = 'Saving...'; const newAvatarUrl = document.getElementById('profile-avatar-input').value.trim(); const newMotto = document.getElementById('profile-motto-input').value.trim(); try { const userRef = firebase.database().ref(`onlineUsers/${uidToSave}`); await userRef.update({ profileAvatar: newAvatarUrl, profileMotto: newMotto }); // Update localStorage only if it's your own profile if (uidToSave === currentUser?.uid) { localStorage.setItem("profileAvatar", newAvatarUrl); localStorage.setItem("profileMotto", newMotto); } if (statusEl) statusEl.textContent = 'Saved Successfully!'; setTimeout(() => { editModal.style.display = 'none'; delete editModal.dataset.targetUid; document.getElementById('profile-popup')?.remove(); }, 1200); } catch (error) { console.error("Error saving profile:", error); if (statusEl) statusEl.textContent = 'Error: Could not save to cloud.'; } finally { if (saveButton) saveButton.disabled = false; } } // =================================================================== // === START: COMBINED PROFILE & MODERATION SYSTEM (FINAL FIX) ===== // =================================================================== // This function handles opening the "Edit Profile" modal FOR YOURSELF. function openEditProfileModal() { const editModal = document.getElementById('edit-profile-modal-overlay'); document.getElementById('profile-avatar-input').value = localStorage.getItem("profileAvatar") || ''; document.getElementById('profile-motto-input').value = localStorage.getItem("profileMotto") || ''; // IMPORTANT: Clear any leftover target UID from a previous admin edit delete editModal.dataset.targetUid; if (editModal) { editModal.style.display = 'flex'; } const statusEl = document.getElementById('profile-modal-status'); if (statusEl) { statusEl.textContent = ''; } } // This function handles saving profile data for YOURSELF or for OTHERS (if you're an admin). async function saveProfile() { const statusEl = document.getElementById('profile-modal-status'); const saveButton = document.getElementById('profile-modal-save'); const editModal = document.getElementById('edit-profile-modal-overlay'); const targetUid = editModal.dataset.targetUid; const currentUser = firebase.auth().currentUser; const uidToSave = targetUid || currentUser?.uid; if (!uidToSave) { if (statusEl) statusEl.textContent = 'Error: No user to save for!'; return; } if (saveButton) saveButton.disabled = true; if (statusEl) statusEl.textContent = 'Saving...'; const newAvatarUrl = document.getElementById('profile-avatar-input').value.trim(); const newMotto = document.getElementById('profile-motto-input').value.trim(); try { const userRef = firebase.database().ref(`onlineUsers/${uidToSave}`); await userRef.update({ profileAvatar: newAvatarUrl, profileMotto: newMotto }); // Update localStorage only if it's your own profile if (uidToSave === currentUser?.uid) { localStorage.setItem("profileAvatar", newAvatarUrl); localStorage.setItem("profileMotto", newMotto); } if (statusEl) statusEl.textContent = 'Saved Successfully!'; setTimeout(() => { editModal.style.display = 'none'; delete editModal.dataset.targetUid; document.getElementById('profile-popup')?.remove(); }, 1200); } catch (error) { console.error("Error saving profile:", error); if (statusEl) statusEl.textContent = 'Error: Could not save to cloud.'; } finally { if (saveButton) saveButton.disabled = false; } } // --- NEW, DEDICATED FUNCTION TO SHOW ANY USER'S PROFILE --- async function showUserProfile(uid) { if (!uid) return; // Close any existing popups first document.getElementById('profile-popup')?.remove(); let avatarUrl = `https://api.dicebear.com/7.x/identicon/svg?seed=${encodeURIComponent(uid)}`, motto = '<i>No motto set.</i>', isOnline = false, userChatColor = '#FFD700', username = 'Anon', playerRep = 0, unlockedBadges = {}; try { const userSnap = await firebase.database().ref("onlineUsers/" + uid).once('value'); if (userSnap.exists()) { const userData = userSnap.val(); username = filterProfanity(userData.name || 'Anon'); if (userData.profileAvatar) avatarUrl = userData.profileAvatar; if (userData.profileMotto) motto = filterProfanity(userData.profileMotto); if (!motto.trim() || motto === '<i></i>') motto = '<i>No motto set.</i>'; isOnline = (Date.now() - (userData.lastActive || 0) < 300000); userChatColor = userData.chatNameColor || '#FFD700'; } const playerDataSnap = await firebase.database().ref("playerData/" + uid).once('value'); if (playerDataSnap.exists()) { const pData = playerDataSnap.val(); playerRep = pData.rep || 0; unlockedBadges = pData.badges || {}; } } catch (err) { console.error(`Failed to fetch profile for UID ${uid}:`, err); } const popup = document.createElement('div'); popup.className = 'profile-popup'; popup.id = 'profile-popup'; popup.dataset.targetUid = uid; popup.dataset.targetName = username; popup.style.setProperty('--menu-color', state.menuColor); let highestRank = { name: 'Unranked', icon: '🌱', level: 0 }; if (config.repMilestones) { for (const level in config.repMilestones) { const repNeeded = parseInt(level); if (playerRep >= repNeeded && repNeeded >= highestRank.level) { highestRank = { ...config.repMilestones[level], level: repNeeded }; } } } const sortedBadgeKeys = Object.keys(unlockedBadges).sort((a, b) => parseInt(a) - parseInt(b)); const badgesHTML = `<div style="margin-top: 5px; margin-bottom: 8px; display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; max-width: 220px;"> ${sortedBadgeKeys.map(level => { const badge = unlockedBadges[level]; return `<span title="${badge.name} (Unlocked at ${level} REP)" style="font-size: 1.5em; cursor: help;">${badge.icon}</span>`; }).join('')} </div>`; let nameDisplay, devTagHTML = '', selfEditButtonHTML = '', adminButtonsHTML = '', giveRepButtonHTML = ''; const isDeveloper = isDev(uid); const currentUser = firebase.auth()?.currentUser; if (isDeveloper) { nameDisplay = rainbowTextStyle(username); devTagHTML = `<span style="background: #e91e63; color: #fff; padding: 2px 7px; border-radius: 4px; font-size: 0.8em; margin-left: 8px; font-weight: 700; vertical-align:middle;">DEV</span>`; } else if (isVip(uid, username)) { nameDisplay = vipGlowStyle(username, userChatColor); } else { nameDisplay = username; } if (currentUser && uid === currentUser.uid) { selfEditButtonHTML = `<button id="profile-edit-btn" title="Edit Your Profile" style="position: absolute; top: 10px; left: 10px; background: rgba(0,0,0,0.3); border: 1px solid #888; color: #fff; width: 30px; height: 30px; border-radius: 50%; font-size: 16px; cursor: pointer; line-height: 1; display: flex; align-items: center; justify-content: center;">✎</button>`; } if (currentUser && isDev(currentUser.uid) && uid !== currentUser.uid) { adminButtonsHTML = `<div style="margin-top:10px; display:flex; gap: 8px;"><button id="admin-edit-profile-btn" class="profile-popup_action_button" style="background-color:${state.menuColor};">🛠️ Edit Profile</button><button id="timeout-chat-btn" class="profile-popup_action_button" style="background-color:#c9302c;">⏰ Timeout</button></div>`; } if (currentUser && uid !== currentUser.uid) { giveRepButtonHTML = `<div style="display: flex; gap: 10px; align-items: center; margin: 15px 0 0 0; width: 90%;"><input id="rep-amount-input" type="number" min="1" placeholder="Amt" style="width: 70px; padding: 8px; border-radius: 5px; border: 1px solid #555; background: #222; color: #eee; text-align: center; font-size: 1em;"><button id="give-rep-btn" data-target-uid="${uid}" style="padding:8px 15px; border-radius:6px; border:none; color:#fff; font-size:14px; font-weight:500; cursor:pointer; background-color:#3F51B5; flex-grow: 1;">👍 Give REP</button></div>`; } const defaultAvatar = `https://api.dicebear.com/7.x/identicon/svg?seed=${encodeURIComponent(uid)}`; // --- THIS IS THE PART WITH THE CORRECTED LAYOUT --- popup.innerHTML = ` ${selfEditButtonHTML} <button class="close-btn" title="Close" onclick="this.parentElement.remove();">×</button> <img class="avatar" src="${avatarUrl}" alt="Avatar" style="border-color:${userChatColor};" onerror="this.src='${defaultAvatar}';"> <div style="font-size:1.23em;font-weight:bold;margin-bottom:2px;">${nameDisplay} ${devTagHTML}</div> <div title="${highestRank.name} - Unlocked at ${highestRank.level} REP" style="color: #ccc; cursor: help; margin-bottom: 8px;"> <span style="font-size: 1.2em; vertical-align: middle;">${highestRank.icon}</span> ${highestRank.name} </div> <div style="margin-bottom:10px;"><span class="status-dot" style="background:${isOnline ? '#0f0':'#888'}"></span><span style="font-size:1.04em;">${isOnline ? 'Online':'Offline'}</span></div> ${badgesHTML} <div style="width: 90%; text-align: left; margin-bottom: 15px;"> <div style="font-size: 0.9em; color: #ccc; margin-bottom: 4px; display: flex; justify-content: space-between;"> <span>REP Level: ${Math.floor(playerRep/100)}</span><span style="font-weight: bold;">${playerRep.toLocaleString()}</span> </div> <div style="background: #222; border-radius: 5px; height: 12px; border: 1px solid #444; padding: 1px;"> <div style="width: ${playerRep%100}%; height: 100%; background: linear-gradient(to right, #4CAF50, #8BC34A); border-radius: 3px;"></div> </div> </div> <div style="margin:8px 0 0 0; color:#ccc; font-style: italic; background: rgba(0,0,0,0.2); padding: 8px 12px; border-radius: 6px; text-align: center; word-break: break-word;"> ${motto} </div> ${giveRepButtonHTML} ${adminButtonsHTML} <div style="margin-top: 15px; font-size: 0.8em; color: #aaa; font-family: 'Courier New', monospace; word-break: break-all;"> UID: <span style="color: #fff; user-select: text; cursor: text;">${uid}</span> </div> `; document.body.appendChild(popup); } document.addEventListener('click', async function(e) { // --- Button Handlers (Order is important!) --- if (e.target.id === 'profile-modal-save') { saveProfile(); return; } if (e.target.id === 'admin-edit-profile-btn') { const profilePopup = e.target.closest('.profile-popup'); const editModal = document.getElementById('edit-profile-modal-overlay'); const currentUser = firebase.auth()?.currentUser; if (!profilePopup || !editModal || !currentUser || !isDev(currentUser.uid)) return; const targetUid = profilePopup.dataset.targetUid; const userSnap = await firebase.database().ref("onlineUsers/" + targetUid).once('value'); const userData = userSnap.val() || {}; document.getElementById('profile-avatar-input').value = userData.profileAvatar || ''; document.getElementById('profile-motto-input').value = userData.profileMotto || ''; editModal.dataset.targetUid = targetUid; editModal.style.display = 'flex'; return; } if (e.target.id === 'timeout-chat-btn') { const profilePopup = e.target.closest('.profile-popup'); const timeoutModal = document.getElementById('timeout-modal-overlay'); const currentUser = firebase.auth()?.currentUser; if (!profilePopup || !timeoutModal || !currentUser || !isDev(currentUser.uid)) return; timeoutModal.dataset.targetUid = profilePopup.dataset.targetUid; timeoutModal.dataset.targetName = profilePopup.dataset.targetName; timeoutModal.style.display = 'flex'; return; } if (e.target.id === 'profile-edit-btn') { openEditProfileModal(); return; } if (e.target.id === 'give-rep-btn') { const giverUid = firebase.auth().currentUser?.uid; const receiverUid = e.target.dataset.targetUid; const amountInput = document.getElementById('rep-amount-input'); const amount = parseInt(amountInput.value, 10); if (!amount || amount < 1) { alert("Please enter a valid, positive amount of REP to give."); return; } if (!giverUid || !receiverUid || giverUid === receiverUid) return; firebase.database().ref(`repTransfers`).push().set({ from: giverUid, to: receiverUid, amount: amount, timestamp: firebase.database.ServerValue.TIMESTAMP }); alert(`You sent ${amount} REP!`); e.target.disabled = true; amountInput.disabled = true; e.target.textContent = "REP Sent!"; return; } // --- Clicks that OPEN a profile --- const userSpan = e.target.closest('.chat-username, .online-username'); if (userSpan) { showUserProfile(userSpan.dataset.uid); return; } const leaderboardRow = e.target.closest('.leaderboard-clickable-row'); if (leaderboardRow) { document.getElementById('rep-leaderboard-modal').style.display = 'none'; showUserProfile(leaderboardRow.dataset.uid); return; } // --- Handle clicks outside of popups to close them --- const profilePopup = document.getElementById('profile-popup'); if (profilePopup && !profilePopup.contains(e.target) && !e.target.closest('.leaderboard-clickable-row, .online-username, .chat-username')) { profilePopup.remove(); } }); // =================================================================== // === END: COMBINED PROFILE & MODERATION SYSTEM (FINAL FIX) ===== // =================================================================== let neonCanvas = null; /* ... (neon line logic largely unchanged, ensure colors update) ... */ let neonCtx = null; let neonLineActive = false; let neonLineColor = state.features.neonLineColor || '#00ffff'; // Initialized from state function createNeonLineCanvas() { if (neonCanvas) { // If canvas exists, just update size and clear if needed neonCanvas.width = window.innerWidth; neonCanvas.height = window.innerHeight; if (neonCtx) neonCtx.clearRect(0,0,neonCanvas.width, neonCanvas.height); // Clear on resize/re-enable return; } neonCanvas = document.createElement('canvas'); neonCanvas.width = window.innerWidth; neonCanvas.height = window.innerHeight; neonCanvas.style.cssText = ` position: fixed; top: 0; left: 0; z-index: 9990; /* Below menu but above game */ pointer-events: none; background: transparent; `; neonCanvas.id = 'neon-line-canvas'; document.body.appendChild(neonCanvas); neonCtx = neonCanvas.getContext('2d'); window.addEventListener('resize', () => { if (neonCanvas) { neonCanvas.width = window.innerWidth; neonCanvas.height = window.innerHeight; // No need to re-set context properties if they don't change on resize } }); } function removeNeonLineCanvas() { // Optional: if you want to fully remove it if (neonCanvas) { neonCanvas.remove(); neonCanvas = null; neonCtx = null; } } function neonLineDraw(event) { if (!neonCanvas || !neonCtx || !neonLineActive) return; // Update context properties based on current state (e.g., color) neonCtx.lineWidth = 2.5; // Slightly thicker neonCtx.lineCap = 'round'; neonCtx.shadowBlur = 12; // Main line glow neonCtx.shadowColor = state.features.neonLineColor; // Use current color from state neonCtx.strokeStyle = state.features.neonLineColor; neonCtx.clearRect(0, 0, neonCanvas.width, neonCanvas.height); const centerX = neonCanvas.width / 2; const centerY = neonCanvas.height / 2; const mouseX = event.clientX; const mouseY = event.clientY; // Draw line neonCtx.beginPath(); neonCtx.moveTo(centerX, centerY); neonCtx.lineTo(mouseX, mouseY); neonCtx.stroke(); // Draw glowing dot at mouse cursor neonCtx.beginPath(); neonCtx.arc(mouseX, mouseY, 6, 0, 2 * Math.PI); neonCtx.fillStyle = state.features.neonLineColor; neonCtx.shadowBlur = 20; // Larger glow for the dot neonCtx.shadowColor = hexToRgba(state.features.neonLineColor, 0.7); // slightly transparent shadow for dot for better effect neonCtx.fill(); } function setAfk(on) { afkOn = on; const afkStatus = document.getElementById('afk-status'); if (afkStatus) { afkStatus.textContent = afkOn ? 'ON' : 'OFF'; afkStatus.style.color = afkOn ? 'lime' : 'red'; } if (typeof updateMenu === "function") updateMenu(); if (afkOn) { if (afkInterval) return; afkInterval = setInterval(() => { if (!state.isInGame) return; const keys = ['ArrowLeft', 'ArrowRight']; const key = keys[Math.floor(Math.random() * 2)]; const type = Math.random() > 0.5 ? 'keydown' : 'keyup'; const evt = new KeyboardEvent(type, { key: key, code: key, keyCode: key === 'ArrowLeft' ? 37 : 39, which: key === 'ArrowLeft' ? 37 : 39, bubbles: true }); document.dispatchEvent(evt); }, Math.random() * 400 + 200); } else { if (afkInterval) clearInterval(afkInterval); afkInterval = null; ['ArrowLeft', 'ArrowRight'].forEach(key => { const evt = new KeyboardEvent('keyup', { key: key, code: key, keyCode: key === 'ArrowLeft' ? 37 : 39, which: key === 'ArrowLeft' ? 37 : 39, bubbles: true }); document.dispatchEvent(evt); }); } } // Initial actions updateMenu(); // Call to build the menu structure and apply styles syncServerBoxWithMenu(); // Sync server box styles updateCSSVariables(); // Set CSS variables based on initial state.menuColor // --- INITIALIZATION --- updateServerIpLoop(); if (state.features.autoRespawn) enableAutoRespawn(); document.addEventListener('click', primeAudio); document.addEventListener('keydown', primeAudio); addServerBox(); patchPlayButtons(); // applyForcedServer(); // Called by patchPlayButtons setup // Start continuous loops zoomLockLoop(); autoBoostLoop(); checkGameState(); drawCircleRestriction(); fpsCounter(); deathSoundObserver(); snakeTrailAnimationLoop(); // The setInterval for snakeTrailPoints is already defined globally where its logic is. // Initial UI setup calls applyPerformanceMode(); pingLoop(); updateMenu(); syncServerBoxWithMenu(); syncChatBoxWithMenu(); updateCSSVariables(); applyUIScale(); applyBackground(); menu.style.display = state.menuVisible ? 'block' : 'none'; if (fpsDisplay) fpsDisplay.style.display = state.features.fpsDisplay ? 'block' : 'none'; setInterval(awardTimeBasedRep, 5 * 60 * 1000); // Check for time-based REP every 5 minutes // --- NEW: One-Time Informational Popup --- (function showOneTimePopup() { const popupVersion = 'betaPopup_v1'; // Change this to 'v2', 'v3' etc. to re-show the popup after an update const hasSeenPopup = localStorage.getItem(popupVersion); if (!hasSeenPopup) { // Create the popup elements const popupOverlay = document.createElement('div'); popupOverlay.className = 'info-popup-overlay'; const popupContent = document.createElement('div'); popupContent.className = 'info-popup-content'; popupContent.innerHTML = ` <h2>Get the Latest Updates!</h2> <p> If you would like the newest, most updated BETA extension before it eventually releases, please join our Discord! </p> <p> <a href="https://dsc.gg/143X" target="_blank">DSC.GG/143X</a> </p> <button id="info-popup-ok-btn" class="info-popup-button">OK</button> `; popupOverlay.appendChild(popupContent); document.body.appendChild(popupOverlay); // Make it visible popupOverlay.style.display = 'flex'; // Add the event listener for the OK button document.getElementById('info-popup-ok-btn').addEventListener('click', () => { popupOverlay.style.display = 'none'; localStorage.setItem(popupVersion, 'true'); // Mark as seen popupOverlay.remove(); // Clean up the element from the page }); } })(); // --- END: One-Time Informational Popup --- })(); // End of the main IIFE
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址