2048
This commit is contained in:
parent
cb758824cd
commit
79a0cfc00f
@ -379,5 +379,11 @@
|
||||
</div>
|
||||
<script src="Score_Request.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>
|
||||
</html>
|
||||
|
||||
970
2048.js
970
2048.js
@ -1,31 +1,21 @@
|
||||
/* ------------------------
|
||||
State & Variables
|
||||
1. GAME STATE & VARIABLES
|
||||
------------------------ */
|
||||
let board = [];
|
||||
let currentScore = 0;
|
||||
|
||||
// 1. Ambil username dari sessionStorage (sesuai sistem login kamu)
|
||||
// Ambil username dari sessionStorage
|
||||
const currentUser = sessionStorage.getItem("loggedInUser") || "guest";
|
||||
|
||||
// 2. Buat nama kunci unik, misal: "highScore2048_budi"
|
||||
// Buat nama kunci unik
|
||||
const storageKey = 'highScore2048_' + currentUser;
|
||||
|
||||
// 3. Ambil skor milik user tersebut saja
|
||||
|
||||
// Ambil skor milik user tersebut
|
||||
let highScore = parseInt(localStorage.getItem(storageKey)) || 0;
|
||||
let lastMoveDir = null;
|
||||
let isMoving = false;
|
||||
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)
|
||||
let soundState = {
|
||||
bg: localStorage.getItem('sound_bg') !== 'false',
|
||||
@ -33,961 +23,9 @@ let soundState = {
|
||||
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)
|
||||
let volumeState = {
|
||||
music: parseInt(localStorage.getItem('vol_music')) || 25,
|
||||
pop: parseInt(localStorage.getItem('vol_pop')) || 90,
|
||||
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