Drawaria Draw-on-Avatar (Beta Privada)

Choose an object and draw it on the selected player’s avatar

当前为 2025-08-10 提交的版本,查看 最新版本

// ==UserScript==
// @name         Drawaria Draw-on-Avatar (Beta Privada)
// @namespace    http://tampermonkey.net/
// @version      1.5
// @description  Choose an object and draw it on the selected player’s avatar
// @author       YouTubeDrawaria
// @match        https://drawaria.online/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=drawaria.online
// @grant        GM_xmlhttpRequest
// @license MIT
// ==/UserScript==

(function () {
    'use strict';

    /* ----------  CONFIGURACIÓN  ---------- */
const JSON_SOURCES = {
    'Fuego': 'https://raw.githubusercontent.com/DrawariaDeveloper/Json-to-Drawaria/main/fire.json',
    'Fuego blue': 'https://raw.githubusercontent.com/DrawariaDeveloper/Json-to-Drawaria/main/bluefire.json',
    'Laser': 'https://raw.githubusercontent.com/DrawariaDeveloper/Json-to-Drawaria/main/laser.json',
    'Ataque': 'https://raw.githubusercontent.com/DrawariaDeveloper/Json-to-Drawaria/main/ataque.json',
    'Cohete': 'https://raw.githubusercontent.com/DrawariaDeveloper/Json-to-Drawaria/main/cohete.json',
    'Defensa': 'https://raw.githubusercontent.com/DrawariaDeveloper/Json-to-Drawaria/main/defensa.json',
    'Espada': 'https://raw.githubusercontent.com/DrawariaDeveloper/Json-to-Drawaria/main/espada.json',
    'Explosion': 'https://raw.githubusercontent.com/DrawariaDeveloper/Json-to-Drawaria/main/explosion.json',
    'Gorra': 'https://raw.githubusercontent.com/DrawariaDeveloper/Json-to-Drawaria/main/gorra.json',
    'Pistola': 'https://raw.githubusercontent.com/DrawariaDeveloper/Json-to-Drawaria/main/pistola.json',
    'Rayo': 'https://raw.githubusercontent.com/DrawariaDeveloper/Json-to-Drawaria/main/rayo.json'
};
    const DEFAULT_JSON_NAME = 'Fuego';

    const DRAW_PADDING = 10; // Espacio en píxeles entre el avatar y el dibujo para posiciones laterales/superiores/inferiores
    const DRAW_PADDING_HAND = 3; // ¡NUEVO! Espacio personalizado para las posiciones de agarre
    const HAND_GRIP_OFFSET_Y = 2; // Desplazamiento vertical para las posiciones de agarre (simula la altura de la mano)

    /* ------------------------------------ */

    let socket;
    const canvas = document.getElementById('canvas');
    // Obtenemos el contexto 2D para dibujar localmente en el canvas de Drawaria
    const ctx = canvas ? canvas.getContext('2d') : null; // Nos aseguramos de que el canvas exista

    const originalSend = WebSocket.prototype.send;
    WebSocket.prototype.send = function (...args) {
        if (!socket) socket = this;
        return originalSend.apply(this, args);
    };

    /* ----------  INTERFAZ DE USUARIO (UI)  ---------- */
    const container = document.createElement('div');
    container.style.cssText = `
        position:fixed; bottom:10px; right:10px; z-index:9999;
        background:rgba(17,17,17,0.85); /* Fondo oscuro ligeramente transparente */
        color:#fff; padding:10px 15px; border-radius:8px;
        font-family: 'Segoe UI', Arial, sans-serif; font-size:13px;
        display:flex; flex-direction:column; gap:10px;
        box-shadow: 0 4px 12px rgba(0,0,0,0.5); /* Sombra más pronunciada */
        cursor: default;
        backdrop-filter: blur(3px); /* Efecto de desenfoque para un aspecto moderno */
    `;

    // Barra de título para arrastrar el panel
    const titleBar = document.createElement('div');
    titleBar.textContent = 'Dibujar en Avatar';
    titleBar.style.cssText = `
        font-weight: bold;
        font-size: 14px;
        text-align: center;
        cursor: grab;
        background: rgba(30,30,30,0.9);
        border-radius: 6px 6px 0 0;
        margin: -10px -15px 10px -15px;
        padding: 10px 15px;
        border-bottom: 1px solid #444; /* Separador sutil */
    `;
    container.appendChild(titleBar);

    const contentDiv = document.createElement('div');
    contentDiv.style.cssText = `
        display:flex; flex-direction:column; gap:8px;
    `;
    container.appendChild(contentDiv);

    // Etiqueta y estilo para todos los selectores
    const selectBaseStyle = `
        flex-grow: 1;
        padding: 7px 10px; border-radius: 5px; border: 1px solid #555;
        background: #333; color: #fff;
        font-size: 13px;
        appearance: none;
        background-image: url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23ffffff%22%20d%3D%22M287%2C197.3L159.2%2C69.5c-3.6-3.6-8.2-5.4-12.8-5.4s-9.2%2C1.8-12.8%2C5.4L5.4%2C197.3c-7.2%2C7.2-7.2%2C18.8%2C0%2C26c3.6%2C3.6%2C8.2%2C5.4%2C12.8%2C5.4s9.2%2C1.8%2C12.8%2C5.4l117%2C117c3.6%2C3.6%2C8.2%2C5.4%2C12.8%2C5.4s9.2%2C1.8%2C12.8%2C5.4l117-117c7.2-7.2%2C7.2-18.8%2C0-26C294.2%2C204.5%2C294.2%2C200.9%2C287%2C197.3z%22%2F%3E%3C%2Fsvg%3E');
        background-repeat: no-repeat;
        background-position: right 8px center;
        background-size: 10px;
        cursor: pointer;
    `;

    // Selector de jugador
    const playerSelectWrapper = document.createElement('div');
    playerSelectWrapper.style.cssText = `display:flex; align-items:center; gap:8px;`;
    playerSelectWrapper.innerHTML = '<span>Jugador:</span>';
    const playerSelect = document.createElement('select');
    playerSelect.style.cssText = selectBaseStyle;
    playerSelectWrapper.appendChild(playerSelect);
    contentDiv.appendChild(playerSelectWrapper);

    // Selector de URL de dibujo
    const urlSelectWrapper = document.createElement('div');
    urlSelectWrapper.style.cssText = `display:flex; align-items:center; gap:8px;`;
    urlSelectWrapper.innerHTML = '<span>Dibujo:</span>';
    const jsonUrlSelect = document.createElement('select');
    jsonUrlSelect.style.cssText = selectBaseStyle;
    for (const name in JSON_SOURCES) {
        const opt = document.createElement('option');
        opt.value = JSON_SOURCES[name];
        opt.textContent = name;
        jsonUrlSelect.appendChild(opt);
    }
    jsonUrlSelect.value = JSON_SOURCES[DEFAULT_JSON_NAME];
    urlSelectWrapper.appendChild(jsonUrlSelect);
    contentDiv.appendChild(urlSelectWrapper);

    // Selector de Posición
    const positionSelectWrapper = document.createElement('div');
    positionSelectWrapper.style.cssText = `display:flex; align-items:center; gap:8px;`;
    positionSelectWrapper.innerHTML = '<span>Posición:</span>';
    const positionSelect = document.createElement('select');
    positionSelect.style.cssText = selectBaseStyle;
    const positions = {
        'Centrado': 'centered',
        'Derecha': 'right',
        'Arriba': 'top',
        'Izquierda': 'left',
        'Abajo': 'bottom',
        'Cabeza': 'head',         // Nueva posición: en la cabeza
        'Agarre Derecha': 'grip_right',  // Nueva posición: para simular agarre en mano derecha
        'Agarre Izquierda': 'grip_left'  // Nueva posición: para simular agarre en mano izquierda
    };
    for (const name in positions) {
        const opt = document.createElement('option');
        opt.value = positions[name];
        opt.textContent = name;
        positionSelect.appendChild(opt);
    }
    positionSelect.value = 'head'; // Establecido a 'head' por defecto, como en la imagen deseada
    positionSelectWrapper.appendChild(positionSelect);
    contentDiv.appendChild(positionSelectWrapper);

    // Botón de dibujo
    const drawBtn = document.createElement('button');
    drawBtn.textContent = 'Dibujar en avatar';
    drawBtn.disabled = true;
    drawBtn.style.cssText = `
        padding: 9px 15px; border-radius: 6px; border: none;
        background: linear-gradient(145deg, #4CAF50, #45a049);
        color: white; font-weight: bold; font-size: 14px;
        cursor: pointer;
        transition: all 0.2s ease;
        box-shadow: 0 2px 5px rgba(0,0,0,0.3);

        &:hover {
            background: linear-gradient(145deg, #45a049, #3d8c41);
            box-shadow: 0 4px 10px rgba(0,0,0,0.4);
            transform: translateY(-1px);
        }
        &:active {
            transform: translateY(0);
            box-shadow: 0 1px 3px rgba(0,0,0,0.2);
        }
        &:disabled {
            background: #666; cursor: not-allowed;
            box-shadow: none;
            opacity: 0.7;
        }
    `;
    contentDiv.appendChild(drawBtn);

    document.body.appendChild(container);

    /* ----------  FUNCIONALIDAD DE ARRASTRE (DRAGGABLE)  ---------- */
    let isDragging = false;
    let offsetX, offsetY;

    titleBar.addEventListener('mousedown', (e) => {
        isDragging = true;
        offsetX = e.clientX - container.getBoundingClientRect().left;
        offsetY = e.clientY - container.getBoundingClientRect().top;
        container.style.cursor = 'grabbing';
        container.style.transition = 'none'; // Deshabilita la transición durante el arrastre
    });

    document.addEventListener('mousemove', (e) => {
        if (!isDragging) return;
        // Evita que el panel se salga de los límites de la ventana
        let newX = e.clientX - offsetX;
        let newY = e.clientY - offsetY;

        newX = Math.max(0, Math.min(newX, window.innerWidth - container.offsetWidth));
        newY = Math.max(0, Math.min(newY, window.innerHeight - container.offsetHeight));

        container.style.left = newX + 'px';
        container.style.top = newY + 'px';
    });

    document.addEventListener('mouseup', () => {
        isDragging = false;
        container.style.cursor = 'default';
        container.style.transition = ''; // Vuelve a habilitar la transición
    });

    /* ----------  LISTA DE JUGADORES  ---------- */
    /* ----------  LISTA DE JUGADORES (VERSIÓN MEJORADA)  ---------- */
    let lastPlayerList = new Set(); // Para detectar cambios reales
    let isUpdatingList = false; // Bandera para prevenir actualizaciones múltiples

    function refreshPlayerList() {
        if (isUpdatingList) return; // Previene ejecuciones múltiples simultáneas

        // Construye un Set con los jugadores actuales para comparar
        const currentPlayers = new Set();
        const playerRows = document.querySelectorAll('.playerlist-row[data-playerid]');

        playerRows.forEach(row => {
            if (row.dataset.self !== 'true' && row.dataset.playerid !== '0') {
                const name = row.querySelector('.playerlist-name a')?.textContent || `Jugador ${row.dataset.playerid}`;
                currentPlayers.add(`${row.dataset.playerid}:${name}`);
            }
        });

        // Solo actualiza si realmente hay cambios en la lista de jugadores
        const playersChanged = currentPlayers.size !== lastPlayerList.size ||
              ![...currentPlayers].every(player => lastPlayerList.has(player));

        if (!playersChanged) return; // No hay cambios reales, no actualizar

        isUpdatingList = true;

        // Guarda la selección actual ANTES de cualquier modificación
        const previousSelection = playerSelect.value;
        const previousSelectedText = playerSelect.selectedOptions?.textContent || '';

        // Limpia y reconstruye la lista
        playerSelect.innerHTML = '';

        playerRows.forEach(row => {
            if (row.dataset.self === 'true') return;
            if (row.dataset.playerid === '0') return;
            const name = row.querySelector('.playerlist-name a')?.textContent || `Jugador ${row.dataset.playerid}`;
            const opt = document.createElement('option');
            opt.value = row.dataset.playerid;
            opt.textContent = name;
            playerSelect.appendChild(opt);
        });

        // ESTRATEGIA MEJORADA DE RESTAURACIÓN
        if (previousSelection) {
            // Intento 1: Restaurar por ID del jugador
            let restored = false;
            for (let option of playerSelect.options) {
                if (option.value === previousSelection) {
                    playerSelect.value = previousSelection;
                    restored = true;
                    break;
                }
            }

            // Intento 2: Si el ID no existe, buscar por nombre
            if (!restored && previousSelectedText) {
                for (let option of playerSelect.options) {
                    if (option.textContent === previousSelectedText) {
                        playerSelect.value = option.value;
                        restored = true;
                        break;
                    }
                }
            }
        }

        // Actualiza el registro de jugadores
        lastPlayerList = new Set(currentPlayers);

        drawBtn.disabled = playerSelect.children.length === 0;
        isUpdatingList = false;
    }

    // Versión optimizada del MutationObserver con debounce
    let refreshTimeout;
    function debouncedRefresh() {
        clearTimeout(refreshTimeout);
        refreshTimeout = setTimeout(refreshPlayerList, 100); // Espera 100ms antes de actualizar
    }


    /* ----------  ANÁLISIS DE JSON DE DIBUJO  ---------- */
    function analyzeJsonBounds(jsonCommands) {
        let min_nx = Infinity, max_nx = -Infinity;
        let min_ny = Infinity, max_ny = -Infinity;

        if (!Array.isArray(jsonCommands) || jsonCommands.length === 0) {
            return { min_nx: 0, max_nx: 0, min_ny: 0, max_ny: 0 };
        }

        for (const cmdArr of jsonCommands) {
            if (cmdArr.length > 2 && Array.isArray(cmdArr[2]) && cmdArr[2].length >= 4) {
                const [nx1, ny1, nx2, ny2] = cmdArr[2];
                min_nx = Math.min(min_nx, nx1, nx2);
                max_nx = Math.max(max_nx, nx1, nx2);
                min_ny = Math.min(min_ny, ny1, ny2);
                max_ny = Math.max(max_ny, ny1, ny2);
            }
        }
        if (min_nx === Infinity || max_nx === -Infinity || min_ny === Infinity || max_ny === -Infinity) {
            return { min_nx: 0, max_nx: 0, min_ny: 0, max_ny: 0 };
        }
        return { min_nx, max_nx, min_ny, max_ny };
    }


    /* ----------  LÓGICA DE DIBUJO  ---------- */
    async function drawOnAvatar(playerId) {
        if (!socket) {
            alert('Socket no está listo. Asegúrate de estar en una partida de Drawaria para usarlo.');
            return;
        }
        const avatar = document.querySelector(`.spawnedavatar[data-playerid="${playerId}"]`);
        if (!avatar) {
            alert('Avatar no encontrado. Asegúrate de que el jugador esté visible en pantalla.');
            return;
        }

        // Obtiene las coordenadas y dimensiones del canvas y del avatar en pantalla
        const cRect = canvas.getBoundingClientRect();
        const aRect = avatar.getBoundingClientRect();

        // Coordenadas del avatar relativas al canvas
        const avatarX = aRect.left - cRect.left;
        const avatarY = aRect.top - cRect.top;
        const avatarWidth = aRect.width;
        const avatarHeight = aRect.height;
        const avatarCenterX = avatarX + avatarWidth / 2;
        const avatarCenterY = avatarY + avatarHeight / 2;

        // Obtiene el JSON del dibujo desde la URL seleccionada
        const url = jsonUrlSelect.value;
        const json = await fetchJson(url);
        if (!json || !Array.isArray(json.commands)) {
            alert('JSON inválido o no se pudo cargar el dibujo. Asegúrate de que el formato sea correcto y la URL accesible.');
            return;
        }

        // Analiza el JSON para obtener sus límites de coordenadas (ej. min_nx, max_nx)
        const { min_nx, max_nx, min_ny, max_ny } = analyzeJsonBounds(json.commands);

        // Calcula el ancho y alto del "bounding box" del dibujo en píxeles,
        // cuando se escala con canvas.width/height (como lo hace el script original).
        const actualDrawPxWidth = (max_nx - min_nx) * canvas.width;
        const actualDrawPxHeight = (max_ny - min_ny) * canvas.height;

        // Estas serán las coordenadas absolutas en el lienzo de Drawaria para el punto (0,0) del JSON
        // (Es decir, el offset que se suma a nx * canvas.width para posicionar el dibujo completo)
        let drawingOriginX;
        let drawingOriginY;

        const currentPosition = positionSelect.value;

        switch (currentPosition) {
            case 'centered':
                // Queremos que el centro del *borde visual del dibujo* esté en el centro del avatar.
                // El centro del dibujo (en coordenadas del JSON) es ((min_nx + max_nx) / 2, (min_ny + max_ny) / 2).
                // Su posición real en píxeles sería: (min_nx * canvas.width + actualDrawPxWidth / 2).
                drawingOriginX = avatarCenterX - (min_nx * canvas.width + actualDrawPxWidth / 2);
                drawingOriginY = avatarCenterY - (min_ny * canvas.height + actualDrawPxHeight / 2);
                break;
            case 'top':
                // Centrado horizontalmente, parte inferior del dibujo alineada con la parte superior del avatar + padding
                drawingOriginX = avatarCenterX - (min_nx * canvas.width + actualDrawPxWidth / 2);
                // La parte inferior del dibujo (min_ny * canvas.height + actualDrawPxHeight) debe estar en avatarY - DRAW_PADDING
                drawingOriginY = (avatarY - DRAW_PADDING) - (min_ny * canvas.height + actualDrawPxHeight);
                break;
            case 'bottom':
                // Centrado horizontalmente, parte superior del dibujo alineada con la parte inferior del avatar + padding
                drawingOriginX = avatarCenterX - (min_nx * canvas.width + actualDrawPxWidth / 2);
                // La parte superior del dibujo (min_ny * canvas.height) debe estar en avatarY + avatarHeight + DRAW_PADDING
                drawingOriginY = (avatarY + avatarHeight + DRAW_PADDING) - (min_ny * canvas.height);
                break;
            case 'left':
                // Centrado verticalmente, parte derecha del dibujo alineada con la parte izquierda del avatar - padding
                drawingOriginY = avatarCenterY - (min_ny * canvas.height + actualDrawPxHeight / 2);
                // La parte derecha del dibujo (min_nx * canvas.width + actualDrawPxWidth) debe estar en avatarX - DRAW_PADDING
                drawingOriginX = (avatarX - DRAW_PADDING) - (min_nx * canvas.width + actualDrawPxWidth);
                break;
            case 'right':
                // Centrado verticalmente, parte izquierda del dibujo alineada con la parte derecha del avatar + padding
                drawingOriginY = avatarCenterY - (min_ny * canvas.height + actualDrawPxHeight / 2);
                // La parte izquierda del dibujo (min_nx * canvas.width) debe estar en avatarX + avatarWidth + DRAW_PADDING
                drawingOriginX = (avatarX + avatarWidth + DRAW_PADDING) - (min_nx * canvas.width);
                break;
            case 'head':
                // Centrado horizontalmente, parte inferior del dibujo alineada con la parte superior del avatar (ajustado para "sombrero")
                drawingOriginX = avatarCenterX - (min_nx * canvas.width + actualDrawPxWidth / 2);
                drawingOriginY = avatarY - (min_ny * canvas.height + actualDrawPxHeight) + (avatarHeight * 0.1); // Pequeña superposición
                break;
            case 'grip_right':
                // A la derecha del avatar, verticalmente desplazado hacia abajo para simular agarre.
                // La parte izquierda del dibujo (min_nx * canvas.width) debe estar a la derecha del avatar + padding personalizado
                drawingOriginX = (avatarX + avatarWidth + DRAW_PADDING_HAND) - (min_nx * canvas.width);
                // Centro vertical del dibujo (min_ny * canvas.height + actualDrawPxHeight / 2) debe estar en el centro del avatar + offset
                drawingOriginY = avatarCenterY - (min_ny * canvas.height + actualDrawPxHeight / 2) + HAND_GRIP_OFFSET_Y;
                break;
            case 'grip_left':
                // A la izquierda del avatar, verticalmente desplazado hacia abajo para simular agarre.
                // La parte derecha del dibujo (min_nx * canvas.width + actualDrawPxWidth) debe estar a la izquierda del avatar - padding personalizado
                drawingOriginX = (avatarX - DRAW_PADDING_HAND) - (min_nx * canvas.width + actualDrawPxWidth);
                // Centro vertical del dibujo (min_ny * canvas.height + actualDrawPxHeight / 2) debe estar en el centro del avatar + offset
                drawingOriginY = avatarCenterY - (min_ny * canvas.height + actualDrawPxHeight / 2) + HAND_GRIP_OFFSET_Y;
                break;
            default: // Fallback a centrado si la posición es inválida
                drawingOriginX = avatarCenterX - (min_nx * canvas.width + actualDrawPxWidth / 2);
                drawingOriginY = avatarCenterY - (min_ny * canvas.height + actualDrawPxHeight / 2);
                break;
        }

        // Dibuja cada segmento
        for (const cmdArr of json.commands) {
            const [, , [nx1, ny1, nx2, ny2, , thickNeg, color]] = cmdArr;

            // Aplica la escala original (nx * canvas.width) y luego traslada con el origen calculado
            const x1 = nx1 * canvas.width + drawingOriginX;
            const y1 = ny1 * canvas.height + drawingOriginY;
            const x2 = nx2 * canvas.width + drawingOriginX;
            const y2 = ny2 * canvas.height + drawingOriginY;

            // Llama a sendDrawCommand, que ahora también dibujará localmente
            sendDrawCommand(x1, y1, x2, y2, color, -thickNeg);
            await new Promise(r => setTimeout(r, 15)); // Pequeño retraso para visibilidad y evitar saturar el servidor
        }
    }

    // Envía el comando de dibujo al socket de Drawaria Y DIBUJA LOCALMENTE EN EL CANVAS
    function sendDrawCommand(x1, y1, x2, y2, color, thickness) {
        // --- Dibujo local (visibilidad del lado del cliente) ---
        if (ctx && canvas) { // Asegúrate de que el contexto 2D y el canvas están disponibles
            ctx.strokeStyle = color;
            ctx.lineWidth = thickness; // Usa la "thickness" proporcionada (ya es positiva)
            ctx.lineCap = 'round'; // Para uniones de línea suaves, como en Drawaria

            ctx.beginPath();
            ctx.moveTo(x1, y1);
            ctx.lineTo(x2, y2);
            ctx.stroke();
        }
        // --- Fin del dibujo local ---

        if (!socket) return; // Si el socket no está listo, no se envía al servidor

        // Drawaria espera coordenadas normalizadas (0-1) respecto al tamaño total del lienzo (canvas)
        const normX1 = (x1 / canvas.width).toFixed(4);
        const normY1 = (y1 / canvas.height).toFixed(4);
        const normX2 = (x2 / canvas.width).toFixed(4);
        const normY2 = (y2 / canvas.height).toFixed(4);
        // El comando usa '0 - thickness' porque el thickNeg original del JSON es un valor negativo
        const cmd = `42["drawcmd",0,[${normX1},${normY1},${normX2},${normY2},false,${0 - thickness},"${color}",0,0,{}]]`;
        socket.send(cmd);
    }

    /* ----------  AYUDAS (HELPERS)  ---------- */
    function fetchJson(url) {
        return new Promise(resolve => {
            GM_xmlhttpRequest({
                method: 'GET',
                url: url,
                onload: r => {
                    try { resolve(JSON.parse(r.responseText)); }
                    catch {
                        console.error('Error al analizar JSON de la URL:', url, r.responseText);
                        resolve(null);
                    }
                },
                onerror: (error) => {
                    console.error('Error al obtener JSON de la URL:', url, error);
                    resolve(null);
                }
            });
        });
    }

    /* ----------  EVENTOS  ---------- */
    /* ----------  EVENTOS (VERSIÓN MEJORADA)  ---------- */
    drawBtn.addEventListener('click', () => {
        const pid = playerSelect.value;
        if (!pid) {
            alert('Por favor, selecciona un jugador.');
            return;
        }
        drawOnAvatar(pid);
    });

    // Observer mejorado con debounce para reducir actualizaciones innecesarias
    const plEl = document.getElementById('playerlist');
    if (plEl) {
        new MutationObserver(debouncedRefresh).observe(plEl, {
            childList: true,
            subtree: true,
            attributes: true,
            attributeFilter: ['data-playerid']
        });
    }

    // Llamada inicial
    refreshPlayerList();

})();