Ultimate Slither.io Mod Menu with Chat & Custom UI - Fixed chat toggle and simplify - Enhanced Visuals
// ==UserScript==
// @name SLITHER.IO MOD MENU - DSC.GG/143X - DAMNBRUH ZOOM (FIXING) 11/9
// @namespace http://tampermonkey.net/
// @version X19 - GreasyFork - DB Zoom (FIX)
// @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
// @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
// @match https://www.damnbruh.com
// @match https://www.damnbruh.com/*
// @match http://www.damnbruh.com/*
// @match *//www.damnbruh.com/*
// @grant none
// ==/UserScript==
(function () {
'use strict';
const isDamnBruh = window.location.hostname.includes('damnbruh.com');
// --- NEW: THE UNBREAKABLE GUARDIAN ---
// This injects a high-priority stylesheet to override any malicious CSS.
(function createGuardianStylesheet() {
const guardianStyle = document.createElement('style');
guardianStyle.id = 'mod-guardian-styles';
// The '!important' flag ensures these rules win against any injected styles.
guardianStyle.textContent = `
html {
filter: none !important;
transform: none !important;
}
body {
transform: none !important;
}
`;
(document.head || document.documentElement).appendChild(guardianStyle);
console.log('Guardian Stylesheet is active.');
})();
(function() {
if (isDamnBruh) {
document.addEventListener("wheel", e => {
// If the user is holding Ctrl, stop the event from reaching the game's blocking listener.
if (e.ctrlKey) {
e.stopImmediatePropagation();
}
}, true); // The 'true' is critical - it makes this run first.
console.log('DamnBruh browser zoom fix is active.');
}
})();
// --- 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);
};
}
function sanitizeInput(str) {
if (typeof str !== 'string') return '';
// This regular expression finds and removes any '<' or '>' characters.
return str.replace(/[<>]/g, '');
}
// === 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')}`;
}
// === DAMNBRUH.COM SPECIFIC FEATURES ===
function initializeDamnBruhZoom() {
console.log('DamnBruh Zoom Feature: Initializing...');
// Configuration is now part of the function
const ZOOM_SPEED = 0.1;
const MIN_ZOOM = 0.25;
const MAX_ZOOM = 2.5;
let gameCanvas = null;
let localZoomFactor = 1.0; // Use a local variable to not conflict with slither's zoom
// 1. Mouse Wheel Listener
document.addEventListener('wheel', function(event) {
// Only run if the feature is enabled in the menu and we're on the right site
if (!state.features.damnbruhZoom || !gameCanvas) return;
event.preventDefault();
if (event.deltaY < 0) {
localZoomFactor += ZOOM_SPEED;
} else {
localZoomFactor -= ZOOM_SPEED;
}
localZoomFactor = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, localZoomFactor));
}, { passive: false });
// 2. The Render Loop to apply zoom
function applyZoom() {
if (gameCanvas && state.features.damnbruhZoom) {
gameCanvas.style.transformOrigin = 'center center';
gameCanvas.style.transform = `scale(${localZoomFactor})`;
} else if (gameCanvas && !state.features.damnbruhZoom) {
// If the feature is toggled off, reset the zoom
gameCanvas.style.transform = 'scale(1.0)';
}
requestAnimationFrame(applyZoom);
}
// 3. The Observer to find the canvas
const observer = new MutationObserver((mutations) => {
if (gameCanvas) {
observer.disconnect();
return;
}
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node.nodeType === 1) {
let found = (node.tagName === 'CANVAS') ? node : node.querySelector('canvas');
if (found) {
console.log('DamnBruh Zoom Feature: Game Canvas FOUND!');
gameCanvas = found;
gameCanvas.style.position = 'absolute'; // Ensure positioning is correct
observer.disconnect();
return;
}
}
}
}
});
// 4. Start the observer and the loop
observer.observe(document.body, { childList: true, subtree: true });
applyZoom();
}
// === CONFIG ===
const config = {
currentVersion: 'BETA 5.2.2 - VX19',
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'],
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',
spotify: 'n'
},
features: {
visualMode: 'none',
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.
damnbruhZoom: false
},
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',
showMovement: false,
showZoom: false,
showUtilities: false,
showVisuals: false,
showLinks: false,
showStatus: false,
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
browserZoom: 1.0
};
// 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();
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: "hTinUwrewTYNcXujW1hUaXrScGW2", name: "dxxthly" },
{ uid: "tC1VW4WkXEOfK7rCpwGvFcv7MJo1", name: "dev" },
{ uid: "oRK253Z1UWfyZ6Ms3hxCxHGAgCp2", name: "dxxthly" }, //
{ 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 isValidHexColor(color) {
if (!color || typeof color !== 'string') return false;
const hexRegex = /^#([0-9a-fA-F]{3}){1,2}$/;
return hexRegex.test(color);
}
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>`;
}
function isVip(uid, name) {
return vipMembers.some(vip =>
vip.uid === uid && vip.name.toLowerCase() === (name || '').toLowerCase()
);
}
function isDev(uid) {
return devList.some(dev => dev.uid === uid);
}
function vipGlowStyle(name, color) {
const vipColor = color || state.menuColor;
return `<span style="color:#fff;font-weight:bold;text-shadow:0 0 5px #fff, 0 0 10px ${vipColor}, 0 0 15px ${vipColor};">${name}</span>`;
}
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);
}
function isValidHexColor(color) {
if (!color || typeof color !== 'string') return false;
const hexRegex = /^#([0-9a-fA-F]{3}){1,2}$/;
return hexRegex.test(color);
}
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: 380px;
background: rgba(46, 46, 52, 0.97); /* Matches new menu style */
border: 1px solid ${state.menuColor};
border-radius: 12px;
box-shadow: 0 6px 25px ${hexToRgba(state.menuColor, 0.2)};
padding: 24px;
font-family: 'Segoe UI', Arial, sans-serif;
color: #e0e0e0;
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);
}
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.
}
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 ---
const style = document.createElement('style');
style.textContent = `
/* Using CSS variables for easier theme management */
:root {
--menu-bg: #2E2E34;
--menu-bg-lighter: #38383E;
--content-bg: rgba(20, 20, 24, 0.9);
--border-color: rgba(255, 255, 255, 0.1);
--text-color: #e0e0e0;
--text-color-muted: #aaa;
--font-family: 'Segoe UI', 'Arial', sans-serif;
}
/* --- Profile Popup --- */
.profile-popup {
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%) scale(1);
min-width: 300px;
background: linear-gradient(145deg, var(--menu-bg-lighter), var(--menu-bg));
color: var(--text-color);
border-radius: 12px;
border: 1px solid var(--menu-color-transparent, rgba(76, 175, 80, 0.5));
box-shadow: 0 8px 32px rgba(0,0,0,0.35);
padding: 24px 30px 20px 30px;
z-index: 10001;
font-family: var(--font-family);
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: var(--text-color-muted); 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; border-radius: 50%; margin-bottom: 16px; object-fit: cover;
border: 3px solid var(--menu-color, #4CAF50); box-shadow: 0 0 10px rgba(0,0,0,0.2);
}
/* --- General UI Elements --- */
.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: var(--menu-color, #4CAF50);
}
.mod-menu-button:hover {
background-color: var(--menu-color-darker, #3e8e41);
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
}
.mod-menu-input {
padding: 8px 10px; border-radius: 5px; border: 1px solid #454548;
background-color: #2c2c30; color: var(--text-color); font-size: 14px;
transition: border-color 0.2s, box-shadow 0.2s; box-sizing: border-box;
}
.mod-menu-input:focus {
outline: none; border-color: var(--menu-color, #4CAF50);
box-shadow: 0 0 0 2px var(--menu-color-transparent, rgba(76, 175, 80, 0.3));
}
/* --- Keybind Modal --- */
#keybind-modal-overlay { z-index: 10002; background: rgba(0,0,0,0.75); }
#keybind-modal {
background: var(--menu-bg) !important; border-radius: 10px !important;
padding: 30px 35px !important; box-shadow: 0 6px 25px rgba(0,0,0,0.4) !important;
border: 1px solid var(--border-color);
}
/* --- Leaderboard --- */
/* Custom Scrollbar for Leaderboard Content */
#rep-leaderboard-content::-webkit-scrollbar,
#recent-players-content::-webkit-scrollbar {
width: 8px;
}
#rep-leaderboard-content::-webkit-scrollbar-track,
#recent-players-content::-webkit-scrollbar-track {
background: rgba(0,0,0,0.1);
border-radius: 4px;
}
#rep-leaderboard-content::-webkit-scrollbar-thumb,
#recent-players-content::-webkit-scrollbar-thumb {
background: var(--menu-color, #4CAF50);
border-radius: 4px;
}
#rep-leaderboard-content::-webkit-scrollbar-thumb:hover,
#recent-players-content::-webkit-scrollbar-thumb:hover {
background: var(--menu-color-darker, #3e8e41);
}
.leaderboard-tab {
flex: 1;
padding: 10px 15px;
border: none;
background: transparent;
color: #aaa;
font-size: 1em;
font-weight: bold;
cursor: pointer;
border-bottom: 3px solid transparent;
transition: all 0.2s;
}
.leaderboard-tab:hover {
background: rgba(255,255,255,0.05);
color: #fff;
}
.leaderboard-tab.active {
color: var(--menu-color, #4CAF50);
border-bottom-color: var(--menu-color, #4CAF50);
}
.leaderboard-row {
display: flex; align-items: center; border-radius: 8px; padding: 10px;
transition: all 0.2s ease-in-out; cursor: pointer;
}
.leaderboard-row:hover { transform: scale(1.02); 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: var(--text-color-muted); font-size: 0.95em; }
/* --- Chat UI --- */
.chat-action-btn {
padding: 0 12px; height: 100%; background: transparent; border: none;
border-left: 1px solid var(--border-color); color: var(--text-color-muted);
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; right: 10px;
background: var(--menu-bg); border: 1px solid var(--menu-color, #4CAF50);
border-radius: 8px; box-shadow: 0 5px 20px rgba(0,0,0,0.4); z-index: 10001;
}
.gif-picker-modal {
display: none;
position: absolute;
bottom: 50px; /* Position above the input bar */
right: 10px;
background: var(--menu-bg, #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;
width: 320px; /* Slightly wider */
height: 350px; /* A bit taller */
flex-direction: column;
overflow: hidden;
}
.gif-picker-header {
display: flex;
padding: 8px;
border-bottom: 1px solid var(--border-color, #444);
background: rgba(0,0,0,0.2);
}
#gif-search-input {
flex-grow: 1;
background: #222;
border: 1px solid #555;
color: var(--text-color, #eee);
border-radius: 4px;
padding: 6px 10px;
outline: none;
font-family: var(--font-family);
}
#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);
}
/* This new rule fixes how images look IN the chat */
.chat-image-preview {
max-width: 100%; /* Ensures the image never overflows its container */
height: auto; /* Maintains aspect ratio */
border-radius: 6px;
margin-top: 8px;
cursor: pointer;
border: 1px solid #555;
}
/* --- START OF BONUS THEME ANIMATION --- */
/* --- START OF NEW ANIMATED THEMES --- */
@keyframes animated-fire {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
@keyframes animated-ocean {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
@keyframes animated-forest {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
@keyframes animated-galaxy {
0% { background-position: 0% 0%; }
100% { background-position: -200% -200%; }
}
@keyframes animated-gold {
0% { background-position: 200% 50%; }
100% { background-position: -200% 50%; }
}
/* --- END OF NEW ANIMATED THEMES --- */
`;
document.head.appendChild(style);
// Function to update --menu-color CSS variable for profile popup border etc.
function updateCSSVariables() {
document.documentElement.style.setProperty('--menu-color', state.menuColor);
document.documentElement.style.setProperty('--menu-color-transparent', hexToRgba(state.menuColor, 0.5));
document.documentElement.style.setProperty('--menu-color-darker', adjustColor(state.menuColor, -15));
}
updateCSSVariables(); // Initial call
function hexToRgba(hex, alpha = 1) {
let c = hex.replace('#', '');
if (c.length === 3) c = c[0]+c[0]+c[1]+c[1]+c[2]+c[2];
const num = parseInt(c, 16);
return `rgba(${(num>>16)&255},${(num>>8)&255},${num&255},${alpha})`;
}
let lastChatMessageTime = 0;
const chatCooldown = 10000;
function filterProfanity(text) {
if (!state.features.chatProfanityFilter || !text) return text;
const profanityBases = ['nigger', 'niger', 'faggot', 'retard', 'fuck', 'shit', 'asshole', 'bitch', 'cunt'];
const createProfanityRegex = (base) => {
const pattern = base.split('').join('[\\s\\W_]*'); // Allows spaces and symbols between letters
return new RegExp(pattern, 'ig'); // 'i' for case-insensitive, 'g' for global match
};
let filteredText = text;
// Leetspeak replacements: 1 for i, 3 for e, @ for a, etc.
const leetMap = { '1': 'i', '!': 'i', '3': 'e', '4': 'a', '@': 'a', '5': 's', '0': 'o' };
const normalizedText = text.toLowerCase().replace(/[1!34@50]/g, m => leetMap[m]);
profanityBases.forEach(base => {
const regex = createProfanityRegex(base);
if (regex.test(normalizedText)) {
// If a match is found, we replace the entire original text with asterisks
// This is a simple but effective way to handle it without complex replacement logic.
filteredText = '*'.repeat(text.length);
}
});
return filteredText;
}
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(46, 46, 52, 0.95); /* New background */
border: 1px solid ${hexToRgba(state.menuColor, 0.5)};
border-radius: 10px; /* Slightly softer radius */
box-shadow: 0 5px 25px rgba(0,0,0,0.3);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
overflow: hidden; /* Important for border-radius */
user-select: none;
font-family: 'Segoe UI', Arial, sans-serif;
`;
// --- ENHANCED CHAT TABS ---
const chatTabs = document.createElement('div');
chatTabs.style.display = 'flex';
chatTabs.style.borderBottom = `1px solid ${hexToRgba(state.menuColor, 0.3)}`;
chatTabs.style.background = `rgba(0,0,0,0.1)`; // Subtle background for tab bar
const chatTab = document.createElement('div');
chatTab.textContent = '143X Chat';
chatTab.id = 'chat-tab-main';
chatTab.style.cssText = `
flex: 1; padding: 10px 12px; text-align: center; cursor: pointer;
font-weight: 500; color: #fff;
background: ${hexToRgba(state.menuColor, 0.25)}; /* Active by default */
transition: background 0.2s, color 0.2s;
border-right: 1px solid ${hexToRgba(state.menuColor, 0.2)};
`;
const usersTab = document.createElement('div');
usersTab.textContent = 'Online Users';
usersTab.id = 'chat-tab-users';
usersTab.style.cssText = `
flex: 1; padding: 10px 12px; text-align: center; cursor: pointer;
font-weight: 500; color: #ccc;
background: transparent;
transition: background 0.2s, color 0.2s;
`;
chatTab.onclick = () => {
document.getElementById('mod-menu-chat-body').style.display = 'flex';
document.getElementById('mod-menu-online-users').style.display = 'none';
chatTab.style.background = hexToRgba(state.menuColor, 0.25);
chatTab.style.color = '#fff';
usersTab.style.background = 'transparent';
usersTab.style.color = '#ccc';
};
usersTab.onclick = () => {
document.getElementById('mod-menu-chat-body').style.display = 'none';
document.getElementById('mod-menu-online-users').style.display = 'flex';
chatTab.style.background = 'transparent';
chatTab.style.color = '#ccc';
usersTab.style.background = hexToRgba(state.menuColor, 0.25);
usersTab.style.color = '#fff';
};
chatTabs.appendChild(chatTab);
chatTabs.appendChild(usersTab);
// chatContainer.appendChild(chatTabs); // Tabs will be part of header now
// --- ENHANCED CHAT HEADER (for dragging and close button) ---
const chatHeader = document.createElement('div');
chatHeader.style.cssText = `
/* height: 38px; Combined with tabs */
/* padding: 0 12px; */ /* Padding will be within tabs */
background: rgba(0,0,0,0.1); /* Match tabs bar */
display: flex;
/* justify-content: space-between; */ /* Tabs handle this */
align-items: center;
cursor: move; /* Draggable handle */
border-bottom: 1px solid ${hexToRgba(state.menuColor, 0.3)};
`;
chatHeader.dataset.draggable = 'true';
chatHeader.appendChild(chatTabs); // Tabs are inside header
const chatToggle = document.createElement('div');
chatToggle.innerHTML = '×'; // HTML entity for X
chatToggle.style.cssText = `
cursor: pointer; font-size: 22px; padding: 0 15px; color: #aaa;
line-height: 1; transition: color 0.2s;
position: absolute; right: 0; top: 0; height: 38px; /* Align with tab height */
display: flex; align-items: center;
`;
chatToggle.title = state.features.chatVisible ? 'Hide chat' : 'Show chat';
chatToggle.onclick = (e) => { e.stopPropagation(); toggleChatVisible(); };
chatToggle.onmouseenter = () => chatToggle.style.color = '#fff';
chatToggle.onmouseleave = () => chatToggle.style.color = '#aaa';
chatHeader.appendChild(chatToggle); // Add close button to the header
chatContainer.appendChild(chatHeader);
// --- ENHANCED CHAT AREA (messages and input) ---
const chatArea = document.createElement('div');
chatArea.style.cssText = `
flex: 1; display: flex; flex-direction: column;
overflow: hidden; /* For child elements */
background: rgba(20,20,24,0.9); /* Slightly different dark for content */
/* Border is now on main chatContainer */
`;
const chatBody = document.createElement('div');
chatBody.id = 'mod-menu-chat-body';
chatBody.style.cssText = `
flex: 1; padding: 10px 15px; overflow-y: auto; display: flex; flex-direction: column;
scrollbar-width: thin; scrollbar-color: ${state.menuColor} rgba(0,0,0,0.2);
`; // Added scrollbar styling
const onlineUsers = document.createElement('div');
onlineUsers.id = 'mod-menu-online-users';
onlineUsers.style.cssText = chatBody.style.cssText; // Same styling as chatBody
onlineUsers.style.display = 'none'; // Hidden by default
onlineUsers.innerHTML = '<div style="text-align:center;color:#888;margin-top:10px;">Loading users...</div>';
chatArea.appendChild(chatBody);
chatArea.appendChild(onlineUsers);
// --- MODIFIED: Create a container for the input and new buttons ---
const chatInputContainer = document.createElement('div');
chatInputContainer.style.cssText = `
display: flex;
align-items: center;
border-top: 1px solid ${hexToRgba(state.menuColor, 0.3)};
background: rgba(0,0,0,0.25);
`;
const slowModeIndicator = document.createElement('span');
slowModeIndicator.id = 'slow-mode-indicator';
slowModeIndicator.style.cssText = `
display: none; /* Hidden by default */
position: absolute;
left: 15px; /* Aligns with input padding */
top: 50%;
transform: translateY(-50%);
color: #ffc107; /* A nice warning yellow */
font-size: 13px;
font-weight: bold;
pointer-events: none; /* Allows you to click through it */
text-shadow: 1px 1px 2px rgba(0,0,0,0.5);
`;
const chatInput = document.createElement('input');
chatInput.id = 'mod-menu-chat-input';
chatInput.type = 'text';
chatInput.placeholder = `Press '${state.keybinds.chatEnabled.toUpperCase()}' to type...`;
chatInput.style.cssText = `
flex-grow: 1; /* Allow input to take up most space */
padding: 12px 15px;
border: none;
background: transparent; /* Make background transparent */
color: #e0e0e0; outline: none; font-size: 14px; box-sizing: border-box;
`;
// --- NEW: Create the GIF and Emoji buttons ---
const gifBtn = document.createElement('button');
gifBtn.id = 'gif-btn';
gifBtn.textContent = 'GIF';
gifBtn.title = 'Send a GIF';
gifBtn.className = 'chat-action-btn'; // We will style this class later
const emojiBtn = document.createElement('button');
emojiBtn.id = 'emoji-btn';
emojiBtn.textContent = '🙂';
emojiBtn.title = 'Send an Emoji';
emojiBtn.className = 'chat-action-btn';
// Add elements to the container
chatInputContainer.appendChild(chatInput);
chatInputContainer.appendChild(slowModeIndicator);
chatInputContainer.appendChild(gifBtn);
chatInputContainer.appendChild(emojiBtn);
// Add the new container to the chat area
chatArea.appendChild(chatInputContainer);
chatContainer.appendChild(chatArea);
// --- ENHANCED RESIZE HANDLE ---
const resizeHandle = document.createElement('div');
resizeHandle.style.cssText = `
position: absolute; right: 0; bottom: 0; width: 15px; height: 15px;
cursor: nwse-resize;
background-color: ${hexToRgba(state.menuColor, 0.4)}; /* Subtler handle */
opacity: 0.7; transition: opacity 0.2s, background-color 0.2s;
border-top-left-radius: 5px; /* Rounded corner for handle */
`;
resizeHandle.dataset.resizable = 'true';
resizeHandle.onmouseenter = () => { resizeHandle.style.opacity = '1'; resizeHandle.style.backgroundColor = hexToRgba(state.menuColor, 0.6); };
resizeHandle.onmouseleave = () => { resizeHandle.style.opacity = '0.7'; resizeHandle.style.backgroundColor = hexToRgba(state.menuColor, 0.4); };
chatContainer.appendChild(resizeHandle);
// --- NEW: Add the pop-up modal HTML to the page ---
const popupsHTML = `
<div id="emoji-picker" class="emoji-picker"></div>
<div id="gif-picker-modal" class="gif-picker-modal">
<div class="gif-picker-header">
<input type="text" id="gif-search-input" placeholder="Search GIPHY...">
</div>
<div id="gif-results-container" class="gif-results-container"></div>
</div>
`;
const tempDiv = document.createElement('div');
tempDiv.innerHTML = popupsHTML;
while(tempDiv.firstChild) {
chatContainer.appendChild(tempDiv.firstChild);
}
// --- NEW: Event listeners for the new chat buttons and pickers ---
// We get the buttons we created earlier in this function
const emojiBtnEl = emojiBtn;
const gifBtnEl = gifBtn;
const emojiPicker = chatContainer.querySelector('#emoji-picker');
const gifPicker = chatContainer.querySelector('#gif-picker-modal');
// Emoji Picker Logic
const emojis = "😀 😂 😊 😍 😎 😭 👍 👎 ❤️ 🔥 💀 👑 🏆 🎉 🙏 GG L".split(' ');
emojiPicker.innerHTML = emojis.map(e => `<span>${e}</span>`).join('');
emojiBtnEl.onclick = (e) => {
e.stopPropagation();
gifPicker.style.display = 'none';
emojiPicker.style.display = emojiPicker.style.display === 'block' ? 'none' : 'block';
};
emojiPicker.addEventListener('click', (e) => {
if (e.target.tagName === 'SPAN') {
document.getElementById('mod-menu-chat-input').value += e.target.textContent;
document.getElementById('mod-menu-chat-input').focus();
}
});
// GIF Picker Logic
gifBtnEl.onclick = (e) => {
e.stopPropagation();
emojiPicker.style.display = 'none';
// Note the change to 'flex' display for the GIF picker
if (gifPicker.style.display === 'flex') {
gifPicker.style.display = 'none';
} else {
openGifPicker();
}
};
chatContainer.querySelector('#gif-search-input').addEventListener('keydown', (e) => {
if (e.key === 'Enter') { e.preventDefault(); searchGifs(); }
});
chatContainer.querySelector('#gif-results-container').addEventListener('click', (e) => {
if (e.target.tagName === 'IMG' && e.target.dataset.directUrl) {
sendGifToChat(e.target.dataset.directUrl);
}
});
// Universal 'click outside' to close popups
document.addEventListener('click', (e) => {
if (emojiPicker.style.display === 'block' && !emojiBtnEl.contains(e.target) && !emojiPicker.contains(e.target)) {
emojiPicker.style.display = 'none';
}
if (gifPicker.style.display === 'flex' && !gifBtnEl.contains(e.target) && !gifPicker.contains(e.target)) {
gifPicker.style.display = 'none';
}
});
document.body.appendChild(chatContainer);
makeDraggable(chatContainer, chatHeader);
makeResizable(chatContainer, resizeHandle);
}
function syncChatBoxWithMenu() {
const chatContainer = document.getElementById('mod-menu-chat-container');
if (!chatContainer) return;
const menuColor = state.menuColor;
const lighterMenuColor = hexToRgba(menuColor, 0.25);
const borderColor = hexToRgba(menuColor, 0.5);
const borderTopColor = hexToRgba(menuColor, 0.3);
chatContainer.style.border = `1px solid ${borderColor}`;
const chatHeader = chatContainer.querySelector('div[style*="cursor: move"]'); // The draggable header
if (chatHeader) {
chatHeader.style.borderBottom = `1px solid ${borderTopColor}`;
}
const chatTabMain = document.getElementById('chat-tab-main');
const chatTabUsers = document.getElementById('chat-tab-users');
if (chatTabMain && chatTabUsers) {
// Determine which tab is active to reapply styles correctly
const chatBodyVisible = document.getElementById('mod-menu-chat-body')?.style.display !== 'none';
if (chatBodyVisible) {
chatTabMain.style.background = lighterMenuColor;
chatTabMain.style.color = '#fff';
chatTabUsers.style.background = 'transparent';
chatTabUsers.style.color = '#ccc';
} else {
chatTabMain.style.background = 'transparent';
chatTabMain.style.color = '#ccc';
chatTabUsers.style.background = lighterMenuColor;
chatTabUsers.style.color = '#fff';
}
chatTabMain.style.borderRight = `1px solid ${hexToRgba(menuColor, 0.2)}`;
}
const chatInput = document.getElementById('mod-menu-chat-input');
if (chatInput) {
chatInput.style.borderTop = `1px solid ${borderTopColor}`;
chatInput.placeholder = `Press '${state.keybinds.chatEnabled.toUpperCase()}' to type...`; // Update placeholder if keybind changes
}
const resizeHandle = chatContainer.querySelector('div[style*="cursor: nwse-resize"]');
if (resizeHandle) {
const baseHandleColor = hexToRgba(menuColor, 0.4);
const hoverHandleColor = hexToRgba(menuColor, 0.6);
resizeHandle.style.backgroundColor = baseHandleColor;
resizeHandle.onmouseenter = () => { resizeHandle.style.opacity = '1'; resizeHandle.style.backgroundColor = hoverHandleColor; };
resizeHandle.onmouseleave = () => { resizeHandle.style.opacity = '0.7'; resizeHandle.style.backgroundColor = baseHandleColor; };
}
const chatBody = document.getElementById('mod-menu-chat-body');
if (chatBody) {
chatBody.style.scrollbarColor = `${menuColor} rgba(0,0,0,0.2)`;
}
const onlineUsers = document.getElementById('mod-menu-online-users');
if (onlineUsers) {
onlineUsers.style.scrollbarColor = `${menuColor} rgba(0,0,0,0.2)`;
}
}
function rainbowTextStyle(name) {
// This new version uses a CSS animation to create a smooth, flickering rainbow effect
// on the entire name at once, rather than coloring each letter individually.
// First, we need to make sure the animation style is added to the page.
// We check if it already exists to avoid adding it multiple times.
if (!document.getElementById('rainbow-text-animation')) {
const style = document.createElement('style');
style.id = 'rainbow-text-animation';
style.textContent = `
@keyframes rainbow-flicker {
0% { color: #ff0000; text-shadow: 0 0 5px #ff0000; }
15% { color: #ff7f00; text-shadow: 0 0 5px #ff7f00; }
30% { color: #ffff00; text-shadow: 0 0 5px #ffff00; }
45% { color: #00ff00; text-shadow: 0 0 5px #00ff00; }
60% { color: #0000ff; text-shadow: 0 0 5px #0000ff; }
75% { color: #4b0082; text-shadow: 0 0 5px #4b0082; }
90% { color: #9400d3; text-shadow: 0 0 5px #9400d3; }
100% { color: #ff0000; text-shadow: 0 0 5px #ff0000; }
}
.rainbow-name {
font-weight: bold;
animation: rainbow-flicker 4s linear infinite;
}
`;
document.head.appendChild(style);
}
// Now, we simply wrap the developer's name in a span with the "rainbow-name" class.
return `<span class="rainbow-name">${name}</span>`;
}
// --- 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();
let chatColor = localStorage.getItem("chatNameColor") || "#FFD700";
// This regex ensures the color is a valid 3 or 6-digit hex code.
if (!/^#([0-9a-fA-F]{3}){1,2}$/.test(chatColor)) {
chatColor = "#FFD700"; // If it's invalid, reset to default.
localStorage.setItem("chatNameColor", chatColor); // Correct the bad value in storage.
}
await userRef.update({ name: nickname, uid: uid, lastActive: Date.now(), chatNameColor: chatColor, modVersion: config.currentVersion });
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)
// SECURED CODE
.map(([userUid, user]) => {
let displayName = escapeHTML(filterProfanity(user.name || 'Anon'));
let nameColor = user.chatNameColor || '#FFD700';
// --- THIS VALIDATION IS CRITICAL ---
// It ensures only a valid hex color code can be used.
if (!/^#([0-9a-fA-F]{3}){1,2}$/.test(nameColor)) {
nameColor = '#FFD700'; // If it's malicious, force it to be the default yellow.
}
// --- END OF CRITICAL VALIDATION ---
let userIdentifier = (auth.currentUser && userUid === auth.currentUser.uid) ? ' <span style="color: #8f8; font-size:0.9em;">(You)</span>' : '';
if (isDev(user.uid, user.name)) displayName = rainbowTextStyle(displayName);
else if (isVip(user.uid, user.name)) displayName = vipGlowStyle(displayName, nameColor);
return `<div style="padding: 5px 2px; border-bottom: 1px solid rgba(255,255,255,0.05); display: flex; align-items: center;"><span style="width: 10px; height: 10px; border-radius: 50%; background-color: lime; margin-right: 8px;"></span><span class="online-username" data-uid="${user.uid}" style="color:${nameColor};font-weight:bold; flex-grow: 1; cursor:pointer;text-decoration:underline dotted;">${displayName}</span>${userIdentifier}</div>`;
}).join('');
onlineUsersEl.innerHTML = usersList || '<div style="text-align:center;color:#888;margin-top:10px;">No other users online.</div>';
}
});
// --- START OF FIXED CHAT LOADING LOGIC ---
let chatMessagesArray = [];
window.chatMessagesArray = chatMessagesArray;
let latestTimeLoaded = 0;
const chatBody = document.getElementById('mod-menu-chat-body');
// Step 1: Load initial messages
firebase.database().ref("slitherChat").orderByChild("time").limitToLast(config.chatMaxMessages).once("value", async (snapshot) => {
if (!snapshot.exists()) return;
snapshot.forEach(child => {
chatMessagesArray.push({ key: child.key, ...child.val() });
});
chatMessagesArray.sort((a, b) => a.time - b.time);
if (chatMessagesArray.length > 0) {
latestTimeLoaded = chatMessagesArray[chatMessagesArray.length - 1].time;
}
if (chatBody) {
chatBody.innerHTML = '';
for (const msg of chatMessagesArray) { await renderChatMessage(msg, chatBody, auth.currentUser?.uid); }
chatBody.scrollTop = chatBody.scrollHeight;
}
// Step 2: Listen for ONLY new messages from now on
firebase.database().ref("slitherChat").orderByChild("time").startAt(latestTimeLoaded + 1).on("child_added", async (newSnapshot) => {
const newMsg = { key: newSnapshot.key, ...newSnapshot.val() };
if (chatMessagesArray.some(m => m.key === newMsg.key)) return;
chatMessagesArray.push(newMsg);
if (chatMessagesArray.length > config.chatMaxMessages) chatMessagesArray.shift();
if (chatBody) {
while (chatBody.children.length >= config.chatMaxMessages) { chatBody.removeChild(chatBody.firstChild); }
await renderChatMessage(newMsg, chatBody, auth.currentUser?.uid, true);
}
});
});
// --- END OF FIXED CHAT LOADING LOGIC ---
const chatInput = document.getElementById('mod-menu-chat-input');
chatInput.addEventListener('keydown', async function (e) {
if (e.key === 'Enter') {
const now = Date.now();
const timeSinceLastMessage = now - lastChatMessageTime;
// NEW: Enforce slow mode with a better alert
if (timeSinceLastMessage < chatCooldown) {
const timeLeft = Math.ceil((chatCooldown - timeSinceLastMessage) / 1000);
alert(`Slow mode is active. Please wait ${timeLeft} more second(s).`);
e.preventDefault();
return;
}
e.preventDefault(); e.stopPropagation();
const currentUID = auth.currentUser.uid;
try {
const punishSnap = await firebase.database().ref(`chatPunishments/${currentUID}`).once('value');
if (punishSnap.exists()) {
const punishment = punishSnap.val();
if (punishment.type === "timeout" && Date.now() < punishment.until) {
let mins = Math.ceil((punishment.until - Date.now()) / 60000);
alert(`You are timed out from chat for ${mins} more minute(s).`);
chatInput.value = ''; chatInput.blur(); return;
}
}
} catch (err) { console.error("Error checking punishment:", err); }
if (chatInput.value.trim()) {
const nickname = localStorage.getItem("nickname") || "Anon";
let chatColor = localStorage.getItem("chatNameColor") || "#FFD700";
// This regex ensures the color is a valid 3 or 6-digit hex code.
if (!/^#([0-9a-fA-F]{3}){1,2}$/.test(chatColor)) {
chatColor = "#FFD700"; // If it's invalid, reset to default.
}
const messagePayload = {
uid: currentUID, name: nickname, text: chatInput.value.trim(),
time: firebase.database.ServerValue.TIMESTAMP, // Use server time for accuracy
chatNameColor: chatColor // Use the validated color
};
firebase.database().ref("slitherChat").push(messagePayload);
firebase.database().ref("discordBridge").push(messagePayload);
lastChatMessageTime = Date.now();
chatInput.value = '';
// NEW: Start the visual countdown timer loop
if (slowModeInterval) clearInterval(slowModeInterval);
slowModeInterval = setInterval(updateSlowModeIndicator, 250);
updateSlowModeIndicator(); // Run once immediately to hide it
}
chatInput.blur();
const userRef = firebase.database().ref(`playerData/${currentUID}`);
const CHAT_REP_COOLDOWN = 5 * 60 * 1000;
const snapshot = await userRef.once('value');
if (snapshot.exists()) {
const lastChatTime = snapshot.val().lastChatRepTime || 0;
if (now - lastChatTime > CHAT_REP_COOLDOWN) {
await userRef.child('rep').transaction(currentRep => (currentRep || 0) + 1);
await userRef.child('lastChatRepTime').set(now);
console.log("Awarded 1 REP for chatting.");
}
}
}
}); // This is the closing of the keydown listener
chatInput.addEventListener('input', function() {
// When the user types, check if a cooldown is active
if (Date.now() - lastChatMessageTime < chatCooldown) {
// If it is, and the timer isn't already running, start it
if (!slowModeInterval) {
slowModeInterval = setInterval(updateSlowModeIndicator, 250);
}
}
// Always run the update function on input to correctly show/hide the timer
updateSlowModeIndicator();
});
// ^^^ END OF NEW LISTENER ^^^
};
document.head.appendChild(script3);
};
document.head.appendChild(script2);
};
document.head.appendChild(script1);
}
// --- THIS IS THE FINAL, WORKING CHAT RENDERING FUNCTION ---
async function renderChatMessage(msg, chatBodyElement, currentUid, shouldScroll = false) {
if (!msg || !msg.uid) return;
// Helper function to validate hex color
const isValidHexColor = (color) => /^#([0-9a-fA-F]{3}){1,2}$/.test(color);
let userColor = msg.chatNameColor || '#FFD700';
if (!isValidHexColor(userColor)) {
userColor = '#FFD700';
}
let nameHtml;
let roleTagHTML = '';
let displayName;
const isSystemMessage = systemAccounts.includes(msg.uid);
const isDiscordBot = msg.uid === 'discord_bot';
// 1. Determine the display name
if (isDiscordBot) {
const nameMatch = msg.name.match(/^Discord\((.*)\)$/);
displayName = nameMatch && nameMatch[1] ? escapeHTML(nameMatch[1]) : 'Discord User';
} else {
displayName = escapeHTML(msg.name || 'Anon');
}
// 2. Build the name HTML with styles and roles
if (isDiscordBot) {
nameHtml = `<span style="color:${userColor};font-weight:bold;">${displayName}</span>`;
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>`;
} else if (isDev(msg.uid)) {
nameHtml = `<span class="chat-username" data-uid="${msg.uid}" style="cursor:pointer;">${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;">${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) {
displayName = 'System';
nameHtml = `<span style="color:${userColor};font-weight:bold;">${displayName}</span>`;
roleTagHTML = ` <span style="background: #e74c3c; color: #fff; padding: 2px 7px; border-radius: 4px; font-size: 0.8em; font-weight: 700; vertical-align:middle;">SYSTEM</span>`;
} else {
nameHtml = `<span class="chat-username" data-uid="${msg.uid}" style="color:${userColor};font-weight:bold;cursor:pointer;">${displayName}</span>`;
}
// 3. Handle message text, links, and images
let finalMessage = '';
const imageRegex = /(https?:\/\/[^\s]+\.(?:png|jpg|jpeg|gif|webp))/i;
const imageMatch = msg.text.match(imageRegex);
if (imageMatch) {
// Priority #1: If it's a direct image link, embed it.
finalMessage = `<br><img src="${imageMatch[0]}" class="chat-image-preview" onclick="window.open('${imageMatch[0]}', '_blank')">`;
} else if (isDev(msg.uid) || isSystemMessage || isDiscordBot) {
// Priority #2: If the user is a Dev/Admin, System, or Bot, auto-linkify their text.
// This also allows them to use other HTML like <b> tags.
finalMessage = linkifyForDevs(msg.text);
} else {
// For everyone else: Sanitize everything, escape HTML, and replace links with the Discord invite.
finalMessage = escapeHTML(filterProfanity(replaceLinksWithDiscord(msg.text)));
}
// 4. Assemble and render
const el = document.createElement('div');
const borderColor = (msg.uid === currentUid) ? state.menuColor : userColor;
const bgColor = (msg.uid === currentUid) ? hexToRgba(state.menuColor, 0.12) : 'rgba(255,255,255,0.04)';
el.style.cssText = `margin-bottom: 8px; word-break: break-word; background: ${bgColor}; padding: 8px 12px; border-radius: 6px; color: #ddd; font-family: inherit; font-size: 14px; line-height: 1.5; border-left: 3px solid ${borderColor};`;
const timestamp = new Date(msg.time).toLocaleTimeString([], { hour12: true, hour: '2-digit', minute: '2-digit' });
el.innerHTML = `<span style="color:#888; font-size:0.9em; margin-right:5px;">${timestamp}</span> <b>${nameHtml}${roleTagHTML}:</b> ${finalMessage}`;
chatBodyElement.appendChild(el);
if (shouldScroll || chatBodyElement.scrollTop >= chatBodyElement.scrollHeight - chatBodyElement.clientHeight - 150) {
chatBodyElement.scrollTop = chatBodyElement.scrollHeight;
}
}
// --- HELPER FUNCTION -
function linkifyForDevs(inputText) {
// This function finds URLs and wraps them in <a> tags. It does NOT escape other HTML.
const urlPattern = /(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig;
return inputText.replace(urlPattern, '<a href="$1" target="_blank" style="color:#87CEEB;text-decoration:underline;">$1</a>');
}
function createTrailOverlayCanvas() {
let overlay = document.getElementById('snake-trail-overlay');
if (overlay) {
overlay.style.display = 'block'; // <-- Always show overlay when trail is ON
return overlay;
}
const gameCanvas = document.querySelector('canvas');
if (!gameCanvas) return null;
overlay = document.createElement('canvas');
overlay.id = 'snake-trail-overlay';
overlay.style.position = 'fixed';
overlay.style.left = gameCanvas.style.left || '0px';
overlay.style.top = gameCanvas.style.top || '0px';
overlay.style.pointerEvents = 'none';
overlay.style.zIndex = '9000';
overlay.width = window.innerWidth; // Match window size as per your original
overlay.height = window.innerHeight; // Match window size
overlay.style.display = 'block'; // <-- Make sure it's visible when created
document.body.appendChild(overlay);
// Adjust overlay size on resize
window.addEventListener('resize', () => {
if (overlay) { // Check if overlay still exists
overlay.width = window.innerWidth;
overlay.height = window.innerHeight;
}
});
return overlay;
}
function toggleChatVisible() {
state.features.chatVisible = !state.features.chatVisible;
const chatContainer = document.getElementById('mod-menu-chat-container');
if (chatContainer) {
chatContainer.style.display = state.features.chatVisible ? 'flex' : 'none';
}
if (typeof updateMenu === "function") updateMenu(); // Ensure updateMenu is called
}
function addChatMessage(messageContent, isSystemMessage = false) {
// This function is largely for local messages if ever needed.
// Firebase handles the main chat display.
console.log("Local addChatMessage called (primarily for debug/local system messages):", messageContent);
// If you want to display these in the chat UI, you would add DOM manipulation here
// similar to renderChatMessage but without Firebase specific data.
}
function updateChatDisplay() {
// This function is effectively replaced by the real-time updates
// from Firebase handled by renderChatMessage.
// console.log("updateChatDisplay called (mostly deprecated).");
}
function makeDraggable(element, handle) {
handle.addEventListener('mousedown', function(e) {
// Check if the event target is the handle itself or a child that shouldn't prevent dragging (e.g. text in header)
// And ensure it's a left-click
if ((e.target.dataset.draggable === 'true' || handle.contains(e.target)) && e.button === 0) {
// Prevent dragging if mousedown on an interactive element within the handle (e.g., a button in chat header)
if (e.target.tagName === 'BUTTON' || e.target.tagName === 'INPUT' || e.target.tagName === 'A' || e.target.closest('button, input, a')) {
return;
}
e.preventDefault(); // Prevent text selection
state.draggingElement = element;
state.dragStartX = e.clientX;
state.dragStartY = e.clientY;
state.elementStartX = parseInt(element.style.left, 10) || 0; // Use current position
state.elementStartY = parseInt(element.style.top, 10) || 0;
}
});
}
function makeResizable(element, handle) {
handle.addEventListener('mousedown', function(e) {
if (e.target.dataset.resizable === 'true' && e.button === 0) {
e.preventDefault();
state.resizingElement = element;
state.dragStartX = e.clientX;
state.dragStartY = e.clientY;
state.elementStartWidth = parseInt(element.style.width, 10) || 300;
state.elementStartHeight = parseInt(element.style.height, 10) || 200;
}
});
}
// --- START: Slow Mode Indicator Logic ---
let slowModeInterval = null; // This will hold our timer
function updateSlowModeIndicator() {
const indicator = document.getElementById('slow-mode-indicator');
const chatInput = document.getElementById('mod-menu-chat-input');
if (!indicator || !chatInput) return; // Exit if elements don't exist
const now = Date.now();
const timeSinceLastMessage = now - lastChatMessageTime;
if (timeSinceLastMessage < chatCooldown && chatInput.value.length > 0) {
// If in cooldown and user is typing, show the timer
const timeLeft = Math.ceil((chatCooldown - timeSinceLastMessage) / 1000);
indicator.textContent = `Slow Mode: ${timeLeft}s`;
indicator.style.display = 'block';
chatInput.style.textIndent = '115px'; // Push the typed text to the right
} else {
// Otherwise, hide the timer
indicator.style.display = 'none';
chatInput.style.textIndent = '0px'; // Reset text position
if (slowModeInterval) {
clearInterval(slowModeInterval); // Stop the timer loop
slowModeInterval = null;
}
}
}
// --- END: Slow Mode Indicator Logic ---
// === NEW CENTRAL SCALING FUNCTION ===
function updateCombinedScale() {
const menu = document.getElementById('mod-menu');
const chat = document.getElementById('mod-menu-chat-container');
// Calculate the final scale needed to counteract browser zoom
const finalScale = state.uiScale / state.browserZoom;
if (menu) {
menu.style.transformOrigin = (menu.style.right && menu.style.right !== 'auto') ? 'top right' : 'top left';
menu.style.transform = `scale(${finalScale})`;
}
if (chat) {
chat.style.transformOrigin = 'top left';
chat.style.transform = `scale(${finalScale})`;
}
}
function applyUIScale() {
// This function now only calls the central handler.
updateCombinedScale();
// We still need to scale the server box separately as it's not a fixed element.
const serverBox = document.getElementById('custom-server-box');
if (serverBox) {
serverBox.style.transformOrigin = 'center top';
serverBox.style.transform = `scale(${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(46, 46, 52, 0.95)'; // --menu-bg with opacity
menu.style.border = `1px solid ${hexToRgba(state.menuColor, 0.5)}`;
menu.style.borderRadius = '12px';
menu.style.zIndex = '9999';
menu.style.color = '#e0e0e0'; // --text-color
menu.style.fontFamily = "'Segoe UI', Arial, sans-serif"; // --font-family
menu.style.fontSize = '14px';
menu.style.boxShadow = '0 8px 32px rgba(0,0,0,0.35)'; // Softer, larger shadow
menu.style.backdropFilter = 'blur(10px)';
menu.style.webkitBackdropFilter = 'blur(10px)'; // For Safari
menu.style.transition = 'border-color 0.3s, box-shadow 0.3s';
menu.style.userSelect = "none";
menu.style.overflow = 'hidden';
document.body.appendChild(menu);
// Persistent Draggable Header for Main Menu
const menuDraggableHeader = document.createElement('div');
menuDraggableHeader.id = 'mod-menu-draggable-header';
menuDraggableHeader.dataset.draggable = 'true'; // For makeDraggable
// Styles for this header will be set in updateMenu to react to state.menuColor and state.menuName
menu.appendChild(menuDraggableHeader);
// Persistent Content Area for Main Menu
const menuContentArea = document.createElement('div');
menuContentArea.id = 'mod-menu-content-area';
menuContentArea.style.padding = '0 20px 15px 20px'; // Padding for content below header
menuContentArea.style.maxHeight = 'calc(90vh - 80px)'; // Max height with some margin
menuContentArea.style.overflowY = 'auto'; // Scrollable content
menuContentArea.style.overflowX = 'hidden';
// Custom scrollbar for content area
menuContentArea.style.scrollbarWidth = 'thin';
menuContentArea.style.scrollbarColor = `${state.menuColor} rgba(0,0,0,0.2)`;
menu.appendChild(menuContentArea);
makeDraggable(menu, menuDraggableHeader); // Initialize dragging
// --- START: Add Resize Handle for Main Menu ---
const menuResizeHandle = document.createElement('div');
menuResizeHandle.id = 'mod-menu-resize-handle';
menuResizeHandle.style.cssText = `
position: absolute;
right: 0;
bottom: 0;
width: 18px; /* A good size for grabbing */
height: 18px;
cursor: nwse-resize; /* The diagonal resize cursor */
z-index: 10000; /* Ensures it's on top of menu content */
opacity: 0.6;
transition: opacity 0.2s, border-color 0.3s;
`;
menuResizeHandle.dataset.resizable = 'true';
menuResizeHandle.onmouseenter = () => { menuResizeHandle.style.opacity = '1'; };
menuResizeHandle.onmouseleave = () => { menuResizeHandle.style.opacity = '0.6'; };
menu.appendChild(menuResizeHandle);
// Now, connect the handle to the resizing logic function
makeResizable(menu, menuResizeHandle);
// --- END: Add Resize Handle ---
// (modal injection):
// REPLACE the old block with this NEW, FIXED block
// (modal injection):
if (!document.getElementById('keybind-modal-overlay')) {
const modalHTML = `
<div id="keybind-modal-overlay" style="display:none; position:fixed; top:0; left:0; width:100vw; height:100vh; z-index:10002; background:rgba(0,0,0,0.75); align-items:center; justify-content:center; font-family: 'Segoe UI', Arial, sans-serif;">
<div id="keybind-modal" style="background:#2E2E34; border-radius:10px; padding:30px 35px; box-shadow:0 6px 25px rgba(0,0,0,0.4); display:flex; flex-direction:column; align-items:center; min-width:320px; border: 1px solid rgba(255,255,255,0.1);">
<div style="color:#fff; font-size:1.4em; font-weight:600; margin-bottom:12px;">Rebind Key</div>
<div id="keybind-modal-action" style="color:#b0b0b0; font-size:1.15em; margin-bottom:18px;">Action: Placeholder</div>
<div style="color:#fff; font-size:1.1em; margin-bottom:24px;">Press a key to assign... <br><small>(Or scroll mouse wheel)</small></div>
<button id="keybind-modal-cancel" style="background:#555; color:#fff; border:none; padding:9px 25px; border-radius:5px; font-size:0.95em; cursor:pointer; transition: background-color 0.2s;">Cancel</button>
</div>
</div>
`;
const tempDiv = document.createElement('div');
tempDiv.innerHTML = modalHTML;
document.body.appendChild(tempDiv.firstElementChild);
// Attach the click listener for the cancel button RIGHT HERE <<< FIX
// This guarantees the button exists before we try to find it.
const cancelBtn = document.getElementById('keybind-modal-cancel');
if (cancelBtn) {
// We can reuse the existing `closeKeybindModal` function. <<< FIX
// It already does everything we need (hides the modal and resets the state).
cancelBtn.onclick = closeKeybindModal;
}
}
// --- 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-themes-btn" class="profile-modal-button" style="background: #673AB7; margin-right: auto;">🎨 Themes</button> <!-- NEW BUTTON -->
<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.
}
// --- NEW: PROFILE THEMES MODAL ---
if (!document.getElementById('themes-modal-overlay')) {
const themesModalStyle = document.createElement('style');
themesModalStyle.textContent = `
.themes-modal-overlay {
display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.7); z-index: 10004; align-items: center; justify-content: center;
}
.themes-modal-content {
background: #2E2E34; border: 1px solid var(--menu-color, #4CAF50); border-radius: 10px;
padding: 20px; width: 500px; max-height: 70vh; display: flex; flex-direction: column;
}
.themes-grid {
display: grid; grid-template-columns: 1fr 1fr; gap: 15px; overflow-y: auto; padding: 10px;
}
.theme-preview {
border: 2px solid #555; border-radius: 8px; padding: 15px; cursor: pointer;
transition: all 0.2s ease; text-align: center;
}
.theme-preview:hover {
border-color: var(--menu-color, #4CAF50);
transform: translateY(-3px);
}
.theme-preview.selected {
border-color: #FFD700;
box-shadow: 0 0 10px #FFD700;
}
`;
document.head.appendChild(themesModalStyle);
const themesModalHTML = `
<div id="themes-modal-overlay" class="themes-modal-overlay">
<div class="themes-modal-content">
<h2 style="text-align:center; color:var(--menu-color, #4CAF50); margin-top:0;">Select a Profile Theme</h2>
<div class="themes-grid">
<!-- Theme options will be added here by the script -->
</div>
<button id="close-themes-modal" style="margin-top: 20px; padding: 10px; border-radius: 5px; border: none; background: #555; color: #fff; cursor: pointer;">Close</button>
</div>
</div>
`;
const tempDiv = document.createElement('div');
tempDiv.innerHTML = themesModalHTML;
document.body.appendChild(tempDiv.firstElementChild);
}
// --- NEW: Send REP Modal ---
if (!document.getElementById('send-rep-modal-overlay')) {
const repModal = document.createElement('div');
repModal.id = 'send-rep-modal-overlay';
repModal.style = `
display:none; position:fixed; top:0; left:0; width:100vw; height:100vh;
z-index:10006; background:rgba(0,0,0,0.7); align-items:center; justify-content:center; font-family: 'Segoe UI', Arial, sans-serif;`;
repModal.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 id="send-rep-title" style="color:#fff;font-size:1.3em;font-weight:600;margin-bottom:20px;">Send REP to Player</div>
<div style="margin-bottom:15px; display:flex; align-items:center; gap: 8px;">
<input id="send-rep-amount" type="number" min="1" value="1" style="width:100px;padding:8px 10px;border-radius:5px;border:1px solid #555;background:#222;color:#eee;font-size:1.1em;text-align:center;">
<span style="color:#fff; font-size:1.1em; font-weight:bold;">REP</span>
</div>
<div id="send-rep-status" style="color:#ffc107; height: 20px; margin-bottom: 10px;"></div>
<div style="display:flex;gap:10px; margin-top:10px;">
<button id="send-rep-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="send-rep-confirm-btn" style="padding:9px 20px;border-radius:5px;background:#4CAF50;color:#fff;border:none;cursor:pointer; font-size: 0.95em; transition: background 0.2s;">Confirm & Send</button>
</div>
</div>
`;
document.body.appendChild(repModal);
}
// --- 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}`);
}
};
}
// --- START: FINAL GAME SUITE MODAL (with Blackjack) ---
if (!document.getElementById('game-modal-overlay')) {
const gameModalHTML = `
<div id="game-modal-overlay" style="display:none; position:fixed; top:0; left:0; width:100vw; height:100vh; z-index:10010; background:rgba(0,0,0,0.85); align-items:center; justify-content:center; font-family: 'Segoe UI', Arial, sans-serif;">
<div id="game-modal-content" style="background:#23232a; border-radius:12px; box-shadow:0 8px 30px rgba(0,0,0,0.5); display:flex; flex-direction:column; min-width:600px; max-width: 90vw; border: 1px solid var(--menu-color, #4CAF50); overflow:hidden;">
<!-- Game Tabs -->
<div id="game-tabs" style="display:flex; background:rgba(0,0,0,0.2);">
<button class="game-tab-btn active" data-gametab="spinner">💎 Daily Spinner</button>
<button class="game-tab-btn" data-gametab="slots">🎰 Slot Machine</button>
<button class="game-tab-btn" data-gametab="roulette">🎲 Roulette</button>
<button class="game-tab-btn" data-gametab="blackjack">🃏 Blackjack</button>
</div>
<div id="game-content-area" style="padding:25px 35px; max-height: 80vh; overflow-y: auto;">
<div id="player-rep-display" style="color: #ddd; margin-bottom: 20px; font-size: 1.2em; text-align:center; font-weight:bold; background: rgba(0,0,0,0.2); padding: 8px; border-radius: 6px;">Your REP: Loading...</div>
<!-- Spinner Game -->
<div id="game-spinner" class="game-container" style="display:flex; flex-direction:column; align-items:center;"></div>
<!-- Slot Machine Game -->
<div id="game-slots" class="game-container" style="display:none; flex-direction:column; align-items:center;"></div>
<!-- Roulette Game -->
<div id="game-roulette" class="game-container" style="display:none; flex-direction:column; align-items:center;"></div>
<!-- Blackjack Game -->
<div id="game-blackjack" class="game-container" style="display:none; flex-direction:column; align-items:center; width:100%;">
<h2 style="color:var(--menu-color, #4CAF50); margin:0 0 15px 0;">Blackjack</h2>
<div id="blackjack-table" style="width:100%;">
<!-- Dealer's Hand -->
<div class="blackjack-hand-area">
<h3 class="blackjack-hand-title">Dealer's Hand (<span id="blackjack-dealer-score">0</span>)</h3>
<div id="blackjack-dealer-cards" class="blackjack-cards"></div>
</div>
<!-- Player's Hand -->
<div class="blackjack-hand-area">
<h3 class="blackjack-hand-title">Your Hand (<span id="blackjack-player-score">0</span>)</h3>
<div id="blackjack-player-cards" class="blackjack-cards"></div>
</div>
</div>
<div id="blackjack-result" class="game-result-text" style="font-size:1.5em;"></div>
<!-- Bet Controls -->
<div id="blackjack-bet-controls" style="margin:15px 0; display:flex; flex-direction:column; align-items:center; gap:10px;">
<div>
<label for="blackjack-bet-amount" style="color:#ccc; margin-right:10px; font-weight:bold;">BET AMOUNT:</label>
<input id="blackjack-bet-amount" type="number" min="1" value="10" style="width:100px; text-align:center; font-size:1.1em; padding: 8px; border-radius:5px; border:1px solid #555; background:#222; color:#eee;">
</div>
<button id="blackjack-deal-btn" class="mod-menu-button" style="font-size: 1.1em; padding: 10px 20px; background-color: #27ae60;">Deal Cards</button>
</div>
<!-- Action Controls -->
<div id="blackjack-action-controls" style="display:none; gap: 15px; margin-top:15px;">
<button id="blackjack-hit-btn" class="mod-menu-button" style="font-size: 1.1em; padding: 10px 20px;">Hit</button>
<button id="blackjack-stand-btn" class="mod-menu-button" style="font-size: 1.1em; padding: 10px 20px; background-color:#c0392b;">Stand</button>
</div>
</div>
</div>
</div>
</div>
`;
const tempDiv = document.createElement('div');
tempDiv.innerHTML = gameModalHTML;
document.body.appendChild(tempDiv.firstElementChild);
// Copy original game content into the new structure
document.getElementById('game-spinner').innerHTML = `<h2 style="color:var(--menu-color, #4CAF50); margin:0 0 20px 0;">Daily REP Spinner</h2><div id="spinner-display" style="font-size: 4em; font-weight: bold; color: #fff; background: #111; padding: 20px 40px; border-radius: 10px; margin-bottom: 20px; min-width: 150px; text-align: center;">0</div><div id="spinner-result" class="game-result-text"></div><button id="spin-button" class="mod-menu-button" style="font-size: 1.2em; padding: 12px 25px;">Spin for REP!</button><div id="spin-timer" style="color: #aaa; margin-top: 15px;"></div>`;
// ** NEW 3x3 SLOT MACHINE HTML **
document.getElementById('game-slots').innerHTML = `
<h2 style="color:var(--menu-color, #4CAF50); margin:0 0 20px 0;">3x3 REP Slot Machine</h2>
<div id="slot-reels" style="display:grid; grid-template-columns: repeat(3, 1fr); gap:15px; background:#111; padding: 20px; border-radius:10px; margin-bottom:20px; border: 2px solid #555;">
<div class="slot-reel">💎</div><div class="slot-reel">💎</div><div class="slot-reel">💎</div>
<div class="slot-reel">7️⃣</div><div class="slot-reel">7️⃣</div><div class="slot-reel">7️⃣</div>
<div class="slot-reel">🍋</div><div class="slot-reel">🍋</div><div class="slot-reel">🍋</div>
</div>
<div id="slot-result" class="game-result-text"></div>
<div style="display:flex; flex-direction:column; align-items:center; gap:10px;">
<div>
<label for="slot-bet-amount" style="color:#ccc; margin-right:10px; font-weight:bold;">BET AMOUNT:</label>
<input id="slot-bet-amount" type="number" min="1" value="5" style="width:100px; text-align:center; font-size:1.1em; padding: 8px; border-radius:5px; border:1px solid #555; background:#222; color:#eee;">
</div>
<button id="slot-spin-btn" class="mod-menu-button" style="font-size: 1.2em; padding: 12px 25px;">Spin!</button>
</div>
`;
document.getElementById('game-roulette').innerHTML = `<h2 style="color:var(--menu-color, #4CAF50); margin:0 0 15px 0;">REP Roulette</h2><div id="roulette-wheel-container" style="position:relative; width:200px; height:200px; margin-bottom:15px;"><div id="roulette-wheel"></div><div id="roulette-ball-marker" style="position: absolute; top: -5px; left: 50%; transform: translateX(-50%); width: 0; height: 0; border-left: 10px solid transparent; border-right: 10px solid transparent; border-top: 15px solid white;"></div></div><div id="roulette-result" class="game-result-text"></div><div id="roulette-bet-controls" style="width:100%; text-align:center;"><div style="margin-bottom:15px;"><label for="roulette-bet-amount" style="color:#ccc; margin-right:10px; font-weight:bold;">BET AMOUNT:</label><input id="roulette-bet-amount" type="number" min="1" value="10" style="width:100px; text-align:center; font-size:1.1em; padding: 8px; border-radius:5px; border:1px solid #555; background:#222; color:#eee;"></div><div id="roulette-bets"><button class="roulette-bet-btn" data-bet="red" style="background:#c0392b; color:white;">Red (2x)</button><button class="roulette-bet-btn" data-bet="black" style="background:#2c3e50; color:white;">Black (2x)</button><button class="roulette-bet-btn" data-bet="green" style="background:#27ae60; color:white;">Green (14x)</button></div></div>`;
const style = document.createElement('style');
style.textContent = `
.slot-reel.win-line {
background-color: #28a745; /* Green background for winning reels */
box-shadow: 0 0 15px #28a745;
transform: scale(1.1);
transition: all 0.2s ease-in-out;
}
.game-tab-btn { flex: 1; background: transparent; border: none; color: #aaa; font-size: 1.1em; padding: 15px; cursor: pointer; transition: all 0.2s; border-bottom: 3px solid transparent; }
.game-tab-btn:hover { background: rgba(255,255,255,0.05); color: #fff; }
.game-tab-btn.active { color: var(--menu-color, #4CAF50); border-bottom-color: var(--menu-color, #4CAF50); }
.game-result-text { color: #FFD700; font-size: 1.2em; height: 25px; margin: 15px 0; font-weight: bold; text-align:center; }
.slot-reel { font-size: 4em; width: 80px; height: 100px; line-height: 100px; text-align:center; background: #2a2a2e; color: #fff; border-radius: 8px; text-shadow: 0 0 5px #000; }
#roulette-wheel { width: 200px; height: 200px; border-radius: 50%; background-image: conic-gradient(green 0 9.7deg, red 9.7deg 19.4deg, black 19.4deg 29.1deg, red 29.1deg 38.8deg, black 38.8deg 48.5deg, red 48.5deg 58.2deg, black 58.2deg 67.9deg, red 67.9deg 77.6deg, black 77.6deg 87.3deg, red 87.3deg 97deg, black 97deg 106.7deg, red 106.7deg 116.4deg, black 116.4deg 126.1deg, red 126.1deg 135.8deg, black 135.8deg 145.5deg, red 145.5deg 155.2deg, black 155.2deg 164.9deg, red 164.9deg 174.6deg, black 174.6deg 184.3deg, red 184.3deg 194deg, black 194deg 203.7deg, red 203.7deg 213.4deg, black 213.4deg 223.1deg, red 223.1deg 232.8deg, black 232.8deg 242.5deg, red 242.5deg 252.2deg, black 252.2deg 261.9deg, red 261.9deg 271.6deg, black 271.6deg 281.3deg, red 281.3deg 291deg, black 291deg 300.7deg, red 300.7deg 310.4deg, black 310.4deg 320.1deg, red 320.1deg 329.8deg, black 329.8deg 339.5deg, red 339.5deg 349.2deg, black 349.2deg 360deg); border: 5px solid #8c6432; transition: transform 4s cubic-bezier(0.2, 0.8, 0.7, 1); }
.roulette-bet-btn { padding: 10px 15px; font-size: 1em; margin: 0 5px; cursor: pointer; border: 2px solid #555; color: #fff; border-radius: 5px; font-weight: bold; }
.roulette-bet-btn.selected { border-color: #FFD700; box-shadow: 0 0 10px #FFD700; transform: scale(1.05); }
.blackjack-hand-area { margin-bottom: 20px; background: rgba(0,0,0,0.2); border-radius: 8px; padding: 15px; }
.blackjack-hand-title { margin: 0 0 10px 0; color: #ccc; border-bottom: 1px solid #444; padding-bottom: 8px; }
.blackjack-cards { display: flex; gap: 10px; min-height: 105px; }
.blackjack-card { width: 70px; height: 100px; border: 2px solid #999; border-radius: 8px; background: white; color: black; display: flex; flex-direction: column; justify-content: space-between; padding: 5px; font-size: 1.5em; font-weight: bold; }
.blackjack-card.red { color: #c0392b; }
.blackjack-card-hidden { background: repeating-linear-gradient(45deg, #555, #555 10px, #444 10px, #444 20px); }
`;
document.head.appendChild(style);
}
// --- END: FINAL GAME SUITE MODAL ---
// --- 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:20px; width:450px; 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:var(--menu-color, #4CAF50); margin-top:0; margin-bottom: 15px; text-align:center; padding-bottom: 10px; border-bottom: 1px solid #444;">Leaderboard</h2>
<!-- NEW TAB BUTTONS -->
<div style="display: flex; border-bottom: 1px solid #444; margin-bottom: 10px;">
<button id="tab-btn-rep" class="leaderboard-tab active">🏆 Top 100 REP</button>
<button id="tab-btn-recent" class="leaderboard-tab">🕓 Recent Players</button>
</div>
<!-- Content Area for REP leaderboard -->
<div id="rep-leaderboard-content" style="display:block; overflow-y: auto; padding-right: 10px;"></div>
<!-- NEW Content Area for Recent Players -->
<div id="recent-players-content" style="display:none; 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
})();
// === NEW BROWSER ZOOM DETECTOR ===
function detectBrowserZoom() {
// window.devicePixelRatio is the most reliable way to track browser zoom.
state.browserZoom = window.devicePixelRatio;
// Now, update the elements' scale to counteract the new browser zoom level.
updateCombinedScale();
}
// --- 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;">BETA 5.2.2 - VX19</div>
`;
}
// Update main menu border and shadow
menu.style.border = `1px solid ${hexToRgba(menuColor, 0.6)}`;
menu.style.boxShadow = `0 6px 25px ${hexToRgba(menuColor, 0.15)}`;
circleVisual.style.border = `2px dashed ${hexToRgba(menuColor, 0.7)}`;
circleVisual.style.boxShadow = `0 0 12px ${hexToRgba(menuColor, 0.3)}, inset 0 0 8px ${hexToRgba(menuColor, 0.2)}`;
// --- START: Update Menu Resize Handle Style ---
const menuResizeHandle = document.getElementById('mod-menu-resize-handle');
if (menuResizeHandle) {
// Creates a visual triangle in the corner using borders
menuResizeHandle.style.borderRight = `2px solid ${hexToRgba(menuColor, 0.8)}`;
menuResizeHandle.style.borderBottom = `2px solid ${hexToRgba(menuColor, 0.8)}`;
}
// --- END: Update Menu Resize Handle Style ---
// Update scrollbar color for content area
if (menuContentArea) {
menuContentArea.style.scrollbarColor = `${menuColor} rgba(0,0,0,0.2)`;
}
const arrow = state.showCustomization ? '▼' : '▶';
const inputStyle = `padding:8px 10px; border-radius:5px; border:1px solid #454548; background-color:#2c2c30; color:#e0e0e0; font-size:14px; box-sizing:border-box; transition: border-color 0.2s, box-shadow 0.2s;`;
const focusStyle = `this.style.borderColor='${menuColor}'; this.style.boxShadow='0 0 0 2px ${hexToRgba(menuColor, 0.3)}';`;
const blurStyle = `this.style.borderColor='#454548'; this.style.boxShadow='none';`;
const buttonStyle = (bgColor = menuColor, textColor = '#fff') =>
`padding:8px 15px; border-radius:6px; border:none; color:${textColor}; font-size:14px; font-weight:500; cursor:pointer; transition:background-color 0.2s, box-shadow 0.2s; background-color:${bgColor};`;
const buttonHoverStyle = (bgColor = menuColor) => `this.style.backgroundColor='${adjustColor(bgColor, -15)}'; this.style.boxShadow='0 2px 8px rgba(0,0,0,0.15)';`;
const buttonLeaveStyle = (bgColor = menuColor) => `this.style.backgroundColor='${bgColor}'; this.style.boxShadow='none';`;
if (state.simplified) {
menu.style.width = state.uiLayout.menu.width !== null ? `${state.uiLayout.menu.width}px` : '340px'; // Slightly wider simplified menu
menuContentArea.innerHTML = `
<div style="display:flex; justify-content:flex-end; margin-bottom:10px;">
<button id="default-menu-btn" title="Expand menu" style="${buttonStyle(menuColor)}; padding: 6px 12px; font-size: 13px;"
onmouseover="${buttonHoverStyle(menuColor)}" onmouseout="${buttonLeaveStyle(menuColor)}">
Full Menu
</button>
</div>
<div style="background:${hexToRgba(menuColor,0.08)}; padding:12px 15px; border-radius:8px; margin-bottom:15px; border: 1px solid ${hexToRgba(menuColor,0.2)};">
<div style="font-size:1.1em; margin-bottom:10px; color:${menuColor}; font-weight:600; text-align:center; padding-bottom:8px; border-bottom: 1px solid ${hexToRgba(menuColor,0.2)};">
Quick Status
</div>
<div style="display: grid; grid-template-columns: auto 1fr; gap: 8px 15px; font-size:14px; line-height:1.8;">
<span><b>Perf Mode:</b></span> <span style="color:#87CEEB; text-align:right;">Low (Optimized)</span>
<span><b>Zoom:</b></span> <span style="text-align:right;">${Math.round(100 / state.zoomFactor)}%</span>
<span><b>FPS:</b></span> <span style="color:#90EE90; text-align:right;">${state.fps}</span>
<span><b>Server:</b></span> <span style="color:#FFD700; text-align:right;">${state.features.showServer ? (state.server || 'N/A') : 'Hidden'}</span>
<span><b>Chat:</b></span> <span style="color:${state.features.chatVisible ? '#90EE90' : '#FF7F7F'}; text-align:right;">${state.features.chatVisible ? 'ON' : 'OFF'}</span>
<span><b>Keybinds:</b></span> <span style="color:${state.features.keybindsEnabled ? '#90EE90' : '#FF7F7F'}; text-align:right;">${state.features.keybindsEnabled ? 'ON' : 'OFF'}</span>
<span><b>Ping:</b></span> <span id="ping-value-simplified" style="color:#FFD700; text-align:right;">${state.ping} ms</span>
</div>
</div>
<div style="text-align:center; font-size:12px; color:#888; margin-top:15px; padding-top:10px; border-top:1px solid #444; line-height:1.6;">
Press <strong>${state.keybinds.toggleMenu.toUpperCase()}</strong> to toggle menu
</div>
`;
// Logic for simplified menu button
setTimeout(() => {
const btn = document.getElementById('default-menu-btn');
if (btn) {
btn.onclick = () => {
state.simplified = false;
sessionStorage.setItem('modMenuSimplified', 'false');
state.features.performanceMode = parseInt(localStorage.getItem('prevPerformanceMode')) || 2; // Restore
applyPerformanceMode();
updateMenu();
};
}
// Update ping display in simplified menu
const pingValueDisplay = document.getElementById("ping-value-simplified");
if (pingValueDisplay) pingValueDisplay.textContent = `${state.ping} ms`;
}, 0);
if (state.features.performanceMode !== 1) {
state.features.performanceMode = 1;
applyPerformanceMode();
}
return; // End simplified menu update
}
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
}
// 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>
<button id="open-games-menu-btn" style="${buttonStyle('#9C27B0')}; padding: 6px 12px; font-size: 13px;" onmouseover="${buttonHoverStyle('#9C27B0')}" onmouseout="${buttonLeaveStyle('#9C27B0')}">🎮 Mini Games</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>
<div id="movement-toggle" style="cursor:pointer; user-select:none; 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;">
${state.showMovement ? '▼' : '▶'} Movement
</div>
<div id="movement-section" style="display:${state.showMovement ? 'block' : 'none'};">
<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>
</div> <!-- ADD THIS CLOSING DIV -->
<div id="zoom-toggle" style="cursor:pointer; user-select:none; 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;">
${state.showZoom ? '▼' : '▶'} Zoom
</div>
<div id="zoom-section" style="display:${state.showZoom ? 'block' : 'none'};">
${!isDamnBruh ? `
<!-- These will only show on Slither.io -->
<p><strong>${state.keybinds.zoomIn.toUpperCase()}: Zoom In</strong></p>
<p><strong>${state.keybinds.zoomOut.toUpperCase()}: Zoom Out</strong></p>
<p><strong>${state.keybinds.zoomReset.toUpperCase()}: Reset Zoom</strong></p>
` : `
<!-- This will only show on Damnbruh.com -->
<p id="damnbruh-zoom-toggle" style="cursor:pointer;">
<strong>Canvas Zoom (Scroll):</strong>
<span style="color:${state.features.damnbruhZoom ? '#90EE90' : '#FF7F7F'}; float:right;">
${state.features.damnbruhZoom ? 'ON' : 'OFF'}
</span>
</p>
<!-- THE NEW TIP TEXT -->
<div style="font-size: 12px; color: #aaa; margin-top: 10px; padding-top: 10px; border-top: 1px solid rgba(255,255,255,0.1);">
<strong>Tip:</strong> For better page zoom, hold <strong>Ctrl</strong> and use your <strong>Mouse Wheel</strong>.
</div>
`}
</div>
<div id="utilities-toggle" style="cursor:pointer; user-select:none; 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;">
${state.showUtilities ? '▼' : '▶'} Utilities
</div>
<div id="utilities-section" style="display:${state.showUtilities ? 'block' : 'none'};">
<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>
<div>
<div id="visuals-toggle" style="cursor:pointer; user-select:none; 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;">
${state.showVisuals ? '▼' : '▶'} Visuals & Audio
</div>
<div id="visuals-section" style="display:${state.showVisuals ? 'block' : 'none'};">
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:12px;">
<strong>Visual Filter:</strong>
<select id="visual-mode-select" style="padding: 6px; border-radius: 5px; background: #2c2c30; color: #e0e0e0; border: 1px solid #454548;">
<option value="none">None</option>
<option value="invert">Invert</option>
<option value="contrast">High Contrast</option>
<option value="grayscale">Grayscale</option>
<option value="sepia">Sepia</option>
</select>
</div>
<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 -->
</div>
<div id="links-toggle" style="cursor:pointer; user-select:none; 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;">
${state.showLinks ? '▼' : '▶'} Links
</div>
<div id="links-section" style="display:${state.showLinks ? 'block' : 'none'};">
<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()}: Soul Collector</strong> <span style="float:right; opacity:0.7;">🔗</span></p>
<p><strong>${(state.keybinds.spotify || 'n').toUpperCase()}: DXXTHLY Spotify</strong> <span style="float:right; opacity:0.7;">🔗</span></p>
</div>
</div>
</div>
<div id="status-toggle" style="background:${hexToRgba(menuColor,0.08)}; color:${menuColor}; font-weight:bold; cursor:pointer; user-select:none; padding:10px 15px; border-radius:8px; margin-bottom:0px; border: 1px solid ${hexToRgba(menuColor,0.2)};">
${state.showStatus ? '▼' : '▶'} Status & Extras
</div>
<div id="status-section" style="display:${state.showStatus ? 'block' : 'none'}; background:${hexToRgba(menuColor,0.08)}; padding:15px; border-radius:8px; margin-bottom:15px; border: 1px solid ${hexToRgba(menuColor,0.2)}; border-top:none; border-top-left-radius:0; border-top-right-radius:0;">
<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();
};
}
// --- Our New Click Handlers ---
const movementToggle = document.getElementById('movement-toggle');
if (movementToggle) {
movementToggle.onclick = () => {
state.showMovement = !state.showMovement;
updateMenu();
};
}
const zoomToggle = document.getElementById('zoom-toggle');
if (zoomToggle) {
zoomToggle.onclick = () => {
state.showZoom = !state.showZoom;
updateMenu();
};
}
const utilitiesToggle = document.getElementById('utilities-toggle');
if (utilitiesToggle) {
utilitiesToggle.onclick = () => {
state.showUtilities = !state.showUtilities;
updateMenu();
};
}
const visualsToggle = document.getElementById('visuals-toggle');
if (visualsToggle) {
visualsToggle.onclick = () => {
state.showVisuals = !state.showVisuals;
updateMenu();
};
}
const linksToggle = document.getElementById('links-toggle');
if (linksToggle) {
linksToggle.onclick = () => {
state.showLinks = !state.showLinks;
updateMenu();
};
}
const statusToggle = document.getElementById('status-toggle');
if (statusToggle) {
statusToggle.onclick = () => {
state.showStatus = !state.showStatus;
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
// --- NEW: Visual Filter Dropdown Logic ---
const visualModeSelect = document.getElementById('visual-mode-select');
if (visualModeSelect) {
// Set the dropdown to show the currently selected mode
visualModeSelect.value = state.features.visualMode;
// When the user changes the dropdown...
visualModeSelect.onchange = () => {
// ...update the state...
state.features.visualMode = visualModeSelect.value;
// ...and apply the new filter.
applyVisualFilter();
};
}
// 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) {
repLeaderboardBtn.onclick = () => {
const modal = document.getElementById('rep-leaderboard-modal');
if (!modal) return;
modal.style.display = 'flex';
const repContent = document.getElementById('rep-leaderboard-content');
const recentContent = document.getElementById('recent-players-content');
const tabRep = document.getElementById('tab-btn-rep');
const tabRecent = document.getElementById('tab-btn-recent');
// --- NEW HELPER FUNCTION: Format Time Ago ---
function formatTimeAgo(timestamp) {
const now = Date.now();
const seconds = Math.floor((now - timestamp) / 1000);
if (seconds < 60) return `<span style="color:#90EE90; font-weight:bold;">Active now</span>`;
const minutes = Math.floor(seconds / 60);
if (minutes < 60) return `Last seen: ${minutes} min${minutes > 1 ? 's' : ''} ago`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `Last seen: ${hours} hour${hours > 1 ? 's' : ''} ago`;
const days = Math.floor(hours / 24);
return `Last seen: ${days} day${days > 1 ? 's' : ''} ago`;
}
// --- Helper Function to Render Player Lists (NOW with 'isRecent' flag) ---
const renderPlayerList = async (playerDataArray, isRecent = false) => {
const userPromises = playerDataArray.map(player => firebase.database().ref(`onlineUsers/${player.uid}`).once('value'));
const userSnapshots = await Promise.all(userPromises);
return playerDataArray.map((player, i) => {
const userProfile = userSnapshots[i].exists() ? userSnapshots[i].val() : { name: 'Offline User', profileAvatar: null };
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 && !isRecent) { 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 && !isRecent) { 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 && !isRecent) { 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 = escapeHTML(filterProfanity(userProfile.name || 'Offline User'));
if (userProfile.name === 'Offline User') { displayName = `<i style="color:#999;">${displayName}</i>`; }
if (isDev(player.uid)) { displayName = rainbowTextStyle(displayName); }
else if (isVip(player.uid, userProfile.name)) { displayName = vipGlowStyle(displayName, '#FFD700'); }
// --- NEW: Display either REP or Last Seen status ---
const subtext = isRecent
? formatTimeAgo(player.lastActive)
: `<b style="color:var(--menu-color, #4CAF50); font-weight:900;">${player.rep.toLocaleString()}</b> REP`;
return `<div class="leaderboard-row leaderboard-clickable-row" data-uid="${player.uid}" style="${rowStyle}">
<div class="leaderboard-rank" style="${rankStyle}">${isRecent ? '' : '#'}${rank}</div>
<img class="leaderboard-avatar" src="${userProfile.profileAvatar || `https://api.dicebear.com/7.x/identicon/svg?seed=${encodeURIComponent(player.uid)}`}" onerror="this.src='https://api.dicebear.com/7.x/identicon/svg?seed=${encodeURIComponent(player.uid)}';">
<div class="leaderboard-info">
<div class="leaderboard-name">${displayName}</div>
<div class="leaderboard-rep">${subtext}</div>
</div>
</div>`;
}).join('');
};
// --- Function to Load Top 100 REP (Unchanged) ---
const loadRepLeaderboard = async () => {
repContent.innerHTML = '<div style="color:#aaa;text-align:center;">Loading Top 100...</div>';
try {
const repSnapshot = await firebase.database().ref('playerData').orderByChild('rep').limitToLast(100).once('value');
if (!repSnapshot.exists()) {
repContent.innerHTML = '<div style="color:#aaa;text-align:center;">No players with REP found.</div>';
return;
}
const players = [];
repSnapshot.forEach(child => {
players.push({ uid: child.key, rep: child.val().rep || 0 });
});
players.sort((a, b) => b.rep - a.rep);
const filteredPlayers = players.filter(p => !isDev(p.uid) && !isSystemAccount(p.uid));
repContent.innerHTML = await renderPlayerList(filteredPlayers, false);
} catch (err) {
console.error("Error loading rep leaderboard:", err);
repContent.innerHTML = `<div style="color:#f77;text-align:center;">Error loading leaderboard.</div>`;
}
};
// --- Function to Load Recent Players (NOW passes 'true' to renderer) ---
const loadRecentPlayers = async () => {
recentContent.innerHTML = '<div style="color:#aaa;text-align:center;">Loading recent players...</div>';
try {
const recentSnapshot = await firebase.database().ref('onlineUsers').orderByChild('lastActive').limitToLast(100).once('value');
if (!recentSnapshot.exists()) {
recentContent.innerHTML = '<div style="color:#aaa;text-align:center;">No recent players found.</div>';
return;
}
const players = [];
recentSnapshot.forEach(child => {
players.push({ uid: child.key, rep: 0, lastActive: child.val().lastActive || 0 });
});
players.sort((a, b) => b.lastActive - a.lastActive);
const filteredPlayers = players.filter(p => !isSystemAccount(p.uid));
recentContent.innerHTML = await renderPlayerList(filteredPlayers, true); // Pass 'true' here
} catch (err) {
console.error("Error loading recent players:", err);
recentContent.innerHTML = `<div style="color:#f77;text-align:center;">Error loading recent players.</div>`;
}
};
// --- Tab Switching Logic (Unchanged) ---
tabRep.onclick = () => {
tabRep.classList.add('active');
tabRecent.classList.remove('active');
repContent.style.display = 'block';
recentContent.style.display = 'none';
loadRepLeaderboard();
};
tabRecent.onclick = () => {
tabRecent.classList.add('active');
tabRep.classList.remove('active');
recentContent.style.display = 'block';
repContent.style.display = 'none';
loadRecentPlayers();
};
// Load the default tab (Top 100 REP) when the modal opens
tabRep.click();
};
}
// 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");
}
if (isDamnBruh) {
const dbZoomToggle = document.getElementById('damnbruh-zoom-toggle');
if (dbZoomToggle) {
dbZoomToggle.onclick = () => {
state.features.damnbruhZoom = !state.features.damnbruhZoom;
updateMenu();
};
}
}
}, 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.spotify:
window.open("https://spti.fi/dxxthly", "_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://dxxthly.itch.io/abyssal-ascension', '_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 = state.circleRadius; // 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();
// === NEW: VISUAL FILTERS LOGIC (Corrected to target the whole screen) ===
function applyVisualFilter() {
// Instead of targeting just the canvas, we will target the entire page body.
const targetElement = document.body;
let filterValue = '';
switch (state.features.visualMode) {
case 'invert':
filterValue = 'invert(1)';
break;
case 'contrast':
filterValue = 'contrast(1.75)';
break;
case 'grayscale':
filterValue = 'grayscale(1)';
break;
case 'sepia':
filterValue = 'sepia(1)';
break;
case 'none':
default:
filterValue = 'none';
break;
}
// Apply the filter to the entire body element.
targetElement.style.filter = filterValue;
}
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() {
// Stop this from running on Damnbruh.com
if (isDamnBruh) return;
if (typeof window.gsc !== 'undefined' && state.isInGame) { // Check 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: PARTICLE ENGINE ---
function startParticleAnimation(theme, canvas) {
if (!canvas) return;
const ctx = canvas.getContext('2d');
let particles = [];
let animationFrameId;
// This function makes sure the particle area is the same size as the profile card
function resizeCanvas() {
const parent = canvas.parentElement;
if (parent) {
canvas.width = parent.offsetWidth;
canvas.height = parent.offsetHeight;
}
}
// This function creates the particles with random positions and speeds
function createParticles(config) {
particles = [];
const area = canvas.width * canvas.height;
const count = Math.min(config.maxCount, Math.floor(area / (config.density || 5000))); // Ensure not too many particles
for (let i = 0; i < count; i++) {
particles.push({
x: Math.random() * canvas.width,
y: Math.random() * canvas.height,
vx: (Math.random() * (config.vx.max - config.vx.min) + config.vx.min) * (Math.random() < 0.5 ? -1 : 1),
vy: (Math.random() * (config.vy.max - config.vy.min) + config.vy.min),
radius: Math.random() * (config.radius.max - config.radius.min) + config.radius.min,
alpha: Math.random() * (config.alpha.max - config.alpha.min) + config.alpha.min,
alphaChange: config.alphaChange || 0,
color: config.colors[Math.floor(Math.random() * config.colors.length)]
});
}
}
// The main animation loop that draws everything
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
particles.forEach((p, index) => {
p.x += p.vx;
p.y += p.vy;
p.alpha -= p.alphaChange;
// Reset particle if it goes off-screen or fades out
if (p.y > canvas.height || p.y < 0 || p.x < 0 || p.x > canvas.width || p.alpha <= 0) {
particles.splice(index, 1); // Remove old particle
// Add a new one to replace it
const config = themeConfigs[theme];
particles.push({
x: Math.random() * canvas.width,
y: canvas.height, // Start from the bottom for rising effects
vx: (Math.random() * (config.vx.max - config.vx.min) + config.vx.min) * (Math.random() < 0.5 ? -1 : 1),
vy: (Math.random() * (config.vy.max - config.vy.min) + config.vy.min),
radius: Math.random() * (config.radius.max - config.radius.min) + config.radius.min,
alpha: Math.random() * (config.alpha.max - config.alpha.min) + config.alpha.min,
alphaChange: config.alphaChange || 0,
color: config.colors[Math.floor(Math.random() * config.colors.length)]
});
}
ctx.beginPath();
ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2);
ctx.fillStyle = `rgba(${p.color}, ${p.alpha})`;
ctx.fill();
});
animationFrameId = requestAnimationFrame(animate);
}
// --- Particle Configurations for Each Theme ---
const themeConfigs = {
fire: {
maxCount: 70, density: 4000,
vx: { min: -0.3, max: 0.3 }, vy: { min: -0.8, max: -0.2 }, // Moves upwards
radius: { min: 1, max: 4 }, alpha: { min: 0.4, max: 0.9 },
alphaChange: 0.005, // Fades out
colors: ['255,87,34', '255,152,0', '244,67,54']
},
ocean: {
maxCount: 50, density: 6000,
vx: { min: -0.2, max: 0.2 }, vy: { min: -0.6, max: -0.1 }, // Moves upwards slowly
radius: { min: 1, max: 5 }, alpha: { min: 0.1, max: 0.5 },
alphaChange: 0.002,
colors: ['255,255,255', '173,216,230']
},
forest: {
maxCount: 100, density: 3000,
vx: { min: -0.1, max: 0.1 }, vy: { min: -0.4, max: -0.05 }, // Barely moves
radius: { min: 0.5, max: 2 }, alpha: { min: 0.3, max: 0.8 },
alphaChange: 0.001,
colors: ['200,255,200', '255,255,255'] // Spores/fireflies
},
matrix: {
maxCount: 150, density: 2000,
vx: { min: 0, max: 0 }, vy: { min: 0.5, max: 1.5 }, // Moves downwards only
radius: { min: 2, max: 4 }, alpha: { min: 0.8, max: 1.0 },
alphaChange: 0.01,
colors: ['0,255,0']
}
};
if (themeConfigs[theme]) {
resizeCanvas();
createParticles(themeConfigs[theme]);
// Small delay to ensure canvas is ready
setTimeout(() => {
if (animationFrameId) cancelAnimationFrame(animationFrameId); // Clear any previous animation
animate();
}, 10);
}
return () => {
cancelAnimationFrame(animationFrameId);
};
}
// --- END: PARTICLE ENGINE ---
// --- START: FINAL GAME SUITE LOGIC (with Blackjack) ---
function showGameMenu() {
const gameModal = document.getElementById('game-modal-overlay');
if (!gameModal) return;
gameModal.style.display = 'flex';
const repDisplay = document.getElementById('player-rep-display');
const user = firebase.auth().currentUser;
if(user) {
const userRepRef = firebase.database().ref(`playerData/${user.uid}/rep`);
userRepRef.on('value', (snapshot) => {
const playerRep = snapshot.val() || 0;
repDisplay.textContent = `Your REP: ${playerRep.toLocaleString()}`;
});
} else {
repDisplay.textContent = 'Log in to play!';
}
const tabButtons = gameModal.querySelectorAll('.game-tab-btn');
const gameContainers = gameModal.querySelectorAll('.game-container');
tabButtons.forEach(button => {
button.onclick = () => {
tabButtons.forEach(btn => btn.classList.remove('active'));
button.classList.add('active');
gameContainers.forEach(container => container.style.display = 'none');
document.getElementById('game-' + button.dataset.gametab).style.display = 'flex';
if(window.spinTimerInterval) clearInterval(window.spinTimerInterval);
if (button.dataset.gametab === 'spinner') startSpinnerGame();
if (button.dataset.gametab === 'slots') startSlotMachineGame();
if (button.dataset.gametab === 'roulette') startRouletteGame();
if (button.dataset.gametab === 'blackjack') startBlackjackGame();
};
});
gameModal.onclick = (e) => {
if (e.target.id === 'game-modal-overlay') {
gameModal.style.display = 'none';
if (window.spinTimerInterval) clearInterval(window.spinTimerInterval);
}
};
// Reset to default tab on open
tabButtons.forEach(btn => btn.classList.remove('active'));
gameContainers.forEach(c => c.style.display = 'none');
tabButtons[0].classList.add('active');
gameContainers[0].style.display = 'flex';
startSpinnerGame();
}
function startSpinnerGame() {
const spinButton = document.getElementById('spin-button');
const spinnerDisplay = document.getElementById('spinner-display');
const spinnerResult = document.getElementById('spinner-result');
const spinTimer = document.getElementById('spin-timer');
const COOLDOWN_HOURS = 24;
let lastSpin = parseInt(localStorage.getItem('lastRepSpinTime') || '0', 10);
const cooldownMillis = COOLDOWN_HOURS * 3600000;
function updateTimer() {
const remainingTime = (lastSpin + cooldownMillis) - Date.now();
if (remainingTime <= 0) {
spinTimer.textContent = 'You can spin now!';
spinButton.disabled = false; spinButton.style.opacity = '1';
if (window.spinTimerInterval) clearInterval(window.spinTimerInterval);
return;
}
const h = Math.floor(remainingTime / 3600000);
const m = Math.floor((remainingTime % 3600000) / 60000);
const s = Math.floor((remainingTime % 60000) / 1000);
spinTimer.textContent = `Next spin in: ${h}h ${m}m ${s}s`;
spinButton.disabled = true; spinButton.style.opacity = '0.5';
}
updateTimer();
if (Date.now() - lastSpin < cooldownMillis) {
window.spinTimerInterval = setInterval(updateTimer, 1000);
}
spinButton.onclick = () => {
if (Date.now() - (parseInt(localStorage.getItem('lastRepSpinTime') || '0', 10)) < cooldownMillis) return;
spinButton.disabled = true;
spinnerResult.textContent = '';
const prizes = [1, 5, 5, 10, 10, 10, 25, 25, 50, 100];
const prize = prizes[Math.floor(Math.random() * prizes.length)];
let spinAnim = setInterval(() => spinnerDisplay.textContent = Math.floor(Math.random() * 100) + 1, 50);
setTimeout(() => {
clearInterval(spinAnim);
spinnerDisplay.textContent = prize;
spinnerResult.textContent = `You won ${prize} REP!`;
lastSpin = Date.now();
localStorage.setItem('lastRepSpinTime', lastSpin.toString());
const user = firebase.auth().currentUser;
if (user) { firebase.database().ref(`playerData/${user.uid}/rep`).transaction(rep => (rep || 0) + prize); }
updateTimer();
window.spinTimerInterval = setInterval(updateTimer, 1000);
}, 2000);
};
}
function startSlotMachineGame() {
const user = firebase.auth().currentUser;
if (!user) return;
const spinBtn = document.getElementById('slot-spin-btn');
const reels = document.querySelectorAll('.slot-reel');
const resultDisplay = document.getElementById('slot-result');
const betAmountInput = document.getElementById('slot-bet-amount');
const userRepRef = firebase.database().ref(`playerData/${user.uid}/rep`);
const BASE_BET = 5; // The original bet amount for payout scaling
const SYMBOLS = ['🍋', '🍒', '🍊', '🍉', '💎', '7️⃣'];
// Payouts are now per-symbol for a 3-in-a-row line
const PAYOUTS = { '🍋': 10, '🍒': 15, '🍊': 20, '🍉': 25, '💎': 50, '7️⃣': 100 };
// Define the 8 winning paylines by their grid indices (0-8)
const paylines = [
[0, 1, 2], // Top row
[3, 4, 5], // Middle row
[6, 7, 8], // Bottom row
[0, 3, 6], // Left column
[1, 4, 7], // Middle column
[2, 5, 8], // Right column
[0, 4, 8], // Diagonal top-left to bottom-right
[2, 4, 6] // Diagonal top-right to bottom-left
];
let isSpinning = false;
// Function to check all paylines for wins
function checkWin(finalReels) {
let totalPayout = 0;
const winningLineIndices = new Set();
paylines.forEach(line => {
const [a, b, c] = line; // Get the indices for this line
// Check if the symbols at these indices are the same
if (finalReels[a] === finalReels[b] && finalReels[b] === finalReels[c]) {
const winningSymbol = finalReels[a];
totalPayout += PAYOUTS[winningSymbol];
// Add the indices of the winning line to a set to highlight them
line.forEach(index => winningLineIndices.add(index));
}
});
return { totalPayout, winningLineIndices };
}
spinBtn.onclick = async () => {
if (isSpinning) return;
const betAmount = parseInt(betAmountInput.value);
if (isNaN(betAmount) || betAmount < 1) {
resultDisplay.textContent = "Invalid bet amount!";
return;
}
const snapshot = await userRepRef.once('value');
if (snapshot.val() < betAmount) {
resultDisplay.textContent = "Not enough REP to play!";
return;
}
isSpinning = true;
spinBtn.disabled = true;
spinBtn.style.opacity = '0.5';
resultDisplay.textContent = '';
reels.forEach(reel => reel.classList.remove('win-line')); // Clear previous win highlights
await userRepRef.transaction(rep => (rep || 0) - betAmount);
let spinCount = 0;
const spinInterval = setInterval(() => {
reels.forEach(reel => reel.textContent = SYMBOLS[Math.floor(Math.random() * SYMBOLS.length)]);
spinCount++;
if (spinCount > 20) {
clearInterval(spinInterval);
finishSpin(betAmount);
}
}, 100);
};
function finishSpin(betAmount) {
// Generate the final state for all 9 reels
const finalReels = Array.from({ length: 9 }, () => SYMBOLS[Math.floor(Math.random() * SYMBOLS.length)]);
// Update the UI with the final symbols
reels.forEach((reel, i) => reel.textContent = finalReels[i]);
const { totalPayout, winningLineIndices } = checkWin(finalReels);
if (totalPayout > 0) {
// Scale the payout based on the bet amount
const payoutMultiplier = betAmount / BASE_BET;
const finalWinnings = Math.floor(totalPayout * payoutMultiplier);
resultDisplay.textContent = `WINNER! You won ${finalWinnings.toLocaleString()} REP!`;
userRepRef.transaction(rep => (rep || 0) + finalWinnings);
// Highlight the winning reels
winningLineIndices.forEach(index => {
reels[index].classList.add('win-line');
});
} else {
resultDisplay.textContent = "Try again!";
}
isSpinning = false;
spinBtn.disabled = false;
spinBtn.style.opacity = '1';
}
}
function startRouletteGame() {
const user = firebase.auth().currentUser; if (!user) return;
const wheel = document.getElementById('roulette-wheel');
const resultDisplay = document.getElementById('roulette-result');
const betAmountInput = document.getElementById('roulette-bet-amount');
const betButtons = document.querySelectorAll('.roulette-bet-btn');
const userRepRef = firebase.database().ref(`playerData/${user.uid}/rep`);
let isSpinning = false, selectedBet = null, currentRotation = 0;
const numbers = [0, 32, 15, 19, 4, 21, 2, 25, 17, 34, 6, 27, 13, 36, 11, 30, 8, 23, 10, 5, 24, 16, 33, 1, 20, 14, 31, 9, 22, 18, 29, 7, 28, 12, 35, 3, 26];
const numberColors = {};
[0].forEach(n => numberColors[n] = 'green');
[1,3,5,7,9,12,14,16,18,19,21,23,25,27,30,32,34,36].forEach(n => numberColors[n] = 'red');
[2,4,6,8,10,11,13,15,17,20,22,24,26,28,29,31,33,35].forEach(n => numberColors[n] = 'black');
const spinWheel = async () => {
if (isSpinning || !selectedBet) { resultDisplay.textContent = "Select a color to bet on!"; return; }
const betAmount = parseInt(betAmountInput.value);
if (isNaN(betAmount) || betAmount <= 0) { resultDisplay.textContent = "Invalid bet amount!"; return; }
const snapshot = await userRepRef.once('value');
if (snapshot.val() < betAmount) { resultDisplay.textContent = "Not enough REP!"; return; }
isSpinning = true;
betButtons.forEach(b => { b.disabled = true; b.style.opacity = 0.5; });
resultDisplay.textContent = 'Spinning...';
await userRepRef.transaction(rep => (rep || 0) - betAmount);
const randomIndex = Math.floor(Math.random() * numbers.length);
const winningNumber = numbers[randomIndex];
const anglePerNumber = 360 / numbers.length;
const randomOffset = (Math.random() - 0.5) * anglePerNumber * 0.8;
const targetRotation = (360 * 5) + (anglePerNumber * (numbers.length - 1 - randomIndex)) + randomOffset;
currentRotation += targetRotation;
wheel.style.transform = `rotate(${currentRotation}deg)`;
setTimeout(() => {
const winningColor = numberColors[winningNumber];
resultDisplay.innerHTML = `Landed on <span style="font-weight:bold; color:${winningColor}; text-shadow: 0 0 5px ${winningColor};">${winningNumber}</span>!`;
const payout = (selectedBet === winningColor) ? betAmount * (winningColor === 'green' ? 14 : 2) : 0;
if (payout > 0) {
resultDisplay.innerHTML += ` You won ${payout.toLocaleString()} REP!`;
userRepRef.transaction(rep => (rep || 0) + payout);
} else {
resultDisplay.innerHTML += ` You lost your bet.`;
}
isSpinning = false;
betButtons.forEach(b => { b.disabled = false; b.style.opacity = 1; });
selectedBet = null;
betButtons.forEach(btn => btn.classList.remove('selected'));
}, 4100);
};
betButtons.forEach(button => button.onclick = () => {
if (isSpinning) return;
betButtons.forEach(btn => btn.classList.remove('selected'));
button.classList.add('selected');
selectedBet = button.dataset.bet;
spinWheel();
});
}
function startBlackjackGame() {
const user = firebase.auth().currentUser; if (!user) return;
const userRepRef = firebase.database().ref(`playerData/${user.uid}/rep`);
const dealBtn = document.getElementById('blackjack-deal-btn'), hitBtn = document.getElementById('blackjack-hit-btn'), standBtn = document.getElementById('blackjack-stand-btn'),
betControls = document.getElementById('blackjack-bet-controls'), actionControls = document.getElementById('blackjack-action-controls'),
betInput = document.getElementById('blackjack-bet-amount'), resultEl = document.getElementById('blackjack-result');
let deck = [], playerHand = [], dealerHand = [], bet = 0;
const createDeck = () => {
deck = []; const suits = ['♥', '♦', '♣', '♠'], values = ['2','3','4','5','6','7','8','9','10','J','Q','K','A'];
for(let suit of suits) { for(let value of values) { deck.push({suit, value}); } }
};
const shuffleDeck = () => { for (let i = deck.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [deck[i], deck[j]] = [deck[j], deck[i]]; } };
const getCardValue = (card) => { if (['J', 'Q', 'K'].includes(card.value)) return 10; if (card.value === 'A') return 11; return parseInt(card.value); };
const getHandScore = (hand) => {
let score = hand.reduce((sum, card) => sum + getCardValue(card), 0);
let aceCount = hand.filter(card => card.value === 'A').length;
while(score > 21 && aceCount > 0) { score -= 10; aceCount--; }
return score;
};
const renderHand = (hand, elementId, scoreId, hideFirstCard = false) => {
const handEl = document.getElementById(elementId); handEl.innerHTML = '';
hand.forEach((card, index) => {
const cardEl = document.createElement('div'); cardEl.className = 'blackjack-card';
if (['♥', '♦'].includes(card.suit)) cardEl.classList.add('red');
if (hideFirstCard && index === 0) { cardEl.classList.add('blackjack-card-hidden'); } else { cardEl.innerHTML = `<span style="align-self: flex-start; font-size: 0.8em;">${card.value}${card.suit}</span><span style="font-size: 1.5em;">${card.suit}</span><span style="align-self: flex-end; transform: rotate(180deg); font-size: 0.8em;">${card.value}${card.suit}</span>`; }
handEl.appendChild(cardEl);
});
document.getElementById(scoreId).textContent = hideFirstCard ? getCardValue(hand[1]) : getHandScore(hand);
};
const endRound = (message, payoutMultiplier) => {
renderHand(dealerHand, 'blackjack-dealer-cards', 'blackjack-dealer-score', false);
resultEl.textContent = message;
if (payoutMultiplier > 0) { userRepRef.transaction(rep => (rep || 0) + Math.floor(bet * payoutMultiplier)); }
betControls.style.display = 'flex'; actionControls.style.display = 'none';
};
dealBtn.onclick = async () => {
bet = parseInt(betInput.value);
if (isNaN(bet) || bet <= 0) { resultEl.textContent = "Invalid bet!"; return; }
const snapshot = await userRepRef.once('value');
if (snapshot.val() < bet) { resultEl.textContent = "Not enough REP!"; return; }
await userRepRef.transaction(rep => (rep || 0) - bet);
createDeck(); shuffleDeck(); playerHand = [deck.pop(), deck.pop()]; dealerHand = [deck.pop(), deck.pop()];
resultEl.textContent = ''; betControls.style.display = 'none'; actionControls.style.display = 'flex';
renderHand(playerHand, 'blackjack-player-cards', 'blackjack-player-score');
renderHand(dealerHand, 'blackjack-dealer-cards', 'blackjack-dealer-score', true);
if (getHandScore(playerHand) === 21) {
if (getHandScore(dealerHand) === 21) { endRound("Push! It's a tie.", 1); }
else { endRound("Blackjack! You win!", 2.5); }
}
};
hitBtn.onclick = () => {
playerHand.push(deck.pop()); renderHand(playerHand, 'blackjack-player-cards', 'blackjack-player-score');
if (getHandScore(playerHand) > 21) { endRound("Bust! You lose.", 0); }
};
standBtn.onclick = () => {
actionControls.style.display = 'none'; renderHand(dealerHand, 'blackjack-dealer-cards', 'blackjack-dealer-score', false);
const dealerTurn = setInterval(() => {
const dealerScore = getHandScore(dealerHand);
if (dealerScore < 17) { dealerHand.push(deck.pop()); renderHand(dealerHand, 'blackjack-dealer-cards', 'blackjack-dealer-score'); }
else {
clearInterval(dealerTurn); const playerScore = getHandScore(playerHand);
if (dealerScore > 21 || playerScore > dealerScore) { endRound("You win!", 2); }
else if (playerScore < dealerScore) { endRound("Dealer wins.", 0); }
else { endRound("Push! It's a tie.", 1); }
}
}, 800);
};
// Initial state reset
betControls.style.display = 'flex'; actionControls.style.display = 'none'; resultEl.textContent = '';
renderHand([], 'blackjack-player-cards', 'blackjack-player-score');
renderHand([], 'blackjack-dealer-cards', 'blackjack-dealer-score');
}
// --- END: FINAL GAME SUITE LOGIC ---
// --- START: FINAL GAME SUITE MODAL (with Blackjack) ---
if (!document.getElementById('game-modal-overlay')) {
const gameModalHTML = `
<div id="game-modal-overlay" style="display:none; position:fixed; top:0; left:0; width:100vw; height:100vh; z-index:10010; background:rgba(0,0,0,0.85); align-items:center; justify-content:center; font-family: 'Segoe UI', Arial, sans-serif;">
<div id="game-modal-content" style="background:#23232a; border-radius:12px; box-shadow:0 8px 30px rgba(0,0,0,0.5); display:flex; flex-direction:column; min-width:600px; max-width: 90vw; border: 1px solid var(--menu-color, #4CAF50); overflow:hidden;">
<!-- Game Tabs -->
<div id="game-tabs" style="display:flex; background:rgba(0,0,0,0.2);">
<button class="game-tab-btn active" data-gametab="spinner">💎 Daily Spinner</button>
<button class="game-tab-btn" data-gametab="slots">🎰 Slot Machine</button>
<button class="game-tab-btn" data-gametab="roulette">🎲 Roulette</button>
<button class="game-tab-btn" data-gametab="blackjack">🃏 Blackjack</button>
</div>
<div id="game-content-area" style="padding:25px 35px; max-height: 80vh; overflow-y: auto;">
<div id="player-rep-display" style="color: #ddd; margin-bottom: 20px; font-size: 1.2em; text-align:center; font-weight:bold; background: rgba(0,0,0,0.2); padding: 8px; border-radius: 6px;">Your REP: Loading...</div>
<!-- Spinner Game -->
<div id="game-spinner" class="game-container" style="display:flex; flex-direction:column; align-items:center;"></div>
<!-- Slot Machine Game -->
<div id="game-slots" class="game-container" style="display:none; flex-direction:column; align-items:center;"></div>
<!-- Roulette Game -->
<div id="game-roulette" class="game-container" style="display:none; flex-direction:column; align-items:center;"></div>
<!-- Blackjack Game -->
<div id="game-blackjack" class="game-container" style="display:none; flex-direction:column; align-items:center; width:100%;">
<h2 style="color:var(--menu-color, #4CAF50); margin:0 0 15px 0;">Blackjack</h2>
<div id="blackjack-table" style="width:100%;">
<!-- Dealer's Hand -->
<div class="blackjack-hand-area">
<h3 class="blackjack-hand-title">Dealer's Hand (<span id="blackjack-dealer-score">0</span>)</h3>
<div id="blackjack-dealer-cards" class="blackjack-cards"></div>
</div>
<!-- Player's Hand -->
<div class="blackjack-hand-area">
<h3 class="blackjack-hand-title">Your Hand (<span id="blackjack-player-score">0</span>)</h3>
<div id="blackjack-player-cards" class="blackjack-cards"></div>
</div>
</div>
<div id="blackjack-result" class="game-result-text" style="font-size:1.5em;"></div>
<!-- Bet Controls -->
<div id="blackjack-bet-controls" style="margin:15px 0; display:flex; flex-direction:column; align-items:center; gap:10px;">
<div>
<label for="blackjack-bet-amount" style="color:#ccc; margin-right:10px; font-weight:bold;">BET AMOUNT:</label>
<input id="blackjack-bet-amount" type="number" min="1" value="10" style="width:100px; text-align:center; font-size:1.1em; padding: 8px; border-radius:5px; border:1px solid #555; background:#222; color:#eee;">
</div>
<button id="blackjack-deal-btn" class="mod-menu-button" style="font-size: 1.1em; padding: 10px 20px; background-color: #27ae60;">Deal Cards</button>
</div>
<!-- Action Controls -->
<div id="blackjack-action-controls" style="display:none; gap: 15px; margin-top:15px;">
<button id="blackjack-hit-btn" class="mod-menu-button" style="font-size: 1.1em; padding: 10px 20px;">Hit</button>
<button id="blackjack-stand-btn" class="mod-menu-button" style="font-size: 1.1em; padding: 10px 20px; background-color:#c0392b;">Stand</button>
</div>
</div>
</div>
</div>
</div>
`;
const tempDiv = document.createElement('div');
tempDiv.innerHTML = gameModalHTML;
document.body.appendChild(tempDiv.firstElementChild);
// Copy original game content into the new structure
document.getElementById('game-spinner').innerHTML = `<h2 style="color:var(--menu-color, #4CAF50); margin:0 0 20px 0;">Daily REP Spinner</h2><div id="spinner-display" style="font-size: 4em; font-weight: bold; color: #fff; background: #111; padding: 20px 40px; border-radius: 10px; margin-bottom: 20px; min-width: 150px; text-align: center;">0</div><div id="spinner-result" class="game-result-text"></div><button id="spin-button" class="mod-menu-button" style="font-size: 1.2em; padding: 12px 25px;">Spin for REP!</button><div id="spin-timer" style="color: #aaa; margin-top: 15px;"></div>`;
document.getElementById('game-slots').innerHTML = `<h2 style="color:var(--menu-color, #4CAF50); margin:0 0 20px 0;">REP Slot Machine</h2><div id="slot-reels" style="display:flex; gap:15px; background:#111; padding: 20px; border-radius:10px; margin-bottom:20px; border: 2px solid #555;"><div class="slot-reel">💎</div><div class="slot-reel">💎</div><div class="slot-reel">💎</div></div><div id="slot-result" class="game-result-text"></div><button id="slot-spin-btn" class="mod-menu-button" style="font-size: 1.2em; padding: 12px 25px;">Spin! (Cost: 5 REP)</button>`;
document.getElementById('game-roulette').innerHTML = `<h2 style="color:var(--menu-color, #4CAF50); margin:0 0 15px 0;">REP Roulette</h2><div id="roulette-wheel-container" style="position:relative; width:200px; height:200px; margin-bottom:15px;"><div id="roulette-wheel"></div><div id="roulette-ball-marker" style="position: absolute; top: -5px; left: 50%; transform: translateX(-50%); width: 0; height: 0; border-left: 10px solid transparent; border-right: 10px solid transparent; border-top: 15px solid white;"></div></div><div id="roulette-result" class="game-result-text"></div><div id="roulette-bet-controls" style="width:100%; text-align:center;"><div style="margin-bottom:15px;"><label for="roulette-bet-amount" style="color:#ccc; margin-right:10px; font-weight:bold;">BET AMOUNT:</label><input id="roulette-bet-amount" type="number" min="1" value="10" style="width:100px; text-align:center; font-size:1.1em; padding: 8px; border-radius:5px; border:1px solid #555; background:#222; color:#eee;"></div><div id="roulette-bets"><button class="roulette-bet-btn" data-bet="red" style="background:#c0392b; color:white;">Red (2x)</button><button class="roulette-bet-btn" data-bet="black" style="background:#2c3e50; color:white;">Black (2x)</button><button class="roulette-bet-btn" data-bet="green" style="background:#27ae60; color:white;">Green (14x)</button></div></div>`;
const style = document.createElement('style');
style.textContent = `
.game-tab-btn { flex: 1; background: transparent; border: none; color: #aaa; font-size: 1.1em; padding: 15px; cursor: pointer; transition: all 0.2s; border-bottom: 3px solid transparent; }
.game-tab-btn:hover { background: rgba(255,255,255,0.05); color: #fff; }
.game-tab-btn.active { color: var(--menu-color, #4CAF50); border-bottom-color: var(--menu-color, #4CAF50); }
.game-result-text { color: #FFD700; font-size: 1.2em; height: 25px; margin: 15px 0; font-weight: bold; text-align:center; }
.slot-reel { font-size: 4em; width: 80px; height: 100px; line-height: 100px; text-align:center; background: #2a2a2e; color: #fff; border-radius: 8px; text-shadow: 0 0 5px #000; }
#roulette-wheel { width: 200px; height: 200px; border-radius: 50%; background-image: conic-gradient(green 0 9.7deg, red 9.7deg 19.4deg, black 19.4deg 29.1deg, red 29.1deg 38.8deg, black 38.8deg 48.5deg, red 48.5deg 58.2deg, black 58.2deg 67.9deg, red 67.9deg 77.6deg, black 77.6deg 87.3deg, red 87.3deg 97deg, black 97deg 106.7deg, red 106.7deg 116.4deg, black 116.4deg 126.1deg, red 126.1deg 135.8deg, black 135.8deg 145.5deg, red 145.5deg 155.2deg, black 155.2deg 164.9deg, red 164.9deg 174.6deg, black 174.6deg 184.3deg, red 184.3deg 194deg, black 194deg 203.7deg, red 203.7deg 213.4deg, black 213.4deg 223.1deg, red 223.1deg 232.8deg, black 232.8deg 242.5deg, red 242.5deg 252.2deg, black 252.2deg 261.9deg, red 261.9deg 271.6deg, black 271.6deg 281.3deg, red 281.3deg 291deg, black 291deg 300.7deg, red 300.7deg 310.4deg, black 310.4deg 320.1deg, red 320.1deg 329.8deg, black 329.8deg 339.5deg, red 339.5deg 349.2deg, black 349.2deg 360deg); border: 5px solid #8c6432; transition: transform 4s cubic-bezier(0.2, 0.8, 0.7, 1); }
.roulette-bet-btn { padding: 10px 15px; font-size: 1em; margin: 0 5px; cursor: pointer; border: 2px solid #555; color: #fff; border-radius: 5px; font-weight: bold; }
.roulette-bet-btn.selected { border-color: #FFD700; box-shadow: 0 0 10px #FFD700; transform: scale(1.05); }
.blackjack-hand-area { margin-bottom: 20px; background: rgba(0,0,0,0.2); border-radius: 8px; padding: 15px; }
.blackjack-hand-title { margin: 0 0 10px 0; color: #ccc; border-bottom: 1px solid #444; padding-bottom: 8px; }
.blackjack-cards { display: flex; gap: 10px; min-height: 105px; }
.blackjack-card { width: 70px; height: 100px; border: 2px solid #999; border-radius: 8px; background: white; color: black; display: flex; flex-direction: column; justify-content: space-between; padding: 5px; font-size: 1.5em; font-weight: bold; }
.blackjack-card.red { color: #c0392b; }
.blackjack-card-hidden { background: repeating-linear-gradient(45deg, #555, #555 10px, #444 10px, #444 20px); }
`;
document.head.appendChild(style);
}
// --- END: FINAL GAME SUITE MODAL ---
// === START: FINAL ADMIN PROFILE/MODERATION CLICK HANDLING SYSTEM ===
// ====================================================XXX===============
// 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 = ''; }
}
// ==========XXX=========================================================
// === START: COMBINED PROFILE & MODERATION SYSTEM (FINAL FIX) =====
// ================================================XXX===================
// This function handles saving profile data for YOURSELF or for OTHERS (if you're an admin).
// This is the SECURED version of the function
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...';
// --- FIX IS HERE: We now use sanitizeInput() on both fields ---
const newAvatarUrl = sanitizeInput(document.getElementById('profile-avatar-input').value.trim());
const newMotto = sanitizeInput(document.getElementById('profile-motto-input').value.trim());
// --- END OF FIX ---
try {
// --- CORRECTED LINE ---
const userRef = firebase.database().ref(`playerData/${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;
}
}
// ===========X===========================================X=============
// === START: COMBINED PROFILE & MODERATION SYSTEM (FINAL FIX) =====
// ===================XX================================================
// --- START OF FINAL PROFILE & THEME REPLACEMENT ---
async function showUserProfile(uid, overrideTheme = null) {
if (!uid) return;
// Clean up any previous profile and its animations
const oldPopup = document.getElementById('profile-popup');
if (oldPopup && typeof oldPopup.cleanup === 'function') {
oldPopup.cleanup();
}
oldPopup?.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 = {}, modVersion = 'Unknown',
profileTheme = 'default';
const currentUser = firebase.auth()?.currentUser;
try {
const userSnap = await firebase.database().ref("onlineUsers/" + uid).once('value');
const playerDataSnap = await firebase.database().ref("playerData/" + uid).once('value');
if (userSnap.exists()) {
const userData = userSnap.val();
username = escapeHTML(filterProfanity(userData.name || 'Anon'));
isOnline = (Date.now() - (userData.lastActive || 0) < 300000);
userChatColor = userData.chatNameColor || '#FFD700';
if (userData.modVersion) modVersion = userData.modVersion;
}
if (playerDataSnap.exists()) {
const pData = playerDataSnap.val();
playerRep = pData.rep || 0;
unlockedBadges = pData.badges || {};
if (pData.profileAvatar) avatarUrl = pData.profileAvatar;
if (pData.profileMotto) motto = escapeHTML(filterProfanity(pData.profileMotto));
if (pData.profileTheme) profileTheme = pData.profileTheme;
}
if (!motto.trim() || motto === '<i></i>') motto = '<i>No motto set.</i>';
} catch (err) { console.error(`Failed to fetch profile for UID ${uid}:`, err); }
if (currentUser && uid === currentUser.uid) {
profileTheme = localStorage.getItem('profileTheme') || profileTheme;
}
if (overrideTheme) {
profileTheme = overrideTheme;
}
const popup = document.createElement('div');
popup.className = 'profile-popup';
popup.id = 'profile-popup';
popup.dataset.targetUid = uid;
popup.dataset.targetName = username;
// --- START: UPGRADED THEMES WITH VISIBLE TEXT ---
const themes = {
'default': { name: 'Default', style: 'color: #ccc; font-weight: bold;' },
'fire': { name: 'Ember', style: 'color: #fff; text-shadow: 1px 1px 4px #000; font-weight: bold; background: linear-gradient(135deg, #4a0b0b, #9d2a07, #210404); border-color: #ff4b2b;', particles: 'fire' },
'ocean': { name: 'Abyss', style: 'color: #fff; text-shadow: 1px 1px 4px #000; font-weight: bold; background: linear-gradient(260deg, #02112d, #062a6c, #0d3b8e); border-color: #4e54c8;', particles: 'ocean' },
'forest': { name: 'Mystic Forest', style: 'color: #fff; text-shadow: 1px 1px 4px #000; font-weight: bold; background: linear-gradient(135deg, #09300d, #145c1a, #031c05); border-color: #71B280;', particles: 'forest' },
'matrix': { name: 'Matrix', style: 'color: #00ff00; text-shadow: 0 0 5px #00ff00, 0 0 2px #fff; font-weight: bold; background-color: #000000; border-color: #00ff00;', particles: 'matrix' },
'galaxy': { name: 'Starfield', style: `color: #fff; text-shadow: 1px 1px 4px #000; font-weight: bold; background-image: radial-gradient(white, rgba(255,255,255,.2) 2px, transparent 40px), radial-gradient(white, rgba(255,255,255,.15) 1px, transparent 30px), radial-gradient(white, rgba(255,255,255,.1) 2px, transparent 40px), radial-gradient(rgba(255,255,255,.4), rgba(255,255,255,.1) 2px, transparent 30px); background-size: 550px 550px, 350px 350px, 250px 250px, 150px 150px; background-position: 0 0, 40px 60px, 130px 270px, 70px 100px; background-color: #0d1024; animation: animated-galaxy 200s linear infinite;` },
'gold': { name: 'Liquid Gold', style: 'color: #000; text-shadow: 1px 1px 2px rgba(255,255,255,0.4); font-weight: bold; background: linear-gradient(100deg, #b29f00, #ffdf33, #b29f00, #ffbf00); background-size: 400% 400%; animation: animated-gold 6s linear infinite; border-color: #ffbf00;', particles: null },
'ice': { name: 'Glacier', style: 'color: #002233; text-shadow: 1px 1px 2px rgba(255,255,255,0.5); font-weight: bold; background: linear-gradient(135deg, #d4f2ff, #83c6e2, #a7d9ed); border-color: #a7d9ed;', particles: null }
};
// --- END: UPGRADED THEMES WITH VISIBLE TEXT ---
if (themes[profileTheme]) {
popup.style.cssText += themes[profileTheme].style;
} else {
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 = '', sendRepButtonHTML = '';
const isDeveloper = isDev(uid);
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 && isDev(currentUser.uid) && uid !== currentUser.uid) { adminButtonsHTML = `<div style="margin-top:10px; display:flex; gap: 8px;"><button id="admin-edit-profile-btn" class="mod-menu-button" style="background-color:${state.menuColor};">🛠️ Edit Profile</button><button id="admin-rename-btn" class="mod-menu-button" style="background-color:#ff9800;">✏️ Change Name</button><button id="timeout-chat-btn" class="mod-menu-button" style="background-color:#c9302c;">⏰ Timeout</button></div>`; }
if (currentUser && uid !== currentUser.uid) {
sendRepButtonHTML = `<button id="send-rep-btn" class="mod-menu-button" style="background-color:#E91E63; font-size: 1em; padding: 10px 18px; margin-top: 15px;">💖 Send REP</button>`;
}
if (currentUser && uid === currentUser.uid) {
selfEditButtonHTML = `<div style="margin-top:15px;"><button id="profile-edit-btn" class="mod-menu-button" style="background-color:#007bff; font-size: 1em; padding: 10px 18px;">✏️ Edit My Profile</button></div>`;
}
const defaultAvatar = `https://i.imgur.com/M6NYjjO.jpeg`;
// The canvas is now added here if a particle theme is selected
const particleCanvasHTML = themes[profileTheme] && themes[profileTheme].particles ? `<canvas id="profile-particle-canvas" style="position:absolute; top:0; left:0; width:100%; height:100%; z-index:0;"></canvas>` : '';
// All content is now wrapped in a container to appear above the canvas
popup.innerHTML = `
${particleCanvasHTML}
<div style="position:relative; z-index:1; display:flex; flex-direction:column; align-items:center; width:100%;">
<button class="close-btn" title="Close" onclick="this.closest('.profile-popup').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>
${selfEditButtonHTML}
${adminButtonsHTML}
${sendRepButtonHTML}
<div style="margin-top: 15px; font-size: 0.8em; color: #aaa; font-family: 'Courier New', monospace; text-align: left; width: 90%;">
<div style="word-break: break-all;">UID: <span style="color: #fff; user-select: text; cursor: text;">${uid}</span></div>
<div>Version: <span style="color: #fff;">${modVersion}</span></div>
</div>
</div>
`;
document.body.appendChild(popup);
const particleCanvas = document.getElementById('profile-particle-canvas');
if (particleCanvas && themes[profileTheme] && themes[profileTheme].particles) {
popup.cleanup = startParticleAnimation(themes[profileTheme].particles, particleCanvas);
}
}
document.addEventListener('click', async function(e) {
if (e.target.closest('.profile-popup .close-btn')) {
const popup = e.target.closest('.profile-popup');
if (popup && typeof popup.cleanup === 'function') {
popup.cleanup();
}
popup?.remove();
return;
}
if (e.target.id === 'profile-modal-save') { saveProfile(); return; }
// ... (admin buttons logic remains unchanged)
if (e.target.id === 'profile-edit-btn') { openEditProfileModal(); return; }
if (e.target.id === 'profile-themes-btn') {
const overlay = document.getElementById('themes-modal-overlay');
const grid = overlay.querySelector('.themes-grid');
const editModal = document.getElementById('edit-profile-modal-overlay');
const targetUid = editModal.dataset.targetUid;
const currentUser = firebase.auth()?.currentUser;
const uidToSaveFor = targetUid || currentUser?.uid;
let selectedThemeId = null;
grid.innerHTML = '';
const closeButton = document.getElementById('close-themes-modal');
closeButton.textContent = 'Apply & Close';
closeButton.style.background = '#4CAF50';
const themes = {
'default': { name: 'Default', style: '' },
'fire': { name: 'Ember', style: 'background: linear-gradient(135deg, #4a0b0b, #9d2a07, #210404); border-color: #ff4b2b;', particles: 'fire' },
'ocean': { name: 'Abyss', style: 'background: linear-gradient(260deg, #02112d, #062a6c, #0d3b8e); border-color: #4e54c8;', particles: 'ocean' },
'forest': { name: 'Mystic Forest', style: 'background: linear-gradient(135deg, #09300d, #145c1a, #031c05); border-color: #71B280;', particles: 'forest' },
'matrix': { name: 'Matrix', style: 'background-color: #000000; border-color: #00ff00;', particles: 'matrix' },
'galaxy': { name: 'Starfield', style: `background-image: radial-gradient(white, rgba(255,255,255,.2) 2px, transparent 40px), radial-gradient(white, rgba(255,255,255,.15) 1px, transparent 30px), radial-gradient(white, rgba(255,255,255,.1) 2px, transparent 40px), radial-gradient(rgba(255,255,255,.4), rgba(255,255,255,.1) 2px, transparent 30px); background-size: 550px 550px, 350px 350px, 250px 250px, 150px 150px; background-position: 0 0, 40px 60px, 130px 270px, 70px 100px; background-color: #0d1024; color: #fff; border-color: #888; animation: animated-galaxy 200s linear infinite;` },
'gold': { name: 'Liquid Gold', style: 'background: linear-gradient(100deg, #b29f00, #ffdf33, #b29f00, #ffbf00); background-size: 400% 400%; animation: animated-gold 6s linear infinite; border-color: #ffbf00; color: #000; font-weight: bold;' },
'ice': { name: 'Glacier', style: 'background: linear-gradient(135deg, #d4f2ff, #83c6e2, #a7d9ed); border-color: #a7d9ed; color: #002233;'}
};
const currentTheme = targetUid ? '' : (localStorage.getItem('profileTheme') || 'default');
selectedThemeId = currentTheme;
for (const [id, theme] of Object.entries(themes)) {
const preview = document.createElement('div');
preview.className = 'theme-preview';
preview.textContent = theme.name;
preview.style.cssText += theme.style;
if (id === currentTheme) {
preview.classList.add('selected');
}
preview.onclick = () => {
selectedThemeId = id;
document.querySelectorAll('.theme-preview').forEach(p => p.classList.remove('selected'));
preview.classList.add('selected');
};
grid.appendChild(preview);
}
closeButton.onclick = async () => {
if (uidToSaveFor && selectedThemeId) {
await firebase.database().ref(`playerData/${uidToSaveFor}`).update({ profileTheme: selectedThemeId });
if (!targetUid) {
localStorage.setItem('profileTheme', selectedThemeId);
showUserProfile(uidToSaveFor, selectedThemeId);
} else {
alert(`Theme for user has been set to "${themes[selectedThemeId].name}".`);
document.getElementById('profile-popup')?.remove();
}
}
overlay.style.display = 'none';
};
overlay.style.display = 'flex';
return;
}
if (e.target.id === 'close-themes-modal' || e.target.id === 'themes-modal-overlay') {
document.getElementById('themes-modal-overlay').style.display = 'none';
return;
}
// --- START: RESTORED ADMIN & SAVE BUTTON LOGIC ---
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 playerDataRef = firebase.database().ref("playerData/" + targetUid);
playerDataRef.once('value', (snapshot) => {
const userData = snapshot.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 === 'admin-rename-btn') {
const profilePopup = e.target.closest('.profile-popup');
const currentUser = firebase.auth()?.currentUser;
if (!profilePopup || !currentUser || !isDev(currentUser.uid)) return;
const targetUid = profilePopup.dataset.targetUid;
const oldName = profilePopup.dataset.targetName;
const newName = prompt(`Enter the new name for "${oldName}":`);
if (!newName || newName.trim().length === 0 || newName.length > 20) {
alert("Invalid name. Name cannot be empty or longer than 20 characters.");
return;
}
const sanitizedName = newName.trim();
firebase.database().ref(`onlineUsers/${targetUid}`).update({ name: sanitizedName })
.then(() => {
firebase.database().ref("slitherChat").push({
uid: "system", name: "System",
text: `An admin changed "${oldName}"'s name to "${sanitizedName}".`,
time: firebase.database.ServerValue.TIMESTAMP, chatNameColor: "#f44336"
});
alert(`Successfully changed name to "${sanitizedName}"!`);
const currentPopup = document.getElementById('profile-popup');
if (currentPopup && typeof currentPopup.cleanup === 'function') {
currentPopup.cleanup();
}
currentPopup?.remove();
}).catch((error) => {
alert("Failed to change name. See console for details.");
console.error("Admin rename error:", error);
});
return;
}
// --- END: RESTORED ADMIN & SAVE BUTTON LOGIC ---
// --- START: MINI-GAME BUTTON CLICK HANDLER ---
if (e.target.id === 'open-games-menu-btn') {
showGameMenu();
return;
}
// --- END: MINI-GAME BUTTON CLICK HANDLER ---
// --- START: SEND REP CLICK HANDLER ---
if (e.target.id === 'send-rep-btn') {
const profilePopup = e.target.closest('.profile-popup');
const repModal = document.getElementById('send-rep-modal-overlay');
if (!profilePopup || !repModal) return;
const targetUid = profilePopup.dataset.targetUid;
const targetName = profilePopup.dataset.targetName;
repModal.dataset.targetUid = targetUid;
repModal.dataset.targetName = targetName;
repModal.querySelector('#send-rep-title').textContent = `Send REP to ${targetName}`;
repModal.querySelector('#send-rep-status').textContent = '';
repModal.querySelector('#send-rep-amount').value = '1';
repModal.style.display = 'flex';
return;
}
if (e.target.id === 'send-rep-cancel-btn') {
const repModal = e.target.closest('#send-rep-modal-overlay');
if (repModal) repModal.style.display = 'none';
return;
}
if (e.target.id === 'send-rep-confirm-btn') {
const repModal = e.target.closest('#send-rep-modal-overlay');
const statusEl = repModal.querySelector('#send-rep-status');
const confirmBtn = e.target;
const amountToSend = parseInt(repModal.querySelector('#send-rep-amount').value, 10);
const recipientUid = repModal.dataset.targetUid;
const sender = firebase.auth().currentUser;
if (!sender || !recipientUid) {
statusEl.textContent = 'Error: Users not found.';
return;
}
if (isNaN(amountToSend) || amountToSend <= 0) {
statusEl.textContent = 'Invalid amount.';
return;
}
confirmBtn.disabled = true;
statusEl.textContent = 'Processing...';
const senderRef = firebase.database().ref(`playerData/${sender.uid}`);
const recipientRef = firebase.database().ref(`playerData/${recipientUid}`);
try {
const senderSnapshot = await senderRef.once('value');
const senderData = senderSnapshot.val();
if (!senderData || (senderData.rep || 0) < amountToSend) {
statusEl.textContent = 'Not enough REP!';
confirmBtn.disabled = false;
return;
}
// Perform transaction
await senderRef.child('rep').transaction(rep => (rep || 0) - amountToSend);
await recipientRef.child('rep').transaction(rep => (rep || 0) + amountToSend);
statusEl.style.color = '#90EE90';
statusEl.textContent = 'REP sent successfully!';
// --- START: New Chat Announcement Code ---
const senderName = localStorage.getItem("nickname") || "Anon";
const recipientName = repModal.dataset.targetName;
if (senderName && recipientName) {
const chatMessage = {
uid: "system",
name: "System",
text: `💖 ${senderName} sent ${amountToSend.toLocaleString()} REP to ${recipientName}!`,
time: firebase.database.ServerValue.TIMESTAMP,
chatNameColor: "#E91E63" // Pink system message
};
firebase.database().ref("slitherChat").push(chatMessage);
}
// --- END: New Chat Announcement Code ---
setTimeout(() => {
repModal.style.display = 'none';
statusEl.style.color = '#ffc107'; // Reset color
confirmBtn.disabled = false;
}, 1500);
} catch (error) {
console.error("REP Sending Error:", error);
statusEl.textContent = 'An error occurred.';
confirmBtn.disabled = false;
}
return;
}
// --- END: SEND REP CLICK HANDLER ---
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;
}
});
// =========================================================XX==========
// === END: COMBINED PROFILE & MODERATION SYSTEM (FINAL FIX) =====
// ======XX=============================================================
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 (isDamnBruh) {
initializeDamnBruhZoom();
} else {
// These are for Slither.io
updateServerIpLoop();
if (state.features.autoRespawn) enableAutoRespawn();
addServerBox();
patchPlayButtons();
zoomLockLoop(); // Slither's zoom loop
}
// These run on both sites
document.addEventListener('click', primeAudio);
document.addEventListener('keydown', primeAudio);
// Start continuous loops that are safe for both
autoBoostLoop();
checkGameState();
drawCircleRestriction();
fpsCounter();
deathSoundObserver();
snakeTrailAnimationLoop();
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();
window.addEventListener('resize', detectBrowserZoom);
detectBrowserZoom(); // Run it once on load
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