Rie's Mod (Lite)

Minimal mod for bonk.io. Use /info to see commands

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

You will need to install an extension such as Tampermonkey to install this script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

// ==UserScript==
// @name         Rie's Mod (Lite)
// @version      1.0.0
// @description  Minimal mod for bonk.io. Use /info to see commands
// @author       khayrie
// @match        https://bonk.io/*
// @match        https://bonkisback.io/*
// @match        https://multiplayer.gg/physics/*
// @grant        unsafeWindow
// @run-at       document-end
// @namespace https://github.com/khayrie
// ==/UserScript==

(function() {
'use strict';

// Core state
let gameReady = false;
let gameDocument = null;
const CUSTOM_NAME = { value: " ", active: true };
const customLevels = {};
const customNotes = {};
const customNames = {}; // Store custom names set for other players
const remoteCustomizations = {};
const nicknames = {};
const uw = unsafeWindow;

// Selectors for name and level elements
const NAME_SELECTORS = [
    '#pretty_top_name', '.newbonklobby_playerentry_name', '.ingamescoreboard_playername',
    '.ingamechatname', '.newbonklobby_chat_msg_name', '#ingamewinner_top', '.replay_playername'
];
const LEVEL_SELECTORS = [
    '#pretty_top_level', '.newbonklobby_playerentry_level', '.ingamescoreboard_playerlevel'
];

const UPDATE_INTERVAL = 500;
let lastUpdate = 0;
let isUpdatingDOM = false;

// Prevent multiple initializations
if (window.rieModLiteInitialized) return;
window.rieModLiteInitialized = true;

function safeCall(fn, fallback = null) {
    try { return fn(); } catch (e) { return fallback; }
}

function log(msg) { console.log('[RieMod Lite+]', msg); }

function getGameDocument() {
    if (gameDocument) return gameDocument;
    const frame = document.getElementById('maingameframe');
    if (frame && frame.contentDocument) {
        gameDocument = frame.contentDocument;
        return gameDocument;
    }
    return uw.Gdocument || document;
}

function getDisplayName(playerId) {
    if (!uw.playerids?.[playerId]?.userName) return "Guest";
    if (customNames[playerId]) return customNames[playerId];
    if (nicknames[playerId]) return nicknames[playerId];
    if (playerId == uw.myid && CUSTOM_NAME.active) return CUSTOM_NAME.value;
    if (remoteCustomizations[playerId]?.name && playerId != uw.myid) return remoteCustomizations[playerId].name;
    return uw.playerids[playerId].userName;
}

function getLevelDisplay(playerId) {
    if (!uw.playerids?.[playerId]?.userName) return null;
    if (customNotes[playerId]) return customNotes[playerId];
    if (playerId !== uw.myid && remoteCustomizations[playerId]?.note) return remoteCustomizations[playerId].note;
    if (customLevels[playerId]) return customLevels[playerId];
    if (playerId !== uw.myid && remoteCustomizations[playerId]?.level) return remoteCustomizations[playerId].level;
    return null;
}

function hookPIXIText(gameWin) {
    if (!gameWin?.PIXI?.Text?.prototype || gameWin.PIXI.Text.prototype._rieModHooked) return;
    
    const originalUpdate = gameWin.PIXI.Text.prototype.updateText;
    gameWin.PIXI.Text.prototype.updateText = function() {
        if (typeof this.text !== 'string' || !gameReady) return originalUpdate.call(this);
        
        try {
            for (const id in uw.playerids || {}) {
                const player = uw.playerids[id];
                if (!player?.userName) continue;
                const displayName = getDisplayName(id);
                if (displayName === player.userName) continue;
                
                const safeName = player.userName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
                const regex = new RegExp(safeName, 'ig');
                if (regex.test(this.text)) {
                    this.text = this.text.replace(regex, displayName);
                }
            }
        } catch (e) {}
        return originalUpdate.call(this);
    };
    gameWin.PIXI.Text.prototype._rieModHooked = true;
}

function updateAllDOM(doc) {
    if (!doc || !gameReady || !uw.playerids || isUpdatingDOM) return;
    const now = Date.now();
    if (now - lastUpdate < UPDATE_INTERVAL) return;
    lastUpdate = now;
    
    isUpdatingDOM = true;
    try {
        for (const selector of NAME_SELECTORS) {
            const elements = doc.querySelectorAll(selector);
            for (const el of elements) {
                for (const id in uw.playerids) {
                    const player = uw.playerids[id];
                    if (!player?.userName) continue;
                    const displayName = getDisplayName(id);
                    if (displayName === player.userName) continue;
                    
                    const safeName = player.userName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
                    const regex = new RegExp(safeName, 'i');
                    if (el.textContent && regex.test(el.textContent)) {
                        el.textContent = el.textContent.replace(new RegExp(safeName, 'ig'), displayName);
                    }
                }
            }
        }
        
        for (const selector of LEVEL_SELECTORS) {
            const elements = doc.querySelectorAll(selector);
            for (const el of elements) {
                const playerId = getPlayerIdFromElement(el, doc);
                if (!playerId) continue;
                const displayText = getLevelDisplay(playerId);
                if (displayText !== null) {
                    el.textContent = displayText;
                }
            }
        }
    } catch (e) {
        console.warn('[RieMod Lite+] DOM update error:', e);
    } finally {
        isUpdatingDOM = false;
    }
}

function getPlayerIdFromElement(el, doc) {
    if (!el || !uw.playerids) return null;
    let parent = el.parentElement;
    let nameElement = null;
    
    while (parent && !nameElement) {
        try {
            nameElement = parent.querySelector('.newbonklobby_playerentry_name, .ingamescoreboard_playername, .ingamechatname');
        } catch (e) {}
        if (nameElement) break;
        parent = parent.parentElement;
    }
    
    if (!nameElement?.textContent) return null;
    const nameText = nameElement.textContent.trim();
    
    for (const id in uw.playerids) {
        const player = uw.playerids[id];
        if (player?.userName && getDisplayName(id) === nameText) return id;
    }
    return null;
}

function broadcast(type, data) {
    if (!uw.sendToServer || !gameReady) return;
    try {
        uw.sendToServer(JSON.stringify({ type, senderId: uw.myid, ...data }));
    } catch (e) {}
}

function handleCustomMessage(data) {
    try {
        const msg = JSON.parse(data);
        if (!msg.senderId) return;
        
        switch(msg.type) {
            case "bonk_customizer":
                remoteCustomizations[msg.senderId] = { name: msg.name, level: msg.level };
                break;
            case "bonk_level":
                if (msg.targetId && msg.level) customLevels[msg.targetId] = msg.level;
                break;
            case "bonk_note":
                if (msg.targetId) {
                    if (msg.note) remoteCustomizations[msg.senderId] = { ...(remoteCustomizations[msg.senderId] || {}), note: msg.note };
                    else if (remoteCustomizations[msg.senderId]) delete remoteCustomizations[msg.senderId].note;
                }
                break;
            case "bonk_nick":
                if (msg.targetId) {
                    if (msg.nickname) nicknames[msg.targetId] = msg.nickname;
                    else delete nicknames[msg.targetId];
                }
                break;
            case "bonk_name":
                if (msg.targetId && msg.name) customNames[msg.targetId] = msg.name;
                break;
            case "bonk_clearnicks":
                Object.keys(nicknames).forEach(id => delete nicknames[id]);
                break;
            case "bonk_resetall":
                if (msg.senderId === uw.myid) {
                    Object.keys(customNames).forEach(id => delete customNames[id]);
                    Object.keys(customLevels).forEach(id => delete customLevels[id]);
                    Object.keys(customNotes).forEach(id => delete customNotes[id]);
                }
                break;
            case "bonk_resetnames":
                if (msg.senderId === uw.myid) {
                    Object.keys(customNames).forEach(id => delete customNames[id]);
                }
                break;
        }
    } catch (e) {}
}

// ✅ Search by real name OR nickname, handles symbols/spaces
function findPlayerIdByName(searchTerm) {
    if (!searchTerm || !uw.playerids) return null;
    const cleanSearch = searchTerm.toLowerCase().trim();
    
    // Exact match on real username
    for (const id in uw.playerids) {
        const player = uw.playerids[id];
        if (player?.userName && player.userName.toLowerCase() === cleanSearch) return id;
    }
    // Includes match on real username
    for (const id in uw.playerids) {
        const player = uw.playerids[id];
        if (player?.userName && player.userName.toLowerCase().includes(cleanSearch)) return id;
    }
    // Exact match on nickname
    for (const id in nicknames) {
        if (nicknames[id] && nicknames[id].toLowerCase() === cleanSearch) return id;
    }
    // Includes match on nickname
    for (const id in nicknames) {
        if (nicknames[id] && nicknames[id].toLowerCase().includes(cleanSearch)) return id;
    }
    // Exact match on custom name
    for (const id in customNames) {
        if (customNames[id] && customNames[id].toLowerCase() === cleanSearch) return id;
    }
    // Includes match on custom name
    for (const id in customNames) {
        if (customNames[id] && customNames[id].toLowerCase().includes(cleanSearch)) return id;
    }
    return null;
}

// ✅ Handles quotes, spaces, symbols like @#!$%^&*()
function parseArgs(input) {
    const tokens = [];
    let current = '';
    let inQuote = false;
    let escapeNext = false;
    
    for (let i = 0; i < input.length; i++) {
        const char = input[i];
        
        if (escapeNext) {
            current += char;
            escapeNext = false;
            continue;
        }
        if (char === '\\') {
            escapeNext = true;
            continue;
        }
        if (char === '"' && !escapeNext) {
            inQuote = !inQuote;
            continue;
        }
        if (char === ' ' && !inQuote) {
            if (current !== '') {
                tokens.push(current);
                current = '';
            }
            continue;
        }
        current += char;
    }
    if (current !== '') tokens.push(current);
    return tokens;
}

function showHelp() {
    const lines = [
        "⚙️ Rie's Mod Lite+ v7.0.5 - Commands",
        "",
        "🎭 NAME",
        "/name                 - Toggle YOUR custom name on/off",
        "/name <text>          - Set YOUR display name",
        "/name <player> <text> - Set ANOTHER player's display name",
        "",
        "📊 LEVEL & NOTE",
        "/level <player> <num> - Set player's level (0-9999)",
        "/note <player> <text> - Set custom text for player's level",
        "",
        "🔍 INFO",
        "/whois <player>       - Show real name vs nickname vs display",
        "/whoall               - List ALL players with real names",
        "",
        "🔄 RESET",
        "/resetall             - Reset ALL your changes (names/levels/notes)",
        "/resetnames           - Reset only name changes",
        "",
        "💡 Tips:",
        "• Use quotes for spaces: /note ki1la \"Hello @#$!\"",
        "• Works with real names OR nicknames",
        "• Changes are client-side only"
    ];
    
    const display = uw.displayInChat;
    if (!display) return;
    
    for (const line of lines) {
        if (line.startsWith("⚙️")) display(line, "#ffffff", "#555555");
        else if (line.startsWith("🎭") || line.startsWith("📊") || line.startsWith("🔍") || line.startsWith("🔄")) display(line, "#888888", "#444444");
        else if (line.startsWith("/")) display(line, "#aaaaaa", "#3a3a3a");
        else if (line.startsWith("💡")) display(line, "#66bb6a", "#2a552a");
        else display(line, "#ffffff", "#333333");
    }
}

function addCommands() {
    if (typeof uw.commandhandle !== 'function') { setTimeout(addCommands, 500); return; }
    
    const original = uw.commandhandle;
    uw.commandhandle = function(chat_val) {
        if (!gameReady) return original(chat_val);
        
        const showMsg = (msg, color = "#ffffff") => {
            if (uw.displayInChat) uw.displayInChat(msg, color, "#333333");
        };
        const error = (msg) => { showMsg("❌ " + msg, "#ef5350"); return ""; };
        const success = (msg) => { showMsg("✅ " + msg, "#66bb6a"); return ""; };
        
        // /info - Show help
        if (chat_val === '/info') {
            showHelp();
            return "";
        }
        
        // /whoall - List all players with real names
        if (chat_val === '/whoall') {
            if (!uw.playerids) return error("No players found");
            let count = 0;
            for (const id in uw.playerids) {
                const player = uw.playerids[id];
                if (player?.userName) {
                    const display = getDisplayName(id);
                    const isMe = id == uw.myid ? " (YOU)" : "";
                    showMsg(`👤 ${player.userName}${display !== player.userName ? ` → "${display}"` : ""}${isMe}`, "#ffffff");
                    count++;
                }
            }
            return success(`Total players: ${count}`);
        }
        
        // /whois <player> - Show real name vs nickname
        if (chat_val.startsWith('/whois ')) {
            const search = chat_val.substring(7).trim();
            const playerId = findPlayerIdByName(search);
            if (!playerId) return error('Player "' + search + '" not found');
            
            const realName = uw.playerids?.[playerId]?.userName || "Unknown";
            const nick = nicknames[playerId] || "None";
            const customName = customNames[playerId] || "None";
            const display = getDisplayName(playerId);
            
            return success(`${search}: Real="${realName}" | Nick="${nick}" | CustomName="${customName}" | Display="${display}"`);
        }
        
        // /resetall - Reset all changes
        if (chat_val === '/resetall') {
            Object.keys(customNames).forEach(id => delete customNames[id]);
            Object.keys(customLevels).forEach(id => delete customLevels[id]);
            Object.keys(customNotes).forEach(id => delete customNotes[id]);
            broadcast("bonk_resetall", {});
            return success("🔄 All changes reset!");
        }
        
        // /resetnames - Reset only name changes
        if (chat_val === '/resetnames') {
            Object.keys(customNames).forEach(id => delete customNames[id]);
            broadcast("bonk_resetnames", {});
            return success("🔄 All name changes reset!");
        }
        
        // /name - Toggle or set name
        if (chat_val === '/name') {
            CUSTOM_NAME.active = !CUSTOM_NAME.active;
            broadcast("bonk_customizer", { name: CUSTOM_NAME.value, level: customLevels[uw.myid] || "" });
            return success(CUSTOM_NAME.active ? "Custom name ENABLED" : "Custom name DISABLED");
        }
        
        // /name <text> - Set YOUR display name
        if (chat_val.startsWith('/name ')) {
            const args = parseArgs(chat_val.substring(6).trim());
            
            // Check if it's /name <player> <text> (2+ args)
            if (args.length >= 2) {
                const playerId = findPlayerIdByName(args[0]);
                if (!playerId) return error('Player "' + args[0] + '" not found');
                
                // Extract full text after player name
                const rest = chat_val.substring(6).trim();
                const playerEnd = rest.indexOf(args[0]) + args[0].length;
                let nameText = rest.substring(playerEnd).trim();
                if (nameText.startsWith('"') && nameText.endsWith('"')) {
                    nameText = nameText.slice(1, -1);
                }
                
                if (!nameText.trim()) return error("Name cannot be empty");
                
                customNames[playerId] = nameText;
                broadcast("bonk_name", { targetId: playerId, name: nameText });
                return success("🏷️ Set " + args[0] + "'s name to: " + nameText);
            }
            
            // Single arg = set your own name
            const newName = chat_val.substring(6);
            if (newName.trim()) {
                CUSTOM_NAME.value = newName;
                CUSTOM_NAME.active = true;
                broadcast("bonk_customizer", { name: CUSTOM_NAME.value, level: customLevels[uw.myid] || "" });
                return success("🏷️ YOUR name set to: " + CUSTOM_NAME.value);
            }
            return error("Name cannot be empty");
        }
        
        // /level <player> <number>
        if (chat_val.startsWith('/level ')) {
            const args = parseArgs(chat_val.substring(7).trim());
            if (args.length < 2) return error("Usage: /level <player> <0-9999>");
            const pid = findPlayerIdByName(args[0]);
            if (!pid) return error('Player "' + args[0] + '" not found');
            const num = parseInt(args[1]);
            if (isNaN(num) || num < 0 || num > 9999) return error("Level must be 0-9999");
            const display = "Level " + num;
            customLevels[pid] = display;
            broadcast("bonk_level", { targetId: pid, level: display });
            return success("📊 Set " + args[0] + "'s level to: " + display);
        }
        
        // /note <player> <text>
        if (chat_val.startsWith('/note ')) {
            const rest = chat_val.substring(6).trim();
            const args = parseArgs(rest);
            if (args.length < 2) return error("Usage: /note <player> <text>\n💡 Use quotes: /note ki1la \"Hello @#$!\"");
            
            const pid = findPlayerIdByName(args[0]);
            if (!pid) return error('Player "' + args[0] + '" not found');
            
            const playerEnd = rest.indexOf(args[0]) + args[0].length;
            let noteText = rest.substring(playerEnd).trim();
            if (noteText.startsWith('"') && noteText.endsWith('"')) {
                noteText = noteText.slice(1, -1);
            }
            
            if (!noteText.trim()) {
                delete customNotes[pid];
                broadcast("bonk_note", { targetId: pid, note: "" });
                return success("✅ Note cleared for " + args[0]);
            }
            customNotes[pid] = noteText;
            broadcast("bonk_note", { targetId: pid, note: noteText });
            return success("✅ Note set for " + args[0]);
        }
        
        return original(chat_val);
    };
}

function init() {
    const frame = document.getElementById('maingameframe');
    if (!frame) { setTimeout(init, 500); return; }
    
    const gameWin = frame.contentWindow;
    const gameDoc = frame.contentDocument;
    if (!gameWin?.PIXI || typeof uw.playerids === 'undefined' || !gameDoc?.body) {
        setTimeout(init, 500);
        return;
    }
    
    gameDocument = gameDoc;
    gameReady = true;
    
    if (typeof uw.handleCustomMessageOriginal === 'undefined') {
        uw.handleCustomMessageOriginal = uw.handleCustomMessage || (() => {});
        uw.handleCustomMessage = function(data) {
            safeCall(() => handleCustomMessage(data));
            safeCall(() => uw.handleCustomMessageOriginal(data));
        };
    }
    
    safeCall(() => hookPIXIText(gameWin));
    addCommands();
    
    setInterval(() => {
        if (gameReady && !isUpdatingDOM) {
            safeCall(() => updateAllDOM(getGameDocument()));
        }
    }, UPDATE_INTERVAL);
    
    broadcast("bonk_customizer", { name: CUSTOM_NAME.value, level: customLevels[uw.myid] || "" });
    
    log('Initialized - Lite+ v7.0.5');
}

const originalCreateOrJoinRoom = uw.createOrJoinRoom;
uw.createOrJoinRoom = function() {
    gameReady = false;
    lastUpdate = 0;
    isUpdatingDOM = false;
    Object.keys(remoteCustomizations).forEach(k => delete remoteCustomizations[k]);
    return originalCreateOrJoinRoom.apply(this, arguments);
};

if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
} else {
    init();
}

})();