// ==UserScript==
// @name Drawaria Avatar Physics Platformer🏃
// @namespace http://tampermonkey.net/
// @version 6.0
// @description This will allow you to move your avatar in the room like a videogame, you will need to see it other screen.
// @author YouTubeDrawaria
// @include https://drawaria.online*
// @icon https://www.google.com/s2/favicons?sz=64&domain=drawaria.online
// @grant none
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// Global variables for canvas and socket.
let drawariaCanvas = null;
let drawariaCtx = null;
let drawariaSocket = null;
let menuControllerInstance = null;
// Hook into WebSocket to capture the game's socket at the earliest opportunity
const originalWebSocketSend = WebSocket.prototype.send;
WebSocket.prototype.send = function (...args) {
if (!drawariaSocket && this.url && this.url.includes('drawaria')) {
drawariaSocket = this;
console.log('🔗 Drawaria WebSocket captured by early hook.');
}
return originalWebSocketSend.apply(this, args);
};
// === Utility Functions ===
function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }
// === Drawing Functions (now checks for drawariaCanvas internally) ===
function sendDrawCommand(x1, y1, x2, y2, color, thickness) {
// Ensure socket and canvas are ready for drawing commands
if (!drawariaSocket || drawariaSocket.readyState !== WebSocket.OPEN || !drawariaCanvas) {
return;
}
const normX1 = (x1 / drawariaCanvas.width).toFixed(4);
const normY1 = (y1 / drawariaCanvas.height).toFixed(4);
const normX2 = (x2 / drawariaCanvas.width).toFixed(4);
const normY2 = (y2 / drawariaCanvas.height).toFixed(4);
const command = `42["drawcmd",0,[${normX1},${normY1},${normX2},${normY2},false,${0 - thickness},"${color}",0,0,{}]]`;
drawariaSocket.send(command);
}
async function drawLine(startX, startY, endX, endY, color = '#000000', thickness = 2) {
sendDrawCommand(startX, startY, endX, endY, color, thickness);
await sleep(10);
}
async function drawRectangle(x, y, width, height, color = '#000000', thickness = 2) {
await drawLine(x, y, x + width, y, color, thickness);
await drawLine(x + width, y, x + width, y + height, color, thickness);
await drawLine(x + width, y + height, x, y + height, color, thickness);
await drawLine(x, y + height, x, y, color, thickness);
}
async function drawCircle(centerX, centerY, radius, color = '#000000', thickness = 2, segments = 24) {
const angleStep = (Math.PI * 2) / segments;
for (let i = 0; i < segments; i++) {
const angle1 = i * angleStep;
const angle2 = (i + 1) * angleStep;
const x1 = centerX + Math.cos(angle1) * radius;
const y1 = centerY + Math.sin(angle1) * radius;
const x2 = centerX + Math.cos(angle2) * radius;
const y2 = centerY + Math.sin(angle2) * radius;
await drawLine(x1, y1, x2, y2, color, thickness);
}
}
// === Nuevas Definiciones Globales de Colores y Hitbox Types ===
// Colores estándar en formato HEX y RGB
const STANDARD_COLORS = {
black: { hex: '#000000', r: 0, g: 0, b: 0 },
white: { hex: '#ffffff', r: 255, g: 255, b: 255 },
gray: { hex: '#7F7F7F', r: 127, g: 127, b: 127 },
red: { hex: '#ff0000', r: 255, g: 0, b: 0 },
green: { hex: '#00ff00', r: 0, g: 255, b: 0 },
blue: { hex: '#0000ff', r: 0, g: 0, b: 255 },
celeste: { hex: '#93cfff', r: 147, g: 207, b: 255 },
yellow: { hex: '#ffff00', r: 255, g: 255, b: 0 },
orange: { hex: '#ff9300', r: 255, g: 147, b: 0 },
violet: { hex: '#7f007f', r: 127, g: 0, b: 127 },
light_pink: { hex: '#ffbfff', r: 255, g: 191, b: 223 }, // Rosa claro
brown: { hex: '#7f3f00', r: 127, g: 63, b: 0 }
};
// Definición de los tipos de hitbox y sus propiedades por defecto
const DEFAULT_HITBOX_TYPES = {
solid: { name: 'Tocable', colorKey: 'black', type: 'solid', description: 'Plataformas sólidas' },
bounce: { name: 'Rebote', colorKey: 'yellow', type: 'bounce', description: 'Te hace rebotar' },
speed_boost: { name: 'Más Velocidad', colorKey: 'red', type: 'speed_boost', description: 'Aumenta tu velocidad' },
water: { name: 'Nadar', colorKey: 'blue', type: 'water', description: 'Permite nadar' },
anti_gravity: { name: 'Gravedad Arriba', colorKey: 'green', type: 'anti_gravity', description: 'Invierte la gravedad' },
normal_gravity: { name: 'Gravedad Normal', colorKey: 'light_pink', type: 'normal_gravity', description: 'Restablece la gravedad' },
push_back: { name: 'Empuje Atrás', colorKey: 'violet', type: 'push_back', description: 'Te empuja en la dirección opuesta' }
};
// Función de ayuda para convertir HEX a RGB
function hexToRgb(hex) {
const bigint = parseInt(hex.slice(1), 16);
const r = (bigint >> 16) & 255;
const g = (bigint >> 8) & 255;
const b = bigint & 255;
return { r, g, b };
}
// ===============================
// SISTEMA DE FÍSICA Y AVATAR
// ===============================
class Vector2D {
constructor(x = 0, y = 0) { this.x = x; this.y = y; }
add(vector) { return new Vector2D(this.x + vector.x, this.y + vector.y); }
subtract(vector) { return new Vector2D(this.x - vector.x, this.y - vector.y); }
multiply(scalar) { return new Vector2D(this.x * scalar, this.y * scalar); }
magnitude() { return Math.sqrt(this.x * this.x + this.y * this.y); }
normalize() { const mag = this.magnitude(); return mag > 0 ? new Vector2D(this.x / mag, this.y / mag) : new Vector2D(0, 0); }
copy() { return new Vector2D(this.x, this.y); }
}
class PhysicsAvatar {
constructor() {
// position.y represents the BOTTOM of the avatar in 0-100 game coords (100 is floor)
this.position = new Vector2D(50, 100);
this.velocity = new Vector2D(0, 0);
this.mass = 1.0;
this.friction = 0.1;
this.jumpForce = 20;
this.moveForce = 15;
this.gravity = 0.8;
this.onGround = false;
this.keys = {};
// Dynamic avatar dimensions in game units (0-100 scale)
this.avatarDimensions = { width: 0, height: 0 };
// Physics states
this.isPhysicsActive = false; // Controlled by menu
this.isSwimming = false;
this.lastBounceSurface = 0; // To prevent infinite bounces
// Specific Drawaria DOM selectors for avatar and game area
this.gameAreaSelectors = [
"#drawing-assistant-overlay", // Priorizar el canvas overlay
"#gamearea",
"#canvas",
"body"
];
this.myAvatarSelector = "body > div.spawnedavatar.spawnedavatar-self";
this.myAvatarElement = null; // Actual DOM element of the avatar
this.gameAreaElement = null; // Actual DOM element of the game area
this.overlayCanvas = null; // Referencia específica al canvas overlay
this.lastSentPosition = { x: -1, y: -1 };
// Altura de la barra de UI inferior (en píxeles)
this.bottomUIOffsetPx = 0;
// Offset de píxeles visual controlado por el usuario para el suelo
this.visualFloorOffsetPixels = 45; // PREDETERMINADO A 5 PÍXELES MÁS ARRIBA
// NUEVO: Sistema de límites inteligente
this.boundaryManager = null;
// Reinicializar Interaction colors (RGB) con los nuevos tipos y colores por defecto
this.surfaceColors = {}; // Se llenará en el constructor de PhysicsMenuController o con setHitboxColors
// NUEVAS PROPIEDADES PARA EFECTOS DE FÍSICA
this.originalGravity = this.gravity; // Guarda la gravedad inicial
this.isAntiGravityActive = false;
this.isSpeedBoostActive = false;
this.currentSpeedBoostFactor = 1.0;
this.pushBackForce = 20; // Fuerza del empuje hacia atrás
this.speedBoostDuration = 1000; // Duración del speed boost en ms
this.speedBoostTimeout = null;
// AGREGAR AnimationShield
this.animationShield = null; // Se inicializa al detectar el avatar
this.setupInput();
this.startAvatarDetection(); // Continuously detect avatar DOM element & sync position (passively)
console.log('🎮 PhysicsAvatar created. Physics are DEACTIVATED by default. Click "Iniciar Física".');
}
setupInput() {
// Specific controls: Alt+Z (Left), Alt+C (Right), Alt+X (Jump)
document.addEventListener('keydown', (e) => {
if (!this.isPhysicsActive) return;
if (e.altKey && e.code === 'KeyZ') {
e.preventDefault();
this.keys['left'] = true;
}
if (e.altKey && e.code === 'KeyC') {
e.preventDefault();
this.keys['right'] = true;
}
if (e.altKey && e.code === 'KeyX') {
e.preventDefault();
this.keys['jump'] = true;
}
});
document.addEventListener('keyup', (e) => {
if (!this.isPhysicsActive) return;
if (e.altKey && e.code === 'KeyZ') {
this.keys['left'] = false;
}
if (e.altKey && e.code === 'KeyC') {
this.keys['right'] = false;
}
if (e.altKey && e.code === 'KeyX') {
this.keys['jump'] = false;
}
});
}
// Nueva función para detectar específicamente el canvas overlay
detectOverlayCanvas() {
const overlayCanvas = document.getElementById('drawing-assistant-overlay');
if (overlayCanvas) {
this.overlayCanvas = overlayCanvas;
console.log('🎯 Canvas overlay detectado:', {
width: overlayCanvas.width,
height: overlayCanvas.height,
styleWidth: overlayCanvas.style.width,
styleHeight: overlayCanvas.style.height
});
return true;
}
return false;
}
// Contenido del bucle de detección (extraído para claridad)
_detectionLoopContent() {
// Try to find avatar element
if (!this.myAvatarElement) {
const selectors = [
"body > div.spawnedavatar.spawnedavatar-self",
".spawnedavatar-self",
"[class*='avatar'][class*='self']",
".avatar.self"
];
for (const selector of selectors) {
const element = document.querySelector(selector);
if (element) {
this.myAvatarElement = element;
console.log(`🎯 Avatar encontrado con selector: ${selector}`);
// Inicializar AnimationShield cuando se encuentra el avatar
if (!this.animationShield) {
this.animationShield = new AnimationShield(this);
console.log('🛡️ AnimationShield inicializado');
}
break;
}
}
}
// Detectar canvas overlay específicamente
if (!this.overlayCanvas) {
this.detectOverlayCanvas();
}
// Try to find the best game area reference element (PRIORIZAR OVERLAY)
if (!this.gameAreaElement) {
for (const selector of this.gameAreaSelectors) {
const element = document.querySelector(selector);
if (element) {
this.gameAreaElement = element;
console.log(`🎮 Game area detectada: ${selector}`);
break; // Salir después de encontrar el mejor elemento
}
}
}
// Si todos los elementos críticos están listos, inicializar BoundaryManager
if (this.myAvatarElement && this.gameAreaElement && this.overlayCanvas && !this.boundaryManager) {
this.boundaryManager = new BoundaryManager(this);
console.log('📐 BoundaryManager inicializado');
}
// Detectar la altura de la UI inferior
if (this.gameAreaElement) {
// Selectores más específicos para barras de UI comunes
const hotbarElement = document.querySelector('.hotbar, #colorpickercontainer, .inputcontainer, [class*="chat-input-row"], [class*="drawing-hotbar"]');
if (hotbarElement) {
this.bottomUIOffsetPx = hotbarElement.offsetHeight > 0 ? hotbarElement.offsetHeight : 0;
} else {
this.bottomUIOffsetPx = 0;
}
}
// If elements are found and physics is NOT active, update position from DOM passively
if (this.myAvatarElement && this.gameAreaElement && !this.isPhysicsActive) {
this.updatePositionFromDOM(true);
}
}
// Continuously try to find the avatar element and game area reference
startAvatarDetection() {
this.detectionInterval = setInterval(() => this._detectionLoopContent(), 50); // Llama al contenido del bucle
}
// Update internal physics position from DOM's avatar element
updatePositionFromDOM(passiveSync = false) {
if (!this.myAvatarElement || !this.gameAreaElement || !drawariaCanvas) return false;
try {
const avatarRect = this.myAvatarElement.getBoundingClientRect();
const gameRect = this.gameAreaElement.getBoundingClientRect();
// Update dynamic avatar dimensions (in game units 0-100)
this.avatarDimensions.width = (avatarRect.width / gameRect.width) * 100;
this.avatarDimensions.height = (avatarRect.height / gameRect.height) * 100;
// Calculate current X based on center of avatar
const currentX = ((avatarRect.left + avatarRect.width / 2 - gameRect.left) / gameRect.width) * 100;
// Calculate current Y based on the BOTTOM of the avatar
const currentY = ((avatarRect.bottom - gameRect.top) / gameRect.height) * 100;
// Only update internal position if it's a significant change or active sync
if (!passiveSync ||
(Math.abs(this.position.x - currentX) > 0.5 || Math.abs(this.position.y - currentY) > 0.5)) {
this.position.x = currentX;
this.position.y = currentY;
if (passiveSync) {
this.velocity = new Vector2D(0, 0); // Reset velocity if not active
this.onGround = (currentY >= 99.5); // Check if near ground
}
}
return true;
} catch (e) {
console.warn("Error reading avatar DOM position:", e);
return false;
}
}
// Activate physics: snap avatar to floor, reset velocity
activatePhysics() {
if (this.isPhysicsActive) return;
this.isPhysicsActive = true;
this.updatePositionFromDOM(); // Get current DOM position and dimensions
// Force position to the ground (100) and reset motion for a stable start
this.position.y = 100; // Snap avatar to the floor (bottom = 100)
this.velocity = new Vector2D(0, 0);
this.onGround = true; // Mark as grounded
this.isSwimming = false; // Not swimming initially
console.log('✅ Physics ACTIVATED. Avatar snapped to floor and motion reset.');
console.log(`🎮 Controls: Alt+Z (left), Alt+C (right), Alt+X (jump).`);
}
// Deactivate physics: stop motion, clear keys
deactivatePhysics() {
if (!this.isPhysicsActive) return;
this.isPhysicsActive = false;
this.keys = {};
this.velocity = new Vector2D(0, 0);
this.onGround = false;
this.isSwimming = false;
// Al desactivar, restaurar gravedad a original
this.gravity = this.originalGravity;
this.isAntiGravityActive = false;
this.isSpeedBoostActive = false;
this.currentSpeedBoostFactor = 1.0;
if(this.speedBoostTimeout) clearTimeout(this.speedBoostTimeout);
console.log('❌ Physics DEACTIVATED. Avatar released from control.');
}
// Main physics update loop (called continuously by menuController)
update() {
if (!this.isPhysicsActive) return;
// Apply gravity (reduced in water)
if (this.isSwimming) {
this.velocity.y += this.gravity * 0.3;
} else {
this.velocity.y += this.gravity;
}
// Handle input
const currentMoveForce = this.moveForce * this.currentSpeedBoostFactor;
if (this.keys['left']) this.velocity.x -= currentMoveForce * (this.isSwimming ? 0.15 : 0.1);
if (this.keys['right']) this.velocity.x += currentMoveForce * (this.isSwimming ? 0.15 : 0.1);
if (this.keys['jump'] && (this.onGround || this.isSwimming)) {
this.velocity.y = -this.jumpForce * (this.isSwimming ? 0.7 : 1.0);
this.onGround = false;
}
// Apply friction (increased in water)
this.velocity.x *= (1 - (this.isSwimming ? 0.15 : this.friction));
if (Math.abs(this.velocity.x) < 0.01) this.velocity.x = 0;
// Calculate next position
let nextX = this.position.x + (this.velocity.x * 0.1);
let nextY = this.position.y + (this.velocity.y * 0.1);
// **APLICAR LÍMITES INTELIGENTES ANTES DE RESOLVER COLISIONES**
if (this.boundaryManager) {
this.boundaryManager.updateCanvasBounds(); // Asegurar que los límites estén actualizados
const bounds = this.boundaryManager.realTimeBounds;
// Restringir X con límites inteligentes
nextX = Math.max(bounds.minX, Math.min(bounds.maxX, nextX));
// Restringir Y con límites inteligentes
nextY = Math.max(bounds.minY, Math.min(bounds.maxY, nextY));
}
// Check and resolve collisions
this.handleCollisions(nextX, nextY);
// Send new position to Drawaria if changed significantly
if (this.hasPositionChanged()) {
this.sendPositionToDrawaria();
}
}
// Handle all types of collisions based on pixel checks
handleCollisions(nextX, nextY) {
const collisions = this.checkAvatarCollisions();
// Obtener el tipo de superficie en el centro o en la parte inferior si no hay centro específico
// Prioridad: centro, luego bottom
let primaryCollision = null;
if (collisions.center && this.surfaceColors[collisions.center.type]) {
primaryCollision = collisions.center;
} else if (collisions.bottom && this.surfaceColors[collisions.bottom.type]) {
primaryCollision = collisions.bottom;
}
// Reiniciar efectos que no son persistentes
if (!primaryCollision || primaryCollision.type !== 'speed_boost') {
if (this.isSpeedBoostActive) {
clearTimeout(this.speedBoostTimeout);
this.isSpeedBoostActive = false;
this.currentSpeedBoostFactor = 1.0;
console.log('💨 Speed Boost terminado/reseteado.');
}
}
if (!primaryCollision || primaryCollision.type !== 'anti_gravity') {
if (this.isAntiGravityActive) {
this.gravity = this.originalGravity;
this.isAntiGravityActive = false;
console.log('⬇️ Gravedad normal restablecida.');
}
}
if (!primaryCollision || primaryCollision.type !== 'water') {
if (this.isSwimming) {
console.log('🏊♂️ Exited water.');
this.isSwimming = false;
}
}
if (primaryCollision) {
switch (primaryCollision.type) {
case 'solid':
// Comportamiento por defecto, ya manejado
break;
case 'bounce':
// Comportamiento por defecto, ya manejado en resolvedY
break;
case 'speed_boost':
if (!this.isSpeedBoostActive) {
this.isSpeedBoostActive = true;
this.currentSpeedBoostFactor = 1.8; // Aumenta la velocidad en un 80%
console.log('⚡ Speed Boost activado!');
// Reiniciar speed boost si se pisa de nuevo antes de que termine
clearTimeout(this.speedBoostTimeout);
this.speedBoostTimeout = setTimeout(() => {
this.isSpeedBoostActive = false;
this.currentSpeedBoostFactor = 1.0;
console.log('💨 Speed Boost terminado.');
}, this.speedBoostDuration);
}
break;
case 'water':
if (!this.isSwimming) console.log('🏊♂️ Entered water.');
this.isSwimming = true;
break;
case 'anti_gravity':
if (!this.isAntiGravityActive) {
this.gravity = -0.8; // Gravedad invertida para ir hacia arriba
this.isAntiGravityActive = true;
this.velocity.y -= 5; // Un pequeño empuje inicial hacia arriba
console.log('⬆️ Gravedad invertida activada!');
}
break;
case 'normal_gravity':
if (this.isAntiGravityActive) {
this.gravity = this.originalGravity;
this.isAntiGravityActive = false;
console.log('⬇️ Gravedad normal restablecida.');
}
break;
case 'push_back':
if (this.onGround) { // Solo empujar si está en el suelo o moviéndose
if (this.velocity.x === 0) { // Si está quieto, empujar en una dirección aleatoria
this.velocity.x = (Math.random() > 0.5 ? 1 : -1) * this.pushBackForce;
} else { // Si se mueve, empujar en la dirección opuesta
this.velocity.x = -Math.sign(this.velocity.x) * this.pushBackForce;
}
this.velocity.y = -5; // Un pequeño salto al ser empujado
this.onGround = false;
console.log('↩️ Empuje hacia atrás activado!');
}
break;
case 'death':
// Ya manejado al principio, pero por si acaso
console.log('💀 Avatar tocó zona de MUERTE - ¡Reseteando!');
this.reset();
return;
default:
// Para cualquier hitbox personalizado sin lógica específica aún
console.log(`ℹ️ Colisión con hitbox tipo '${primaryCollision.type}'`);
break;
}
}
// Resolve X-axis movement
let resolvedX = nextX;
if ((this.keys['left'] && collisions.left && collisions.left.type === 'solid') ||
(this.keys['right'] && collisions.right && collisions.right.type === 'solid')) {
this.velocity.x = 0; // Stop horizontal movement
resolvedX = this.position.x; // Stay at current X
}
this.position.x = resolvedX; // Ya está limitado por boundaries del update()
// Resolve Y-axis movement
let resolvedY = nextY;
if (this.velocity.y > 0) { // Moving downwards (falling or landing)
if (collisions.bottom && (collisions.bottom.type === 'solid' || collisions.bottom.type === 'bounce')) {
this.velocity.y = 0; // Stop vertical motion
resolvedY = this.position.y; // Snap to current Y if hit (avoid falling through)
this.onGround = true; // Mark as grounded
// Handle bounce surface
if (collisions.bottom.type === 'bounce' && Date.now() - this.lastBounceSurface > 200) { // Small cooldown
console.log('🟡 Hit bounce pad!');
this.velocity.y = -this.jumpForce * 1.5; // Stronger bounce
this.onGround = false;
this.lastBounceSurface = Date.now();
}
}
} else if (this.velocity.y < 0) { // Moving upwards (jumping)
if (collisions.top && collisions.top.type === 'solid') {
this.velocity.y = 0; // Stop vertical motion (hit ceiling)
resolvedY = this.position.y; // Snap to current Y
}
}
this.position.y = resolvedY; // Ya está limitado por boundaries del update()
// **VERIFICACIÓN FINAL DE SUELO INTELIGENTE**
if (this.boundaryManager) {
const maxY = this.boundaryManager.realTimeBounds.maxY;
if (this.position.y >= maxY - this.boundaryManager.gameUnitFloorTolerance) { // Tolerancia para asegurar el snap
this.position.y = maxY;
this.velocity.y = 0;
this.onGround = true;
} else if (this.position.y < maxY - this.boundaryManager.gameUnitFloorTolerance && collisions.bottom && collisions.bottom.type !== 'solid' && primaryCollision?.type !== 'water') {
// Si no está en el suelo y no hay colisión sólida debajo, y no está en agua, no está en el suelo
this.onGround = false;
}
} else { // Fallback si boundaryManager no está listo
if (this.position.y >= 99.5) { // Si muy cerca de 100 (suelo)
this.position.y = 100; // Snap precisamente al suelo
this.velocity.y = 0;
this.onGround = true;
} else if (!collisions.bottom || collisions.bottom.type === 'water') {
this.onGround = false;
}
}
}
// Checks a pixel color at a specific game coordinate (0-100) with optional game offsets
checkPixelCollision(gameX, gameY, gameOffsetX = 0, gameOffsetY = 0) {
if (!drawariaCanvas || !drawariaCtx || !this.gameAreaElement) return null;
try {
const effectiveGameX = gameX + gameOffsetX;
const effectiveGameY = gameY + gameOffsetY;
const canvasWidthPx = drawariaCanvas.width;
const canvasHeightPx = drawariaCanvas.height;
// Convert combined game coordinates to canvas pixel coordinates
const pixelX = Math.floor((effectiveGameX / 100) * canvasWidthPx);
const pixelY = Math.floor((effectiveGameY / 100) * canvasHeightPx);
// Ensure within canvas bounds
if (pixelX < 0 || pixelX >= canvasWidthPx || pixelY < 0 || pixelY >= canvasHeightPx) {
return null;
}
const imageData = drawariaCtx.getImageData(pixelX, pixelY, 1, 1);
const pixelData = imageData.data;
const r = pixelData[0];
const g = pixelData[1];
const b = pixelData[2];
const a = pixelData[3];
if (a < 100) return null; // Treat mostly transparent pixels as no collision
for (const typeKey in this.surfaceColors) {
const colorData = this.surfaceColors[typeKey];
const tolerance = 40; // Increased tolerance for color variations
if (Math.abs(r - colorData.r) < tolerance &&
Math.abs(g - colorData.g) < tolerance &&
Math.abs(b - colorData.b) < tolerance) {
return { color: colorData.hex, type: colorData.type, r, g, b };
}
}
return null; // No special color found (treat as non-collidable or background)
} catch (error) {
// This can happen if canvas is not yet ready or cross-origin
// console.warn('Error in pixel detection:', error);
return null;
}
}
// Checks multiple points around the avatar for collisions
checkAvatarCollisions() {
const collisions = { bottom: null, top: null, left: null, right: null, center: null };
const halfWidth = this.avatarDimensions.width / 2;
const height = this.avatarDimensions.height;
const checkOffset = 0.5; // Small offset to check just outside the avatar boundary
// Bottom-center (for ground and bounce pads)
collisions.bottom = this.checkPixelCollision(this.position.x, this.position.y + checkOffset);
// Top-center (for ceiling collision)
collisions.top = this.checkPixelCollision(this.position.x, this.position.y - height - checkOffset);
// Mid-height left/right (for side walls)
const midY = this.position.y - (height / 2);
collisions.left = this.checkPixelCollision(this.position.x - halfWidth - checkOffset, midY);
collisions.right = this.checkPixelCollision(this.position.x + halfWidth + checkOffset, midY);
// Center (for water/death zones that affect the whole avatar)
collisions.center = this.checkPixelCollision(this.position.x, midY);
return collisions;
}
hasPositionChanged() {
const threshold = 0.1; // Minimum change threshold to send command, reducido para más frecuencia
return Math.abs(this.position.x - this.lastSentPosition.x) > threshold ||
Math.abs(this.position.y - this.lastSentPosition.y) > threshold;
}
// Sends the physics position to Drawaria's avatar system
sendPositionToDrawaria() {
const targetX = this.position.x;
const targetYBottom = this.position.y; // This is the BOTTOM-Y in our physics model
let adjustedDisplayY;
if (this.avatarDimensions.height > 0) {
// Adjust Y to be the CENTER of the avatar, as Drawaria usually expects center or top-left
// (Bottom Y - Half Avatar Height)
adjustedDisplayY = targetYBottom - (this.avatarDimensions.height / 2);
} else {
// Fallback estimate if dimensions not yet acquired
adjustedDisplayY = targetYBottom - 6; // Rough estimate for center if avatar is 12 units high
}
// Ensure adjusted Y stays within 0-100 game unit bounds
adjustedDisplayY = Math.max(0, Math.min(100, adjustedDisplayY));
// SIEMPRE actualizar elementos visuales PRIMERO (no como fallback)
this.updateVisualElements(targetX, adjustedDisplayY); // Pasa adjustedDisplayY que es el centro
const socket = this.getAnyAvailableSocket();
// Try window.game.sendMove first
if (window.game && typeof window.game.sendMove === "function") {
try {
window.game.sendMove(targetX, adjustedDisplayY);
this.lastSentPosition = { x: targetX, y: targetYBottom }; // Store physics position
return;
} catch (error) {
console.warn('Error with game.sendMove, trying WebSocket fallback:', error);
}
}
// Fallback to WebSocket command
if (socket) {
try {
const encodedPos = 1e4 * Math.floor((targetX / 100) * 1e4) + Math.floor((adjustedDisplayY / 100) * 1e4);
socket.send(`42["clientcmd",103,[${encodedPos},false]]`);
this.lastSentPosition = { x: targetX, y: targetYBottom };
return;
} catch (error) {
console.warn('Error sending movement command via WebSocket:', error);
}
}
}
getAnyAvailableSocket() {
if (drawariaSocket && drawariaSocket.readyState === WebSocket.OPEN) { return drawariaSocket; }
if (window._io && window._io.socket && window._io.socket.readyState === WebSocket.OPEN) {
drawariaSocket = window._io.socket; return drawariaSocket;
}
for (const prop in window) {
try { if (window[prop] instanceof WebSocket && window[prop].readyState === WebSocket.OPEN && window[prop].url.includes('drawaria')) {
drawariaSocket = window[prop]; return drawariaSocket; }
} catch (e) { /* Access denied */ }
}
return null;
}
// Update local visual elements (brush cursor, avatar DOM element)
updateVisualElements(targetX, targetYCenter) { // targetYCenter es la posición Y del centro del avatar
// PRIORIZAR el canvas overlay si está disponible, sino gameAreaElement
const effectiveGameArea = this.overlayCanvas || this.gameAreaElement;
if (!effectiveGameArea) {
console.warn('No game area or overlay canvas available for visual updates');
return;
}
const gameRect = effectiveGameArea.getBoundingClientRect();
const displayWidth = gameRect.width;
const displayHeight = gameRect.height; // Altura total del área de visualización
// Calcula la altura base del área jugable (altura total - offset UI inferior detectado)
const basePlayableHeightPx = Math.max(1, displayHeight - this.bottomUIOffsetPx);
// Aplica el offset visual de píxeles controlado por el usuario para el suelo
const finalPlayableHeightPx = basePlayableHeightPx - this.visualFloorOffsetPixels;
// Usa dimensiones estimadas si las reales no están disponibles
const effectiveAvatarGameHeight = this.avatarDimensions.height > 0 ? this.avatarDimensions.height : 12; // Fallback a 12 unidades de juego
const effectiveAvatarGameWidth = this.avatarDimensions.width > 0 ? this.avatarDimensions.width : 8; // Fallback a 8 unidades de juego
// Calcula las dimensiones del avatar en píxeles, basado en la altura total de visualización
const avatarPixelWidth = (effectiveAvatarGameWidth / 100) * displayWidth;
const avatarPixelHeight = (effectiveAvatarGameHeight / 100) * displayHeight;
// Mapea la unidad Y del juego (targetYCenter, 0-100) al centro en píxeles del área jugable final.
let avatarCenterPixelY = (targetYCenter / 100) * finalPlayableHeightPx;
// Luego, calcula la posición en píxeles para la PARTE SUPERIOR del avatar.
let pixelYTop = avatarCenterPixelY - (avatarPixelHeight / 2);
// Cálculo de la posición X
const pixelX = (targetX / 100) * displayWidth - (avatarPixelWidth / 2); // targetX es el centro
// Aplicar límites para los elementos visuales (asegura que el avatar está dentro de finalPlayableHeightPx)
const constrainedPixelX = Math.max(0, Math.min(pixelX, displayWidth - avatarPixelWidth));
const constrainedPixelYTop = Math.max(0, Math.min(pixelYTop, finalPlayableHeightPx - avatarPixelHeight));
// Actualizar brush cursor
const brushCursor = document.querySelector('.brushcursor');
if (brushCursor) {
brushCursor.classList.remove('brushcursor-hidden');
brushCursor.style.transform = `translate(${constrainedPixelX}px, ${constrainedPixelYTop}px)`;
brushCursor.style.display = 'block';
brushCursor.style.transition = 'transform 0.05s ease-out';
}
if (this.myAvatarElement) {
this.myAvatarElement.style.transform = `translate(${constrainedPixelX}px, ${constrainedPixelYTop}px)`;
this.myAvatarElement.style.position = 'absolute';
this.myAvatarElement.style.zIndex = '100';
this.myAvatarElement.style.transition = 'transform 0.05s ease-out';
this.myAvatarElement.offsetHeight; // Forzar un repaint
}
}
reset() {
// Reset to current X, but force Y to ground (100) and zero velocity
this.position.x = this.position.x; // Keep current X
this.position.y = 100; // Force to ground
this.velocity = new Vector2D(0, 0);
this.onGround = true;
this.isSwimming = false;
// Al resetear, restaurar gravedad a original
this.gravity = this.originalGravity;
this.isAntiGravityActive = false;
this.isSpeedBoostActive = false;
this.currentSpeedBoostFactor = 1.0;
if(this.speedBoostTimeout) clearTimeout(this.speedBoostTimeout);
console.log('🔄 Avatar reseteado a posición actual y suelo.');
}
boost() {
if (!this.isPhysicsActive) return;
this.velocity.y = -this.jumpForce * 2;
this.onGround = false;
console.log('🚀 Boost aplicado');
}
destroy() {
if (this.detectionInterval) {
clearInterval(this.detectionInterval);
}
if (this.animationShield) { // Destruir AnimationShield
this.animationShield.destroy();
}
if (this.boundaryManager) { // Destruir BoundaryManager
this.boundaryManager.destroy();
}
}
}
// ======================================
// CLASE PARA PROTEGER CONTROLES LOCALES DE ANIMACIONES DE SERVIDOR
// ======================================
class AnimationShield {
constructor(avatar) {
this.avatar = avatar;
this.observer = null;
this.reapplyInterval = null; // Para re-aplicar constantemente si es necesario
this.setupProtection();
console.log('🛡️ AnimationShield activado.');
}
setupProtection() {
// Monitorear continuamente el avatar element para protegerlo
this.startMonitoringAvatar();
}
startMonitoringAvatar() {
if (this.avatar.myAvatarElement) {
this.protectElement(this.avatar.myAvatarElement);
}
// Si el avatar element no está disponible al principio, inténtalo periódicamente
this.reapplyInterval = setInterval(() => {
if (this.avatar.myAvatarElement && !this.observer) {
this.protectElement(this.avatar.myAvatarElement);
}
}, 500); // Comprobar cada 0.5 segundos
}
protectElement(element) {
if (this.observer) this.observer.disconnect(); // Desconectar si ya está observando
this.observer = new MutationObserver((mutations) => {
if (!this.avatar.isPhysicsActive) return; // Solo proteger si la física está activa
mutations.forEach((mutation) => {
if (mutation.type === 'attributes' && mutation.attributeName === 'style') {
const style = element.style;
// Si detectamos una animación o transición que no es nuestra, la forzamos a detenerse
if (style.animation || style.transition !== 'transform 0.05s ease-out') {
this.observer.disconnect(); // Desconectar temporalmente para evitar loops
style.animation = '';
style.transition = 'transform 0.05s ease-out'; // Restaurar nuestra transición
// Reconectar observer después de un breve momento
setTimeout(() => {
if (this.observer) {
this.observer.observe(element, { attributes: true, attributeFilter: ['style'] });
}
}, 0);
}
// Asegurar propiedades de posicionamiento
if (style.position !== 'absolute') style.position = 'absolute';
if (style.zIndex !== '100') style.zIndex = '100';
if (style.pointerEvents !== 'auto') style.pointerEvents = 'auto';
}
});
});
this.observer.observe(element, { attributes: true, attributeFilter: ['style'] });
console.log('👁️ Monitoring avatar element style for interference.');
}
destroy() {
if (this.observer) {
this.observer.disconnect();
this.observer = null;
}
if (this.reapplyInterval) {
clearInterval(this.reapplyInterval);
this.reapplyInterval = null;
}
console.log('🛡️ AnimationShield desactivado y destruido.');
}
}
// ======================================
// CLASE PARA GESTIONAR LÍMITES DEL CANVAS Y COLISIONES
// ======================================
class BoundaryManager {
constructor(avatar) {
this.avatar = avatar;
this.canvasBounds = null;
this.realTimeBounds = {
minX: 0,
maxX: 100,
minY: 0,
maxY: 100
};
this.gameUnitFloorTolerance = 0.5; // Pequeña tolerancia para la detección del suelo de la física (unidades de juego 0-100)
this.updateInterval = null;
this.startRealTimeUpdates();
}
startRealTimeUpdates() {
// Actualización continua de límites en tiempo real
this.updateInterval = setInterval(() => {
this.updateCanvasBounds();
// La aplicación de los límites se hace en PhysicsAvatar.update() y handleCollisions()
}, 16); // ~60 FPS para detección suave
}
updateCanvasBounds() {
// Detectar canvas overlay principal, mainCanvas o gameArea
const overlayCanvas = document.getElementById('drawing-assistant-overlay');
const mainCanvas = document.getElementById('canvas');
const gameArea = document.getElementById('gamearea');
const activeElement = overlayCanvas || mainCanvas || gameArea;
if (activeElement) {
const rect = activeElement.getBoundingClientRect();
const style = window.getComputedStyle(activeElement);
// Obtener dimensiones reales considerando CSS y atributos
const realWidth = overlayCanvas ?
(overlayCanvas.width || parseFloat(style.width)) :
(activeElement.offsetWidth || rect.width);
const realHeight = overlayCanvas ?
(overlayCanvas.height || parseFloat(style.height)) :
(activeElement.offsetHeight || rect.height);
this.canvasBounds = {
element: activeElement,
rect: rect,
width: realWidth,
height: realHeight,
top: rect.top,
left: rect.left
};
// Calcular límites en coordenadas del juego (0-100)
this.calculateGameBounds();
}
}
calculateGameBounds() {
if (!this.canvasBounds || !this.avatar.myAvatarElement) return;
try {
// Dimensiones del avatar en unidades de juego (0-100)
// Usamos avatarDimensions del PhysicsAvatar que ya están en unidades de juego
const avatarWidthGameUnits = this.avatar.avatarDimensions.width > 0 ? this.avatar.avatarDimensions.width : 8;
const avatarHeightGameUnits = this.avatar.avatarDimensions.height > 0 ? this.avatar.avatarDimensions.height : 12;
// Límites inteligentes para el sistema de física (0-100 game units)
// minX/maxX para el CENTRO del avatar
this.realTimeBounds = {
minX: avatarWidthGameUnits / 2, // Centro no se salga por la izquierda
maxX: 100 - (avatarWidthGameUnits / 2), // Centro no se salga por la derecha
minY: avatarHeightGameUnits, // Para que el TOP del avatar no se salga por arriba
maxY: 100 - this.gameUnitFloorTolerance // El "suelo" de la física, ligeramente por encima de 100 para estabilidad
};
} catch (error) {
console.warn('Error calculando límites del juego:', error);
}
}
// Debugging visual
debugBounds() {
console.log('🔍 Información de límites:');
console.log('Canvas bounds:', this.canvasBounds);
console.log('Game bounds (0-100):', this.realTimeBounds);
console.log('Avatar current position (0-100):', this.avatar.position);
console.log('Avatar dimensions (0-100):', this.avatar.avatarDimensions);
console.log('Bottom UI Offset (px):', this.avatar.bottomUIOffsetPx);
console.log('Visual Floor Offset (px):', this.avatar.visualFloorOffsetPixels);
}
destroy() {
if (this.updateInterval) {
clearInterval(this.updateInterval);
this.updateInterval = null;
}
console.log('📐 BoundaryManager desactivado y destruido.');
}
}
// ======================================
// MENÚ PRINCIPAL CON PESTAÑAS
// ======================================
class PhysicsMenuController {
constructor() {
this.avatar = null;
this.isRunning = false; // Controlled by menu buttons
this.environmentDrawn = false;
this.autoMovement = false;
this.autoJump = false;
this.boundaryBounce = false;
// Nuevas propiedades para la sección de Hitbox Colors
this.hitboxColorMap = {}; // Almacena los colores HEX actuales para cada tipo
this.customHitboxTypes = []; // Para los tipos de hitbox personalizados
this.nextCustomHitboxId = 0;
this.selectedHitboxType = 'solid'; // Tipo de hitbox actualmente seleccionado para cambiar su color
this.initHitboxColors(); // Inicializar el mapa de colores al inicio
this.setupEventListeners();
this.startStatusUpdates();
this.startUpdateLoop(); // Physics loop always running, but avatar.update() is guarded by isPhysicsActive
}
// NUEVO: Inicializar los colores de los hitboxes
initHitboxColors() {
for (const key in DEFAULT_HITBOX_TYPES) {
const typeInfo = DEFAULT_HITBOX_TYPES[key];
this.hitboxColorMap[key] = STANDARD_COLORS[typeInfo.colorKey].hex;
}
}
// NUEVO: Aplicar los colores actuales al PhysicsAvatar
applyHitboxColorsToAvatar() {
if (!this.avatar) return;
this.avatar.surfaceColors = {}; // Limpiar los anteriores
for (const typeKey in this.hitboxColorMap) {
const hex = this.hitboxColorMap[typeKey];
const rgb = hexToRgb(hex);
this.avatar.surfaceColors[typeKey] = {
r: rgb.r, g: rgb.g, b: rgb.b,
type: typeKey, // El tipo de interacción es la misma key
hex: hex
};
}
console.log('🎨 Colores de Hitbox aplicados al avatar:', this.avatar.surfaceColors);
}
// MODIFICAR initPhysicsSystem para aplicar los colores iniciales
initPhysicsSystem() {
if (this.avatar) return;
this.avatar = new PhysicsAvatar();
this.applyHitboxColorsToAvatar(); // Aplicar colores al avatar recién creado
console.log('🎮 Physics Avatar system initialized. Physics are currently DEACTIVATED.');
this.updateSliders();
}
// NUEVO: Método para actualizar la UI del color seleccionado
updateSelectedColorInput() {
const colorInput = document.getElementById('hitbox-color-input');
if (colorInput && this.hitboxColorMap[this.selectedHitboxType]) {
colorInput.value = this.hitboxColorMap[this.selectedHitboxType];
}
const selectedTypeDisplay = document.getElementById('selected-hitbox-type-display');
if (selectedTypeDisplay) {
selectedTypeDisplay.textContent = DEFAULT_HITBOX_TYPES[this.selectedHitboxType]?.name ||
(this.customHitboxTypes.find(t => t.key === this.selectedHitboxType)?.name) ||
this.selectedHitboxType;
}
}
setupEventListeners() {
// Tab switching
document.querySelectorAll('#physics-menu .tab-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const targetTab = e.target.dataset.tab;
this.switchTab(targetTab);
});
});
// Control buttons
document.getElementById('start-physics').addEventListener('click', () => {
if (this.avatar) {
this.avatar.activatePhysics();
this.isRunning = true;
document.getElementById('physics-status').textContent = 'Activo';
console.log('🚀 Physics system started.');
} else { console.warn('Physics Avatar not initialized yet. Cannot start.'); }
});
document.getElementById('stop-physics').addEventListener('click', () => {
if (this.avatar) {
this.avatar.deactivatePhysics();
this.isRunning = false;
document.getElementById('physics-status').textContent = 'Detenido';
console.log('⏹️ Physics system stopped.');
} else { console.warn('Physics Avatar not initialized yet. Cannot stop.'); }
});
document.getElementById('reset-avatar').addEventListener('click', () => {
if (this.avatar) this.avatar.reset(); else console.warn('Physics Avatar not initialized yet. Cannot reset.');
});
document.getElementById('avatar-boost').addEventListener('click', () => {
if (this.avatar) this.avatar.boost(); else console.warn('Physics Avatar not initialized yet. Cannot boost.');
});
// Physics sliders - update if avatar is initialized
document.getElementById('gravity-slider').addEventListener('input', (e) => {
if (this.avatar) {
this.avatar.gravity = parseFloat(e.target.value);
this.avatar.originalGravity = parseFloat(e.target.value); // También actualizar la gravedad original
}
document.getElementById('gravity-display').textContent = parseFloat(e.target.value);
});
document.getElementById('move-force-slider').addEventListener('input', (e) => {
if (this.avatar) this.avatar.moveForce = parseFloat(e.target.value);
document.getElementById('move-force-display').textContent = parseFloat(e.target.value);
});
document.getElementById('jump-force-slider').addEventListener('input', (e) => {
if (this.avatar) this.avatar.jumpForce = parseFloat(e.target.value);
document.getElementById('jump-force-display').textContent = parseFloat(e.target.value);
});
document.getElementById('friction-slider').addEventListener('input', (e) => {
if (this.avatar) this.avatar.friction = parseFloat(e.target.value);
document.getElementById('friction-display').textContent = parseFloat(e.target.value);
});
document.getElementById('mass-slider').addEventListener('input', (e) => {
if (this.avatar) this.avatar.mass = parseFloat(e.target.value);
document.getElementById('mass-display').textContent = parseFloat(e.target.value);
});
// Preset buttons - apply if avatar is initialized
document.getElementById('preset-normal').addEventListener('click', () => { if (this.avatar) this.setPreset('normal'); });
document.getElementById('preset-heavy').addEventListener('click', () => { if (this.avatar) this.setPreset('heavy'); });
document.getElementById('preset-light').addEventListener('click', () => { if (this.avatar) this.setPreset('light'); });
document.getElementById('preset-bouncy').addEventListener('click', () => { if (this.avatar) this.setPreset('bouncy'); });
// NUEVO: Control de offset visual del suelo (ahora en píxeles)
document.getElementById('floor-offset-slider').addEventListener('input', (e) => {
const newOffset = parseFloat(e.target.value); // Este es un valor en píxeles para ajustar visualmente
if (this.avatar) {
this.avatar.visualFloorOffsetPixels = newOffset;
}
document.getElementById('floor-offset-display').textContent = newOffset;
});
// NUEVO: Botón de debug
document.getElementById('debug-boundaries').addEventListener('click', () => {
if (this.avatar && this.avatar.boundaryManager) {
this.avatar.boundaryManager.debugBounds();
} else {
console.warn('BoundaryManager no está inicializado para debug.');
}
});
// Automation toggles
document.getElementById('auto-movement').addEventListener('change', (e) => {
this.autoMovement = e.target.checked;
});
document.getElementById('auto-jump').addEventListener('change', (e) => {
this.autoJump = e.target.checked;
});
document.getElementById('boundary-bounce').addEventListener('change', (e) => {
this.boundaryBounce = e.target.checked;
});
// NUEVO: Event listeners para Hitbox Colors
document.getElementById('hitbox-color-input').addEventListener('input', (e) => {
const newHex = e.target.value;
this.hitboxColorMap[this.selectedHitboxType] = newHex;
this.updateHitboxColorSwatch(this.selectedHitboxType, newHex); // Actualizar el swatch
this.applyHitboxColorsToAvatar(); // Aplicar al avatar inmediatamente
});
// Event listeners para los botones de selección de hitbox (estos se crean dinámicamente)
// Usamos delegación de eventos para los botones de selección
document.getElementById('tab-hitbox-colors').addEventListener('click', (e) => {
if (e.target.closest('.select-hitbox-type')) { // Usar closest para el botón padre
const btn = e.target.closest('.select-hitbox-type');
const type = btn.dataset.hitboxType;
this.selectedHitboxType = type;
this.updateSelectedColorInput();
document.querySelectorAll('#tab-hitbox-colors .select-hitbox-type').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
}
});
document.getElementById('stop-automation').addEventListener('click', () => this.stopAutomation());
}
switchTab(targetTab) {
document.querySelectorAll('#physics-menu .tab-btn').forEach(btn => btn.classList.remove('active'));
document.querySelectorAll('#physics-menu .tab-panel').forEach(panel => panel.classList.remove('active'));
document.querySelector(`#physics-menu [data-tab="${targetTab}"]`).classList.add('active');
document.getElementById(`tab-${targetTab}`).classList.add('active');
// Si cambiamos a la pestaña de hitboxes, actualizar su UI
if (targetTab === 'hitbox-colors') {
this.populateHitboxColorsUI();
}
}
startPhysics() { // This method is now called by the menu button click, and activates Avatar physics
if (this.isRunning) return;
if (!this.avatar) {
console.warn('Physics Avatar not yet available to start physics. Please wait for initialization.');
return;
}
this.avatar.activatePhysics();
this.isRunning = true;
document.getElementById('physics-status').textContent = 'Activo';
console.log('🚀 Physics system started via menu button.');
}
stopPhysics() { // This method is now called by the menu button click, and deactivates Avatar physics
if (!this.isRunning) return;
if (!this.avatar) {
console.warn('Physics Avatar not available. Cannot stop.');
return;
}
this.avatar.deactivatePhysics();
this.isRunning = false;
document.getElementById('physics-status').textContent = 'Detenido';
console.log('⏹️ Physics system stopped via menu button.');
}
setPreset(preset) {
if (!this.avatar) return;
switch(preset) {
case 'normal':
this.avatar.gravity = 0.8; this.avatar.moveForce = 15; this.avatar.jumpForce = 20;
this.avatar.friction = 0.1; this.avatar.mass = 1.0; break;
case 'heavy':
this.avatar.gravity = 1.2; this.avatar.moveForce = 8; this.avatar.jumpForce = 15;
this.avatar.friction = 0.15; this.avatar.mass = 3.0; break;
case 'light':
this.avatar.gravity = 0.3; this.avatar.moveForce = 25; this.avatar.jumpForce = 30;
this.avatar.friction = 0.05; this.avatar.mass = 0.3; break;
case 'bouncy':
this.avatar.gravity = 0.6; this.avatar.moveForce = 20; this.avatar.jumpForce = 35;
this.avatar.friction = 0.02; this.avatar.mass = 0.8; break;
}
this.avatar.originalGravity = this.avatar.gravity; // Asegurar que la gravedad original también se actualice
this.updateSliders();
console.log(`🎯 Preset applied: ${preset}`);
}
updateSliders() {
if (!this.avatar) return;
document.getElementById('gravity-slider').value = this.avatar.gravity;
document.getElementById('gravity-display').textContent = this.avatar.gravity;
document.getElementById('move-force-slider').value = this.avatar.moveForce;
document.getElementById('move-force-display').textContent = this.avatar.moveForce;
document.getElementById('jump-force-slider').value = this.avatar.jumpForce;
document.getElementById('jump-force-display').textContent = this.avatar.jumpForce;
document.getElementById('friction-slider').value = this.avatar.friction;
document.getElementById('friction-display').textContent = this.avatar.friction;
document.getElementById('mass-slider').value = this.avatar.mass;
document.getElementById('mass-display').textContent = this.avatar.mass;
}
async drawPlatform() {
if (!drawariaCanvas) { console.warn('Cannot draw: Drawaria canvas not detected yet.'); return; }
const x = 100 + Math.random() * (drawariaCanvas.width - 300);
const y = 200 + Math.random() * (drawariaCanvas.height - 400);
await drawRectangle(x, y, 150, 15, this.selectedColor, this.selectedThickness);
console.log(`🏗️ Plataforma dibujada en (${x}, ${y})`);
}
async drawCircleObstacle() {
if (!drawariaCanvas) { console.warn('Cannot draw: Drawaria canvas not detected yet.'); return; }
const x = 100 + Math.random() * (drawariaCanvas.width - 200);
const y = 100 + Math.random() * (drawariaCanvas.height - 200);
const radius = 20 + Math.random() * 30;
await drawCircle(x, y, radius, this.selectedColor, this.selectedThickness);
console.log(`⭕ Círculo dibujado en (${x}, ${y}) con radio ${radius}`);
}
async drawEnvironment() {
if (this.environmentDrawn) { console.log('🌍 Entorno ya dibujado. Ignorando.'); return; }
if (!drawariaCanvas) { console.warn('Cannot draw: Drawaria canvas not detected yet for environment.'); return; }
console.log('🌍 Dibujando entorno completo...');
await drawRectangle(50, drawariaCanvas.height - 30, drawariaCanvas.width - 100, 20, '#4a4a4a', 3);
await drawRectangle(100, drawariaCanvas.height - 150, 150, 15, '#6a6a6a', 2);
await drawRectangle(300, drawariaCanvas.height - 250, 120, 15, '#6a6a6a', 2);
await drawRectangle(500, drawariaCanvas.height - 180, 100, 15, '#6a6a6a', 2);
await drawCircle(200, drawariaCanvas.height - 100, 25, '#8a4a4a', 2);
await drawCircle(450, drawariaCanvas.height - 120, 20, '#8a4a4a', 2);
this.environmentDrawn = true;
console.log('✅ Entorno dibujado completamente');
}
clearCanvas() {
if (drawariaSocket && drawariaSocket.readyState === WebSocket.OPEN) {
drawariaSocket.send('42["drawcmd",1,[]]'); // Command to clear canvas on Drawaria server
this.environmentDrawn = false;
console.log('🗑️ Canvas cleared.');
} else {
console.warn('❌ WebSocket not open. Cannot send clear canvas command.');
}
}
startDemoMode() {
this.autoMovement = true;
document.getElementById('auto-movement').checked = true;
if (this.avatar) this.startPhysics();
console.log('🎪 Demo Mode Activated.');
}
startChaosMode() {
if (!this.avatar) return;
this.avatar.gravity = Math.random() * 2; this.avatar.moveForce = 10 + Math.random() * 20;
this.avatar.jumpForce = 15 + Math.random() * 25;
this.autoMovement = true;
document.getElementById('auto-movement').checked = true;
this.startPhysics();
this.updateSliders();
console.log('🌪️ Chaos Mode Activated.');
}
stopAutomation() {
this.autoMovement = false; this.autoJump = false; this.boundaryBounce = false;
document.getElementById('auto-movement').checked = false;
document.getElementById('auto-jump').checked = false;
document.getElementById('boundary-bounce').checked = false;
// Restablecer el offset visual del suelo
if (this.avatar) {
this.avatar.visualFloorOffsetPixels = 45; // Valor predeterminado de 5
}
document.getElementById('floor-offset-slider').value = 5;
document.getElementById('floor-offset-display').textContent = 5;
console.log('⏹️ Automation stopped.');
}
startUpdateLoop() {
const update = () => {
// This requestAnimationFrame loop is continuous
if (this.isRunning && this.avatar) {
this.avatar.update();
if (this.autoMovement) {
if (Math.random() < 0.02) {
this.avatar.keys['left'] = false; this.avatar.keys['right'] = false;
const randomMove = Math.random();
if (randomMove < 0.4) this.avatar.keys['left'] = true;
else if (randomMove < 0.8) this.avatar.keys['right'] = true;
}
}
if (this.autoJump && this.avatar.onGround && Math.random() < 0.05) {
this.avatar.keys['jump'] = true;
setTimeout(() => this.avatar.keys['jump'] = false, 100);
}
}
requestAnimationFrame(update);
};
update(); // Initial call to start the loop
}
startStatusUpdates() {
setInterval(() => {
const avatarPositionElem = document.getElementById('avatar-position');
const avatarGroundedElem = document.getElementById('avatar-grounded');
const socketStatusElem = document.getElementById('socket-status');
const velocityXElem = document.getElementById('velocity-x');
const velocityYElem = document.getElementById('velocity-y');
const canvasStatusElem = document.getElementById('canvas-status');
// Update UI based on avatar's state (even if not fully initialized, show N/A)
if (avatarPositionElem) avatarPositionElem.textContent = this.avatar ? `${this.avatar.position.x.toFixed(1)}, ${this.avatar.position.y.toFixed(1)}` : 'N/A';
if (avatarGroundedElem) {
let status = this.avatar ? (this.avatar.onGround ? 'Sí' : 'No') : 'N/A';
if (this.avatar && this.avatar.isSwimming) status += ' (Nadando)';
avatarGroundedElem.textContent = status;
} else if (avatarGroundedElem) {
avatarGroundedElem.textContent = 'N/A';
}
if (socketStatusElem) socketStatusElem.textContent = (drawariaSocket && drawariaSocket.readyState === WebSocket.OPEN) ? '✅ Conectado' : '❌ Desconectado';
if (velocityXElem) velocityXElem.textContent = this.avatar ? this.avatar.velocity.x.toFixed(2) : 'N/A';
if (velocityYElem) velocityYElem.textContent = this.avatar ? this.avatar.velocity.y.toFixed(2) : 'N/A';
if (canvasStatusElem) canvasStatusElem.textContent = drawariaCanvas ? '✅ Sí' : '❌ No';
}, 100);
}
// NUEVO: Método para poblar la UI de los colores de hitbox
populateHitboxColorsUI() {
const defaultHitboxesContainer = document.getElementById('default-hitboxes-container');
const customHitboxesContainer = document.getElementById('custom-hitboxes-container');
if (!defaultHitboxesContainer || !customHitboxesContainer) {
console.warn('Contenedores de hitbox no encontrados en el DOM.');
return;
}
defaultHitboxesContainer.innerHTML = '';
customHitboxesContainer.innerHTML = '';
for (const typeKey in DEFAULT_HITBOX_TYPES) {
const typeInfo = DEFAULT_HITBOX_TYPES[typeKey];
const currentHex = this.hitboxColorMap[typeKey];
defaultHitboxesContainer.innerHTML += `
<div class="hitbox-item">
<button class="select-hitbox-type ${this.selectedHitboxType === typeKey ? 'active' : ''}" data-hitbox-type="${typeKey}">
<span class="hitbox-swatch" style="background-color: ${currentHex};"></span>
<span class="hitbox-name">${typeInfo.name}</span>
<span class="hitbox-description">${typeInfo.description}</span>
</button>
</div>
`;
}
this.customHitboxTypes.forEach(customType => {
const currentHex = this.hitboxColorMap[customType.key];
customHitboxesContainer.innerHTML += `
<div class="hitbox-item">
<button class="select-hitbox-type ${this.selectedHitboxType === customType.key ? 'active' : ''}" data-hitbox-type="${customType.key}">
<span class="hitbox-swatch" style="background-color: ${currentHex};"></span>
<input type="text" class="custom-hitbox-name" value="${customType.name}" data-hitbox-key="${customType.key}" placeholder="Nombre de Hitbox">
<button class="remove-hitbox-type danger-btn" data-hitbox-key="${customType.key}">×</button>
</button>
</div>
`;
});
// Asignar event listeners para la gestión de custom hitboxes
this.addCustomHitboxEventListeners();
this.updateSelectedColorInput(); // Asegurar que el input de color refleje el tipo seleccionado
}
// NUEVO: Método para añadir un hitbox personalizado a la UI
addCustomHitboxUI() {
const newKey = `custom_${this.nextCustomHitboxId++}`;
const newName = `Custom ${this.nextCustomHitboxId}`;
const defaultColor = STANDARD_COLORS.gray.hex;
this.hitboxColorMap[newKey] = defaultColor;
this.customHitboxTypes.push({ key: newKey, name: newName, type: 'solid' }); // Por defecto como 'solid'
this.selectedHitboxType = newKey; // Seleccionar el nuevo hitbox
this.populateHitboxColorsUI(); // Regenerar la UI
this.applyHitboxColorsToAvatar(); // Aplicar cambios al avatar
console.log(`➕ Hitbox personalizado '${newName}' agregado.`);
}
// NUEVO: Método para manejar eventos de custom hitboxes
addCustomHitboxEventListeners() {
const customHitboxesContainer = document.getElementById('custom-hitboxes-container');
if (!customHitboxesContainer) return;
customHitboxesContainer.querySelectorAll('.remove-hitbox-type').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation(); // Prevenir que el clic se propague al botón padre .select-hitbox-type
const keyToRemove = e.target.dataset.hitboxKey;
this.removeCustomHitbox(keyToRemove);
});
});
customHitboxesContainer.querySelectorAll('.custom-hitbox-name').forEach(input => {
input.addEventListener('change', (e) => {
const keyToUpdate = e.target.dataset.hitboxKey;
const newName = e.target.value;
const customType = this.customHitboxTypes.find(t => t.key === keyToUpdate);
if (customType) {
customType.name = newName;
}
});
});
}
// NUEVO: Método para remover un hitbox personalizado
removeCustomHitbox(key) {
delete this.hitboxColorMap[key];
this.customHitboxTypes = this.customHitboxTypes.filter(type => type.key !== key);
this.populateHitboxColorsUI(); // Regenerar la UI
// Si el hitbox seleccionado fue el eliminado, seleccionar 'solid' por defecto
if (this.selectedHitboxType === key) {
this.selectedHitboxType = 'solid';
}
this.updateSelectedColorInput();
this.applyHitboxColorsToAvatar(); // Aplicar cambios al avatar
console.log(`🗑️ Hitbox '${key}' eliminado.`);
}
// NUEVO: Actualizar el color del "swatch"
updateHitboxColorSwatch(typeKey, hex) {
const swatch = document.querySelector(`.select-hitbox-type[data-hitbox-type="${typeKey}"] .hitbox-swatch`);
if (swatch) {
swatch.style.backgroundColor = hex;
}
}
}
// --- Core Logic for Menu and Physics Initialization ---
// 1. Create and show the menu container immediately (DOM only)
function createAndShowMenuContainer() {
if (document.getElementById('physics-menu')) {
console.log('✅ Menu container already exists in DOM. Skipping recreation.');
return;
}
const menu = document.createElement('div');
menu.id = 'physics-menu';
menu.innerHTML = `
<div class="menu-header">
<div class="menu-title">
<span class="menu-icon">🚀</span>
<span>Avatar Physics Control</span>
</div>
<div class="menu-controls">
<button id="menu-minimize" class="menu-btn">−</button>
<button id="menu-close" class="menu-btn">×</button>
</div>
</div>
<div class="menu-content">
<!-- Navigation Tabs -->
<div class="menu-tabs">
<button class="tab-btn active" data-tab="control">🎮 Control</button>
<button class="tab-btn" data-tab="physics">⚡ Física</button>
<button class="tab-btn" data-tab="hitbox-colors">🎨 Hitbox Colors</button>
<button class="tab-btn" data-tab="automation">🤖 Auto</button>
</div>
<!-- Tab Content -->
<div class="tab-panel active" id="tab-control">
<div class="control-section">
<h4>🎯 Control del Avatar</h4>
<div class="button-grid">
<button id="start-physics" class="action-btn primary">
<span class="btn-icon">▶️</span> Iniciar Física
</button>
<button id="stop-physics" class="action-btn secondary">
<span class="btn-icon">⏹️</span> Detener
</button>
<button id="reset-avatar" class="action-btn warning">
<span class="btn-icon">🔄</span> Reset Posición
</button>
<button id="avatar-boost" class="action-btn special">
<span class="btn-icon">🚀</span> Super Salto
</button>
</div>
<div class="status-display">
<div class="status-item">
<span class="status-label">Estado:</span>
<span id="physics-status" class="status-value">Detenido</span>
</div>
<div class="status-item">
<span class="status-label">Posición:</span>
<span id="avatar-position" class="status-value">N/A</span>
</div>
<div class="status-item">
<span class="status-label">En suelo:</span>
<span id="avatar-grounded" class="status-value">N/A</span>
</div>
</div>
</div>
<div class="controls-info">
<h4>🎯 Controles del Teclado</h4>
<div class="controls-list">
<div class="control-item">
<span class="key">Alt+Z</span>
<span class="desc">Mover izquierda</span>
</div>
<div class="control-item">
<span class="key">Alt+C</span>
<span class="desc">Mover derecha</span>
</div>
<div class="control-item">
<span class="key">Alt+X</span>
<span class="desc">Saltar</span>
</div>
</div>
</div>
</div>
<!-- Physics Tab -->
<div class="tab-panel" id="tab-physics">
<div class="physics-section">
<h4>⚡ Configuración Física</h4>
<div class="slider-group">
<label>Gravedad: <span id="gravity-display">0.8</span></label>
<input type="range" id="gravity-slider" min="0" max="2" step="0.1" value="0.8">
</div>
<div class="slider-group">
<label>Fuerza de Movimiento: <span id="move-force-display">15</span></label>
<input type="range" id="move-force-slider" min="5" max="30" step="1" value="15">
</div>
<div class="slider-group">
<label>Fuerza de Salto: <span id="jump-force-display">20</span></label>
<input type="range" id="jump-force-slider" min="10" max="40" step="1" value="20">
</div>
<div class="slider-group">
<label>Fricción: <span id="friction-display">0.1</span></label>
<input type="range" id="friction-slider" min="0" max="0.5" step="0.01" value="0.1">
</div>
<div class="slider-group">
<label>Masa: <span id="mass-display">1.0</span></label>
<input type="range" id="mass-slider" min="0.1" max="5" step="0.1" value="1.0">
</div>
<div class="preset-buttons">
<button id="preset-normal" class="preset-btn">🔧 Normal</button>
<button id="preset-heavy" class="preset-btn">🪨 Pesado</button>
<button id="preset-light" class="preset-btn">🪶 Liviano</button>
<button id="preset-bouncy" class="preset-btn">🏀 Saltarín</button>
</div>
<!-- NUEVO: Control de offset visual del suelo -->
<div class="slider-group">
<label>Offset Suelo Visual (px): <span id="floor-offset-display">5</span></label>
<input type="range" id="floor-offset-slider" min="-20" max="20" step="1" value="5">
</div>
<!-- NUEVO: Botón de debug -->
<div class="debug-controls">
<button id="debug-boundaries" class="preset-btn">🔍 Debug Límites</button>
</div>
</div>
</div>
<!-- Hitbox Colors Tab -->
<div class="tab-panel" id="tab-hitbox-colors">
<div class="hitbox-colors-section">
<h4>🎨 Colores y Efectos de Hitbox</h4>
<div class="color-picker-control">
<label>Editar Color para: <span id="selected-hitbox-type-display">Tocable</span></label>
<input type="color" id="hitbox-color-input" value="#000000">
</div>
<h5>Hitbox Types por Defecto</h5>
<div id="default-hitboxes-container" class="hitbox-grid">
<!-- Los hitboxes por defecto se generarán aquí -->
</div>
</div>
</div>
<!-- Automation Tab -->
<div class="tab-panel" id="tab-automation">
<div class="automation-section">
<h4>🤖 Automatización</h4>
<div class="automation-controls">
<div class="toggle-group">
<label class="toggle-label">
<input type="checkbox" id="auto-movement">
<span class="toggle-slider"></span>
<span class="toggle-text">Movimiento Automático</span>
</label>
</div>
<div class="toggle-group">
<label class="toggle-label">
<input type="checkbox" id="auto-jump">
<span class="toggle-slider"></span>
<span class="toggle-text">Salto Automático</span>
</label>
</div>
<div class="toggle-group">
<label class="toggle-label">
<input type="checkbox" id="boundary-bounce">
<span class="toggle-slider"></span>
<span class="toggle-text">Rebote en Bordes</span>
</label>
</div>
</div>
<div class="automation-presets">
<button id="stop-automation" class="auto-btn secondary">
<span class="btn-icon">⏹️</span> Detener Auto
</button>
</div>
</div>
</div>
<!-- Info Tab -->
`;
styleMenu(menu);
document.body.appendChild(menu);
console.log('✅ Menu DOM structure appended to body.');
// Setup immediate menu controls (minimize/close)
document.getElementById('menu-minimize').addEventListener('click', () => {
const content = document.querySelector('#physics-menu .menu-content');
const isHidden = content.style.display === 'none';
content.style.display = isHidden ? 'block' : 'none';
document.getElementById('menu-minimize').textContent = isHidden ? '−' : '+';
});
document.getElementById('menu-close').addEventListener('click', () => {
document.getElementById('physics-menu').style.display = 'none';
});
makeDraggable(menu);
}
// Apply CSS styles to the menu (with !important for overriding game styles)
function styleMenu(menu) {
const style = document.createElement('style');
style.textContent = `
#physics-menu {
position: fixed !important;
top: 20px !important;
right: 20px !important;
left: auto !important;
bottom: auto !important;
width: 420px !important;
max-height: 85vh !important;
background: linear-gradient(145deg, #1e1e2e, #2d3748) !important;
border: 2px solid #4299e1 !important;
border-radius: 16px !important;
box-shadow: 0 20px 40px rgba(0,0,0,0.4) !important;
color: white !important;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif !important;
z-index: 2147483647 !important; /* Max z-index */
backdrop-filter: blur(10px) !important;
overflow: hidden !important;
display: block !important; /* Ensure it's visible */
opacity: 1 !important; /* Ensure full opacity */
visibility: visible !important; /* Ensure visibility */
pointer-events: auto !important; /* Ensure clicks are not blocked */
transform: none !important; /* Prevent any default transforms */
}
.menu-header {
display: flex; justify-content: space-between; align-items: center; padding: 16px 20px;
background: linear-gradient(90deg, #4299e1, #3182ce); cursor: grab; user-select: none !important;
} .menu-title { display: flex; align-items: center; gap: 8px; font-size: 16px; font-weight: bold; }
.menu-icon { font-size: 20px; } .menu-controls { display: flex; gap: 8px; }
.menu-btn { background: rgba(255,255,255,0.2); border: none; color: white; width: 28px; height: 28px;
border-radius: 6px; cursor: pointer; font-weight: bold; transition: background 0.3s; }
.menu-btn:hover { background: rgba(255,255,255,0.3); } .menu-content { max-height: 70vh; overflow-y: auto; }
.menu-tabs { display: flex; background: rgba(255,255,255,0.05); border-bottom: 1px solid rgba(255,255,255,0.1); }
.tab-btn { flex: 1; padding: 12px 8px; background: none; border: none; color: #a0aec0; cursor: pointer;
font-size: 11px; font-weight: 500; transition: all 0.3s; border-bottom: 3px solid transparent; }
.tab-btn:hover { background: rgba(255,255,255,0.05); color: white; }
.tab-btn.active { background: rgba(66,153,225,0.2); color: #4299e1; border-bottom-color: #4299e1; }
.tab-content { padding: 20px; } .tab-panel { display: none; } .tab-panel.active { display: block; }
.control-section, .physics-section, .hitbox-colors-section, .automation-section, .info-section { margin-bottom: 20px; }
h4 { margin: 0 0 15px 0; color: #4299e1; font-size: 14px; font-weight: bold; padding-bottom: 8px;
border-bottom: 1px solid rgba(66,153,225,0.3); }
h5 { margin: 15px 0 8px 0; color: #63b3ed; font-size: 13px; }
.button-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 20px; }
.action-btn { padding: 12px 16px; border: none; border-radius: 8px; cursor: pointer; font-weight: bold;
font-size: 12px; transition: all 0.3s; display: flex; align-items: center; justify-content: center; gap: 8px; }
.action-btn.primary { background: linear-gradient(145deg, #48bb78, #38a169); color: white; }
.action-btn.secondary { background: linear-gradient(145deg, #718096, #4a5568); color: white; }
.action-btn.warning { background: linear-gradient(145deg, #ed8936, #dd6b20); color: white; }
.action-btn.special { background: linear-gradient(145deg, #9f7aea, #805ad5); color: white; }
.action-btn:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.3); }
.status-display { background: rgba(255,255,255,0.05); padding: 15px; border-radius: 8px; margin-bottom: 20px; }
.status-item { display: flex; justify-content: space-between; margin-bottom: 8px; }
.status-label { color: #a0aec0; font-size: 12px; }
.status-value { color: #4299e1; font-weight: bold; font-size: 12px; }
.controls-info { background: rgba(255,255,255,0.03); padding: 15px; border-radius: 8px; }
.controls-list { display: flex; flex-direction: column; gap: 8px; }
.control-item { display: flex; justify-content: space-between; align-items: center; }
.key { background: #4299e1; color: white; padding: 4px 8px; border-radius: 4px; font-size: 10px; font-weight: bold; }
.desc { color: #a0aec0; font-size: 11px; } .slider-group { margin-bottom: 15px; }
.slider-group label { display: block; margin-bottom: 8px; color: #e2e8f0; font-size: 12px; font-weight: 500; }
.slider-group input[type="range"] { width: 100%; height: 6px; border-radius: 3px; background: #4a5568; outline: none; -webkit-appearance: none; }
.slider-group input[type="range"]::-webkit-slider-thumb { appearance: none; width: 18px; height: 18px; border-radius: 50%; background: #4299e1; cursor: pointer; }
.preset-buttons { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 15px; }
.preset-btn { padding: 10px; background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.2);
border-radius: 6px; color: white; cursor: pointer; font-size: 11px; transition: all 0.3s; }
.preset-btn:hover { background: rgba(255,255,255,0.2); transform: translateY(-1px); }
.debug-controls { margin-top: 15px; } /* Estilo adicional para los controles de debug */
/* Estilos para la nueva sección de Hitbox Colors */
.hitbox-colors-section { padding: 20px; }
.color-picker-control { margin-bottom: 20px; display: flex; align-items: center; gap: 10px; }
.color-picker-control label { color: #e2e8f0; font-size: 12px; font-weight: 500; }
.color-picker-control #selected-hitbox-type-display { color: #4299e1; font-weight: bold; }
.color-picker-control input[type="color"] { width: 40px; height: 28px; border: none; border-radius: 4px; cursor: pointer; }
.hitbox-grid {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 15px;
}
.hitbox-item {
flex: 1 1 calc(50% - 8px); /* Dos columnas, con espacio entre ellas */
max-width: calc(50% - 8px);
min-width: 150px;
}
.hitbox-item button.select-hitbox-type {
background: rgba(255,255,255,0.08);
border: 1px solid rgba(255,255,255,0.1);
border-radius: 8px;
color: white;
cursor: pointer;
font-size: 11px;
padding: 8px 10px;
width: 100%;
text-align: left;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 8px;
}
.hitbox-item button.select-hitbox-type:hover {
background: rgba(255,255,255,0.15);
border-color: #4299e1;
}
.hitbox-item button.select-hitbox-type.active {
background: rgba(66,153,225,0.2);
border-color: #4299e1;
box-shadow: 0 0 8px rgba(66,153,225,0.4);
}
.hitbox-swatch {
width: 18px;
height: 18px;
border-radius: 4px;
border: 1px solid rgba(255,255,255,0.3);
display: inline-block;
flex-shrink: 0;
}
.hitbox-name {
font-weight: bold;
color: #e2e8f0;
}
.hitbox-description {
font-size: 10px;
color: #a0aec0;
margin-left: 8px;
flex-grow: 1; /* Para que la descripción ocupe el espacio restante */
}
.custom-hitbox-name {
background: rgba(255,255,255,0.1);
border: 1px solid rgba(255,255,255,0.2);
border-radius: 4px;
color: white;
padding: 4px 6px;
font-size: 11px;
flex-grow: 1;
margin-left: 5px;
margin-right: 5px;
}
.custom-hitbox-name:focus { outline: none; border-color: #4299e1; }
.remove-hitbox-type {
background: #e53e3e;
border: none;
color: white;
width: 20px;
height: 20px;
border-radius: 50%;
cursor: pointer;
font-size: 12px;
font-weight: bold;
flex-shrink: 0;
padding: 0;
line-height: 1;
}
.remove-hitbox-type:hover { background: #c53030; }
/* Scrollbar */
#physics-menu ::-webkit-scrollbar { width: 6px; }
#physics-menu ::-webkit-scrollbar-track { background: rgba(255,255,255,0.1); border-radius: 3px; }
#physics-menu ::-webkit-scrollbar-thumb { background: #4299e1; border-radius: 3px; }
#physics-menu ::-webkit-scrollbar-thumb:hover { background: #3182ce; }
`;
document.head.appendChild(style);
}
// Make the menu draggable
function makeDraggable(element) {
let isDragging = false;
let offsetX, offsetY;
const header = element.querySelector('.menu-header');
header.addEventListener('mousedown', (e) => {
isDragging = true;
const rect = element.getBoundingClientRect();
offsetX = e.clientX - rect.left;
offsetY = e.clientY - rect.top;
header.style.cursor = 'grabbing';
});
document.addEventListener('mousemove', (e) => {
if (isDragging) {
e.preventDefault();
let newX = e.clientX - offsetX;
let newY = e.clientY - offsetY;
const maxX = window.innerWidth - element.offsetWidth;
const maxY = window.innerHeight - element.offsetHeight;
newX = Math.max(0, Math.min(newX, maxX));
newY = Math.max(0, Math.min(newY, maxY));
element.style.left = `${newX}px`;
element.style.top = `${newY}px`;
element.style.right = 'auto';
element.style.bottom = 'auto';
}
});
document.addEventListener('mouseup', () => {
isDragging = false;
header.style.cursor = 'grab';
});
}
// Function to ensure the menu stays visible (called periodically)
function ensureMenuVisibility() {
const menu = document.getElementById('physics-menu');
if (menu) {
menu.style.setProperty('display', 'block', 'important');
menu.style.setProperty('opacity', '1', 'important');
menu.style.setProperty('visibility', 'visible', 'important');
menu.style.setProperty('pointer-events', 'auto', 'important');
}
}
// Initialize the core physics logic and menu functionality
function initializePhysicsAndMenu() {
// Attempt to get canvas immediately
drawariaCanvas = document.getElementById('canvas');
if (drawariaCanvas && !drawariaCtx) {
drawariaCtx = drawariaCanvas.getContext('2d');
console.log('🎨 Drawaria Canvas and Context detected.');
}
// Create the Menu Controller instance immediately if it doesn't exist
if (!menuControllerInstance) {
menuControllerInstance = new PhysicsMenuController();
console.log('🎮 PhysicsMenuController instance created. Waiting for manual activation.');
// Cargar la UI de hitboxes al inicio
menuControllerInstance.populateHitboxColorsUI();
}
// Initialize PhysicsAvatar within the controller if it hasn't been done yet.
// This will allow the PhysicsAvatar to start detecting the DOM avatar.
if (menuControllerInstance && !menuControllerInstance.avatar) {
menuControllerInstance.initPhysicsSystem();
}
}
// --- Entry point ---
// Create and show the menu structure as soon as the DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', createAndShowMenuContainer);
} else {
createAndShowMenuContainer();
}
// Start the continuous check for physics and menu functionality activation
// This will also ensure the menu's visibility persistently
setInterval(initializePhysicsAndMenu, 500);
setInterval(ensureMenuVisibility, 500); // Ensure menu visibility periodically
console.log('🌌 Drawaria Avatar Physics (v6.0) Loader initiated. Menu should be visible. Physics control is MANUAL.');
})();