您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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(); })();