// ==UserScript==
// @name SLITHER.IO MOD MENU - DSC.GG/143X CHAT, PROFILES, +REP, ZOOM, LEADERBOARD, GIFS & MORE
// @namespace http://tampermonkey.net/
// @version X6
// @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: 'VX6',
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 = 7000;
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 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(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();
if (now - lastChatMessageTime < chatCooldown) {
alert(`Slow down! You can only send a message every 5 seconds.`);
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 = '';
}
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.");
}
}
}
});
};
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;
}
});
}
// 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
// (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;">VX6</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)}`;
// 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