2048
This commit is contained in:
parent
cb758824cd
commit
79a0cfc00f
@ -379,5 +379,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<script src="Score_Request.js"></script>
|
<script src="Score_Request.js"></script>
|
||||||
<script src="2048.js"></script>
|
<script src="2048.js"></script>
|
||||||
|
<script src="Audio_2048.js"></script>
|
||||||
|
<script src="Visual_Effects_2048.js"></script>
|
||||||
|
<script src="User_Interface_2048.js"></script>
|
||||||
|
<script src="2048_Logic.js"></script>
|
||||||
|
<script src="2048_Controls.js"></script>
|
||||||
|
<script src="Main_2048.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
970
2048.js
970
2048.js
@ -1,31 +1,21 @@
|
|||||||
/* ------------------------
|
/* ------------------------
|
||||||
State & Variables
|
1. GAME STATE & VARIABLES
|
||||||
------------------------ */
|
------------------------ */
|
||||||
let board = [];
|
let board = [];
|
||||||
let currentScore = 0;
|
let currentScore = 0;
|
||||||
|
|
||||||
// 1. Ambil username dari sessionStorage (sesuai sistem login kamu)
|
// Ambil username dari sessionStorage
|
||||||
const currentUser = sessionStorage.getItem("loggedInUser") || "guest";
|
const currentUser = sessionStorage.getItem("loggedInUser") || "guest";
|
||||||
|
|
||||||
// 2. Buat nama kunci unik, misal: "highScore2048_budi"
|
// Buat nama kunci unik
|
||||||
const storageKey = 'highScore2048_' + currentUser;
|
const storageKey = 'highScore2048_' + currentUser;
|
||||||
|
|
||||||
// 3. Ambil skor milik user tersebut saja
|
// Ambil skor milik user tersebut
|
||||||
|
|
||||||
let highScore = parseInt(localStorage.getItem(storageKey)) || 0;
|
let highScore = parseInt(localStorage.getItem(storageKey)) || 0;
|
||||||
let lastMoveDir = null;
|
let lastMoveDir = null;
|
||||||
let isMoving = false;
|
let isMoving = false;
|
||||||
let mergesInCurrentMove = 0;
|
let mergesInCurrentMove = 0;
|
||||||
|
|
||||||
/* ------------------------
|
|
||||||
Audio Setup
|
|
||||||
------------------------ */
|
|
||||||
const audio = {
|
|
||||||
bg: new Audio("Background_Music.mp3"),
|
|
||||||
pop: new Audio("Pop.mp3"),
|
|
||||||
merge: new Audio("Merge.mp3")
|
|
||||||
};
|
|
||||||
|
|
||||||
// Sound State (baca dari localStorage atau default ON)
|
// Sound State (baca dari localStorage atau default ON)
|
||||||
let soundState = {
|
let soundState = {
|
||||||
bg: localStorage.getItem('sound_bg') !== 'false',
|
bg: localStorage.getItem('sound_bg') !== 'false',
|
||||||
@ -33,961 +23,9 @@ let soundState = {
|
|||||||
merge: localStorage.getItem('sound_merge') !== 'false'
|
merge: localStorage.getItem('sound_merge') !== 'false'
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update audio volumes based on state
|
|
||||||
function updateAudioVolumes() {
|
|
||||||
audio.bg.volume = soundState.bg ? 0.25 : 0;
|
|
||||||
audio.pop.volume = soundState.pop ? 0.9 : 0;
|
|
||||||
audio.merge.volume = soundState.merge ? 1.0 : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
audio.bg.loop = true;
|
|
||||||
|
|
||||||
function tryPlayBg() {
|
|
||||||
if (!soundState.bg) return; // Jangan play kalau muted
|
|
||||||
|
|
||||||
audio.bg.play().catch(() => {
|
|
||||||
const unlock = () => {
|
|
||||||
if (soundState.bg) audio.bg.play().catch(()=>{});
|
|
||||||
window.removeEventListener("keydown", unlock);
|
|
||||||
window.removeEventListener("click", unlock);
|
|
||||||
};
|
|
||||||
window.addEventListener("keydown", unlock, { once: true });
|
|
||||||
window.addEventListener("click", unlock, { once: true });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== AUTO TUTORIAL FOR FIRST TIME USERS ===== */
|
|
||||||
function checkAndShowTutorial() {
|
|
||||||
const showTutorial = sessionStorage.getItem("showTutorial");
|
|
||||||
const loggedInUser = sessionStorage.getItem("loggedInUser");
|
|
||||||
|
|
||||||
// Jika user baru (showTutorial = "true"), tampilkan tutorial otomatis
|
|
||||||
if (showTutorial === "true" && loggedInUser) {
|
|
||||||
setTimeout(() => {
|
|
||||||
const tutorialOverlay = document.getElementById('tutorial-overlay');
|
|
||||||
if (tutorialOverlay) {
|
|
||||||
tutorialOverlay.style.display = 'flex';
|
|
||||||
}
|
|
||||||
// Set flag agar tidak muncul lagi di session ini
|
|
||||||
sessionStorage.setItem("showTutorial", "false");
|
|
||||||
}, 500); // Delay 500ms agar halaman sudah fully loaded
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ------------------------
|
|
||||||
DOM Ready
|
|
||||||
------------------------ */
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
|
||||||
updateHighScoreDisplay();
|
|
||||||
setupBoard();
|
|
||||||
addNewTile();
|
|
||||||
addNewTile();
|
|
||||||
updateAudioVolumes(); // Apply saved sound settings
|
|
||||||
tryPlayBg();
|
|
||||||
document.addEventListener("keydown", handleKey);
|
|
||||||
setupEventListeners();
|
|
||||||
checkAndShowTutorial();
|
|
||||||
});
|
|
||||||
|
|
||||||
/* ------------------------
|
|
||||||
Event Listeners Setup
|
|
||||||
------------------------ */
|
|
||||||
function setupEventListeners() {
|
|
||||||
// Tutorial Modal
|
|
||||||
const btnTutorial = document.getElementById('btn-tutorial');
|
|
||||||
const tutorialOverlay = document.getElementById('tutorial-overlay');
|
|
||||||
const closeTutorial = document.getElementById('close-tutorial');
|
|
||||||
|
|
||||||
if (btnTutorial) {
|
|
||||||
btnTutorial.addEventListener('click', function() {
|
|
||||||
tutorialOverlay.style.display = 'flex';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (closeTutorial) {
|
|
||||||
closeTutorial.addEventListener('click', function() {
|
|
||||||
tutorialOverlay.style.display = 'none';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tutorialOverlay) {
|
|
||||||
tutorialOverlay.addEventListener('click', function(e) {
|
|
||||||
if (e.target === tutorialOverlay) {
|
|
||||||
tutorialOverlay.style.display = 'none';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restart button (top right)
|
|
||||||
const btnRestart = document.getElementById('btn-restart');
|
|
||||||
if (btnRestart) {
|
|
||||||
btnRestart.addEventListener('click', restartGame);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Game over modal buttons
|
|
||||||
const btnPlayAgain = document.getElementById('btn-play-again');
|
|
||||||
const btnHome = document.getElementById('btn-home');
|
|
||||||
const gameOverClose = document.getElementById('game-over-close');
|
|
||||||
|
|
||||||
if (btnPlayAgain) {
|
|
||||||
btnPlayAgain.addEventListener('click', playAgain);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (btnHome) {
|
|
||||||
btnHome.addEventListener('click', goHome);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (gameOverClose) {
|
|
||||||
gameOverClose.addEventListener('click', hideGameOver);
|
|
||||||
}
|
|
||||||
|
|
||||||
const gameOverOverlay = document.getElementById('game-over-overlay');
|
|
||||||
if (gameOverOverlay) {
|
|
||||||
gameOverOverlay.addEventListener('click', function(e) {
|
|
||||||
if (e.target === this) {
|
|
||||||
hideGameOver();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sound Control Buttons
|
|
||||||
const btnSoundBg = document.getElementById('btn-sound-bg');
|
|
||||||
const btnSoundPop = document.getElementById('btn-sound-pop');
|
|
||||||
const btnSoundMerge = document.getElementById('btn-sound-merge');
|
|
||||||
|
|
||||||
if (btnSoundBg) {
|
|
||||||
btnSoundBg.addEventListener('click', () => toggleSound('bg'));
|
|
||||||
updateSoundButtonState(btnSoundBg, soundState.bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (btnSoundPop) {
|
|
||||||
btnSoundPop.addEventListener('click', () => toggleSound('pop'));
|
|
||||||
updateSoundButtonState(btnSoundPop, soundState.pop);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (btnSoundMerge) {
|
|
||||||
btnSoundMerge.addEventListener('click', () => toggleSound('merge'));
|
|
||||||
updateSoundButtonState(btnSoundMerge, soundState.merge);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ------------------------
|
|
||||||
Setup & Rendering
|
|
||||||
------------------------ */
|
|
||||||
function setupBoard() {
|
|
||||||
board = [];
|
|
||||||
currentScore = 0;
|
|
||||||
updateScoreDisplay();
|
|
||||||
|
|
||||||
const container = document.getElementById("board");
|
|
||||||
if (!container) {
|
|
||||||
console.error("Board element not found (#board).");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
container.innerHTML = "";
|
|
||||||
for (let r = 0; r < 4; r++) {
|
|
||||||
board[r] = [];
|
|
||||||
for (let c = 0; c < 4; c++) {
|
|
||||||
board[r][c] = 0;
|
|
||||||
const tile = document.createElement("div");
|
|
||||||
tile.id = `${r}-${c}`;
|
|
||||||
tile.className = "tile";
|
|
||||||
container.appendChild(tile);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateTile(row, col, num) {
|
|
||||||
const tile = document.getElementById(`${row}-${col}`);
|
|
||||||
if (!tile) return;
|
|
||||||
|
|
||||||
tile.className = "tile";
|
|
||||||
|
|
||||||
if (num > 0) {
|
|
||||||
tile.textContent = num;
|
|
||||||
tile.classList.add("tile-" + num);
|
|
||||||
} else {
|
|
||||||
tile.textContent = "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function refreshBoard() {
|
|
||||||
for (let r = 0; r < 4; r++) {
|
|
||||||
for (let c = 0; c < 4; c++) {
|
|
||||||
updateTile(r, c, board[r][c]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
updateScoreDisplay();
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ------------------------
|
|
||||||
Score Management
|
|
||||||
------------------------ */
|
|
||||||
function updateScoreDisplay() {
|
|
||||||
const scoreEl = document.getElementById("score");
|
|
||||||
if (scoreEl) {
|
|
||||||
scoreEl.textContent = currentScore;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentScore > highScore) {
|
|
||||||
highScore = currentScore;
|
|
||||||
// Gunakan storageKey yang sudah kita buat di atas (dinamis sesuai user)
|
|
||||||
localStorage.setItem(storageKey, highScore);
|
|
||||||
updateHighScoreDisplay();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateHighScoreDisplay() {
|
|
||||||
const highScoreEl = document.getElementById('high-score');
|
|
||||||
if (highScoreEl) {
|
|
||||||
highScoreEl.textContent = highScore;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetScore() {
|
|
||||||
currentScore = 0;
|
|
||||||
updateScoreDisplay();
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ------------------------
|
|
||||||
Add New Tile
|
|
||||||
------------------------ */
|
|
||||||
function addNewTile() {
|
|
||||||
const empty = [];
|
|
||||||
for (let r = 0; r < 4; r++) {
|
|
||||||
for (let c = 0; c < 4; c++) {
|
|
||||||
if (board[r][c] === 0) empty.push({ r, c });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (empty.length === 0) return false;
|
|
||||||
|
|
||||||
const spot = empty[Math.floor(Math.random() * empty.length)];
|
|
||||||
board[spot.r][spot.c] = 2;
|
|
||||||
|
|
||||||
const tile = document.getElementById(`${spot.r}-${spot.c}`);
|
|
||||||
if (tile) {
|
|
||||||
tile.classList.add("new");
|
|
||||||
playSound(audio.pop);
|
|
||||||
setTimeout(() => tile.classList.remove("new"), 300);
|
|
||||||
}
|
|
||||||
updateTile(spot.r, spot.c, 2);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Safe playSound with mute check */
|
|
||||||
function playSound(soundObj) {
|
|
||||||
try {
|
|
||||||
// Check if sound is enabled
|
|
||||||
if (soundObj === audio.pop && !soundState.pop) return;
|
|
||||||
if (soundObj === audio.merge && !soundState.merge) return;
|
|
||||||
if (soundObj === audio.bg && !soundState.bg) return;
|
|
||||||
|
|
||||||
soundObj.currentTime = 0;
|
|
||||||
soundObj.play().catch(() => {});
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ------------------------
|
|
||||||
Movement Logic
|
|
||||||
------------------------ */
|
|
||||||
function filterZero(row) {
|
|
||||||
return row.filter(n => n !== 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
function slide(row) {
|
|
||||||
row = filterZero(row);
|
|
||||||
let mergedThisMove = false;
|
|
||||||
let mergedPositions = [];
|
|
||||||
let mergeCount = 0;
|
|
||||||
|
|
||||||
for (let i = 0; i < row.length - 1; i++) {
|
|
||||||
if (row[i] === row[i + 1]) {
|
|
||||||
row[i] = row[i] * 2;
|
|
||||||
|
|
||||||
playSound(audio.merge);
|
|
||||||
|
|
||||||
if (navigator.vibrate) {
|
|
||||||
navigator.vibrate([80, 20, 80]);
|
|
||||||
}
|
|
||||||
|
|
||||||
currentScore += row[i];
|
|
||||||
row[i + 1] = 0;
|
|
||||||
mergedThisMove = true;
|
|
||||||
mergedPositions.push(i);
|
|
||||||
mergeCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
row = filterZero(row);
|
|
||||||
while (row.length < 4) row.push(0);
|
|
||||||
return { row, merged: mergedThisMove, mergedPositions, mergeCount };
|
|
||||||
}
|
|
||||||
|
|
||||||
function arraysEqual(a, b) {
|
|
||||||
return a.length === b.length && a.every((v, i) => v === b[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Move functions */
|
|
||||||
function moveLeft() {
|
|
||||||
let moved = false;
|
|
||||||
let mergedCells = [];
|
|
||||||
mergesInCurrentMove = 0;
|
|
||||||
|
|
||||||
for (let r = 0; r < 4; r++) {
|
|
||||||
const { row: newRow, mergedPositions, mergeCount } = slide(board[r]);
|
|
||||||
if (!arraysEqual(newRow, board[r])) moved = true;
|
|
||||||
board[r] = newRow;
|
|
||||||
|
|
||||||
mergesInCurrentMove += mergeCount;
|
|
||||||
|
|
||||||
if (mergedPositions && mergedPositions.length > 0) {
|
|
||||||
mergedPositions.forEach(c => {
|
|
||||||
mergedCells.push({ r, c });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (moved) {
|
|
||||||
refreshBoard();
|
|
||||||
triggerComboEffect(mergedCells, mergesInCurrentMove);
|
|
||||||
}
|
|
||||||
return moved;
|
|
||||||
}
|
|
||||||
|
|
||||||
function moveRight() {
|
|
||||||
let moved = false;
|
|
||||||
let mergedCells = [];
|
|
||||||
mergesInCurrentMove = 0;
|
|
||||||
|
|
||||||
for (let r = 0; r < 4; r++) {
|
|
||||||
let reversed = [...board[r]].reverse();
|
|
||||||
const { row: slid, mergedPositions, mergeCount } = slide(reversed);
|
|
||||||
let newRow = slid.reverse();
|
|
||||||
if (!arraysEqual(newRow, board[r])) moved = true;
|
|
||||||
board[r] = newRow;
|
|
||||||
|
|
||||||
mergesInCurrentMove += mergeCount;
|
|
||||||
|
|
||||||
if (mergedPositions && mergedPositions.length > 0) {
|
|
||||||
mergedPositions.forEach(pos => {
|
|
||||||
const c = 3 - pos;
|
|
||||||
mergedCells.push({ r, c });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (moved) {
|
|
||||||
refreshBoard();
|
|
||||||
triggerComboEffect(mergedCells, mergesInCurrentMove);
|
|
||||||
}
|
|
||||||
return moved;
|
|
||||||
}
|
|
||||||
|
|
||||||
function moveUp() {
|
|
||||||
let moved = false;
|
|
||||||
let mergedCells = [];
|
|
||||||
mergesInCurrentMove = 0;
|
|
||||||
|
|
||||||
for (let c = 0; c < 4; c++) {
|
|
||||||
const col = [board[0][c], board[1][c], board[2][c], board[3][c]];
|
|
||||||
const { row: newCol, mergedPositions, mergeCount } = slide(col);
|
|
||||||
for (let r = 0; r < 4; r++) {
|
|
||||||
if (board[r][c] !== newCol[r]) moved = true;
|
|
||||||
board[r][c] = newCol[r];
|
|
||||||
}
|
|
||||||
|
|
||||||
mergesInCurrentMove += mergeCount;
|
|
||||||
|
|
||||||
if (mergedPositions && mergedPositions.length > 0) {
|
|
||||||
mergedPositions.forEach(r => {
|
|
||||||
mergedCells.push({ r, c });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (moved) {
|
|
||||||
refreshBoard();
|
|
||||||
triggerComboEffect(mergedCells, mergesInCurrentMove);
|
|
||||||
}
|
|
||||||
return moved;
|
|
||||||
}
|
|
||||||
|
|
||||||
function moveDown() {
|
|
||||||
let moved = false;
|
|
||||||
let mergedCells = [];
|
|
||||||
mergesInCurrentMove = 0;
|
|
||||||
|
|
||||||
for (let c = 0; c < 4; c++) {
|
|
||||||
const col = [board[3][c], board[2][c], board[1][c], board[0][c]];
|
|
||||||
const { row: slid, mergedPositions, mergeCount } = slide(col);
|
|
||||||
const newCol = slid.reverse();
|
|
||||||
for (let r = 0; r < 4; r++) {
|
|
||||||
if (board[r][c] !== newCol[r]) moved = true;
|
|
||||||
board[r][c] = newCol[r];
|
|
||||||
}
|
|
||||||
|
|
||||||
mergesInCurrentMove += mergeCount;
|
|
||||||
|
|
||||||
if (mergedPositions && mergedPositions.length > 0) {
|
|
||||||
mergedPositions.forEach(pos => {
|
|
||||||
const r = 3 - pos;
|
|
||||||
mergedCells.push({ r, c });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (moved) {
|
|
||||||
refreshBoard();
|
|
||||||
triggerComboEffect(mergedCells, mergesInCurrentMove);
|
|
||||||
}
|
|
||||||
return moved;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ------------------------
|
|
||||||
Input Handling
|
|
||||||
------------------------ */
|
|
||||||
function handleKey(e) {
|
|
||||||
if (isMoving) return;
|
|
||||||
|
|
||||||
let moved = false;
|
|
||||||
|
|
||||||
if (e.key === "ArrowLeft") {
|
|
||||||
e.preventDefault();
|
|
||||||
moved = moveLeft();
|
|
||||||
}
|
|
||||||
else if (e.key === "ArrowRight") {
|
|
||||||
e.preventDefault();
|
|
||||||
moved = moveRight();
|
|
||||||
}
|
|
||||||
else if (e.key === "ArrowUp") {
|
|
||||||
e.preventDefault();
|
|
||||||
moved = moveUp();
|
|
||||||
}
|
|
||||||
else if (e.key === "ArrowDown") {
|
|
||||||
e.preventDefault();
|
|
||||||
moved = moveDown();
|
|
||||||
}
|
|
||||||
else if (e.key === "a" || e.key === "A") {
|
|
||||||
e.preventDefault();
|
|
||||||
moved = moveLeft();
|
|
||||||
}
|
|
||||||
else if (e.key === "d" || e.key === "D") {
|
|
||||||
e.preventDefault();
|
|
||||||
moved = moveRight();
|
|
||||||
}
|
|
||||||
else if (e.key === "w" || e.key === "W") {
|
|
||||||
e.preventDefault();
|
|
||||||
moved = moveUp();
|
|
||||||
}
|
|
||||||
else if (e.key === "s" || e.key === "S") {
|
|
||||||
e.preventDefault();
|
|
||||||
moved = moveDown();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (moved) {
|
|
||||||
isMoving = true;
|
|
||||||
setTimeout(() => {
|
|
||||||
const added = addNewTile();
|
|
||||||
if (!added || !canMove()) {
|
|
||||||
setTimeout(() => showGameOver(), 300);
|
|
||||||
}
|
|
||||||
isMoving = false;
|
|
||||||
}, 100);
|
|
||||||
} else {
|
|
||||||
const b = document.getElementById("board");
|
|
||||||
if (b) {
|
|
||||||
b.classList.add("shake");
|
|
||||||
setTimeout(()=>b.classList.remove("shake"), 400);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function canMove() {
|
|
||||||
for (let r = 0; r < 4; r++) {
|
|
||||||
for (let c = 0; c < 4; c++) {
|
|
||||||
if (board[r][c] === 0) return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let r = 0; r < 4; r++) {
|
|
||||||
for (let c = 0; c < 4; c++) {
|
|
||||||
const current = board[r][c];
|
|
||||||
if (c < 3 && board[r][c + 1] === current) return true;
|
|
||||||
if (r < 3 && board[r + 1][c] === current) return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ------------------------
|
|
||||||
Touch Swipe
|
|
||||||
------------------------ */
|
|
||||||
let touchStartX = 0;
|
|
||||||
let touchStartY = 0;
|
|
||||||
|
|
||||||
document.addEventListener("touchstart", function (e) {
|
|
||||||
const t = e.touches[0];
|
|
||||||
touchStartX = t.clientX;
|
|
||||||
touchStartY = t.clientY;
|
|
||||||
}, { passive: true });
|
|
||||||
|
|
||||||
document.addEventListener("touchend", function (e) {
|
|
||||||
if (isMoving) return;
|
|
||||||
|
|
||||||
const t = e.changedTouches[0];
|
|
||||||
const dx = t.clientX - touchStartX;
|
|
||||||
const dy = t.clientY - touchStartY;
|
|
||||||
|
|
||||||
let moved = false;
|
|
||||||
|
|
||||||
if (Math.abs(dx) > Math.abs(dy) && Math.abs(dx) > 30) {
|
|
||||||
if (dx > 0) {
|
|
||||||
moved = moveRight();
|
|
||||||
} else {
|
|
||||||
moved = moveLeft();
|
|
||||||
}
|
|
||||||
} else if (Math.abs(dy) > 30) {
|
|
||||||
if (dy > 0) {
|
|
||||||
moved = moveDown();
|
|
||||||
} else {
|
|
||||||
moved = moveUp();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (moved) {
|
|
||||||
isMoving = true;
|
|
||||||
setTimeout(() => {
|
|
||||||
const added = addNewTile();
|
|
||||||
if (!added || !canMove()) {
|
|
||||||
setTimeout(() => showGameOver(), 300);
|
|
||||||
}
|
|
||||||
isMoving = false;
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
}, { passive: true });
|
|
||||||
|
|
||||||
/* ------------------------
|
|
||||||
Game Controls
|
|
||||||
------------------------ */
|
|
||||||
function restartGame() {
|
|
||||||
hideGameOver();
|
|
||||||
resetScore();
|
|
||||||
setupBoard();
|
|
||||||
addNewTile();
|
|
||||||
addNewTile();
|
|
||||||
refreshBoard();
|
|
||||||
isMoving = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function playAgain() {
|
|
||||||
restartGame();
|
|
||||||
}
|
|
||||||
|
|
||||||
function goHome() {
|
|
||||||
try {
|
|
||||||
audio.bg.pause();
|
|
||||||
audio.bg.currentTime = 0;
|
|
||||||
} catch (e) {}
|
|
||||||
window.location.href = "Homepage.html";
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ------------------------
|
|
||||||
Game Over Modal
|
|
||||||
------------------------ */
|
|
||||||
function showGameOver() {
|
|
||||||
const finalScore = currentScore;
|
|
||||||
|
|
||||||
// --- TAMBAHKAN BAGIAN INI (MULAI) ---
|
|
||||||
// Mengecek apakah fungsi saveScore ada (dari file Score_Request.js)
|
|
||||||
if (typeof saveScore === 'function') {
|
|
||||||
console.log("Mengirim skor ke database:", finalScore); // Debugging
|
|
||||||
saveScore(finalScore);
|
|
||||||
} else {
|
|
||||||
console.error("Fungsi saveScore tidak ditemukan! Pastikan Score_Request.js sudah diload.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const isNewHighScore = finalScore >= highScore && finalScore > 0;
|
|
||||||
|
|
||||||
const finalScoreEl = document.getElementById('final-score');
|
|
||||||
if (finalScoreEl) {
|
|
||||||
finalScoreEl.textContent = finalScore;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newHighScoreBadge = document.getElementById('new-high-score-badge');
|
|
||||||
const highScoreDisplay = document.getElementById('high-score-display');
|
|
||||||
|
|
||||||
if (isNewHighScore) {
|
|
||||||
if (newHighScoreBadge) newHighScoreBadge.style.display = 'inline-block';
|
|
||||||
if (highScoreDisplay) highScoreDisplay.style.display = 'none';
|
|
||||||
} else {
|
|
||||||
if (newHighScoreBadge) newHighScoreBadge.style.display = 'none';
|
|
||||||
if (highScoreDisplay) highScoreDisplay.style.display = 'block';
|
|
||||||
const modalHighScore = document.getElementById('modal-high-score');
|
|
||||||
if (modalHighScore) modalHighScore.textContent = highScore;
|
|
||||||
}
|
|
||||||
|
|
||||||
const gameOverOverlay = document.getElementById('game-over-overlay');
|
|
||||||
if (gameOverOverlay) {
|
|
||||||
gameOverOverlay.style.display = 'flex';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideGameOver() {
|
|
||||||
const gameOverOverlay = document.getElementById('game-over-overlay');
|
|
||||||
if (gameOverOverlay) {
|
|
||||||
gameOverOverlay.style.display = 'none';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* =============================================
|
|
||||||
ADVANCED VOLUME CONTROL SYSTEM
|
|
||||||
============================================= */
|
|
||||||
|
|
||||||
// Volume State (0-100 for each sound)
|
// Volume State (0-100 for each sound)
|
||||||
let volumeState = {
|
let volumeState = {
|
||||||
music: parseInt(localStorage.getItem('vol_music')) || 25,
|
music: parseInt(localStorage.getItem('vol_music')) || 25,
|
||||||
pop: parseInt(localStorage.getItem('vol_pop')) || 90,
|
pop: parseInt(localStorage.getItem('vol_pop')) || 90,
|
||||||
merge: parseInt(localStorage.getItem('vol_merge')) || 100
|
merge: parseInt(localStorage.getItem('vol_merge')) || 100
|
||||||
};
|
};
|
||||||
|
|
||||||
// Apply volumes on load
|
|
||||||
function initVolumeControl() {
|
|
||||||
// Set audio volumes
|
|
||||||
audio.bg.volume = volumeState.music / 100;
|
|
||||||
audio.pop.volume = volumeState.pop / 100;
|
|
||||||
audio.merge.volume = volumeState.merge / 100;
|
|
||||||
|
|
||||||
// Update sliders
|
|
||||||
const musicSlider = document.getElementById('vol-music');
|
|
||||||
const popSlider = document.getElementById('vol-pop');
|
|
||||||
const mergeSlider = document.getElementById('vol-merge');
|
|
||||||
|
|
||||||
if (musicSlider) {
|
|
||||||
musicSlider.value = volumeState.music;
|
|
||||||
updateSliderFill(musicSlider, volumeState.music);
|
|
||||||
document.getElementById('vol-music-display').textContent = volumeState.music + '%';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (popSlider) {
|
|
||||||
popSlider.value = volumeState.pop;
|
|
||||||
updateSliderFill(popSlider, volumeState.pop);
|
|
||||||
document.getElementById('vol-pop-display').textContent = volumeState.pop + '%';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mergeSlider) {
|
|
||||||
mergeSlider.value = volumeState.merge;
|
|
||||||
updateSliderFill(mergeSlider, volumeState.merge);
|
|
||||||
document.getElementById('vol-merge-display').textContent = volumeState.merge + '%';
|
|
||||||
}
|
|
||||||
|
|
||||||
updateMainSoundIcon();
|
|
||||||
|
|
||||||
// Event listeners for sliders
|
|
||||||
if (musicSlider) {
|
|
||||||
musicSlider.addEventListener('input', (e) => {
|
|
||||||
const val = parseInt(e.target.value);
|
|
||||||
volumeState.music = val;
|
|
||||||
audio.bg.volume = val / 100;
|
|
||||||
localStorage.setItem('vol_music', val);
|
|
||||||
document.getElementById('vol-music-display').textContent = val + '%';
|
|
||||||
updateSliderFill(e.target, val);
|
|
||||||
updateMainSoundIcon();
|
|
||||||
|
|
||||||
// Auto-play BG music if volume > 0
|
|
||||||
if (val > 0 && audio.bg.paused) {
|
|
||||||
tryPlayBg();
|
|
||||||
} else if (val === 0) {
|
|
||||||
audio.bg.pause();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (popSlider) {
|
|
||||||
popSlider.addEventListener('input', (e) => {
|
|
||||||
const val = parseInt(e.target.value);
|
|
||||||
volumeState.pop = val;
|
|
||||||
audio.pop.volume = val / 100;
|
|
||||||
localStorage.setItem('vol_pop', val);
|
|
||||||
document.getElementById('vol-pop-display').textContent = val + '%';
|
|
||||||
updateSliderFill(e.target, val);
|
|
||||||
updateMainSoundIcon();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mergeSlider) {
|
|
||||||
mergeSlider.addEventListener('input', (e) => {
|
|
||||||
const val = parseInt(e.target.value);
|
|
||||||
volumeState.merge = val;
|
|
||||||
audio.merge.volume = val / 100;
|
|
||||||
localStorage.setItem('vol_merge', val);
|
|
||||||
document.getElementById('vol-merge-display').textContent = val + '%';
|
|
||||||
updateSliderFill(e.target, val);
|
|
||||||
updateMainSoundIcon();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Toggle panel visibility
|
|
||||||
const btnSoundMain = document.getElementById('btn-sound-main');
|
|
||||||
const volumePanel = document.getElementById('volume-panel');
|
|
||||||
|
|
||||||
if (btnSoundMain && volumePanel) {
|
|
||||||
btnSoundMain.addEventListener('click', (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
const isVisible = volumePanel.style.display === 'block';
|
|
||||||
volumePanel.style.display = isVisible ? 'none' : 'block';
|
|
||||||
});
|
|
||||||
|
|
||||||
// Close panel when clicking outside
|
|
||||||
document.addEventListener('click', (e) => {
|
|
||||||
if (!volumePanel.contains(e.target) && !btnSoundMain.contains(e.target)) {
|
|
||||||
volumePanel.style.display = 'none';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Prevent panel click from closing
|
|
||||||
volumePanel.addEventListener('click', (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update slider fill effect
|
|
||||||
function updateSliderFill(slider, value) {
|
|
||||||
slider.style.setProperty('--value', value + '%');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update main sound icon based on volumes
|
|
||||||
function updateMainSoundIcon() {
|
|
||||||
const btnMain = document.getElementById('btn-sound-main');
|
|
||||||
if (!btnMain) return;
|
|
||||||
|
|
||||||
const iconFull = btnMain.querySelector('.sound-full');
|
|
||||||
const iconMedium = btnMain.querySelector('.sound-medium');
|
|
||||||
const iconLow = btnMain.querySelector('.sound-low');
|
|
||||||
const iconMuted = btnMain.querySelector('.sound-muted');
|
|
||||||
|
|
||||||
// Calculate total volume average
|
|
||||||
const totalVolume = volumeState.music + volumeState.pop + volumeState.merge;
|
|
||||||
const avgVolume = totalVolume / 3;
|
|
||||||
|
|
||||||
// Hide all icons first
|
|
||||||
iconFull.style.display = 'none';
|
|
||||||
iconMedium.style.display = 'none';
|
|
||||||
iconLow.style.display = 'none';
|
|
||||||
iconMuted.style.display = 'none';
|
|
||||||
|
|
||||||
// Show appropriate icon based on average
|
|
||||||
if (totalVolume === 0) {
|
|
||||||
iconMuted.style.display = 'block';
|
|
||||||
btnMain.classList.add('all-muted');
|
|
||||||
} else {
|
|
||||||
btnMain.classList.remove('all-muted');
|
|
||||||
|
|
||||||
if (avgVolume >= 60) {
|
|
||||||
iconFull.style.display = 'block';
|
|
||||||
} else if (avgVolume >= 30) {
|
|
||||||
iconMedium.style.display = 'block';
|
|
||||||
} else {
|
|
||||||
iconLow.style.display = 'block';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize on DOM load (add this to your existing DOMContentLoaded)
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
|
||||||
// ... existing code ...
|
|
||||||
initVolumeControl(); // ADD THIS LINE
|
|
||||||
});
|
|
||||||
|
|
||||||
/* =============================================
|
|
||||||
COMBO EFFECTS
|
|
||||||
============================================= */
|
|
||||||
function triggerComboEffect(mergedCells, comboCount) {
|
|
||||||
if (mergedCells.length === 0) return;
|
|
||||||
|
|
||||||
mergedCells.forEach(cell => {
|
|
||||||
const tile = document.getElementById(`${cell.r}-${cell.c}`);
|
|
||||||
if (!tile) return;
|
|
||||||
|
|
||||||
tile.classList.add('merge');
|
|
||||||
setTimeout(() => tile.classList.remove('merge'), 300);
|
|
||||||
|
|
||||||
createParticleBurst(tile);
|
|
||||||
|
|
||||||
tile.style.boxShadow = '0 0 40px currentColor';
|
|
||||||
setTimeout(() => {
|
|
||||||
tile.style.boxShadow = '';
|
|
||||||
}, 300);
|
|
||||||
|
|
||||||
const rect = tile.getBoundingClientRect();
|
|
||||||
const centerX = rect.left + rect.width / 2;
|
|
||||||
const centerY = rect.top + rect.height / 2;
|
|
||||||
const tileValue = parseInt(tile.textContent);
|
|
||||||
createScorePopup(centerX, centerY, tileValue);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (comboCount >= 2) {
|
|
||||||
showComboPopup(comboCount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showComboPopup(comboCount) {
|
|
||||||
const board = document.getElementById('board');
|
|
||||||
if (!board) return;
|
|
||||||
|
|
||||||
const rect = board.getBoundingClientRect();
|
|
||||||
const centerX = rect.left + rect.width / 2;
|
|
||||||
const centerY = rect.top + rect.height / 2;
|
|
||||||
|
|
||||||
const popup = document.createElement('div');
|
|
||||||
popup.className = 'combo-popup';
|
|
||||||
popup.style.left = centerX + 'px';
|
|
||||||
popup.style.top = centerY + 'px';
|
|
||||||
popup.style.position = 'fixed';
|
|
||||||
popup.style.fontWeight = '900';
|
|
||||||
popup.style.pointerEvents = 'none';
|
|
||||||
popup.style.zIndex = '9999';
|
|
||||||
popup.style.transform = 'translate(-50%, -50%)';
|
|
||||||
popup.style.textTransform = 'uppercase';
|
|
||||||
popup.style.letterSpacing = '3px';
|
|
||||||
|
|
||||||
if (comboCount === 2) {
|
|
||||||
popup.textContent = 'COMBO x2!';
|
|
||||||
popup.style.fontSize = '36px';
|
|
||||||
popup.style.color = '#00ff99';
|
|
||||||
popup.style.textShadow = '0 0 30px rgba(0, 255, 153, 1), 0 0 50px rgba(0, 255, 153, 0.5)';
|
|
||||||
} else if (comboCount === 3) {
|
|
||||||
popup.textContent = 'AMAZING x3!';
|
|
||||||
popup.style.fontSize = '42px';
|
|
||||||
popup.style.color = '#ff00ff';
|
|
||||||
popup.style.textShadow = '0 0 35px rgba(255, 0, 255, 1), 0 0 60px rgba(255, 0, 255, 0.6)';
|
|
||||||
} else if (comboCount >= 4) {
|
|
||||||
popup.textContent = 'PERFECT x' + comboCount + '!';
|
|
||||||
popup.style.fontSize = '48px';
|
|
||||||
popup.style.color = '#ffd700';
|
|
||||||
popup.style.textShadow = '0 0 40px rgba(255, 215, 0, 1), 0 0 70px rgba(255, 215, 0, 0.7)';
|
|
||||||
}
|
|
||||||
|
|
||||||
document.body.appendChild(popup);
|
|
||||||
|
|
||||||
popup.animate([
|
|
||||||
{
|
|
||||||
transform: 'translate(-50%, -50%) scale(0.3) rotate(-10deg)',
|
|
||||||
opacity: 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
transform: 'translate(-50%, -50%) scale(1.3) rotate(5deg)',
|
|
||||||
opacity: 1,
|
|
||||||
offset: 0.3
|
|
||||||
},
|
|
||||||
{
|
|
||||||
transform: 'translate(-50%, -50%) scale(1.1) rotate(-2deg)',
|
|
||||||
opacity: 1,
|
|
||||||
offset: 0.6
|
|
||||||
},
|
|
||||||
{
|
|
||||||
transform: 'translate(-50%, -50%) scale(0.8) rotate(0deg)',
|
|
||||||
opacity: 0
|
|
||||||
}
|
|
||||||
], {
|
|
||||||
duration: 1200,
|
|
||||||
easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)'
|
|
||||||
}).onfinish = () => popup.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
function createParticleBurst(tileElement) {
|
|
||||||
const rect = tileElement.getBoundingClientRect();
|
|
||||||
const centerX = rect.left + rect.width / 2;
|
|
||||||
const centerY = rect.top + rect.height / 2;
|
|
||||||
|
|
||||||
const tileValue = parseInt(tileElement.textContent);
|
|
||||||
const tileColor = getTileColor(tileValue);
|
|
||||||
|
|
||||||
const particleCount = 8 + Math.floor(Math.random() * 5);
|
|
||||||
|
|
||||||
for (let i = 0; i < particleCount; i++) {
|
|
||||||
const particle = document.createElement('div');
|
|
||||||
particle.className = 'merge-particle';
|
|
||||||
particle.style.left = centerX + 'px';
|
|
||||||
particle.style.top = centerY + 'px';
|
|
||||||
particle.style.background = tileColor;
|
|
||||||
|
|
||||||
document.body.appendChild(particle);
|
|
||||||
|
|
||||||
const angle = (Math.PI * 2 * i) / particleCount + (Math.random() - 0.5) * 0.5;
|
|
||||||
const velocity = 60 + Math.random() * 40;
|
|
||||||
const tx = Math.cos(angle) * velocity;
|
|
||||||
const ty = Math.sin(angle) * velocity;
|
|
||||||
|
|
||||||
particle.animate([
|
|
||||||
{
|
|
||||||
transform: 'translate(0, 0) scale(1)',
|
|
||||||
opacity: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
transform: `translate(${tx}px, ${ty}px) scale(0)`,
|
|
||||||
opacity: 0
|
|
||||||
}
|
|
||||||
], {
|
|
||||||
duration: 500 + Math.random() * 200,
|
|
||||||
easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)'
|
|
||||||
}).onfinish = () => particle.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function createScorePopup(x, y, score) {
|
|
||||||
const popup = document.createElement('div');
|
|
||||||
popup.className = 'score-popup';
|
|
||||||
popup.textContent = '+' + score;
|
|
||||||
popup.style.left = x + 'px';
|
|
||||||
popup.style.top = y + 'px';
|
|
||||||
popup.style.position = 'fixed';
|
|
||||||
popup.style.fontSize = '24px';
|
|
||||||
popup.style.fontWeight = '900';
|
|
||||||
popup.style.color = '#ffd700';
|
|
||||||
popup.style.textShadow = '0 0 20px rgba(255, 215, 0, 0.8)';
|
|
||||||
popup.style.pointerEvents = 'none';
|
|
||||||
popup.style.zIndex = '9999';
|
|
||||||
popup.style.transform = 'translate(-50%, -50%)';
|
|
||||||
|
|
||||||
document.body.appendChild(popup);
|
|
||||||
|
|
||||||
popup.animate([
|
|
||||||
{
|
|
||||||
transform: 'translate(-50%, -50%) scale(0.5)',
|
|
||||||
opacity: 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
transform: 'translate(-50%, -70px) scale(1.2)',
|
|
||||||
opacity: 1,
|
|
||||||
offset: 0.3
|
|
||||||
},
|
|
||||||
{
|
|
||||||
transform: 'translate(-50%, -120px) scale(1)',
|
|
||||||
opacity: 0
|
|
||||||
}
|
|
||||||
], {
|
|
||||||
duration: 1000,
|
|
||||||
easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)'
|
|
||||||
}).onfinish = () => popup.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTileColor(value) {
|
|
||||||
const colors = {
|
|
||||||
2: '#00eaff',
|
|
||||||
4: '#00ff99',
|
|
||||||
8: '#ff00ff',
|
|
||||||
16: '#ff0066',
|
|
||||||
32: '#ffaa00',
|
|
||||||
64: '#ff0000',
|
|
||||||
128: '#5f00ff',
|
|
||||||
256: '#00ffea',
|
|
||||||
512: '#ff00aa',
|
|
||||||
1024: '#00ffaa',
|
|
||||||
2048: '#ffd700'
|
|
||||||
};
|
|
||||||
return colors[value] || '#00eaff';
|
|
||||||
}
|
|
||||||
71
2048_Controls.js
Normal file
71
2048_Controls.js
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
/* ------------------------
|
||||||
|
6. INPUT HANDLER
|
||||||
|
------------------------ */
|
||||||
|
function handleKey(e) {
|
||||||
|
if (isMoving) return;
|
||||||
|
|
||||||
|
let moved = false;
|
||||||
|
const k = e.key;
|
||||||
|
|
||||||
|
if (k === "ArrowLeft" || k === "a" || k === "A") { e.preventDefault(); 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) {
|
||||||
|
isMoving = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
const added = addNewTile();
|
||||||
|
if (!added || !canMove()) {
|
||||||
|
setTimeout(() => showGameOver(), 300);
|
||||||
|
}
|
||||||
|
isMoving = false;
|
||||||
|
}, 100);
|
||||||
|
} else {
|
||||||
|
// Shake effect on invalid move
|
||||||
|
const b = document.getElementById("board");
|
||||||
|
if (b) {
|
||||||
|
b.classList.add("shake");
|
||||||
|
setTimeout(()=>b.classList.remove("shake"), 400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Touch Swipe */
|
||||||
|
let touchStartX = 0;
|
||||||
|
let touchStartY = 0;
|
||||||
|
|
||||||
|
document.addEventListener("touchstart", function (e) {
|
||||||
|
const t = e.touches[0];
|
||||||
|
touchStartX = t.clientX;
|
||||||
|
touchStartY = t.clientY;
|
||||||
|
}, { passive: true });
|
||||||
|
|
||||||
|
document.addEventListener("touchend", function (e) {
|
||||||
|
if (isMoving) return;
|
||||||
|
|
||||||
|
const t = e.changedTouches[0];
|
||||||
|
const dx = t.clientX - touchStartX;
|
||||||
|
const dy = t.clientY - touchStartY;
|
||||||
|
|
||||||
|
let moved = false;
|
||||||
|
|
||||||
|
if (Math.abs(dx) > Math.abs(dy) && Math.abs(dx) > 30) {
|
||||||
|
if (dx > 0) moved = moveRight();
|
||||||
|
else moved = moveLeft();
|
||||||
|
} else if (Math.abs(dy) > 30) {
|
||||||
|
if (dy > 0) moved = moveDown();
|
||||||
|
else moved = moveUp();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (moved) {
|
||||||
|
isMoving = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
const added = addNewTile();
|
||||||
|
if (!added || !canMove()) {
|
||||||
|
setTimeout(() => showGameOver(), 300);
|
||||||
|
}
|
||||||
|
isMoving = false;
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}, { passive: true });
|
||||||
195
2048_Logic.js
Normal file
195
2048_Logic.js
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
/* ------------------------
|
||||||
|
5. GAME LOGIC
|
||||||
|
------------------------ */
|
||||||
|
function addNewTile() {
|
||||||
|
const empty = [];
|
||||||
|
for (let r = 0; r < 4; r++) {
|
||||||
|
for (let c = 0; c < 4; c++) {
|
||||||
|
if (board[r][c] === 0) empty.push({ r, c });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty.length === 0) return false;
|
||||||
|
|
||||||
|
const spot = empty[Math.floor(Math.random() * empty.length)];
|
||||||
|
board[spot.r][spot.c] = 2;
|
||||||
|
|
||||||
|
const tile = document.getElementById(`${spot.r}-${spot.c}`);
|
||||||
|
if (tile) {
|
||||||
|
tile.classList.add("new");
|
||||||
|
playSound(audio.pop);
|
||||||
|
setTimeout(() => tile.classList.remove("new"), 300);
|
||||||
|
}
|
||||||
|
updateTile(spot.r, spot.c, 2);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterZero(row) {
|
||||||
|
return row.filter(n => n !== 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function slide(row) {
|
||||||
|
row = filterZero(row);
|
||||||
|
let mergedThisMove = false;
|
||||||
|
let mergedPositions = [];
|
||||||
|
let mergeCount = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < row.length - 1; i++) {
|
||||||
|
if (row[i] === row[i + 1]) {
|
||||||
|
row[i] = row[i] * 2;
|
||||||
|
|
||||||
|
playSound(audio.merge);
|
||||||
|
|
||||||
|
if (navigator.vibrate) {
|
||||||
|
navigator.vibrate([80, 20, 80]);
|
||||||
|
}
|
||||||
|
|
||||||
|
currentScore += row[i];
|
||||||
|
row[i + 1] = 0;
|
||||||
|
mergedThisMove = true;
|
||||||
|
mergedPositions.push(i);
|
||||||
|
mergeCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
row = filterZero(row);
|
||||||
|
while (row.length < 4) row.push(0);
|
||||||
|
return { row, merged: mergedThisMove, mergedPositions, mergeCount };
|
||||||
|
}
|
||||||
|
|
||||||
|
function arraysEqual(a, b) {
|
||||||
|
return a.length === b.length && a.every((v, i) => v === b[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Move functions */
|
||||||
|
function moveLeft() {
|
||||||
|
let moved = false;
|
||||||
|
let mergedCells = [];
|
||||||
|
mergesInCurrentMove = 0;
|
||||||
|
|
||||||
|
for (let r = 0; r < 4; r++) {
|
||||||
|
const { row: newRow, mergedPositions, mergeCount } = slide(board[r]);
|
||||||
|
if (!arraysEqual(newRow, board[r])) moved = true;
|
||||||
|
board[r] = newRow;
|
||||||
|
|
||||||
|
mergesInCurrentMove += mergeCount;
|
||||||
|
|
||||||
|
if (mergedPositions && mergedPositions.length > 0) {
|
||||||
|
mergedPositions.forEach(c => {
|
||||||
|
mergedCells.push({ r, c });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (moved) {
|
||||||
|
refreshBoard();
|
||||||
|
triggerComboEffect(mergedCells, mergesInCurrentMove);
|
||||||
|
}
|
||||||
|
return moved;
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveRight() {
|
||||||
|
let moved = false;
|
||||||
|
let mergedCells = [];
|
||||||
|
mergesInCurrentMove = 0;
|
||||||
|
|
||||||
|
for (let r = 0; r < 4; r++) {
|
||||||
|
let reversed = [...board[r]].reverse();
|
||||||
|
const { row: slid, mergedPositions, mergeCount } = slide(reversed);
|
||||||
|
let newRow = slid.reverse();
|
||||||
|
if (!arraysEqual(newRow, board[r])) moved = true;
|
||||||
|
board[r] = newRow;
|
||||||
|
|
||||||
|
mergesInCurrentMove += mergeCount;
|
||||||
|
|
||||||
|
if (mergedPositions && mergedPositions.length > 0) {
|
||||||
|
mergedPositions.forEach(pos => {
|
||||||
|
const c = 3 - pos;
|
||||||
|
mergedCells.push({ r, c });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (moved) {
|
||||||
|
refreshBoard();
|
||||||
|
triggerComboEffect(mergedCells, mergesInCurrentMove);
|
||||||
|
}
|
||||||
|
return moved;
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveUp() {
|
||||||
|
let moved = false;
|
||||||
|
let mergedCells = [];
|
||||||
|
mergesInCurrentMove = 0;
|
||||||
|
|
||||||
|
for (let c = 0; c < 4; c++) {
|
||||||
|
const col = [board[0][c], board[1][c], board[2][c], board[3][c]];
|
||||||
|
const { row: newCol, mergedPositions, mergeCount } = slide(col);
|
||||||
|
for (let r = 0; r < 4; r++) {
|
||||||
|
if (board[r][c] !== newCol[r]) moved = true;
|
||||||
|
board[r][c] = newCol[r];
|
||||||
|
}
|
||||||
|
|
||||||
|
mergesInCurrentMove += mergeCount;
|
||||||
|
|
||||||
|
if (mergedPositions && mergedPositions.length > 0) {
|
||||||
|
mergedPositions.forEach(r => {
|
||||||
|
mergedCells.push({ r, c });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (moved) {
|
||||||
|
refreshBoard();
|
||||||
|
triggerComboEffect(mergedCells, mergesInCurrentMove);
|
||||||
|
}
|
||||||
|
return moved;
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveDown() {
|
||||||
|
let moved = false;
|
||||||
|
let mergedCells = [];
|
||||||
|
mergesInCurrentMove = 0;
|
||||||
|
|
||||||
|
for (let c = 0; c < 4; c++) {
|
||||||
|
const col = [board[3][c], board[2][c], board[1][c], board[0][c]];
|
||||||
|
const { row: slid, mergedPositions, mergeCount } = slide(col);
|
||||||
|
const newCol = slid.reverse();
|
||||||
|
for (let r = 0; r < 4; r++) {
|
||||||
|
if (board[r][c] !== newCol[r]) moved = true;
|
||||||
|
board[r][c] = newCol[r];
|
||||||
|
}
|
||||||
|
|
||||||
|
mergesInCurrentMove += mergeCount;
|
||||||
|
|
||||||
|
if (mergedPositions && mergedPositions.length > 0) {
|
||||||
|
mergedPositions.forEach(pos => {
|
||||||
|
const r = 3 - pos;
|
||||||
|
mergedCells.push({ r, c });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (moved) {
|
||||||
|
refreshBoard();
|
||||||
|
triggerComboEffect(mergedCells, mergesInCurrentMove);
|
||||||
|
}
|
||||||
|
return moved;
|
||||||
|
}
|
||||||
|
|
||||||
|
function canMove() {
|
||||||
|
for (let r = 0; r < 4; r++) {
|
||||||
|
for (let c = 0; c < 4; c++) {
|
||||||
|
if (board[r][c] === 0) return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let r = 0; r < 4; r++) {
|
||||||
|
for (let c = 0; c < 4; c++) {
|
||||||
|
const current = board[r][c];
|
||||||
|
if (c < 3 && board[r][c + 1] === current) return true;
|
||||||
|
if (r < 3 && board[r + 1][c] === current) return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
168
Audio_2048.js
Normal file
168
Audio_2048.js
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
/* ------------------------
|
||||||
|
2. AUDIO MANAGER
|
||||||
|
------------------------ */
|
||||||
|
const audio = {
|
||||||
|
bg: new Audio("Background_Music.mp3"),
|
||||||
|
pop: new Audio("Pop.mp3"),
|
||||||
|
merge: new Audio("Merge.mp3")
|
||||||
|
};
|
||||||
|
|
||||||
|
audio.bg.loop = true;
|
||||||
|
|
||||||
|
// Update audio volumes based on state & sliders
|
||||||
|
function updateAudioVolumes() {
|
||||||
|
audio.bg.volume = soundState.bg ? (volumeState.music / 100) : 0;
|
||||||
|
audio.pop.volume = soundState.pop ? (volumeState.pop / 100) : 0;
|
||||||
|
audio.merge.volume = soundState.merge ? (volumeState.merge / 100) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function tryPlayBg() {
|
||||||
|
if (!soundState.bg || volumeState.music === 0) return;
|
||||||
|
|
||||||
|
audio.bg.play().catch(() => {
|
||||||
|
const unlock = () => {
|
||||||
|
if (soundState.bg && volumeState.music > 0) audio.bg.play().catch(()=>{});
|
||||||
|
window.removeEventListener("keydown", unlock);
|
||||||
|
window.removeEventListener("click", unlock);
|
||||||
|
};
|
||||||
|
window.addEventListener("keydown", unlock, { once: true });
|
||||||
|
window.addEventListener("click", unlock, { once: true });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Safe playSound with mute check */
|
||||||
|
function playSound(soundObj) {
|
||||||
|
try {
|
||||||
|
if (soundObj === audio.pop && (!soundState.pop || volumeState.pop === 0)) return;
|
||||||
|
if (soundObj === audio.merge && (!soundState.merge || volumeState.merge === 0)) return;
|
||||||
|
if (soundObj === audio.bg && (!soundState.bg || volumeState.music === 0)) return;
|
||||||
|
|
||||||
|
soundObj.currentTime = 0;
|
||||||
|
soundObj.play().catch(() => {});
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize Volume Sliders
|
||||||
|
function initVolumeControl() {
|
||||||
|
updateAudioVolumes();
|
||||||
|
|
||||||
|
const musicSlider = document.getElementById('vol-music');
|
||||||
|
const popSlider = document.getElementById('vol-pop');
|
||||||
|
const mergeSlider = document.getElementById('vol-merge');
|
||||||
|
|
||||||
|
if (musicSlider) {
|
||||||
|
musicSlider.value = volumeState.music;
|
||||||
|
updateSliderFill(musicSlider, volumeState.music);
|
||||||
|
document.getElementById('vol-music-display').textContent = volumeState.music + '%';
|
||||||
|
|
||||||
|
musicSlider.addEventListener('input', (e) => {
|
||||||
|
const val = parseInt(e.target.value);
|
||||||
|
volumeState.music = val;
|
||||||
|
audio.bg.volume = val / 100; // Direct update
|
||||||
|
localStorage.setItem('vol_music', val);
|
||||||
|
document.getElementById('vol-music-display').textContent = val + '%';
|
||||||
|
updateSliderFill(e.target, val);
|
||||||
|
updateMainSoundIcon();
|
||||||
|
|
||||||
|
if (val > 0 && audio.bg.paused && soundState.bg) {
|
||||||
|
tryPlayBg();
|
||||||
|
} else if (val === 0) {
|
||||||
|
audio.bg.pause();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (popSlider) {
|
||||||
|
popSlider.value = volumeState.pop;
|
||||||
|
updateSliderFill(popSlider, volumeState.pop);
|
||||||
|
document.getElementById('vol-pop-display').textContent = volumeState.pop + '%';
|
||||||
|
|
||||||
|
popSlider.addEventListener('input', (e) => {
|
||||||
|
const val = parseInt(e.target.value);
|
||||||
|
volumeState.pop = val;
|
||||||
|
audio.pop.volume = val / 100;
|
||||||
|
localStorage.setItem('vol_pop', val);
|
||||||
|
document.getElementById('vol-pop-display').textContent = val + '%';
|
||||||
|
updateSliderFill(e.target, val);
|
||||||
|
updateMainSoundIcon();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mergeSlider) {
|
||||||
|
mergeSlider.value = volumeState.merge;
|
||||||
|
updateSliderFill(mergeSlider, volumeState.merge);
|
||||||
|
document.getElementById('vol-merge-display').textContent = volumeState.merge + '%';
|
||||||
|
|
||||||
|
mergeSlider.addEventListener('input', (e) => {
|
||||||
|
const val = parseInt(e.target.value);
|
||||||
|
volumeState.merge = val;
|
||||||
|
audio.merge.volume = val / 100;
|
||||||
|
localStorage.setItem('vol_merge', val);
|
||||||
|
document.getElementById('vol-merge-display').textContent = val + '%';
|
||||||
|
updateSliderFill(e.target, val);
|
||||||
|
updateMainSoundIcon();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMainSoundIcon();
|
||||||
|
setupVolumePanelEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupVolumePanelEvents() {
|
||||||
|
const btnSoundMain = document.getElementById('btn-sound-main');
|
||||||
|
const volumePanel = document.getElementById('volume-panel');
|
||||||
|
|
||||||
|
if (btnSoundMain && volumePanel) {
|
||||||
|
btnSoundMain.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const isVisible = volumePanel.style.display === 'block';
|
||||||
|
volumePanel.style.display = isVisible ? 'none' : 'block';
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
if (!volumePanel.contains(e.target) && !btnSoundMain.contains(e.target)) {
|
||||||
|
volumePanel.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
volumePanel.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSliderFill(slider, value) {
|
||||||
|
slider.style.setProperty('--value', value + '%');
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateMainSoundIcon() {
|
||||||
|
const btnMain = document.getElementById('btn-sound-main');
|
||||||
|
if (!btnMain) return;
|
||||||
|
|
||||||
|
const iconFull = btnMain.querySelector('.sound-full');
|
||||||
|
const iconMedium = btnMain.querySelector('.sound-medium');
|
||||||
|
const iconLow = btnMain.querySelector('.sound-low');
|
||||||
|
const iconMuted = btnMain.querySelector('.sound-muted');
|
||||||
|
|
||||||
|
const totalVolume = volumeState.music + volumeState.pop + volumeState.merge;
|
||||||
|
const avgVolume = totalVolume / 3;
|
||||||
|
|
||||||
|
if (iconFull) iconFull.style.display = 'none';
|
||||||
|
if (iconMedium) iconMedium.style.display = 'none';
|
||||||
|
if (iconLow) iconLow.style.display = 'none';
|
||||||
|
if (iconMuted) iconMuted.style.display = 'none';
|
||||||
|
|
||||||
|
if (totalVolume === 0) {
|
||||||
|
if (iconMuted) iconMuted.style.display = 'block';
|
||||||
|
btnMain.classList.add('all-muted');
|
||||||
|
} else {
|
||||||
|
btnMain.classList.remove('all-muted');
|
||||||
|
if (avgVolume >= 60) {
|
||||||
|
if (iconFull) iconFull.style.display = 'block';
|
||||||
|
} else if (avgVolume >= 30) {
|
||||||
|
if (iconMedium) iconMedium.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
if (iconLow) iconLow.style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
94
Main_2048.js
Normal file
94
Main_2048.js
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
/* ------------------------
|
||||||
|
7. MAIN INITIALIZATION
|
||||||
|
------------------------ */
|
||||||
|
function restartGame() {
|
||||||
|
hideGameOver();
|
||||||
|
resetScore();
|
||||||
|
setupBoard();
|
||||||
|
addNewTile();
|
||||||
|
addNewTile();
|
||||||
|
refreshBoard();
|
||||||
|
isMoving = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function playAgain() {
|
||||||
|
restartGame();
|
||||||
|
}
|
||||||
|
|
||||||
|
function goHome() {
|
||||||
|
try {
|
||||||
|
audio.bg.pause();
|
||||||
|
audio.bg.currentTime = 0;
|
||||||
|
} catch (e) {}
|
||||||
|
window.location.href = "Homepage.html";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Event Listeners Setup */
|
||||||
|
function setupEventListeners() {
|
||||||
|
// Tutorial Modal
|
||||||
|
const btnTutorial = document.getElementById('btn-tutorial');
|
||||||
|
const tutorialOverlay = document.getElementById('tutorial-overlay');
|
||||||
|
const closeTutorial = document.getElementById('close-tutorial');
|
||||||
|
|
||||||
|
if (btnTutorial) btnTutorial.addEventListener('click', () => tutorialOverlay.style.display = 'flex');
|
||||||
|
if (closeTutorial) closeTutorial.addEventListener('click', () => tutorialOverlay.style.display = 'none');
|
||||||
|
if (tutorialOverlay) tutorialOverlay.addEventListener('click', (e) => {
|
||||||
|
if (e.target === tutorialOverlay) tutorialOverlay.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Restart & Game Over buttons
|
||||||
|
const btnRestart = document.getElementById('btn-restart');
|
||||||
|
if (btnRestart) btnRestart.addEventListener('click', restartGame);
|
||||||
|
|
||||||
|
const btnPlayAgain = document.getElementById('btn-play-again');
|
||||||
|
if (btnPlayAgain) btnPlayAgain.addEventListener('click', playAgain);
|
||||||
|
|
||||||
|
const btnHome = document.getElementById('btn-home');
|
||||||
|
if (btnHome) btnHome.addEventListener('click', goHome);
|
||||||
|
|
||||||
|
const gameOverClose = document.getElementById('game-over-close');
|
||||||
|
if (gameOverClose) gameOverClose.addEventListener('click', hideGameOver);
|
||||||
|
|
||||||
|
const gameOverOverlay = document.getElementById('game-over-overlay');
|
||||||
|
if (gameOverOverlay) gameOverOverlay.addEventListener('click', function(e) {
|
||||||
|
if (e.target === this) hideGameOver();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sound Buttons (Mute Toggles)
|
||||||
|
const btnSoundBg = document.getElementById('btn-sound-bg');
|
||||||
|
const btnSoundPop = document.getElementById('btn-sound-pop');
|
||||||
|
const btnSoundMerge = document.getElementById('btn-sound-merge');
|
||||||
|
|
||||||
|
if (btnSoundBg) btnSoundBg.addEventListener('click', () => {
|
||||||
|
soundState.bg = !soundState.bg;
|
||||||
|
localStorage.setItem('sound_bg', soundState.bg);
|
||||||
|
updateAudioVolumes();
|
||||||
|
if(soundState.bg) tryPlayBg(); else audio.bg.pause();
|
||||||
|
// Tambahkan logika update tombol UI jika ada (toggle class)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (btnSoundPop) btnSoundPop.addEventListener('click', () => {
|
||||||
|
soundState.pop = !soundState.pop;
|
||||||
|
localStorage.setItem('sound_pop', soundState.pop);
|
||||||
|
updateAudioVolumes();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (btnSoundMerge) btnSoundMerge.addEventListener('click', () => {
|
||||||
|
soundState.merge = !soundState.merge;
|
||||||
|
localStorage.setItem('sound_merge', soundState.merge);
|
||||||
|
updateAudioVolumes();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* DOM Ready */
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
updateHighScoreDisplay();
|
||||||
|
setupBoard();
|
||||||
|
addNewTile();
|
||||||
|
addNewTile();
|
||||||
|
initVolumeControl(); // Starts audio logic
|
||||||
|
tryPlayBg();
|
||||||
|
document.addEventListener("keydown", handleKey);
|
||||||
|
setupEventListeners();
|
||||||
|
checkAndShowTutorial();
|
||||||
|
});
|
||||||
126
User_Interface_2048.js
Normal file
126
User_Interface_2048.js
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
/* ------------------------
|
||||||
|
4. UI MANAGER
|
||||||
|
------------------------ */
|
||||||
|
function setupBoard() {
|
||||||
|
board = [];
|
||||||
|
currentScore = 0;
|
||||||
|
updateScoreDisplay();
|
||||||
|
|
||||||
|
const container = document.getElementById("board");
|
||||||
|
if (!container) {
|
||||||
|
console.error("Board element not found (#board).");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.innerHTML = "";
|
||||||
|
for (let r = 0; r < 4; r++) {
|
||||||
|
board[r] = [];
|
||||||
|
for (let c = 0; c < 4; c++) {
|
||||||
|
board[r][c] = 0;
|
||||||
|
const tile = document.createElement("div");
|
||||||
|
tile.id = `${r}-${c}`;
|
||||||
|
tile.className = "tile";
|
||||||
|
container.appendChild(tile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTile(row, col, num) {
|
||||||
|
const tile = document.getElementById(`${row}-${col}`);
|
||||||
|
if (!tile) return;
|
||||||
|
|
||||||
|
tile.className = "tile";
|
||||||
|
|
||||||
|
if (num > 0) {
|
||||||
|
tile.textContent = num;
|
||||||
|
tile.classList.add("tile-" + num);
|
||||||
|
} else {
|
||||||
|
tile.textContent = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshBoard() {
|
||||||
|
for (let r = 0; r < 4; r++) {
|
||||||
|
for (let c = 0; c < 4; c++) {
|
||||||
|
updateTile(r, c, board[r][c]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateScoreDisplay();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateScoreDisplay() {
|
||||||
|
const scoreEl = document.getElementById("score");
|
||||||
|
if (scoreEl) {
|
||||||
|
scoreEl.textContent = currentScore;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentScore > highScore) {
|
||||||
|
highScore = currentScore;
|
||||||
|
localStorage.setItem(storageKey, highScore);
|
||||||
|
updateHighScoreDisplay();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateHighScoreDisplay() {
|
||||||
|
const highScoreEl = document.getElementById('high-score');
|
||||||
|
if (highScoreEl) {
|
||||||
|
highScoreEl.textContent = highScore;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetScore() {
|
||||||
|
currentScore = 0;
|
||||||
|
updateScoreDisplay();
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkAndShowTutorial() {
|
||||||
|
const showTutorial = sessionStorage.getItem("showTutorial");
|
||||||
|
const loggedInUser = sessionStorage.getItem("loggedInUser");
|
||||||
|
|
||||||
|
if (showTutorial === "true" && loggedInUser) {
|
||||||
|
setTimeout(() => {
|
||||||
|
const tutorialOverlay = document.getElementById('tutorial-overlay');
|
||||||
|
if (tutorialOverlay) {
|
||||||
|
tutorialOverlay.style.display = 'flex';
|
||||||
|
}
|
||||||
|
sessionStorage.setItem("showTutorial", "false");
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showGameOver() {
|
||||||
|
const finalScore = currentScore;
|
||||||
|
|
||||||
|
if (typeof saveScore === 'function') {
|
||||||
|
console.log("Mengirim skor ke database:", finalScore);
|
||||||
|
saveScore(finalScore);
|
||||||
|
} else {
|
||||||
|
console.error("Fungsi saveScore tidak ditemukan! Pastikan Score_Request.js sudah diload.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const isNewHighScore = finalScore >= highScore && finalScore > 0;
|
||||||
|
|
||||||
|
const finalScoreEl = document.getElementById('final-score');
|
||||||
|
if (finalScoreEl) finalScoreEl.textContent = finalScore;
|
||||||
|
|
||||||
|
const newHighScoreBadge = document.getElementById('new-high-score-badge');
|
||||||
|
const highScoreDisplay = document.getElementById('high-score-display');
|
||||||
|
|
||||||
|
if (isNewHighScore) {
|
||||||
|
if (newHighScoreBadge) newHighScoreBadge.style.display = 'inline-block';
|
||||||
|
if (highScoreDisplay) highScoreDisplay.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
if (newHighScoreBadge) newHighScoreBadge.style.display = 'none';
|
||||||
|
if (highScoreDisplay) highScoreDisplay.style.display = 'block';
|
||||||
|
const modalHighScore = document.getElementById('modal-high-score');
|
||||||
|
if (modalHighScore) modalHighScore.textContent = highScore;
|
||||||
|
}
|
||||||
|
|
||||||
|
const gameOverOverlay = document.getElementById('game-over-overlay');
|
||||||
|
if (gameOverOverlay) gameOverOverlay.style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideGameOver() {
|
||||||
|
const gameOverOverlay = document.getElementById('game-over-overlay');
|
||||||
|
if (gameOverOverlay) gameOverOverlay.style.display = 'none';
|
||||||
|
}
|
||||||
151
Visual_Effects_2048.js
Normal file
151
Visual_Effects_2048.js
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
/* ------------------------
|
||||||
|
3. VISUAL EFFECTS
|
||||||
|
------------------------ */
|
||||||
|
function triggerComboEffect(mergedCells, comboCount) {
|
||||||
|
if (mergedCells.length === 0) return;
|
||||||
|
|
||||||
|
mergedCells.forEach(cell => {
|
||||||
|
const tile = document.getElementById(`${cell.r}-${cell.c}`);
|
||||||
|
if (!tile) return;
|
||||||
|
|
||||||
|
tile.classList.add('merge');
|
||||||
|
setTimeout(() => tile.classList.remove('merge'), 300);
|
||||||
|
|
||||||
|
createParticleBurst(tile);
|
||||||
|
|
||||||
|
tile.style.boxShadow = '0 0 40px currentColor';
|
||||||
|
setTimeout(() => {
|
||||||
|
tile.style.boxShadow = '';
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
const rect = tile.getBoundingClientRect();
|
||||||
|
const centerX = rect.left + rect.width / 2;
|
||||||
|
const centerY = rect.top + rect.height / 2;
|
||||||
|
const tileValue = parseInt(tile.textContent);
|
||||||
|
createScorePopup(centerX, centerY, tileValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (comboCount >= 2) {
|
||||||
|
showComboPopup(comboCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showComboPopup(comboCount) {
|
||||||
|
const board = document.getElementById('board');
|
||||||
|
if (!board) return;
|
||||||
|
|
||||||
|
const rect = board.getBoundingClientRect();
|
||||||
|
const centerX = rect.left + rect.width / 2;
|
||||||
|
const centerY = rect.top + rect.height / 2;
|
||||||
|
|
||||||
|
const popup = document.createElement('div');
|
||||||
|
popup.className = 'combo-popup';
|
||||||
|
popup.style.left = centerX + 'px';
|
||||||
|
popup.style.top = centerY + 'px';
|
||||||
|
popup.style.position = 'fixed';
|
||||||
|
popup.style.fontWeight = '900';
|
||||||
|
popup.style.pointerEvents = 'none';
|
||||||
|
popup.style.zIndex = '9999';
|
||||||
|
popup.style.transform = 'translate(-50%, -50%)';
|
||||||
|
popup.style.textTransform = 'uppercase';
|
||||||
|
popup.style.letterSpacing = '3px';
|
||||||
|
|
||||||
|
if (comboCount === 2) {
|
||||||
|
popup.textContent = 'COMBO x2!';
|
||||||
|
popup.style.fontSize = '36px';
|
||||||
|
popup.style.color = '#00ff99';
|
||||||
|
popup.style.textShadow = '0 0 30px rgba(0, 255, 153, 1), 0 0 50px rgba(0, 255, 153, 0.5)';
|
||||||
|
} else if (comboCount === 3) {
|
||||||
|
popup.textContent = 'AMAZING x3!';
|
||||||
|
popup.style.fontSize = '42px';
|
||||||
|
popup.style.color = '#ff00ff';
|
||||||
|
popup.style.textShadow = '0 0 35px rgba(255, 0, 255, 1), 0 0 60px rgba(255, 0, 255, 0.6)';
|
||||||
|
} else if (comboCount >= 4) {
|
||||||
|
popup.textContent = 'PERFECT x' + comboCount + '!';
|
||||||
|
popup.style.fontSize = '48px';
|
||||||
|
popup.style.color = '#ffd700';
|
||||||
|
popup.style.textShadow = '0 0 40px rgba(255, 215, 0, 1), 0 0 70px rgba(255, 215, 0, 0.7)';
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.appendChild(popup);
|
||||||
|
|
||||||
|
popup.animate([
|
||||||
|
{ transform: 'translate(-50%, -50%) scale(0.3) rotate(-10deg)', opacity: 0 },
|
||||||
|
{ transform: 'translate(-50%, -50%) scale(1.3) rotate(5deg)', opacity: 1, offset: 0.3 },
|
||||||
|
{ transform: 'translate(-50%, -50%) scale(1.1) rotate(-2deg)', opacity: 1, offset: 0.6 },
|
||||||
|
{ transform: 'translate(-50%, -50%) scale(0.8) rotate(0deg)', opacity: 0 }
|
||||||
|
], {
|
||||||
|
duration: 1200,
|
||||||
|
easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)'
|
||||||
|
}).onfinish = () => popup.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
function createParticleBurst(tileElement) {
|
||||||
|
const rect = tileElement.getBoundingClientRect();
|
||||||
|
const centerX = rect.left + rect.width / 2;
|
||||||
|
const centerY = rect.top + rect.height / 2;
|
||||||
|
|
||||||
|
const tileValue = parseInt(tileElement.textContent);
|
||||||
|
const tileColor = getTileColor(tileValue);
|
||||||
|
|
||||||
|
const particleCount = 8 + Math.floor(Math.random() * 5);
|
||||||
|
|
||||||
|
for (let i = 0; i < particleCount; i++) {
|
||||||
|
const particle = document.createElement('div');
|
||||||
|
particle.className = 'merge-particle';
|
||||||
|
particle.style.left = centerX + 'px';
|
||||||
|
particle.style.top = centerY + 'px';
|
||||||
|
particle.style.background = tileColor;
|
||||||
|
|
||||||
|
document.body.appendChild(particle);
|
||||||
|
|
||||||
|
const angle = (Math.PI * 2 * i) / particleCount + (Math.random() - 0.5) * 0.5;
|
||||||
|
const velocity = 60 + Math.random() * 40;
|
||||||
|
const tx = Math.cos(angle) * velocity;
|
||||||
|
const ty = Math.sin(angle) * velocity;
|
||||||
|
|
||||||
|
particle.animate([
|
||||||
|
{ transform: 'translate(0, 0) scale(1)', opacity: 1 },
|
||||||
|
{ transform: `translate(${tx}px, ${ty}px) scale(0)`, opacity: 0 }
|
||||||
|
], {
|
||||||
|
duration: 500 + Math.random() * 200,
|
||||||
|
easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)'
|
||||||
|
}).onfinish = () => particle.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createScorePopup(x, y, score) {
|
||||||
|
const popup = document.createElement('div');
|
||||||
|
popup.className = 'score-popup';
|
||||||
|
popup.textContent = '+' + score;
|
||||||
|
popup.style.left = x + 'px';
|
||||||
|
popup.style.top = y + 'px';
|
||||||
|
popup.style.position = 'fixed';
|
||||||
|
popup.style.fontSize = '24px';
|
||||||
|
popup.style.fontWeight = '900';
|
||||||
|
popup.style.color = '#ffd700';
|
||||||
|
popup.style.textShadow = '0 0 20px rgba(255, 215, 0, 0.8)';
|
||||||
|
popup.style.pointerEvents = 'none';
|
||||||
|
popup.style.zIndex = '9999';
|
||||||
|
popup.style.transform = 'translate(-50%, -50%)';
|
||||||
|
|
||||||
|
document.body.appendChild(popup);
|
||||||
|
|
||||||
|
popup.animate([
|
||||||
|
{ transform: 'translate(-50%, -50%) scale(0.5)', opacity: 0 },
|
||||||
|
{ transform: 'translate(-50%, -70px) scale(1.2)', opacity: 1, offset: 0.3 },
|
||||||
|
{ transform: 'translate(-50%, -120px) scale(1)', opacity: 0 }
|
||||||
|
], {
|
||||||
|
duration: 1000,
|
||||||
|
easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)'
|
||||||
|
}).onfinish = () => popup.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTileColor(value) {
|
||||||
|
const colors = {
|
||||||
|
2: '#00eaff', 4: '#00ff99', 8: '#ff00ff', 16: '#ff0066',
|
||||||
|
32: '#ffaa00', 64: '#ff0000', 128: '#5f00ff', 256: '#00ffea',
|
||||||
|
512: '#ff00aa', 1024: '#00ffaa', 2048: '#ffd700'
|
||||||
|
};
|
||||||
|
return colors[value] || '#00eaff';
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user