Drawaria The Animator Mod

Choose an object and draw it on the selected player’s avatar, with special effects. Includes a Stop button.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Drawaria The Animator Mod
// @namespace    http://tampermonkey.net/
// @version      1.15
// @description  Choose an object and draw it on the selected player’s avatar, with special effects. Includes a Stop button.
// @author       YouTubeDrawaria
// @match        https://drawaria.online/*
// @match        https://*.drawaria.online/*
// @icon         https://drawaria.online/avatar/cache/0d886640-6bde-11f0-8d47-cbcdc07da1cc.1754411246971.jpg
// @grant        GM_xmlhttpRequest
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    /* ----------  CONFIGURACIÓN  ---------- */
    // JSONs de Dibujos Normales (sin efectos especiales acoplados)
    const JSON_SOURCES = {
        'Ninguno': '', // Opción para no dibujar un JSON
        'Ataque': 'https://raw.githubusercontent.com/DrawariaDeveloper/Json-to-Drawaria/main/ataque.json',
        'Pistola': 'https://raw.githubusercontent.com/DrawariaDeveloper/Json-to-Drawaria/main/pistola.json',
        'Espada': 'https://raw.githubusercontent.com/DrawariaDeveloper/Json-to-Drawaria/main/espada.json',
        'Escudo': 'https://raw.githubusercontent.com/DrawariaDeveloper/Json-to-Drawaria/main/escudo.json',
        'Defensa': 'https://raw.githubusercontent.com/DrawariaDeveloper/Json-to-Drawaria/main/defensa.json',
        'Cohete': 'https://raw.githubusercontent.com/DrawariaDeveloper/Json-to-Drawaria/main/cohete.json',
        'Laser': 'https://raw.githubusercontent.com/DrawariaDeveloper/Json-to-Drawaria/main/laser.json',
        'Explosion': 'https://raw.githubusercontent.com/DrawariaDeveloper/Json-to-Drawaria/main/explosion.json',
        'Rayo': 'https://raw.githubusercontent.com/DrawariaDeveloper/Json-to-Drawaria/main/rayo.json',
        'Gorra': 'https://raw.githubusercontent.com/DrawariaDeveloper/Json-to-Drawaria/main/gorra.json',
        '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',
    };
    const DEFAULT_JSON_NAME = 'Ninguno';

    // Efectos Procedurales o JSONs que actúan como "efectos" (sin rotación ni posición configurable por el usuario)
    const JSON_EFFECTS = {
        'Ninguno': '',
        'Arco y Flecha': 'effect:arrow_chaser',
        'Aura de Fuego': 'effect:fire_aura_circular',
        'Bomba': 'effect:bomb',
        'Búmeran (Guiado)': 'effect:boomerang_guided',
        'Cohete Espacial': 'effect:space_rocket',
        'Disparo Pistola': 'effect:pistol_shoot',
        'Dron Seguidor': 'effect:drone_follower_ray',
        'Escopeta (Spread)': 'effect:shotgun_blast',
        'Espadazo': 'effect:sword_slash_arc',
        'Flashlight Supernova': 'effect:flashlight_star',
        'Granada Pegajosa': 'effect:sticky_grenade_proj',
        'Lanzagranadas (Arc)': 'effect:grenade_launcher',
        'Látigo Eléctrico': 'effect:electric_whip_snap',
        'Martillazo Sísmico': 'effect:seismic_smash_wave',
        'Mina de Defensa': 'effect:proximity_mine_trap',
        'Muro de Tierra': 'effect:earth_wall_shield',
        'Rifle Láser': 'effect:laser_rifle_beam',
        'Rayo Zigzag': 'effect:lightning_zigzag',
        'Tormenta de Hielo': 'effect:ice_storm_area',
        'Tornado de Viento': 'effect:wind_tornado_spin',
    };
    const DEFAULT_EFFECT_NAME = 'Ninguno';

    // URLs específicas para JSONs usados por efectos procedurales
    const BOMBA_JSON_URL = 'https://raw.githubusercontent.com/DrawariaDeveloper/Json-to-Drawaria/main/bomba.json';
    const PISTOLA_JSON_URL = 'https://raw.githubusercontent.com/DrawariaDeveloper/Json-to-Drawaria/main/pistola.json';
    const ARCO_JSON_URL = 'https://raw.githubusercontent.com/DrawariaDeveloper/Json-to-Drawaria/main/arco.json';
    const LANZAGRANADAS_JSON_URL = 'https://raw.githubusercontent.com/DrawariaDeveloper/Json-to-Drawaria/main/lanzagranadas.json';
    const RIFLE_JSON_URL = 'https://raw.githubusercontent.com/DrawariaDeveloper/Json-to-Drawaria/main/rifle.json';
    const BOOMERANG_JSON_URL = 'https://raw.githubusercontent.com/DrawariaDeveloper/Json-to-Drawaria/main/boomerang.json';
    const ESPADA_JSON_URL = 'https://raw.githubusercontent.com/DrawariaDeveloper/Json-to-Drawaria/main/espada.json';
    const MARTILLO_JSON_URL = 'https://raw.githubusercontent.com/DrawariaDeveloper/Json-to-Drawaria/main/martillo.json';
    const LATIGO_JSON_URL = 'https://raw.githubusercontent.com/DrawariaDeveloper/Json-to-Drawaria/main/latigo.json';
    const GRANADA_JSON_URL = 'https://raw.githubusercontent.com/DrawariaDeveloper/Json-to-Drawaria/main/granada.json';
    const MINA_JSON_URL = 'https://raw.githubusercontent.com/DrawariaDeveloper/Json-to-Drawaria/main/mina.json';
    const ESCOPETA_JSON_URL = 'https://raw.githubusercontent.com/DrawariaDeveloper/Json-to-Drawaria/main/escopeta.json';
    const DRON_JSON_URL = 'https://raw.githubusercontent.com/DrawariaDeveloper/Json-to-Drawaria/main/dron.json';

    const DRAW_PADDING = 10;
    const DRAW_PADDING_HAND = 3;
    const HAND_GRIP_OFFSET_Y = 2;
    const REPEAT_ACTION_DELAY = 15; // Retardo en ms entre cada segmento de una misma acción (para que el dibujo aparezca fluido)
    const WAIT_ACTION_DELAY = 500; // Retardo en ms entre cada repetición completa del dibujo/efecto (0.5 segundos)

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

    let socket;
    const canvas = document.getElementById('canvas');
    const ctx = canvas ? canvas.getContext('2d') : null;

    let stopSignal = false; // <-- NUEVO: Señal para detener animaciones
    let stopBtn; // <-- NUEVO: Referencia al botón de detener

    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.9);
        color:#fff; padding:12px 18px; border-radius:10px;
        font-family: 'Segoe UI', Arial, sans-serif; font-size:13px;
        display:flex; flex-direction:column; gap:12px;
        box-shadow: 0 6px 15px rgba(0,0,0,0.6);
        cursor: default;
        backdrop-filter: blur(5px);
        border: 1px solid rgba(60,60,60,0.5);
    `;

    const titleBar = document.createElement('div');
    titleBar.textContent = 'The Animator Mod';
    titleBar.style.cssText = `
        font-weight: bold;
        font-size: 15px;
        text-align: center;
        cursor: grab;
        background: linear-gradient(180deg, rgba(40,40,40,0.95), rgba(25,25,25,0.95));
        border-radius: 8px 8px 0 0;
        margin: -12px -18px 12px -18px;
        padding: 10px 18px;
        border-bottom: 1px solid #555;
        color: #ADD8E6;
    `;
    container.appendChild(titleBar);

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

    const baseInputStyle = `
        flex-grow: 1;
        padding: 7px 10px; border-radius: 5px; border: 1px solid #555;
        background: #333; color: #fff;
        font-size: 13px;
    `;
    const selectBaseStyle = baseInputStyle + `
        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;
    `;

    function createLabeledRow(parent, labelText, inputElement) {
        const wrapper = document.createElement('div');
        wrapper.style.cssText = `display:flex; align-items:center; gap:10px;`;
        const label = document.createElement('span');
        label.textContent = labelText;
        wrapper.appendChild(label);
        wrapper.appendChild(inputElement);
        parent.appendChild(wrapper);
        return { wrapper, label, inputElement };
    }

    const playerSelect = document.createElement('select');
    playerSelect.style.cssText = selectBaseStyle;
    createLabeledRow(contentDiv, 'Jugador:', playerSelect);

    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];
    createLabeledRow(contentDiv, 'Dibujo:', jsonUrlSelect);

    const effectSelect = document.createElement('select');
    effectSelect.style.cssText = selectBaseStyle;
    for (const name in JSON_EFFECTS) {
        const opt = document.createElement('option');
        opt.value = JSON_EFFECTS[name];
        opt.textContent = name;
        effectSelect.appendChild(opt);
    }
    effectSelect.value = JSON_EFFECTS[DEFAULT_EFFECT_NAME];
    createLabeledRow(contentDiv, 'Efectos:', effectSelect);

    jsonUrlSelect.addEventListener('change', () => {
        if (jsonUrlSelect.value !== '') {
            effectSelect.value = JSON_EFFECTS['Ninguno'];
        }
    });

    effectSelect.addEventListener('change', () => {
        if (effectSelect.value !== '') {
            jsonUrlSelect.value = JSON_SOURCES['Ninguno'];

            // Auto-configurar posición para "Disparo Pistola"
            if (effectSelect.value === 'effect:pistol_shoot') {
                positionSelect.value = 'grip_right'; // Cambiado a grip_right, ya que el JSON de pistola en general se dibuja a la derecha.
                console.log('Auto-configurado: Posición cambiada a "Agarre Derecha" para Disparo Pistola');
            }
        }
    });

    const positionSelect = document.createElement('select');
    positionSelect.style.cssText = selectBaseStyle;
    const positions = {
        'Cabeza': 'head',
        'Agarre Derecha': 'grip_right',
        'Agarre Izquierda': 'grip_left',
        'Derecha': 'right',
        'Izquierda': 'left',
        'Arriba': 'top',
        'Abajo': 'bottom',
        'Centrado': 'centered'
    };
    for (const name in positions) {
        const opt = document.createElement('option');
        opt.value = positions[name];
        opt.textContent = name;
        positionSelect.appendChild(opt);
    }
    positionSelect.value = 'head';
    createLabeledRow(contentDiv, 'Posición:', positionSelect);

    const orientationSelect = document.createElement('select');
    orientationSelect.style.cssText = selectBaseStyle;
    const orientations = {
        'Actual': 'none',
        'Derecha (90°)': 'right',
        'Izquierda (-90°)': 'left',
        'Abajo (180°)': 'down',
        'Arriba (0°)' : 'up'
    };
    for (const name in orientations) {
        const opt = document.createElement('option');
        opt.value = orientations[name];
        opt.textContent = name;
        orientationSelect.appendChild(opt);
    }
    orientationSelect.value = 'none';
    createLabeledRow(contentDiv, 'Orientación:', orientationSelect);

    const sizeInput = document.createElement('input');
    sizeInput.type = 'number';
    sizeInput.min = '0.1';
    sizeInput.max = '2.0';
    sizeInput.step = '0.1';
    sizeInput.value = '1.0';
    sizeInput.style.cssText = baseInputStyle + `width: 60px; text-align: center;`;
    createLabeledRow(contentDiv, 'Tamaño (Escala):', sizeInput);

    const repeatActionToggle = document.createElement('input');
    repeatActionToggle.type = 'checkbox';
    repeatActionToggle.id = 'repeatActionToggle';
    repeatActionToggle.style.cssText = `margin-right: 5px; cursor: pointer; transform: scale(1.2);`;
    const repeatActionLabel = document.createElement('label');
    repeatActionLabel.htmlFor = 'repeatActionToggle';
    repeatActionLabel.textContent = ` Repetir Acción (cada ${WAIT_ACTION_DELAY / 1000}s)`;
    repeatActionLabel.style.cssText = `display: flex; align-items: center; cursor: pointer;`;
    const repeatActionWrapper = document.createElement('div');
    repeatActionWrapper.style.cssText = `display:flex; align-items:center; gap:0;`;
    repeatActionWrapper.appendChild(repeatActionToggle);
    repeatActionWrapper.appendChild(repeatActionLabel);
    contentDiv.appendChild(repeatActionWrapper);


    const drawBtn = document.createElement('button');
    drawBtn.textContent = 'Dibujar en avatar';
    drawBtn.disabled = true;
    drawBtn.style.cssText = `
        padding: 10px 18px; border-radius: 8px; border: none;
        background: linear-gradient(145deg, #4CAF50, #45a049);
        color: white; font-weight: bold; font-size: 15px;
        cursor: pointer;
        transition: all 0.2s ease;
        box-shadow: 0 3px 8px rgba(0,0,0,0.4);

        &:hover {
            background: linear-gradient(145deg, #45a049, #3d8c41);
            box-shadow: 0 5px 12px rgba(0,0,0,0.5);
            transform: translateY(-2px);
        }
        &: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);

    // NUEVO: Botón para detener la animación actual
    stopBtn = document.createElement('button');
    stopBtn.textContent = 'Detener Animación';
    stopBtn.disabled = true;
    stopBtn.style.cssText = `
        margin-top: 5px; /* Espacio entre botones */
        padding: 8px 16px; border-radius: 8px; border: none;
        background: linear-gradient(145deg, #f44336, #d32f2f); /* Rojo */
        color: white; font-weight: bold; font-size: 14px;
        cursor: pointer;
        transition: all 0.2s ease;
        box-shadow: 0 3px 8px rgba(0,0,0,0.4);

        &:hover {
            background: linear-gradient(145deg, #d32f2f, #b71c1c);
            box-shadow: 0 5px 12px rgba(0,0,0,0.5);
            transform: translateY(-2px);
        }
        &: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(stopBtn);

    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';
    });

    document.addEventListener('mousemove', (e) => {
        if (!isDragging) return;
        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 = '';
    });

    /* ----------  LISTA DE JUGADORES (VERSIÓN MEJORADA)  ---------- */
    let lastPlayerList = new Set();
    let isUpdatingList = false;

    function refreshPlayerList() {
        if (isUpdatingList) return;

        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}`);
            }
        });

        const playersChanged = currentPlayers.size !== lastPlayerList.size ||
              ![...currentPlayers].every(player => lastPlayerList.has(player));

        if (!playersChanged) return;

        isUpdatingList = true;

        const previousSelection = playerSelect.value;
        const previousSelectedText = playerSelect.selectedOptions?.[0]?.textContent || '';

        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);
        });

        if (previousSelection) {
            let restored = false;
            for (let option of playerSelect.options) {
                if (option.value === previousSelection) {
                    playerSelect.value = previousSelection;
                    restored = true;
                    break;
                }
            }

            if (!restored && previousSelectedText) {
                for (let option of playerSelect.options) {
                    if (option.textContent === previousSelectedText) {
                        playerSelect.value = option.value;
                        restored = true;
                        break;
                    }
                }
            }
        }

        lastPlayerList = new Set(currentPlayers);

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

    let refreshTimeout;
    function debouncedRefresh() {
        clearTimeout(refreshTimeout);
        refreshTimeout = setTimeout(refreshPlayerList, 100);
    }


    /* ----------  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 PRINCIPAL (para JSONs) ---------- */
    let repeatIntervalId = null;
    let isDrawing = false; // Bandera para evitar múltiples ejecuciones de drawJsonCommands/efectos

    /**
     * Dibuja un JSON en el avatar del jugador, aplicando posición, orientación y escala.
     * Esta función es la que interpreta los comandos de dibujo de un JSON.
     * @param {string} targetPlayerId El ID del jugador objetivo para colocar el JSON.
     * @param {string|null} jsonUrlOverride Si se proporciona, usa esta URL de JSON en lugar de la seleccionada en la UI.
     * @param {string|null} positionOverride Si se proporciona, usa esta posición en lugar de la seleccionada en la UI.
     * @param {string|null} orientationOverride Si se proporciona, usa esta orientación en lugar de la seleccionada en la UI.
     * @param {number|null} sizeFactorOverride Si se proporciona, usa este factor de escala en lugar del de la UI.
     */
    async function drawJsonCommands(targetPlayerId, jsonUrlOverride = null, positionOverride = null, orientationOverride = null, sizeFactorOverride = null) {
        if (stopSignal) { console.log('drawJsonCommands detenido por señal.'); return; }
        if (!socket) {
            console.warn('drawJsonCommands: Socket no está listo. No se puede dibujar en el servidor.');
        }
        const avatar = document.querySelector(`.spawnedavatar[data-playerid="${targetPlayerId}"]`);
        if (!avatar) {
            console.warn('drawJsonCommands: Avatar no encontrado para el ID:', targetPlayerId, 'No se puede dibujar.');
            return;
        }

        const cRect = canvas.getBoundingClientRect();
        const aRect = avatar.getBoundingClientRect();

        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;

        // USA LOS OVERRIDES SI ESTÁN PRESENTES, SINO USA LOS VALORES DE LA UI
        const url = jsonUrlOverride || jsonUrlSelect.value;
        const currentPosition = positionOverride || positionSelect.value;
        const orientation = orientationOverride || orientationSelect.value;
        const sizeFactor = sizeFactorOverride !== null ? sizeFactorOverride : parseFloat(sizeInput.value) || 1.0;

        if (!url || url === '' || url.startsWith('effect:')) {
            console.log('drawJsonCommands: No se proporcionó una URL de JSON válida o es un efecto procedural.');
            return;
        }

        const json = await fetchJson(url);
        if (stopSignal) return;
        if (!json || !Array.isArray(json.commands)) {
            console.error('drawJsonCommands: JSON inválido o no se pudo cargar el dibujo de la URL:', url);
            alert('JSON inválido o no se pudo cargar el dibujo. Asegúrate de que el formato sea correcto y la URL accesible.');
            return;
        }

        const { min_nx, max_nx, min_ny, max_ny } = analyzeJsonBounds(json.commands);

        // Bounding box del dibujo *escalado* (antes de posicionar/rotar)
        const scaledDrawWidth = (max_nx - min_nx) * canvas.width * sizeFactor;
        const scaledDrawHeight = (max_ny - min_ny) * canvas.height * sizeFactor;

        // Origen del dibujo si estuviera posicionado en (0,0) del canvas y escalado
        const scaledOriginalOriginX = min_nx * canvas.width * sizeFactor;
        const scaledOriginalOriginY = min_ny * canvas.height * sizeFactor;

        // Calcular el centro del bounding box escalado (usado como pivote de rotación)
        const pivotX = scaledOriginalOriginX + scaledDrawWidth / 2;
        const pivotY = scaledOriginalOriginY + scaledDrawHeight / 2;

        let drawingOriginX; // Posición final del punto (0,0) del dibujo en el canvas
        let drawingOriginY;

        // Calcular drawingOriginX/Y basado en la posición deseada (usando currentPosition)
        switch (currentPosition) {
            case 'centered':
                drawingOriginX = avatarCenterX - pivotX;
                drawingOriginY = avatarCenterY - pivotY;
                break;
            case 'top':
                drawingOriginX = avatarCenterX - pivotX;
                drawingOriginY = (avatarY - DRAW_PADDING) - scaledDrawHeight - scaledOriginalOriginY; // Ajuste para que la base del dibujo quede arriba
                break;
            case 'bottom':
                drawingOriginX = avatarCenterX - pivotX;
                drawingOriginY = (avatarY + avatarHeight + DRAW_PADDING) - scaledOriginalOriginY; // Ajuste para que la parte superior quede abajo
                break;
            case 'left':
                drawingOriginY = avatarCenterY - pivotY;
                drawingOriginX = (avatarX - DRAW_PADDING) - scaledDrawWidth - scaledOriginalOriginX; // Ajuste para que el lado derecho quede a la izquierda
                break;
            case 'right':
                drawingOriginY = avatarCenterY - pivotY;
                drawingOriginX = (avatarX + avatarWidth + DRAW_PADDING) - scaledOriginalOriginX; // Ajuste para que el lado izquierdo quede a la derecha
                break;
            case 'head':
                drawingOriginX = avatarCenterX - pivotX;
                drawingOriginY = avatarY - scaledDrawHeight - scaledOriginalOriginY + (avatarHeight * 0.1);
                break;
            case 'grip_right':
                drawingOriginX = (avatarX + avatarWidth + DRAW_PADDING_HAND) - scaledOriginalOriginX;
                drawingOriginY = avatarCenterY - pivotY + HAND_GRIP_OFFSET_Y;
                break;
            case 'grip_left':
                drawingOriginX = (avatarX - DRAW_PADDING_HAND) - scaledDrawWidth - scaledOriginalOriginX;
                drawingOriginY = avatarCenterY - pivotY + HAND_GRIP_OFFSET_Y;
                break;
            default:
                drawingOriginX = avatarCenterX - pivotX;
                drawingOriginY = avatarCenterY - pivotY;
                break;
        }

        // Determinar ángulo de rotación (usando 'orientation')
        let rotationAngleRad = 0;
        switch (orientation) {
            case 'right': rotationAngleRad = Math.PI / 2; break;
            case 'left': rotationAngleRad = -Math.PI / 2; break;
            case 'down': rotationAngleRad = Math.PI; break;
            case 'up':
            case 'none':
            default: rotationAngleRad = 0; break;
        }

        // NOTE: The original script explicitly stated to ignore 'orientation' and force facing right.
        // If actual rotation based on 'orientation' is desired, the following commented out rotation logic would be needed.
        // For now, it behaves as the original script's comment suggested, making JSONs appear right-facing relative to the avatar.

        for (const cmdArr of json.commands) {
            if (stopSignal) { console.log('drawJsonCommands detenido por señal.'); return; }
            if (repeatIntervalId && !repeatActionToggle.checked) {
                console.log('drawJsonCommands: Interrupción por toggle inactivo.');
                return;
            }

            const [, , [nx1, ny1, nx2, ny2, , thickNeg, color]] = cmdArr;

            // Coordenadas base escaladas
            let currentX1 = (nx1 * canvas.width * sizeFactor) - scaledOriginalOriginX;
            let currentY1 = (ny1 * canvas.height * sizeFactor) - scaledOriginalOriginY;
            let currentX2 = (nx2 * canvas.width * sizeFactor) - scaledOriginalOriginX;
            let currentY2 = (ny2 * canvas.height * sizeFactor) - scaledOriginalOriginY;

            // FORZAR QUE TODOS MIREN HACIA LA DERECHA SIEMPRE
            // Ignorar completamente la variable 'orientation'
            // If actual rotation based on `orientation` is desired, uncomment the rotation logic below and remove these direct assignments.
            const finalX1 = currentX1 + drawingOriginX;
            const finalY1 = currentY1 + drawingOriginY;
            const finalX2 = currentX2 + drawingOriginX;
            const finalY2 = currentY2 + drawingOriginY;

            sendDrawCommand(finalX1, finalY1, finalX2, finalY2, color, -thickNeg);
            await new Promise(r => setTimeout(r, REPEAT_ACTION_DELAY));
        }
    }

    // Envía el comando de dibujo al socket de Drawaria Y DIBUJA LOCALMENTE EN EL CANVAS
    function sendDrawCommand(x1, y1, x2, y2, color, thickness) {
        // Asegurarse de que las coordenadas sean números enteros para un mejor rendimiento y visualización
        x1 = Math.round(x1); y1 = Math.round(y1);
        x2 = Math.round(x2); y2 = Math.round(y2);

        if (ctx && canvas) {
            ctx.strokeStyle = color;
            ctx.lineWidth = thickness;
            ctx.lineCap = 'round';
            ctx.lineJoin = 'round';

            ctx.beginPath();
            ctx.moveTo(x1, y1);
            ctx.lineTo(x2, y2);
            ctx.stroke();
        }

        if (!socket) return;

        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);
        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);
                }
            });
        });
    }

    /**
     * Obtiene las coordenadas del centro del objetivo o un punto de agarre.
     * @param {string} playerId El ID del jugador objetivo.
     * @param {string} attachmentPointName Nombre del punto de acoplamiento (e.g., 'grip_right', 'head', 'centered').
     * @returns {object|null} - {x, y} de las coordenadas del punto de acoplamiento o null si no se encuentra.
     */
    function _getAttachmentPoint(playerId, attachmentPointName = 'centered') {
        const avatar = document.querySelector(`.spawnedavatar[data-playerid="${playerId}"]`);
        if (!avatar) {
            console.warn(`_getAttachmentPoint: Avatar no encontrado para el jugador ${playerId}.`);
            return null;
        }

        const cRect = canvas.getBoundingClientRect();
        const aRect = avatar.getBoundingClientRect();

        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;

        let attachX, attachY;

        switch (attachmentPointName) {
            case 'grip_right':
                attachX = avatarX + avatarWidth + DRAW_PADDING_HAND;
                attachY = avatarCenterY + HAND_GRIP_OFFSET_Y;
                break;
            case 'grip_left':
                attachX = avatarX - DRAW_PADDING_HAND;
                attachY = avatarCenterY + HAND_GRIP_OFFSET_Y;
                break;
            case 'head':
                attachX = avatarCenterX;
                attachY = avatarY + (avatarHeight * 0.1); // Parte superior de la cabeza
                break;
            case 'bottom':
                attachX = avatarCenterX;
                attachY = avatarY + avatarHeight + DRAW_PADDING; // Parte inferior del avatar
                break;
            case 'centered':
            default:
                attachX = avatarCenterX;
                attachY = avatarCenterY;
                break;
        }
        return { x: attachX, y: attachY };
    }

    // Función auxiliar para obtener coordenadas del centro del objetivo
    function getTargetCoords(targetPlayerId) {
        return _getAttachmentPoint(targetPlayerId, 'centered');
    }

    // Función auxiliar para calcular distancia entre dos puntos
    function distance(x1, y1, x2, y2) {
        return Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
    }

    /* ----------  FUNCIONES DE EFECTOS PROCEDURALES  ---------- */

    // Función de ráfaga de explosión (usada por el efecto Bomba)
    async function explosionBlast(centerX, centerY, size = 1.0) {
        if (stopSignal) { console.log('explosionBlast detenida.'); return; }
        const steps = 80;
        const maxRadius = 100 * size;

        const explosionColors = [
            'hsl(0, 100%, 60%)', 'hsl(15, 100%, 65%)', 'hsl(30, 100%, 60%)',
            'hsl(45, 100%, 65%)', 'hsl(60, 100%, 70%)', 'hsl(25, 100%, 55%)', 'hsl(10, 100%, 50%)',
        ];

        for (let i = 0; i < steps; i++) {
            if (stopSignal) { console.log('explosionBlast detenida en bucle.'); return; }
            if (!socket || (repeatIntervalId && !repeatActionToggle.checked)) {
                console.log('explosionBlast: Detenido por interrupción o socket no disponible.');
                break;
            }

            const progress = i / steps;
            const particlesThisStep = 2 + Math.floor(progress * 5);

            for (let p = 0; p < particlesThisStep; p++) {
                const angle = Math.random() * Math.PI * 2;
                const distance = progress * maxRadius * (0.8 + Math.random() * 0.4);

                const endX = centerX + distance * Math.cos(angle);
                const endY = centerY + distance * Math.sin(angle);

                const colorIndex = Math.floor(Math.random() * explosionColors.length);
                const color = explosionColors[colorIndex];
                const thickness = Math.max(1, 8 - progress * 7 + Math.random() * 2);

                sendDrawCommand(centerX, centerY, endX, endY, color, thickness);
            }
            await new Promise(resolve => setTimeout(resolve, 25 + progress * 15));
        }
    }

    // Efecto: Dibuja la Bomba (JSON) y luego hace la Explosión (procedural)
    async function drawBombWithExplosion(playerId) {
        if (stopSignal) { console.log('drawBombWithExplosion detenida.'); return; }
        console.log(`drawBombWithExplosion: Iniciando efecto en ${playerId}...`);

        // Bomb will appear on the *selected target player's* avatar
        const avatar = document.querySelector(`.spawnedavatar[data-playerid="${playerId}"]`);
        if (!avatar) {
            console.warn('drawBombWithExplosion: Avatar no encontrado.');
            return;
        }

        // Antes de dibujar la bomba, guardar las coordenadas donde debería estar el centro de la explosión
        const bombPlacement = _getAttachmentPoint(playerId, 'bottom'); // La bomba en el suelo
        if (!bombPlacement) { console.warn('drawBombWithExplosion: No se pudo determinar el punto de colocación de la bomba.'); return; }

        const explosionPointX = bombPlacement.x;
        const explosionPointY = bombPlacement.y;

        console.log(`drawBombWithExplosion: Dibujando bomba JSON...`);
        // Dibuja el JSON de la bomba, centrado en la parte inferior del avatar
        await drawJsonCommands(playerId, BOMBA_JSON_URL, 'bottom', 'none', 1.0);
        if (stopSignal) return;

        if (!socket || (repeatIntervalId && !repeatActionToggle.checked)) {
            console.log('drawBombWithExplosion: Interrumpido antes de la explosión.');
            return;
        }

        console.log('drawBombWithExplosion: Bomba dibujada. Esperando 2 segundos para la explosión...');
        await new Promise(resolve => setTimeout(resolve, 2000));
        if (stopSignal) return;

        console.log('drawBombWithExplosion: Iniciando explosión procedural...');
        await explosionBlast(explosionPointX, explosionPointY, 1.2); // Explosión en el punto guardado
        console.log('drawBombWithExplosion: Explosión completada.');
    }

// Efecto Rayo Zigzag Perseguidor (OPTIMIZADO Y MÁS FLUIDO)
async function lightningZigzagChaser(targetPlayerId) {
    if (stopSignal) { console.log('lightningZigzagChaser detenida.'); return; }
    console.log(`lightningZigzagChaser: Iniciando efecto optimizado en ${targetPlayerId}...`);
    if (!socket) {
        console.warn('lightningZigzagChaser: Socket no disponible.');
        return;
    }

    const cRect = canvas.getBoundingClientRect();

    const getTargetCoordsDynamic = () => {
        const currentAvatar = document.querySelector(`.spawnedavatar[data-playerid="${targetPlayerId}"]`);
        if (!currentAvatar) return null;
        const currentARect = currentAvatar.getBoundingClientRect();
        return {
            x: Math.round((currentARect.left - cRect.left) + (currentARect.width / 2)),
            y: Math.round((currentARect.top - cRect.top) + (currentARect.height / 2))
        };
    };

    // Esquinas optimizadas con coordenadas enteras[5]
    const corners = [
        { x: 20, y: 20 },
        { x: Math.round(canvas.width - 20), y: 20 },
        { x: 20, y: Math.round(canvas.height - 20) },
        { x: Math.round(canvas.width - 20), y: Math.round(canvas.height - 20) }
    ];
    const startCorner = corners[Math.floor(Math.random() * corners.length)];

    let currentX = startCorner.x;
    let currentY = startCorner.y;

    const totalSegments = 25;
    const zigzagIntensity = 28;
    const lightningColors = ['#FFFFFF', '#E0E6FF', '#6495ED', '#4169E1'];

    // Variables para suavizado del movimiento
    let previousAngle = 0;
    const smoothingFactor = 0.3;

    for (let segment = 0; segment < totalSegments; segment++) {
        if (stopSignal) { console.log('lightningZigzagChaser detenida en bucle.'); return; }
        if (!socket || (repeatIntervalId && !repeatActionToggle.checked)) {
            console.log('lightningZigzagChaser: Detenido por interrupción.');
            break;
        }

        const progress = segment / totalSegments;
        const targetCoords = getTargetCoordsDynamic();

        if (!targetCoords) {
            console.log('lightningZigzagChaser: Objetivo desaparecido.');
            break;
        }

        const targetX = targetCoords.x;
        const targetY = targetCoords.y;

        // Movimiento más suave hacia el objetivo[1]
        const stepSize = 0.13 + (progress * 0.05); // Acelera ligeramente hacia el final
        const directX = Math.round(currentX + (targetX - currentX) * stepSize);
        const directY = Math.round(currentY + (targetY - currentY) * stepSize);

        const directionX = targetX - currentX;
        const directionY = targetY - currentY;
        const distance = Math.sqrt(directionX * directionX + directionY * directionY);

        if (distance > 8) {
            const perpX = -directionY / distance;
            const perpY = directionX / distance;

            // Zigzag más suave y natural[1][2]
            const baseZigzag = Math.sin(segment * 0.8) * zigzagIntensity * (1 - progress * 0.6);
            const noiseZigzag = (Math.random() - 0.5) * 15 * (1 - progress * 0.3); // Ruido adicional
            const smoothedZigzag = baseZigzag + noiseZigzag;

            // Suavizado del ángulo para transiciones más fluidas
            const currentAngle = Math.atan2(directionY, directionX);
            const angleDiff = currentAngle - previousAngle;
            const smoothedAngle = previousAngle + angleDiff * smoothingFactor;
            previousAngle = smoothedAngle;

            const finalZigzag = smoothedZigzag * Math.sin(progress * Math.PI); // Curva de intensidad

            const nextX = Math.round(directX + perpX * finalZigzag);
            const nextY = Math.round(directY + perpY * finalZigzag);

            // BATCH RENDERING: Agrupar todas las capas del segmento[3][5]
            const segmentLayers = [];

            // Preparar todas las capas antes de dibujar
            for (let layer = 0; layer < 3; layer++) {
                const colorIndex = (segment + layer) % lightningColors.length; // Variación más consistente
                const color = lightningColors[colorIndex];
                const thickness = Math.max(1, 7 - layer * 2);

                // Offset más sutil para capas[2]
                const offsetX = Math.round((Math.random() - 0.5) * (4 - layer));
                const offsetY = Math.round((Math.random() - 0.5) * (4 - layer));

                segmentLayers.push({
                    startX: currentX + offsetX,
                    startY: currentY + offsetY,
                    endX: nextX + offsetX,
                    endY: nextY + offsetY,
                    color: color,
                    thickness: thickness
                });
            }

            // BATCH: Dibujar todas las capas seguidas[5]
            segmentLayers.forEach(layer => {
                sendDrawCommand(
                    layer.startX,
                    layer.startY,
                    layer.endX,
                    layer.endY,
                    layer.color,
                    layer.thickness
                );
            });

            // Mini-delay después del batch para fluidez
            await new Promise(resolve => setTimeout(resolve, 12));
            if (stopSignal) return;

            // Efectos adicionales cada pocos segmentos para más belleza
            if (segment % 4 === 0 && progress < 0.8) {
                // Chispas laterales ocasionales[1]
                const sparkAngle = currentAngle + (Math.random() - 0.5) * Math.PI * 0.5;
                const sparkDistance = 15 + Math.random() * 10;
                const sparkX = Math.round(nextX + Math.cos(sparkAngle) * sparkDistance);
                const sparkY = Math.round(nextY + Math.sin(sparkAngle) * sparkDistance);

                sendDrawCommand(nextX, nextY, sparkX, sparkY, '#E0E6FF', 1);

                // Mini-delay para chispas
                await new Promise(resolve => setTimeout(resolve, 8));
                if (stopSignal) return;
            }

            currentX = directX;
            currentY = directY;
        } else {
            // Cerca del objetivo - movimiento más directo y suave
            const finalStepX = Math.round(currentX + (targetX - currentX) * 0.3);
            const finalStepY = Math.round(currentY + (targetY - currentY) * 0.3);

            // Rayo final más grueso y brillante
            sendDrawCommand(currentX, currentY, finalStepX, finalStepY, '#FFFFFF', 5);
            await new Promise(resolve => setTimeout(resolve, 8));
            if (stopSignal) return;
            sendDrawCommand(currentX, currentY, finalStepX, finalStepY, '#E0E6FF', 3);

            currentX = targetX;
            currentY = targetY;
            break; // Llegamos al objetivo
        }

        // Delay principal ajustado para fluidez[4]
        await new Promise(resolve => setTimeout(resolve, 85)); // Era 100ms, ahora 85ms
    }

    // Conexión final brillante al objetivo
    if (socket && !stopSignal && !(repeatIntervalId && !repeatActionToggle.checked)) {
        const targetCoords = getTargetCoordsDynamic();
        if (targetCoords) {
            // Rayo final intenso
            for (let finalLayer = 0; finalLayer < 4; finalLayer++) {
                if (stopSignal) return;
                const finalColor = lightningColors[finalLayer % lightningColors.length];
                const finalThickness = Math.max(2, 8 - finalLayer * 2);

                sendDrawCommand(currentX, currentY, targetCoords.x, targetCoords.y, finalColor, finalThickness);
                await new Promise(resolve => setTimeout(resolve, 15)); // Delay entre capas finales
            }
            if (stopSignal) return;
            await lightningImpact(targetCoords.x, targetCoords.y);
        } else {
            console.warn('lightningZigzagChaser: Objetivo no encontrado para el impacto final.');
        }
    }
    console.log('lightningZigzagChaser: Efecto optimizado completado.');
}

// Impacto mantiene el código original
async function lightningImpact(centerX, centerY) {
    if (stopSignal) { console.log('lightningImpact detenida.'); return; }
    const impactSteps = 15;
    const maxRadius = 50;

    console.log(`lightningImpact: Impacto en (${centerX}, ${centerY})`);

    for (let step = 0; step < impactSteps; step++) {
        if (stopSignal) { console.log('lightningImpact detenida en bucle.'); return; }
        if (!socket || (repeatIntervalId && !repeatActionToggle.checked)) {
            console.log('lightningImpact: Detenido por interrupción.');
            break;
        }

        const progress = step / impactSteps;
        const currentRadius = Math.round(maxRadius * progress);
        const raysThisStep = 8;

        for (let ray = 0; ray < raysThisStep; ray++) {
            const angle = (ray / raysThisStep) * 2 * Math.PI + Math.random() * 0.3;
            const rayLength = Math.round(currentRadius + Math.random() * 18);

            const endX = centerX + rayLength * Math.cos(angle);
            const endY = centerY + rayLength * Math.sin(angle);

            const midDistance = Math.round(rayLength * 0.6);
            const midAngle = angle + (Math.random() - 0.5) * 0.3;
            const midX = centerX + midDistance * Math.cos(midAngle);
            const midY = centerY + midDistance * Math.sin(midAngle);

            const colors = ['#FFFFFF', '#E0E6FF', '#6495ED'];
            const color = colors[Math.floor(Math.random() * colors.length)];
            const thickness = Math.max(1, 6 - progress * 4);

            sendDrawCommand(centerX, centerY, midX, midY, color, thickness);
            sendDrawCommand(midX, midY, endX, endY, color, thickness * 0.7);
        }
        await new Promise(resolve => setTimeout(resolve, 85));
    }
    console.log('lightningImpact: Impacto completado.');
}

// Función auxiliar para ajustar intensidad del color (usado en aura de fuego)
function adjustColorIntensity(hexColor, intensity) {
    if (!hexColor.startsWith('#') || hexColor.length !== 7) {
        return hexColor;
    }
    const r = parseInt(hexColor.substr(1, 2), 16);
    const g = parseInt(hexColor.substr(3, 2), 16);
    const b = parseInt(hexColor.substr(5, 2), 16);

    const newR = Math.floor(r * intensity);
    const newG = Math.floor(g * intensity);
    const newB = Math.floor(b * intensity);

    return `rgb(${newR}, ${newG}, ${newB})`;
}

// Efecto: Aura de Fuego Circular (ULTRA OPTIMIZADO para servidor)
async function circularFireAura(targetPlayerId, duration = 500) {
    if (stopSignal) { console.log('circularFireAura detenida.'); return; }
    if (!socket) {
        console.warn('circularFireAura: Socket no disponible.');
        return;
    }

    const cRect = canvas.getBoundingClientRect();

    const getCenterCoords = () => {
        const currentAvatar = document.querySelector(`.spawnedavatar[data-playerid="${targetPlayerId}"]`);
        if (!currentAvatar) return null;
        const currentARect = currentAvatar.getBoundingClientRect();
        return {
            // Coordenadas enteras para optimización[4]
            x: Math.floor((currentARect.left - cRect.left) + (currentARect.width / 2)),
            y: Math.floor((currentARect.top - cRect.top) + (currentARect.height / 2))
        };
    };

    const minRadius = 30;
    const maxRadius = 90;
    const ringCount = 5;
    const flamesPerRing = 20;

    const fireGradient = [
        '#FFFF99', '#FFCC00', '#FF9900', '#FF6600', '#FF3300', '#CC0000'
    ];

    const startTime = Date.now();
    let frame = 0;

    console.log(`circularFireAura: Creando aura de fuego ultra optimizada para jugador ${targetPlayerId}... (duración: ${duration}ms)`);

    while (Date.now() - startTime < duration) {
        if (stopSignal) { console.log('circularFireAura detenida en bucle.'); return; }
        if (!socket || (repeatIntervalId && !repeatActionToggle.checked)) {
            console.log('circularFireAura: Detenida por interrupción o socket no disponible.');
            break;
        }

        frame++;

        const currentCenter = getCenterCoords();
        if (!currentCenter) {
            console.log('circularFireAura: Objetivo desaparecido, deteniendo aura de fuego.');
            return;
        }
        const centerX = currentCenter.x;
        const centerY = currentCenter.y;

        // BATCH ULTRA PEQUEÑO: Un anillo por vez[1][3]
        for (let ring = 0; ring < ringCount; ring++) {
            if (stopSignal) return;
            const ringProgress = ring / ringCount;
            const ringRadius = minRadius + (maxRadius - minRadius) * ringProgress;

            const colorIndex = Math.min(ring, fireGradient.length - 1);
            const ringColor = fireGradient[colorIndex];

            // BATCH MICROSCÓPICO: Procesar llamas en grupos de 4[1]
            for (let flameBatch = 0; flameBatch < flamesPerRing; flameBatch += 4) {
                if (stopSignal) return;
                for (let flame = flameBatch; flame < Math.min(flameBatch + 4, flamesPerRing); flame++) {
                    if (stopSignal) return;
                    const baseAngle = (flame / flamesPerRing) * 2 * Math.PI;

                    const timeOffset = frame * 0.08 + ring * 0.4;
                    const flameVariation =
                        Math.sin(baseAngle * 4 + timeOffset) * 8 +
                        Math.sin(baseAngle * 7 + timeOffset * 1.3) * 5 +
                        Math.cos(baseAngle * 3 + timeOffset * 0.7) * 6;

                    const actualRadius = ringRadius + flameVariation;

                    // Coordenadas enteras[4]
                    const flameX = Math.floor(centerX + actualRadius * Math.cos(baseAngle));
                    const flameY = Math.floor(centerY + actualRadius * Math.sin(baseAngle));

                    const innerRadius = ringRadius * 0.65;
                    const innerX = Math.floor(centerX + innerRadius * Math.cos(baseAngle));
                    const innerY = Math.floor(centerY + innerRadius * Math.sin(baseAngle));

                    const flickerIntensity = 0.6 + 0.4 * Math.sin(frame * 0.12 + flame * 0.6);

                    if (flickerIntensity > 0.7) {
                        const thickness = Math.max(1, 5 - ringProgress * 3 + Math.random() * 2);

                        sendDrawCommand(innerX, innerY, flameX, flameY, ringColor, thickness);

                        // Micro-delay después de cada llama
                        await new Promise(resolve => setTimeout(resolve, 8)); // 8ms por llama
                        if (stopSignal) return;

                        if (ring === ringCount - 1 && Math.random() < 0.15) {
                            const sparkDistance = actualRadius + Math.random() * 15;
                            const sparkX = Math.floor(centerX + sparkDistance * Math.cos(baseAngle));
                            const sparkY = Math.floor(centerY + sparkDistance * Math.sin(baseAngle));

                            sendDrawCommand(flameX, flameY, sparkX, sparkY, '#FFCC00', 1);

                            // Delay adicional para chispas
                            await new Promise(resolve => setTimeout(resolve, 12)); // 12ms por chispa
                            if (stopSignal) return;
                        }
                    }
                }

                // Delay entre batches de llamas[3]
                await new Promise(resolve => setTimeout(resolve, 25)); // 25ms entre grupos de 4 llamas
                if (stopSignal) return;
            }

            // Conexiones con batches ultra pequeños
            if (frame % 3 === 0) {
                const connectionBatches = Math.ceil((flamesPerRing / 2) / 2); // Grupos de 2 conexiones

                for (let connBatch = 0; connBatch < connectionBatches; connBatch++) {
                    if (stopSignal) return;
                    const startConn = connBatch * 2;
                    const endConn = Math.min(startConn + 2, flamesPerRing / 2);

                    for (let connection = startConn; connection < endConn; connection++) {
                        if (stopSignal) return;
                        const angle1 = (connection * 2 / flamesPerRing) * 2 * Math.PI;
                        const angle2 = ((connection * 2 + 1) / flamesPerRing) * 2 * Math.PI;

                        const x1 = Math.floor(centerX + ringRadius * Math.cos(angle1));
                        const y1 = Math.floor(centerY + ringRadius * Math.sin(angle1));
                        const x2 = Math.floor(centerX + ringRadius * Math.cos(angle2));
                        const y2 = Math.floor(centerY + ringRadius * Math.sin(angle2));

                        sendDrawCommand(x1, y1, x2, y2, ringColor, Math.max(1, 4 - ringProgress * 2));

                        // Micro-delay entre conexiones
                        await new Promise(resolve => setTimeout(resolve, 15)); // 15ms por conexión
                        if (stopSignal) return;
                    }

                    // Delay entre batches de conexiones
                    if (connBatch < connectionBatches - 1) {
                        await new Promise(resolve => setTimeout(resolve, 30)); // 30ms entre grupos de conexiones
                        if (stopSignal) return;
                    }
                }
            }

            // Delay LARGO entre anillos[1]
            await new Promise(resolve => setTimeout(resolve, 80)); // 80ms entre anillos
            if (stopSignal) return;
        }

        // Delay principal ULTRA aumentado[5]
        await new Promise(resolve => setTimeout(resolve, 150)); // Era 60ms, ahora 150ms
    }

    // Desvanecer el aura si no fue interrumpida
    if (socket && !stopSignal && !(repeatIntervalId && !repeatActionToggle.checked)) {
        const currentCenter = getCenterCoords();
        if(currentCenter) {
            await fireAuraFadeOutUltraOptimized(currentCenter.x, currentCenter.y, maxRadius);
        } else {
            console.warn('circularFireAura: Objetivo no encontrado para el desvanecimiento final.');
        }
    }
    console.log('circularFireAura: Aura de fuego ultra optimizada finalizada.');
}

// Desvanecimiento ultra optimizado
async function fireAuraFadeOutUltraOptimized(centerX, centerY, radius) {
    if (stopSignal) { console.log('fireAuraFadeOut detenida.'); return; }
    const fadeSteps = 15;

    // Coordenadas enteras[4]
    centerX = Math.floor(centerX);
    centerY = Math.floor(centerY);

    console.log('fireAuraFadeOut: Desvaneciendo aura de fuego ultra optimizada...');

    for (let step = fadeSteps; step > 0; step--) {
        if (stopSignal) { console.log('fireAuraFadeOut detenida en bucle.'); return; }
        if (!socket || (repeatIntervalId && !repeatActionToggle.checked)) {
            console.log('fireAuraFadeOut: Detenido por interrupción o socket no disponible.');
            break;
        }

        const fadeIntensity = step / fadeSteps;
        const currentRadius = radius * fadeIntensity;
        const rings = Math.max(1, Math.floor(4 * fadeIntensity));

        // BATCH MICROSCÓPICO: Un anillo por vez[1]
        for (let ring = 0; ring < rings; ring++) {
            if (stopSignal) return;
            const ringRadius = currentRadius * (0.4 + ring * 0.2);
            const segments = Math.max(8, Math.floor(16 * fadeIntensity));

            // Procesar segmentos en grupos de 3[3]
            for (let segBatch = 0; segBatch < segments; segBatch += 3) {
                if (stopSignal) return;
                for (let segment = segBatch; segment < Math.min(segBatch + 3, segments); segment++) {
                    const angle1 = (segment / segments) * 2 * Math.PI;
                    const angle2 = ((segment + 1) / segments) * 2 * Math.PI;

                    const x1 = Math.floor(centerX + ringRadius * Math.cos(angle1));
                    const y1 = Math.floor(centerY + ringRadius * Math.sin(angle1));
                    const x2 = Math.floor(centerX + ringRadius * Math.cos(angle2));
                    const y2 = Math.floor(centerY + ringRadius * Math.sin(angle2));

                    const color = ring < 2 ? '#FF6600' : '#CC0000';
                    const thickness = Math.max(1, fadeIntensity * 4);

                    const r = parseInt(color.substr(1, 2), 16);
                    const g = parseInt(color.substr(3, 2), 16);
                    const b = parseInt(color.substr(5, 2), 16);
                    const fadedColor = `rgba(${r}, ${g}, ${b}, ${fadeIntensity})`;

                    sendDrawCommand(x1, y1, x2, y2, fadedColor, thickness);

                    // Micro-delay entre segmentos
                    await new Promise(resolve => setTimeout(resolve, 20)); // 20ms por segmento
                    if (stopSignal) return;
                }

                // Delay entre batches de segmentos[5]
                await new Promise(resolve => setTimeout(resolve, 35)); // 35ms entre grupos de 3 segmentos
                if (stopSignal) return;
            }

            // Delay entre anillos de fade
            await new Promise(resolve => setTimeout(resolve, 50)); // 50ms entre anillos
            if (stopSignal) return;
        }

        await new Promise(resolve => setTimeout(resolve, 120)); // Era 70ms, ahora 120ms
    }
    console.log('fireAuraFadeOut: Desvanecimiento ultra optimizado completado.');
}

    // Efecto: Disparo de Pistola (pistola en jugador propio, disparo al objetivo)
    async function pistolShootEffect(targetPlayerId) {
        if (stopSignal) { console.log('pistolShootEffect detenida.'); return; }
        console.log(`pistolShootEffect: Iniciando efecto - pistola en jugador propio, disparando a ${targetPlayerId}...`);

        const ownPlayerId = getOwnPlayerId(); // Obtener el ID del jugador propio
        if (!ownPlayerId) {
            console.warn('pistolShootEffect: No se pudo encontrar tu jugador propio.');
            return;
        }

        const ownAvatar = document.querySelector(`.spawnedavatar[data-playerid="${ownPlayerId}"]`);
        if (!ownAvatar) {
            console.warn('pistolShootEffect: Tu avatar no está visible en el canvas.');
            return;
        }

        const targetAvatar = document.querySelector(`.spawnedavatar[data-playerid="${targetPlayerId}"]`);
        if (!targetAvatar) {
            console.warn('pistolShootEffect: Avatar objetivo no encontrado.');
            return;
        }

        // Calcula el punto de "agarre derecho" para la pistola en el jugador propio
        const pistolAttachPoint = _getAttachmentPoint(ownPlayerId, 'grip_right');
        if (!pistolAttachPoint) { console.warn('pistolShootEffect: No se pudo determinar el punto de agarre de la pistola.'); return; }

        // Offset para la boca del cañón de la pistola, asumiendo orientación "derecha"
        const muzzleOffsetX = 47; // Desplazamiento horizontal desde el punto de agarre
        const muzzleOffsetY = -18; // Desplazamiento vertical para que quede por encima de la mano

        const muzzleX = pistolAttachPoint.x + muzzleOffsetX;
        const muzzleY = pistolAttachPoint.y + muzzleOffsetY;

        console.log('pistolShootEffect: Dibujando pistola en tu jugador...');
        // Dibuja la pistola en tu jugador, forzando la posición y orientación para el JSON
        await drawJsonCommands(ownPlayerId, PISTOLA_JSON_URL, 'grip_right', 'right', 1.0);
        if (stopSignal) return;

        if (!socket || (repeatIntervalId && !repeatActionToggle.checked)) {
            console.log('pistolShootEffect: Interrumpido antes del disparo.');
            return;
        }

        console.log('pistolShootEffect: Pistola dibujada. Esperando 0.8s para disparar...');
        await new Promise(r => setTimeout(r, 800));
        if (stopSignal) return;

        // Obtener coordenadas del OBJETIVO (no de tu jugador)
        const targetCoords = getTargetCoords(targetPlayerId);
        if (!targetCoords) {
            console.warn('pistolShootEffect: Objetivo desaparecido, no se puede disparar.');
            return;
        }

        console.log(`pistolShootEffect: Disparando desde tu jugador (${muzzleX}, ${muzzleY}) hacia objetivo (${targetCoords.x}, ${targetCoords.y})`);
        await fireBullet(muzzleX, muzzleY, targetCoords.x, targetCoords.y);
        console.log('pistolShootEffect: Disparo completado.');
    }


    // Función para animar la bala desde la pistola hasta el objetivo
    async function fireBullet(startX, startY, targetX, targetY) {
        if (stopSignal) { console.log('fireBullet detenida.'); return; }
        console.log(`fireBullet: Iniciando bala de (${startX}, ${startY}) a (${targetX}, ${targetY})...`);

        const bulletSteps = 25;
        const bulletSpeed = 1 / bulletSteps;

        const bulletColor = '#FFD700';  // Dorado para la bala
        const trailColor = '#FFA500';   // Naranja para la estela

        for (let step = 0; step <= bulletSteps; step++) {
            if (stopSignal) { console.log('fireBullet detenida en bucle.'); return; }
            if (!socket || (repeatIntervalId && !repeatActionToggle.checked)) {
                console.log('fireBullet: Disparo de bala interrumpido.');
                break;
            }

            const progress = step * bulletSpeed;

            const bulletX = startX + (targetX - startX) * progress;
            const bulletY = startY + (targetY - startY) * progress;

            const bulletSize = 3;
            sendDrawCommand(
                bulletX - bulletSize, bulletY - bulletSize,
                bulletX + bulletSize, bulletY + bulletSize,
                bulletColor, 4
            );

            if (step > 0) {
                const prevProgress = (step - 1) * bulletSpeed;
                const prevBulletX = startX + (targetX - startX) * prevProgress;
                const prevBulletY = startY + (targetY - startY) * prevProgress;

                sendDrawCommand(prevBulletX, prevBulletY, bulletX, bulletY, trailColor, 2);
            }

            await new Promise(resolve => setTimeout(resolve, 30));
        }

        if (socket && !stopSignal && !(repeatIntervalId && !repeatActionToggle.checked)) {
            await bulletImpact(targetX, targetY);
        }
        console.log('fireBullet: Bala finalizada.');
    }

    async function muzzleFlash(x, y) {
        if (stopSignal) { console.log('muzzleFlash detenida.'); return; }
        const flashSteps = 8;
        const flashRadius = 20;

        const flashColors = ['#FFFF00', '#FFA500', '#FF4500', '#FF6347'];

        console.log(`muzzleFlash: Creando fogonazo en (${x}, ${y})`);

        for (let step = 0; step < flashSteps; step++) {
            if (stopSignal) { console.log('muzzleFlash detenida en bucle.'); return; }
            if (!socket || (repeatIntervalId && !repeatActionToggle.checked)) break;

            const progress = step / flashSteps;
            const currentRadius = flashRadius * (1 - progress * 0.7);
            const flashIntensity = 1 - progress;

            const rayCount = 6;
            for (let ray = 0; ray < rayCount; ray++) {
                const angle = (ray / rayCount) * 2 * Math.PI + Math.random() * 0.5;
                const rayLength = currentRadius + Math.random() * 10;

                const endX = x + rayLength * Math.cos(angle);
                const endY = y + rayLength * Math.sin(angle);

                const colorIndex = Math.floor(Math.random() * flashColors.length);
                const color = flashColors[colorIndex];
                const thickness = Math.max(1, flashIntensity * 5);

                sendDrawCommand(x, y, endX, endY, color, thickness);
            }
            await new Promise(resolve => setTimeout(resolve, 50));
        }
        console.log('muzzleFlash: Fogonazo completado.');
    }

    async function bulletImpact(x, y) {
        if (stopSignal) { console.log('bulletImpact detenida.'); return; }
        const impactSteps = 15;
        const impactRadius = 25;
        const impactColors = ['#FF4500', '#FFD700', '#FF6347', '#FFA500'];

        console.log(`bulletImpact: Impacto de bala en (${x}, ${y})`);

        for (let step = 0; step < impactSteps; step++) {
            if (stopSignal) { console.log('bulletImpact detenida en bucle.'); return; }
            if (!socket || (repeatIntervalId && !repeatActionToggle.checked)) break;

            const progress = step / impactSteps;
            const currentRadius = impactRadius * progress;
            const sparkCount = 8;

            for (let spark = 0; spark < sparkCount; spark++) {
                const angle = (spark / sparkCount) * 2 * Math.PI + Math.random() * 0.3;
                const sparkDistance = currentRadius + Math.random() * 15;

                const endX = x + sparkDistance * Math.cos(angle);
                const endY = y + sparkDistance * Math.sin(angle);

                const colorIndex = Math.floor(Math.random() * impactColors.length);
                const color = impactColors[colorIndex];
                const thickness = Math.max(1, 4 - progress * 3);

                sendDrawCommand(x, y, endX, endY, color, thickness);
            }
            await new Promise(resolve => setTimeout(resolve, 60));
        }
        console.log('bulletImpact: Impacto completado.');
    }


    // Efecto: Cohete Espacial Perseguidor
    async function spaceRocketChaser(targetPlayerId) {
        if (stopSignal) { console.log('spaceRocketChaser detenida.'); return; }
        console.log(`spaceRocketChaser: Iniciando efecto en ${targetPlayerId}...`);
        if (!socket) {
            console.warn('spaceRocketChaser: Socket no disponible.');
            return;
        }

        const cRect = canvas.getBoundingClientRect();

        const getTargetCoordsDynamic = () => { // Usar la versión dinámica para seguir al jugador
            const currentAvatar = document.querySelector(`.spawnedavatar[data-playerid="${targetPlayerId}"]`);
            if (!currentAvatar) return null;
            const currentARect = currentAvatar.getBoundingClientRect();
            return {
                x: Math.round((currentARect.left - cRect.left) + (currentARect.width / 2)),
                y: Math.round((currentARect.top - cRect.top) + (currentARect.height / 2))
            };
        };

        const spawnSides = [
            { x: 20, y: Math.round(Math.random() * canvas.height) },
            { x: Math.round(canvas.width - 20), y: Math.round(Math.random() * canvas.height) },
            { x: Math.round(Math.random() * canvas.width), y: 20 },
            { x: Math.round(Math.random() * canvas.width), y: Math.round(canvas.height - 20) }
        ];
        const spawnPoint = spawnSides[Math.floor(Math.random() * spawnSides.length)];

        let rocketX = spawnPoint.x;
        let rocketY = spawnPoint.y;

        const totalSteps = 80;
        const rocketSpeed = 0.08;

        for (let step = 0; step < totalSteps; step++) {
            if (stopSignal) { console.log('spaceRocketChaser detenida en bucle.'); return; }
            if (!socket || (repeatIntervalId && !repeatActionToggle.checked)) {
                console.log('spaceRocketChaser: Detenido por interrupción.');
                break;
            }

            const targetCoords = getTargetCoordsDynamic(); // Usar la versión dinámica
            if (!targetCoords) {
                console.log('spaceRocketChaser: Objetivo desaparecido.');
                break;
            }

            const targetX = targetCoords.x;
            const targetY = targetCoords.y;

            const directionX = targetX - rocketX;
            const directionY = targetY - rocketY;
            const distance = Math.sqrt(directionX * directionX + directionY * directionY);

            if (distance < 15) {
                console.log('spaceRocketChaser: ¡Colisión detectada!');
                await rocketExplosion(rocketX, rocketY);
                return;
            }

            const normalizedX = directionX / distance;
            const normalizedY = directionY / distance;

            const nextX = rocketX + normalizedX * distance * rocketSpeed;
            const nextY = rocketY + normalizedY * distance * rocketSpeed;

            const angle = Math.atan2(directionY, directionX);

            await drawSpaceRocket(rocketX, rocketY, nextX, nextY, angle, step);
            if (stopSignal) return;

            rocketX = nextX;
            rocketY = nextY;

            const baseDelay = 45;
            const progress = step / totalSteps;
            const speedFactor = 1 + progress;
            await new Promise(resolve => setTimeout(resolve, baseDelay / speedFactor));
        }

        // Si no colisionó, explotar en la última posición conocida del objetivo
        if (socket && !stopSignal && !(repeatIntervalId && !repeatActionToggle.checked)) {
            const finalTarget = getTargetCoordsDynamic(); // Usar la versión dinámica
            if (finalTarget) {
                console.log('spaceRocketChaser: Camino completo. Iniciando explosión final...');
                await rocketExplosion(finalTarget.x, finalTarget.y);
            } else {
                console.warn('spaceRocketChaser: Objetivo no encontrado para la explosión final.');
            }
        }
        console.log('spaceRocketChaser: Efecto completado.');
    }

    async function drawSpaceRocket(currentX, currentY, nextX, nextY, angle, step) {
        if (stopSignal) return;
        const rocketSize = 12;
        const thrusterLength = 15;

        const rocketColors = {
            body: '#C0C0C0',
            nose: '#FF6B6B',
            thruster: '#FF4500',
            flame: '#FFD700'
        };

        const cosA = Math.cos(angle);
        const sinA = Math.sin(angle);

        const noseX = nextX + cosA * rocketSize;
        const noseY = nextY + sinA * rocketSize;

        const bodyStartX = nextX - cosA * (rocketSize * 0.3);
        const bodyStartY = nextY - sinA * (rocketSize * 0.3);

        const perpX = -sinA * (rocketSize * 0.4);
        const perpY = cosA * (rocketSize * 0.4);

        const finLeft1X = bodyStartX + perpX;
        const finLeft1Y = bodyStartY + perpY;
        const finRight1X = bodyStartX - perpX;
        const finRight1Y = bodyStartY - perpY;

        const tailX = nextX - cosA * rocketSize;
        const tailY = nextY - sinA * rocketSize;

        sendDrawCommand(bodyStartX, bodyStartY, noseX, noseY, rocketColors.body, 4);
        sendDrawCommand(bodyStartX, bodyStartY, noseX, noseY, rocketColors.nose, 2);

        sendDrawCommand(bodyStartX, bodyStartY, finLeft1X, finLeft1Y, rocketColors.body, 2);
        sendDrawCommand(bodyStartX, bodyStartY, finRight1X, finRight1Y, rocketColors.body, 2);

        const flameIntensity = 0.7 + 0.3 * Math.sin(step * 0.3);
        if (flameIntensity > 0.8) {
            const flameLength = thrusterLength * flameIntensity;
            const flameEndX = tailX - cosA * flameLength;
            const flameEndY = tailY - sinA * flameLength;

            sendDrawCommand(tailX, tailY, flameEndX, flameEndY, rocketColors.flame, 3);

            const flame2X = flameEndX - cosA * 5 + perpX * 0.3;
            const flame2Y = flameEndY - sinA * 5 + perpY * 0.3;
            const flame3X = flameEndX - cosA * 5 - perpX * 0.3;
            const flame3Y = flameEndY - sinA * 5 - perpY * 0.3;

            sendDrawCommand(tailX, tailY, flame2X, flame2Y, rocketColors.thruster, 2);
            sendDrawCommand(tailX, tailY, flame3X, flame3Y, rocketColors.thruster, 2);
        }

        sendDrawCommand(currentX, currentY, nextX, nextY, '#87CEEB', 1);
    }

async function rocketExplosion(centerX, centerY) {
    if (stopSignal) { console.log('rocketExplosion detenida.'); return; }
    const explosionSteps = 20; // REDUCIDO de 30 a 20
    const maxRadius = 70; // REDUCIDO de 80 a 70

    // Coordenadas enteras para evitar sub-pixel rendering[3]
    centerX = Math.floor(centerX);
    centerY = Math.floor(centerY);

    console.log(`rocketExplosion: ¡Explosión ULTRA optimizada en (${centerX}, ${centerY})!`);

    // Pre-calcular ángulos para batch rendering[1]
    const fragmentsPerStep = 12; // REDUCIDO de 15 a 12
    const explosionColors = ['#FF4500', '#FFD700', '#FF6B6B']; // REDUCIDO de 5 a 3 colores
    const preCalculatedAngles = [];

    for (let i = 0; i < fragmentsPerStep; i++) {
        preCalculatedAngles.push((i / fragmentsPerStep) * 2 * Math.PI);
    }

    for (let step = 0; step < explosionSteps; step++) {
        if (stopSignal) { console.log('rocketExplosion detenida en bucle.'); return; }
        if (!socket || (repeatIntervalId && !repeatActionToggle.checked)) {
            console.log('rocketExplosion: Detenida por interrupción.');
            break;
        }

        const progress = step / explosionSteps;
        const currentRadius = Math.floor(maxRadius * progress); // Coordenadas enteras[3]

        // ULTRA BATCH RENDERING: Una sola operación por color[1][2]
        for (let colorIdx = 0; colorIdx < explosionColors.length; colorIdx++) {
            if (stopSignal) return;
            const color = explosionColors[colorIdx];
            const commandBatch = [];

            // Preparar TODOS los comandos de este color antes de enviar[2]
            for (let fragment = 0; fragment < fragmentsPerStep; fragment++) {
                // Solo procesar fragmentos de este color
                if (fragment % explosionColors.length !== colorIdx) continue;

                const angle = preCalculatedAngles[fragment] + Math.random() * 0.3;
                const fragmentDistance = Math.floor(currentRadius + Math.random() * 20);

                const endX = Math.floor(centerX + fragmentDistance * Math.cos(angle));
                const endY = Math.floor(centerY + fragmentDistance * Math.sin(angle));

                const thickness = Math.max(1, Math.floor(6 - progress * 4));

                commandBatch.push({
                    startX: centerX,
                    startY: centerY,
                    endX,
                    endY,
                    thickness
                });
            }

            // BATCH: Enviar todos los comandos del mismo color juntos[1][4]
            commandBatch.forEach(cmd => {
                sendDrawCommand(cmd.startX, cmd.startY, cmd.endX, cmd.endY, color, cmd.thickness);
            });

            // Delay MÍNIMO entre colores
            if (colorIdx < explosionColors.length - 1) {
                await new Promise(resolve => setTimeout(resolve, 25)); // 25ms entre colores
                if (stopSignal) return;
            }
        }

        // Chispas ULTRA REDUCIDAS - solo en pasos específicos[5]
        if (step % 4 === 0 && progress < 0.6) {
            const sparkCount = 3; // ULTRA REDUCIDO
            for (let spark = 0; spark < sparkCount; spark++) {
                const sparkAngle = (spark / sparkCount) * 2 * Math.PI;
                const sparkRadius = Math.floor(currentRadius * 1.1);
                const sparkX = Math.floor(centerX + sparkRadius * Math.cos(sparkAngle));
                const sparkY = Math.floor(centerY + sparkRadius * Math.sin(sparkAngle));

                const sparkEndX = Math.floor(sparkX + (Math.random() - 0.5) * 8);
                const sparkEndY = Math.floor(sparkY + (Math.random() - 0.5) * 8);

                sendDrawCommand(sparkX, sparkY, sparkEndX, sparkEndY, '#FFFF00', 1);
            }

            await new Promise(resolve => setTimeout(resolve, 30)); // Delay para chispas
            if (stopSignal) return;
        }

        // Delay ULTRA AUMENTADO para evitar sobrecarga[4]
        const baseDelay = 80 + progress * 40; // Era 40 + progress * 20
        await new Promise(resolve => setTimeout(resolve, Math.max(baseDelay, 100))); // Mínimo 100ms
    }

    // Flash final ULTRA SIMPLIFICADO
    if (!stopSignal) {
        await ultraSimplifiedFlash(centerX, centerY);
    }

    console.log('rocketExplosion: Explosión ULTRA optimizada completada.');
}

// Flash final ultra simplificado para evitar crashes
async function ultraSimplifiedFlash(centerX, centerY) {
    if (stopSignal) return;
    const flashSteps = 6; // ULTRA REDUCIDO de 8
    const flashColors = ['#FFFFFF', '#FFD700']; // Solo 2 colores

    for (let step = 0; step < flashSteps; step++) {
        if (stopSignal) return;
        if (!socket || (repeatIntervalId && !repeatActionToggle.checked)) break;

        const progress = step / flashSteps;
        const intensity = 1 - progress;
        const flashRadius = Math.floor(50 * intensity); // Coordenadas enteras[3]

        // BATCH: Solo 1 color por step para máxima optimización[1]
        const color = flashColors[step % flashColors.length];
        const rayCount = 8; // REDUCIDO

        // Pre-calcular todos los rayos antes de enviar[2]
        const rayBatch = [];
        for (let ray = 0; ray < rayCount; ray++) {
            const rayAngle = (ray / rayCount) * 2 * Math.PI;
            const rayEndX = Math.floor(centerX + flashRadius * Math.cos(rayAngle));
            const rayEndY = Math.floor(centerY + flashRadius * Math.sin(rayAngle));

            rayBatch.push({ endX: rayEndX, endY: rayEndY });
        }

        // Enviar batch completo[4]
        rayBatch.forEach(ray => {
            sendDrawCommand(centerX, centerY, ray.endX, ray.endY, color, Math.max(1, 4 * intensity));
        });

        await new Promise(resolve => setTimeout(resolve, 120)); // DELAY ULTRA AUMENTADO
    }
}


    // Efecto: Flashlight Supernova
    async function flashlightStarChaser(targetPlayerId) {
        if (stopSignal) { console.log('flashlightStarChaser detenida.'); return; }
        console.log(`flashlightStarChaser: Iniciando efecto en ${targetPlayerId}...`);
        if (!socket) {
            console.warn('flashlightStarChaser: Socket no disponible.');
            return;
        }

        const cRect = canvas.getBoundingClientRect();

        const getTargetCoordsDynamic = () => { // Usar la versión dinámica para seguir al jugador
            const currentAvatar = document.querySelector(`.spawnedavatar[data-playerid="${targetPlayerId}"]`);
            if (!currentAvatar) return null;
            const currentARect = currentAvatar.getBoundingClientRect();
            return {
                x: Math.round((currentARect.left - cRect.left) + (currentARect.width / 2)),
                y: Math.round((currentARect.top - cRect.top) + (currentARect.height / 2))
            };
        };

        const spawnCorners = [
            { x: 30, y: 30 },
            { x: Math.round(canvas.width - 30), y: 30 },
            { x: 30, y: Math.round(canvas.height - 30) },
            { x: Math.round(canvas.width - 30), y: Math.round(canvas.height - 30) }
        ];

        const spawnPoint = spawnCorners[Math.floor(Math.random() * spawnCorners.length)];
        let starX = spawnPoint.x;
        let starY = spawnPoint.y;

        const totalSteps = 25; // Reducido significativamente
        const starSpeed = 0.2; // Más rápido para compensar
        const baseDelay = 110; // Más tiempo entre frames

        for (let step = 0; step < totalSteps; step++) {
            if (stopSignal) { console.log('flashlightStarChaser detenida en bucle.'); return; }
            if (!socket || (repeatIntervalId && !repeatActionToggle.checked)) {
                console.log('flashlightStarChaser: Detenido por interrupción.');
                break;
            }

            const targetCoords = getTargetCoordsDynamic(); // Usar la versión dinámica
            if (!targetCoords) {
                console.log('flashlightStarChaser: Objetivo perdido.');
                break;
            }

            const directionX = targetCoords.x - starX;
            const directionY = targetCoords.y - starY;
            const distance = Math.sqrt(directionX * directionX + directionY * directionY);

            if (distance < 25) {
                console.log('flashlightStarChaser: ¡Colisión! Iniciando explosión optimizada...');
                await veryOptimizedExplosion(starX, starY);
                return;
            }

            const normalizedX = directionX / distance;
            const normalizedY = directionY / distance;

            starX = starX + normalizedX * distance * starSpeed;
            starY = starY + normalizedY * distance * starSpeed;

            await drawVeryOptimizedStar(starX, starY, step);
            if (stopSignal) return;

            const progress = step / totalSteps;
            const adaptiveDelay = baseDelay + (progress * 30);
            await new Promise(resolve => setTimeout(resolve, adaptiveDelay));
        }

        // Si no colisionó, explotar en la última posición conocida del objetivo
        if (socket && !stopSignal && !(repeatIntervalId && !repeatActionToggle.checked)) {
            const finalTarget = getTargetCoordsDynamic(); // Usar la versión dinámica
            if (finalTarget) {
                console.log('flashlightStarChaser: Camino completo. Iniciando explosión final...');
                await veryOptimizedExplosion(finalTarget.x, finalTarget.y);
            } else {
                console.warn('flashlightStarChaser: Objetivo no encontrado para la explosión final.');
            }
        }
        console.log('flashlightStarChaser: Efecto completado.');
    }

    async function drawVeryOptimizedStar(x, y, step) {
        if (stopSignal) return;
        const colors = ['#FFFFFF', '#9370DB', '#4169E1'];

        const coreSize = 6;
        sendDrawCommand(x - coreSize, y, x + coreSize, y, colors[0], 4);
        sendDrawCommand(x, y - coreSize, x, y + coreSize, colors[0], 4);

        const rayLength = 12;
        for (let ray = 0; ray < 3; ray++) {
            const angle = (ray / 3) * Math.PI * 2 + step * 0.15;
            const endX = x + rayLength * Math.cos(angle);
            const endY = y + rayLength * Math.sin(angle);
            sendDrawCommand(x, y, endX, endY, colors[1], 2);
        }

        const auraSize = 8;
        const auraAngle = step * 0.1;
        const auraX = x + auraSize * Math.cos(auraAngle);
        const auraY = y + auraSize * Math.sin(auraAngle);
        sendDrawCommand(x, y, auraX, auraY, colors[2], 1);
    }

    async function veryOptimizedExplosion(centerX, centerY) {
        if (stopSignal) { console.log('veryOptimizedExplosion detenida.'); return; }
        console.log(`veryOptimizedExplosion: Explosión en (${centerX}, ${centerY})`);

        await veryOptimizedFlash(centerX, centerY);
        if (stopSignal || !socket || (repeatIntervalId && !repeatActionToggle.checked)) return;

        await veryOptimizedWave(centerX, centerY);
        if (stopSignal || !socket || (repeatIntervalId && !repeatActionToggle.checked)) return;

        console.log('veryOptimizedExplosion: Explosión completada.');
    }

    async function veryOptimizedFlash(centerX, centerY) {
        if (stopSignal) return;
        const flashSteps = 5;
        const maxRadius = 35;
        const colors = ['#FFFFFF', '#E0E6FF'];

        for (let step = 0; step < flashSteps; step++) {
            if (stopSignal || !socket || (repeatIntervalId && !repeatActionToggle.checked)) break;

            const progress = step / flashSteps;
            const radius = maxRadius * (1 - progress * 0.6);
            const intensity = 1 - progress;

            const rayCount = 6;
            for (let ray = 0; ray < rayCount; ray++) {
                const angle = (ray / rayCount) * 2 * Math.PI;
                const rayLength = radius * intensity;

                const endX = centerX + rayLength * Math.cos(angle);
                const endY = centerY + rayLength * Math.sin(angle);

                const color = colors[step % colors.length];
                const thickness = Math.max(1, intensity * 4);

                sendDrawCommand(centerX, centerY, endX, endY, color, thickness);
            }
            await new Promise(resolve => setTimeout(resolve, 100));
        }
    }

    async function veryOptimizedWave(centerX, centerY) {
        if (stopSignal) return;
        const waveSteps = 10;
        const maxRadius = 70;
        const color = '#4169E1';

        for (let step = 0; step < waveSteps; step++) {
            if (stopSignal || !socket || (repeatIntervalId && !repeatActionToggle.checked)) break;

            const progress = step / waveSteps;
            const waveRadius = maxRadius * progress;
            const intensity = 1 - progress;

            const segments = 8;
            for (let seg = 0; seg < segments; seg++) {
                const angle1 = (seg / segments) * 2 * Math.PI;
                const angle2 = ((seg + 1) / segments) * 2 * Math.PI;

                const x1 = centerX + waveRadius * Math.cos(angle1);
                const y1 = centerY + waveRadius * Math.sin(angle1);
                const x2 = centerX + waveRadius * Math.cos(angle2);
                const y2 = centerY + waveRadius * Math.sin(angle2);

                const thickness = Math.max(1, intensity * 3);
                sendDrawCommand(x1, y1, x2, y2, color, thickness);
            }
            await new Promise(resolve => setTimeout(resolve, 120));
        }
    }

    // Efecto: Arco y Flecha Perseguidor
    async function drawArrowChaser(targetPlayerId) {
        if (stopSignal) { console.log('drawArrowChaser detenida.'); return; }
        console.log(`drawArrowChaser: Iniciando efecto en ${targetPlayerId}.`);

        const ownPlayerId = getOwnPlayerId(); // Get own player ID
        if (!ownPlayerId) { console.warn('drawArrowChaser: No se pudo encontrar tu jugador propio.'); return; }

        await drawJsonCommands(ownPlayerId, ARCO_JSON_URL, 'grip_right', 'right', 1.0);
        if (stopSignal) return;

        const bowAttachPoint = _getAttachmentPoint(ownPlayerId, 'grip_right');
        if (!bowAttachPoint) { console.warn('drawArrowChaser: No se pudo determinar el punto de agarre del arco.'); return; }

        const arrowLaunchOffsetX = 50;
        const arrowLaunchOffsetY = 0;

        const arrowOrigin = {
            x: bowAttachPoint.x + arrowLaunchOffsetX,
            y: bowAttachPoint.y + arrowLaunchOffsetY
        };

        const totalSteps = 40;
        const arrowSpeedFactor = 0.1;
        const wobbleIntensity = 15;
        const arrowColor = '#A52A2A';
        const featherColor = '#FFFFFF';

        let currentX = arrowOrigin.x;
        let currentY = arrowOrigin.y;

        for (let step = 0; step < totalSteps; step++) {
            if (stopSignal) { console.log('drawArrowChaser detenida en bucle.'); return; }
            if (!socket || (repeatIntervalId && !repeatActionToggle.checked)) {
                console.log('drawArrowChaser: Detenido por interrupción.'); break;
            }

            const targetCoords = getTargetCoords(targetPlayerId);
            if (!targetCoords) { console.log('drawArrowChaser: Objetivo perdido.'); break; }

            const directionX = targetCoords.x - currentX;
            const directionY = targetCoords.y - currentY;
            const dist = distance(currentX, currentY, targetCoords.x, targetCoords.y);

            if (dist < 15) {
                await bulletImpact(currentX, currentY);
                return;
            }

            const normalizedX = directionX / dist;
            const normalizedY = directionY / dist;

            const wobbleOffset = Math.sin(step * 0.8) * wobbleIntensity * (1 - step / totalSteps);
            const perpX = -normalizedY;
            const perpY = normalizedX;

            const nextX = currentX + normalizedX * dist * arrowSpeedFactor + perpX * wobbleOffset;
            const nextY = currentY + normalizedY * dist * arrowSpeedFactor + perpY * wobbleOffset;

            const angle = Math.atan2(directionY, directionX);

            await _drawArrow(currentX, currentY, nextX, nextY, angle, arrowColor, featherColor);
            if (stopSignal) return;

            currentX = nextX;
            currentY = nextY;

            await new Promise(resolve => setTimeout(resolve, 50));
        }

        const finalTarget = getTargetCoords(targetPlayerId);
        if (finalTarget && socket && !stopSignal && !(repeatIntervalId && !repeatActionToggle.checked)) {
            await bulletImpact(finalTarget.x, finalTarget.y);
        }
        console.log('drawArrowChaser: Efecto completado.');
    }

    // Dibuja una flecha (segmento principal y plumas simplificadas)
    async function _drawArrow(x1, y1, x2, y2, angle, color, featherColor) {
        if (stopSignal) return;
        const arrowHeadLength = 10;
        const featherLength = 8;
        const featherAngleOffset = Math.PI / 6;

        sendDrawCommand(x1, y1, x2, y2, color, 2);

        const tipX1 = x2 - arrowHeadLength * Math.cos(angle - Math.PI / 6);
        const tipY1 = y2 - arrowHeadLength * Math.sin(angle - Math.PI / 6);
        const tipX2 = x2 - arrowHeadLength * Math.cos(angle + Math.PI / 6);
        const tipY2 = y2 - arrowHeadLength * Math.sin(angle + Math.PI / 6);

        sendDrawCommand(x2, y2, tipX1, tipY1, color, 2);
        sendDrawCommand(x2, y2, tipX2, tipY2, color, 2);

        const tailX = x1 - (Math.cos(angle) * 5);
        const tailY = y1 - (Math.sin(angle) * 5);

        const feather1X = tailX - featherLength * Math.cos(angle + featherAngleOffset);
        const feather1Y = tailY - featherLength * Math.sin(angle + featherAngleOffset);
        const feather2X = tailX - featherLength * Math.cos(angle - featherAngleOffset);
        const feather2Y = tailY - featherLength * Math.sin(angle - featherAngleOffset);

        sendDrawCommand(tailX, tailY, feather1X, feather1Y, featherColor, 1);
        sendDrawCommand(tailX, tailY, feather2X, feather2Y, featherColor, 1);
    }

// Efecto: Escopeta - Portal Mágico (ULTRA DELAYS para servidor)
async function drawShotgunBlast(targetPlayerId) {
    if (stopSignal) { console.log('drawShotgunBlast detenida.'); return; }
    console.log(`drawShotgunBlast: Iniciando portal mágico en ${targetPlayerId}.`);

    const ownPlayerId = getOwnPlayerId();
    if (!ownPlayerId) { console.warn('drawShotgunBlast: No se pudo encontrar tu jugador propio.'); return; }

    await drawJsonCommands(ownPlayerId, ESCOPETA_JSON_URL, 'grip_right', 'right', 1.0);
    if (stopSignal) return;

    await new Promise(resolve => setTimeout(resolve, 300));
    if (stopSignal) return;

    const shotgunAttachPoint = _getAttachmentPoint(ownPlayerId, 'grip_right');
    if (!shotgunAttachPoint) { console.warn('drawShotgunBlast: No se pudo determinar el punto de agarre de la escopeta.'); return; }

    const portalCenter = {
        x: shotgunAttachPoint.x + 80,
        y: shotgunAttachPoint.y + -20
    };

    const targetCoords = getTargetCoords(targetPlayerId);
    if (!targetCoords) { console.warn('drawShotgunBlast: No se pudo determinar el objetivo.'); return; }

    console.log('drawShotgunBlast: Abriendo portal dimensional...');

    await openMagicPortalUltraDelayed(portalCenter.x, portalCenter.y);
    if (stopSignal) return;
    await new Promise(resolve => setTimeout(resolve, 500));
    if (stopSignal) return;
    await launchMagicProjectilesUltraDelayed(portalCenter, targetCoords);
    if (stopSignal) return;
    await new Promise(resolve => setTimeout(resolve, 500));
    if (stopSignal) return;
    await closeMagicPortalUltraDelayed(portalCenter.x, portalCenter.y);

    console.log('drawShotgunBlast: Portal mágico completado.');
}

// Abrir portal con ULTRA delays
async function openMagicPortalUltraDelayed(centerX, centerY) {
    if (stopSignal) return;
    const openingSteps = 20;
    const maxRadius = 50;
    const portalColors = ['#9400D3', '#4B0082', '#8A2BE2', '#9932CC'];
    const starColors = ['#FFD700', '#FFFFFF', '#00FFFF'];

    centerX = Math.floor(centerX);
    centerY = Math.floor(centerY);

    for (let step = 0; step < openingSteps; step++) {
        if (stopSignal || !socket || (repeatIntervalId && !repeatActionToggle.checked)) break;

        const progress = step / openingSteps;
        const currentRadius = maxRadius * Math.sin((progress * Math.PI) / 2);

        for (let colorIdx = 0; colorIdx < portalColors.length; colorIdx += 2) {
            if (stopSignal) return;
            const ringSegments = 16;
            for (let segBatch = 0; segBatch < ringSegments; segBatch += 4) {
                if (stopSignal) return;
                for (let seg = segBatch; seg < Math.min(segBatch + 4, ringSegments); seg++) {
                    if (Math.floor((seg + step) % portalColors.length) !== colorIdx) continue;
                    const angle1 = (seg / ringSegments) * 2 * Math.PI + step * 0.1;
                    const angle2 = ((seg + 1) / ringSegments) * 2 * Math.PI + step * 0.1;
                    const x1 = Math.floor(centerX + currentRadius * Math.cos(angle1));
                    const y1 = Math.floor(centerY + currentRadius * Math.sin(angle1) * 0.7);
                    const x2 = Math.floor(centerX + currentRadius * Math.cos(angle2));
                    const y2 = Math.floor(centerY + currentRadius * Math.sin(angle2) * 0.7);
                    const thickness = Math.max(2, 6 - progress * 2);
                    sendDrawCommand(x1, y1, x2, y2, portalColors[colorIdx], thickness);
                }
                await new Promise(resolve => setTimeout(resolve, 15));
                if (stopSignal) return;
            }
            await new Promise(resolve => setTimeout(resolve, 25));
            if (stopSignal) return;
        }

        if (step > 5) {
            const energyLines = 8;
            for (let lineBatch = 0; lineBatch < energyLines; lineBatch += 2) {
                if (stopSignal) return;
                for (let line = lineBatch; line < Math.min(lineBatch + 2, energyLines); line++) {
                    const angle = (line / energyLines) * 2 * Math.PI + Math.random() * 0.3;
                    const startRadius = currentRadius * 1.2;
                    const endRadius = currentRadius * 0.3;
                    const startX = Math.floor(centerX + startRadius * Math.cos(angle));
                    const startY = Math.floor(centerY + startRadius * Math.sin(angle) * 0.7);
                    const endX = Math.floor(centerX + endRadius * Math.cos(angle));
                    const endY = Math.floor(centerY + endRadius * Math.sin(angle) * 0.7);
                    const color = starColors[Math.floor(Math.random() * starColors.length)];
                    sendDrawCommand(startX, startY, endX, endY, color, 2);
                }
                await new Promise(resolve => setTimeout(resolve, 20));
                if (stopSignal) return;
            }
        }

        for (let particle = 0; particle < 3; particle++) {
            if (stopSignal) return;
            const particleAngle = Math.random() * 2 * Math.PI;
            const particleRadius = currentRadius * (0.8 + Math.random() * 0.4);
            const px = Math.floor(centerX + particleRadius * Math.cos(particleAngle));
            const py = Math.floor(centerY + particleRadius * Math.sin(particleAngle) * 0.7);
            sendDrawCommand(px - 2, py - 2, px + 2, py + 2, '#FFD700', 2);
            if (particle < 2) {
                await new Promise(resolve => setTimeout(resolve, 10));
                if (stopSignal) return;
            }
        }
        await new Promise(resolve => setTimeout(resolve, 150));
    }
}

// Proyectiles con delays ULTRA aumentados
async function launchMagicProjectilesUltraDelayed(portalCenter, targetCoords) {
    if (stopSignal) return;
    const numProjectiles = 5;
    const projectileColors = ['#FF1493', '#00CED1', '#32CD32', '#FFD700', '#FF69B4'];

    for (let i = 0; i < numProjectiles; i++) {
        if (stopSignal || !socket || (repeatIntervalId && !repeatActionToggle.checked)) break;
        console.log(`Lanzando proyectil ${i + 1}/${numProjectiles}`);
        await launchSingleMagicProjectileUltraDelayed(portalCenter, targetCoords, projectileColors[i], i);
        if (stopSignal) return;
        await new Promise(resolve => setTimeout(resolve, 400));
    }
}

// Proyectil individual ULTRA ralentizado
async function launchSingleMagicProjectileUltraDelayed(startPoint, targetCoords, color, index) {
    if (stopSignal) return;
    const totalSteps = 25;
    const sparkTrail = [];
    const offsetAngle = (index - 2) * 0.3;
    const curveIntensity = 30;

    let currentX = Math.floor(startPoint.x);
    let currentY = Math.floor(startPoint.y);

    for (let step = 0; step < totalSteps; step++) {
        if (stopSignal || !socket || (repeatIntervalId && !repeatActionToggle.checked)) break;

        const progress = step / totalSteps;
        const baseX = startPoint.x + (targetCoords.x - startPoint.x) * progress;
        const baseY = startPoint.y + (targetCoords.y - startPoint.y) * progress;
        const curve = Math.sin(progress * Math.PI) * curveIntensity;
        const nextX = Math.floor(baseX + Math.cos(offsetAngle) * curve);
        const nextY = Math.floor(baseY + Math.sin(offsetAngle) * curve - curve * 0.5);

        sendDrawCommand(currentX, currentY, nextX, nextY, color, 4);
        await new Promise(resolve => setTimeout(resolve, 15));
        if (stopSignal) return;

        const auraRadius = 8;
        const auraSpokes = 6;
        for (let spokeBatch = 0; spokeBatch < auraSpokes; spokeBatch += 2) {
            if (stopSignal) return;
            for (let spoke = spokeBatch; spoke < Math.min(spokeBatch + 2, auraSpokes); spoke++) {
                const spokeAngle = (spoke / auraSpokes) * 2 * Math.PI + step * 0.2;
                const auraX = Math.floor(nextX + auraRadius * Math.cos(spokeAngle));
                const auraY = Math.floor(nextY + auraRadius * Math.sin(spokeAngle));
                sendDrawCommand(nextX, nextY, auraX, auraY, color, 1);
            }
            await new Promise(resolve => setTimeout(resolve, 8));
            if (stopSignal) return;
        }

        sparkTrail.push({ x: nextX, y: nextY, life: 1.0 });
        if (sparkTrail.length > 8) sparkTrail.shift();
        const trailBatch = 4;
        for (let t = 0; t < sparkTrail.length; t += trailBatch) {
            if (stopSignal) return;
            for (let idx = t; idx < Math.min(t + trailBatch, sparkTrail.length); idx++) {
                const spark = sparkTrail[idx];
                const trailIntensity = spark.life * (idx / sparkTrail.length);
                if (trailIntensity > 0.3) {
                    sendDrawCommand(spark.x - 1, spark.y - 1, spark.x + 1, spark.y + 1, color, Math.max(1, 3 * trailIntensity));
                }
                spark.life -= 0.1;
            }
            if (t + trailBatch < sparkTrail.length) {
                await new Promise(resolve => setTimeout(resolve, 5));
                if (stopSignal) return;
            }
        }
        currentX = nextX;
        currentY = nextY;
        await new Promise(resolve => setTimeout(resolve, 85));
    }
    if (!stopSignal) await magicImpactBurstUltraDelayed(currentX, currentY, color);
}

// Impacto ULTRA ralentizado
async function magicImpactBurstUltraDelayed(x, y, color) {
    if (stopSignal) return;
    const burstSteps = 10;
    const burstRadius = 25;
    x = Math.floor(x);
    y = Math.floor(y);

    for (let step = 0; step < burstSteps; step++) {
        if (stopSignal || !socket || (repeatIntervalId && !repeatActionToggle.checked)) break;
        const progress = step / burstSteps;
        const currentRadius = burstRadius * progress;
        const intensity = 1 - progress;
        const sparkCount = 8;
        for (let spark = 0; spark < sparkCount; spark++) {
            if (stopSignal) return;
            const angle = (spark / sparkCount) * 2 * Math.PI + Math.random() * 0.5;
            const sparkDistance = currentRadius + Math.random() * 10;
            const endX = Math.floor(x + sparkDistance * Math.cos(angle));
            const endY = Math.floor(y + sparkDistance * Math.sin(angle));
            sendDrawCommand(x, y, endX, endY, color, Math.max(1, 3 * intensity));
            await new Promise(resolve => setTimeout(resolve, 12));
            if (stopSignal) return;
        }
        await new Promise(resolve => setTimeout(resolve, 100));
    }
}

// Cierre ULTRA ralentizado
async function closeMagicPortalUltraDelayed(centerX, centerY) {
    if (stopSignal) return;
    const closingSteps = 15;
    const startRadius = 50;
    centerX = Math.floor(centerX);
    centerY = Math.floor(centerY);

    for (let step = 0; step < closingSteps; step++) {
        if (stopSignal || !socket || (repeatIntervalId && !repeatActionToggle.checked)) break;
        const progress = step / closingSteps;
        const currentRadius = startRadius * (1 - progress);
        const intensity = 1 - progress;
        const implosionLines = 12;
        for (let lineBatch = 0; lineBatch < implosionLines; lineBatch += 3) {
            if (stopSignal) return;
            for (let line = lineBatch; line < Math.min(lineBatch + 3, implosionLines); line++) {
                const angle = (line / implosionLines) * 2 * Math.PI;
                const startX = Math.floor(centerX + currentRadius * Math.cos(angle));
                const startY = Math.floor(centerY + currentRadius * Math.sin(angle) * 0.7);
                const endRadius = currentRadius * 0.3;
                const endX = Math.floor(centerX + endRadius * Math.cos(angle));
                const endY = Math.floor(centerY + endRadius * Math.sin(angle) * 0.7);
                sendDrawCommand(startX, startY, endX, endY, '#9400D3', Math.max(1, 4 * intensity));
            }
            await new Promise(resolve => setTimeout(resolve, 30));
            if (stopSignal) return;
        }
        await new Promise(resolve => setTimeout(resolve, 180));
    }
    if (stopSignal) return;
    await new Promise(resolve => setTimeout(resolve, 500));
    if (stopSignal) return;
    sendDrawCommand(centerX - 15, centerY, centerX + 15, centerY, '#FFFFFF', 6);
    await new Promise(resolve => setTimeout(resolve, 100));
    if (stopSignal) return;
    sendDrawCommand(centerX, centerY - 15, centerX, centerY + 15, '#FFFFFF', 6);
}

// Proyectiles optimizados
async function launchMagicProjectilesOptimized(portalCenter, targetCoords) {
    if (stopSignal) return;
    const numProjectiles = 3;
    const projectileColors = ['#FF1493', '#00CED1', '#FFD700'];

    for (let i = 0; i < numProjectiles; i++) {
        if (stopSignal || !socket || (repeatIntervalId && !repeatActionToggle.checked)) break;
        await launchSingleMagicProjectileOptimized(portalCenter, targetCoords, projectileColors[i], i);
        if (stopSignal) return;
        await new Promise(resolve => setTimeout(resolve, 200));
    }
}

// Proyectil individual optimizado
async function launchSingleMagicProjectileOptimized(startPoint, targetCoords, color, index) {
    if (stopSignal) return;
    const totalSteps = 15;
    const curveIntensity = 20;
    let currentX = startPoint.x;
    let currentY = startPoint.y;

    for (let step = 0; step < totalSteps; step++) {
        if (stopSignal || !socket || (repeatIntervalId && !repeatActionToggle.checked)) break;
        const progress = step / totalSteps;
        const offsetAngle = (index - 1) * 0.4;
        const baseX = startPoint.x + (targetCoords.x - startPoint.x) * progress;
        const baseY = startPoint.y + (targetCoords.y - startPoint.y) * progress;
        const curve = Math.sin(progress * Math.PI) * curveIntensity;
        const nextX = baseX + Math.cos(offsetAngle) * curve;
        const nextY = baseY - curve * 0.3;

        sendDrawCommand(currentX, currentY, nextX, nextY, color, 3);

        if (step % 2 === 0) {
            const auraRadius = 6;
            const auraSpokes = 3;
            for (let spoke = 0; spoke < auraSpokes; spoke++) {
                const spokeAngle = (spoke / auraSpokes) * 2 * Math.PI;
                const auraX = nextX + auraRadius * Math.cos(spokeAngle);
                const auraY = nextY + auraRadius * Math.sin(spokeAngle);
                sendDrawCommand(nextX, nextY, auraX, auraY, color, 1);
            }
        }
        currentX = nextX;
        currentY = nextY;
        await new Promise(resolve => setTimeout(resolve, 70));
    }
    if (!stopSignal) await magicImpactBurstOptimized(currentX, currentY, color);
}

// Impacto optimizado
async function magicImpactBurstOptimized(x, y, color) {
    if (stopSignal) return;
    const burstSteps = 6;
    const burstRadius = 20;

    for (let step = 0; step < burstSteps; step++) {
        if (stopSignal || !socket || (repeatIntervalId && !repeatActionToggle.checked)) break;
        const progress = step / burstSteps;
        const currentRadius = burstRadius * progress;
        const intensity = 1 - progress;
        const sparkCount = 4;
        for (let spark = 0; spark < sparkCount; spark++) {
            const angle = (spark / sparkCount) * 2 * Math.PI;
            const sparkDistance = currentRadius + Math.random() * 8;
            const endX = x + sparkDistance * Math.cos(angle);
            const endY = y + sparkDistance * Math.sin(angle);
            sendDrawCommand(x, y, endX, endY, color, Math.max(1, 2 * intensity));
        }
        await new Promise(resolve => setTimeout(resolve, 80));
    }
}

// Cierre optimizado del portal
async function closeMagicPortalOptimized(centerX, centerY) {
    if (stopSignal) return;
    const closingSteps = 8;
    const startRadius = 40;

    for (let step = 0; step < closingSteps; step++) {
        if (stopSignal || !socket || (repeatIntervalId && !repeatActionToggle.checked)) break;
        const progress = step / closingSteps;
        const currentRadius = startRadius * (1 - progress);
        const intensity = 1 - progress;
        const implosionLines = 6;
        for (let line = 0; line < implosionLines; line++) {
            const angle = (line / implosionLines) * 2 * Math.PI;
            const startX = centerX + currentRadius * Math.cos(angle);
            const startY = centerY + currentRadius * Math.sin(angle) * 0.7;
            sendDrawCommand(startX, startY, centerX, centerY, '#9400D3', Math.max(1, 3 * intensity));
        }
        await new Promise(resolve => setTimeout(resolve, 120));
    }
    if (stopSignal) return;
    await new Promise(resolve => setTimeout(resolve, 300));
    if (stopSignal) return;
    sendDrawCommand(centerX - 10, centerY, centerX + 10, centerY, '#FFFFFF', 4);
    sendDrawCommand(centerX, centerY - 10, centerX, centerY + 10, '#FFFFFF', 4);
}


    // Efecto: Lanzagranadas (Arco + Explosión Retardada)
    async function drawGrenadeLauncher(targetPlayerId) {
        if (stopSignal) { console.log('drawGrenadeLauncher detenida.'); return; }
        console.log(`drawGrenadeLauncher: Iniciando efecto en ${targetPlayerId}.`);

        const ownPlayerId = getOwnPlayerId();
        if (!ownPlayerId) { console.warn('drawGrenadeLauncher: No se pudo encontrar tu jugador propio.'); return; }

        await drawJsonCommands(ownPlayerId, LANZAGRANADAS_JSON_URL, 'grip_right', 'right', 1.0);
        if (stopSignal) return;

        const launcherAttachPoint = _getAttachmentPoint(ownPlayerId, 'grip_right');
        if (!launcherAttachPoint) { console.warn('drawGrenadeLauncher: No se pudo determinar el punto de agarre del lanzagranadas.'); return; }

        const launchPoint = {
            x: launcherAttachPoint.x + 40,
            y: launcherAttachPoint.y - 20
        };

        const targetCoords = getTargetCoords(targetPlayerId);
        if (!launchPoint || !targetCoords) { console.warn('drawGrenadeLauncher: No se pudo determinar el punto de lanzamiento.'); return; }

        const grenadeColor = '#6A5ACD';
        const arcHeight = 80;
        const totalFrames = 40;
        const fuseTimeMs = 2000;

        let grenadeX = launchPoint.x;
        let grenadeY = launchPoint.y;

        console.log('drawGrenadeLauncher: Lanzando granada...');
        for (let frame = 0; frame < totalFrames; frame++) {
            if (stopSignal || !socket || (repeatIntervalId && !repeatActionToggle.checked)) break;
            const progress = frame / totalFrames;
            const nextX = launchPoint.x + (targetCoords.x - launchPoint.x) * progress;
            const nextY = launchPoint.y + (targetCoords.y - launchPoint.y) * progress - arcHeight * Math.sin(Math.PI * progress);
            sendDrawCommand(grenadeX, grenadeY, nextX, nextY, grenadeColor, 3);
            grenadeX = nextX;
            grenadeY = nextY;
            await new Promise(resolve => setTimeout(resolve, 40));
        }
        if (stopSignal) return;
        await new Promise(resolve => setTimeout(resolve, fuseTimeMs));
        if (stopSignal) return;

        if (socket && !(repeatIntervalId && !repeatActionToggle.checked)) {
            await explosionBlast(grenadeX, grenadeY, 1.5);
        }
        console.log('drawGrenadeLauncher: Granada explotada.');
    }

async function blueMuzzleBall(x, y) {
    if (stopSignal) return;
    const steps = 8;
    const maxRadius = 20;
    const colors = ['#87CEEB', '#ADD8E6', '#00BFFF', '#1E90FF'];

    for (let step = 0; step < steps; step++) {
        if (stopSignal || !socket || (repeatIntervalId && !repeatActionToggle.checked)) break;
        const progress = step / steps;
        const currentRadius = maxRadius * Math.sin(Math.PI * progress);
        const intensity = 1 - progress;
        const coreRays = 8;
        for (let ray = 0; ray < coreRays; ray++) {
            const angle = (ray / coreRays) * 2 * Math.PI;
            const rayLength = currentRadius * intensity;
            const endX = x + rayLength * Math.cos(angle);
            const endY = y + rayLength * Math.sin(angle);
            const color = colors[Math.min(step, colors.length - 1)];
            const thickness = Math.max(1, 16 * intensity);
            sendDrawCommand(x, y, endX, endY, color, thickness);
        }
        const crossSize = currentRadius * 0.8;
        sendDrawCommand(x - crossSize, y, x + crossSize, y, '#FFFFFF', Math.max(1, 4 * intensity));
        sendDrawCommand(x, y - crossSize, x, y + crossSize, '#FFFFFF', Math.max(1, 4 * intensity));
        await new Promise(resolve => setTimeout(resolve, 40));
    }
}


    // Efecto: Rifle Láser Perforante
    async function drawLaserRifleBeam(targetPlayerId) {
        if (stopSignal) { console.log('drawLaserRifleBeam detenida.'); return; }
        console.log(`drawLaserRifleBeam: Iniciando efecto en ${targetPlayerId}.`);

        const ownPlayerId = getOwnPlayerId();
        if (!ownPlayerId) { console.warn('drawLaserRifleBeam: No se pudo encontrar tu jugador propio.'); return; }

        await drawJsonCommands(ownPlayerId, RIFLE_JSON_URL, 'grip_right', 'right', 1.0);
        if (stopSignal) return;

        const rifleAttachPoint = _getAttachmentPoint(ownPlayerId, 'grip_right');
        if (!rifleAttachPoint) { console.warn('drawLaserRifleBeam: No se pudo determinar el punto de agarre del rifle.'); return; }

        const barrelTip = {
            x: rifleAttachPoint.x + 60,
            y: rifleAttachPoint.y - 16
        };

        const targetCoords = getTargetCoords(targetPlayerId);
        if (!barrelTip || !targetCoords) { console.warn('drawLaserRifleBeam: No se pudo determinar orígen/objetivo.'); return; }

        console.log('drawLaserRifleBeam: Generando fogonazo azul...');
        await blueMuzzleBall(barrelTip.x, barrelTip.y);
        if (stopSignal) return;

        await new Promise(resolve => setTimeout(resolve, 100));
        if (stopSignal) return;

        const laserColorCore = '#FFFFFF';
        const laserColorFringe = '#00FFFF';
        const laserThickness = 6;
        const laserDurationFrames = 15;

        console.log('drawLaserRifleBeam: Disparando láser...');
        for (let frame = 0; frame < laserDurationFrames; frame++) {
            if (stopSignal || !socket || (repeatIntervalId && !repeatActionToggle.checked)) break;
            sendDrawCommand(barrelTip.x, barrelTip.y, targetCoords.x, targetCoords.y, laserColorCore, laserThickness);
            sendDrawCommand(barrelTip.x, barrelTip.y, targetCoords.x, targetCoords.y, laserColorFringe, laserThickness * 1.5);
            for (let i = 0; i < 3; i++) {
                const progress = Math.random();
                const sparkX = barrelTip.x + (targetCoords.x - barrelTip.x) * progress + (Math.random() - 0.5) * 5;
                const sparkY = barrelTip.y + (targetCoords.y - barrelTip.y) * progress + (Math.random() - 0.5) * 5;
                sendDrawCommand(sparkX, sparkY, sparkX + 1, sparkY + 1, '#FFD700', 1);
            }
            await new Promise(resolve => setTimeout(resolve, 50));
        }
        console.log('drawLaserRifleBeam: Láser disparado.');
    }

    // Efecto: Búmeran (Guiado)
    async function drawBoomerangGuided(targetPlayerId) {
        if (stopSignal) { console.log('drawBoomerangGuided detenida.'); return; }
        console.log(`drawBoomerangGuided: Iniciando efecto en ${targetPlayerId}.`);

        const ownPlayerId = getOwnPlayerId();
        if (!ownPlayerId) { console.warn('drawBoomerangGuided: No se pudo encontrar tu jugador propio.'); return; }

        await drawJsonCommands(ownPlayerId, BOOMERANG_JSON_URL, 'grip_right', 'none', 1.0);
        if (stopSignal) return;

        const boomerangAttachPoint = _getAttachmentPoint(ownPlayerId, 'grip_right');
        if (!boomerangAttachPoint) { console.warn('drawBoomerangGuided: No se pudo determinar el punto de agarre del bumerán.'); return; }

        const startPoint = {
            x: boomerangAttachPoint.x + 40,
            y: boomerangAttachPoint.y - 5
        };

        const targetCoords = getTargetCoords(targetPlayerId);
        if (!startPoint || !targetCoords) { console.warn('drawBoomerangGuided: No se pudo determinar orígen/objetivo.'); return; }

        const controlPointOffset = 100;
        const totalFrames = 60;
        const spinSpeed = 0.2;
        const boomerangColor = '#8B4513';
        const trailColor = '#D2B48C';
        let boomerangAngle = 0;

        console.log('drawBoomerangGuided: Lanzando bumerán...');
        for (let frame = 0; frame < totalFrames; frame++) {
            if (stopSignal || !socket || (repeatIntervalId && !repeatActionToggle.checked)) break;

            const progress = frame / totalFrames;
            const curveFactor = Math.sin(Math.PI * progress);

            let currentTargetX = (progress < 0.5) ? targetCoords.x : startPoint.x;
            let currentTargetY = (progress < 0.5) ? targetCoords.y : startPoint.y;

            const t = progress;
            const mt = 1 - t;
            const controlX = startPoint.x + (targetCoords.x - startPoint.x) / 2 + controlPointOffset * curveFactor * Math.cos(boomerangAngle * 2);
            const controlY = startPoint.y + (targetCoords.y - startPoint.y) / 2 + controlPointOffset * curveFactor * Math.sin(boomerangAngle * 2);
            const boomerangX = mt * mt * startPoint.x + 2 * mt * t * controlX + t * t * currentTargetX;
            const boomerangY = mt * mt * startPoint.y + 2 * mt * t * controlY + t * t * currentTargetY;

            boomerangAngle += spinSpeed;
            await _drawBoomerangShape(boomerangX, boomerangY, boomerangAngle, boomerangColor);
            if (stopSignal) return;

            if (frame > 0) {
                const prevProgress = (frame - 1) / totalFrames;
                const prevControlX = startPoint.x + (targetCoords.x - startPoint.x) / 2 + controlPointOffset * Math.sin(Math.PI * prevProgress) * Math.cos((boomerangAngle - spinSpeed) * 2);
                const prevControlY = startPoint.y + (targetCoords.y - startPoint.y) / 2 + controlPointOffset * Math.sin(Math.PI * prevProgress) * Math.sin((boomerangAngle - spinSpeed) * 2);
                const prevBoomerangX = (1 - prevProgress) * ((1 - prevProgress) * startPoint.x + prevProgress * prevControlX) + prevProgress * ((1 - prevProgress) * prevControlX + prevProgress * (prevProgress < 0.5 ? targetCoords.x : startPoint.x));
                const prevBoomerangY = (1 - prevProgress) * ((1 - prevProgress) * startPoint.y + prevProgress * prevControlY) + prevProgress * ((1 - prevProgress) * prevControlY + prevProgress * (prevProgress < 0.5 ? targetCoords.y : startPoint.y));
                sendDrawCommand(prevBoomerangX, prevBoomerangY, boomerangX, boomerangY, trailColor, 1);
            }

            if (progress < 0.5 && distance(boomerangX, boomerangY, targetCoords.x, targetCoords.y) < 20) {
                console.log('drawBoomerangGuided: Búmeran impacta objetivo!');
                await bulletImpact(targetCoords.x, targetCoords.y);
                if (stopSignal) return;
                await new Promise(resolve => setTimeout(resolve, 500));
                if (stopSignal) return;
            }
            if (progress >= 0.5 && distance(boomerangX, boomerangY, startPoint.x, startPoint.y) < 20) {
                console.log('drawBoomerangGuided: Búmeran regresa al origen!');
                return;
            }
            await new Promise(resolve => setTimeout(resolve, 60));
        }
        console.log('drawBoomerangGuided: Búmeran finalizado.');
    }

    // Función auxiliar para dibujar la forma del bumerán (simplificada)
    async function _drawBoomerangShape(x, y, angle, color) {
        if (stopSignal) return;
        const armLength = 20;
        const armAngle = Math.PI / 4;
        const cx = x;
        const cy = y;
        const p1x = cx + armLength * Math.cos(angle);
        const p1y = cy + armLength * Math.sin(angle);
        const p2x = cx + armLength * Math.cos(angle + armAngle);
        const p2y = cy + armLength * Math.sin(angle + armAngle);
        const p3x = cx + armLength * Math.cos(angle - armAngle);
        const p3y = cy + armLength * Math.sin(angle - armAngle);
        sendDrawCommand(cx, cy, p1x, p1y, color, 4);
        sendDrawCommand(cx, cy, p2x, p2y, color, 4);
        sendDrawCommand(cx, cy, p3x, p3y, color, 4);
    }

// Efecto: Espada - Absorción de Energía (ULTRA RALENTIZADO para servidor)
async function drawSwordSlashArc(targetPlayerId) {
    if (stopSignal) { console.log('drawSwordSlashArc detenida.'); return; }
    console.log(`drawSwordSlashArc: Iniciando absorción de energía ultra optimizada en ${targetPlayerId}.`);

    const ownPlayerId = getOwnPlayerId();
    if (!ownPlayerId) { console.warn('drawSwordSlashArc: No se pudo encontrar tu jugador propio.'); return; }

    await drawJsonCommands(ownPlayerId, ESPADA_JSON_URL, 'grip_right', 'right', 1.0);
    if (stopSignal) return;
    await new Promise(resolve => setTimeout(resolve, 500));
    if (stopSignal) return;

    const swordAttachPoint = _getAttachmentPoint(ownPlayerId, 'grip_right');
    if (!swordAttachPoint) { console.warn('drawSwordSlashArc: No se pudo determinar el punto de agarre de la espada.'); return; }

    const targetCoords = getTargetCoords(targetPlayerId);
    if (!targetCoords) { console.warn('drawSwordSlashArc: No se pudo determinar el objetivo.'); return; }

    const absorptionPoint = {
        x: Math.floor(swordAttachPoint.x + 60),
        y: Math.floor(swordAttachPoint.y - 15)
    };

    console.log('drawSwordSlashArc: Iniciando drenaje ultra ralentizado...');
    await createEnergyConnectionUltra(targetCoords, absorptionPoint);
    if (stopSignal) return;
    await new Promise(resolve => setTimeout(resolve, 800));
    if (stopSignal) return;
    await drainEnergyFlowUltra(targetCoords, absorptionPoint, targetPlayerId);
    if (stopSignal) return;
    await new Promise(resolve => setTimeout(resolve, 800));
    if (stopSignal) return;
    await finalizeEnergyAbsorptionUltra(absorptionPoint);

    console.log('drawSwordSlashArc: Absorción ultra optimizada completada.');
}

// Conexión inicial con batches ultra pequeños
async function createEnergyConnectionUltra(sourceCoords, absorptionPoint) {
    if (stopSignal) return;
    const connectionSteps = 12;
    const energyColors = ['#9400D3', '#FF1493', '#00FFFF', '#FFD700'];
    sourceCoords.x = Math.floor(sourceCoords.x);
    sourceCoords.y = Math.floor(sourceCoords.y);

    for (let step = 0; step < connectionSteps; step++) {
        if (stopSignal || !socket || (repeatIntervalId && !repeatActionToggle.checked)) break;
        const progress = step / connectionSteps;
        const tentacles = 4;
        for (let tentacle = 0; tentacle < tentacles; tentacle++) {
            if (stopSignal) return;
            const tentacleAngle = (tentacle / tentacles) * 2 * Math.PI;
            const tentacleRadius = 30 * progress;
            const tentacleStartX = Math.floor(sourceCoords.x + tentacleRadius * Math.cos(tentacleAngle));
            const tentacleStartY = Math.floor(sourceCoords.y + tentacleRadius * Math.sin(tentacleAngle));
            const midProgress = progress * 0.7;
            const tentacleEndX = Math.floor(tentacleStartX + (absorptionPoint.x - tentacleStartX) * midProgress);
            const tentacleEndY = Math.floor(tentacleStartY + (absorptionPoint.y - tentacleStartY) * midProgress);
            const color = energyColors[tentacle % energyColors.length];
            const thickness = Math.max(2, 5 - progress * 2);
            sendDrawCommand(tentacleStartX, tentacleStartY, tentacleEndX, tentacleEndY, color, thickness);
            await new Promise(resolve => setTimeout(resolve, 60));
            if (stopSignal) return;

            if (step % 3 === 0) {
                const sparkX = Math.floor(tentacleEndX + (Math.random() - 0.5) * 10);
                const sparkY = Math.floor(tentacleEndY + (Math.random() - 0.5) * 10);
                sendDrawCommand(tentacleEndX, tentacleEndY, sparkX, sparkY, '#FFFFFF', 1);
                await new Promise(resolve => setTimeout(resolve, 20));
                if (stopSignal) return;
            }
        }
        const pulseRadius = 25 + Math.sin(step * 0.8) * 10;
        const pulseSegments = 8;
        for (let seg = 0; seg < pulseSegments; seg++) {
            if (stopSignal) return;
            const angle = (seg / pulseSegments) * 2 * Math.PI;
            const pulseX = Math.floor(sourceCoords.x + pulseRadius * Math.cos(angle));
            const pulseY = Math.floor(sourceCoords.y + pulseRadius * Math.sin(angle));
            const pulseIntensity = 1 - progress;
            const pulseColor = `rgba(255, 0, 100, ${pulseIntensity * 0.6})`;
            sendDrawCommand(sourceCoords.x, sourceCoords.y, pulseX, pulseY, pulseColor, Math.max(1, 3 * pulseIntensity));
            await new Promise(resolve => setTimeout(resolve, 25));
            if (stopSignal) return;
        }
        await new Promise(resolve => setTimeout(resolve, 200));
    }
}

// Drenaje con batches microscópicos
async function drainEnergyFlowUltra(sourceCoords, absorptionPoint, targetPlayerId) {
    if (stopSignal) return;
    const drainDuration = 4000;
    const startTime = Date.now();
    let frame = 0;
    const flowColors = ['#9400D3', '#8A2BE2', '#FF1493', '#00FFFF', '#FFD700'];
    const streamCount = 3;

    while (Date.now() - startTime < drainDuration) {
        if (stopSignal || !socket || (repeatIntervalId && !repeatActionToggle.checked)) break;
        frame++;
        const currentTargetCoords = getTargetCoords(targetPlayerId) || sourceCoords;
        currentTargetCoords.x = Math.floor(currentTargetCoords.x);
        currentTargetCoords.y = Math.floor(currentTargetCoords.y);

        for (let stream = 0; stream < streamCount; stream++) {
            if (stopSignal) return;
            const streamOffset = (stream / streamCount) * 2 * Math.PI;
            const streamPhase = frame * 0.1 + streamOffset;
            const particleCount = 4;
            for (let particle = 0; particle < particleCount; particle++) {
                if (stopSignal) return;
                const particleProgress = (particle / particleCount) + (frame * 0.05) % 1;
                const baseX = currentTargetCoords.x + (absorptionPoint.x - currentTargetCoords.x) * particleProgress;
                const baseY = currentTargetCoords.y + (absorptionPoint.y - currentTargetCoords.y) * particleProgress;
                const waveIntensity = 15 * Math.sin(particleProgress * Math.PI);
                const waveX = Math.floor(baseX + waveIntensity * Math.cos(streamPhase + particleProgress * 4));
                const waveY = Math.floor(baseY + waveIntensity * Math.sin(streamPhase + particleProgress * 4) * 0.5);
                const color = flowColors[stream % flowColors.length];
                const intensity = 1 - particleProgress;
                const thickness = Math.max(1, 4 * intensity);
                sendDrawCommand(waveX - 2, waveY - 2, waveX + 2, waveY + 2, color, thickness);
                await new Promise(resolve => setTimeout(resolve, 30));
                if (stopSignal) return;

                if (particleProgress > 0.1) {
                    const trailX = Math.floor(waveX - 8 * Math.cos(streamPhase));
                    const trailY = Math.floor(waveY - 8 * Math.sin(streamPhase) * 0.5);
                    sendDrawCommand(waveX, waveY, trailX, trailY, color, Math.max(1, thickness * 0.6));
                    await new Promise(resolve => setTimeout(resolve, 15));
                    if (stopSignal) return;
                }
            }
            await new Promise(resolve => setTimeout(resolve, 100));
            if (stopSignal) return;
        }
        if (frame % 6 === 0) {
            const drainPulse = Math.sin(frame * 0.3) * 20 + 30;
            const drainSegments = 8;
            for (let seg = 0; seg < drainSegments; seg++) {
                if (stopSignal) return;
                const angle = (seg / drainSegments) * 2 * Math.PI + frame * 0.1;
                const drainX = Math.floor(currentTargetCoords.x + drainPulse * Math.cos(angle));
                const drainY = Math.floor(currentTargetCoords.y + drainPulse * Math.sin(angle));
                const drainColor = `rgba(255, ${100 - frame % 100}, 0, 0.7)`;
                sendDrawCommand(currentTargetCoords.x, currentTargetCoords.y, drainX, drainY, drainColor, 2);
                await new Promise(resolve => setTimeout(resolve, 40));
                if (stopSignal) return;
            }
        }
        if (frame % 8 === 0) {
            const accumulation = Math.sin(frame * 0.2) * 15 + 20;
            const accumulationSpokes = 6;
            for (let spoke = 0; spoke < accumulationSpokes; spoke++) {
                if (stopSignal) return;
                const spokeAngle = (spoke / accumulationSpokes) * 2 * Math.PI + frame * 0.15;
                const accX = Math.floor(absorptionPoint.x + accumulation * Math.cos(spokeAngle));
                const accY = Math.floor(absorptionPoint.y + accumulation * Math.sin(spokeAngle));
                sendDrawCommand(absorptionPoint.x, absorptionPoint.y, accX, accY, '#FFD700', 3);
                await new Promise(resolve => setTimeout(resolve, 35));
                if (stopSignal) return;
            }
        }
        await new Promise(resolve => setTimeout(resolve, 150));
    }
}

// Finalización ultra ralentizada
async function finalizeEnergyAbsorptionUltra(absorptionPoint) {
    if (stopSignal) return;
    const finalizationSteps = 15;
    const maxRadius = 40;
    const finalColors = ['#FFFFFF', '#FFD700', '#00FFFF'];

    for (let step = 0; step < finalizationSteps; step++) {
        if (stopSignal || !socket || (repeatIntervalId && !repeatActionToggle.checked)) break;
        const progress = step / finalizationSteps;
        const currentRadius = maxRadius * Math.sin(progress * Math.PI);
        const intensity = 1 - progress;
        const burstRays = 12;
        for (let colorIdx = 0; colorIdx < finalColors.length; colorIdx++) {
            if (stopSignal) return;
            for (let ray = 0; ray < burstRays; ray += 6) {
                if (stopSignal) return;
                for (let r = ray; r < Math.min(ray + 2, burstRays); r++) {
                    if (r % finalColors.length !== colorIdx) continue;
                    const rayAngle = (r / burstRays) * 2 * Math.PI + step * 0.2;
                    const rayLength = Math.floor(currentRadius + Math.random() * 15);
                    const rayX = Math.floor(absorptionPoint.x + rayLength * Math.cos(rayAngle));
                    const rayY = Math.floor(absorptionPoint.y + rayLength * Math.sin(rayAngle));
                    const thickness = Math.max(1, 5 * intensity);
                    sendDrawCommand(absorptionPoint.x, absorptionPoint.y, rayX, rayY, finalColors[colorIdx], thickness);
                    await new Promise(resolve => setTimeout(resolve, 45));
                    if (stopSignal) return;
                }
                await new Promise(resolve => setTimeout(resolve, 80));
                if (stopSignal) return;
            }
            await new Promise(resolve => setTimeout(resolve, 120));
            if (stopSignal) return;
        }
        const coreSize = Math.floor(12 * intensity);
        const coreSegments = 4;
        for (let seg = 0; seg < coreSegments; seg++) {
            if (stopSignal) return;
            const coreAngle = (seg / coreSegments) * 2 * Math.PI;
            const coreX = Math.floor(absorptionPoint.x + coreSize * Math.cos(coreAngle));
            const coreY = Math.floor(absorptionPoint.y + coreSize * Math.sin(coreAngle));
            sendDrawCommand(absorptionPoint.x, absorptionPoint.y, coreX, coreY, '#FFFFFF', Math.max(2, 6 * intensity));
            await new Promise(resolve => setTimeout(resolve, 50));
            if (stopSignal) return;
        }
        await new Promise(resolve => setTimeout(resolve, 250));
    }
    if (stopSignal) return;
    await new Promise(resolve => setTimeout(resolve, 600));
    if (stopSignal) return;
    sendDrawCommand(absorptionPoint.x - 20, absorptionPoint.y, absorptionPoint.x + 20, absorptionPoint.y, '#FFFFFF', 8);
    await new Promise(resolve => setTimeout(resolve, 300));
    if (stopSignal) return;
    sendDrawCommand(absorptionPoint.x, absorptionPoint.y - 20, absorptionPoint.x, absorptionPoint.y + 20, '#FFFFFF', 8);
}


// Efecto: Martillo - Red Trampa que Encierra al Objetivo
async function drawSeismicSmashWave(targetPlayerId) {
    if (stopSignal) { console.log('drawSeismicSmashWave detenida.'); return; }
    console.log(`drawSeismicSmashWave: Iniciando red trampa en ${targetPlayerId}.`);

    const ownPlayerId = getOwnPlayerId();
    if (!ownPlayerId) { console.warn('drawSeismicSmashWave: No se pudo encontrar tu jugador propio.'); return; }

    await drawJsonCommands(ownPlayerId, MARTILLO_JSON_URL, 'grip_right', 'down', 1.0);
    if (stopSignal) return;
    await new Promise(resolve => setTimeout(resolve, 300));
    if (stopSignal) return;

    const hammerPoint = _getAttachmentPoint(ownPlayerId, 'grip_right');
    const targetPoint = getTargetCoords(targetPlayerId);
    if (!hammerPoint || !targetPoint) { console.warn('drawSeismicSmashWave: No se pudieron determinar los puntos.'); return; }

    const hammerX = Math.floor(hammerPoint.x);
    const hammerY = Math.floor(hammerPoint.y);
    const targetX = Math.floor(targetPoint.x);
    const targetY = Math.floor(targetPoint.y);

    console.log('drawSeismicSmashWave: ¡Lanzando red trampa!');

    await launchNetProjectiles(hammerX, hammerY, targetX, targetY);
    if (stopSignal) return;
    await new Promise(resolve => setTimeout(resolve, 400));
    if (stopSignal) return;
    await expandTrapNet(targetX, targetY);
    if (stopSignal) return;
    await new Promise(resolve => setTimeout(resolve, 400));
    if (stopSignal) return;
    await closeTrapNet(targetX, targetY);

    console.log('drawSeismicSmashWave: Red trampa completada.');
}

// Lanzar proyectiles de red con batch rendering
async function launchNetProjectiles(startX, startY, targetX, targetY) {
    if (stopSignal) return;
    const projectileSteps = 15;
    const netColors = ['#8B4513', '#A0522D', '#CD853F'];

    for (let step = 0; step < projectileSteps; step++) {
        if (stopSignal || !socket || (repeatIntervalId && !repeatActionToggle.checked)) break;
        const progress = step / projectileSteps;
        for (let colorIdx = 0; colorIdx < netColors.length; colorIdx++) {
            if (stopSignal) return;
            const color = netColors[colorIdx];
            const projectilesThisColor = [];
            const projectileCount = 3;
            for (let proj = 0; proj < projectileCount; proj++) {
                if (proj % netColors.length !== colorIdx) continue;
                const angle = (proj / projectileCount) * 0.6 - 0.3;
                const currentX = Math.floor(startX + (targetX - startX) * progress);
                const currentY = Math.floor(startY + (targetY - startY) * progress - 20 * Math.sin(Math.PI * progress));
                const offsetX = Math.floor(Math.cos(angle) * 15);
                const offsetY = Math.floor(Math.sin(angle) * 15);
                projectilesThisColor.push({ x: currentX + offsetX, y: currentY + offsetY });
            }
            projectilesThisColor.forEach(proj => {
                const prevX = Math.floor(startX + (targetX - startX) * Math.max(0, progress - 0.1));
                const prevY = Math.floor(startY + (targetY - startY) * Math.max(0, progress - 0.1));
                sendDrawCommand(prevX, prevY, proj.x, proj.y, color, 3);
                sendDrawCommand(proj.x - 3, proj.y - 3, proj.x + 3, proj.y + 3, color, 2);
            });
            await new Promise(resolve => setTimeout(resolve, 15));
            if (stopSignal) return;
        }
        await new Promise(resolve => setTimeout(resolve, 60));
    }
}

// Expandir red trampa alrededor del objetivo
async function expandTrapNet(centerX, centerY) {
    if (stopSignal) return;
    const expansionSteps = 18;
    const maxRadius = 80;
    const netColor = '#8B4513';
    const accentColor = '#CD853F';

    for (let step = 0; step < expansionSteps; step++) {
        if (stopSignal || !socket || (repeatIntervalId && !repeatActionToggle.checked)) break;
        const progress = step / expansionSteps;
        const currentRadius = Math.floor(maxRadius * progress);
        const netCommands = [];
        const rings = Math.min(4, Math.floor(progress * 4) + 1);
        for (let ring = 0; ring < rings; ring++) {
            const ringRadius = Math.floor((currentRadius / rings) * (ring + 1));
            const segments = 12;
            for (let seg = 0; seg < segments; seg++) {
                const angle1 = (seg / segments) * 2 * Math.PI;
                const angle2 = ((seg + 1) / segments) * 2 * Math.PI;
                const x1 = Math.floor(centerX + ringRadius * Math.cos(angle1));
                const y1 = Math.floor(centerY + ringRadius * Math.sin(angle1));
                const x2 = Math.floor(centerX + ringRadius * Math.cos(angle2));
                const y2 = Math.floor(centerY + ringRadius * Math.sin(angle2));
                netCommands.push({ x1, y1, x2, y2, color: netColor, thickness: 2 });
            }
        }
        const radialLines = 8;
        for (let line = 0; line < radialLines; line++) {
            const angle = (line / radialLines) * 2 * Math.PI;
            const endX = Math.floor(centerX + currentRadius * Math.cos(angle));
            const endY = Math.floor(centerY + currentRadius * Math.sin(angle));
            netCommands.push({ x1: centerX, y1: centerY, x2: endX, y2: endY, color: netColor, thickness: 2 });
        }
        netCommands.forEach(cmd => sendDrawCommand(cmd.x1, cmd.y1, cmd.x2, cmd.y2, cmd.color, cmd.thickness));
        await new Promise(resolve => setTimeout(resolve, 25));
        if (stopSignal) return;

        if (step % 3 === 0) {
            const nodeCommands = [];
            for (let ring = 1; ring <= rings; ring++) {
                const ringRadius = Math.floor((currentRadius / rings) * ring);
                const nodes = 6;
                for (let node = 0; node < nodes; node++) {
                    const angle = (node / nodes) * 2 * Math.PI;
                    const nodeX = Math.floor(centerX + ringRadius * Math.cos(angle));
                    const nodeY = Math.floor(centerY + ringRadius * Math.sin(angle));
                    nodeCommands.push({ x1: nodeX - 2, y1: nodeY - 2, x2: nodeX + 2, y2: nodeY + 2, color: accentColor, thickness: 3 });
                }
            }
            nodeCommands.forEach(cmd => sendDrawCommand(cmd.x1, cmd.y1, cmd.x2, cmd.y2, cmd.color, cmd.thickness));
            await new Promise(resolve => setTimeout(resolve, 20));
            if (stopSignal) return;
        }
        await new Promise(resolve => setTimeout(resolve, 180));
    }
}

// Cerrar la trampa con efecto de captura
async function closeTrapNet(centerX, centerY) {
    if (stopSignal) return;
    const closingSteps = 12;
    const initialRadius = 80;
    const finalRadius = 25;
    const trapColor = '#654321';
    const sparkColor = '#DAA520';

    for (let step = 0; step < closingSteps; step++) {
        if (stopSignal || !socket || (repeatIntervalId && !repeatActionToggle.checked)) break;
        const progress = step / closingSteps;
        const currentRadius = Math.floor(initialRadius - (initialRadius - finalRadius) * progress);
        const intensity = 1 - progress;
        const contractionCommands = [];
        const contractionLines = 10;
        for (let line = 0; line < contractionLines; line++) {
            const angle = (line / contractionLines) * 2 * Math.PI;
            const outerX = Math.floor(centerX + currentRadius * Math.cos(angle));
            const outerY = Math.floor(centerY + currentRadius * Math.sin(angle));
            const innerX = Math.floor(centerX + (currentRadius * 0.3) * Math.cos(angle));
            const innerY = Math.floor(centerY + (currentRadius * 0.3) * Math.sin(angle));
            contractionCommands.push({ x1: outerX, y1: outerY, x2: innerX, y2: innerY, color: trapColor, thickness: Math.max(1, 4 * intensity) });
        }
        contractionCommands.forEach(cmd => sendDrawCommand(cmd.x1, cmd.y1, cmd.x2, cmd.y2, cmd.color, cmd.thickness));
        await new Promise(resolve => setTimeout(resolve, 30));
        if (stopSignal) return;

        if (step % 2 === 0) {
            const sparkCommands = [];
            const sparkCount = 6;
            for (let spark = 0; spark < sparkCount; spark++) {
                const sparkAngle = (spark / sparkCount) * 2 * Math.PI + Math.random() * 0.5;
                const sparkRadius = currentRadius + Math.random() * 10;
                const sparkX = Math.floor(centerX + sparkRadius * Math.cos(sparkAngle));
                const sparkY = Math.floor(centerY + sparkRadius * Math.sin(sparkAngle));
                const sparkEndX = Math.floor(sparkX + (Math.random() - 0.5) * 15);
                const sparkEndY = Math.floor(sparkY + (Math.random() - 0.5) * 15);
                sparkCommands.push({ x1: sparkX, y1: sparkY, x2: sparkEndX, y2: sparkEndY, color: sparkColor, thickness: 1 });
            }
            sparkCommands.forEach(cmd => sendDrawCommand(cmd.x1, cmd.y1, cmd.x2, cmd.y2, cmd.color, cmd.thickness));
            await new Promise(resolve => setTimeout(resolve, 25));
            if (stopSignal) return;
        }
        await new Promise(resolve => setTimeout(resolve, 100));
    }
    if (stopSignal) return;
    await new Promise(resolve => setTimeout(resolve, 200));
    if (stopSignal) return;
    const pulseRadius = finalRadius;
    for (let pulse = 0; pulse < 3; pulse++) {
        if (stopSignal) return;
        const pulseSegments = 8;
        for (let seg = 0; seg < pulseSegments; seg++) {
            const angle = (seg / pulseSegments) * 2 * Math.PI;
            const pulseX = Math.floor(centerX + pulseRadius * Math.cos(angle));
            const pulseY = Math.floor(centerY + pulseRadius * Math.sin(angle));
            sendDrawCommand(centerX, centerY, pulseX, pulseY, sparkColor, 3);
        }
        await new Promise(resolve => setTimeout(resolve, 150));
    }
}

// Efecto: Látigo - Solo Clones + Sol + Quemado (ULTRA OPTIMIZADO)
async function drawElectricWhipSnap(targetPlayerId) {
    if (stopSignal) { console.log('drawElectricWhipSnap detenida.'); return; }
    console.log(`drawElectricWhipSnap: Iniciando efecto ultra optimizado en ${targetPlayerId}.`);

    const ownPlayerId = getOwnPlayerId();
    if (!ownPlayerId) { console.warn('drawElectricWhipSnap: No se pudo encontrar tu jugador propio.'); return; }

    await drawJsonCommands(ownPlayerId, LATIGO_JSON_URL, 'grip_right', 'right', 1.0);
    if (stopSignal) return;
    await new Promise(resolve => setTimeout(resolve, 600));
    if (stopSignal) return;

    const targetCoords = getTargetCoords(targetPlayerId);
    if (!targetCoords) { console.warn('drawElectricWhipSnap: No se pudo determinar el objetivo.'); return; }

    const centerX = Math.floor(targetCoords.x);
    const centerY = Math.floor(targetCoords.y);

    console.log('drawElectricWhipSnap: Iniciando ritual ultra optimizado...');
    await createClonesUltraOptimized(centerX, centerY);
    if (stopSignal) return;
    await new Promise(resolve => setTimeout(resolve, 800));
    if (stopSignal) return;
    await emergingSunUltraOptimized(centerX, centerY);
    if (stopSignal) return;
    await new Promise(resolve => setTimeout(resolve, 800));
    if (stopSignal) return;
    await burnPlayerUltraOptimized(centerX, centerY);

    console.log('drawElectricWhipSnap: Ritual ultra optimizado completado.');
}

// Clones ultra optimizados con batch rendering completo
async function createClonesUltraOptimized(centerX, centerY) {
    if (stopSignal) return;
    const cloneSteps = 12;
    const maxRadius = 90;
    const cloneCount = 5;
    const cloneColors = ['#FFD700', '#FFA500'];

    for (let step = 0; step < cloneSteps; step++) {
        if (stopSignal || !socket || (repeatIntervalId && !repeatActionToggle.checked)) break;
        const progress = step / cloneSteps;
        const currentRadius = Math.floor(maxRadius * progress);
        for (let colorIdx = 0; colorIdx < cloneColors.length; colorIdx++) {
            if (stopSignal) return;
            const color = cloneColors[colorIdx];
            const allCloneCommands = [];
            for (let clone = 0; clone < cloneCount; clone++) {
                if (clone % cloneColors.length !== colorIdx) continue;
                const angle = (clone / cloneCount) * 2 * Math.PI + step * 0.1;
                const cloneX = Math.floor(centerX + currentRadius * Math.cos(angle));
                const cloneY = Math.floor(centerY + currentRadius * Math.sin(angle));
                const size = Math.floor(12 + Math.sin(step * 0.4) * 4);
                allCloneCommands.push(
                    { x1: cloneX - size, y1: cloneY, x2: cloneX + size, y2: cloneY },
                    { x1: cloneX, y1: cloneY - size, x2: cloneX, y2: cloneY + size },
                    { x1: cloneX - size, y1: cloneY - size, x2: cloneX + size, y2: cloneY + size }
                );
            }
            allCloneCommands.forEach(cmd => sendDrawCommand(cmd.x1, cmd.y1, cmd.x2, cmd.y2, color, colorIdx === 0 ? 3 : 1));
            await new Promise(resolve => setTimeout(resolve, 80));
            if (stopSignal) return;
        }
        await new Promise(resolve => setTimeout(resolve, 180));
    }
}

// Sol emergente ultra optimizado
async function emergingSunUltraOptimized(centerX, centerY) {
    if (stopSignal) return;
    const sunSteps = 15;
    const maxSunRadius = 70;
    const sunColors = ['#FFFF00', '#FFA500'];

    for (let step = 0; step < sunSteps; step++) {
        if (stopSignal || !socket || (repeatIntervalId && !repeatActionToggle.checked)) break;
        const progress = step / sunSteps;
        const currentRadius = Math.floor(maxSunRadius * Math.sin((progress * Math.PI) / 2));
        for (let colorIdx = 0; colorIdx < sunColors.length; colorIdx++) {
            if (stopSignal) return;
            const color = sunColors[colorIdx];
            const layerRadius = Math.floor(currentRadius * (1 - colorIdx * 0.3));
            const rayCount = 10 - colorIdx * 2;
            const allSunCommands = [];
            for (let ray = 0; ray < rayCount; ray++) {
                const angle = (ray / rayCount) * 2 * Math.PI + step * 0.1;
                const rayLength = Math.floor(layerRadius + Math.sin(step * 0.3 + ray) * 8);
                const rayEndX = Math.floor(centerX + rayLength * Math.cos(angle));
                const rayEndY = Math.floor(centerY + rayLength * Math.sin(angle));
                allSunCommands.push({ x1: centerX, y1: centerY, x2: rayEndX, y2: rayEndY, thickness: Math.max(1, (3 - colorIdx) * (1 - progress * 0.2)) });
            }
            const coronaSegments = 8;
            for (let seg = 0; seg < coronaSegments; seg++) {
                const segAngle = (seg / coronaSegments) * 2 * Math.PI;
                const coronaX = Math.floor(centerX + layerRadius * 0.7 * Math.cos(segAngle));
                const coronaY = Math.floor(centerY + layerRadius * 0.7 * Math.sin(segAngle));
                allSunCommands.push({ x1: centerX, y1: centerY, x2: coronaX, y2: coronaY, thickness: Math.max(1, 2 - colorIdx) });
            }
            allSunCommands.forEach(cmd => sendDrawCommand(cmd.x1, cmd.y1, cmd.x2, cmd.y2, color, cmd.thickness));
            await new Promise(resolve => setTimeout(resolve, 100));
            if (stopSignal) return;
        }
        await new Promise(resolve => setTimeout(resolve, 200));
    }
}

// Quemado del jugador ultra optimizado
async function burnPlayerUltraOptimized(centerX, centerY) {
    if (stopSignal) return;
    const burnSteps = 12;
    const fireColors = ['#FF4500', '#FFD700'];

    for (let step = 0; step < burnSteps; step++) {
        if (stopSignal || !socket || (repeatIntervalId && !repeatActionToggle.checked)) break;
        const progress = step / burnSteps;
        const intensity = 1 - progress;
        const burnRadius = Math.floor(50 * progress);
        for (let colorIdx = 0; colorIdx < fireColors.length; colorIdx++) {
            if (stopSignal) return;
            const color = fireColors[colorIdx];
            const allFireCommands = [];
            const flameCount = 8;
            for (let flame = 0; flame < flameCount; flame++) {
                if (flame % fireColors.length !== colorIdx) continue;
                const flameAngle = (flame / flameCount) * 2 * Math.PI + step * 0.15;
                const flameDistance = Math.floor(burnRadius + Math.random() * 15);
                const flameX = Math.floor(centerX + flameDistance * Math.cos(flameAngle));
                const flameY = Math.floor(centerY + flameDistance * Math.sin(flameAngle) - Math.random() * 10);
                allFireCommands.push({ x1: centerX, y1: centerY, x2: flameX, y2: flameY, thickness: Math.max(1, Math.floor(4 * intensity)) });
                if (Math.random() < 0.3) {
                    const sparkX = Math.floor(flameX + (Math.random() - 0.5) * 8);
                    const sparkY = Math.floor(flameY + (Math.random() - 0.5) * 8);
                    allFireCommands.push({ x1: flameX, y1: flameY, x2: sparkX, y2: sparkY, thickness: 1 });
                }
            }
            allFireCommands.forEach(cmd => {
                const fireColor = cmd.thickness === 1 ? '#FFFF00' : color;
                sendDrawCommand(cmd.x1, cmd.y1, cmd.x2, cmd.y2, fireColor, cmd.thickness);
            });
            await new Promise(resolve => setTimeout(resolve, 90));
            if (stopSignal) return;
        }
        await new Promise(resolve => setTimeout(resolve, 220));
    }
}


    // Efecto: Granada Pegajosa
    async function drawStickyGrenadeProj(playerId) {
        if (stopSignal) { console.log('drawStickyGrenadeProj detenida.'); return; }
        console.log(`drawStickyGrenadeProj: Iniciando efecto en ${playerId}.`);
        const avatar = document.querySelector(`.spawnedavatar[data-playerid="${playerId}"]`);
        if (!avatar) { console.warn('drawStickyGrenadeProj: Avatar no encontrado.'); return; }

        await drawJsonCommands(playerId, GRANADA_JSON_URL, 'grip_right', 'none', 0.8);
        if (stopSignal) return;

        const grenadeAttachPoint = _getAttachmentPoint(playerId, 'grip_right');
        if (!grenadeAttachPoint) { console.warn('drawStickyGrenadeProj: No se pudo determinar el punto de agarre de la granada.'); return; }

        const throwOrigin = {
            x: grenadeAttachPoint.x + 20,
            y: grenadeAttachPoint.y + 0
        };

        const targetCoords = getTargetCoords(playerId);
        if (!throwOrigin || !targetCoords) { console.warn('drawStickyGrenadeProj: No se pudo determinar origen/objetivo.'); return; }

        const fuseTimeMs = 2500;
        const flightTimeMs = 800;
        const flightSteps = 20;
        let grenadeCurrentX = throwOrigin.x;
        let grenadeCurrentY = throwOrigin.y;

        console.log('drawStickyGrenadeProj: Lanzando granada pegajosa...');
        for (let step = 0; step < flightSteps; step++) {
            if (stopSignal || !socket || (repeatIntervalId && !repeatActionToggle.checked)) break;
            const progress = step / flightSteps;
            const nextX = throwOrigin.x + (targetCoords.x - throwOrigin.x) * progress;
            const nextY = throwOrigin.y + (targetCoords.y - throwOrigin.y) * progress - 50 * Math.sin(Math.PI * progress);
            sendDrawCommand(grenadeCurrentX, grenadeCurrentY, nextX, nextY, '#808080', 3);
            grenadeCurrentX = nextX;
            grenadeCurrentY = nextY;
            await new Promise(resolve => setTimeout(resolve, flightTimeMs / flightSteps));
        }
        if (stopSignal) return;

        const finalGrenadeX = targetCoords.x;
        const finalGrenadeY = targetCoords.y - 15;

        console.log('drawStickyGrenadeProj: Granada se ha pegado al avatar. Iniciando temporizador...');
        const blinkInterval = setInterval(() => {
            if (stopSignal || !socket || (repeatIntervalId && !repeatActionToggle.checked)) { clearInterval(blinkInterval); return; }
            const blinkColor = Math.random() > 0.5 ? '#FF0000' : '#FFFF00';
            sendDrawCommand(finalGrenadeX - 5, finalGrenadeY, finalGrenadeX + 5, finalGrenadeY, blinkColor, 2);
            sendDrawCommand(finalGrenadeX, finalGrenadeY - 5, finalGrenadeX, finalGrenadeY + 5, blinkColor, 2);
        }, 100);

        await new Promise(resolve => setTimeout(resolve, fuseTimeMs));
        clearInterval(blinkInterval);
        if (stopSignal) return;

        if (socket && !(repeatIntervalId && !repeatActionToggle.checked)) {
            await explosionBlast(finalGrenadeX, finalGrenadeY, 1.0);
        }
        console.log('drawStickyGrenadeProj: Granada explotada.');
    }

// Efecto: Campo de Fuerza Protector (ULTRA OPTIMIZADO)
async function drawProximityMineTrap(playerId) {
    if (stopSignal) { console.log('drawProximityMineTrap detenida.'); return; }
    console.log(`drawProximityMineTrap: Iniciando campo de fuerza ULTRA optimizado en ${playerId}.`);
    const avatar = document.querySelector(`.spawnedavatar[data-playerid="${playerId}"]`);
    if (!avatar) { console.warn('drawProximityMineTrap: Avatar no encontrado.'); return; }

    const mineGroundPosition = _getAttachmentPoint(playerId, 'bottom');
    if (!mineGroundPosition) { console.warn('drawProximityMineTrap: No se pudo determinar la posición para el generador.'); return; }

    await drawJsonCommands(playerId, MINA_JSON_URL, 'bottom', 'none', 1.0);
    if (stopSignal) return;
    await new Promise(resolve => setTimeout(resolve, 800));
    if (stopSignal) return;

    const centerX = Math.floor(mineGroundPosition.x);
    const centerY = Math.floor(mineGroundPosition.y);

    console.log('drawProximityMineTrap: ¡Activando campo ULTRA optimizado!');

    await initializeForceFieldUltra(centerX, centerY);
    if (stopSignal) return;
    await new Promise(resolve => setTimeout(resolve, 1000));
    if (stopSignal) return;
    await activeForceFieldUltra(centerX, centerY);
    if (stopSignal) return;
    await new Promise(resolve => setTimeout(resolve, 1000));
    if (stopSignal) return;
    // await deactivateForceFieldUltra(centerX, centerY); // This function was missing, commented out

    console.log('drawProximityMineTrap: Campo ULTRA optimizado finalizado.');
}

// Inicialización ULTRA optimizada con batch rendering completo
async function initializeForceFieldUltra(centerX, centerY) {
    if (stopSignal) return;
    const initSteps = 10;
    const maxRadius = 80;
    const fieldColors = ['#00BFFF', '#4169E1'];
    const preCalculatedAngles = [];
    const segments = 12;
    for (let seg = 0; seg < segments; seg++) {
        preCalculatedAngles.push((seg / segments) * 2 * Math.PI);
    }

    for (let step = 0; step < initSteps; step++) {
        if (stopSignal || !socket || (repeatIntervalId && !repeatActionToggle.checked)) break;
        const progress = step / initSteps;
        const currentRadius = Math.floor(maxRadius * Math.sin((progress * Math.PI) / 2));
        for (let colorIdx = 0; colorIdx < fieldColors.length; colorIdx++) {
            if (stopSignal) return;
            const color = fieldColors[colorIdx];
            const allCommands = [];
            const ringRadius = Math.floor(currentRadius * (1 - colorIdx * 0.3));
            preCalculatedAngles.forEach((baseAngle, segIdx) => {
                const angle1 = baseAngle + step * 0.1;
                const angle2 = preCalculatedAngles[(segIdx + 1) % preCalculatedAngles.length] + step * 0.1;
                const x1 = Math.floor(centerX + ringRadius * Math.cos(angle1));
                const y1 = Math.floor(centerY + ringRadius * Math.sin(angle1));
                const x2 = Math.floor(centerX + ringRadius * Math.cos(angle2));
                const y2 = Math.floor(centerY + ringRadius * Math.sin(angle2));
                allCommands.push({ x1, y1, x2, y2 });
            });
            const energyRays = 4;
            for (let ray = 0; ray < energyRays; ray++) {
                const rayAngle = (ray / energyRays) * 2 * Math.PI + step * 0.15;
                const rayStartX = Math.floor(centerX + ringRadius * Math.cos(rayAngle));
                const rayStartY = Math.floor(centerY + ringRadius * Math.sin(rayAngle));
                const rayEndX = Math.floor(centerX + (ringRadius * 0.4) * Math.cos(rayAngle));
                const rayEndY = Math.floor(centerY + (ringRadius * 0.4) * Math.sin(rayAngle));
                allCommands.push({ x1: rayStartX, y1: rayStartY, x2: rayEndX, y2: rayEndY });
            }
            const thickness = Math.max(1, (3 - colorIdx) * (0.5 + progress * 0.5));
            allCommands.forEach(cmd => sendDrawCommand(cmd.x1, cmd.y1, cmd.x2, cmd.y2, color, thickness));
            await new Promise(resolve => setTimeout(resolve, 120));
            if (stopSignal) return;
        }
        await new Promise(resolve => setTimeout(resolve, 250));
    }
}
async function activeForceFieldUltra(centerX, centerY) {
    // Placeholder for the missing function to prevent errors
    // A real implementation would go here
    if (stopSignal) return;
    console.log("activeForceFieldUltra (placeholder) executed.");
    await new Promise(resolve => setTimeout(resolve, 1000));
}

    // Efecto: Tormenta de Hielo
    async function drawIceStormArea(playerId) {
        console.log(`drawIceStormArea: Iniciando efecto en ${playerId}.`);
        const avatarCenter = getTargetCoords(playerId);
        if (!avatarCenter) { console.warn('drawIceStormArea: Avatar no encontrado.'); return; }

        const stormDurationMs = 5000;
        const startTime = Date.now();
        let frame = 0;
        const stormColors = ['#ADD8E6', '#E0FFFF', '#FFFFFF', '#B0E0E6']; // Tonos de azul claro y blanco

        console.log('drawIceStormArea: Desatando tormenta de hielo...');
        while (Date.now() - startTime < stormDurationMs) {
            if (!socket || (repeatIntervalId && !repeatActionToggle.checked)) { break; }

            frame++;
            const currentAvatarCenter = getTargetCoords(playerId);
            if (!currentAvatarCenter) { console.log('drawIceStormArea: Objetivo desaparecido.'); return; }
            const centerX = currentAvatarCenter.x;
            const centerY = currentAvatarCenter.y;

            // Partículas que caen (copos de nieve)
            for (let i = 0; i < 5; i++) {
                const x = centerX + (Math.random() - 0.5) * 150;
                const y = centerY - 80 + Math.random() * 160; // Área vertical
                const size = Math.random() * 3 + 1;
                sendDrawCommand(x, y, x + size, y + size, '#FFFFFF', 1); // Pequeños puntos
            }

            // Estalactitas/Fragmentos de hielo al azar
            if (Math.random() < 0.2) { // Menos frecuentes
                const x = centerX + (Math.random() - 0.5) * 100;
                const y1 = centerY - 50 + Math.random() * 20;
                const y2 = y1 + 10 + Math.random() * 20;
                const color = stormColors[Math.floor(Math.random() * stormColors.length)];
                sendDrawCommand(x, y1, x, y2, color, Math.max(1, Math.random() * 3));
            }

            // Anillo gélido que pulsa
            const pulseRadius = 60 + 10 * Math.sin(frame * 0.1);
            const pulseThickness = 2 + 1 * Math.sin(frame * 0.1);
            const segments = 12;
            for(let i=0; i<segments; i++) {
                const angle1 = (i / segments) * 2 * Math.PI;
                const angle2 = ((i + 1) / segments) * 2 * Math.PI;
                const x1 = centerX + pulseRadius * Math.cos(angle1);
                const y1 = centerY + pulseRadius * Math.sin(angle1);
                const x2 = centerX + pulseRadius * Math.cos(angle2);
                const y2 = centerY + pulseRadius * Math.sin(angle2);
                sendDrawCommand(x1, y1, x2, y2, '#B0E0E6', pulseThickness);
            }

            await new Promise(resolve => setTimeout(resolve, 100)); // Frame rate para la tormenta
        }
        console.log('drawIceStormArea: Tormenta de hielo finalizada.');
    }

// Inicialización de cristales con batch rendering completo
async function initializeCrystals(centerX, centerY) {
    if (stopSignal) return;
    const initSteps = 8;
    const crystalColors = ['#E0FFFF', '#B0E0E6'];
    const crystalPositions = [];
    const crystalCount = 6;
    for (let i = 0; i < crystalCount; i++) {
        const angle = (i / crystalCount) * 2 * Math.PI;
        const distance = 40 + Math.random() * 30;
        crystalPositions.push({ x: Math.floor(centerX + distance * Math.cos(angle)), y: Math.floor(centerY + distance * Math.sin(angle)), baseAngle: angle });
    }

    for (let step = 0; step < initSteps; step++) {
        if (stopSignal || !socket || (repeatIntervalId && !repeatActionToggle.checked)) break;
        const progress = step / initSteps;
        for (let colorIdx = 0; colorIdx < crystalColors.length; colorIdx++) {
            if (stopSignal) return;
            const color = crystalColors[colorIdx];
            const allCrystalCommands = [];
            crystalPositions.forEach((crystal, crystalIdx) => {
                if (crystalIdx % crystalColors.length !== colorIdx) return;
                const size = Math.floor(15 * progress);
                const rotation = step * 0.1 + crystal.baseAngle;
                const hexPoints = [];
                for (let point = 0; point < 6; point++) {
                    const pointAngle = (point / 6) * 2 * Math.PI + rotation;
                    const pointDistance = size * (0.8 + Math.sin(step * 0.2 + point) * 0.2);
                    hexPoints.push({ x: Math.floor(crystal.x + pointDistance * Math.cos(pointAngle)), y: Math.floor(crystal.y + pointDistance * Math.sin(pointAngle)) });
                }
                for (let i = 0; i < hexPoints.length; i++) {
                    const nextIndex = (i + 1) % hexPoints.length;
                    allCrystalCommands.push({ x1: hexPoints[i].x, y1: hexPoints[i].y, x2: hexPoints[nextIndex].x, y2: hexPoints[nextIndex].y });
                }
                allCrystalCommands.push({ x1: crystal.x - 2, y1: crystal.y - 2, x2: crystal.x + 2, y2: crystal.y + 2 });
            });
            const thickness = Math.max(1, (3 - colorIdx) * (0.5 + progress * 0.5));
            allCrystalCommands.forEach(cmd => sendDrawCommand(cmd.x1, cmd.y1, cmd.x2, cmd.y2, color, thickness));
            await new Promise(resolve => setTimeout(resolve, 180));
            if (stopSignal) return;
        }
        await new Promise(resolve => setTimeout(resolve, 300));
    }
}

// Crecimiento detallado de cristales con optimización extrema
async function growDetailedCrystals(centerX, centerY) {
    if (stopSignal) return;
    const growthSteps = 10;
    const detailColors = ['#FFFFFF', '#E0FFFF', '#B0E0E6'];
    const detailedCrystals = [];
    const mainCrystals = 5;
    for (let main = 0; main < mainCrystals; main++) {
        const mainAngle = (main / mainCrystals) * 2 * Math.PI;
        const mainDistance = 50;
        const mainX = Math.floor(centerX + mainDistance * Math.cos(mainAngle));
        const mainY = Math.floor(centerY + mainDistance * Math.sin(mainAngle));
        detailedCrystals.push({ centerX: mainX, centerY: mainY, baseAngle: mainAngle, branches: [] });
        const branches = 4;
        for (let branch = 0; branch < branches; branch++) {
            const branchAngle = mainAngle + (branch / branches) * Math.PI;
            detailedCrystals[main].branches.push({ angle: branchAngle, length: 20 + Math.random() * 15 });
        }
    }

    for (let step = 0; step < growthSteps; step++) {
        if (stopSignal || !socket || (repeatIntervalId && !repeatActionToggle.checked)) break;
        const progress = step / growthSteps;
        for (let colorIdx = 0; colorIdx < detailColors.length; colorIdx++) {
            if (stopSignal) return;
            const color = detailColors[colorIdx];
            const allDetailCommands = [];
            detailedCrystals.forEach((crystal, crystalIdx) => {
                if (crystalIdx % detailColors.length !== colorIdx) return;
                const crystalSize = Math.floor(25 * progress);
                const rotation = step * 0.08;
                const starPoints = 8;
                for (let point = 0; point < starPoints; point++) {
                    const isOuter = point % 2 === 0;
                    const pointAngle = (point / starPoints) * 2 * Math.PI + rotation;
                    const pointDistance = crystalSize * (isOuter ? 1.0 : 0.6);
                    const pointX = Math.floor(crystal.centerX + pointDistance * Math.cos(pointAngle));
                    const pointY = Math.floor(crystal.centerY + pointDistance * Math.sin(pointAngle));
                    allDetailCommands.push({ x1: crystal.centerX, y1: crystal.centerY, x2: pointX, y2: pointY });
                    if (point < starPoints - 1) {
                        const nextPoint = point + 1;
                        const nextIsOuter = nextPoint % 2 === 0;
                        const nextAngle = (nextPoint / starPoints) * 2 * Math.PI + rotation;
                        const nextDistance = crystalSize * (nextIsOuter ? 1.0 : 0.6);
                        const nextX = Math.floor(crystal.centerX + nextDistance * Math.cos(nextAngle));
                        const nextY = Math.floor(crystal.centerY + nextDistance * Math.sin(nextAngle));
                        allDetailCommands.push({ x1: pointX, y1: pointY, x2: nextX, y2: nextY });
                    }
                }
                crystal.branches.forEach(branch => {
                    const branchLength = Math.floor(branch.length * progress);
                    const branchEndX = Math.floor(crystal.centerX + branchLength * Math.cos(branch.angle));
                    const branchEndY = Math.floor(crystal.centerY + branchLength * Math.sin(branch.angle));
                    allDetailCommands.push({ x1: crystal.centerX, y1: crystal.centerY, x2: branchEndX, y2: branchEndY });
                    const subBranches = 2;
                    for (let sub = 0; sub < subBranches; sub++) {
                        const subAngle = branch.angle + (sub - 0.5) * 0.5;
                        const subLength = Math.floor(branchLength * 0.6);
                        const subX = Math.floor(branchEndX + subLength * Math.cos(subAngle));
                        const subY = Math.floor(branchEndY + subLength * Math.sin(subAngle));
                        allDetailCommands.push({ x1: branchEndX, y1: branchEndY, x2: subX, y2: subY });
                    }
                });
            });
            const microBatchSize = 8;
            for (let batch = 0; batch < allDetailCommands.length; batch += microBatchSize) {
                if (stopSignal) return;
                const microBatch = allDetailCommands.slice(batch, batch + microBatchSize);
                const thickness = Math.max(1, (4 - colorIdx) * (0.3 + progress * 0.7));
                microBatch.forEach(cmd => sendDrawCommand(cmd.x1, cmd.y1, cmd.x2, cmd.y2, color, thickness));
                await new Promise(resolve => setTimeout(resolve, 60));
                if (stopSignal) return;
            }
            await new Promise(resolve => setTimeout(resolve, 200));
            if (stopSignal) return;
        }
        await new Promise(resolve => setTimeout(resolve, 400));
    }
}

// Cristalización final ultra optimizada
async function finalCrystallization(centerX, centerY) {
    if (stopSignal) return;
    const finalSteps = 6;
    const finalColor = '#FFFFFF';
    const finalPattern = [];
    const layers = 3;
    for (let layer = 0; layer < layers; layer++) {
        const layerRadius = 60 + layer * 20;
        const layerElements = 8 - layer * 2;
        for (let element = 0; element < layerElements; element++) {
            const angle = (element / layerElements) * 2 * Math.PI;
            const elementX = Math.floor(centerX + layerRadius * Math.cos(angle));
            const elementY = Math.floor(centerY + layerRadius * Math.sin(angle));
            finalPattern.push({ centerX: elementX, centerY: elementY, layer: layer, size: 15 - layer * 3 });
        }
    }

    for (let step = 0; step < finalSteps; step++) {
        if (stopSignal || !socket || (repeatIntervalId && !repeatActionToggle.checked)) break;
        const progress = step / finalSteps;
        const intensity = 1 - progress;
        const allFinalCommands = [];
        finalPattern.forEach(element => {
            const currentSize = Math.floor(element.size * (0.5 + progress * 0.5));
            const diamond = [ { x: element.centerX, y: element.centerY - currentSize }, { x: element.centerX + currentSize, y: element.centerY }, { x: element.centerX, y: element.centerY + currentSize }, { x: element.centerX - currentSize, y: element.centerY }];
            for (let i = 0; i < diamond.length; i++) {
                const nextIndex = (i + 1) % diamond.length;
                allFinalCommands.push({ x1: diamond[i].x, y1: diamond[i].y, x2: diamond[nextIndex].x, y2: diamond[nextIndex].y, thickness: Math.max(1, Math.floor((3 - element.layer) * intensity)) });
            }
            allFinalCommands.push({ x1: element.centerX - currentSize / 2, y1: element.centerY, x2: element.centerX + currentSize / 2, y2: element.centerY, thickness: Math.max(1, Math.floor(2 * intensity)) });
            allFinalCommands.push({ x1: element.centerX, y1: element.centerY - currentSize / 2, x2: element.centerX, y2: element.centerY + currentSize / 2, thickness: Math.max(1, Math.floor(2 * intensity)) });
        });
        const ultraMicroBatch = 6;
        for (let batch = 0; batch < allFinalCommands.length; batch += ultraMicroBatch) {
            if (stopSignal) return;
            const microBatch = allFinalCommands.slice(batch, batch + ultraMicroBatch);
            microBatch.forEach(cmd => sendDrawCommand(cmd.x1, cmd.y1, cmd.x2, cmd.y2, finalColor, cmd.thickness));
            await new Promise(resolve => setTimeout(resolve, 80));
            if (stopSignal) return;
        }
        await new Promise(resolve => setTimeout(resolve, 500));
    }
    if (stopSignal) return;
    await new Promise(resolve => setTimeout(resolve, 800));
    if (stopSignal) return;
    sendDrawCommand(centerX - 20, centerY, centerX + 20, centerY, '#FFFFFF', 8);
    await new Promise(resolve => setTimeout(resolve, 300));
    if (stopSignal) return;
    sendDrawCommand(centerX, centerY - 20, centerX, centerY + 20, '#FFFFFF', 8);
}

// Efecto: Tornado de Viento (Solo delays - efecto circular mantenido exacto)
async function drawWindTornadoSpin(playerId) {
    if (stopSignal) { console.log('drawWindTornadoSpin detenida.'); return; }
    console.log(`drawWindTornadoSpin: Iniciando efecto en ${playerId}.`);
    const avatarCenter = getTargetCoords(playerId);
    if (!avatarCenter) { console.warn('drawWindTornadoSpin: Avatar no encontrado.'); return; }

    const tornadoDurationMs = 5000;
    const startTime = Date.now();
    let frame = 0;
    const tornadoHeight = 150;
    const tornadoRadius = 50;
    const rotationSpeed = 0.15;
    const spiralCount = 3;
    const windColors = ['#D3D3D3', '#A9A9A9', '#778899'];

    console.log('drawWindTornadoSpin: Generando tornado de viento...');
    while (Date.now() - startTime < tornadoDurationMs) {
        if (stopSignal || !socket || (repeatIntervalId && !repeatActionToggle.checked)) break;
        frame++;
        const currentAvatarCenter = getTargetCoords(playerId);
        if (!currentAvatarCenter) { console.log('drawWindTornadoSpin: Objetivo desaparecido.'); return; }

        const centerX = Math.floor(currentAvatarCenter.x);
        const centerY = Math.floor(currentAvatarCenter.y);

        for (let i = 0; i < spiralCount; i++) {
            if (stopSignal) return;
            const spiralOffset = (2 * Math.PI / spiralCount) * i;
            for (let seg = 0; seg < 20; seg++) {
                const progress = seg / 20;
                const currentAngle = frame * rotationSpeed + spiralOffset + progress * Math.PI * 4;
                const currentHeight = tornadoHeight * progress;
                const currentRadius = tornadoRadius * (1 - progress * 0.5);
                const x1 = Math.floor(centerX + currentRadius * Math.cos(currentAngle));
                const y1 = Math.floor(centerY - tornadoHeight / 2 + currentHeight + currentRadius * Math.sin(currentAngle));
                const nextAngle = frame * rotationSpeed + spiralOffset + (seg + 1) / 20 * Math.PI * 4;
                const nextHeight = tornadoHeight * ((seg + 1) / 20);
                const nextRadius = tornadoRadius * (1 - ((seg + 1) / 20) * 0.5);
                const x2 = Math.floor(centerX + nextRadius * Math.cos(nextAngle));
                const y2 = Math.floor(centerY - tornadoHeight / 2 + nextHeight + nextRadius * Math.sin(nextAngle));
                const color = windColors[i % windColors.length];
                sendDrawCommand(x1, y1, x2, y2, color, Math.max(1, 3 * (1 - progress)));
                if (seg > 0 && seg % 10 === 0) {
                    await new Promise(resolve => setTimeout(resolve, 2));
                    if (stopSignal) return;
                }
            }
            if (i < spiralCount - 1) {
                await new Promise(resolve => setTimeout(resolve, 8));
                if (stopSignal) return;
            }
        }
        await new Promise(resolve => setTimeout(resolve, 140));
    }
    console.log('drawWindTornadoSpin: Tornado de viento finalizado.');
}


    // Efecto: Muro de Tierra
    async function drawEarthWallShield(playerId) {
        if (stopSignal) { console.log('drawEarthWallShield detenida.'); return; }
        console.log(`drawEarthWallShield: Iniciando efecto en ${playerId}.`);
        const avatar = document.querySelector(`.spawnedavatar[data-playerid="${playerId}"]`);
        if (!avatar) { console.warn('drawEarthWallShield: Avatar no encontrado.'); return; }

        const avatarCenter = getTargetCoords(playerId);
        if (!avatarCenter) { console.warn('drawEarthWallShield: Avatar no encontrado para el muro.'); return; }

        const wallDurationMs = 3000;
        const startTime = Date.now();
        let frame = 0;
        const wallWidth = 100;
        const wallHeight = 80;
        const earthColors = ['#8B4513', '#A0522D', '#D2B48C'];
        const initialWallX = avatarCenter.x;
        const initialWallY = avatarCenter.y + avatar.getBoundingClientRect().height / 2 + 10;

        console.log('drawEarthWallShield: Levantando muro de tierra...');
        while (Date.now() - startTime < wallDurationMs) {
            if (stopSignal || !socket || (repeatIntervalId && !repeatActionToggle.checked)) break;
            frame++;
            const progress = (Date.now() - startTime) / wallDurationMs;
            const opacity = 1 - progress;
            const currentHeight = Math.min(wallHeight, frame * 5);
            const currentWallX = initialWallX;
            const currentWallY = initialWallY - currentHeight;

            for (let i = 0; i < 5; i++) {
                const startX = currentWallX - wallWidth / 2 + (Math.random() - 0.5) * 10;
                const endX = currentWallX + wallWidth / 2 + (Math.random() - 0.5) * 10;
                const y = currentWallY + (Math.random() * currentHeight);
                const thickness = Math.max(1, 8 * opacity * Math.random());
                const color = earthColors[Math.floor(Math.random() * earthColors.length)];
                sendDrawCommand(startX, y, endX, y, color, thickness);
            }
            await new Promise(resolve => setTimeout(resolve, 100));
        }
        console.log('drawEarthWallShield: Muro de tierra finalizado.');
    }

// Efecto: Dron Seguidor con Rayo
async function drawDroneFollowerRay(playerId) {
    if (stopSignal) { console.log('drawDroneFollowerRay detenida.'); return; }
    console.log(`drawDroneFollowerRay: Iniciando efecto en ${playerId}.`);

    console.log('drawDroneFollowerRay: Dibujando dron JSON...');
    await drawJsonCommands(playerId, DRON_JSON_URL, 'head', 'none', 1.0);
    if (stopSignal) return;

    await new Promise(resolve => setTimeout(resolve, 1000));
    if (stopSignal || !socket || (repeatIntervalId && !repeatActionToggle.checked)) return;

    const avatarCenter = getTargetCoords(playerId);
    if (!avatarCenter) { console.warn('drawDroneFollowerRay: Avatar no encontrado.'); return; }

    const droneDurationMs = 8000;
    const startTime = Date.now();
    let frame = 0;
    const orbitRadius = 60;
    const droneSize = 10;
    const droneColor = '#800080';
    const laserColor = '#FF00FF';

    console.log('drawDroneFollowerRay: Iniciando efecto de seguimiento y disparo...');
    while (Date.now() - startTime < droneDurationMs) {
        if (stopSignal || !socket || (repeatIntervalId && !repeatActionToggle.checked)) break;
        frame++;
        const currentAvatarCenter = getTargetCoords(playerId);
        if (!currentAvatarCenter) { console.log('drawDroneFollowerRay: Objetivo desaparecido.'); return; }
        const centerX = currentAvatarCenter.x;
        const centerY = currentAvatarCenter.y;
        const droneAngle = frame * 0.1;
        const droneX = centerX + orbitRadius * Math.cos(droneAngle);
        const droneY = centerY + orbitRadius * Math.sin(droneAngle) * 0.5;

        sendDrawCommand(droneX - droneSize / 2, droneY - droneSize / 2, droneX + droneSize / 2, droneY + droneSize / 2, droneColor, 3);

        if (frame % 10 === 0) {
            console.log('drawDroneFollowerRay: Dron disparando rayo!');
            const rayTargetX = centerX + (Math.random() - 0.5) * 20;
            const rayTargetY = centerY + (Math.random() - 0.5) * 20;
            sendDrawCommand(droneX, droneY, rayTargetX, rayTargetY, laserColor, 2);
        }
        await new Promise(resolve => setTimeout(resolve, 80));
    }
    console.log('drawDroneFollowerRay: Dron finalizado.');
}



    /* ----------  EVENTOS  ---------- */

    // NUEVO: Event listener para el botón de detener
    stopBtn.addEventListener('click', () => {
        console.log('Botón de detener presionado. Enviando señal de parada.');
        stopSignal = true;

        if (repeatIntervalId) {
            clearInterval(repeatIntervalId);
            repeatIntervalId = null;
            console.log('Intervalo de repetición detenido.');
        }

        // Restaurar el estado de los botones inmediatamente
        drawBtn.textContent = 'Dibujar en avatar';
        drawBtn.style.background = 'linear-gradient(145deg, #4CAF50, #45a049)';
        drawBtn.disabled = false;
        stopBtn.disabled = true;
        isDrawing = false; // Forzar el reseteo del estado de dibujo
    });

    drawBtn.addEventListener('click', async () => {
        const pid = playerSelect.value;
        if (!pid) {
            alert('Por favor, selecciona un jugador.');
            return;
        }

        const selectedDrawingUrl = jsonUrlSelect.value;
        const selectedEffectValue = effectSelect.value;

        // Si el botón dice "Detener", significa que una repetición está activa
        if (repeatIntervalId) {
            console.log('Botón de detener repetición presionado.');
            stopSignal = true; // También detiene la animación actual
            clearInterval(repeatIntervalId);
            repeatIntervalId = null;

            // Restaurar estado de los botones
            drawBtn.textContent = 'Dibujar en avatar';
            drawBtn.style.background = 'linear-gradient(145deg, #4CAF50, #45a049)';
            stopBtn.disabled = true; // Deshabilitar el botón de detener dedicado
            isDrawing = false;
            return;
        }

        // Determinar qué acción ejecutar
        let actionToExecute = null;
        let effectiveWaitDelay = WAIT_ACTION_DELAY;

        // Establecer el estado inicial para la nueva acción
        stopSignal = false; // Resetear la señal de parada
        isDrawing = true;
        drawBtn.disabled = true;
        stopBtn.disabled = false;

        try {
            if (selectedEffectValue && selectedEffectValue.startsWith('effect:')) {
                // ... (toda la lógica de switch para efectos procedurales)
                 switch (selectedEffectValue) {
                    case 'effect:bomb': actionToExecute = () => drawBombWithExplosion(pid); effectiveWaitDelay = WAIT_ACTION_DELAY + 2500; break;
                    case 'effect:lightning_zigzag': actionToExecute = () => lightningZigzagChaser(pid); effectiveWaitDelay = WAIT_ACTION_DELAY + 2500; break;
                    case 'effect:fire_aura_circular': actionToExecute = () => circularFireAura(pid, 500); effectiveWaitDelay = WAIT_ACTION_DELAY + 500; break;
                    case 'effect:space_rocket': actionToExecute = () => spaceRocketChaser(pid); effectiveWaitDelay = WAIT_ACTION_DELAY + 4500; break;
                    case 'effect:pistol_shoot': actionToExecute = () => pistolShootEffect(pid); effectiveWaitDelay = WAIT_ACTION_DELAY + 1500; break;
                    case 'effect:flashlight_star': actionToExecute = () => flashlightStarChaser(pid); effectiveWaitDelay = WAIT_ACTION_DELAY + 2500; break;
                    case 'effect:arrow_chaser': actionToExecute = () => drawArrowChaser(pid); effectiveWaitDelay = WAIT_ACTION_DELAY + 2000; break;
                    case 'effect:shotgun_blast': actionToExecute = () => drawShotgunBlast(pid); effectiveWaitDelay = WAIT_ACTION_DELAY + 1000; break;
                    case 'effect:grenade_launcher': actionToExecute = () => drawGrenadeLauncher(pid); effectiveWaitDelay = WAIT_ACTION_DELAY + 3000; break;
                    case 'effect:laser_rifle_beam': actionToExecute = () => drawLaserRifleBeam(pid); effectiveWaitDelay = WAIT_ACTION_DELAY + 1000; break;
                    case 'effect:boomerang_guided': actionToExecute = () => drawBoomerangGuided(pid); effectiveWaitDelay = WAIT_ACTION_DELAY + 4000; break;
                    case 'effect:sword_slash_arc': actionToExecute = () => drawSwordSlashArc(pid); effectiveWaitDelay = WAIT_ACTION_DELAY + 1000; break;
                    case 'effect:seismic_smash_wave': actionToExecute = () => drawSeismicSmashWave(pid); effectiveWaitDelay = WAIT_ACTION_DELAY + 2000; break;
                    case 'effect:electric_whip_snap': actionToExecute = () => drawElectricWhipSnap(pid); effectiveWaitDelay = WAIT_ACTION_DELAY + 1500; break;
                    case 'effect:sticky_grenade_proj': actionToExecute = () => drawStickyGrenadeProj(pid); effectiveWaitDelay = WAIT_ACTION_DELAY + 3500; break;
                    case 'effect:proximity_mine_trap': actionToExecute = () => drawProximityMineTrap(pid); effectiveWaitDelay = WAIT_ACTION_DELAY + 1000; break;
                    case 'effect:ice_storm_area': actionToExecute = () => drawIceStormArea(pid); effectiveWaitDelay = WAIT_ACTION_DELAY + 5000; break;
                    case 'effect:wind_tornado_spin': actionToExecute = () => drawWindTornadoSpin(pid); effectiveWaitDelay = WAIT_ACTION_DELAY + 5000; break;
                    case 'effect:earth_wall_shield': actionToExecute = () => drawEarthWallShield(pid); effectiveWaitDelay = WAIT_ACTION_DELAY + 3000; break;
                    case 'effect:drone_follower_ray': actionToExecute = () => drawDroneFollowerRay(pid); effectiveWaitDelay = WAIT_ACTION_DELAY + 8000; break;
                    default:
                        console.error('Efecto procedural no reconocido:', selectedEffectValue);
                        alert('Efecto procedural no reconocido o no implementado.');
                        return; // Salir y resetear en finally
                }
            } else if (selectedEffectValue && selectedEffectValue !== JSON_EFFECTS['Ninguno']) {
                actionToExecute = () => drawJsonCommands(pid, selectedEffectValue);
            } else if (selectedDrawingUrl && selectedDrawingUrl !== JSON_SOURCES['Ninguno']) {
                actionToExecute = () => drawJsonCommands(pid);
            } else {
                alert('Por favor, selecciona un Dibujo o un Efecto.');
                return; // Salir y resetear en finally
            }

            if (repeatActionToggle.checked) {
                drawBtn.textContent = 'Detener Repetición';
                drawBtn.style.background = 'linear-gradient(145deg, #f44336, #d32f2f)';
                drawBtn.disabled = false; // El botón de repetir ahora es el de detener
                console.log('Evento click: Iniciando repetición...');

                const repeatedAction = async () => {
                    if (stopSignal || !socket || !repeatActionToggle.checked) {
                        if (repeatIntervalId) clearInterval(repeatIntervalId);
                        repeatIntervalId = null;
                        drawBtn.textContent = 'Dibujar en avatar';
                        drawBtn.style.background = 'linear-gradient(145deg, #4CAF50, #45a049)';
                        stopBtn.disabled = true;
                        isDrawing = false;
                        console.log('Repetición detenida automáticamente.');
                        return;
                    }
                    if (isDrawing) {
                        console.log('Saltando repetición: Una acción aún está en progreso.');
                        return;
                    }
                    isDrawing = true;
                    try {
                        await actionToExecute();
                    } finally {
                        isDrawing = false;
                    }
                    console.log(`Evento click: Acción repetida. Próximo en ${effectiveWaitDelay / 1000} segundos.`);
                };

                await repeatedAction(); // Ejecutar la primera vez
                if (!stopSignal) { // No establecer intervalo si se detuvo durante la primera ejecución
                    repeatIntervalId = setInterval(repeatedAction, effectiveWaitDelay);
                }
            } else {
                // Ejecutar la acción una sola vez
                console.log('Evento click: Ejecutando acción una vez.');
                await actionToExecute();
                console.log('Evento click: Acción única finalizada.');
            }
        } finally {
            // Este bloque se ejecuta después de que la acción termine (naturalmente o por detención)
            // Solo restaurar la UI si no estamos en un ciclo de repetición
            if (!repeatIntervalId) {
                drawBtn.disabled = false;
                stopBtn.disabled = true;
                isDrawing = false;
                console.log("Acción finalizada, estado de UI restaurado.");
            }
        }
    });

/**
 * Obtiene el ID del jugador propio usando las clases CSS de Drawaria
 * @returns {string|null} - ID del jugador propio o null si no se encuentra
 */
function getOwnPlayerId() {
    // Método 1: Buscar por clase CSS en lista de jugadores
    const ownPlayerName = document.querySelector('.playerlist-row .playerlist-name-self');
    if (ownPlayerName) {
        const ownPlayerRow = ownPlayerName.closest('.playerlist-row');
        if (ownPlayerRow) {
            return ownPlayerRow.dataset.playerid;
        }
    }

    // Método 2: Buscar directamente en el avatar si está visible
    const ownAvatar = document.querySelector('.spawnedavatar-self');
    if (ownAvatar) {
        return ownAvatar.dataset.playerid;
    }

    console.warn('getOwnPlayerId: No se pudo encontrar el jugador propio.');
    return null;
}

/**
 * Obtiene las coordenadas del centro del jugador propio
 * @returns {object|null} - {x, y} o null si no se encuentra
 */
function getOwnPlayerCoords() {
    const ownPlayerId = getOwnPlayerId();
    if (!ownPlayerId) return null;
    return getTargetCoords(ownPlayerId); // Reutilizar getTargetCoords que usa _getAttachmentPoint
}


    // Asegurarse de limpiar el intervalo si el usuario cambia de página o cierra el script
    window.addEventListener('beforeunload', () => {
        if (repeatIntervalId) {
            clearInterval(repeatIntervalId);
            repeatIntervalId = null;
        }
        stopSignal = true; // Señal de parada al salir
    });

    const plEl = document.getElementById('playerlist');
    if (plEl) {
        new MutationObserver(debouncedRefresh).observe(plEl, {
            childList: true,
            subtree: true,
            attributes: true,
            attributeFilter: ['data-playerid']
        });
    }

    refreshPlayerList();

})();