/** * Arkinoid — Minimalist Architectural Arcade Game * Designed for ARC.CALENDAR */ // Mobile Detection const isMobile = ('ontouchstart' in window || navigator.maxTouchPoints > 0); const BASE_PADDLE_WIDTH = isMobile ? 60 : 120; const HIGHSCORE_KEY = isMobile ? "arkinoid_highscores_mobile" : "arkinoid_highscores_desktop"; // Web Audio API Sound System class SoundSystem { constructor() { this.ctx = null; this.muted = false; } init() { if (!this.ctx) { try { const AudioContextClass = window.AudioContext || window.webkitAudioContext; if (AudioContextClass) { this.ctx = new AudioContextClass(); } } catch (e) { console.warn("Web Audio API is not supported in this browser:", e); } } } playTone(freqStart, freqEnd, type, duration, volume = 0.1) { if (this.muted) return; this.init(); if (!this.ctx || this.ctx.state === 'suspended') return; try { const osc = this.ctx.createOscillator(); const gain = this.ctx.createGain(); osc.type = type; osc.frequency.setValueAtTime(freqStart, this.ctx.currentTime); if (freqEnd && freqEnd !== freqStart) { osc.frequency.exponentialRampToValueAtTime(freqEnd, this.ctx.currentTime + duration); } gain.gain.setValueAtTime(volume, this.ctx.currentTime); gain.gain.exponentialRampToValueAtTime(0.0001, this.ctx.currentTime + duration); osc.connect(gain); gain.connect(this.ctx.destination); osc.start(); osc.stop(this.ctx.currentTime + duration); } catch (e) { console.warn("Audio play failed:", e); } } playBrickHit() { this.playTone(300, 150, 'triangle', 0.08, 0.12); } playPaddleHit() { this.playTone(180, 280, 'sine', 0.12, 0.12); } playWallHit() { this.playTone(150, 150, 'sine', 0.08, 0.08); } playPowerup() { this.playTone(300, 600, 'square', 0.22, 0.04); } playLaser() { this.playTone(700, 200, 'sawtooth', 0.08, 0.05); } playLoseLife() { this.playTone(300, 80, 'sawtooth', 0.4, 0.12); } playLevelUp() { this.playTone(523.25, 523.25, 'sine', 0.08, 0.08); // C5 setTimeout(() => this.playTone(659.25, 659.25, 'sine', 0.08, 0.08), 80); // E5 setTimeout(() => this.playTone(783.99, 783.99, 'sine', 0.08, 0.08), 160); // G5 setTimeout(() => this.playTone(1046.50, 1046.50, 'sine', 0.25, 0.08), 240); // C6 } playGameOver() { this.playTone(392.00, 196.00, 'sawtooth', 0.5, 0.12); // G4 to G3 } } const sounds = new SoundSystem(); // Fallback Mock Data in case API fails const fallbackCompetitions = [ { title: "Городской эко-дизайн", image: "https://images.unsplash.com/photo-1518005020951-eccb494ad742?auto=format&fit=crop&w=300&q=80" }, { title: "Конкурс небоскребов", image: "https://images.unsplash.com/photo-1486406146926-c627a92ad1ab?auto=format&fit=crop&w=300&q=80" }, { title: "Дизайн павильона", image: "https://images.unsplash.com/photo-1503387762-592deb58ef4e?auto=format&fit=crop&w=300&q=80" }, { title: "Расширение музея", image: "https://images.unsplash.com/photo-1558618666-fcd25c85cd6b?auto=format&fit=crop&w=300&q=80" }, { title: "Эко-деревня", image: "https://images.unsplash.com/photo-1510798831971-661eb04b3739?auto=format&fit=crop&w=300&q=80" }, { title: "Дом у озера", image: "https://images.unsplash.com/photo-1470770841072-f978cf4d019e?auto=format&fit=crop&w=300&q=80" } ]; // Grid Constants const BRICK_SIZE = 45; const COL_SPACING = 55; const ROW_SPACING = 70; const PADDLE_HEIGHT = 16; const BALL_SPEED_START = 5.5; const MAX_BALL_SPEED = 10; // Game State Variables let competitions = []; let loadedImages = []; let isImagesLoaded = false; let score = 0; let level = 1; let lives = 3; let gameOver = false; let gameWon = false; let gamePaused = false; let levelTransitionTimer = 0; let lastSavedScoreIndex = -1; const balls = []; const paddle = { x: 0, y: 0, width: BASE_PADDLE_WIDTH, height: PADDLE_HEIGHT, speed: 8, targetX: 0, sticky: false, lasersLeft: 0, laserCooldown: 0 }; const bricks = []; const particles = []; const powerups = []; const lasers = []; // Powerup Management let activePowerup = null; // 'widen', 'sticky', 'laser' let powerupTimer = 0; // DOM Elements const canvas = document.getElementById("gameCanvas"); const ctx = canvas.getContext("2d"); const overlay = document.getElementById("game-overlay"); const overlayContent = document.getElementById("overlay-content"); const overlayTitle = document.getElementById("overlay-title"); const overlayDesc = document.getElementById("overlay-desc"); const overlayLoader = document.getElementById("overlay-loader"); const overlayActions = document.getElementById("overlay-actions"); const overlayLeaderboard = document.getElementById("overlay-leaderboard"); const leaderboardTitle = document.getElementById("leaderboard-title"); const leaderboardEntries = document.getElementById("leaderboard-entries"); const btnStart = document.getElementById("btn-start"); // Name Input Overlay Elements const overlayRecordInput = document.getElementById("overlay-record-input"); const playerNameInput = document.getElementById("player-name-input"); const btnSubmitName = document.getElementById("btn-submit-name"); const soundToggle = document.getElementById("sound-toggle"); const soundIcon = document.getElementById("sound-icon"); const hudScore = document.getElementById("hud-score"); const hudLives = document.getElementById("hud-lives"); const activePowerupCard = document.getElementById("active-powerup-card"); const powerupName = document.getElementById("powerup-name"); const powerupProgress = document.getElementById("powerup-progress"); const pauseIndicator = document.getElementById("pause-indicator"); const mobTouchHints = document.getElementById("mob-touch-hints"); // Controls State const keys = { ArrowLeft: false, ArrowRight: false, KeyA: false, KeyD: false }; let pointerX = null; // --- Helper: Generate 3-Letter Upper Abbreviations --- function getContestLabel(title) { if (!title) return "CON"; const clean = title.replace(/[^a-zA-Zа-яА-Я0-9\s]/g, "").trim(); const words = clean.split(/\s+/).filter(w => w.length > 0); if (words.length >= 3) { return (words[0][0] + words[1][0] + words[2][0]).toUpperCase(); } else if (words.length === 2) { return (words[0].substring(0, 2) + words[1][0]).toUpperCase(); } else if (words[0] && words[0].length >= 3) { return words[0].substring(0, 3).toUpperCase(); } return "CON"; } // --- HUD Updates --- function updateLivesHUD() { if (hudLives) { hudLives.innerHTML = ""; for (let i = 0; i < lives; i++) { const dot = document.createElement("span"); dot.innerText = "●"; dot.style.color = "#D76F6A"; dot.style.fontSize = "20px"; dot.style.lineHeight = "1"; hudLives.appendChild(dot); } } } // --- Initialization --- async function fetchCompetitions() { try { const response = await fetch("https://arc-calendar-api.perov-nik.workers.dev/"); if (!response.ok) throw new Error("API failed"); const data = await response.json(); const records = data.records || (Array.isArray(data) ? data : []); competitions = records.map(record => { const f = record.fields; let imgUrl = ""; if (f["Ссылка на картинку"]) imgUrl = f["Ссылка на картинку"]; else if (f["Картинка"] && Array.isArray(f["Картинка"]) && f["Картинка"].length > 0) { imgUrl = f["Картинка"][0].url || f["Картинка"][0].thumbnails?.large?.url || ""; } if (!imgUrl || imgUrl.includes("unsplash.com")) { imgUrl = "https://images.unsplash.com/photo-1558618666-fcd25c85cd6b?auto=format&fit=crop&w=300&q=80"; } return { title: f["Название конкурса"] || "Конкурс", image: imgUrl }; }).filter(c => c.image); if (competitions.length === 0) throw new Error("No items parsed"); } catch (e) { console.warn("API Error, using fallback:", e); competitions = [...fallbackCompetitions]; } } async function preloadImages() { overlayLoader.classList.remove("hidden"); overlayActions.classList.add("hidden"); overlayLeaderboard.classList.add("hidden"); overlayRecordInput.classList.add("hidden"); overlayDesc.innerText = "LOADING COVERS..."; await fetchCompetitions(); let loadedCount = 0; const promises = competitions.map(comp => { return new Promise((resolve) => { const img = new Image(); img.crossOrigin = "anonymous"; img.onload = () => { loadedImages.push(img); loadedCount++; overlayDesc.innerText = `LOADED: ${loadedCount} OF ${competitions.length}`; resolve(); }; img.onerror = () => { const canvasDummy = document.createElement("canvas"); canvasDummy.width = 10; canvasDummy.height = 10; const ctxDummy = canvasDummy.getContext("2d"); ctxDummy.fillStyle = "#3A6B75"; ctxDummy.fillRect(0, 0, 10, 10); const dummyImg = new Image(); dummyImg.onload = () => { loadedImages.push(dummyImg); loadedCount++; resolve(); }; dummyImg.onerror = () => { loadedCount++; resolve(); }; dummyImg.src = canvasDummy.toDataURL(); }; img.src = comp.image; }); }); const timeoutPromise = new Promise((resolve) => { setTimeout(() => { console.warn("Image load timeout. Proceeding."); resolve(); }, 6000); }); await Promise.race([Promise.all(promises), timeoutPromise]); if (loadedImages.length === 0) { const canvasDummy = document.createElement("canvas"); canvasDummy.width = 10; canvasDummy.height = 10; const ctxDummy = canvasDummy.getContext("2d"); ctxDummy.fillStyle = "#3A6B75"; ctxDummy.fillRect(0, 0, 10, 10); const dummyImg = new Image(); await new Promise((res) => { dummyImg.onload = res; dummyImg.onerror = res; dummyImg.src = canvasDummy.toDataURL(); }); loadedImages.push(dummyImg); } isImagesLoaded = true; overlayLoader.classList.add("hidden"); overlayActions.classList.remove("hidden"); overlayDesc.innerText = "COVERS LOADED. READY?"; } // --- Layouts Configurations --- function getLevelLayout(lvl) { const layout = []; if (lvl === 1) { // Level 1: Pyramid of Cheops const cheopsColors = ["#e6194B", "#D76F6A", "#e07a10", "#f032e6", "#3cb44b", "#008080"]; for (let r = -3; r <= 1; r++) { const width = 1 + (r + 3) * 2; const color = cheopsColors[r + 3] || "#3A6B75"; for (let c = -Math.floor(width/2); c <= Math.floor(width/2); c++) { layout.push({ col: c, row: r, color: color }); } } } else if (lvl === 2) { // Level 2: Space Invader const invader = [ { col: -2, row: -3.5, color: "#e07a10" }, { col: 2, row: -3.5, color: "#e07a10" }, { col: -2, row: -2.5, color: "#e07a10" }, { col: 2, row: -2.5, color: "#e07a10" }, { col: -3, row: -1.5, color: "#e6194B" }, { col: -2, row: -1.5, color: "#e6194B" }, { col: 2, row: -1.5, color: "#e6194B" }, { col: 3, row: -1.5, color: "#e6194B" }, { col: -4, row: -0.5, color: "#f032e6" }, { col: -3, row: -0.5, color: "#f032e6" }, { col: -1, row: -0.5, color: "#f032e6" }, { col: 1, row: -0.5, color: "#f032e6" }, { col: 3, row: -0.5, color: "#f032e6" }, { col: 4, row: -0.5, color: "#f032e6" }, { col: -4, row: 0.5, color: "#3cb44b" }, { col: -3, row: 0.5, color: "#3cb44b" }, { col: -2, row: 0.5, color: "#3cb44b" }, { col: -1, row: 0.5, color: "#3cb44b" }, { col: 1, row: 0.5, color: "#3cb44b" }, { col: 2, row: 0.5, color: "#3cb44b" }, { col: 3, row: 0.5, color: "#3cb44b" }, { col: 4, row: 0.5, color: "#3cb44b" }, { col: -3, row: 1.5, color: "#008080" }, { col: 3, row: 1.5, color: "#008080" }, { col: -4, row: 2.5, color: "#008080" }, { col: 4, row: 2.5, color: "#008080" }, { col: -2, row: 3.5, color: "#008080" }, { col: -1, row: 3.5, color: "#008080" }, { col: 1, row: 3.5, color: "#008080" }, { col: 2, row: 3.5, color: "#008080" } ]; layout.push(...invader); } else if (lvl === 3) { // Level 3: Architectural Ziggurat const colors = ["#e6194B", "#D76F6A", "#e07a10", "#f032e6", "#3cb44b", "#008080"]; for (let r = -3; r <= 2; r++) { const width = 3 + (r + 3) * 2; const color = colors[r + 3] || "#3A6B75"; for (let c = -width/2; c < width/2; c++) { layout.push({ col: c + 0.5, row: r, color: color }); } } } else { // Level 4: The letter K const kColor = "#D76F6A"; for (let r = -3; r <= 3; r++) { layout.push({ col: -2, row: r, color: kColor }); } layout.push({ col: -1, row: -1, color: kColor }); layout.push({ col: 0, row: -2, color: kColor }); layout.push({ col: 1, row: -3, color: kColor }); layout.push({ col: -1, row: 1, color: kColor }); layout.push({ col: 0, row: 2, color: kColor }); layout.push({ col: 1, row: 3, color: kColor }); layout.push({ col: -1, row: 0, color: kColor }); } return layout; } function initLevel() { bricks.length = 0; powerups.length = 0; lasers.length = 0; particles.length = 0; const layout = getLevelLayout(level); const imgCount = loadedImages.length || 1; layout.forEach((cell, idx) => { const imgIndex = idx % imgCount; const compInfo = competitions[imgIndex] || fallbackCompetitions[imgIndex % fallbackCompetitions.length]; bricks.push({ gridX: cell.col * COL_SPACING, gridY: cell.row * ROW_SPACING, x: 0, y: 0, width: BRICK_SIZE, height: BRICK_SIZE, image: loadedImages[imgIndex], label: getContestLabel(compInfo?.title), fallbackColor: cell.color, hits: 1, maxHits: 1, active: true }); }); deactivatePowerup(); resize(); resetBallAndPaddle(); updateLivesHUD(); } function resetBallAndPaddle() { balls.length = 0; paddle.width = BASE_PADDLE_WIDTH; paddle.x = (canvas.width - paddle.width) / 2; paddle.y = canvas.height - 60; paddle.sticky = false; paddle.lasersLeft = 0; paddle.laserCooldown = 0; balls.push({ x: canvas.width / 2, y: paddle.y - 12, vx: 0, vy: 0, radius: 6, active: true, stuck: true, stuckOffset: BASE_PADDLE_WIDTH / 2 }); // Restore touch hints on mobile when not started if (isMobile) { mobTouchHints.classList.remove("hidden"); mobTouchHints.classList.add("flex"); } } function launchBall() { let launchedAny = false; balls.forEach(ball => { if (ball.stuck) { const angle = (Math.random() * 40 - 20) * Math.PI / 180; const speed = BALL_SPEED_START + level * 0.5; ball.vx = speed * Math.sin(angle); ball.vy = -speed * Math.cos(angle); ball.stuck = false; launchedAny = true; } }); if (launchedAny) { sounds.playPaddleHit(); if (isMobile) { mobTouchHints.classList.add("hidden"); mobTouchHints.classList.remove("flex"); } } } function createExplosion(x, y, w, h, fallbackColor) { const particleCount = 20; for (let i = 0; i < particleCount; i++) { particles.push({ x: x + w / 2, y: y + h / 2, vx: (Math.random() - 0.5) * 5, vy: (Math.random() - 0.5) * 5, size: Math.random() * 4 + 2, color: fallbackColor || "#3A6B75", alpha: 1, decay: Math.random() * 0.04 + 0.03 }); } } const POWERUP_TYPES = [ { type: 'widen', color: '#3A6B75', icon: '↔', label: 'Длинная ракетка' }, { type: 'multiball', color: '#3A6B75', icon: '☄', label: 'Мульти-шар' }, { type: 'sticky', color: '#3A6B75', icon: '★', label: 'Липкая ракетка' }, { type: 'laser', color: '#3A6B75', icon: '⚡', label: 'Лазерные лучи' }, { type: 'life', color: '#3A6B75', icon: '♥', label: 'Дополнительная жизнь' } ]; function spawnPowerup(x, y) { if (Math.random() > 0.3) return; const typeInfo = POWERUP_TYPES[Math.floor(Math.random() * POWERUP_TYPES.length)]; powerups.push({ x: x, y: y, vy: 2.2, width: 24, height: 24, type: typeInfo.type, color: typeInfo.color, icon: typeInfo.icon, label: typeInfo.label }); } function deactivatePowerup() { paddle.width = BASE_PADDLE_WIDTH; paddle.sticky = false; paddle.lasersLeft = 0; activePowerupCard.classList.add("hidden"); activePowerup = null; powerupTimer = 0; } function activatePowerup(type, label, icon) { sounds.playPowerup(); deactivatePowerup(); activePowerupCard.classList.remove("hidden"); powerupName.innerText = label.toUpperCase(); if (type === 'widen') { activePowerup = 'widen'; paddle.width = Math.round(BASE_PADDLE_WIDTH * 1.5); powerupTimer = 600; // 10 seconds } else if (type === 'sticky') { activePowerup = 'sticky'; paddle.sticky = true; powerupTimer = 600; // 10 seconds } else if (type === 'laser') { activePowerup = 'laser'; paddle.lasersLeft = 6; powerupProgress.style.width = "100%"; } else if (type === 'multiball') { activePowerupCard.classList.add("hidden"); const refBall = balls[0] || { x: canvas.width/2, y: paddle.y - 12, vx: 0, vy: -5, stuck: false }; const isRefStuck = !!refBall.stuck; const speed = BALL_SPEED_START + level * 0.5; const baseVy = refBall.vy !== 0 ? -Math.abs(refBall.vy) : -speed; const baseVx = refBall.vx; balls.push({ x: refBall.x, y: refBall.y, vx: isRefStuck ? 0 : baseVx + (Math.random() * 2 - 1), vy: isRefStuck ? 0 : baseVy, radius: 6, active: true, stuck: isRefStuck, stuckOffset: isRefStuck ? Math.max(0, Math.min(paddle.width, refBall.stuckOffset - 20)) : 0 }); balls.push({ x: refBall.x, y: refBall.y, vx: isRefStuck ? 0 : baseVx + (Math.random() * 2 - 1), vy: isRefStuck ? 0 : baseVy, radius: 6, active: true, stuck: isRefStuck, stuckOffset: isRefStuck ? Math.max(0, Math.min(paddle.width, refBall.stuckOffset + 20)) : 0 }); } else if (type === 'life') { activePowerupCard.classList.add("hidden"); lives++; updateLivesHUD(); } } function fireLaser() { if (paddle.lasersLeft <= 0 || paddle.laserCooldown > 0) return; lasers.push({ x: paddle.x + 10, y: paddle.y - 10, vy: -8 }); lasers.push({ x: paddle.x + paddle.width - 10, y: paddle.y - 10, vy: -8 }); paddle.lasersLeft--; paddle.laserCooldown = 20; sounds.playLaser(); const pct = (paddle.lasersLeft / 6) * 100; powerupProgress.style.width = `${pct}%`; if (paddle.lasersLeft <= 0) { deactivatePowerup(); } } function isColliding(rect1, rect2) { return rect1.x < rect2.x + rect2.width && rect1.x + rect1.width > rect2.x && rect1.y < rect2.y + rect2.height && rect1.y + rect1.height > rect2.y; } function update() { if (gameOver || gameWon || gamePaused) return; if (levelTransitionTimer > 0) { levelTransitionTimer--; if (levelTransitionTimer === 0) { initLevel(); } return; } if (powerupTimer > 0) { powerupTimer--; const pct = (powerupTimer / 600) * 100; powerupProgress.style.width = `${pct}%`; if (powerupTimer === 0) { deactivatePowerup(); } } let moveDir = 0; if (keys.ArrowLeft || keys.KeyA) moveDir = -1; if (keys.ArrowRight || keys.KeyD) moveDir = 1; if (moveDir !== 0) { paddle.x += moveDir * paddle.speed; } else if (pointerX !== null) { const targetX = pointerX - paddle.width / 2; paddle.x += (targetX - paddle.x) * 0.28; } if (paddle.x < 0) paddle.x = 0; if (paddle.x > canvas.width - paddle.width) paddle.x = canvas.width - paddle.width; if (paddle.laserCooldown > 0) paddle.laserCooldown--; for (let i = lasers.length - 1; i >= 0; i--) { const l = lasers[i]; l.y += l.vy; let hit = false; for (let j = 0; j < bricks.length; j++) { const b = bricks[j]; if (b.active && isColliding({ x: l.x - 2, y: l.y - 10, width: 4, height: 10 }, b)) { hit = true; b.hits--; sounds.playBrickHit(); if (b.hits <= 0) { b.active = false; createExplosion(b.x, b.y, b.width, b.height, b.fallbackColor); spawnPowerup(b.x + b.width/2, b.y + b.height/2); score += 100 * level; hudScore.innerText = score; } break; } } if (hit || l.y < 0) { lasers.splice(i, 1); } } for (let i = balls.length - 1; i >= 0; i--) { const ball = balls[i]; if (!ball.active) continue; if (ball.stuck) { ball.x = paddle.x + (ball.stuckOffset !== undefined ? ball.stuckOffset : paddle.width / 2); ball.y = paddle.y - ball.radius; continue; } ball.x += ball.vx; ball.y += ball.vy; if (ball.x - ball.radius < 0) { ball.x = ball.radius; ball.vx = -ball.vx; sounds.playWallHit(); } if (ball.x + ball.radius > canvas.width) { ball.x = canvas.width - ball.radius; ball.vx = -ball.vx; sounds.playWallHit(); } if (ball.y - ball.radius < 0) { ball.y = ball.radius; ball.vy = -ball.vy; sounds.playWallHit(); } if (ball.y - ball.radius > canvas.height) { ball.active = false; balls.splice(i, 1); if (balls.length === 0) { lives--; updateLivesHUD(); sounds.playLoseLife(); if (lives <= 0) { gameOver = true; sounds.playGameOver(); const scores = loadHighScores(); const isNewHighScore = scores.length === 0 || score > 0; if (isNewHighScore) { setTimeout(() => { showOverlayRecordInput(); }, 500); } else { showOverlayWithActions("GAME OVER", `YOU SCORED ${score} POINTS.`); } } else { resetBallAndPaddle(); } } continue; } if (ball.vy > 0 && ball.y + ball.radius >= paddle.y && ball.y - ball.radius <= paddle.y + paddle.height && ball.x >= paddle.x && ball.x <= paddle.x + paddle.width) { if (paddle.sticky) { ball.stuck = true; ball.stuckOffset = Math.max(0, Math.min(paddle.width, ball.x - paddle.x)); ball.vx = 0; ball.vy = 0; } else { const hitPoint = (ball.x - paddle.x) / paddle.width; const angle = (hitPoint - 0.5) * (Math.PI * 0.45); const speed = Math.min(Math.sqrt(ball.vx*ball.vx + ball.vy*ball.vy) + 0.15, MAX_BALL_SPEED); ball.vx = speed * Math.sin(angle); ball.vy = -speed * Math.cos(angle); sounds.playPaddleHit(); } } for (let j = 0; j < bricks.length; j++) { const b = bricks[j]; if (!b.active) continue; const ballBox = { x: ball.x - ball.radius, y: ball.y - ball.radius, width: ball.radius*2, height: ball.radius*2 }; if (isColliding(ballBox, b)) { b.hits--; sounds.playBrickHit(); const ballPrevX = ball.x - ball.vx; if (ballPrevX + ball.radius <= b.x || ballPrevX - ball.radius >= b.x + b.width) { ball.vx = -ball.vx; } else { ball.vy = -ball.vy; } if (b.hits <= 0) { b.active = false; createExplosion(b.x, b.y, b.width, b.height, b.fallbackColor); spawnPowerup(b.x + b.width/2, b.y + b.height/2); score += 100 * level; hudScore.innerText = score; } break; } } } for (let i = powerups.length - 1; i >= 0; i--) { const drop = powerups[i]; drop.y += drop.vy; const paddleBox = { x: paddle.x, y: paddle.y, width: paddle.width, height: paddle.height }; if (isColliding(drop, paddleBox)) { activatePowerup(drop.type, drop.label, drop.icon); powerups.splice(i, 1); continue; } if (drop.y > canvas.height) { powerups.splice(i, 1); } } for (let i = particles.length - 1; i >= 0; i--) { const p = particles[i]; p.x += p.vx; p.y += p.vy; p.alpha -= p.decay; if (p.alpha <= 0) { particles.splice(i, 1); } } const activeBricks = bricks.filter(b => b.active).length; if (activeBricks === 0 && bricks.length > 0) { level++; sounds.playLevelUp(); if (level > 4) { gameWon = true; const scores = loadHighScores(); const isNewHighScore = scores.length === 0 || score > 0; if (isNewHighScore) { setTimeout(() => { showOverlayRecordInput(); }, 500); } else { showOverlayWithActions("VICTORY!", `CONGRATULATIONS! SCORE: ${score}`); } } else { balls.length = 0; powerups.length = 0; lasers.length = 0; particles.length = 0; levelTransitionTimer = 120; } } } // --- Draw Loop --- function draw() { ctx.clearRect(0, 0, canvas.width, canvas.height); if (levelTransitionTimer > 0) { ctx.save(); ctx.fillStyle = "#3A6B75"; ctx.font = "24px 'Silkscreen', monospace"; ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.fillText(`LEVEL ${level}`, canvas.width / 2, canvas.height / 2); ctx.restore(); return; } // 1. Draw Bricks for (let i = 0; i < bricks.length; i++) { const b = bricks[i]; if (!b.active) continue; ctx.save(); if (b.image && b.image.complete && b.image.width > 0) { ctx.drawImage(b.image, b.x, b.y, b.width, b.height); } else { ctx.fillStyle = b.fallbackColor || "#3A6B75"; ctx.fillRect(b.x, b.y, b.width, b.height); } ctx.strokeStyle = "#3A6B75"; ctx.lineWidth = 1; ctx.strokeRect(b.x, b.y, b.width, b.height); ctx.fillStyle = "#3A6B75"; const baseFontSize = 8; const scale = b.width / BRICK_SIZE; const fontSize = Math.max(6, Math.round(baseFontSize * scale)); ctx.font = `${fontSize}px 'Silkscreen', monospace`; ctx.textAlign = "center"; ctx.textBaseline = "top"; ctx.fillText(b.label, b.x + b.width / 2, b.y + b.height + 4); ctx.restore(); } // 2. Draw Lasers ctx.fillStyle = "#D76F6A"; for (let i = 0; i < lasers.length; i++) { const l = lasers[i]; ctx.fillRect(l.x - 2, l.y - 10, 4, 10); } // 3. Draw Drops (Powerups) for (let i = 0; i < powerups.length; i++) { const drop = powerups[i]; ctx.save(); ctx.fillStyle = "#3A6B75"; ctx.fillRect(drop.x, drop.y, drop.width, drop.height); ctx.fillStyle = "#ffffff"; ctx.font = "11px 'Silkscreen', monospace"; ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.fillText(drop.icon, drop.x + drop.width/2, drop.y + drop.height/2); ctx.restore(); } // 4. Draw Particles for (let i = 0; i < particles.length; i++) { const p = particles[i]; ctx.save(); ctx.globalAlpha = p.alpha; ctx.fillStyle = p.color; ctx.fillRect(p.x, p.y, p.size, p.size); ctx.restore(); } // 5. Draw Paddle ctx.save(); ctx.fillStyle = "#3A6B75"; ctx.fillRect(paddle.x, paddle.y, paddle.width, paddle.height); if (paddle.lasersLeft > 0) { ctx.fillStyle = "#D76F6A"; ctx.fillRect(paddle.x + 4, paddle.y - 4, 6, 4); ctx.fillRect(paddle.x + paddle.width - 10, paddle.y - 4, 6, 4); } ctx.restore(); // 6. Draw Ball ctx.fillStyle = "#3A6B75"; for (let i = 0; i < balls.length; i++) { const ball = balls[i]; if (!ball.active) continue; ctx.beginPath(); ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2); ctx.fill(); } } function resize() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; paddle.y = canvas.height - 60; let scale = 1.0; let minCol = 0; let maxCol = 0; bricks.forEach(cell => { const col = cell.gridX / COL_SPACING; if (col < minCol) minCol = col; if (col > maxCol) maxCol = col; }); const colSpan = (maxCol - minCol + 1.2); const requiredWidth = colSpan * COL_SPACING; const availableWidth = canvas.width - 24; if (requiredWidth > availableWidth) { scale = availableWidth / requiredWidth; } bricks.forEach(b => { b.width = Math.round(BRICK_SIZE * scale); b.height = Math.round(BRICK_SIZE * scale); b.x = Math.round(canvas.width / 2 + b.gridX * scale - b.width / 2); b.y = Math.round(canvas.height / 2 + b.gridY * scale - b.height / 2 - 80); }); if (paddle.x > canvas.width - paddle.width) { paddle.x = canvas.width - paddle.width; } } // --- Overlay Screen Management --- function showOverlay(title, desc, showLeaderboard = true) { overlayTitle.innerText = title; overlayDesc.innerText = desc; overlayRecordInput.classList.add("hidden"); overlay.classList.remove("opacity-0", "pointer-events-none"); overlay.classList.add("opacity-100"); overlayContent.classList.remove("scale-95"); overlayContent.classList.add("scale-100"); const scores = loadHighScores(); if (showLeaderboard && scores.length > 0) { overlayLeaderboard.classList.remove("hidden"); renderHighScoresOverlay(); } else { overlayLeaderboard.classList.add("hidden"); } } function showOverlayWithActions(title, desc, showLeaderboard = true) { overlayActions.classList.remove("hidden"); showOverlay(title, desc, showLeaderboard); } function showOverlayRecordInput() { overlayTitle.innerText = "NEW RECORD!"; overlayDesc.innerText = `YOUR SCORE: ${score}`; overlayLeaderboard.classList.add("hidden"); overlayActions.classList.add("hidden"); overlayRecordInput.classList.remove("hidden"); playerNameInput.value = ""; overlay.classList.remove("opacity-0", "pointer-events-none"); overlay.classList.add("opacity-100"); overlayContent.classList.remove("scale-95"); overlayContent.classList.add("scale-100"); setTimeout(() => { playerNameInput.focus(); }, 100); } function submitPlayerName() { const rawName = playerNameInput.value; const cleanName = rawName.trim().substring(0, 19).toUpperCase() || "PLAYER"; saveHighScore(cleanName); sounds.playPaddleHit(); overlayRecordInput.classList.add("transition-all", "duration-300", "opacity-0"); setTimeout(() => { overlayRecordInput.classList.add("hidden"); overlayRecordInput.classList.remove("opacity-0"); overlayTitle.innerText = "HIGHSCORE"; overlayDesc.innerText = `YOUR SCORE: ${score}`; overlayLeaderboard.classList.remove("hidden"); overlayLeaderboard.classList.add("opacity-0"); renderHighScoresOverlay(); overlayActions.classList.remove("hidden"); overlayActions.classList.add("opacity-0"); setTimeout(() => { overlayLeaderboard.classList.add("transition-all", "duration-300", "opacity-100"); overlayLeaderboard.classList.remove("opacity-0"); overlayActions.classList.add("transition-all", "duration-300", "opacity-100"); overlayActions.classList.remove("opacity-0"); }, 50); }, 300); } function hideOverlay() { overlay.classList.remove("opacity-100"); overlay.classList.add("opacity-0", "pointer-events-none"); overlayContent.classList.remove("scale-100"); overlayContent.classList.add("scale-95"); } function loadHighScores() { const data = localStorage.getItem(HIGHSCORE_KEY); return data ? JSON.parse(data) : []; } function saveHighScore(name) { if (score === 0) return; const scores = loadHighScores(); const cleanName = (name || "PLAYER").substring(0, 19).toUpperCase(); const newEntry = { name: cleanName, score: score, timestamp: Date.now() }; scores.push(newEntry); scores.sort((a, b) => b.score - a.score); localStorage.setItem(HIGHSCORE_KEY, JSON.stringify(scores)); lastSavedScoreIndex = scores.findIndex(e => e.timestamp === newEntry.timestamp); } function renderHighScoresOverlay() { if (leaderboardTitle) { leaderboardTitle.innerText = isMobile ? "HIGHSCORE (MOBILE)" : "HIGHSCORE (DESKTOP)"; } const scores = loadHighScores(); leaderboardEntries.innerHTML = ""; if (scores.length === 0) { leaderboardEntries.innerHTML = `
NO RECORDS YET
`; return; } const topLimit = Math.min(10, scores.length); for (let i = 0; i < topLimit; i++) { const entry = scores[i]; const rank = i + 1; const isCurrent = (i === lastSavedScoreIndex); const item = document.createElement("div"); item.className = `flex items-center justify-between ${isCurrent ? "font-bold text-[#D76F6A]" : "text-[#3A6B75]"}`; item.innerHTML = ` ${rank}. ${entry.name || "PLAYER"} ${entry.score} `; leaderboardEntries.appendChild(item); } if (lastSavedScoreIndex >= 10 && lastSavedScoreIndex < scores.length) { const gap = document.createElement("div"); gap.className = "text-center text-xs text-[#3A6B75]/40 tracking-widest my-1 select-none"; gap.innerText = "• • •"; leaderboardEntries.appendChild(gap); const entry = scores[lastSavedScoreIndex]; const rank = lastSavedScoreIndex + 1; const item = document.createElement("div"); item.className = "flex items-center justify-between font-bold text-[#D76F6A]"; item.innerHTML = ` ${rank}. ${entry.name || "PLAYER"} ${entry.score} `; leaderboardEntries.appendChild(item); } } function setupControls() { window.addEventListener("keydown", (e) => { const isTyping = (document.activeElement === playerNameInput); if (e.code === "Space" && !isTyping) { const hasStuck = balls.some(b => b.stuck); if (hasStuck && !gameOver && !gameWon && !gamePaused && levelTransitionTimer === 0) { launchBall(); } if (paddle.lasersLeft > 0 && !hasStuck && !gamePaused) { fireLaser(); } e.preventDefault(); } if (e.code === "Enter" && isTyping) { submitPlayerName(); e.preventDefault(); } if (e.code in keys && !isTyping) { keys[e.code] = true; e.preventDefault(); } if ((e.code === "Escape" || e.code === "KeyP") && !isTyping) { togglePause(); } }); window.addEventListener("keyup", (e) => { if (e.code in keys) { keys[e.code] = false; } }); const trackPointer = (clientX) => { const relativeX = clientX / window.innerWidth; pointerX = relativeX * canvas.width; }; window.addEventListener("mousemove", (e) => { trackPointer(e.clientX); }); window.addEventListener("mousedown", (e) => { if (e.target.closest("button") || e.target.closest("a") || e.target.closest("input")) return; const hasStuck = balls.some(b => b.stuck); if (hasStuck && !gameOver && !gameWon && !gamePaused && levelTransitionTimer === 0) { launchBall(); } else if (paddle.lasersLeft > 0 && !hasStuck && !gamePaused) { fireLaser(); } }); window.addEventListener("touchstart", (e) => { if (e.touches.length > 0) { trackPointer(e.touches[0].clientX); } }, { passive: true }); window.addEventListener("touchmove", (e) => { if (e.touches.length > 0) { trackPointer(e.touches[0].clientX); } }, { passive: true }); window.addEventListener("touchend", () => { pointerX = null; }); soundToggle.addEventListener("click", () => { sounds.muted = !sounds.muted; soundIcon.innerText = sounds.muted ? "🔇" : "🔊"; if (!sounds.muted) sounds.init(); }); btnSubmitName.addEventListener("click", () => { submitPlayerName(); }); btnStart.addEventListener("click", () => { if (gameOver || gameWon) { score = 0; level = 1; lives = 3; gameOver = false; gameWon = false; lastSavedScoreIndex = -1; hudScore.innerText = score; updateLivesHUD(); } hideOverlay(); initLevel(); sounds.init(); }); } function togglePause() { if (gameOver || gameWon || levelTransitionTimer > 0) return; gamePaused = !gamePaused; if (gamePaused) { pauseIndicator.style.opacity = "1"; } else { pauseIndicator.style.opacity = "0"; } } function loop(timestamp) { update(); draw(); requestAnimationFrame(loop); } // detect touch devices if ('ontouchstart' in window || navigator.maxTouchPoints > 0) { mobTouchHints.classList.remove("hidden"); mobTouchHints.classList.add("flex"); } window.addEventListener("DOMContentLoaded", () => { resize(); window.addEventListener("resize", resize); setupControls(); updateLivesHUD(); preloadImages().then(() => { showOverlayWithActions("ARKINOID", "DESTROY COMPETITION COVERS AND SET THE BEST HIGH SCORE.", false); }); requestAnimationFrame(loop); });