您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Advanced tennis physics with professional Wimbledon court and spin system!
// ==UserScript== // @name Drawaria Physics Engine Tennis🥎 // @namespace http://tampermonkey.net/ // @version 1.0.0 // @description Advanced tennis physics with professional Wimbledon court and spin system! // @author YouTubeDrawaria // @match https://drawaria.online/* // @grant none // @license MIT // @icon https://www.google.com/s2/favicons?sz=64&domain=drawaria.online // ==/UserScript== (function() { 'use strict'; /* ---------- SHARED SYSTEM COMPONENTS ---------- */ let drawariaSocket = null; let drawariaCanvas = null; let drawariaCtx = null; // Optimized command queue const commandQueue = []; let batchProcessor = null; const BATCH_SIZE = 15; const BATCH_INTERVAL = 40; const positionCache = new Map(); const MOVEMENT_THRESHOLD = 2; // ✅ COLORES OFICIALES DE TENIS const TENNIS_COURT_COLORS = { courtColor: '#228B22', // Verde césped Wimbledon lineColor: '#FFFFFF', // Líneas blancas oficiales netColor: '#000000', // Red negra postColor: '#8B4513', // Postes marrones textColor: '#FFFFFF', // Texto blanco clayColor: '#CD853F', // Arcilla (superficie alternativa) hardColor: '#4169E1' // Cemento azul (superficie alternativa) }; // Tennis physics constants[1][2] const TENNIS_PHYSICS = { GRAVITY: 300, // Menor que baloncesto BALL_MASS: 0.1, // Pelota más liviana BALL_RADIUS: 15, // Pelota más pequeña TIMESTEP: 1/60, MAX_VELOCITY: 800, // Más rápida que baloncesto AIR_RESISTANCE: 0.003, // Menos resistencia RESTITUTION_BALL: 0.75, // Rebote medio de tenis RESTITUTION_NET: 0.1, // Rebote muy bajo en la red RESTITUTION_WALL: 0.6, FRICTION_COURT: 0.85, // Alta fricción en césped PLAYER_INTERACTION_FORCE: 300, PLAYER_PUSH_MULTIPLIER: 2.2, PLAYER_RESTITUTION: 0.95, PLAYER_DETECTION_RADIUS_MULTIPLIER: 2.5, // Tennis specific SERVE_FORCE: 400, SPIN_FACTOR: 0.8, BALL_COLOR: '#9ACD32', // Verde lima tenis NET_HEIGHT: 40, COURT_SURFACE: 'grass' // grass, clay, hard }; const TENNIS_MATCH = { SETS_TO_WIN: 2, GAMES_TO_WIN: 6, POINTS: [0, 15, 30, 40, 'DEUCE', 'ADV'], SERVE_AREAS: { left: { x1: 0, y1: 0, x2: 0.5, y2: 0.5 }, right: { x1: 0.5, y1: 0, x2: 1, y2: 0.5 } } }; let isDrawing = false; let isStopped = false; // WebSocket interception[3] 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 for tennis engine.'); startBatchProcessor(); } return originalWebSocketSend.apply(this, args); }; function startBatchProcessor() { if (batchProcessor) return; batchProcessor = setInterval(() => { if (!drawariaSocket || drawariaSocket.readyState !== WebSocket.OPEN || commandQueue.length === 0) { return; } const batch = commandQueue.splice(0, BATCH_SIZE); batch.forEach(cmd => { try { drawariaSocket.send(cmd); } catch (e) { console.warn('Failed to send command:', e); } }); }, BATCH_INTERVAL); } function enqueueDrawCommand(x1, y1, x2, y2, color, thickness) { if (!drawariaCanvas || !drawariaSocket) 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 cmd = `42["drawcmd",0,[${normX1},${normY1},${normX2},${normY2},false,${-Math.abs(thickness)},"${color}",0,0,{}]]`; commandQueue.push(cmd); if (drawariaCtx) { drawariaCtx.strokeStyle = color; drawariaCtx.lineWidth = thickness; drawariaCtx.lineCap = 'round'; drawariaCtx.lineJoin = 'round'; drawariaCtx.beginPath(); drawariaCtx.moveTo(x1, y1); drawariaCtx.lineTo(x2, y2); drawariaCtx.stroke(); } } // Helper functions function clamp(value, min, max) { return Math.min(Math.max(value, min), max); } function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } // ✅ SISTEMA DE COORDENADAS DE TENIS PROFESIONAL function getCanvasSize() { return { width: drawariaCanvas.width, height: drawariaCanvas.height }; } function calculateTennisCoordinates() { const size = getCanvasSize(); const coords = { // Dimensiones oficiales de cancha de tenis (proporción 2:1) court: { x: Math.floor(size.width * 0.1), y: Math.floor(size.height * 0.1), width: Math.floor(size.width * 0.8), height: Math.floor(size.height * 0.8) }, // Red central net: { x: Math.floor(size.width * 0.5), y1: Math.floor(size.height * 0.1), y2: Math.floor(size.height * 0.9), height: TENNIS_PHYSICS.NET_HEIGHT, postHeight: 50 }, // Líneas de servicio serviceLines: { leftService: Math.floor(size.width * 0.35), rightService: Math.floor(size.width * 0.65), topService: Math.floor(size.height * 0.3), bottomService: Math.floor(size.height * 0.7) }, // Líneas laterales (singles y doubles) sideLines: { leftSingles: Math.floor(size.width * 0.15), rightSingles: Math.floor(size.width * 0.85), leftDoubles: Math.floor(size.width * 0.1), rightDoubles: Math.floor(size.width * 0.9) }, // Líneas de fondo baseLines: { top: Math.floor(size.height * 0.1), bottom: Math.floor(size.height * 0.9) }, // Texto text: { x: Math.floor(size.width * 0.5), y: Math.floor(size.height * 0.05), pixelSize: Math.max(2, Math.floor(size.width * 0.004)) } }; return coords; } function sendDrawCommand(x, y, x2, y2, color, thickness) { if (!drawariaSocket || !drawariaCanvas) return; const normX = (x / drawariaCanvas.width).toFixed(4); const normY = (y / drawariaCanvas.height).toFixed(4); const normX2 = (x2 / drawariaCanvas.width).toFixed(4); const normY2 = (y2 / drawariaCanvas.height).toFixed(4); const command = `42["drawcmd",0,[${normX},${normY},${normX2},${normY2},false,${0 - thickness},"${color}",0,0,{}]]`; drawariaSocket.send(command); } async function drawLineLocalAndServer(startX, startY, endX, endY, color, thickness, delay = 50) { if (isStopped) { isDrawing = false; return; } const canvasSize = getCanvasSize(); startX = clamp(startX, -50, canvasSize.width + 50); startY = clamp(startY, 0, canvasSize.height); endX = clamp(endX, -50, canvasSize.width + 50); endY = clamp(endY, 0, canvasSize.height); if (drawariaCtx && startX >= 0 && startX <= canvasSize.width && startY >= 0 && startY <= canvasSize.height) { drawariaCtx.strokeStyle = color; drawariaCtx.lineWidth = thickness; drawariaCtx.lineCap = 'round'; drawariaCtx.lineJoin = 'round'; drawariaCtx.beginPath(); drawariaCtx.moveTo(startX, startY); drawariaCtx.lineTo(endX, endY); drawariaCtx.stroke(); } sendDrawCommand(startX, startY, endX, endY, color, thickness); await sleep(delay); } async function drawPixel(x, y, color, size = 2) { if (isStopped) return; const canvasSize = getCanvasSize(); x = clamp(x, 0, canvasSize.width - size); y = clamp(y, 0, canvasSize.height - size); if (drawariaCtx) { drawariaCtx.fillStyle = color; drawariaCtx.fillRect(x, y, size, size); } sendDrawCommand(x, y, x + 1, y + 1, color, size); await sleep(15); } // ✅ FUNCIONES DE DIBUJO DE CANCHA DE TENIS async function drawTennisCourtSurface() { if (isStopped) return; updateStatus(document.getElementById('tennis-status'), "🎾 Dibujando superficie de césped Wimbledon...", TENNIS_COURT_COLORS.courtColor); const canvasSize = getCanvasSize(); const coords = calculateTennisCoordinates(); // Superficie principal de césped for (let y = coords.court.y; y < coords.court.y + coords.court.height; y += 8) { await drawLineLocalAndServer(coords.court.x, y, coords.court.x + coords.court.width, y, TENNIS_COURT_COLORS.courtColor, 2, 25); if (isStopped) break; } // Líneas de textura para simular césped for (let y = coords.court.y + 10; y < coords.court.y + coords.court.height - 10; y += 20) { for (let x = coords.court.x + 10; x < coords.court.x + coords.court.width - 10; x += 30) { await drawLineLocalAndServer(x, y, x + 15, y, '#32CD32', 1, 10); if (isStopped) break; } if (isStopped) break; } } async function drawTennisCourtLines(coords) { if (isStopped) return; updateStatus(document.getElementById('tennis-status'), "⚪ Dibujando líneas oficiales de tenis...", TENNIS_COURT_COLORS.lineColor); const lineThickness = Math.max(3, Math.floor(drawariaCanvas.width * 0.006)); // Perímetro exterior (doubles) await drawRectangleOutline({ x: coords.sideLines.leftDoubles, y: coords.baseLines.top, width: coords.sideLines.rightDoubles - coords.sideLines.leftDoubles, height: coords.baseLines.bottom - coords.baseLines.top }, TENNIS_COURT_COLORS.lineColor, lineThickness); // Líneas laterales singles await drawLineLocalAndServer( coords.sideLines.leftSingles, coords.baseLines.top, coords.sideLines.leftSingles, coords.baseLines.bottom, TENNIS_COURT_COLORS.lineColor, lineThickness, 60 ); await drawLineLocalAndServer( coords.sideLines.rightSingles, coords.baseLines.top, coords.sideLines.rightSingles, coords.baseLines.bottom, TENNIS_COURT_COLORS.lineColor, lineThickness, 60 ); // Líneas de servicio horizontales await drawLineLocalAndServer( coords.sideLines.leftSingles, coords.serviceLines.topService, coords.sideLines.rightSingles, coords.serviceLines.topService, TENNIS_COURT_COLORS.lineColor, lineThickness, 70 ); await drawLineLocalAndServer( coords.sideLines.leftSingles, coords.serviceLines.bottomService, coords.sideLines.rightSingles, coords.serviceLines.bottomService, TENNIS_COURT_COLORS.lineColor, lineThickness, 70 ); // Línea central de servicio await drawLineLocalAndServer( coords.net.x, coords.serviceLines.topService, coords.net.x, coords.serviceLines.bottomService, TENNIS_COURT_COLORS.lineColor, lineThickness, 80 ); } async function drawTennisNet(coords) { if (isStopped) return; updateStatus(document.getElementById('tennis-status'), "🕸️ Instalando red de tenis...", TENNIS_COURT_COLORS.netColor); // Postes de la red const postWidth = 8; const postHeight = coords.net.postHeight; // Poste izquierdo await drawRectangleOutline({ x: coords.net.x - postWidth/2, y: coords.net.y1 - postHeight, width: postWidth, height: postHeight }, TENNIS_COURT_COLORS.postColor, 3); // Poste derecho await drawRectangleOutline({ x: coords.net.x - postWidth/2, y: coords.net.y2, width: postWidth, height: postHeight }, TENNIS_COURT_COLORS.postColor, 3); // Red central (líneas verticales) const netDensity = 12; for (let i = 0; i < netDensity; i++) { const netY = coords.net.y1 + (i * (coords.net.y2 - coords.net.y1) / netDensity); await drawLineLocalAndServer( coords.net.x, netY, coords.net.x, netY + (coords.net.y2 - coords.net.y1) / netDensity, TENNIS_COURT_COLORS.netColor, 2, 30 ); if (isStopped) break; } // Líneas horizontales de la red const horizontalNetLines = 8; for (let i = 0; i < horizontalNetLines; i++) { const netX = coords.net.x - 2 + (i * 0.5); await drawLineLocalAndServer( netX, coords.net.y1, netX, coords.net.y2, TENNIS_COURT_COLORS.netColor, 1, 25 ); if (isStopped) break; } } // ✅ FUNCIONES GEOMÉTRICAS async function drawRectangleOutline(rectCoords, color, thickness) { await drawLineLocalAndServer(rectCoords.x, rectCoords.y, rectCoords.x + rectCoords.width, rectCoords.y, color, thickness, 40); await drawLineLocalAndServer(rectCoords.x + rectCoords.width, rectCoords.y, rectCoords.x + rectCoords.width, rectCoords.y + rectCoords.height, color, thickness, 40); await drawLineLocalAndServer(rectCoords.x + rectCoords.width, rectCoords.y + rectCoords.height, rectCoords.x, rectCoords.y + rectCoords.height, color, thickness, 40); await drawLineLocalAndServer(rectCoords.x, rectCoords.y + rectCoords.height, rectCoords.x, rectCoords.y, color, thickness, 40); } // ✅ TEXTO TENNIS EN PIXEL ART const TENNIS_LETTERS = { 'T': [[1,1,1,1,1],[0,0,1,0,0],[0,0,1,0,0],[0,0,1,0,0],[0,0,1,0,0]], 'E': [[1,1,1,1],[1,0,0,0],[1,1,1,0],[1,0,0,0],[1,1,1,1]], 'N': [[1,0,0,1],[1,1,0,1],[1,0,1,1],[1,0,0,1],[1,0,0,1]], 'I': [[1,1,1],[0,1,0],[0,1,0],[0,1,0],[1,1,1]], 'S': [[1,1,1,1],[1,0,0,0],[1,1,1,1],[0,0,0,1],[1,1,1,1]] }; async function drawTennisPixelText(text, coords) { if (isStopped) return; const letterSpacing = coords.text.pixelSize * 6; const textWidth = text.length * letterSpacing; let currentX = coords.text.x - (textWidth / 2); for (let i = 0; i < text.length; i++) { if (isStopped) break; const letter = text[i].toUpperCase(); if (letter === ' ') { currentX += letterSpacing; continue; } const pattern = TENNIS_LETTERS[letter]; if (!pattern) continue; for (let row = 0; row < pattern.length; row++) { for (let col = 0; col < pattern[row].length; col++) { if (pattern[row][col] === 1) { const pixelX = currentX + (col * coords.text.pixelSize); const pixelY = coords.text.y + (row * coords.text.pixelSize); const canvasSize = getCanvasSize(); if (pixelX >= 0 && pixelX < canvasSize.width && pixelY >= 0 && pixelY < canvasSize.height) { await drawPixel(pixelX, pixelY, TENNIS_COURT_COLORS.textColor, coords.text.pixelSize); } } } } currentX += letterSpacing; await sleep(100); } } // ✅ FUNCIÓN PRINCIPAL: CANCHA DE TENIS COMPLETA async function drawCompleteTennisCourt() { if (isDrawing) { alert('Ya está en curso un dibujo. Presiona "Parar" para cancelar.'); return; } if (!drawariaSocket || !drawariaCanvas || !drawariaCtx) { alert('No se detectó conexión o canvas. Asegúrate de estar en una sala de juego.'); return; } isDrawing = true; isStopped = false; const statusDiv = document.getElementById('tennis-status') || createStatusDiv(); try { const coords = calculateTennisCoordinates(); const canvasSize = getCanvasSize(); console.log(`🎾 Cancha de tenis Wimbledon iniciada:`); console.log(`📏 Canvas: ${canvasSize.width}x${canvasSize.height}`); updateStatus(statusDiv, `🎾 CANCHA DE TENIS WIMBLEDON: ${canvasSize.width}x${canvasSize.height}`, "#228B22"); await sleep(800); // FASE 1: SUPERFICIE DE CÉSPED updateStatus(statusDiv, "🎾 FASE 1: Superficie de césped Wimbledon...", TENNIS_COURT_COLORS.courtColor); await drawTennisCourtSurface(); await sleep(300); if (isStopped) return; // FASE 2: LÍNEAS OFICIALES updateStatus(statusDiv, "⚪ FASE 2: Líneas oficiales de tenis...", TENNIS_COURT_COLORS.lineColor); await drawTennisCourtLines(coords); await sleep(300); if (isStopped) return; // FASE 3: RED Y POSTES updateStatus(statusDiv, "🕸️ FASE 3: Instalando red de tenis...", TENNIS_COURT_COLORS.netColor); await drawTennisNet(coords); await sleep(300); if (isStopped) return; // FASE 4: TEXTO TENNIS updateStatus(statusDiv, "🎮 FASE 4: Texto blanco 'TENNIS'...", TENNIS_COURT_COLORS.textColor); await drawTennisPixelText("TENNIS", coords); // CANCHA COMPLETA updateStatus(statusDiv, "🏆 ¡CANCHA DE TENIS WIMBLEDON COMPLETA! 🎾🏆", "#006400"); setTimeout(() => { if (statusDiv && statusDiv.parentNode) { statusDiv.style.opacity = 0; setTimeout(() => statusDiv.remove(), 500); } }, 4000); } catch (error) { console.error("Error en cancha de tenis:", error); updateStatus(statusDiv, `❌ Error: ${error.message}`, "#B22222"); } finally { isDrawing = false; } } function createStatusDiv() { const statusDiv = document.createElement('div'); statusDiv.id = 'tennis-status'; statusDiv.style.cssText = ` position: fixed; top: 20px; left: 50%; transform: translateX(-50%); background: linear-gradient(135deg, #228B22 0%, #32CD32 100%); color: white; padding: 20px 45px; border-radius: 35px; font-weight: bold; z-index: 10000; transition: opacity 0.5s; text-align: center; min-width: 480px; box-shadow: 0 15px 35px rgba(0,0,0,0.5); text-shadow: 1px 1px 3px rgba(0,0,0,0.4); border: 2px solid #FFFFFF; `; document.body.appendChild(statusDiv); return statusDiv; } function updateStatus(statusDiv, message, color) { if (!statusDiv) return; statusDiv.textContent = message; if (color) { statusDiv.style.background = color; } statusDiv.style.opacity = 1; } /* ---------- ADVANCED TENNIS PHYSICS ENGINE ---------- */ class AdvancedDrawariaTennis { constructor() { this.initialized = false; this.isActive = false; this.physicsObjects = new Map(); this.objectIdCounter = 0; this.lastRenderTime = 0; this.renderInterval = 1000 / 30; // Sistema de raquetas virtuales[4][5] this.racketSystem = { playerRackets: new Map(), racketLength: 60, racketWidth: 8, hitCooldown: 300 }; // Tennis match scoring this.tennisMatch = { active: false, scores: { p1: { sets: 0, games: 0, points: 0 }, p2: { sets: 0, games: 0, points: 0 } }, serving: 'p1', courtSurface: 'grass', lastServeTime: 0 }; this.gameStats = { totalHits: 0, maxVelocityReached: 0, ballsCreated: 0, totalAces: 0, netHits: 0 }; this.controls = { showDebug: false, defaultBallColor: TENNIS_PHYSICS.BALL_COLOR, courtSurface: 'grass' }; this.playerTracker = { players: new Map(), detectionRadius: TENNIS_PHYSICS.BALL_RADIUS * TENNIS_PHYSICS.PLAYER_DETECTION_RADIUS_MULTIPLIER, lastUpdateTime: 0 }; this.init(); } init() { if (this.initialized) return; const checkGameReady = () => { const gameCanvas = document.getElementById('canvas'); if (gameCanvas) { this.canvasElement = gameCanvas; drawariaCanvas = gameCanvas; this.canvasContext = gameCanvas.getContext('2d'); drawariaCtx = gameCanvas.getContext('2d'); this.initialized = true; this.createTennisPanel(); console.log('✅ Advanced Tennis Physics Engine v1.0 initialized'); } else { setTimeout(checkGameReady, 100); } }; checkGameReady(); } createTennisPanel() { const existingPanel = document.getElementById('tennis-physics-panel'); if (existingPanel) existingPanel.remove(); const panel = document.createElement('div'); panel.id = 'tennis-physics-panel'; panel.style.cssText = ` position: fixed !important; top: 20px !important; right: 20px !important; width: 380px !important; z-index: 2147483647 !important; background: linear-gradient(135deg, #0f2f0f, #1a4a1a) !important; border: 2px solid #32CD32 !important; border-radius: 15px !important; color: white !important; font-family: 'Segoe UI', Arial, sans-serif !important; overflow: hidden !important; box-shadow: 0 0 30px rgba(50,205,50,0.4) !important; `; const header = document.createElement('div'); header.id = 'tennis-panel-header'; header.style.cssText = ` background: linear-gradient(45deg, #228B22, #32CD32) !important; padding: 12px 20px !important; font-weight: bold !important; text-align: center !important; font-size: 14px !important; cursor: move !important; user-select: none !important; display: flex !important; justify-content: space-between !important; align-items: center !important; `; const title = document.createElement('div'); title.innerHTML = '🎾 WIMBLEDON TENNIS ENGINE v1.0'; title.style.flex = '1'; const buttonContainer = document.createElement('div'); buttonContainer.style.cssText = `display: flex !important; gap: 8px !important;`; const minimizeBtn = document.createElement('button'); minimizeBtn.id = 'tennis-minimize-btn'; minimizeBtn.innerHTML = '−'; minimizeBtn.style.cssText = ` width: 25px !important; height: 25px !important; background: rgba(255,255,255,0.2) !important; border: none !important; border-radius: 4px !important; color: white !important; cursor: pointer !important; font-size: 16px !important; line-height: 1 !important; padding: 0 !important; `; const closeBtn = document.createElement('button'); closeBtn.id = 'tennis-close-btn'; closeBtn.innerHTML = '×'; closeBtn.style.cssText = ` width: 25px !important; height: 25px !important; background: rgba(255,0,0,0.6) !important; border: none !important; border-radius: 4px !important; color: white !important; cursor: pointer !important; font-size: 18px !important; line-height: 1 !important; padding: 0 !important; `; buttonContainer.appendChild(minimizeBtn); buttonContainer.appendChild(closeBtn); header.appendChild(title); header.appendChild(buttonContainer); const content = document.createElement('div'); content.id = 'tennis-panel-content'; content.style.cssText = `padding: 20px !important;`; content.innerHTML = ` <!-- CREATE TENNIS COURT --> <div style="margin-bottom: 15px; text-align: center;"> <button id="create-tennis-court-btn" style=" width: 100%; padding: 12px; background: linear-gradient(135deg, #228B22, #32CD32); color: white; border: none; border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: bold; margin-bottom: 10px; box-shadow: 0 4px 15px rgba(34,139,34,0.3); ">🎾 Create Wimbledon Tennis Court</button> </div> <!-- LAUNCH TENNIS ENGINE --> <div style="margin-bottom: 15px; text-align: center;"> <button id="toggle-tennis-physics" style=" width: 100%; padding: 12px; background: linear-gradient(135deg, #32CD32, #9ACD32); color: white; border: none; border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: bold; ">🚀 Launch Tennis Engine</button> </div> <!-- TENNIS BALL CREATION --> <div style="display: flex; gap: 10px; margin-bottom: 15px;"> <button id="add-tennis-ball-btn" style=" flex: 1; padding: 8px; background: linear-gradient(135deg, #9ACD32, #32CD32); color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 12px; font-weight: bold; ">🎾 Add Tennis Ball</button> <button id="serve-ball-btn" style=" flex: 1; padding: 8px; background: linear-gradient(135deg, #FFD700, #FFA500); color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 12px; font-weight: bold; ">🏆 Serve</button> </div> <!-- COURT SURFACE SELECTION --> <div style="margin-bottom: 15px;"> <label style="display: block; margin-bottom: 5px; font-size: 12px; color: #32CD32;"> 🏟️ Court Surface: </label> <select id="court-surface" style=" width: 100%; padding: 8px; border: 1px solid #32CD32; border-radius: 6px; background: #1a4a1a; color: white; font-size: 12px; "> <option value="grass">🌱 Grass (Wimbledon)</option> <option value="clay">🧱 Clay (Roland Garros)</option> <option value="hard">🏢 Hard Court (US Open)</option> </select> </div> <!-- ACTION BUTTONS --> <div style="display: flex; gap: 8px; margin-bottom: 15px;"> <button id="reset-tennis-btn" style=" flex: 1; padding: 8px; background: linear-gradient(135deg, #74b9ff, #0984e3); color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: bold; font-size: 11px; ">🔄 Reset</button> <button id="stop-tennis-court-btn" style=" flex: 1; padding: 8px; background: linear-gradient(135deg, #e74c3c, #c0392b); color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: bold; font-size: 11px; ">⛔ Stop Court</button> </div> <!-- TENNIS MODES --> <div style="margin-bottom: 15px;"> <h4 style="margin: 0 0 10px 0; font-size: 13px; color: #32CD32; text-align: center;">🌟 Tennis Modes</h4> <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px;"> <button id="match-mode-toggle" class="tennis-mode-toggle" style=" padding: 8px; background: linear-gradient(135deg, #444, #666); color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 10px; font-weight: bold; ">🏆 Match Mode</button> <button id="clean-tennis-canvas-btn" style=" padding: 8px; background: linear-gradient(135deg, #e17055, #d63031); color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 10px; font-weight: bold; ">🧹 Clean Court</button> </div> </div> <!-- CLEAR ALL --> <div style="margin-bottom: 15px;"> <button id="clear-tennis-balls-btn" style=" width: 100%; padding: 10px; background: linear-gradient(135deg, #990000, #cc0000); color: white; border: none; border-radius: 8px; cursor: pointer; font-weight: bold; ">🗑️ Clear All Tennis Balls</button> </div> <!-- TENNIS SCOREBOARD --> <div id="tennis-scoreboard" style=" display: none; background: rgba(0,0,0,0.4); padding: 15px; border-radius: 8px; text-align: center; margin-bottom: 15px; border: 2px solid #FFD700; "> <h4 style="margin: 0 0 10px 0; color: #FFD700; font-size: 14px;">🎾 WIMBLEDON SCORE</h4> <div style="display: flex; justify-content: space-between; font-size: 14px; font-weight: bold;"> <div style="color: #ff6b6b;"> P1: <span id="tennis-score-p1-sets">0</span>-<span id="tennis-score-p1-games">0</span> <br><span id="tennis-score-p1-points" style="font-size: 18px;">0</span> </div> <div style="color: #666; font-size: 12px;">vs</div> <div style="color: #74b9ff;"> P2: <span id="tennis-score-p2-sets">0</span>-<span id="tennis-score-p2-games">0</span> <br><span id="tennis-score-p2-points" style="font-size: 18px;">0</span> </div> </div> <div style="margin-top: 10px; font-size: 11px; color: #FFD700;"> Serving: <span id="serving-player">P1</span> </div> </div> <!-- TENNIS STATS --> <div id="tennis-stats" style=" background: rgba(0,0,0,0.3); padding: 10px; border-radius: 6px; font-size: 10px; text-align: center; border: 1px solid rgba(50,205,50,0.3); "> <div>Tennis Balls: <span id="tennis-ball-count">0</span> | Hits: <span id="hits-count">0</span></div> <div>Aces: <span id="aces-count">0</span> | Net Hits: <span id="net-hits-count">0</span></div> <div>Max Speed: <span id="tennis-max-speed">0</span> km/h</div> <div>Surface: <span id="surface-info">Grass</span></div> </div> <!-- HELP TEXT --> <div style=" text-align: center; margin-top: 15px; font-size: 9px; color: rgba(255,255,255,0.6); border-top: 1px solid rgba(255,255,255,0.1); padding-top: 10px; "> Professional Wimbledon Court • Tennis Physics<br> <span style="color: #32CD32;">Spin effects • Net detection • Court surfaces</span> </div> `; panel.appendChild(header); panel.appendChild(content); document.body.appendChild(panel); this.makeTennisPanelDraggable(); this.setupTennisPanelButtons(); this.setupTennisEventListeners(); this.startTennisStatsMonitoring(); } setupTennisEventListeners() { // Tennis court controls document.getElementById('create-tennis-court-btn')?.addEventListener('click', () => drawCompleteTennisCourt()); document.getElementById('toggle-tennis-physics')?.addEventListener('click', () => this.toggleTennisPhysics()); document.getElementById('stop-tennis-court-btn')?.addEventListener('click', () => this.stopTennisCourtDrawing()); // Tennis ball creation document.getElementById('add-tennis-ball-btn')?.addEventListener('click', () => this.addRandomTennisBall()); document.getElementById('serve-ball-btn')?.addEventListener('click', () => this.serveTennisBall()); // Actions document.getElementById('reset-tennis-btn')?.addEventListener('click', () => this.resetAllTennisBalls()); document.getElementById('clear-tennis-balls-btn')?.addEventListener('click', () => this.clearAllTennisBalls()); document.getElementById('match-mode-toggle')?.addEventListener('click', () => this.toggleTennisMatch()); document.getElementById('clean-tennis-canvas-btn')?.addEventListener('click', () => this.cleanTennisCourt()); // Court surface document.getElementById('court-surface')?.addEventListener('change', (e) => { this.controls.courtSurface = e.target.value; this.updateSurfacePhysics(e.target.value); this.showTennisFeedback(`🏟️ Court Surface: ${e.target.options[e.target.selectedIndex].text}`, '#32CD32'); }); // Canvas click for tennis ball if (this.canvasElement) { this.canvasElement.addEventListener('click', (e) => this.createTennisBall(e.clientX - this.canvasElement.getBoundingClientRect().left, e.clientY - this.canvasElement.getBoundingClientRect().top)); } } stopTennisCourtDrawing() { isStopped = true; const statusDiv = document.getElementById('tennis-status'); if (statusDiv) { updateStatus(statusDiv, "⛔ Dibujo de cancha detenido", "#B22222"); } this.showTennisFeedback('⛔ Tennis court drawing stopped', '#B22222'); } /* ---------- TENNIS PHYSICS ENGINE ---------- */ toggleTennisPhysics() { const toggleBtn = document.getElementById('toggle-tennis-physics'); if (!this.isActive) { this.startTennisPhysics(); if (toggleBtn) { toggleBtn.textContent = '🛑 Stop Tennis Engine'; toggleBtn.style.background = 'linear-gradient(135deg, #f56565, #e53e3e)'; } } else { this.stopTennisPhysics(); if (toggleBtn) { toggleBtn.textContent = '🚀 Launch Tennis Engine'; toggleBtn.style.background = 'linear-gradient(135deg, #32CD32, #9ACD32)'; } } } startTennisPhysics() { if (this.isActive) return; this.isActive = true; this.startTennisGameLoop(); this.showTennisFeedback('🚀 Wimbledon Tennis Engine Started!', '#32CD32'); } stopTennisPhysics() { this.isActive = false; this.showTennisFeedback('🛑 Tennis Engine Stopped', '#f56565'); } startTennisGameLoop() { if (!this.isActive) return; const currentTime = performance.now(); if (currentTime - this.lastRenderTime >= this.renderInterval) { this.updateTennisPhysics(); this.renderTennisBalls(); this.lastRenderTime = currentTime; } requestAnimationFrame(() => this.startTennisGameLoop()); } updateTennisPhysics() { const dt = TENNIS_PHYSICS.TIMESTEP; // Surface-specific physics adjustments let gravityMultiplier = 1; let frictionMultiplier = 1; let bounceMultiplier = 1; switch(this.controls.courtSurface) { case 'clay': frictionMultiplier = 1.5; bounceMultiplier = 0.8; break; case 'hard': frictionMultiplier = 0.8; bounceMultiplier = 1.1; break; case 'grass': default: frictionMultiplier = 1.0; bounceMultiplier = 0.9; break; } // Update tennis balls this.physicsObjects.forEach(ball => { if (ball.type !== 'tennis') return; // Apply air resistance ball.vx *= (1 - TENNIS_PHYSICS.AIR_RESISTANCE * dt); ball.vy *= (1 - TENNIS_PHYSICS.AIR_RESISTANCE * dt); // Apply gravity ball.vy += TENNIS_PHYSICS.GRAVITY * gravityMultiplier * dt; // Apply spin effects (Magnus effect) if (ball.spin) { const spinForce = ball.spin * TENNIS_PHYSICS.SPIN_FACTOR; ball.vx += spinForce * dt; ball.vy += spinForce * 0.5 * dt; ball.spin *= 0.98; // Spin decay } // Update position ball.x += ball.vx * dt; ball.y += ball.vy * dt; this.handleTennisBoundaryCollisions(ball, frictionMultiplier, bounceMultiplier); this.handleTennisNetCollision(ball); // Velocity limiting const speed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy); if (speed > this.gameStats.maxVelocityReached) { this.gameStats.maxVelocityReached = speed; } if (speed > TENNIS_PHYSICS.MAX_VELOCITY) { ball.vx = (ball.vx / speed) * TENNIS_PHYSICS.MAX_VELOCITY; ball.vy = (ball.vy / speed) * TENNIS_PHYSICS.MAX_VELOCITY; } }); this.handleTennisBallCollisions(); this.handleTennisPlayerCollisions(); if (this.tennisMatch.active) { this.checkTennisScoring(); } } updateSurfacePhysics(surface) { this.controls.courtSurface = surface; // Update existing balls' physics this.physicsObjects.forEach(ball => { if (ball.type !== 'tennis') return; switch(surface) { case 'clay': ball.friction = TENNIS_PHYSICS.FRICTION_COURT * 1.5; ball.restitution = TENNIS_PHYSICS.RESTITUTION_BALL * 0.8; break; case 'hard': ball.friction = TENNIS_PHYSICS.FRICTION_COURT * 0.8; ball.restitution = TENNIS_PHYSICS.RESTITUTION_BALL * 1.1; break; case 'grass': default: ball.friction = TENNIS_PHYSICS.FRICTION_COURT; ball.restitution = TENNIS_PHYSICS.RESTITUTION_BALL; break; } }); } /* ---------- TENNIS BALL CREATION ---------- */ addRandomTennisBall() { if (!this.canvasElement) return; const padding = 80; const x = Math.random() * (this.canvasElement.width - 2 * padding) + padding; const y = Math.random() * (this.canvasElement.height * 0.3 - 2 * padding) + padding; this.createTennisBall(x, y); } serveTennisBall() { if (!this.canvasElement) return; const coords = calculateTennisCoordinates(); // Serve desde línea de fondo const isP1Serving = this.tennisMatch.serving === 'p1'; const serveX = isP1Serving ? coords.court.x + 50 : coords.court.x + coords.court.width - 50; const serveY = this.canvasElement.height * 0.85; const ball = this.createTennisBall(serveX, serveY); // Aplicar fuerza de saque const serveDirection = isP1Serving ? 1 : -1; ball.vx = TENNIS_PHYSICS.SERVE_FORCE * serveDirection * 0.6; ball.vy = -TENNIS_PHYSICS.SERVE_FORCE * 0.8; ball.spin = Math.random() * 20 - 10; // Spin aleatorio this.showTennisFeedback(`🎾 ${this.tennisMatch.serving.toUpperCase()} SERVES!`, '#FFD700'); } createTennisBall(x, y) { const id = `tennis_${this.objectIdCounter++}`; const ball = { id: id, type: 'tennis', x: x, y: y, vx: 0, vy: 0, radius: TENNIS_PHYSICS.BALL_RADIUS, color: TENNIS_PHYSICS.BALL_COLOR, mass: TENNIS_PHYSICS.BALL_MASS, restitution: TENNIS_PHYSICS.RESTITUTION_BALL, friction: TENNIS_PHYSICS.FRICTION_COURT, lastRenderX: -9999, lastRenderY: -9999, creationTime: performance.now(), lastCollisionTime: 0, // Tennis specific properties spin: 0, lastBounceTime: 0, bounceCount: 0, lastHitTime: 0, isInPlay: true }; this.physicsObjects.set(id, ball); this.gameStats.ballsCreated++; return ball; } /* ---------- TENNIS COLLISION HANDLING ---------- */ handleTennisBoundaryCollisions(ball, frictionMultiplier, bounceMultiplier) { if (!this.canvasElement) return; const ballHalfSize = ball.radius; const coords = calculateTennisCoordinates(); // Límites de la cancha const boundaries = { left: coords.court.x + ballHalfSize, right: coords.court.x + coords.court.width - ballHalfSize, top: coords.court.y + ballHalfSize, bottom: coords.court.y + coords.court.height - ballHalfSize }; // Colisiones laterales if (ball.x < boundaries.left) { ball.x = boundaries.left; ball.vx = -ball.vx * TENNIS_PHYSICS.RESTITUTION_WALL * bounceMultiplier; ball.vy *= frictionMultiplier; } else if (ball.x > boundaries.right) { ball.x = boundaries.right; ball.vx = -ball.vx * TENNIS_PHYSICS.RESTITUTION_WALL * bounceMultiplier; ball.vy *= frictionMultiplier; } // Colisiones verticales if (ball.y < boundaries.top) { ball.y = boundaries.top; ball.vy = -ball.vy * TENNIS_PHYSICS.RESTITUTION_WALL * bounceMultiplier; ball.vx *= frictionMultiplier; } else if (ball.y > boundaries.bottom) { ball.y = boundaries.bottom; ball.vy = -ball.vy * ball.restitution * bounceMultiplier; ball.vx *= ball.friction * frictionMultiplier; // Registrar rebote ball.bounceCount++; ball.lastBounceTime = performance.now(); } } handleTennisNetCollision(ball) { const coords = calculateTennisCoordinates(); const netX = coords.net.x; const netTop = coords.net.y1; const netBottom = coords.net.y2; const netHeight = TENNIS_PHYSICS.NET_HEIGHT; // Verificar colisión con la red (área vertical) if (Math.abs(ball.x - netX) < ball.radius + 5 && ball.y > netTop && ball.y < netBottom && ball.y > netBottom - netHeight) { // La pelota golpea la red ball.vx = -ball.vx * TENNIS_PHYSICS.RESTITUTION_NET; ball.vy *= 0.5; // Pierde velocidad vertical ball.spin = 0; // Pierde spin // Posicionar fuera de la red if (ball.x < netX) { ball.x = netX - ball.radius - 5; } else { ball.x = netX + ball.radius + 5; } this.gameStats.netHits++; this.showTennisFeedback('🕸️ NET HIT!', '#ff4757'); } } handleTennisBallCollisions() { const ballsArray = Array.from(this.physicsObjects.values()).filter(obj => obj.type === 'tennis'); for (let i = 0; i < ballsArray.length; i++) { const ballA = ballsArray[i]; for (let j = i + 1; j < ballsArray.length; j++) { const ballB = ballsArray[j]; const dx = ballB.x - ballA.x; const dy = ballB.y - ballA.y; const distance = Math.sqrt(dx * dx + dy * dy); const minDistance = ballA.radius + ballB.radius; if (distance < minDistance && distance !== 0) { // Separar pelotas const normalX = dx / distance; const normalY = dy / distance; const overlap = minDistance - distance; ballA.x -= normalX * overlap * 0.5; ballA.y -= normalY * overlap * 0.5; ballB.x += normalX * overlap * 0.5; ballB.y += normalY * overlap * 0.5; // Intercambiar velocidades (rebote elástico) const tempVx = ballA.vx; const tempVy = ballA.vy; ballA.vx = ballB.vx * 0.9; ballA.vy = ballB.vy * 0.9; ballB.vx = tempVx * 0.9; ballB.vy = tempVy * 0.9; // Intercambiar spin const tempSpin = ballA.spin; ballA.spin = ballB.spin * 0.5; ballB.spin = tempSpin * 0.5; } } } } /* ---------- SISTEMA DE RAQUETAS VIRTUALES ---------- */ handleTennisPlayerCollisions() { const players = this.getTennisPlayerPositions(); if (players.length === 0) return; this.physicsObjects.forEach(ball => { if (ball.type !== 'tennis') return; players.forEach(player => { const dx = ball.x - player.x; const dy = ball.y - player.y; const distance = Math.sqrt(dx * dx + dy * dy); const racketReach = this.racketSystem.racketLength; if (distance < racketReach && distance > 0) { const currentTime = performance.now(); if (currentTime - ball.lastHitTime > this.racketSystem.hitCooldown) { this.executeTennisHit(ball, player, dx, dy, distance); ball.lastHitTime = currentTime; this.gameStats.totalHits++; } } }); }); } executeTennisHit(ball, player, dx, dy, distance) { const normalX = dx / distance; const normalY = dy / distance; // Fuerza del golpe basada en proximidad const hitIntensity = Math.max(0.3, (this.racketSystem.racketLength - distance) / this.racketSystem.racketLength); const baseHitForce = TENNIS_PHYSICS.PLAYER_INTERACTION_FORCE * hitIntensity; // Aplicar fuerza del golpe ball.vx += normalX * baseHitForce * TENNIS_PHYSICS.TIMESTEP * 2; ball.vy += normalY * baseHitForce * TENNIS_PHYSICS.TIMESTEP * 2; // Añadir velocidad del jugador si se está moviendo if (player.vx || player.vy) { const playerSpeed = Math.sqrt((player.vx || 0) ** 2 + (player.vy || 0) ** 2); if (playerSpeed > 30) { ball.vx += (player.vx || 0) * 0.8; ball.vy += (player.vy || 0) * 0.8; } } // Generar spin aleatorio en el golpe ball.spin = (Math.random() - 0.5) * 40 * hitIntensity; // Efecto visual del golpe this.showTennisHitEffect(ball.x, ball.y, player.type); } showTennisHitEffect(x, y, playerType) { const hitColor = playerType === 'self' ? '#48bb78' : '#f56565'; this.showTennisFeedback(`🎾 ${playerType === 'self' ? 'YOUR' : 'OPPONENT'} HIT!`, hitColor); } getTennisPlayerPositions() { const currentTime = performance.now(); const players = []; if (!drawariaCanvas) return players; const canvasRect = drawariaCanvas.getBoundingClientRect(); const deltaTime = currentTime - this.playerTracker.lastUpdateTime; // Self player const selfPlayer = document.querySelector('div.spawnedavatar.spawnedavatar-self'); if (selfPlayer) { const rect = selfPlayer.getBoundingClientRect(); const currentPos = { type: 'self', id: selfPlayer.dataset.playerid || 'self', x: rect.left - canvasRect.left + rect.width / 2, y: rect.top - canvasRect.top + rect.height / 2, width: rect.width, height: rect.height, radius: Math.max(rect.width, rect.height) / 2, vx: 0, vy: 0 }; const prevPlayer = this.playerTracker.players.get('self'); if (prevPlayer && deltaTime > 0) { currentPos.vx = (currentPos.x - prevPlayer.x) / (deltaTime / 1000); currentPos.vy = (currentPos.y - prevPlayer.y) / (deltaTime / 1000); } players.push(currentPos); this.playerTracker.players.set('self', currentPos); } // Other players const otherPlayers = document.querySelectorAll('div.spawnedavatar.spawnedavatar-otherplayer'); otherPlayers.forEach((player, index) => { const rect = player.getBoundingClientRect(); const playerId = player.dataset.playerid || `other_${index}`; const currentPos = { type: 'other', id: playerId, x: rect.left - canvasRect.left + rect.width / 2, y: rect.top - canvasRect.top + rect.height / 2, width: rect.width, height: rect.height, radius: Math.max(rect.width, rect.height) / 2, vx: 0, vy: 0 }; const prevPlayer = this.playerTracker.players.get(playerId); if (prevPlayer && deltaTime > 0) { currentPos.vx = (currentPos.x - prevPlayer.x) / (deltaTime / 1000); currentPos.vy = (currentPos.y - prevPlayer.y) / (deltaTime / 1000); } players.push(currentPos); this.playerTracker.players.set(playerId, currentPos); }); this.playerTracker.lastUpdateTime = currentTime; return players; } /* ---------- TENNIS SCORING SYSTEM ---------- */ checkTennisScoring() { if (!this.tennisMatch.active || !this.canvasElement) return; const coords = calculateTennisCoordinates(); this.physicsObjects.forEach(ball => { if (ball.type !== 'tennis' || !ball.isInPlay) return; // Verificar si la pelota está dentro de la cancha después del rebote if (ball.bounceCount > 0 && ball.lastBounceTime > 0) { const isInCourt = this.checkTennisBallInCourt(ball, coords); if (!isInCourt) { this.scoreTennisPoint(ball); ball.isInPlay = false; } } // Verificar doble rebote (punto perdido) if (ball.bounceCount >= 2) { this.scoreTennisPoint(ball, 'double_bounce'); ball.isInPlay = false; } }); } checkTennisBallInCourt(ball, coords) { return ball.x >= coords.sideLines.leftSingles && ball.x <= coords.sideLines.rightSingles && ball.y >= coords.baseLines.top && ball.y <= coords.baseLines.bottom; } async scoreTennisPoint(ball, reason = 'out') { const opponent = this.tennisMatch.serving === 'p1' ? 'p2' : 'p1'; if (reason === 'out' || reason === 'double_bounce') { this.tennisMatch.scores[opponent].points++; } await this.updateTennisScore(); const reasonText = reason === 'double_bounce' ? 'DOUBLE BOUNCE' : 'OUT'; this.showTennisFeedback(`🎾 ${reasonText}! Point to ${opponent.toUpperCase()}`, '#FFD700'); // Cambiar servidor cada juego if (this.checkGameWon()) { this.tennisMatch.serving = this.tennisMatch.serving === 'p1' ? 'p2' : 'p1'; } setTimeout(() => { this.clearAllTennisBalls(false); if (this.tennisMatch.active) { this.serveTennisBall(); } }, 2000); } async updateTennisScore() { // Convertir puntos numéricos a sistema de tenis (0, 15, 30, 40, deuce, etc.) const p1Points = this.convertToTennisPoints(this.tennisMatch.scores.p1.points); const p2Points = this.convertToTennisPoints(this.tennisMatch.scores.p2.points); document.getElementById('tennis-score-p1-points').textContent = p1Points; document.getElementById('tennis-score-p2-points').textContent = p2Points; document.getElementById('tennis-score-p1-sets').textContent = this.tennisMatch.scores.p1.sets; document.getElementById('tennis-score-p1-games').textContent = this.tennisMatch.scores.p1.games; document.getElementById('tennis-score-p2-sets').textContent = this.tennisMatch.scores.p2.sets; document.getElementById('tennis-score-p2-games').textContent = this.tennisMatch.scores.p2.games; document.getElementById('serving-player').textContent = this.tennisMatch.serving.toUpperCase(); } convertToTennisPoints(numericPoints) { const tennisPoints = ['0', '15', '30', '40']; if (numericPoints < 4) { return tennisPoints[numericPoints]; } else if (numericPoints === 4) { return 'DEUCE'; } else { return 'ADV'; } } checkGameWon() { const p1 = this.tennisMatch.scores.p1; const p2 = this.tennisMatch.scores.p2; // Verificar si alguien ganó el punto if ((p1.points >= 4 && p1.points - p2.points >= 2) || (p2.points >= 4 && p2.points - p1.points >= 2)) { // Resetear puntos y sumar juego if (p1.points > p2.points) { p1.games++; } else { p2.games++; } p1.points = 0; p2.points = 0; // Verificar si alguien ganó el set if ((p1.games >= 6 && p1.games - p2.games >= 2) || (p2.games >= 6 && p2.games - p1.games >= 2)) { if (p1.games > p2.games) { p1.sets++; } else { p2.sets++; } p1.games = 0; p2.games = 0; // Verificar si alguien ganó el match if (p1.sets >= TENNIS_MATCH.SETS_TO_WIN || p2.sets >= TENNIS_MATCH.SETS_TO_WIN) { this.endTennisMatch(p1.sets > p2.sets ? 'p1' : 'p2'); } } return true; } return false; } /* ---------- TENNIS MATCH MODE ---------- */ toggleTennisMatch() { const button = document.getElementById('match-mode-toggle'); const scoreboard = document.getElementById('tennis-scoreboard'); this.tennisMatch.active = !this.tennisMatch.active; if (this.tennisMatch.active) { button.style.background = 'linear-gradient(135deg, #FFD700, #FFA500)'; button.setAttribute('data-active', 'true'); scoreboard.style.display = 'block'; this.setupTennisMatch(); this.showTennisFeedback('🏆 WIMBLEDON MATCH MODE ACTIVATED!', '#FFD700'); } else { button.style.background = 'linear-gradient(135deg, #444, #666)'; button.removeAttribute('data-active'); scoreboard.style.display = 'none'; this.resetTennisMatch(); this.showTennisFeedback('🏆 Match Mode Deactivated', '#666'); } } async setupTennisMatch() { await drawCompleteTennisCourt(); this.resetTennisMatchScores(); await this.updateTennisScore(); setTimeout(() => { this.serveTennisBall(); }, 1500); } resetTennisMatchScores() { this.tennisMatch.scores = { p1: { sets: 0, games: 0, points: 0 }, p2: { sets: 0, games: 0, points: 0 } }; this.tennisMatch.serving = 'p1'; } resetTennisMatch() { this.resetTennisMatchScores(); if (this.tennisMatch.active) { this.clearAllTennisBalls(false); setTimeout(() => { drawCompleteTennisCourt().then(() => { this.serveTennisBall(); }); }, 500); } } async endTennisMatch(winner) { this.showTennisFeedback(`🏆 ${winner.toUpperCase()} WINS THE WIMBLEDON MATCH!`, '#FFD700'); setTimeout(() => { this.resetTennisMatch(); }, 4000); } /* ---------- RENDERING ---------- */ renderTennisBalls() { this.physicsObjects.forEach(obj => { if (obj.type !== 'tennis') return; const dx = Math.abs(obj.x - obj.lastRenderX); const dy = Math.abs(obj.y - obj.lastRenderY); const needsServerRedraw = dx > MOVEMENT_THRESHOLD || dy > MOVEMENT_THRESHOLD; if (needsServerRedraw) { // Borrar posición anterior if (obj.lastRenderX !== -9999 || obj.lastRenderY !== -9999) { this.drawTennisBall(obj.lastRenderX, obj.lastRenderY, obj.radius, '#FFFFFF'); } // Dibujar en nueva posición this.drawTennisBall(obj.x, obj.y, obj.radius, obj.color); obj.lastRenderX = obj.x; obj.lastRenderY = obj.y; } }); } drawTennisBall(x, y, radius, color) { const effectiveThickness = radius * 2.2; // Más pequeña que baloncesto enqueueDrawCommand(x, y, x + 0.1, y + 0.1, color, effectiveThickness); } /* ---------- UTILITY FUNCTIONS ---------- */ clearAllTennisBalls(showFeedback = true) { this.physicsObjects.clear(); positionCache.clear(); if (drawariaCtx && drawariaCanvas) { drawariaCtx.clearRect(0, 0, drawariaCanvas.width, drawariaCanvas.height); } if (showFeedback) { this.showTennisFeedback('🗑️ ALL TENNIS BALLS CLEARED!', '#cc0000'); } } resetAllTennisBalls() { if (this.canvasElement) { this.physicsObjects.forEach(obj => { obj.x = this.canvasElement.width / 2 + (Math.random() - 0.5) * 100; obj.y = this.canvasElement.height / 2 + (Math.random() - 0.5) * 100; obj.vx = 0; obj.vy = 0; obj.spin = 0; obj.lastRenderX = -9999; obj.lastRenderY = -9999; obj.bounceCount = 0; obj.isInPlay = true; }); this.showTennisFeedback('🔄 All tennis balls reset to center court!', '#74b9ff'); } } async cleanTennisCourt() { if (!drawariaCanvas) return; this.showTennisFeedback('🧹 Cleaning Wimbledon Court...', '#e17055'); const canvasWidth = drawariaCanvas.width; const canvasHeight = drawariaCanvas.height; for (let y = 0; y < canvasHeight; y += 80) { for (let x = 0; x < canvasWidth; x += 80) { const width = Math.min(80, canvasWidth - x); const height = Math.min(80, canvasHeight - y); enqueueDrawCommand(x, y, x + width, y + height, '#FFFFFF', Math.max(width, height)); await sleep(3); } } if (drawariaCtx) { drawariaCtx.clearRect(0, 0, canvasWidth, canvasHeight); } this.showTennisFeedback('🧹 Wimbledon Court Cleaned!', '#00d084'); } /* ---------- PANEL FUNCTIONALITY ---------- */ makeTennisPanelDraggable() { const panel = document.getElementById('tennis-physics-panel'); const header = document.getElementById('tennis-panel-header'); let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0; if (header) { header.onmousedown = dragMouseDown; } else { panel.onmousedown = dragMouseDown; } function dragMouseDown(e) { e = e || window.event; e.preventDefault(); pos3 = e.clientX; pos4 = e.clientY; document.onmouseup = closeDragElement; document.onmousemove = elementDrag; panel.classList.add('panel-dragging'); } function elementDrag(e) { e = e || window.event; e.preventDefault(); pos1 = pos3 - e.clientX; pos2 = pos4 - e.clientY; pos3 = e.clientX; pos4 = e.clientY; const newTop = panel.offsetTop - pos2; const newLeft = panel.offsetLeft - pos1; const maxLeft = window.innerWidth - panel.offsetWidth; const maxTop = window.innerHeight - panel.offsetHeight; panel.style.top = Math.min(Math.max(0, newTop), maxTop) + "px"; panel.style.left = Math.min(Math.max(0, newLeft), maxLeft) + "px"; panel.style.right = 'auto'; } function closeDragElement() { document.onmouseup = null; document.onmousemove = null; panel.classList.remove('panel-dragging'); } } setupTennisPanelButtons() { const minimizeBtn = document.getElementById('tennis-minimize-btn'); const closeBtn = document.getElementById('tennis-close-btn'); const content = document.getElementById('tennis-panel-content'); const panel = document.getElementById('tennis-physics-panel'); let isMinimized = false; // MINIMIZE BUTTON minimizeBtn?.addEventListener('click', (e) => { e.stopPropagation(); if (!panel) return; if (!isMinimized) { content.style.display = 'none'; panel.style.height = 'auto'; minimizeBtn.innerHTML = '+'; isMinimized = true; this.showTennisFeedback('📱 Tennis Panel Minimized', '#32CD32'); } else { content.style.display = 'block'; panel.style.height = 'auto'; minimizeBtn.innerHTML = '−'; isMinimized = false; this.showTennisFeedback('📱 Tennis Panel Restored', '#32CD32'); } }); // CLOSE BUTTON closeBtn?.addEventListener('click', (e) => { e.stopPropagation(); if (!panel) return; if (confirm('¿Estás seguro de que quieres cerrar el motor de tenis?')) { if (this.isActive) { this.stopTennisPhysics(); } isStopped = true; panel.remove(); this.showTennisFeedback('❌ Tennis Engine Closed', '#ff4757'); console.log('🔴 Tennis Panel closed by user'); } }); // Hover effects [minimizeBtn, closeBtn].forEach(btn => { if (!btn) return; btn.addEventListener('mouseenter', () => btn.style.opacity = '0.8'); btn.addEventListener('mouseleave', () => btn.style.opacity = '1'); }); } startTennisStatsMonitoring() { setInterval(() => { document.getElementById('tennis-ball-count').textContent = this.physicsObjects.size; document.getElementById('hits-count').textContent = this.gameStats.totalHits; document.getElementById('aces-count').textContent = this.gameStats.totalAces; document.getElementById('net-hits-count').textContent = this.gameStats.netHits; document.getElementById('tennis-max-speed').textContent = Math.round(this.gameStats.maxVelocityReached * 3.6); // Convert to km/h document.getElementById('surface-info').textContent = this.controls.courtSurface.charAt(0).toUpperCase() + this.controls.courtSurface.slice(1); }, 1000); } showTennisFeedback(message, color) { const feedback = document.createElement('div'); feedback.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: ${color}; color: white; padding: 15px 25px; border-radius: 10px; font-weight: bold; z-index: 2147483648; font-size: 16px; box-shadow: 0 4px 20px rgba(0,0,0,0.3); opacity: 0; transition: opacity 0.3s ease-in-out; text-shadow: 1px 1px 3px rgba(0,0,0,0.5); border: 2px solid #FFD700; `; feedback.innerHTML = message; document.body.appendChild(feedback); setTimeout(() => feedback.style.opacity = '1', 10); setTimeout(() => feedback.style.opacity = '0', 2500); setTimeout(() => feedback.remove(), 2800); } } /* ---------- GLOBAL INITIALIZATION ---------- */ let tennisEngine = null; const initTennisEngine = () => { if (!tennisEngine) { console.log('🎾 Initializing Wimbledon Tennis Physics Engine v1.0...'); tennisEngine = new AdvancedDrawariaTennis(); setTimeout(() => { const confirmMsg = document.createElement('div'); confirmMsg.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: linear-gradient(45deg, #228B22, #32CD32); color: white; padding: 25px 35px; border-radius: 20px; font-size: 16px; font-weight: bold; z-index: 2147483648; text-align: center; box-shadow: 0 0 40px rgba(34,139,34,0.6); opacity: 0; transition: opacity 0.3s ease-in-out; border: 3px solid #FFD700; `; confirmMsg.innerHTML = ` 🎾 WIMBLEDON TENNIS ENGINE v1.0 LOADED! 🎾<br> <div style="font-size: 12px; margin-top: 10px; color: #E0FFE0;"> ✅ Professional Wimbledon Court • Tennis Physics • Spin Effects<br> ✅ Net Detection • Court Surfaces • Tennis Scoring System<br> ✅ Virtual Rackets • Match Mode • Realistic Ball Physics </div> `; document.body.appendChild(confirmMsg); setTimeout(() => confirmMsg.style.opacity = '1', 10); setTimeout(() => confirmMsg.style.opacity = '0', 4000); setTimeout(() => confirmMsg.remove(), 4300); }, 1000); } }; // Enhanced CSS for Tennis styling const tennisStyle = document.createElement('style'); tennisStyle.textContent = ` @keyframes tennis-serve { 0% { transform: scale(0) rotate(0deg); opacity: 1; } 50% { transform: scale(1.3) rotate(180deg); opacity: 0.9; } 100% { transform: scale(0) rotate(360deg); opacity: 0; } } @keyframes court-shine { 0% { box-shadow: 0 0 15px rgba(34, 139, 34, 0.3); } 50% { box-shadow: 0 0 25px rgba(50, 205, 50, 0.6); } 100% { box-shadow: 0 0 15px rgba(34, 139, 34, 0.3); } } .tennis-mode-toggle[data-active="true"] { animation: court-shine 2s infinite; } #tennis-physics-panel { transition: none !important; } #tennis-panel-header:hover { background: linear-gradient(45deg, #228B22, #32CD32) !important; } #tennis-minimize-btn:hover { background: rgba(255,255,255,0.4) !important; } #tennis-close-btn:hover { background: rgba(255,0,0,0.8) !important; } /* Tennis court specific styling */ .wimbledon-court { background: linear-gradient(45deg, #228B22, #32CD32); } .tennis-ball-spin { animation: tennis-ball-rotation 0.5s linear infinite; } @keyframes tennis-ball-rotation { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } /* Tennis match styling */ @keyframes tennis-serve-ready { 0%, 100% { transform: translateY(0) scale(1); } 50% { transform: translateY(-8px) scale(1.05); } } .tennis-serving { animation: tennis-serve-ready 1s infinite ease-in-out; } /* Status div tennis styling */ #tennis-status { font-family: 'Arial Black', Arial, sans-serif !important; animation: court-shine 3s infinite; } /* Button hover effects */ button:hover { transform: translateY(-2px) !important; transition: all 0.2s ease !important; } /* Tennis color scheme */ .tennis-green { color: #228B22; } .tennis-lime { color: #32CD32; } .tennis-white { color: #FFFFFF; } .tennis-black { color: #000000; } .tennis-brown { color: #8B4513; } /* Tennis specific animations */ @keyframes tennis-net-hit { 0% { opacity: 1; } 50% { opacity: 0.3; transform: scale(1.2); } 100% { opacity: 1; transform: scale(1); } } .tennis-net-collision { animation: tennis-net-hit 0.3s ease-out; } `; document.head.appendChild(tennisStyle); // Initialize when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initTennisEngine); } else { initTennisEngine(); } setTimeout(initTennisEngine, 2000); console.log('🎾 Advanced Wimbledon Tennis Physics Engine v1.0 loaded successfully! 🎾'); console.log('🏟️ Features: Professional Court • Tennis Physics • Spin Effects • Net Detection'); console.log('🏆 Ready for Wimbledon-style tennis matches in Drawaria!'); })();