// ==UserScript==
// @name Drawaria Pong Engine
// @namespace http://tampermonkey.net/
// @version 1.4
// @description Create a vertical and ultra-smooth Pong game on the Drawaria canvas
// @author YouTubeDrawaria
// @match https://drawaria.online/*
// @grant none
// @icon data:image/gif;base64,R0lGODlhEAAQAKEDAP+/3/9/vwAAAP///yH/C05FVFNDQVBFMi4wAwEAAAAh/glqZXN1czIwOTkAIfkEAQACAwAsAAAAABAAEAAAAkCcL5nHlgFiWE3AiMFkNnvBed42CCJgmlsnplhyonIEZ8ElQY8U66X+oZF2ogkIYcFpKI6b4uls3pyKqfGJzRYAACH5BAEIAAMALAgABQAFAAMAAAIFhI8ioAUAIfkEAQgAAwAsCAAGAAUAAgAAAgSEDHgFADs=
// @license MIT
// ==/UserScript==
(function() {
'use strict';
/* ---------- SHARED SYSTEM COMPONENTS (Optimized for Drawaria) ---------- */
let drawariaSocket = null;
let drawariaCanvas = null;
let drawariaCtx = null;
// Cola de comandos optimizada con agrupamiento inteligente
const commandQueue = [];
let batchProcessor = null;
const BATCH_SIZE = 10; // Aumentado ligeramente para más fluidez
const BATCH_INTERVAL = 40; // Reducido el intervalo para enviar a ~25 FPS de red.
// Intercepta WebSocket para capturar el socket del juego
const originalWebSocketSend = WebSocket.prototype.send;
WebSocket.prototype.send = function(...args) {
if (!drawariaSocket && this.url && this.url.includes('drawaria')) {
drawariaSocket = this;
console.log('🔗 Drawaria WebSocket capturado para el juego de Pong.');
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('⚠️ Fallo al enviar el comando:', e);
}
});
}, BATCH_INTERVAL);
}
/**
* Función unificada para encolar comandos de dibujo (Filled Shapes).
* @param {number} x1 - Coordenada X inicial (centro de la forma)
* @param {number} y1 - Coordenada Y inicial (centro de la forma)
* @param {string} color - Color del objeto (ej. '#FFFFFF')
* @param {number} thickness - Grosor efectivo (negativo para relleno en el servidor)
* @param {boolean} isServerCommand - Si se añade a la cola del servidor.
*/
function enqueueDrawCommand(x1, y1, color, thickness, isServerCommand = true) {
if (!drawariaCanvas) return;
const thickAbs = Math.abs(thickness);
// Renderizado local para retroalimentación visual inmediata (MAX FPS)
if (drawariaCtx) {
drawariaCtx.fillStyle = color; // Usar fillStyle para mejor representación de formas llenas
// Simulación de círculo (para la pelota)
if (thickAbs < 100) { // Asumimos que grosor pequeño es la pelota
drawariaCtx.beginPath();
drawariaCtx.arc(x1, y1, thickAbs / 2.5, 0, Math.PI * 2);
drawariaCtx.fill();
} else { // Simulación de rectángulo para la paleta (aunque Drawaria lo dibuje como un óvalo)
// Para la paleta, dibujamos el óvalo centrado con el grosor de la paleta.
drawariaCtx.strokeStyle = color;
drawariaCtx.lineWidth = thickAbs;
drawariaCtx.lineCap = 'butt'; // Mejor para rectángulos simulados
drawariaCtx.beginPath();
drawariaCtx.moveTo(x1, y1 - thickAbs / 2 + 0.1); // Pequeña línea vertical
drawariaCtx.lineTo(x1, y1 + thickAbs / 2 - 0.1);
drawariaCtx.stroke();
}
}
// SERVER COMMAND (para otros jugadores)
if (isServerCommand && drawariaSocket) {
const normX1 = (x1 / drawariaCanvas.width).toFixed(4);
const normY1 = (y1 / drawariaCanvas.height).toFixed(4);
// Usamos un desplazamiento mínimo para simular un "punto" para la forma rellena
const normX2 = (x1 / drawariaCanvas.width + 0.0001).toFixed(4);
const normY2 = (y1 / drawariaCanvas.height + 0.0001).toFixed(4);
// La clave: grosor negativo para forma rellena
const cmd = `42["drawcmd",0,[${normX1},${normY1},${normX2},${normY2},false,${-thickAbs},"${color}",0,0,{}]]`;
commandQueue.push(cmd);
}
}
/* ---------- PONG GAME LOGIC AND PHYSICS ---------- */
const PONG_CONSTANTS = {
PADDLE_WIDTH: 15,
PADDLE_HEIGHT: 120,
PADDLE_SPEED: 400,
BALL_RADIUS: 20,
BALL_INITIAL_SPEED: 350,
TIMESTEP: 1 / 60, // Física a 60 Hz
MAX_VELOCITY: 800,
WALL_OFFSET: 30,
BACKGROUND_COLOR: '#000000', // Tinted black
BOT_PADDLE_COLOR: '#FF0000', // Red
PLAYER_PADDLE_COLOR: '#0000FF', // Blue
BALL_COLOR: '#FFFFFF', // White
RENDER_INTERVAL: 1000 / 30, // Red a 30 FPS (para comandos)
// Grosor estandarizado (es el grosor negativo que se enviará al servidor)
BALL_THICKNESS: 20 * 2.5, // ~50
PADDLE_THICKNESS: 120 * 1.05, // ~126 (ligeramente más que la altura)
};
class VerticalPongGame {
constructor() {
this.isActive = false;
this.lastTime = 0;
this.renderTime = 0;
this.score = { player: 0, bot: 0 };
this.input = { up: false, down: false };
this.player = null;
this.bot = null;
this.ball = null;
this.W = 0;
this.H = 0;
this.init();
}
init() {
const checkGameReady = () => {
const gameCanvas = document.getElementById('canvas');
if (gameCanvas) {
drawariaCanvas = gameCanvas;
drawariaCtx = gameCanvas.getContext('2d');
this.W = drawariaCanvas.width;
this.H = drawariaCanvas.height;
this.createGamePanel();
this.setupEventListeners();
// Llama a dibujar el fondo negro una única vez al inicio
this.drawBackground();
console.log('✅ Drawaria Pong Game inicializado.');
} else {
setTimeout(checkGameReady, 100);
}
};
checkGameReady();
}
drawBackground() {
// Dibuja el fondo negro de forma PERMANENTE en el servidor.
enqueueDrawCommand(
this.W / 2, this.H / 2,
PONG_CONSTANTS.BACKGROUND_COLOR,
Math.max(this.W, this.H) * 2,
true // Enviar al servidor
);
}
// ... (createGamePanel, setupEventListeners, toggleGame, pauseGame remain the same)
createGamePanel() {
const existingPanel = document.getElementById('pong-game-panel');
if (existingPanel) existingPanel.remove();
const panel = document.createElement('div');
panel.id = 'pong-game-panel';
panel.style.cssText = `
position: fixed !important;
top: 250px !important;
right: 20px !important;
width: 250px !important;
z-index: 2147483647 !important;
background: linear-gradient(135deg, #001f3f, #000a14) !important;
border: 2px solid #0000FF !important;
border-radius: 12px !important;
color: white !important;
font-family: 'Segoe UI', Arial, sans-serif !important;
box-shadow: 0 0 20px rgba(0, 0, 255, 0.3) !important;
padding: 15px !important;
text-align: center !important;
`;
panel.innerHTML = `
<h3 style="margin-top: 0; color: #0000FF;">🔵 Vertical Pong Engine 🔴</h3>
<div style="margin-bottom: 10px; font-size: 1.2em; font-weight: bold;">
Score: <span id="player-score">0</span> - <span id="bot-score">0</span>
</div>
<button id="toggle-game" style="
width: 100%;
padding: 10px;
background: #0000FF;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
font-weight: bold;
">▶️ Start Pong</button>
<div id="game-message" style="
margin-top: 10px;
color: #FFD700;
font-weight: bold;
">Ready to Play!</div>
<div style="
margin-top: 15px;
font-size: 10px;
color: rgba(255,255,255,0.6);
text-align: left;
">
<p style="margin: 3px 0;">Control Player Paddle (Blue):</p>
<ul style="padding-left: 20px; margin: 3px 0;">
<li>ArrowUp or W: Move Up</li>
<li>ArrowDown or S: Move Down</li>
</ul>
</div>
`;
document.body.appendChild(panel);
this.makePanelDraggable(panel);
}
setupEventListeners() {
document.getElementById('toggle-game').addEventListener('click', () => this.toggleGame());
document.addEventListener('keydown', (e) => this.handleKeyInput(e, true));
document.addEventListener('keyup', (e) => this.handleKeyInput(e, false));
}
toggleGame() {
if (!this.isActive) {
this.startGame();
document.getElementById('toggle-game').textContent = '⏸️ Pause Game';
document.getElementById('toggle-game').style.background = '#FFD700';
} else {
this.pauseGame();
document.getElementById('toggle-game').textContent = '▶️ Resume Game';
document.getElementById('toggle-game').style.background = '#0000FF';
}
}
startGame() {
if (this.isActive || !drawariaCanvas) return;
this.isActive = true;
this.score = { player: 0, bot: 0 };
this.resetGameObjects();
this.updateScoreDisplay();
// Re-draw background just in case was overdrawn
this.drawBackground();
this.gameLoop(performance.now());
console.log('🚀 Pong Game Started!');
}
pauseGame() {
this.isActive = false;
document.getElementById('game-message').textContent = 'Game Paused!';
console.log('🛑 Pong Game Paused.');
}
resetGameObjects() {
const W = this.W;
const H = this.H;
const P = PONG_CONSTANTS;
// Initialize Player Paddle (Blue, Right Side)
this.player = {
id: 'player', color: P.PLAYER_PADDLE_COLOR, thickness: P.PADDLE_THICKNESS,
x: W - P.WALL_OFFSET - P.PADDLE_WIDTH / 2, y: H / 2,
W: P.PADDLE_WIDTH, H: P.PADDLE_HEIGHT,
lastRenderX: W - P.WALL_OFFSET - P.PADDLE_WIDTH / 2, lastRenderY: H / 2
};
// Initialize Bot Paddle (Red, Left Side)
this.bot = {
id: 'bot', color: P.BOT_PADDLE_COLOR, thickness: P.PADDLE_THICKNESS,
x: P.WALL_OFFSET + P.PADDLE_WIDTH / 2, y: H / 2,
W: P.PADDLE_WIDTH, H: P.PADDLE_HEIGHT,
lastRenderX: P.WALL_OFFSET + P.PADDLE_WIDTH / 2, lastRenderY: H / 2
};
this.resetBall();
}
resetBall(direction = 1) {
const W = this.W;
const H = this.H;
const P = PONG_CONSTANTS;
// Reset ball to center
this.ball = {
id: 'ball', color: P.BALL_COLOR, thickness: P.BALL_THICKNESS,
x: W / 2, y: H / 2, radius: P.BALL_RADIUS,
lastRenderX: W / 2, lastRenderY: H / 2,
// Initial velocity: Random vertical, fixed horizontal (direction * speed)
vx: direction * P.BALL_INITIAL_SPEED,
vy: (Math.random() * 2 - 1) * P.BALL_INITIAL_SPEED * 0.5
};
}
gameLoop(currentTime) {
if (!this.isActive) return;
const dt = PONG_CONSTANTS.TIMESTEP;
const timeElapsed = (currentTime - (this.lastTime || currentTime)) / 1000;
this.lastTime = currentTime;
// --- 1. LOCAL CLEAR (For local ultra-fluidity) ---
if (drawariaCtx) {
drawariaCtx.clearRect(0, 0, this.W, this.H);
}
// --- 2. Update Physics ---
this.updatePhysics(dt);
// --- 3. LOCAL RENDER (Draws locally at maximum FPS via enqueueDrawCommand) ---
this.localRender();
// --- 4. NETWORK RENDER (Sends commands to server at fixed rate) ---
if (currentTime - this.renderTime >= PONG_CONSTANTS.RENDER_INTERVAL) {
this.networkRender();
this.renderTime = currentTime;
}
requestAnimationFrame((t) => this.gameLoop(t));
}
localRender() {
// Draw background locally (optional, but ensures black background on local client)
enqueueDrawCommand(
this.W / 2, this.H / 2,
PONG_CONSTANTS.BACKGROUND_COLOR,
Math.max(this.W, this.H) * 2,
false // NO enviar al servidor
);
this.drawObject(this.player, false);
this.drawObject(this.bot, false);
this.drawObject(this.ball, false);
}
networkRender() {
const objects = [this.player, this.bot, this.ball];
objects.forEach(obj => {
const isMoved = Math.abs(obj.x - obj.lastRenderX) > 1 || Math.abs(obj.y - obj.lastRenderY) > 1;
if (isMoved || obj.lastRenderX === -9999) {
// Step 1: ERASE (Dirty Erase) at old position using background color
if (obj.lastRenderX !== -9999) {
this.drawObject(obj, true, obj.lastRenderX, obj.lastRenderY, PONG_CONSTANTS.BACKGROUND_COLOR);
}
// Step 2: DRAW at new position
this.drawObject(obj, true, obj.x, obj.y, obj.color);
// Update last render position
obj.lastRenderX = obj.x;
obj.lastRenderY = obj.y;
}
});
}
drawObject(obj, isServer, x, y, color) {
const drawX = x !== undefined ? x : obj.x;
const drawY = y !== undefined ? y : obj.y;
const drawColor = color !== undefined ? color : obj.color;
enqueueDrawCommand(drawX, drawY, drawColor, obj.thickness, isServer);
}
updatePhysics(dt) {
if (!this.ball || !this.player || !this.bot) return;
// --- 1. Update Player Paddle Position ---
let playerNewY = this.player.y;
if (this.input.up) {
playerNewY -= PONG_CONSTANTS.PADDLE_SPEED * dt;
}
if (this.input.down) {
playerNewY += PONG_CONSTANTS.PADDLE_SPEED * dt;
}
// Clamp player paddle position
const playerMinY = PONG_CONSTANTS.PADDLE_HEIGHT / 2;
const playerMaxY = this.H - PONG_CONSTANTS.PADDLE_HEIGHT / 2;
this.player.y = Math.max(playerMinY, Math.min(playerMaxY, playerNewY));
// --- 2. Update Bot Paddle Position (Simple AI) ---
const botCenter = this.bot.y;
const ballCenter = this.ball.y;
const deltaY = ballCenter - botCenter;
const botSpeed = PONG_CONSTANTS.PADDLE_SPEED * 0.7;
if (Math.abs(deltaY) > 20) {
this.bot.y += Math.sign(deltaY) * Math.min(Math.abs(deltaY), botSpeed * dt);
}
// Clamp bot paddle position
const botMinY = PONG_CONSTANTS.PADDLE_HEIGHT / 2;
const botMaxY = this.H - PONG_CONSTANTS.PADDLE_HEIGHT / 2;
this.bot.y = Math.max(botMinY, Math.min(botMaxY, this.bot.y));
// --- 3. Update Ball Position ---
this.ball.x += this.ball.vx * dt;
this.ball.y += this.ball.vy * dt;
// --- 4. Ball Wall Collision (Top/Bottom) ---
const minH = this.ball.radius;
const maxH = this.H - this.ball.radius;
if (this.ball.y < minH || this.ball.y > maxH) {
this.ball.vy *= -1;
this.ball.y = this.ball.y < minH ? minH : maxH;
}
// --- 5. Ball Paddle Collision (Player & Bot) ---
this.checkPaddleCollision(this.player);
this.checkPaddleCollision(this.bot);
// --- 6. Ball Goal Check (Left/Right) ---
this.checkGoal();
}
checkPaddleCollision(paddle) {
const ball = this.ball;
const paddleHalfH = paddle.H / 2;
// Horizontal intersection point (closest edge of the paddle to the ball)
let paddleHitX;
if (paddle === this.player) {
paddleHitX = paddle.x - paddle.W / 2; // Left edge of player paddle
} else { // bot
paddleHitX = paddle.x + paddle.W / 2; // Right edge of bot paddle
}
// Vertical check: within paddle Y-bounds
const isCollidingY = ball.y + ball.radius > paddle.y - paddleHalfH &&
ball.y - ball.radius < paddle.y + paddleHalfH;
// Horizontal check: ball hits the side of the paddle
const isCollidingX = (ball.vx > 0 && ball.x + ball.radius >= paddleHitX && paddle === this.player) ||
(ball.vx < 0 && ball.x - ball.radius <= paddleHitX && paddle === this.bot);
if (isCollidingX && isCollidingY) {
// Positional correction: Push ball out to prevent multi-hit
if (paddle === this.player) {
ball.x = paddleHitX - ball.radius;
} else {
ball.x = paddleHitX + ball.radius;
}
// 1. Reverse horizontal direction
ball.vx *= -1;
// 2. Angle/Vertical speed adjustment
const relativeIntersectY = (ball.y - paddle.y);
const normalizedRelativeIntersectionY = relativeIntersectY / paddleHalfH; // -1 to 1
const bounceAngle = normalizedRelativeIntersectionY * (Math.PI / 4);
const speed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy) * 1.05;
ball.vx = Math.cos(bounceAngle) * speed * Math.sign(ball.vx);
ball.vy = Math.sin(bounceAngle) * speed;
// 3. Limit max speed
const currentSpeed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy);
if (currentSpeed > PONG_CONSTANTS.MAX_VELOCITY) {
ball.vx = (ball.vx / currentSpeed) * PONG_CONSTANTS.MAX_VELOCITY;
ball.vy = (ball.vy / currentSpeed) * PONG_CONSTANTS.MAX_VELOCITY;
}
}
}
checkGoal() {
const W = this.W;
const ballX = this.ball.x;
if (ballX < 0) {
// Goal scored by Player (passed left side)
this.score.player++;
this.updateScoreDisplay();
document.getElementById('game-message').textContent = 'GOAL! Player Scores!';
this.resetBall(-1); // Start ball towards the bot (left)
} else if (ballX > W) {
// Goal scored by Bot (passed right side)
this.score.bot++;
this.updateScoreDisplay();
document.getElementById('game-message').textContent = 'GOAL! Bot Scores!';
this.resetBall(1); // Start ball towards the player (right)
}
}
// ... (handleKeyInput, updateScoreDisplay, makePanelDraggable remain the same)
handleKeyInput(e, isKeyDown) {
const key = e.key.toLowerCase();
if (key === 'arrowup' || key === 'w') {
this.input.up = isKeyDown;
e.preventDefault();
}
if (key === 'arrowdown' || key === 's') {
this.input.down = isKeyDown;
e.preventDefault();
}
}
updateScoreDisplay() {
document.getElementById('player-score').textContent = this.score.player;
document.getElementById('bot-score').textContent = this.score.bot;
document.getElementById('game-message').textContent = `Player: ${this.score.player} | Bot: ${this.score.bot}`;
}
makePanelDraggable(panel) {
let isDragging = false;
let currentX, currentY, initialX, initialY, xOffset = 0, yOffset = 0;
const header = panel.querySelector('h3');
if (!header) return;
const dragStart = (e) => {
initialX = e.clientX - xOffset;
initialY = e.clientY - yOffset;
isDragging = true;
panel.style.cursor = 'grabbing';
};
const dragEnd = () => {
initialX = currentX;
initialY = currentY;
isDragging = false;
panel.style.cursor = 'grab';
};
const drag = (e) => {
if (isDragging) {
e.preventDefault();
currentX = e.clientX - initialX;
currentY = e.clientY - initialY;
xOffset = currentX;
yOffset = currentY;
panel.style.transform = `translate3d(${currentX}px, ${currentY}px, 0)`;
}
};
header.style.cursor = 'grab';
header.addEventListener("mousedown", dragStart);
document.addEventListener("mouseup", dragEnd);
document.addEventListener("mousemove", drag);
}
}
// Initialization of the Pong Game
const initPongGame = () => {
const pongGame = new VerticalPongGame();
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initPongGame);
} else {
setTimeout(initPongGame, 500);
}
})();