Choose an object and draw it on the selected player’s avatar
当前为
// ==UserScript==
// @name Drawaria Draw-on-Avatar (Beta Privada)
// @namespace http://tampermonkey.net/
// @version 1.5
// @description Choose an object and draw it on the selected player’s avatar
// @author YouTubeDrawaria
// @match https://drawaria.online/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=drawaria.online
// @grant GM_xmlhttpRequest
// @license MIT
// ==/UserScript==
(function () {
'use strict';
/* ---------- CONFIGURACIÓN ---------- */
const JSON_SOURCES = {
'Fuego': 'https://raw.githubusercontent.com/DrawariaDeveloper/Json-to-Drawaria/main/fire.json',
'Fuego blue': 'https://raw.githubusercontent.com/DrawariaDeveloper/Json-to-Drawaria/main/bluefire.json',
'Laser': 'https://raw.githubusercontent.com/DrawariaDeveloper/Json-to-Drawaria/main/laser.json',
'Ataque': 'https://raw.githubusercontent.com/DrawariaDeveloper/Json-to-Drawaria/main/ataque.json',
'Cohete': 'https://raw.githubusercontent.com/DrawariaDeveloper/Json-to-Drawaria/main/cohete.json',
'Defensa': 'https://raw.githubusercontent.com/DrawariaDeveloper/Json-to-Drawaria/main/defensa.json',
'Espada': 'https://raw.githubusercontent.com/DrawariaDeveloper/Json-to-Drawaria/main/espada.json',
'Explosion': 'https://raw.githubusercontent.com/DrawariaDeveloper/Json-to-Drawaria/main/explosion.json',
'Gorra': 'https://raw.githubusercontent.com/DrawariaDeveloper/Json-to-Drawaria/main/gorra.json',
'Pistola': 'https://raw.githubusercontent.com/DrawariaDeveloper/Json-to-Drawaria/main/pistola.json',
'Rayo': 'https://raw.githubusercontent.com/DrawariaDeveloper/Json-to-Drawaria/main/rayo.json'
};
const DEFAULT_JSON_NAME = 'Fuego';
const DRAW_PADDING = 10; // Espacio en píxeles entre el avatar y el dibujo para posiciones laterales/superiores/inferiores
const DRAW_PADDING_HAND = 3; // ¡NUEVO! Espacio personalizado para las posiciones de agarre
const HAND_GRIP_OFFSET_Y = 2; // Desplazamiento vertical para las posiciones de agarre (simula la altura de la mano)
/* ------------------------------------ */
let socket;
const canvas = document.getElementById('canvas');
// Obtenemos el contexto 2D para dibujar localmente en el canvas de Drawaria
const ctx = canvas ? canvas.getContext('2d') : null; // Nos aseguramos de que el canvas exista
const originalSend = WebSocket.prototype.send;
WebSocket.prototype.send = function (...args) {
if (!socket) socket = this;
return originalSend.apply(this, args);
};
/* ---------- INTERFAZ DE USUARIO (UI) ---------- */
const container = document.createElement('div');
container.style.cssText = `
position:fixed; bottom:10px; right:10px; z-index:9999;
background:rgba(17,17,17,0.85); /* Fondo oscuro ligeramente transparente */
color:#fff; padding:10px 15px; border-radius:8px;
font-family: 'Segoe UI', Arial, sans-serif; font-size:13px;
display:flex; flex-direction:column; gap:10px;
box-shadow: 0 4px 12px rgba(0,0,0,0.5); /* Sombra más pronunciada */
cursor: default;
backdrop-filter: blur(3px); /* Efecto de desenfoque para un aspecto moderno */
`;
// Barra de título para arrastrar el panel
const titleBar = document.createElement('div');
titleBar.textContent = 'Dibujar en Avatar';
titleBar.style.cssText = `
font-weight: bold;
font-size: 14px;
text-align: center;
cursor: grab;
background: rgba(30,30,30,0.9);
border-radius: 6px 6px 0 0;
margin: -10px -15px 10px -15px;
padding: 10px 15px;
border-bottom: 1px solid #444; /* Separador sutil */
`;
container.appendChild(titleBar);
const contentDiv = document.createElement('div');
contentDiv.style.cssText = `
display:flex; flex-direction:column; gap:8px;
`;
container.appendChild(contentDiv);
// Etiqueta y estilo para todos los selectores
const selectBaseStyle = `
flex-grow: 1;
padding: 7px 10px; border-radius: 5px; border: 1px solid #555;
background: #333; color: #fff;
font-size: 13px;
appearance: none;
background-image: url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23ffffff%22%20d%3D%22M287%2C197.3L159.2%2C69.5c-3.6-3.6-8.2-5.4-12.8-5.4s-9.2%2C1.8-12.8%2C5.4L5.4%2C197.3c-7.2%2C7.2-7.2%2C18.8%2C0%2C26c3.6%2C3.6%2C8.2%2C5.4%2C12.8%2C5.4s9.2%2C1.8%2C12.8%2C5.4l117%2C117c3.6%2C3.6%2C8.2%2C5.4%2C12.8%2C5.4s9.2%2C1.8%2C12.8%2C5.4l117-117c7.2-7.2%2C7.2-18.8%2C0-26C294.2%2C204.5%2C294.2%2C200.9%2C287%2C197.3z%22%2F%3E%3C%2Fsvg%3E');
background-repeat: no-repeat;
background-position: right 8px center;
background-size: 10px;
cursor: pointer;
`;
// Selector de jugador
const playerSelectWrapper = document.createElement('div');
playerSelectWrapper.style.cssText = `display:flex; align-items:center; gap:8px;`;
playerSelectWrapper.innerHTML = '<span>Jugador:</span>';
const playerSelect = document.createElement('select');
playerSelect.style.cssText = selectBaseStyle;
playerSelectWrapper.appendChild(playerSelect);
contentDiv.appendChild(playerSelectWrapper);
// Selector de URL de dibujo
const urlSelectWrapper = document.createElement('div');
urlSelectWrapper.style.cssText = `display:flex; align-items:center; gap:8px;`;
urlSelectWrapper.innerHTML = '<span>Dibujo:</span>';
const jsonUrlSelect = document.createElement('select');
jsonUrlSelect.style.cssText = selectBaseStyle;
for (const name in JSON_SOURCES) {
const opt = document.createElement('option');
opt.value = JSON_SOURCES[name];
opt.textContent = name;
jsonUrlSelect.appendChild(opt);
}
jsonUrlSelect.value = JSON_SOURCES[DEFAULT_JSON_NAME];
urlSelectWrapper.appendChild(jsonUrlSelect);
contentDiv.appendChild(urlSelectWrapper);
// Selector de Posición
const positionSelectWrapper = document.createElement('div');
positionSelectWrapper.style.cssText = `display:flex; align-items:center; gap:8px;`;
positionSelectWrapper.innerHTML = '<span>Posición:</span>';
const positionSelect = document.createElement('select');
positionSelect.style.cssText = selectBaseStyle;
const positions = {
'Centrado': 'centered',
'Derecha': 'right',
'Arriba': 'top',
'Izquierda': 'left',
'Abajo': 'bottom',
'Cabeza': 'head', // Nueva posición: en la cabeza
'Agarre Derecha': 'grip_right', // Nueva posición: para simular agarre en mano derecha
'Agarre Izquierda': 'grip_left' // Nueva posición: para simular agarre en mano izquierda
};
for (const name in positions) {
const opt = document.createElement('option');
opt.value = positions[name];
opt.textContent = name;
positionSelect.appendChild(opt);
}
positionSelect.value = 'head'; // Establecido a 'head' por defecto, como en la imagen deseada
positionSelectWrapper.appendChild(positionSelect);
contentDiv.appendChild(positionSelectWrapper);
// Botón de dibujo
const drawBtn = document.createElement('button');
drawBtn.textContent = 'Dibujar en avatar';
drawBtn.disabled = true;
drawBtn.style.cssText = `
padding: 9px 15px; border-radius: 6px; border: none;
background: linear-gradient(145deg, #4CAF50, #45a049);
color: white; font-weight: bold; font-size: 14px;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 2px 5px rgba(0,0,0,0.3);
&:hover {
background: linear-gradient(145deg, #45a049, #3d8c41);
box-shadow: 0 4px 10px rgba(0,0,0,0.4);
transform: translateY(-1px);
}
&:active {
transform: translateY(0);
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
}
&:disabled {
background: #666; cursor: not-allowed;
box-shadow: none;
opacity: 0.7;
}
`;
contentDiv.appendChild(drawBtn);
document.body.appendChild(container);
/* ---------- FUNCIONALIDAD DE ARRASTRE (DRAGGABLE) ---------- */
let isDragging = false;
let offsetX, offsetY;
titleBar.addEventListener('mousedown', (e) => {
isDragging = true;
offsetX = e.clientX - container.getBoundingClientRect().left;
offsetY = e.clientY - container.getBoundingClientRect().top;
container.style.cursor = 'grabbing';
container.style.transition = 'none'; // Deshabilita la transición durante el arrastre
});
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
// Evita que el panel se salga de los límites de la ventana
let newX = e.clientX - offsetX;
let newY = e.clientY - offsetY;
newX = Math.max(0, Math.min(newX, window.innerWidth - container.offsetWidth));
newY = Math.max(0, Math.min(newY, window.innerHeight - container.offsetHeight));
container.style.left = newX + 'px';
container.style.top = newY + 'px';
});
document.addEventListener('mouseup', () => {
isDragging = false;
container.style.cursor = 'default';
container.style.transition = ''; // Vuelve a habilitar la transición
});
/* ---------- LISTA DE JUGADORES ---------- */
/* ---------- LISTA DE JUGADORES (VERSIÓN MEJORADA) ---------- */
let lastPlayerList = new Set(); // Para detectar cambios reales
let isUpdatingList = false; // Bandera para prevenir actualizaciones múltiples
function refreshPlayerList() {
if (isUpdatingList) return; // Previene ejecuciones múltiples simultáneas
// Construye un Set con los jugadores actuales para comparar
const currentPlayers = new Set();
const playerRows = document.querySelectorAll('.playerlist-row[data-playerid]');
playerRows.forEach(row => {
if (row.dataset.self !== 'true' && row.dataset.playerid !== '0') {
const name = row.querySelector('.playerlist-name a')?.textContent || `Jugador ${row.dataset.playerid}`;
currentPlayers.add(`${row.dataset.playerid}:${name}`);
}
});
// Solo actualiza si realmente hay cambios en la lista de jugadores
const playersChanged = currentPlayers.size !== lastPlayerList.size ||
![...currentPlayers].every(player => lastPlayerList.has(player));
if (!playersChanged) return; // No hay cambios reales, no actualizar
isUpdatingList = true;
// Guarda la selección actual ANTES de cualquier modificación
const previousSelection = playerSelect.value;
const previousSelectedText = playerSelect.selectedOptions?.textContent || '';
// Limpia y reconstruye la lista
playerSelect.innerHTML = '';
playerRows.forEach(row => {
if (row.dataset.self === 'true') return;
if (row.dataset.playerid === '0') return;
const name = row.querySelector('.playerlist-name a')?.textContent || `Jugador ${row.dataset.playerid}`;
const opt = document.createElement('option');
opt.value = row.dataset.playerid;
opt.textContent = name;
playerSelect.appendChild(opt);
});
// ESTRATEGIA MEJORADA DE RESTAURACIÓN
if (previousSelection) {
// Intento 1: Restaurar por ID del jugador
let restored = false;
for (let option of playerSelect.options) {
if (option.value === previousSelection) {
playerSelect.value = previousSelection;
restored = true;
break;
}
}
// Intento 2: Si el ID no existe, buscar por nombre
if (!restored && previousSelectedText) {
for (let option of playerSelect.options) {
if (option.textContent === previousSelectedText) {
playerSelect.value = option.value;
restored = true;
break;
}
}
}
}
// Actualiza el registro de jugadores
lastPlayerList = new Set(currentPlayers);
drawBtn.disabled = playerSelect.children.length === 0;
isUpdatingList = false;
}
// Versión optimizada del MutationObserver con debounce
let refreshTimeout;
function debouncedRefresh() {
clearTimeout(refreshTimeout);
refreshTimeout = setTimeout(refreshPlayerList, 100); // Espera 100ms antes de actualizar
}
/* ---------- ANÁLISIS DE JSON DE DIBUJO ---------- */
function analyzeJsonBounds(jsonCommands) {
let min_nx = Infinity, max_nx = -Infinity;
let min_ny = Infinity, max_ny = -Infinity;
if (!Array.isArray(jsonCommands) || jsonCommands.length === 0) {
return { min_nx: 0, max_nx: 0, min_ny: 0, max_ny: 0 };
}
for (const cmdArr of jsonCommands) {
if (cmdArr.length > 2 && Array.isArray(cmdArr[2]) && cmdArr[2].length >= 4) {
const [nx1, ny1, nx2, ny2] = cmdArr[2];
min_nx = Math.min(min_nx, nx1, nx2);
max_nx = Math.max(max_nx, nx1, nx2);
min_ny = Math.min(min_ny, ny1, ny2);
max_ny = Math.max(max_ny, ny1, ny2);
}
}
if (min_nx === Infinity || max_nx === -Infinity || min_ny === Infinity || max_ny === -Infinity) {
return { min_nx: 0, max_nx: 0, min_ny: 0, max_ny: 0 };
}
return { min_nx, max_nx, min_ny, max_ny };
}
/* ---------- LÓGICA DE DIBUJO ---------- */
async function drawOnAvatar(playerId) {
if (!socket) {
alert('Socket no está listo. Asegúrate de estar en una partida de Drawaria para usarlo.');
return;
}
const avatar = document.querySelector(`.spawnedavatar[data-playerid="${playerId}"]`);
if (!avatar) {
alert('Avatar no encontrado. Asegúrate de que el jugador esté visible en pantalla.');
return;
}
// Obtiene las coordenadas y dimensiones del canvas y del avatar en pantalla
const cRect = canvas.getBoundingClientRect();
const aRect = avatar.getBoundingClientRect();
// Coordenadas del avatar relativas al canvas
const avatarX = aRect.left - cRect.left;
const avatarY = aRect.top - cRect.top;
const avatarWidth = aRect.width;
const avatarHeight = aRect.height;
const avatarCenterX = avatarX + avatarWidth / 2;
const avatarCenterY = avatarY + avatarHeight / 2;
// Obtiene el JSON del dibujo desde la URL seleccionada
const url = jsonUrlSelect.value;
const json = await fetchJson(url);
if (!json || !Array.isArray(json.commands)) {
alert('JSON inválido o no se pudo cargar el dibujo. Asegúrate de que el formato sea correcto y la URL accesible.');
return;
}
// Analiza el JSON para obtener sus límites de coordenadas (ej. min_nx, max_nx)
const { min_nx, max_nx, min_ny, max_ny } = analyzeJsonBounds(json.commands);
// Calcula el ancho y alto del "bounding box" del dibujo en píxeles,
// cuando se escala con canvas.width/height (como lo hace el script original).
const actualDrawPxWidth = (max_nx - min_nx) * canvas.width;
const actualDrawPxHeight = (max_ny - min_ny) * canvas.height;
// Estas serán las coordenadas absolutas en el lienzo de Drawaria para el punto (0,0) del JSON
// (Es decir, el offset que se suma a nx * canvas.width para posicionar el dibujo completo)
let drawingOriginX;
let drawingOriginY;
const currentPosition = positionSelect.value;
switch (currentPosition) {
case 'centered':
// Queremos que el centro del *borde visual del dibujo* esté en el centro del avatar.
// El centro del dibujo (en coordenadas del JSON) es ((min_nx + max_nx) / 2, (min_ny + max_ny) / 2).
// Su posición real en píxeles sería: (min_nx * canvas.width + actualDrawPxWidth / 2).
drawingOriginX = avatarCenterX - (min_nx * canvas.width + actualDrawPxWidth / 2);
drawingOriginY = avatarCenterY - (min_ny * canvas.height + actualDrawPxHeight / 2);
break;
case 'top':
// Centrado horizontalmente, parte inferior del dibujo alineada con la parte superior del avatar + padding
drawingOriginX = avatarCenterX - (min_nx * canvas.width + actualDrawPxWidth / 2);
// La parte inferior del dibujo (min_ny * canvas.height + actualDrawPxHeight) debe estar en avatarY - DRAW_PADDING
drawingOriginY = (avatarY - DRAW_PADDING) - (min_ny * canvas.height + actualDrawPxHeight);
break;
case 'bottom':
// Centrado horizontalmente, parte superior del dibujo alineada con la parte inferior del avatar + padding
drawingOriginX = avatarCenterX - (min_nx * canvas.width + actualDrawPxWidth / 2);
// La parte superior del dibujo (min_ny * canvas.height) debe estar en avatarY + avatarHeight + DRAW_PADDING
drawingOriginY = (avatarY + avatarHeight + DRAW_PADDING) - (min_ny * canvas.height);
break;
case 'left':
// Centrado verticalmente, parte derecha del dibujo alineada con la parte izquierda del avatar - padding
drawingOriginY = avatarCenterY - (min_ny * canvas.height + actualDrawPxHeight / 2);
// La parte derecha del dibujo (min_nx * canvas.width + actualDrawPxWidth) debe estar en avatarX - DRAW_PADDING
drawingOriginX = (avatarX - DRAW_PADDING) - (min_nx * canvas.width + actualDrawPxWidth);
break;
case 'right':
// Centrado verticalmente, parte izquierda del dibujo alineada con la parte derecha del avatar + padding
drawingOriginY = avatarCenterY - (min_ny * canvas.height + actualDrawPxHeight / 2);
// La parte izquierda del dibujo (min_nx * canvas.width) debe estar en avatarX + avatarWidth + DRAW_PADDING
drawingOriginX = (avatarX + avatarWidth + DRAW_PADDING) - (min_nx * canvas.width);
break;
case 'head':
// Centrado horizontalmente, parte inferior del dibujo alineada con la parte superior del avatar (ajustado para "sombrero")
drawingOriginX = avatarCenterX - (min_nx * canvas.width + actualDrawPxWidth / 2);
drawingOriginY = avatarY - (min_ny * canvas.height + actualDrawPxHeight) + (avatarHeight * 0.1); // Pequeña superposición
break;
case 'grip_right':
// A la derecha del avatar, verticalmente desplazado hacia abajo para simular agarre.
// La parte izquierda del dibujo (min_nx * canvas.width) debe estar a la derecha del avatar + padding personalizado
drawingOriginX = (avatarX + avatarWidth + DRAW_PADDING_HAND) - (min_nx * canvas.width);
// Centro vertical del dibujo (min_ny * canvas.height + actualDrawPxHeight / 2) debe estar en el centro del avatar + offset
drawingOriginY = avatarCenterY - (min_ny * canvas.height + actualDrawPxHeight / 2) + HAND_GRIP_OFFSET_Y;
break;
case 'grip_left':
// A la izquierda del avatar, verticalmente desplazado hacia abajo para simular agarre.
// La parte derecha del dibujo (min_nx * canvas.width + actualDrawPxWidth) debe estar a la izquierda del avatar - padding personalizado
drawingOriginX = (avatarX - DRAW_PADDING_HAND) - (min_nx * canvas.width + actualDrawPxWidth);
// Centro vertical del dibujo (min_ny * canvas.height + actualDrawPxHeight / 2) debe estar en el centro del avatar + offset
drawingOriginY = avatarCenterY - (min_ny * canvas.height + actualDrawPxHeight / 2) + HAND_GRIP_OFFSET_Y;
break;
default: // Fallback a centrado si la posición es inválida
drawingOriginX = avatarCenterX - (min_nx * canvas.width + actualDrawPxWidth / 2);
drawingOriginY = avatarCenterY - (min_ny * canvas.height + actualDrawPxHeight / 2);
break;
}
// Dibuja cada segmento
for (const cmdArr of json.commands) {
const [, , [nx1, ny1, nx2, ny2, , thickNeg, color]] = cmdArr;
// Aplica la escala original (nx * canvas.width) y luego traslada con el origen calculado
const x1 = nx1 * canvas.width + drawingOriginX;
const y1 = ny1 * canvas.height + drawingOriginY;
const x2 = nx2 * canvas.width + drawingOriginX;
const y2 = ny2 * canvas.height + drawingOriginY;
// Llama a sendDrawCommand, que ahora también dibujará localmente
sendDrawCommand(x1, y1, x2, y2, color, -thickNeg);
await new Promise(r => setTimeout(r, 15)); // Pequeño retraso para visibilidad y evitar saturar el servidor
}
}
// Envía el comando de dibujo al socket de Drawaria Y DIBUJA LOCALMENTE EN EL CANVAS
function sendDrawCommand(x1, y1, x2, y2, color, thickness) {
// --- Dibujo local (visibilidad del lado del cliente) ---
if (ctx && canvas) { // Asegúrate de que el contexto 2D y el canvas están disponibles
ctx.strokeStyle = color;
ctx.lineWidth = thickness; // Usa la "thickness" proporcionada (ya es positiva)
ctx.lineCap = 'round'; // Para uniones de línea suaves, como en Drawaria
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
}
// --- Fin del dibujo local ---
if (!socket) return; // Si el socket no está listo, no se envía al servidor
// Drawaria espera coordenadas normalizadas (0-1) respecto al tamaño total del lienzo (canvas)
const normX1 = (x1 / canvas.width).toFixed(4);
const normY1 = (y1 / canvas.height).toFixed(4);
const normX2 = (x2 / canvas.width).toFixed(4);
const normY2 = (y2 / canvas.height).toFixed(4);
// El comando usa '0 - thickness' porque el thickNeg original del JSON es un valor negativo
const cmd = `42["drawcmd",0,[${normX1},${normY1},${normX2},${normY2},false,${0 - thickness},"${color}",0,0,{}]]`;
socket.send(cmd);
}
/* ---------- AYUDAS (HELPERS) ---------- */
function fetchJson(url) {
return new Promise(resolve => {
GM_xmlhttpRequest({
method: 'GET',
url: url,
onload: r => {
try { resolve(JSON.parse(r.responseText)); }
catch {
console.error('Error al analizar JSON de la URL:', url, r.responseText);
resolve(null);
}
},
onerror: (error) => {
console.error('Error al obtener JSON de la URL:', url, error);
resolve(null);
}
});
});
}
/* ---------- EVENTOS ---------- */
/* ---------- EVENTOS (VERSIÓN MEJORADA) ---------- */
drawBtn.addEventListener('click', () => {
const pid = playerSelect.value;
if (!pid) {
alert('Por favor, selecciona un jugador.');
return;
}
drawOnAvatar(pid);
});
// Observer mejorado con debounce para reducir actualizaciones innecesarias
const plEl = document.getElementById('playerlist');
if (plEl) {
new MutationObserver(debouncedRefresh).observe(plEl, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['data-playerid']
});
}
// Llamada inicial
refreshPlayerList();
})();