Drawaria The Animator Mod

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

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

})();