SLITHER.IO STANDALONE CHAT MOD (NOW TALK WITH SLITHER GAMERS!)

Adds the 143X community chat with profiles, GIFs, Discord integration, and settings to Slither.io.

目前為 2025-07-26 提交的版本,檢視 最新版本

// ==UserScript==
// @name         SLITHER.IO STANDALONE CHAT MOD (NOW TALK WITH SLITHER GAMERS!)
// @namespace    http://tampermonkey.net/
// @version      11.5
// @description  Adds the 143X community chat with profiles, GIFs, Discord integration, and settings to Slither.io.
// @author       dxxthly & waynesg (Adapted by AI for standalone use)
// @match        http://slither.io/
// @match        https://slither.io/
// @match        http://slither.com/io
// @match        https://slither.com/io
// @grant        none
// @icon         https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQUNcRl2Rh40pZLhgffYGFDRLbYJ4qfMNwddQ&s
// ==/UserScript==

(function() {
    'use strict';

    // --- 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.');
    })();
    // --- END OF GUARDIAN ---

    let hasInitialized = false;

    // =================================================================================
    // INITIALIZATION CHECKER
    // This is the new, robust way to start the script.
    // It will repeatedly check if the slither.io page is ready.
    // =================================================================================
    const initChecker = setInterval(() => {
        // We check for the 'login' div, which is a reliable indicator that the core page has loaded.
        if (document.getElementById('login') && !hasInitialized) {
            hasInitialized = true; // Prevents the script from running multiple times
            clearInterval(initChecker); // Stop checking once we've started
            main(); // Run the main script logic
        }
    }, 100); // Check every 100 milliseconds

    // REPLACE WITH THIS
    const systemAccounts = [
        "system",
        "discord_bot",
        "system_badge" // <-- THE FIX IS HERE
    ];

    // =================================================================================
    // MAIN SCRIPT FUNCTION
    // All of our chat logic now lives inside this function.
    // =================================================================================
    function main() {
        // --- CONFIGURATION ---
        const config = {
            chatMaxMessages: 75,
            firebaseConfig: {
                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"
            },
            devList: [{ uid: "CiOpgh1RLBg3l5oXn0SAho66Po93" }, { uid: "PZA5qgKWsPTXc278pyx7NwROf313" }, { uid: "P75eMwh756Rb6h1W6iqQfHN2Dm92" }],
            vipMembers: [{ uid: "crcOY9hoRrfayStCxMVm7Zdx2W92", name: "stevao" }, { uid: "DhGhICAZwkRa7wuMsyquM9a5uO92", name: "LUANBLAYNER" }]
        };

        // --- STYLES (CSS INJECTION) ---
        const style = document.createElement('style');
        style.textContent = `
            #chat-container { position: fixed; left: 20px; top: 100px; width: 380px; height: 400px; z-index: 9999; display: flex; flex-direction: column; background: rgba(28, 28, 32, 0.97); border: 1px solid #4CAF50; border-radius: 8px; box-shadow: 0 5px 20px rgba(0,0,0,0.3); overflow: hidden; user-select: none; resize: both; min-width: 320px; min-height: 250px; }
            #chat-header { display: flex; align-items: center; border-bottom: 1px solid #4CAF50; background: rgba(0,0,0,0.2); cursor: move; }
            .chat-tab { flex: 1; padding: 10px 12px; text-align: center; cursor: pointer; font-weight: 500; transition: background 0.2s, color 0.2s; }
            #chat-tab-main { background: rgba(76, 175, 80, 0.25); color: #fff; } #chat-tab-users { background: transparent; color: #ccc; }
            #chat-settings-btn, #chat-hide-btn { background: none; border: none; color: #ccc; font-size: 20px; padding: 0 12px; cursor: pointer; transition: color 0.2s; border-left: 1px solid rgba(255,255,255,0.1); }
            #chat-settings-btn:hover, #chat-hide-btn:hover { color: white; }
            #chat-area { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
            #chat-body, #online-users { flex: 1; padding: 10px 15px; overflow-y: auto; display: flex; flex-direction: column; gap: 5px; scrollbar-width: thin; scrollbar-color: #4CAF50 rgba(0,0,0,0.2); }
            #online-users { display: none; }
            #chat-input-container { display: flex; align-items: center; border-top: 1px solid #4CAF50; }
            #chat-input { flex-grow: 1; padding: 12px 15px; border: none; background: transparent; color: #e0e0e0; outline: none; font-size: 14px; }
            .chat-username { font-weight: bold; cursor: pointer; text-decoration: underline dotted; }
            .chat-modal-overlay { display: none; position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; z-index: 10001; background: rgba(0,0,0,0.75); align-items: center; justify-content: center; }
            .chat-modal-content { background: #2E2E34; border-radius: 10px; padding: 25px 30px; min-width: 400px; display: flex; flex-direction: column; gap: 15px; border: 1px solid #555; }
            .chat-modal-content h2 { margin: 0 0 10px 0; color: #4CAF50; text-align: center; }
            .settings-field { display: flex; flex-direction: column; gap: 5px; } .settings-field label { color: #bbb; font-size: 0.9em; }
            .settings-field input[type="text"] { width: 100%; box-sizing: border-box; padding: 10px; background: #222; border: 1px solid #555; border-radius: 5px; color: #eee; font-size: 1em; }
            .color-input-wrapper { display: flex; align-items: center; gap: 10px; padding: 5px; background: #222; border: 1px solid #555; border-radius: 5px; }
            .color-input-wrapper input[type="color"] { width: 30px; height: 30px; border: none; background: none; cursor: pointer; padding: 0; }
            .color-input-wrapper input[type="text"] { flex-grow: 1; border: none; background: none; color: #eee; padding: 5px; }
            .modal-buttons { display: flex; justify-content: flex-end; gap: 10px; margin-top: 10px; }
            .modal-btn { padding: 9px 20px; border: none; border-radius: 5px; font-size: 0.95em; cursor: pointer; }
            #settings-save-btn { background: #4CAF50; color: #fff; } #settings-cancel-btn { background: #555; color: #fff; }
            .profile-popup { position: fixed; z-index: 10002; display: flex; flex-direction: column; align-items: center; left: 50%; top: 50%; transform: translate(-50%, -50%); min-width: 280px; background: #2A2A2F; color: #e0e0e0; border-radius: 12px; border: 1px solid #4CAF50; box-shadow: 0 8px 32px rgba(0,0,0,0.35); padding: 24px 30px; }
            .profile-popup .avatar { width: 72px; height: 72px; border-radius: 50%; object-fit: cover; border: 3px solid #4CAF50; margin-bottom: 16px; }
            .profile-popup .close-btn { position: absolute; top: 12px; right: 12px; background: none; border: none; color: #aaa; font-size: 1.6em; cursor: pointer; }
        `;
        document.head.appendChild(style);

        // --- HELPER FUNCTIONS ---
        const escapeHTML = (str) => { const p = document.createElement('p'); p.appendChild(document.createTextNode(str)); return p.innerHTML; };
        const isDev = (uid) => config.devList.some(dev => dev.uid === uid);
        const isVip = (uid, name) => config.vipMembers.some(vip => vip.uid === uid && vip.name.toLowerCase() === (name || '').toLowerCase());
        const rainbowTextStyle = (name) => name.split('').map((char, i) => `<span style="color:${["#ef3550","#f48fb1","#7e57c2","#2196f3","#26c6da","#43a047","#eeff41","#f9a825","#ff5722"][i % 9]}; font-weight: bold;">${char}</span>`).join('');
        const vipGlowStyle = (name, color) => `<span style="color:#fff;font-weight:bold;text-shadow:0 0 5px #fff, 0 0 10px ${color}, 0 0 15px ${color};">${name}</span>`;

        // --- UI & MODAL CREATION ---
        const chatContainer = document.createElement('div'); chatContainer.id = 'chat-container';
        chatContainer.innerHTML = `
            <div id="chat-header"> <div id="chat-tab-main" class="chat-tab">143X Chat</div> <div id="chat-tab-users" class="chat-tab">Online Users</div> <button id="chat-settings-btn" title="Settings">⚙️</button> <button id="chat-hide-btn" title="Hide Chat">×</button> </div>
            <div id="chat-area"> <div id="chat-body"><p style="color:#888; text-align:center;">Initializing...</p></div> <div id="online-users"></div> <div id="chat-input-container"><input id="chat-input" type="text" placeholder="Connecting..." disabled></div> </div>`;
        document.body.appendChild(chatContainer);
        makeDraggable(chatContainer, document.getElementById('chat-header'));
        const settingsModal = document.createElement('div'); settingsModal.id = 'settings-modal-overlay'; settingsModal.className = 'chat-modal-overlay';
        settingsModal.innerHTML = `
            <div id="settings-modal" class="chat-modal-content">
                <h2>Chat & Profile Settings</h2>
                <div class="settings-field"><label>Nickname</label><input type="text" id="settings-nickname" maxlength="20"></div>
                <div class="settings-field"><label>Chat Name Color</label><div class="color-input-wrapper"><input type="color" id="settings-name-color"><input type="text" id="settings-name-color-hex" maxlength="7"></div></div>
                <div class="settings-field"><label>Profile Avatar URL</label><input type="text" id="settings-avatar"></div>
                <div class="settings-field"><label>Profile Motto</label><input type="text" id="settings-motto" maxlength="60"></div>
                <div class="modal-buttons"><button id="settings-cancel-btn" class="modal-btn">Cancel</button><button id="settings-save-btn" class="modal-btn">Save</button></div>
            </div>`;
        document.body.appendChild(settingsModal);

        // --- FIREBASE & CHAT LOGIC ---
        function loadFirebaseAndInit() {
            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 = () => initializeChat(window.firebase); document.head.appendChild(script3); }; document.head.appendChild(script2); }; document.head.appendChild(script1);
        }

        function initializeChat(firebase) {
            if (!firebase.apps.length) firebase.initializeApp(config.firebaseConfig);
            const auth = firebase.auth(); const db = firebase.database();
            auth.signInAnonymously().catch(err => console.error("Firebase Sign-In Error:", err));
            auth.onAuthStateChanged(user => {
                if (user) {
                    const chatInput = document.getElementById('chat-input'); chatInput.disabled = false; chatInput.placeholder = 'Type and press Enter...'; document.getElementById('chat-body').innerHTML = '';
                    let nickname = localStorage.getItem("slitherChatNickname");
                    if (!nickname) { nickname = prompt("Welcome! Please enter a nickname:", "Player") || "Anon"; localStorage.setItem("slitherChatNickname", nickname); }
                    const userRef = db.ref("onlineUsers/" + user.uid);
                    userRef.onDisconnect().remove();
                    const localData = { name: nickname, chatNameColor: localStorage.getItem("slitherChatNameColor") || "#FFD700", profileAvatar: localStorage.getItem("slitherChatAvatar") || "", profileMotto: localStorage.getItem("slitherChatMotto") || "" };
                    userRef.update({ ...localData, uid: user.uid, lastActive: firebase.database.ServerValue.TIMESTAMP });
                    setInterval(() => userRef.update({ lastActive: Date.now() }), 45000);
                    listenForOnlineUsers(db, user.uid);
                    setupEventListeners(db, user.uid);
                }
            });

            let chatMessagesArray = [];
            let latestTimeLoaded = 0;

            const chatBody = document.getElementById('chat-body');
            const currentUser = auth.currentUser;

            // Step 1: Load initial messages
            db.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, currentUser?.uid); }
                    chatBody.scrollTop = chatBody.scrollHeight;
                }
                // Step 2: Listen for ONLY new messages from now on
                db.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, currentUser?.uid, true);
                    }
                });
            });
        }

        
        function renderChatMessage(msg, chatBodyElement, currentUid, shouldScroll = false) {
            // Check if state object and ignoredUsers list exist before using them
            if (window.standaloneState && window.standaloneState.ignoredUsers && window.standaloneState.ignoredUsers[msg.uid]) return;
            if (!msg || !msg.uid) return;

            // Security: Validate the color first.
            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 messageText = msg.text;
            let roleTagHTML = '';
            let displayName;

            const isSystemMessage = msg.uid === 'system' || msg.uid === 'system_badge';
            const isDiscordBot = msg.uid === 'discord_bot';

            // 1. Determine the display name and message text
            if (isDiscordBot) {
                const nameMatch = msg.name.match(/^Discord\((.*)\)$/);
                displayName = nameMatch && nameMatch[1] ? escapeHTML(nameMatch[1]) : 'Discord User';
                messageText = escapeHTML(msg.text);
            } else {
                displayName = escapeHTML(msg.name || 'Anon');
                messageText = msg.text;
            }

            // 2. Build the name's 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'; // Force name to System for all system UIDs
                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. Finalize message text and handle images/HTML
            let finalMessage = '';
            const imageRegex = /(https?:\/\/[^\s]+\.(?:png|jpg|jpeg|gif|webp))/i;
            const imageMatch = messageText.match(imageRegex);

            if (imageMatch) {
                finalMessage = `<br><img src="${imageMatch[0]}" style="max-width:90%; border-radius:6px; margin-top:5px; cursor:pointer;" onclick="window.open('${imageMatch[0]}', '_blank')">`;
                messageText = escapeHTML(messageText.replace(imageRegex, '').trim());
                finalMessage = messageText + finalMessage;
            } else if (isSystemMessage || isDev(msg.uid)) {
                finalMessage = messageText; // Allow HTML
            } else {
                finalMessage = escapeHTML(messageText); // Escape for everyone else
            }

            // 4. Assemble and render the final message element
            const el = document.createElement('div');
            const borderColor = (msg.uid === currentUid) ? '#4CAF50' : userColor;
            const bgColor = (msg.uid === currentUid) ? 'rgba(76, 175, 80, 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;
            }
        }

        function listenForOnlineUsers(db, currentUid) { /* ... same as before ... */ }
        async function showUserProfile(db, uid) { /* ... same as before ... */ }

        // --- EVENT LISTENERS ---
        function setupEventListeners(db, uid) {
            document.getElementById('chat-tab-main').addEventListener('click', () => { document.getElementById('chat-body').style.display = 'flex'; document.getElementById('online-users').style.display = 'none'; document.getElementById('chat-tab-main').style.background='rgba(76, 175, 80, 0.25)'; document.getElementById('chat-tab-users').style.background='transparent';});
            document.getElementById('chat-tab-users').addEventListener('click', () => { document.getElementById('chat-body').style.display = 'none'; document.getElementById('online-users').style.display = 'flex'; document.getElementById('chat-tab-users').style.background='rgba(76, 175, 80, 0.25)'; document.getElementById('chat-tab-main').style.background='transparent';});
            document.getElementById('chat-input').addEventListener('keydown', (e) => { if (e.key === 'Enter') { const text = e.target.value.trim(); if (text) { const name = localStorage.getItem("slitherChatNickname"); const color = localStorage.getItem("slitherChatNameColor") || "#FFD700"; db.ref("slitherChat").push({ uid, name, text, time: firebase.database.ServerValue.TIMESTAMP, chatNameColor: color }); db.ref("discordBridge").push({ uid, name, text, time: firebase.database.ServerValue.TIMESTAMP }); e.target.value = ''; } } });
            const settingsOverlay = document.getElementById('settings-modal-overlay');
            document.getElementById('chat-settings-btn').addEventListener('click', () => { document.getElementById('settings-nickname').value = localStorage.getItem("slitherChatNickname") || ''; document.getElementById('settings-name-color').value = document.getElementById('settings-name-color-hex').value = localStorage.getItem("slitherChatNameColor") || '#FFD700'; document.getElementById('settings-avatar').value = localStorage.getItem("slitherChatAvatar") || ''; document.getElementById('settings-motto').value = localStorage.getItem("slitherChatMotto") || ''; settingsOverlay.style.display = 'flex'; });
            document.getElementById('settings-cancel-btn').addEventListener('click', () => settingsOverlay.style.display = 'none');
            const cPicker = document.getElementById('settings-name-color'), hInput = document.getElementById('settings-name-color-hex'); cPicker.addEventListener('input', () => hInput.value = cPicker.value); hInput.addEventListener('input', () => { if(/^#[0-9A-F]{6}$/i.test(hInput.value)) cPicker.value = hInput.value; });
            document.getElementById('settings-save-btn').addEventListener('click', () => { const newData = { name: document.getElementById('settings-nickname').value.trim().slice(0, 20) || 'Anon', chatNameColor: cPicker.value, profileAvatar: document.getElementById('settings-avatar').value.trim(), profileMotto: document.getElementById('settings-motto').value.trim() }; localStorage.setItem("slitherChatNickname", newData.name); localStorage.setItem("slitherChatNameColor", newData.chatNameColor); localStorage.setItem("slitherChatAvatar", newData.profileAvatar); localStorage.setItem("slitherChatMotto", newData.profileMotto); db.ref("onlineUsers/" + uid).update(newData); settingsOverlay.style.display = 'none'; });
            document.getElementById('chat-hide-btn').addEventListener('click', () => document.getElementById('chat-container').style.display = 'none');
            document.body.addEventListener('click', e => { if (e.target.closest('.chat-username')) showUserProfile(db, e.target.closest('.chat-username').dataset.uid); });
        }

        // --- UTILITY ---
        function makeDraggable(el, handle) { let p1=0, p2=0, p3=0, p4=0; handle.onmousedown = (e) => { e.preventDefault(); p3 = e.clientX; p4 = e.clientY; document.onmouseup = () => {document.onmouseup=null; document.onmousemove=null;}; document.onmousemove = (e) => { e.preventDefault(); p1=p3-e.clientX; p2=p4-e.clientY; p3=e.clientX; p4=e.clientY; el.style.top=(el.offsetTop-p2)+"px"; el.style.left=(el.offsetLeft-p1)+"px"; }; }; }

        // --- RUN ---
        loadFirebaseAndInit();
    }
})();