// ==UserScript==
// @name Drawaria Racing Minigame Mod
// @namespace http://tampermonkey.net/
// @version 1.2
// @description Top-down racing minigame with polished physics and AI, overlay on drawaria.online.
// @author YouTubeDrawaria
// @include https://drawaria.online/*
// @include https://*.drawaria.online/*
// @include https://drawaria.online*
// @icon https://www.google.com/s2/favicons?sz=64&domain=drawaria.online/room/
// @grant GM_registerMenuCommand
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// =====================================================================
// CONFIGURACIÓN GLOBAL
// =====================================================================
const CONFIG = {
// ACTIVATION_KEY: 'r', // ELIMINADO: La activación ahora es por Menú de Usuario.
NUM_AI_CARS: 2, // Cantidad de autos controlados por IA
CANVAS_Z_INDEX: 10000, // Asegura que el juego esté encima de todo
LAP_GOAL: 3, // Vueltas para ganar
// Parámetros de la Física (Ajustados para jugabilidad tipo arcade)
ACCELERATION: 0.17, // Aceleración más rápida (usado en Car.update)
MAX_SPD: 6, // Velocidad máxima (usado en Car.update)
BRAKE: 0.13, // Fuerza de frenado (usado en Car.update)
FRICTION: 0.015, // Fricción mínima (usado en Car.update)
GRIP: 0.85, // Agarre: 0.85 permite drift sutil y controlable (usado en Car.update)
TURN_GAIN: 0.065, // Ganancia de giro (usado en Car.update)
MIN_TURN_SPEED: 0.45, // Mínimo giro incluso a baja velocidad (usado en Car.update)
};
// =====================================================================
// UTILIDADES DE FÍSICA Y MATEMÁTICAS
// =====================================================================
const degToRad = (deg) => deg * (Math.PI / 180);
const dist = (x1, y1, x2, y2) => Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
const normalizeAngle = (angle) => {
while (angle < 0) angle += 2 * Math.PI;
while (angle >= 2 * Math.PI) angle -= 2 * Math.PI;
return angle;
};
// =====================================================================
// CLASE VIRTUALJOYSTICK (SIN CAMBIOS)
// =====================================================================
class VirtualJoystick {
// ... [Código de VirtualJoystick sin cambios] ...
constructor(canvas) {
this.canvas = canvas;
this.active = false;
this.baseX = 0;
this.baseY = 0;
this.stickX = 0;
this.stickY = 0;
this.radius = 60;
this.maxPull = 45;
this.steering = 0;
this.thrust = 0;
this.initialBaseX = 100;
this.initialBaseY = canvas.height - 100;
this.canvas.addEventListener('touchstart', this.handleStart.bind(this), { passive: false });
this.canvas.addEventListener('touchmove', this.handleMove.bind(this), { passive: false });
this.canvas.addEventListener('touchend', this.handleEnd.bind(this), { passive: false });
this.canvas.addEventListener('touchcancel', this.handleEnd.bind(this), { passive: false });
}
handleStart(e) {
e.preventDefault();
const rect = this.canvas.getBoundingClientRect();
this.baseX = this.baseX || this.initialBaseX;
this.baseY = this.baseY || this.initialBaseY;
for (let touch of e.changedTouches) {
const touchX = touch.clientX - rect.left;
const touchY = touch.clientY - rect.top;
if (dist(touchX, touchY, this.baseX, this.baseY) < this.radius + this.maxPull) {
this.active = true;
this.stickX = touchX;
this.stickY = touchY;
break;
}
}
}
handleMove(e) {
if (!this.active) return;
e.preventDefault();
const rect = this.canvas.getBoundingClientRect();
let touch = Array.from(e.changedTouches).find(t =>
dist(t.clientX - rect.left, t.clientY - rect.top, this.baseX, this.baseY) < this.radius + this.maxPull * 2
);
if (!touch) return;
let dx = touch.clientX - rect.left - this.baseX;
let dy = touch.clientY - rect.top - this.baseY;
let distance = dist(0, 0, dx, dy);
if (distance > this.maxPull) {
dx *= this.maxPull / distance;
dy *= this.maxPull / distance;
}
this.stickX = this.baseX + dx;
this.stickY = this.baseY + dy;
this.steering = dx / this.maxPull;
this.thrust = -dy / this.maxPull;
}
handleEnd(e) {
const rect = this.canvas.getBoundingClientRect();
let endedJoystickTouch = Array.from(e.changedTouches).some(t =>
dist(t.clientX - rect.left, t.clientY - rect.top, this.baseX, this.baseY) < this.radius + this.maxPull * 2
);
if (endedJoystickTouch) {
this.active = false;
this.steering = 0;
this.thrust = 0;
this.stickX = this.baseX;
this.stickY = this.baseY;
}
}
draw(ctx) {
this.baseX = this.canvas.width * 0.1;
this.baseY = this.canvas.height * 0.85;
if (!this.active) {
this.stickX = this.baseX;
this.stickY = this.baseY;
}
ctx.beginPath();
ctx.arc(this.baseX, this.baseY, this.radius, 0, Math.PI * 2, false);
ctx.fillStyle = 'rgba(0, 0, 0, 0.4)';
ctx.fill();
ctx.beginPath();
ctx.arc(this.stickX, this.stickY, this.radius * 0.4, 0, Math.PI * 2, false);
ctx.fillStyle = 'rgba(255, 0, 0, 0.7)';
ctx.fill();
}
cleanup() {
this.canvas.removeEventListener('touchstart', this.handleStart);
this.canvas.removeEventListener('touchmove', this.handleMove);
this.canvas.removeEventListener('touchend', this.handleEnd);
this.canvas.removeEventListener('touchcancel', this.handleEnd);
}
}
// =====================================================================
// CLASE CAR (AUTO) - FÍSICA Y AI MEJORADAS
// =====================================================================
class Car {
constructor(x, y, angle, color, isPlayer = false, waypoints = []) {
this.x = x;
this.y = y;
this.angle = angle;
this.color = color;
this.isPlayer = isPlayer;
this.width = 15;
this.height = 25;
this.vx = 0;
this.vy = 0;
this.speed = 0;
this.thrustInput = 0;
this.turnLeft = false;
this.turnRight = false;
this.waypoints = waypoints;
this.currentWaypoint = 0;
this.laps = 0;
this.lastCheckpoint = 0;
this.position = 1;
this.dustTrail = [];
}
/**
* Lógica de Física (Arcade-Drift Controlable)
* Implementación de la física robusta solicitada por el usuario.
*/
update(inputs) {
let thrust = 0, turnLeft = false, turnRight = false;
if (!this.isPlayer) {
// La IA actualiza sus propios controles
this._updateAI();
thrust = this.thrustInput;
turnLeft = this.turnLeft;
turnRight = this.turnRight;
} else {
// El jugador usa los inputs
thrust = inputs.thrust;
turnLeft = inputs.turnLeft;
turnRight = inputs.turnRight;
}
// Constantes de la nueva física
const ACC = CONFIG.ACCELERATION;
const MAX_SPD = CONFIG.MAX_SPD;
const BRAKE = CONFIG.BRAKE;
const FRICTION = CONFIG.FRICTION;
const GRIP = CONFIG.GRIP;
const TURN_GAIN = CONFIG.TURN_GAIN;
const MIN_TURN_SPEED = CONFIG.MIN_TURN_SPEED;
// 1. Aceleración/Frenado (solo en alcance adelante o atrás)
let forward = Math.cos(this.angle - Math.PI/2);
let forwardY = Math.sin(this.angle - Math.PI/2);
// Ajuste de aceleración/frenado
if (thrust > 0.1) {
this.vx += forward * ACC * thrust;
this.vy += forwardY * ACC * thrust;
} else if (thrust < -0.1) {
// Frenar como aceleración inversa
this.vx -= forward * BRAKE * Math.abs(thrust);
this.vy -= forwardY * BRAKE * Math.abs(thrust);
}
// 2. Fricción - reduce todo movimiento ligeramente
this.vx *= (1 - FRICTION);
this.vy *= (1 - FRICTION);
// 3. Aplica grip para limitar el drift lateral
let speed = Math.sqrt(this.vx ** 2 + this.vy ** 2);
if (speed > 0.001) {
// Vector en la dirección del auto
let carDirX = forward;
let carDirY = forwardY;
// Proyección de la velocidad sobre el eje del auto
let dot = this.vx * carDirX + this.vy * carDirY;
let lateralVx = this.vx - (dot * carDirX);
let lateralVy = this.vy - (dot * carDirY);
// Reduce el lateral (drift)
this.vx -= lateralVx * GRIP;
this.vy -= lateralVy * GRIP;
}
// 4. Limita la velocidad máxima
let vel = Math.sqrt(this.vx ** 2 + this.vy ** 2);
if (vel > MAX_SPD) {
this.vx *= MAX_SPD / vel;
this.vy *= MAX_SPD / vel;
}
// 5. Giro - giro suave
let turn = 0;
if (turnLeft) turn -= 1;
if (turnRight) turn += 1;
// Ganancia de giro escalado con la velocidad, pero nunca cero
this.angle += turn * (TURN_GAIN * Math.max(MIN_TURN_SPEED, speed / MAX_SPD));
this.angle = normalizeAngle(this.angle);
// 6. Movimiento
this.x += this.vx;
this.y += this.vy;
// 7. Guardar para velocímetro, drift, etc.
this.speed = Math.sqrt(this.vx ** 2 + this.vy ** 2);
this._addDustTrail();
}
/**
* Lógica de AI Mejorada (Apunta directamente y modula la velocidad en curvas)
*/
_updateAI() {
if (this.waypoints.length === 0) return;
const target = this.waypoints[this.currentWaypoint];
let dx = target.x - this.x;
let dy = target.y - this.y;
const targetDist = dist(0, 0, dx, dy);
const AI_TURN_LIMIT = 0.05; // Margen de error para el giro
const AI_SLOWDOWN_DIST = 150; // Distancia para empezar a frenar
// 1. Avance de Waypoint: Solo por distancia (con más waypoints funciona bien)
if (targetDist < 40) {
this.currentWaypoint = (this.currentWaypoint + 1) % this.waypoints.length;
if (this.currentWaypoint === 0) {
this.laps++;
}
}
// 2. Ángulo Objetivo (Sin el PI/2 extra, solo apunta al waypoint)
let targetAngle = Math.atan2(dy, dx) + Math.PI / 2; // Mantener PI/2 por la orientación del sprite
targetAngle = normalizeAngle(targetAngle);
let diff = targetAngle - this.angle;
if (diff > Math.PI) diff -= 2 * Math.PI;
if (diff < -Math.PI) diff += 2 * Math.PI;
// 3. Control de Giro Suavizado (umbral de giro más amplio)
this.turnLeft = diff < -AI_TURN_LIMIT;
this.turnRight = diff > AI_TURN_LIMIT;
// 4. Modulación de Aceleración (Frenado Progresivo)
const absDiff = Math.abs(diff);
// Ajuste de aceleración: acelera menos cuanto más tenga que girar
let thrust = 1 - (absDiff / Math.PI) * 2; // 1 (recto) a -1 (giro de 180)
thrust = Math.max(0.2, thrust); // Mínimo de aceleración para no detenerse
// Frenar extra si la curva es muy cerrada y está cerca
if (absDiff > degToRad(45) && targetDist < AI_SLOWDOWN_DIST) {
thrust = Math.min(thrust, 0.4); // Forzar la velocidad a ser baja en curvas cerradas
}
this.thrustInput = thrust;
}
// ... [Los métodos _addDustTrail, draw, correctCollision son los mismos] ...
_addDustTrail() {
const slipAngle = Math.abs(normalizeAngle(this.angle - Math.atan2(this.vy, this.vx) - Math.PI / 2));
if (this.speed > 1.5 && slipAngle > degToRad(20) && Math.random() < 0.3) {
this.dustTrail.push({
x: this.x - Math.cos(this.angle - Math.PI / 2) * this.height * 0.4,
y: this.y - Math.sin(this.angle - Math.PI / 2) * this.height * 0.4,
size: 4 + Math.random() * 3,
opacity: 1
});
}
this.dustTrail = this.dustTrail.filter(p => {
p.opacity -= 0.05;
p.size *= 0.95;
return p.opacity > 0;
});
}
draw(ctx) {
this.dustTrail.forEach(p => {
ctx.fillStyle = `rgba(255, 255, 255, ${p.opacity * 0.5})`;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, 2 * Math.PI);
ctx.fill();
});
ctx.save();
ctx.translate(this.x, this.y);
ctx.rotate(this.angle);
ctx.fillStyle = 'rgba(0, 0, 0, 0.3)';
ctx.fillRect(-this.width / 2 + 2, -this.height / 2 + 2, this.width, this.height);
ctx.fillStyle = this.color;
ctx.fillRect(-this.width / 2, -this.height / 2, this.width, this.height);
ctx.fillStyle = 'rgba(255, 255, 255, 0.4)';
ctx.fillRect(-this.width / 3, -this.height / 3, this.width / 1.5, this.height / 4);
if (this.isPlayer) {
ctx.strokeStyle = 'gold';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(0, 0, this.height * 0.7, 0, 2 * Math.PI);
ctx.stroke();
}
ctx.restore();
}
correctCollision(normalX, normalY) {
const pushStrength = 0.5;
this.x += normalX * pushStrength;
this.y += normalY * pushStrength;
this.vx *= 0.8;
this.vy *= 0.8;
}
}
// =====================================================================
// CLASE RACINGGAME (CONTROLADOR PRINCIPAL)
// =====================================================================
class RacingGame {
constructor() {
this.isRunning = false;
this.overlayDiv = null;
this.canvas = null;
this.ctx = null;
this.lastTime = 0;
this.rafHandle = null;
this.width = 0;
this.height = 0;
this.trackCenterX = 0;
this.trackCenterY = 0;
this.trackOuterW = 0;
this.trackOuterH = 0;
this.trackInnerW = 0;
this.trackInnerH = 0;
this.trackCornerRadius = 0;
this.trackWidth = 100;
this.keys = {};
this.playerCar = null;
this.aiCars = [];
this.cars = [];
this.joystick = null;
this.obstacles = [];
this.checkpoints = [];
this.startTime = 0;
this.gameStatus = 'RACING';
}
start() {
if (this.isRunning) return;
this.isRunning = true;
this._setupDOM();
this._initDimensions();
this._setupListeners();
this._setupGameObjects();
this.startTime = Date.now();
this.rafHandle = requestAnimationFrame(this._gameLoop.bind(this));
}
stop() {
if (!this.isRunning) return;
this.isRunning = false;
cancelAnimationFrame(this.rafHandle);
this._cleanupListeners();
this._cleanupDOM();
}
// ... [Métodos _setupDOM, _initDimensions, _setupCheckpoints sin cambios importantes] ...
_setupDOM() {
this.overlayDiv = document.createElement('div');
this.overlayDiv.id = 'racing-overlay';
this.overlayDiv.style.cssText = `
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
z-index: ${CONFIG.CANVAS_Z_INDEX}; background-color: rgba(0, 0, 0, 0.8);
display: flex; justify-content: center; align-items: center;
`;
this.canvas = document.createElement('canvas');
this.canvas.id = 'racing-canvas';
this.overlayDiv.appendChild(this.canvas);
document.body.appendChild(this.overlayDiv);
this.ctx = this.canvas.getContext('2d');
this.joystick = new VirtualJoystick(this.canvas);
}
_initDimensions() {
this.width = window.innerWidth * 0.95;
this.height = window.innerHeight * 0.95;
this.canvas.width = this.width;
this.canvas.height = this.height;
this.trackCenterX = this.width / 2;
this.trackCenterY = this.height / 2;
const trackSize = Math.min(this.width, this.height) * 0.8;
this.trackOuterW = trackSize * 1.5;
this.trackOuterH = trackSize;
this.trackCornerRadius = Math.min(this.trackOuterW, this.trackOuterH) * 0.15;
this.trackWidth = 100;
this.trackInnerW = this.trackOuterW - 2 * this.trackWidth;
this.trackInnerH = this.trackOuterH - 2 * this.trackWidth;
this.joystick.initialBaseY = this.height - 100;
this._setupCheckpoints();
}
_setupCheckpoints() {
const gap = 10;
this.checkpoints = [
// 0: Start/Finish
{ x1: this.trackCenterX - 50, y1: this.trackCenterY + this.trackOuterH / 2 - gap, x2: this.trackCenterX + 50, y2: this.trackCenterY + this.trackOuterH / 2 - gap, hit: false, isStart: true },
// 1: Mid-Right
{ x1: this.trackCenterX + this.trackOuterW / 2 - gap, y1: this.trackCenterY - 50, x2: this.trackCenterX + this.trackOuterW / 2 - gap, y2: this.trackCenterY + 50, hit: false, isStart: false },
// 2: Mid-Top
{ x1: this.trackCenterX - 50, y1: this.trackCenterY - this.trackOuterH / 2 + gap, x2: this.trackCenterX + 50, y2: this.trackCenterY - this.trackOuterH / 2 + gap, hit: false, isStart: false },
// 3: Mid-Left
{ x1: this.trackCenterX - this.trackOuterW / 2 + gap, y1: this.trackCenterY - 50, x2: this.trackCenterX - this.trackOuterW / 2 + gap, y2: this.trackCenterY + 50, hit: false, isStart: false },
];
}
/**
* Generación de 8 Waypoints para una IA más suave (Mejorado)
*/
_setupGameObjects() {
const trackW_Half = this.trackOuterW / 2 - this.trackWidth / 2;
const trackH_Half = this.trackOuterH / 2 - this.trackWidth / 2;
// 8 Waypoints para una mejor navegación del óvalo
const waypoints = [
{ x: this.trackCenterX, y: this.trackCenterY - trackH_Half }, // Top-Center (0)
{ x: this.trackCenterX + trackW_Half / 2, y: this.trackCenterY - trackH_Half / 2 }, // Top-Right (1)
{ x: this.trackCenterX + trackW_Half, y: this.trackCenterY }, // Mid-Right (2)
{ x: this.trackCenterX + trackW_Half / 2, y: this.trackCenterY + trackH_Half / 2 }, // Bottom-Right (3)
{ x: this.trackCenterX, y: this.trackCenterY + trackH_Half }, // Bottom-Center (4) - START/FINISH
{ x: this.trackCenterX - trackW_Half / 2, y: this.trackCenterY + trackH_Half / 2 }, // Bottom-Left (5)
{ x: this.trackCenterX - trackW_Half, y: this.trackCenterY }, // Mid-Left (6)
{ x: this.trackCenterX - trackW_Half / 2, y: this.trackCenterY - trackH_Half / 2 }, // Top-Left (7)
];
const startY = this.trackCenterY + trackH_Half;
const startAngle = 0; // Mirando hacia arriba
this.playerCar = new Car(this.trackCenterX - 20, startY, startAngle, '#1E90FF', true);
this.cars = [this.playerCar];
for (let i = 0; i < CONFIG.NUM_AI_CARS; i++) {
const colors = ['#FFD700', '#3CB371'];
const aiCar = new Car(this.trackCenterX + (i * 20), startY + 10, startAngle, colors[i % colors.length], false, waypoints);
// La IA debe empezar en el waypoint 4 (Bottom-Center)
aiCar.currentWaypoint = 4;
this.aiCars.push(aiCar);
this.cars.push(aiCar);
}
this.cars.forEach(car => car.lastCheckpoint = this.checkpoints.length - 1);
this.obstacles = [
{ x: this.trackCenterX, y: this.trackCenterY - this.trackInnerH / 4, radius: 20 },
{ x: this.trackCenterX, y: this.trackCenterY + this.trackInnerH / 4, radius: 20 },
{ x: this.trackCenterX + 20, y: this.trackCenterY + 10, radius: 20 }
];
// ELIMINADO: this._setupButtons(); // Botones gestionados por GM_registerMenuCommand
}
// ELIMINADO: _setupButtons() { /* Lógica del botón de salir eliminada */ }
_setupListeners() {
this.boundKeyDown = this._handleKeyDown.bind(this);
this.boundKeyUp = this._handleKeyUp.bind(this);
this.boundResize = this._initDimensions.bind(this);
// ELIMINADO: this.boundKeyToggle = this._handleGlobalToggle.bind(this);
document.addEventListener('keydown', this.boundKeyDown);
document.addEventListener('keyup', this.boundKeyUp);
window.addEventListener('resize', this.boundResize);
// ELIMINADO: document.addEventListener('keydown', this.boundKeyToggle);
}
_cleanupListeners() {
document.removeEventListener('keydown', this.boundKeyDown);
document.removeEventListener('keyup', this.boundKeyUp);
window.removeEventListener('resize', this.boundResize);
// ELIMINADO: document.removeEventListener('keydown', this.boundKeyToggle);
if (this.joystick) this.joystick.cleanup();
}
_cleanupDOM() {
if (this.overlayDiv && this.overlayDiv.parentNode) {
this.overlayDiv.parentNode.removeChild(this.overlayDiv);
}
// Eliminada la lógica para ocultar el botón de Reiniciar, ya que no se crea.
}
_handleKeyDown(e) {
if (e.key === 'Escape') {
this.stop();
return;
}
switch (e.key.toLowerCase()) {
case 'arrowup': case 'w': this.keys.accelerate = true; break;
case 'arrowdown': case 's': this.keys.brake = true; break;
case 'arrowleft': case 'a': this.keys.turnLeft = true; break;
case 'arrowright': case 'd': this.keys.turnRight = true; break;
}
}
_handleKeyUp(e) {
switch (e.key.toLowerCase()) {
case 'arrowup': case 'w': this.keys.accelerate = false; break;
case 'arrowdown': case 's': this.keys.brake = false; break;
case 'arrowleft': case 'a': this.keys.turnLeft = false; break;
case 'arrowright': case 'd': this.keys.turnRight = false; break;
}
}
_getCarInputs(car) {
if (car.isPlayer && this.joystick && this.joystick.active) {
return {
thrust: this.joystick.thrust,
turnLeft: this.joystick.steering < -0.1,
turnRight: this.joystick.steering > 0.1
};
} else if (car.isPlayer) {
return {
thrust: this.keys.accelerate ? 1 : (this.keys.brake ? -1 : 0),
turnLeft: this.keys.turnLeft,
turnRight: this.keys.turnRight
};
}
return {};
}
// ELIMINADO: _handleGlobalToggle(e) { ... }
// ... [Los métodos _gameLoop, _update, _checkCollisions, _checkLapProgress, _draw, _drawTrack, _drawRoundedRect, _drawStylizedObstacle, _drawUI, _drawWinScreen siguen aquí] ...
_gameLoop(time) {
if (!this.isRunning) return;
const deltaTime = (time - this.lastTime) / (1000 / 60);
this.lastTime = time;
this._update(deltaTime);
this._draw();
this.rafHandle = requestAnimationFrame(this._gameLoop.bind(this));
}
_update(deltaTime) {
if (this.gameStatus === 'FINISHED') return;
this.cars.forEach(car => {
const inputs = this._getCarInputs(car);
car.update(inputs, deltaTime);
});
this.cars.forEach(car => this._checkCollisions(car));
this.cars.forEach(car => this._checkLapProgress(car));
this.cars.sort((a, b) => {
if (a.laps !== b.laps) return b.laps - a.laps;
if (a.currentWaypoint !== b.currentWaypoint) return b.currentWaypoint - a.currentWaypoint;
return dist(a.x, a.y, b.x, b.y);
});
this.cars.forEach((car, index) => car.position = index + 1);
if (this.playerCar.laps >= CONFIG.LAP_GOAL) {
this.gameStatus = 'FINISHED';
}
}
_checkCollisions(car) {
const w = this.trackOuterW / 2;
const h = this.trackOuterH / 2;
const innerW = this.trackInnerW / 2;
const innerH = this.trackInnerH / 2;
const x = this.trackCenterX;
const y = this.trackCenterY;
if (car.x > x - innerW && car.x < x + innerW && car.y > y - innerH && car.y < y + innerH) {
const angle = Math.atan2(car.y - y, car.x - x);
car.correctCollision(Math.cos(angle), Math.sin(angle));
}
const trackW = this.trackOuterW / 2;
const trackH = this.trackOuterH / 2;
if (car.x < x - trackW || car.x > x + trackW || car.y < y - trackH || car.y > y + trackH) {
const angle = Math.atan2(y - car.y, x - car.x);
car.correctCollision(Math.cos(angle), Math.sin(angle));
}
this.obstacles.forEach(obs => {
if (dist(car.x, car.y, obs.x, obs.y) < obs.radius + car.width / 2) {
car.speed *= 0.6;
const angle = Math.atan2(car.y - obs.y, car.x - obs.x);
car.x += Math.cos(angle) * 5;
car.y += Math.sin(angle) * 5;
}
});
}
_checkLapProgress(car) {
const cpIndex = (car.lastCheckpoint + 1) % this.checkpoints.length;
const cp = this.checkpoints[cpIndex];
const hitDistance = 15;
if (car.x > cp.x1 - hitDistance && car.x < cp.x2 + hitDistance &&
car.y > cp.y1 - hitDistance && car.y < cp.y2 + hitDistance) {
if (cp.isStart && car.lastCheckpoint === this.checkpoints.length - 1) {
car.laps++;
car.lastCheckpoint = 0;
} else if (!cp.isStart && cpIndex === car.lastCheckpoint + 1) {
car.lastCheckpoint = cpIndex;
}
}
}
_draw() {
this.ctx.clearRect(0, 0, this.width, this.height);
this._drawTrack();
this.cars.forEach(car => car.draw(this.ctx));
if ('ontouchstart' in window) {
this.joystick.draw(this.ctx);
}
this._drawUI();
if (this.gameStatus === 'FINISHED') {
this._drawWinScreen();
}
}
_drawTrack() {
const ctx = this.ctx;
const x = this.trackCenterX;
const y = this.trackCenterY;
const w = this.trackOuterW;
const h = this.trackOuterH;
const r = this.trackCornerRadius;
const innerW = this.trackInnerW;
const innerH = this.trackInnerH;
const borderWidth = 10;
const outerBorder = 20;
ctx.fillStyle = '#D4C49F';
ctx.fillRect(0, 0, this.width, this.height);
ctx.fillStyle = 'rgba(0, 0, 0, 0.05)';
for (let i = 0; i < this.width; i += 20) {
ctx.beginPath();
ctx.moveTo(i, 0);
ctx.lineTo(i + 10, this.height);
ctx.stroke();
}
ctx.fillStyle = '#AA8A60';
this._drawRoundedRect(x, y, w, h, r);
ctx.fill();
ctx.strokeStyle = '#FF0000';
ctx.lineWidth = outerBorder;
this._drawRoundedRect(x, y, w + outerBorder, h + outerBorder, r + outerBorder / 2, true);
ctx.strokeStyle = '#FFFFFF';
ctx.setLineDash([outerBorder * 2, outerBorder * 2]);
this._drawRoundedRect(x, y, w + outerBorder, h + outerBorder, r + outerBorder / 2, true);
ctx.setLineDash([]);
ctx.strokeStyle = '#0000FF';
ctx.lineWidth = borderWidth;
this._drawRoundedRect(x, y, innerW, innerH, r * 0.5, true);
ctx.strokeStyle = '#FFFFFF';
ctx.setLineDash([borderWidth * 2, borderWidth * 2]);
this._drawRoundedRect(x, y, innerW, innerH, r * 0.5, true);
ctx.setLineDash([]);
ctx.fillStyle = '#C2B280';
this._drawRoundedRect(x, y, innerW - borderWidth, innerH - borderWidth, r * 0.5);
ctx.fill();
const cp = this.checkpoints[0];
ctx.strokeStyle = 'white';
ctx.lineWidth = 6;
ctx.setLineDash([10, 10]);
ctx.beginPath();
ctx.moveTo(cp.x1, cp.y1);
ctx.lineTo(cp.x2, cp.y2);
ctx.stroke();
ctx.setLineDash([]);
this.obstacles.forEach(obs => this._drawStylizedObstacle(ctx, obs));
}
_drawRoundedRect(x, y, w, h, r, stroke = false) {
const ctx = this.ctx;
const x0 = x - w / 2;
const y0 = y - h / 2;
ctx.beginPath();
ctx.moveTo(x0 + r, y0);
ctx.arcTo(x0 + w, y0, x0 + w, y0 + h, r);
ctx.arcTo(x0 + w, y0 + h, x0, y0 + h, r);
ctx.arcTo(x0, y0 + h, x0, y0, r);
ctx.arcTo(x0, y0, x0 + w, y0, r);
ctx.closePath();
if (stroke) ctx.stroke();
}
_drawStylizedObstacle(ctx, obs) {
ctx.save();
ctx.translate(obs.x, obs.y);
ctx.beginPath();
ctx.arc(4, 4, obs.radius, 0, 2 * Math.PI);
ctx.fillStyle = 'rgba(0, 0, 0, 0.2)';
ctx.fill();
ctx.fillStyle = '#3CB371';
ctx.fillRect(-8, -obs.radius, 16, obs.radius * 2);
ctx.fillRect(-obs.radius, -8, obs.radius * 2, 16);
ctx.fillStyle = 'rgba(255, 255, 255, 0.4)';
ctx.fillRect(-4, -obs.radius, 8, obs.radius * 2 * 0.3);
ctx.restore();
}
_drawUI() {
const ctx = this.ctx;
const timeElapsed = (Date.now() - this.startTime) / 1000;
const speedKPH = (this.playerCar.speed * 10).toFixed(1);
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
ctx.fillRect(0, 0, 250, 150);
ctx.fillStyle = 'white';
ctx.font = 'bold 24px Arial';
ctx.fillText(`POSICIÓN: ${this.playerCar.position}/${this.cars.length}`, 10, 30);
ctx.font = '20px Arial';
ctx.fillText(`VUELTAS: ${this.playerCar.laps}/${CONFIG.LAP_GOAL}`, 10, 60);
ctx.fillText(`TIEMPO: ${timeElapsed.toFixed(2)}s`, 10, 90);
ctx.fillText(`VELOCIDAD: ${speedKPH} Km/h`, 10, 120);
const mapSize = 150;
const mapX = this.width - mapSize - 10;
const mapY = 10;
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
ctx.fillRect(mapX, mapY, mapSize, mapSize);
const mapScaleX = mapSize / this.trackOuterW;
const mapScaleY = mapSize / this.trackOuterH;
ctx.save();
ctx.translate(mapX + mapSize / 2, mapY + mapSize / 2);
ctx.fillStyle = '#AA8A60';
this._drawRoundedRect(0, 0, this.trackOuterW * mapScaleX, this.trackOuterH * mapScaleY, this.trackCornerRadius * mapScaleX);
ctx.fill();
ctx.fillStyle = '#C2B280';
this._drawRoundedRect(0, 0, this.trackInnerW * mapScaleX, this.trackInnerH * mapScaleY, this.trackCornerRadius * mapScaleX * 0.5);
ctx.fill();
this.cars.forEach(car => {
const mapCarX = (car.x - this.trackCenterX) * mapScaleX;
const mapCarY = (car.y - this.trackCenterY) * mapScaleY;
ctx.fillStyle = car.isPlayer ? 'gold' : car.color;
ctx.beginPath();
ctx.arc(mapCarX, mapCarY, 3, 0, 2 * Math.PI);
ctx.fill();
});
ctx.restore();
}
_drawWinScreen() {
const ctx = this.ctx;
const centerX = this.width / 2;
const centerY = this.height / 2;
const finalTime = ((Date.now() - this.startTime) / 1000).toFixed(2);
ctx.fillStyle = 'rgba(0, 0, 0, 0.8)';
ctx.fillRect(0, 0, this.width, this.height);
ctx.fillStyle = '#FFD700';
ctx.font = 'bold 72px Arial';
ctx.textAlign = 'center';
ctx.fillText('CARRERA TERMINADA', centerX, centerY - 50);
ctx.fillStyle = 'white';
ctx.font = '40px Arial';
ctx.fillText(`Tu Tiempo: ${finalTime} segundos`, centerX, centerY + 20);
ctx.fillText(`Posición Final: ${this.playerCar.position}/${this.cars.length}`, centerX, centerY + 70);
// ELIMINADA toda la lógica de creación y visualización del botón de Reiniciar.
ctx.font = '30px Arial';
ctx.fillText(`Usa el menú de Tampermonkey para Iniciar, Detener o Reiniciar.`, centerX, centerY + 140);
}
}
// =====================================================================
// INICIADOR DEL SCRIPT MODIFICADO
// =====================================================================
const gameInstance = new RacingGame();
/**
* Función para iniciar el juego.
*/
function startGame() {
if (!gameInstance.isRunning) {
gameInstance.start();
}
}
/**
* Función para detener el juego.
*/
function stopGame() {
if (gameInstance.isRunning) {
gameInstance.stop();
}
}
/**
* Función para reiniciar la carrera. Solo funciona si el juego está activo.
*/
function restartGame() {
if (gameInstance.isRunning) {
gameInstance.stop();
// Pequeño retraso para asegurar que el DOM se limpie completamente antes de reiniciar
setTimeout(() => gameInstance.start(), 50);
} else {
gameInstance.start(); // Si no está corriendo, simplemente inicia.
}
}
function initRacingGame() {
// Registrar comandos de menú para iniciar, detener y reiniciar el juego
if (typeof GM_registerMenuCommand !== 'undefined') {
GM_registerMenuCommand("Drawaria Racing: Iniciar Carrera", startGame);
GM_registerMenuCommand("Drawaria Racing: Detener Carrera", stopGame);
GM_registerMenuCommand("Drawaria Racing: Reiniciar Carrera", restartGame);
} else {
console.error('GM_registerMenuCommand no está disponible. Asegúrate de que el script tiene el permiso @grant.');
}
console.log(`Drawaria Racing Minigame v3: Usa el menú de usuario de Tampermonkey para iniciar, detener y reiniciar la carrera.`);
}
window.addEventListener('load', () => {
setTimeout(initRacingGame, 100);
});
window.drawariaRacingGameInstance = gameInstance;
})();