Rie's Mod (Lite)

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

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==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();
}

})();