Coment section

This commit is contained in:
Jevinca Marvella 2025-12-16 01:25:10 +07:00
parent 7499811a24
commit 4243584706
2 changed files with 174 additions and 55 deletions

View File

@ -1,33 +1,58 @@
/* ------------------------ /* ==========================================
6. INPUT HANDLER 2048 CONTROLS
------------------------ ==========================================
*/ fungsi utama:
1. handleKey() - mendeteksi keyboard (Arrow/WASD)
2. Touch Events - meneteksi swipe mobile
3. inputLocked - prevent konflik saat panel volume buka
========================================== */
// flag untuk blokir input game saat setting panel aktif
let inputLocked = false; let inputLocked = false;
/* ==========================================
KEYBOARD INPUT HANDLER
========================================== */
function handleKey(e) { function handleKey(e) {
if (isMoving || inputLocked) return; // ⛔ BLOK kalau sedang buka sound panel // Blokir input kalau:
// 1. Tile sedang bergerak (isMoving = true)
// 2. Panel volume sedang dibuka (inputLocked = true)
if (isMoving || inputLocked) return;
let moved = false; let moved = false;
const k = e.key; const k = e.key;
if (k === "ArrowLeft" || k === "a" || k === "A") { e.preventDefault(); moved = moveLeft(); } // Deteksi 4 arah: Arrow keys atau WASD
else if (k === "ArrowRight" || k === "d" || k === "D") { e.preventDefault(); moved = moveRight(); } if (k === "ArrowLeft" || k === "a" || k === "A") {
else if (k === "ArrowUp" || k === "w" || k === "W") { e.preventDefault(); moved = moveUp(); } e.preventDefault(); // Cegah scroll halaman
else if (k === "ArrowDown" || k === "s" || k === "S") { e.preventDefault(); moved = moveDown(); } moved = moveLeft(); }
else if (k === "ArrowRight" || k === "d" || k === "D") {
e.preventDefault();
moved = moveRight(); }
else if (k === "ArrowUp" || k === "w" || k === "W") {
e.preventDefault();
moved = moveUp(); }
else if (k === "ArrowDown" || k === "s" || k === "S") {
e.preventDefault();
moved = moveDown(); }
if (moved) { // Kalau tile berhasil bergerak
isMoving = true; if (moved) {
isMoving = true; // Lock input biar nggak spam
// Delay 100ms untuk animasi selesai
setTimeout(() => { setTimeout(() => {
const added = addNewTile(); const added = addNewTile(); // Spawn tile baru
// Cek game over: board penuh ATAU nggak bisa move
if (!added || !canMove()) { if (!added || !canMove()) {
setTimeout(() => showGameOver(), 300); setTimeout(() => showGameOver(), 300);
} }
isMoving = false; isMoving = false; // Unlock input
}, 100); }, 100);
} else { } else {
// Shake effect on invalid move //Invalid move - Kasih feedback shake
const b = document.getElementById("board"); const b = document.getElementById("board");
if (b) { if (b) {
b.classList.add("shake"); b.classList.add("shake");
@ -36,37 +61,49 @@ function handleKey(e) {
} }
} }
/* Touch Swipe */ /* ==========================================
TOUCH/SWIPE INPUT HANDLER (Mobile)
========================================== */
// Simpan posisi saat awal sentuhan
let touchStartX = 0; let touchStartX = 0;
let touchStartY = 0; let touchStartY = 0;
// Event 1: Catat posisi awal saat jari menyentuh layar
document.addEventListener("touchstart", function (e) { document.addEventListener("touchstart", function (e) {
if (inputLocked) return; // ⛔ jangan catat swipe if (inputLocked) return; // Jangan catat swipe kalau panel volume aktif
const t = e.touches[0]; const t = e.touches[0];
touchStartX = t.clientX; touchStartX = t.clientX; // Posisi X awal
touchStartY = t.clientY; touchStartY = t.clientY; // Posisi Y awal
}, { passive: true }); }, { passive: true }); // Passive = performa lebih smooth
// Event 2: Deteksi arah swipe saat jari diangkat
document.addEventListener("touchend", function (e) { document.addEventListener("touchend", function (e) {
if (isMoving || inputLocked) return; // ⛔ swipe diblok saat sound panel aktif if (isMoving || inputLocked) return; // cek isMoving DAN inputLocked
const t = e.changedTouches[0]; const t = e.changedTouches[0];
const dx = t.clientX - touchStartX;
const dy = t.clientY - touchStartY; // Hitung selisih posisi (delta)
const dx = t.clientX - touchStartX; //Horizontal movement
const dy = t.clientY - touchStartY; //Vertical movement
let moved = false; let moved = false;
// Tentukan arah swipe berdasarkan delta terbesar
if (Math.abs(dx) > Math.abs(dy) && Math.abs(dx) > 30) { if (Math.abs(dx) > Math.abs(dy) && Math.abs(dx) > 30) {
if (dx > 0) moved = moveRight(); // Swipe HORIZONTAL (kiri/kanan)
else moved = moveLeft(); // Minimal 30px biar nggak terlalu sensitif
if (dx > 0) moved = moveRight(); // swipe kanan
else moved = moveLeft(); // swipe kiri
} else if (Math.abs(dy) > 30) { } else if (Math.abs(dy) > 30) {
if (dy > 0) moved = moveDown(); // Swipe VERTICAL (atas/bawah)
else moved = moveUp(); if (dy > 0) moved = moveDown(); // swipe bawah
else moved = moveUp(); // swipe atas
} }
// Logic sama seperti keyboard handler
if (moved) { if (moved) {
isMoving = true; isMoving = true;
setTimeout(() => { setTimeout(() => {

View File

@ -1,79 +1,138 @@
/* ------------------------ /* ==========================================
5. GAME LOGIC 2048 LOGIC
------------------------ */ ==========================================
fungsi inti:
1. addNewTile() - spawn tile baru (angka 2)
2. slide() - algoritma merge tile
3. move functions - 4 arah pergerakan
4. canMove() - Cek game over
========================================== */
/* ==========================================
SPAWN TILE BARU
========================================== */
function addNewTile() { function addNewTile() {
// Step 1: Cari semua cell kosong (value = 0)
const empty = []; const empty = [];
for (let r = 0; r < 4; r++) { for (let r = 0; r < 4; r++) {
for (let c = 0; c < 4; c++) { for (let c = 0; c < 4; c++) {
if (board[r][c] === 0) empty.push({ r, c }); if (board[r][c] === 0) empty.push({ r, c });
} }
} }
// Kalau board penuh, return false (game over)
if (empty.length === 0) return false; if (empty.length === 0) return false;
// Step 2: Pilih 1 posisi random dari cell kosong
const spot = empty[Math.floor(Math.random() * empty.length)]; const spot = empty[Math.floor(Math.random() * empty.length)];
board[spot.r][spot.c] = 2; board[spot.r][spot.c] = 2; // Tile baru selalu angka 2
// Step 3: Animasi "new" + sound effect
const tile = document.getElementById(`${spot.r}-${spot.c}`); const tile = document.getElementById(`${spot.r}-${spot.c}`);
if (tile) { if (tile) {
tile.classList.add("new"); tile.classList.add("new"); // Trigger animasi pop
playSound(audio.pop); playSound(audio.pop); // Sound effect
setTimeout(() => tile.classList.remove("new"), 300); setTimeout(() => tile.classList.remove("new"), 300);
} }
updateTile(spot.r, spot.c, 2);
updateTile(spot.r, spot.c, 2); // Update visual tile
return true; return true;
} }
/* ==========================================
HELPER: FILTER ANGKA NOL
========================================== */
function filterZero(row) { function filterZero(row) {
// Buang semua 0 dari array
// Contoh: [2, 0, 2, 0] → [2, 2]
return row.filter(n => n !== 0); return row.filter(n => n !== 0);
} }
/* ==========================================
ALGORITMA SLIDE & MERGE (CORE!)
==========================================
Ini yang bikin 2+2=4, 4+4=8, dst
========================================== */
function slide(row) { function slide(row) {
// Step 1: Filter zero - kumpulkan tile
// [2, 0, 2, 4] → [2, 2, 4]
row = filterZero(row); row = filterZero(row);
let mergedThisMove = false;
let mergedPositions = [];
let mergeCount = 0;
let mergedThisMove = false;
let mergedPositions = []; // Posisi tile yang di-merge (untuk animasi)
let mergeCount = 0; // Hitung berapa kali merge (untuk combo)
// Step 2: Loop cek tile sebelahan
for (let i = 0; i < row.length - 1; i++) { for (let i = 0; i < row.length - 1; i++) {
if (row[i] === row[i + 1]) { if (row[i] === row[i + 1]) {
row[i] = row[i] * 2; // MERGE! Tile sama ketemu
row[i] = row[i] * 2; // Tile pertama jadi 2x lipat
playSound(audio.merge); playSound(audio.merge); // Sound effect merge
// Haptic feedback (getaran) di mobile
if (navigator.vibrate) { if (navigator.vibrate) {
navigator.vibrate([80, 20, 80]); navigator.vibrate([80, 20, 80]);
} }
currentScore += row[i]; currentScore += row[i]; // Tambah score
row[i + 1] = 0; row[i + 1] = 0; // Tile kedua hilang (jadi 0)
mergedThisMove = true; mergedThisMove = true;
mergedPositions.push(i); mergedPositions.push(i); // Simpan posisi untuk animasi
mergeCount++; mergeCount++;
} }
} }
// Step 3: Filter zero lagi & padding
// [4, 0, 4] → [4, 4] → [4, 4, 0, 0]
row = filterZero(row); row = filterZero(row);
while (row.length < 4) row.push(0); while (row.length < 4) row.push(0); // Padding 0 di kanan
return { row, merged: mergedThisMove, mergedPositions, mergeCount };
return {
row,
merged: mergedThisMove,
mergedPositions,
mergeCount };
} }
/* ==========================================
HELPER: CEK ARRAY SAMA ATAU TIDAK
========================================== */
function arraysEqual(a, b) { function arraysEqual(a, b) {
// Bandingin setiap elemen
// Dipakai untuk cek apakah board berubah setelah move
return a.length === b.length && a.every((v, i) => v === b[i]); return a.length === b.length && a.every((v, i) => v === b[i]);
} }
/* Move functions */ /* ==========================================
MOVE FUNCTIONS - 4 ARAH PERGERAKAN
==========================================
Semua punya struktur yang sama:
1. Loop setiap baris/kolom
2. Panggil slide() untuk merge
3. Cek perubahan dengan arraysEqual()
4. Refresh board kalau ada perubahan
========================================== */
/* MOVE LEFT - Geser ke kiri */
function moveLeft() { function moveLeft() {
let moved = false; let moved = false;
let mergedCells = []; let mergedCells = []; // Track cell yang di-merge
mergesInCurrentMove = 0; mergesInCurrentMove = 0; // Reset combo counter
// Loop setiap baris (horizontal)
for (let r = 0; r < 4; r++) { for (let r = 0; r < 4; r++) {
const { row: newRow, mergedPositions, mergeCount } = slide(board[r]); const { row: newRow, mergedPositions, mergeCount } = slide(board[r]);
// Cek apakah row berubah
if (!arraysEqual(newRow, board[r])) moved = true; if (!arraysEqual(newRow, board[r])) moved = true;
board[r] = newRow; board[r] = newRow;
mergesInCurrentMove += mergeCount; mergesInCurrentMove += mergeCount;
// Simpan posisi cell yang merge untuk combo effect
if (mergedPositions && mergedPositions.length > 0) { if (mergedPositions && mergedPositions.length > 0) {
mergedPositions.forEach(c => { mergedPositions.forEach(c => {
mergedCells.push({ r, c }); mergedCells.push({ r, c });
@ -81,6 +140,7 @@ function moveLeft() {
} }
} }
// Kalau ada pergerakan, update visual + effect
if (moved) { if (moved) {
refreshBoard(); refreshBoard();
triggerComboEffect(mergedCells, mergesInCurrentMove); triggerComboEffect(mergedCells, mergesInCurrentMove);
@ -88,23 +148,28 @@ function moveLeft() {
return moved; return moved;
} }
/* MOVE RIGHT - Geser ke kanan */
function moveRight() { function moveRight() {
let moved = false; let moved = false;
let mergedCells = []; let mergedCells = [];
mergesInCurrentMove = 0; mergesInCurrentMove = 0;
// TRICK: Reverse → slide → reverse lagi
// Supaya bisa pakai logic slide yang sama
for (let r = 0; r < 4; r++) { for (let r = 0; r < 4; r++) {
let reversed = [...board[r]].reverse(); let reversed = [...board[r]].reverse();
const { row: slid, mergedPositions, mergeCount } = slide(reversed); const { row: slid, mergedPositions, mergeCount } = slide(reversed);
let newRow = slid.reverse(); let newRow = slid.reverse(); // Balik lagi ke posisi asli
if (!arraysEqual(newRow, board[r])) moved = true; if (!arraysEqual(newRow, board[r])) moved = true;
board[r] = newRow; board[r] = newRow;
mergesInCurrentMove += mergeCount; mergesInCurrentMove += mergeCount;
// Convert posisi merge ke koordinat asli (dari kanan)
if (mergedPositions && mergedPositions.length > 0) { if (mergedPositions && mergedPositions.length > 0) {
mergedPositions.forEach(pos => { mergedPositions.forEach(pos => {
const c = 3 - pos; const c = 3 - pos; // Mirror position
mergedCells.push({ r, c }); mergedCells.push({ r, c });
}); });
} }
@ -117,14 +182,19 @@ function moveRight() {
return moved; return moved;
} }
/* MOVE UP - Geser ke atas */
function moveUp() { function moveUp() {
let moved = false; let moved = false;
let mergedCells = []; let mergedCells = [];
mergesInCurrentMove = 0; mergesInCurrentMove = 0;
// Loop setiap kolom (vertical)
for (let c = 0; c < 4; c++) { for (let c = 0; c < 4; c++) {
// Ambil kolom vertikal sebagai array
const col = [board[0][c], board[1][c], board[2][c], board[3][c]]; const col = [board[0][c], board[1][c], board[2][c], board[3][c]];
const { row: newCol, mergedPositions, mergeCount } = slide(col); const { row: newCol, mergedPositions, mergeCount } = slide(col);
// Update board per row
for (let r = 0; r < 4; r++) { for (let r = 0; r < 4; r++) {
if (board[r][c] !== newCol[r]) moved = true; if (board[r][c] !== newCol[r]) moved = true;
board[r][c] = newCol[r]; board[r][c] = newCol[r];
@ -146,15 +216,18 @@ function moveUp() {
return moved; return moved;
} }
/* MOVE DOWN - Geser ke bawah */
function moveDown() { function moveDown() {
let moved = false; let moved = false;
let mergedCells = []; let mergedCells = [];
mergesInCurrentMove = 0; mergesInCurrentMove = 0;
for (let c = 0; c < 4; c++) { for (let c = 0; c < 4; c++) {
//TRICK: Ambil kolom dari bawah ke atas (reversed)
const col = [board[3][c], board[2][c], board[1][c], board[0][c]]; const col = [board[3][c], board[2][c], board[1][c], board[0][c]];
const { row: slid, mergedPositions, mergeCount } = slide(col); const { row: slid, mergedPositions, mergeCount } = slide(col);
const newCol = slid.reverse(); const newCol = slid.reverse(); // Balik ke urutan normal
for (let r = 0; r < 4; r++) { for (let r = 0; r < 4; r++) {
if (board[r][c] !== newCol[r]) moved = true; if (board[r][c] !== newCol[r]) moved = true;
board[r][c] = newCol[r]; board[r][c] = newCol[r];
@ -162,9 +235,10 @@ function moveDown() {
mergesInCurrentMove += mergeCount; mergesInCurrentMove += mergeCount;
// Convert posisi merge ke koordinat asli (dari bawah)
if (mergedPositions && mergedPositions.length > 0) { if (mergedPositions && mergedPositions.length > 0) {
mergedPositions.forEach(pos => { mergedPositions.forEach(pos => {
const r = 3 - pos; const r = 3 - pos; // Mirror position
mergedCells.push({ r, c }); mergedCells.push({ r, c });
}); });
} }
@ -177,19 +251,27 @@ function moveDown() {
return moved; return moved;
} }
/* ==========================================
CEK GAME OVER
========================================== */
function canMove() { function canMove() {
// Kondisi 1: Ada cell kosong?
for (let r = 0; r < 4; r++) { for (let r = 0; r < 4; r++) {
for (let c = 0; c < 4; c++) { for (let c = 0; c < 4; c++) {
if (board[r][c] === 0) return true; if (board[r][c] === 0) return true; // Masih ada ruang
} }
} }
// Kondisi 2: Ada tile sebelahan yang bisa di-merge?
for (let r = 0; r < 4; r++) { for (let r = 0; r < 4; r++) {
for (let c = 0; c < 4; c++) { for (let c = 0; c < 4; c++) {
const current = board[r][c]; const current = board[r][c];
//cek kanan
if (c < 3 && board[r][c + 1] === current) return true; if (c < 3 && board[r][c + 1] === current) return true;
//cek bawah
if (r < 3 && board[r + 1][c] === current) return true; if (r < 3 && board[r + 1][c] === current) return true;
} }
} }
// Kalau kedua kondisi false → STUCK = Game Over
return false; return false;
} }