const canvas = document.getElementById("gameCanvas"); const ctx = canvas.getContext("2d"); const miniCanvas = document.getElementById("miniMapCanvas"); const miniCtx = miniCanvas.getContext("2d"); const tileCountX = 24; const tileCountY = 21; const tileSize = Math.floor(canvas.width / tileCountX); canvas.height = tileSize * tileCountY; const headUp = new Image(); headUp.src = "assets/snake/head_up.png"; const headDown = new Image(); headDown.src = "assets/snake/head_down.png"; const headLeft = new Image(); headLeft.src = "assets/snake/head_left.png"; const headRight = new Image(); headRight.src = "assets/snake/head_right.png"; const bodyHor = new Image(); bodyHor.src = "assets/snake/body_horizontal.png"; const bodyVer = new Image(); bodyVer.src = "assets/snake/body_vertical.png"; const tailUp = new Image(); tailUp.src = "assets/snake/tail_up.png"; const tailDown = new Image(); tailDown.src = "assets/snake/tail_down.png"; const tailLeft = new Image(); tailLeft.src = "assets/snake/tail_left.png"; const tailRight = new Image(); tailRight.src = "assets/snake/tail_right.png"; const appleImg = new Image(); appleImg.src = "assets/apple.png"; const upSound = document.getElementById("upSound"); const downSound = document.getElementById("downSound"); const leftSound = document.getElementById("leftSound"); const rightSound = document.getElementById("rightSound"); const eatSound = document.getElementById("eatSound"); const hitSound = document.getElementById("hitSound"); const levelUpSound = document.getElementById("levelUpSound"); let isSoundOn = localStorage.getItem("sound") !== "off"; let snake, prevSnake, velocity, directionQueue, food; let score; let highscore = sessionStorage.getItem("highscore") || 0; let lives = 3; let isPaused = false; let isCountingDown = false; let isHit = false; let isGameOver = false; let level = 1; let lastMoveTime = 0; let moveDelay = 250; let progress = 0; let frameCount = 0; let fpsLastTime = performance.now(); let currentFPS = 0; let blinkCount = 0; let maxBlink = 5; let blinkTimer = 0; let lastFaceDir = { dx: 1, dy: 0 }; let guideSource = null; document.getElementById("highscore").innerText = highscore; function unlockAudio() { const sounds = [upSound, downSound, leftSound, rightSound, eatSound, hitSound]; sounds.forEach(sound => { sound.volume = 0; sound.play().then(() => { sound.pause(); sound.currentTime = 0; sound.volume = 1; }).catch(() => {}); }); } function playSound(sound) { if (!isSoundOn) return; const clone = sound.cloneNode(); clone.play(); } function startGame() { unlockAudio(); document.getElementById("startScreen").style.display = "none"; document.getElementById("gameScreen").style.display = "block"; initGame(); } function initGame() { snake = [{ x: 6, y: 10 }, { x: 5, y: 10 }]; prevSnake = JSON.parse(JSON.stringify(snake)); velocity = { x: 0, y: 0 }; directionQueue = []; lastFaceDir = { dx: 1, dy: 0 }; food = { x: 18, y: 10 }; score = 0; lives = 3; level = 1; moveDelay = 250; document.getElementById("score").innerText = score; document.getElementById("levelText").innerText = "Level 1"; updateLivesUI(); } function gameLoop(time = 0) { frameCount++; const elapsed = performance.now() - fpsLastTime; if (elapsed >= 1000) { currentFPS = Math.round((frameCount * 1000) / elapsed); document.getElementById("game-fps").innerText = currentFPS + " FPS"; frameCount = 0; fpsLastTime = performance.now(); } update(time); draw(); requestAnimationFrame(gameLoop); } requestAnimationFrame(gameLoop); function togglePause() { if (isGameOver) return; isPaused = !isPaused; document.getElementById("pauseScreen").style.display = isPaused ? "flex" : "none"; } function resumeGame() { document.getElementById("pauseScreen").style.display = "none"; const overlay = document.getElementById("countdown-overlay"); const text = document.getElementById("countdown-text"); isCountingDown = true; overlay.style.display = "flex"; let count = 3; text.innerText = count; const countdownInterval = setInterval(() => { count--; if (count > 0) { text.innerText = count; } else { clearInterval(countdownInterval); overlay.style.display = "none"; isCountingDown = false; isPaused = false; } }, 1000); } function exitGame() { isPaused = false; document.getElementById("pauseScreen").style.display = "none"; document.getElementById("gameScreen").style.display = "none"; document.getElementById("startScreen").style.display = "flex"; initGame(); } function update(time) { if (!snake) return; if (isHit) { if (time - blinkTimer > 100) { blinkTimer = time; blinkCount++; } } if (isPaused) return; if (isGameOver) { if (time - blinkTimer > 100) { blinkTimer = time; blinkCount++; if (blinkCount >= maxBlink) { document.getElementById("gameScreen").style.display = "none"; document.getElementById("startScreen").style.display = "flex"; isGameOver = false; initGame(); } } return; } if (time - lastMoveTime > moveDelay) { prevSnake = JSON.parse(JSON.stringify(snake)); if (directionQueue.length > 0) { velocity = directionQueue.shift(); if (gestureChangeTimestamp > 0) { const inputLag = Math.round(performance.now() - gestureChangeTimestamp); document.getElementById("game-latency").innerText = inputLag + " ms"; gestureChangeTimestamp = 0; } } const head = { x: snake[0].x + velocity.x, y: snake[0].y + velocity.y }; if (velocity.x === 0 && velocity.y === 0) return; if ( head.x < 0 || head.x >= tileCountX || head.y < 0 || head.y >= tileCountY ) return gameOver(); for (let part of snake) { if (part.x === head.x && part.y === head.y) { return gameOver(); } } snake.unshift(head); if (head.x === food.x && head.y === food.y) { playSound(eatSound); score++; document.getElementById("score").innerText = score; updateHighscore(); updateLevel(); spawnFood(); } else { snake.pop(); } lastMoveTime = time; } progress = Math.min((time - lastMoveTime) / moveDelay, 1); } function gameOver() { playSound(hitSound); lives--; updateLivesUI(); if (lives > 0) { isHit = true; isPaused = true; blinkCount = 0; blinkTimer = 0; setTimeout(() => { snake = [{ x: 6, y: 10 }, { x: 5, y: 10 }]; prevSnake = JSON.parse(JSON.stringify(snake)); velocity = { x: 0, y: 0 }; directionQueue = []; lastFaceDir = { dx: 1, dy: 0 }; food = { x: 18, y: 10 }; moveDelay = 250; isHit = false; isPaused = false; }, 500); return; } isGameOver = true; blinkCount = 0; blinkTimer = 0; if (score > highscore) { highscore = score; sessionStorage.setItem("highscore", highscore); document.getElementById("highscore").innerText = highscore; } } function draw() { if (!snake) return; ctx.fillStyle = "black"; ctx.fillRect(0, 0, canvas.width, canvas.height); drawGrid(); if ((!isGameOver && !isHit) || blinkCount % 2 === 0) { for (let i = 0; i < snake.length; i++) { const current = snake[i]; const previous = prevSnake[i] || current; const interpX = previous.x + (current.x - previous.x) * progress; const interpY = previous.y + (current.y - previous.y) * progress; const x = interpX * tileSize; const y = interpY * tileSize; if (i === 0) { let dx = current.x - previous.x; let dy = current.y - previous.y; if (dx !== 0 || dy !== 0) { lastFaceDir = { dx, dy }; } else { dx = lastFaceDir.dx; dy = lastFaceDir.dy; } let img, w, h; if (dx > 0) { img = headRight; w = 120; h = 108; } else if (dx < 0) { img = headLeft; w = 120; h = 108; } else if (dy > 0) { img = headDown; w = 108; h = 120; } else { img = headUp; w = 108; h = 120; } drawScaledImage(img, x, y, w, h); } else if (i === snake.length - 1) { let dx = previous.x - current.x; let dy = previous.y - current.y; if (dx === 0 && dy === 0) { dx = -lastFaceDir.dx; dy = -lastFaceDir.dy; } let img, w, h; if (dx > 0) { img = tailLeft; w = 100; h = 92; } else if (dx < 0) { img = tailRight; w = 100; h = 92; } else if (dy > 0) { img = tailUp; w = 92; h = 100; } else { img = tailDown; w = 92; h = 100; } drawScaledImage(img, x, y, w, h); } else { const parent = snake[i - 1]; let img, w, h; if (current.y === parent.y) { img = bodyHor; w = 100; h = 92; } else { img = bodyVer; w = 92; h = 100; } drawScaledImage(img, x, y, w, h); } } } ctx.drawImage( appleImg, food.x * tileSize, food.y * tileSize, tileSize, tileSize ); drawMiniMap(); } function drawGrid() { for (let y = 0; y < tileCountY; y++) { for (let x = 0; x < tileCountX; x++) { ctx.fillStyle = (x + y) % 2 === 0 ? "#AAD751" : "#A2D149"; ctx.fillRect(x * tileSize, y * tileSize, tileSize + 1, tileSize + 1); } } } function drawScaledImage(img, x, y, baseW, baseH) { const scale = tileSize / 100; const width = baseW * scale; const height = baseH * scale; const offsetX = (tileSize - width) / 2; const offsetY = (tileSize - height) / 2; ctx.drawImage(img, x + offsetX, y + offsetY, width + 1, height + 1); } function drawMiniMap() { if (!snake) return; const scaleX = miniCanvas.width / tileCountX; const scaleY = miniCanvas.height / tileCountY; miniCtx.fillStyle = "#1F2020"; miniCtx.fillRect(0, 0, miniCanvas.width, miniCanvas.height); miniCtx.fillStyle = "#E7471D"; miniCtx.fillRect( food.x * scaleX, food.y * scaleY, scaleX, scaleY ); snake.forEach((part, index) => { miniCtx.fillStyle = index === 0 ? "#6AB04A" : "#6AB04A"; miniCtx.fillRect( part.x * scaleX, part.y * scaleY, scaleX, scaleY ); }); const head = snake[0]; const cx = head.x * scaleX + scaleX / 2; const cy = head.y * scaleY + scaleY / 2; miniCtx.strokeStyle = "rgba(255,255,255,0.5)"; miniCtx.lineWidth = 1; miniCtx.beginPath(); miniCtx.moveTo(cx, 0); miniCtx.lineTo(cx, miniCanvas.height); miniCtx.moveTo(0, cy); miniCtx.lineTo(miniCanvas.width, cy); miniCtx.stroke(); miniCtx.fillStyle = "white"; miniCtx.font = "10px Arial"; miniCtx.fillText("N", miniCanvas.width / 2 - 3, 10); miniCtx.fillText("S", miniCanvas.width / 2 - 3, miniCanvas.height - 2); miniCtx.fillText("W", 2, miniCanvas.height / 2 + 3); miniCtx.fillText("E", miniCanvas.width - 10, miniCanvas.height / 2 + 3); } function spawnFood() { let valid = false; while (!valid) { food.x = Math.floor(Math.random() * (tileCountX - 2)) + 1; food.y = Math.floor(Math.random() * (tileCountY - 2)) + 1; valid = !snake.some(p => p.x === food.x && p.y === food.y); } } function isGameRunning() { return document.getElementById("gameScreen").style.display === "block"; } function updateHighscore() { if (score > highscore) { highscore = score; sessionStorage.setItem("highscore", highscore); document.getElementById("highscore").innerText = highscore; } } function updateLivesUI() { const hearts = [ document.getElementById("life1"), document.getElementById("life2"), document.getElementById("life3") ]; for (let i = 0; i < 3; i++) { hearts[i].src = i < lives ? "assets/heart-full.png" : "assets/heart.png"; } } function updateLevel() { const newLevel = Math.floor(score / 10) + 1; if (newLevel !== level) { level = newLevel; document.getElementById("levelText").innerText = "Level " + level; playSound(levelUpSound); } moveDelay = Math.max(100, 250 - (level - 1) * 10); } function toggleSound() { isSoundOn = !isSoundOn; localStorage.setItem("sound", isSoundOn ? "on" : "off"); updateSoundUI(); } function updateSoundUI() { const soundBtn = document.getElementById("soundToggle"); if (soundBtn) { soundBtn.innerText = isSoundOn ? "Efek Suara (ON)" : "Efek Suara (OFF)"; } } window.onload = () => { updateSoundUI(); }; function showGuide(source) { guideSource = source; document.getElementById("guidePopup").style.display = "flex"; const guideBox = document.querySelector(".guide-box"); guideBox.scrollTop = 0; } function closeGuide() { document.getElementById("guidePopup").style.display = "none"; document.activeElement.blur(); } function showTutorial() { onboardingComplete = false; currentStep = 1; document.getElementById("startScreen").style.display = "none"; const onboardingScreen = document.getElementById("onboardingScreen"); onboardingScreen.style.display = "flex"; document.getElementById("step1").style.display = "block"; document.getElementById("step2").style.display = "none"; document.getElementById("step3").style.display = "none"; const statusBox = document.getElementById("cameraStatus"); if (statusBox) { statusBox.innerText = "Mendeteksi Tangan..."; statusBox.classList.remove("ready"); } } const hand = document.querySelector(".demo-hand"); const gestureLabel = document.getElementById("gestureLabel"); const animations = [ { x: "-50%", y: "-150%", text: "Atas" }, { x: "100%", y: "-50%", text: "Kanan" }, { x: "-50%", y: "50%", text: "Bawah" }, { x: "-200%", y: "-50%", text: "Kiri" } ]; let index = 0; setInterval(() => { const anim = animations[index]; hand.style.transform = `translate(${anim.x}, ${anim.y})`; gestureLabel.innerText = anim.text; index = (index + 1) % animations.length; }, 1000); // document.addEventListener("keydown", (e) => { // if (e.key === "Escape") { // const guideVisible = document.getElementById("guidePopup").style.display === "flex"; // if (guideVisible) { // closeGuide(); // if (guideSource === "pause") { // document.getElementById("pauseScreen").style.display = "flex"; // } else { // document.getElementById("startScreen").style.display = "flex"; // } // return; // } // if (isGameRunning()) { // togglePause(); // return; // } // } // if (isPaused) return; // let newDir = null; // let sound = null; // switch (e.key) { // case "ArrowUp": newDir = { x: 0, y: -1 }; sound = upSound; break; // case "ArrowDown": newDir = { x: 0, y: 1 }; sound = downSound; break; // case "ArrowLeft": newDir = { x: -1, y: 0 }; sound = leftSound; break; // case "ArrowRight": newDir = { x: 1, y: 0 }; sound = rightSound; break; // } // if (!newDir) return; // if (velocity.x === 0 && velocity.y === 0) { // const defaultDir = { // x: snake[0].x - snake[1].x, // y: snake[0].y - snake[1].y // }; // if (newDir.x === -defaultDir.x && newDir.y === -defaultDir.y) return; // playSound(sound); // velocity = newDir; // return; // } // const lastDir = directionQueue.length > 0 // ? directionQueue[directionQueue.length - 1] // : velocity; // if (newDir.x === -lastDir.x && newDir.y === -lastDir.y) return; // if (newDir.x === lastDir.x && newDir.y === lastDir.y) return; // playSound(sound); // if (directionQueue.length < 3) { // directionQueue.push(newDir); // } // });