- (() => {
- 'use strict';
- const canvas = document.getElementById('pinball-canvas');
- if (!canvas) return;
- const ctx = canvas.getContext('2d');
- const config = window.PINBALL_CONFIG || {};
- const labels = config.labels || {};
- const scoreEl = document.getElementById('pinball-score');
- const bestEl = document.getElementById('pinball-best');
- const ballsEl = document.getElementById('pinball-balls');
- const multiplierEl = document.getElementById('pinball-multiplier');
- const messageEl = document.getElementById('pinball-message');
- const statusEl = document.getElementById('pinball-status');
- const startButton = document.getElementById('pinball-start');
- const WIDTH = 900;
- const HEIGHT = 1200;
- const FIXED_STEP = 1 / 120;
- const BALL_RADIUS = 15;
- const GRAVITY = 470;
- const MAX_SPEED = 1120;
- const game = {
- running: false,
- waitingLaunch: false,
- score: 0,
- best: Number(localStorage.getItem('pinballBest') || 0),
- balls: 3,
- multiplier: 1,
- combo: 0,
- comboTimer: 0,
- accumulator: 0,
- lastTime: 0,
- shake: 0,
- flash: 0,
- ball: null,
- particles: [],
- keys: { left: false, right: false },
- flippers: {
- left: { pivot: { x: 355, y: 1010 }, length: 84, angle: 0.22, rest: 0.22, active: -0.43, pressed: false },
- right: { pivot: { x: 545, y: 1010 }, length: 84, angle: Math.PI - 0.22, rest: Math.PI - 0.22, active: Math.PI + 0.43, pressed: false }
- }
- };
- const bumpers = [
- { x: 300, y: 335, r: 54, value: 100, color: '#ff0049', pulse: 0 },
- { x: 450, y: 280, r: 56, value: 150, color: '#16d9ff', pulse: 0 },
- { x: 600, y: 335, r: 54, value: 100, color: '#ff4fd8', pulse: 0 },
- { x: 450, y: 510, r: 46, value: 200, color: '#8c3cff', pulse: 0 }
- ];
- const targets = [
- { x: 205, y: 540, w: 24, h: 78, hit: false },
- { x: 670, y: 540, w: 24, h: 78, hit: false },
- { x: 258, y: 690, w: 65, h: 18, hit: false },
- { x: 577, y: 690, w: 65, h: 18, hit: false }
- ];
- const walls = [
- [145, 112, 90, 870], [145, 112, 720, 112], [750, 260, 750, 1040],
- [90, 870, 250, 1115], [810, 870, 650, 1115],
- [190, 185, 175, 570], [710, 185, 725, 570],
- [175, 570, 255, 760], [725, 570, 645, 760],
- [255, 760, 300, 900], [645, 760, 600, 900],
- [750, 190, 840, 190], [840, 190, 840, 1050]
- ];
- function clamp(value, min, max) {
- return Math.max(min, Math.min(max, value));
- }
- function formatScore(value) {
- return Math.max(0, Math.floor(value)).toString().padStart(6, '0');
- }
- function updateHud() {
- scoreEl.textContent = formatScore(game.score);
- bestEl.textContent = formatScore(game.best);
- ballsEl.textContent = String(game.balls);
- multiplierEl.textContent = `x${game.multiplier}`;
- }
- function showMessage(title, subtitle) {
- messageEl.querySelector('strong').textContent = title;
- messageEl.querySelector('span').textContent = subtitle || '';
- messageEl.classList.add('is-visible');
- }
- function hideMessage() {
- messageEl.classList.remove('is-visible');
- }
- function newBall() {
- game.ball = {
- x: 795,
- y: 1010,
- vx: 0,
- vy: 0,
- r: BALL_RADIUS,
- trail: [],
- launching: false,
- launchTime: 0
- };
- game.waitingLaunch = true;
- statusEl.textContent = labels.launchReady || 'Press Space to launch';
- }
- function startGame() {
- game.score = 0;
- game.balls = 3;
- game.multiplier = 1;
- game.combo = 0;
- targets.forEach((target) => { target.hit = false; });
- game.running = true;
- hideMessage();
- startButton.textContent = labels.restart || 'Play again';
- newBall();
- updateHud();
- }
- function launchBall() {
- if (!game.running || !game.waitingLaunch || !game.ball) return;
- game.waitingLaunch = false;
- game.ball.x = 795;
- game.ball.y = 1010;
- /* Lanzamiento guiado: evita que la bola quede atrapada en el carril. */
- game.ball.vx = 0;
- game.ball.vy = 0;
- game.ball.launching = true;
- game.ball.launchTime = 0;
- statusEl.textContent = '';
- burst(game.ball.x, game.ball.y, '#16d9ff', 14);
- }
- function loseBall() {
- game.balls -= 1;
- updateHud();
- if (game.balls > 0) {
- window.setTimeout(newBall, 650);
- return;
- }
- endGame();
- }
- async function endGame() {
- game.running = false;
- game.waitingLaunch = false;
- const isBest = game.score > game.best;
- if (isBest) {
- game.best = game.score;
- localStorage.setItem('pinballBest', String(game.best));
- }
- updateHud();
- showMessage(labels.gameOver || 'Game over', isBest ? (labels.newBest || 'New high score!') : formatScore(game.score));
- await saveScore(game.score);
- }
- function addScore(base, x, y, color) {
- game.combo += 1;
- game.comboTimer = 2.5;
- if (game.combo >= 12) game.multiplier = 5;
- else if (game.combo >= 7) game.multiplier = 3;
- else if (game.combo >= 3) game.multiplier = 2;
- game.score += base * game.multiplier;
- game.flash = 0.16;
- burst(x, y, color, 12);
- updateHud();
- }
- function burst(x, y, color, count) {
- for (let i = 0; i < count; i += 1) {
- const angle = Math.random() * Math.PI * 2;
- const speed = 80 + Math.random() * 260;
- game.particles.push({
- x, y,
- vx: Math.cos(angle) * speed,
- vy: Math.sin(angle) * speed,
- life: 0.35 + Math.random() * 0.45,
- maxLife: 0.8,
- color
- });
- }
- }
- function closestPointOnSegment(px, py, ax, ay, bx, by) {
- const abx = bx - ax;
- const aby = by - ay;
- const lengthSq = abx * abx + aby * aby || 1;
- const t = clamp(((px - ax) * abx + (py - ay) * aby) / lengthSq, 0, 1);
- return { x: ax + abx * t, y: ay + aby * t, t };
- }
- function collideSegment(ball, wall, bounce = 0.84, boost = 0) {
- const [ax, ay, bx, by] = wall;
- const point = closestPointOnSegment(ball.x, ball.y, ax, ay, bx, by);
- let dx = ball.x - point.x;
- let dy = ball.y - point.y;
- let distance = Math.hypot(dx, dy);
- if (distance >= ball.r + 5) return false;
- if (distance < 0.001) {
- dx = -(by - ay);
- dy = bx - ax;
- distance = Math.hypot(dx, dy);
- }
- const nx = dx / distance;
- const ny = dy / distance;
- const overlap = ball.r + 5 - distance;
- ball.x += nx * overlap;
- ball.y += ny * overlap;
- const velocityAlongNormal = ball.vx * nx + ball.vy * ny;
- if (velocityAlongNormal < 0) {
- ball.vx -= (1 + bounce) * velocityAlongNormal * nx;
- ball.vy -= (1 + bounce) * velocityAlongNormal * ny;
- ball.vx += nx * boost;
- ball.vy += ny * boost;
- }
- return true;
- }
- function collideBumper(ball, bumper) {
- const dx = ball.x - bumper.x;
- const dy = ball.y - bumper.y;
- const distance = Math.hypot(dx, dy) || 1;
- const minimum = ball.r + bumper.r;
- if (distance >= minimum) return;
- const nx = dx / distance;
- const ny = dy / distance;
- ball.x = bumper.x + nx * minimum;
- ball.y = bumper.y + ny * minimum;
- const speed = Math.max(520, Math.hypot(ball.vx, ball.vy) * 1.05);
- ball.vx = nx * speed;
- ball.vy = ny * speed;
- bumper.pulse = 1;
- game.shake = 7;
- addScore(bumper.value, bumper.x, bumper.y, bumper.color);
- }
- function flipperSegment(flipper) {
- return [
- flipper.pivot.x,
- flipper.pivot.y,
- flipper.pivot.x + Math.cos(flipper.angle) * flipper.length,
- flipper.pivot.y + Math.sin(flipper.angle) * flipper.length
- ];
- }
- function updateFlipper(flipper, dt, isLeft) {
- const target = flipper.pressed ? flipper.active : flipper.rest;
- const difference = Math.atan2(Math.sin(target - flipper.angle), Math.cos(target - flipper.angle));
- flipper.angle += difference * Math.min(1, dt * 28);
- if (game.ball && collideSegment(game.ball, flipperSegment(flipper), 0.92, flipper.pressed ? 390 : 70)) {
- if (flipper.pressed) {
- game.ball.vy -= 260;
- game.ball.vx += isLeft ? 70 : -70;
- addScore(15, game.ball.x, game.ball.y, '#ffffff');
- }
- }
- }
- function collideTarget(ball, target) {
- const nearestX = clamp(ball.x, target.x, target.x + target.w);
- const nearestY = clamp(ball.y, target.y, target.y + target.h);
- const dx = ball.x - nearestX;
- const dy = ball.y - nearestY;
- if (dx * dx + dy * dy > ball.r * ball.r) return;
- target.hit = true;
- ball.vx *= -1.03;
- ball.vy -= 80;
- addScore(250, target.x + target.w / 2, target.y + target.h / 2, '#ffcc33');
- if (targets.every((item) => item.hit)) {
- game.balls += 1;
- targets.forEach((item) => { item.hit = false; });
- statusEl.textContent = 'EXTRA BALL';
- updateHud();
- }
- }
- function update(dt) {
- game.flippers.left.pressed = game.keys.left;
- game.flippers.right.pressed = game.keys.right;
- updateFlipper(game.flippers.left, dt, true);
- updateFlipper(game.flippers.right, dt, false);
- bumpers.forEach((bumper) => { bumper.pulse = Math.max(0, bumper.pulse - dt * 4); });
- game.flash = Math.max(0, game.flash - dt);
- game.shake = Math.max(0, game.shake - dt * 30);
- game.comboTimer -= dt;
- if (game.comboTimer <= 0 && game.combo > 0) {
- game.combo = 0;
- game.multiplier = 1;
- updateHud();
- }
- game.particles.forEach((particle) => {
- particle.x += particle.vx * dt;
- particle.y += particle.vy * dt;
- particle.vy += 180 * dt;
- particle.life -= dt;
- });
- game.particles = game.particles.filter((particle) => particle.life > 0);
- const ball = game.ball;
- if (!game.running || !ball || game.waitingLaunch) return;
- if (ball.launching) {
- ball.launchTime += dt;
- if (ball.launchTime < 0.72) {
- const progress = ball.launchTime / 0.72;
- ball.x = 795;
- ball.y = 1010 + (225 - 1010) * progress;
- } else if (ball.launchTime < 0.98) {
- const progress = (ball.launchTime - 0.72) / 0.26;
- const eased = progress * progress * (3 - 2 * progress);
- ball.x = 795 + (680 - 795) * eased;
- ball.y = 225 + (245 - 225) * eased;
- } else {
- ball.launching = false;
- ball.x = 680;
- ball.y = 245;
- ball.vx = -360;
- ball.vy = 135;
- }
- ball.trail.unshift({ x: ball.x, y: ball.y });
- ball.trail.length = Math.min(ball.trail.length, 13);
- return;
- }
- ball.vy += GRAVITY * dt;
- const speed = Math.hypot(ball.vx, ball.vy);
- if (speed > MAX_SPEED) {
- ball.vx = ball.vx / speed * MAX_SPEED;
- ball.vy = ball.vy / speed * MAX_SPEED;
- }
- ball.x += ball.vx * dt;
- ball.y += ball.vy * dt;
- ball.trail.unshift({ x: ball.x, y: ball.y });
- ball.trail.length = Math.min(ball.trail.length, 13);
- walls.forEach((wall) => collideSegment(ball, wall));
- bumpers.forEach((bumper) => collideBumper(ball, bumper));
- targets.forEach((target) => collideTarget(ball, target));
- /*
- * Perímetro físico de seguridad.
- * Los raíles decorativos tienen pequeñas separaciones; estas barreras
- * impiden que una bola rápida atraviese esos huecos.
- */
- if (ball.x - ball.r < 92) {
- ball.x = 92 + ball.r;
- ball.vx = Math.abs(ball.vx) * 0.86;
- }
- if (ball.x + ball.r > 742) {
- ball.x = 742 - ball.r;
- ball.vx = -Math.abs(ball.vx) * 0.86;
- }
- if (ball.y - ball.r < 112) {
- ball.y = 112 + ball.r;
- ball.vy = Math.abs(ball.vy) * 0.86;
- }
- if (!Number.isFinite(ball.x) || !Number.isFinite(ball.y)) {
- game.ball = null;
- loseBall();
- return;
- }
- if (ball.y > HEIGHT + 40) {
- game.ball = null;
- loseBall();
- }
- }
- function roundedRect(x, y, width, height, radius, fill, stroke) {
- ctx.beginPath();
- ctx.roundRect(x, y, width, height, radius);
- if (fill) { ctx.fillStyle = fill; ctx.fill(); }
- if (stroke) { ctx.strokeStyle = stroke; ctx.stroke(); }
- }
- function drawNeonLine(points, color, width = 8, glow = 20) {
- ctx.save();
- ctx.lineCap = 'round';
- ctx.lineJoin = 'round';
- ctx.strokeStyle = color;
- ctx.lineWidth = width;
- ctx.shadowColor = color;
- ctx.shadowBlur = glow;
- ctx.beginPath();
- points.forEach((point, index) => {
- if (index === 0) ctx.moveTo(point[0], point[1]);
- else ctx.lineTo(point[0], point[1]);
- });
- ctx.stroke();
- ctx.restore();
- }
- function drawRamp(points, color) {
- ctx.save();
- ctx.lineCap = 'round';
- ctx.lineJoin = 'round';
- ctx.strokeStyle = 'rgba(5, 3, 16, 0.88)';
- ctx.lineWidth = 34;
- ctx.beginPath();
- points.forEach((point, index) => {
- if (index === 0) ctx.moveTo(point[0], point[1]);
- else ctx.lineTo(point[0], point[1]);
- });
- ctx.stroke();
- ctx.strokeStyle = color;
- ctx.lineWidth = 6;
- ctx.shadowColor = color;
- ctx.shadowBlur = 20;
- ctx.stroke();
- ctx.restore();
- }
- function drawSlingShot(points, color) {
- ctx.save();
- ctx.fillStyle = 'rgba(17, 8, 35, 0.9)';
- ctx.strokeStyle = color;
- ctx.lineWidth = 7;
- ctx.shadowColor = color;
- ctx.shadowBlur = 20;
- ctx.beginPath();
- ctx.moveTo(points[0][0], points[0][1]);
- points.slice(1).forEach((point) => ctx.lineTo(point[0], point[1]));
- ctx.closePath();
- ctx.fill();
- ctx.stroke();
- ctx.restore();
- }
- function drawArrow(x, y, rotation, color) {
- ctx.save();
- ctx.translate(x, y);
- ctx.rotate(rotation);
- ctx.fillStyle = color;
- ctx.shadowColor = color;
- ctx.shadowBlur = 18;
- ctx.beginPath();
- ctx.moveTo(0, -22);
- ctx.lineTo(18, 10);
- ctx.lineTo(6, 10);
- ctx.lineTo(6, 27);
- ctx.lineTo(-6, 27);
- ctx.lineTo(-6, 10);
- ctx.lineTo(-18, 10);
- ctx.closePath();
- ctx.fill();
- ctx.restore();
- }
- function drawBoard() {
- /* El fondo se controla desde pinball.css. El canvas solo dibuja piezas. */
- roundedRect(38, 25, 824, 1148, 38, 'rgba(4, 2, 12, 0.34)', 'rgba(165, 92, 255, 0.8)');
- ctx.save();
- ctx.textAlign = 'center';
- ctx.fillStyle = '#ffffff';
- ctx.font = '900 30px Arial';
- ctx.shadowColor = '#a55cff';
- ctx.shadowBlur = 18;
- ctx.fillText('DANILOPEZ25.ES', WIDTH / 2, 78);
- ctx.fillStyle = '#ff0049';
- ctx.font = '800 16px Arial';
- ctx.fillText('NEON PINBALL', WIDTH / 2, 105);
- ctx.restore();
- /* Arco superior y carriles principales. */
- ctx.save();
- ctx.strokeStyle = 'rgba(34, 231, 255, 0.72)';
- ctx.lineWidth = 5;
- ctx.shadowColor = '#22e7ff';
- ctx.shadowBlur = 18;
- ctx.beginPath();
- ctx.arc(450, 355, 260, Math.PI * 1.08, Math.PI * 1.92);
- ctx.stroke();
- ctx.restore();
- drawRamp([[245, 610], [205, 490], [235, 300], [350, 195]], '#ff0049');
- drawRamp([[655, 610], [695, 490], [665, 300], [550, 195]], '#22e7ff');
- drawNeonLine([[145, 112], [90, 870], [250, 1115]], '#8c3cff', 10, 24);
- drawNeonLine([[755, 112], [785, 1040], [650, 1115]], '#8c3cff', 10, 24);
- drawNeonLine([[145, 112], [755, 112]], '#ff0049', 8, 22);
- /* Canal de lanzamiento derecho. */
- roundedRect(750, 178, 90, 875, 30, 'rgba(10, 7, 24, 0.75)', 'rgba(255, 0, 73, 0.75)');
- drawNeonLine([[840, 1035], [840, 190], [750, 190]], '#ff0049', 5, 15);
- for (let y = 880; y >= 330; y -= 95) drawArrow(795, y, 0, 'rgba(255, 0, 73, 0.55)');
- /* Decoración central con profundidad. */
- ctx.save();
- const core = ctx.createRadialGradient(450, 505, 10, 450, 505, 145);
- core.addColorStop(0, 'rgba(255, 0, 73, 0.24)');
- core.addColorStop(0.45, 'rgba(140, 60, 255, 0.12)');
- core.addColorStop(1, 'rgba(0, 0, 0, 0)');
- ctx.fillStyle = core;
- ctx.beginPath();
- ctx.arc(450, 505, 145, 0, Math.PI * 2);
- ctx.fill();
- ctx.restore();
- bumpers.forEach(drawBumper);
- targets.forEach(drawTarget);
- drawSlingShot([[210, 780], [315, 900], [255, 920]], '#ff0049');
- drawSlingShot([[690, 780], [585, 900], [645, 920]], '#22e7ff');
- drawArrow(300, 655, -0.2, '#ffcc19');
- drawArrow(600, 655, 0.2, '#22e7ff');
- drawArrow(450, 830, 0, '#ff0049');
- drawLaneLights();
- drawFlipper(game.flippers.left, '#ff0049');
- drawFlipper(game.flippers.right, '#ff0049');
- /* Zona de pérdida y guías inferiores. */
- ctx.save();
- const drain = ctx.createLinearGradient(0, 960, 0, 1160);
- drain.addColorStop(0, 'rgba(255, 0, 73, 0)');
- drain.addColorStop(1, 'rgba(255, 0, 73, 0.28)');
- ctx.fillStyle = drain;
- ctx.beginPath();
- ctx.moveTo(90, 870);
- ctx.lineTo(300, 1160);
- ctx.lineTo(600, 1160);
- ctx.lineTo(810, 870);
- ctx.lineTo(650, 1115);
- ctx.lineTo(250, 1115);
- ctx.closePath();
- ctx.fill();
- ctx.restore();
- }
- function drawBumper(bumper) {
- const scale = 1 + bumper.pulse * 0.15;
- ctx.save();
- ctx.translate(bumper.x, bumper.y);
- ctx.scale(scale, scale);
- ctx.shadowColor = bumper.color;
- ctx.shadowBlur = 28 + bumper.pulse * 30;
- ctx.fillStyle = '#120b22';
- ctx.strokeStyle = bumper.color;
- ctx.lineWidth = 8;
- ctx.beginPath();
- ctx.arc(0, 0, bumper.r, 0, Math.PI * 2);
- ctx.fill();
- ctx.stroke();
- ctx.shadowBlur = 0;
- ctx.fillStyle = '#fff';
- ctx.font = '900 22px Arial';
- ctx.textAlign = 'center';
- ctx.textBaseline = 'middle';
- ctx.fillText(String(bumper.value), 0, 1);
- ctx.restore();
- }
- function drawTarget(target) {
- ctx.save();
- ctx.shadowColor = target.hit ? '#ffcc33' : '#8c3cff';
- ctx.shadowBlur = target.hit ? 24 : 10;
- roundedRect(target.x, target.y, target.w, target.h, 7, target.hit ? '#ffcc33' : '#5a279f', '#e7d6ff');
- ctx.restore();
- }
- function drawLaneLights() {
- const multipliers = [1, 2, 3, 5];
- multipliers.forEach((value, index) => {
- const y = 625 + index * 62;
- const active = game.multiplier >= value;
- ctx.beginPath();
- ctx.arc(450, y, 23, 0, Math.PI * 2);
- ctx.fillStyle = active ? 'rgba(22, 217, 255, .28)' : 'rgba(255, 255, 255, .04)';
- ctx.fill();
- ctx.strokeStyle = active ? '#16d9ff' : '#6f6481';
- ctx.lineWidth = 3;
- ctx.stroke();
- ctx.fillStyle = active ? '#fff' : '#988da8';
- ctx.font = '800 17px Arial';
- ctx.fillText(`x${value}`, 450, y + 6);
- });
- }
- function drawFlipper(flipper, color) {
- const segment = flipperSegment(flipper);
- ctx.save();
- ctx.lineCap = 'round';
- ctx.shadowColor = color;
- ctx.shadowBlur = 22;
- ctx.strokeStyle = '#ffffff';
- ctx.lineWidth = 31;
- ctx.beginPath();
- ctx.moveTo(segment[0], segment[1]);
- ctx.lineTo(segment[2], segment[3]);
- ctx.stroke();
- ctx.shadowBlur = 0;
- ctx.strokeStyle = color;
- ctx.lineWidth = 8;
- ctx.stroke();
- ctx.restore();
- }
- function drawBall() {
- if (!game.ball) return;
- const ball = game.ball;
- ball.trail.forEach((point, index) => {
- const alpha = (1 - index / ball.trail.length) * 0.28;
- ctx.fillStyle = `rgba(22, 217, 255, ${alpha})`;
- ctx.beginPath();
- ctx.arc(point.x, point.y, Math.max(2, ball.r - index), 0, Math.PI * 2);
- ctx.fill();
- });
- const gradient = ctx.createRadialGradient(ball.x - 6, ball.y - 7, 2, ball.x, ball.y, ball.r);
- gradient.addColorStop(0, '#ffffff');
- gradient.addColorStop(0.35, '#d8f9ff');
- gradient.addColorStop(1, '#76709a');
- ctx.shadowColor = '#16d9ff';
- ctx.shadowBlur = 20;
- ctx.fillStyle = gradient;
- ctx.beginPath();
- ctx.arc(ball.x, ball.y, ball.r, 0, Math.PI * 2);
- ctx.fill();
- ctx.shadowBlur = 0;
- }
- function drawParticles() {
- game.particles.forEach((particle) => {
- ctx.globalAlpha = clamp(particle.life / particle.maxLife, 0, 1);
- ctx.fillStyle = particle.color;
- ctx.fillRect(particle.x - 3, particle.y - 3, 6, 6);
- });
- ctx.globalAlpha = 1;
- }
- function render() {
- ctx.clearRect(0, 0, WIDTH, HEIGHT);
- ctx.save();
- if (game.shake > 0) {
- ctx.translate((Math.random() - 0.5) * game.shake, (Math.random() - 0.5) * game.shake);
- }
- drawBoard();
- drawBall();
- drawParticles();
- if (game.flash > 0) {
- ctx.fillStyle = `rgba(255,255,255,${game.flash * 0.6})`;
- ctx.fillRect(0, 0, WIDTH, HEIGHT);
- }
- ctx.restore();
- }
- function loop(time) {
- if (!game.lastTime) game.lastTime = time;
- const frameTime = Math.min(0.04, (time - game.lastTime) / 1000);
- game.lastTime = time;
- game.accumulator += frameTime;
- while (game.accumulator >= FIXED_STEP) {
- update(FIXED_STEP);
- game.accumulator -= FIXED_STEP;
- }
- render();
- requestAnimationFrame(loop);
- }
- async function loadBest() {
- try {
- const response = await fetch(config.topUrl, { headers: { Accept: 'application/json' } });
- if (!response.ok) return;
- const data = await response.json();
- const rows = Array.isArray(data) ? data : (data.top || data.scores || []);
- if (rows.length) game.best = Math.max(game.best, Number(rows[0].score || 0));
- updateHud();
- } catch (_) {
- updateHud();
- }
- }
- async function saveScore(score) {
- if (!config.saveUrl || score <= 0) return;
- try {
- const form = new FormData();
- form.append('game', 'pinball');
- form.append('score', String(Math.floor(score)));
- await fetch(config.saveUrl, { method: 'POST', body: form, headers: { Accept: 'application/json' } });
- } catch (_) {
- // The local high score remains available even if the server is offline.
- }
- }
- function setKey(code, pressed) {
- if (code === 'ArrowLeft' || code === 'KeyA') game.keys.left = pressed;
- if (code === 'ArrowRight' || code === 'KeyD') game.keys.right = pressed;
- if (pressed && code === 'Space') launchBall();
- }
- const blockedKeys = new Set(['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Space']);
- window.addEventListener('keydown', (event) => {
- if (blockedKeys.has(event.code)) event.preventDefault();
- setKey(event.code, true);
- }, { passive: false });
- window.addEventListener('keyup', (event) => {
- if (blockedKeys.has(event.code)) event.preventDefault();
- setKey(event.code, false);
- }, { passive: false });
- window.addEventListener('blur', () => {
- game.keys.left = false;
- game.keys.right = false;
- });
- function bindTouch(id, key) {
- const button = document.getElementById(id);
- if (!button) return;
- const press = (event) => {
- event.preventDefault();
- button.classList.add('is-pressed');
- if (key === 'launch') launchBall();
- else game.keys[key] = true;
- };
- const release = (event) => {
- event.preventDefault();
- button.classList.remove('is-pressed');
- if (key !== 'launch') game.keys[key] = false;
- };
- button.addEventListener('pointerdown', press);
- button.addEventListener('pointerup', release);
- button.addEventListener('pointercancel', release);
- button.addEventListener('pointerleave', release);
- }
- startButton.addEventListener('click', startGame);
- canvas.addEventListener('pointerdown', (event) => {
- if (event.pointerType !== 'mouse') launchBall();
- });
- bindTouch('pinball-touch-left', 'left');
- bindTouch('pinball-touch-right', 'right');
- bindTouch('pinball-touch-launch', 'launch');
- updateHud();
- /* El ranking previo ya se carga desde la view. Evitamos repetir la petición. */
- render();
- requestAnimationFrame(loop);
- })();