Drawaria Physics Engine Tennis🥎

Advanced tennis physics with professional Wimbledon court and spin system!

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

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

})();