This commit is contained in:
Jevinca Marvella 2025-11-30 16:24:36 +07:00
parent c14ba859b8
commit b08d1b5ce7
3 changed files with 295 additions and 30 deletions

169
2048.css
View File

@ -120,6 +120,175 @@ h1 {
z-index: 100; z-index: 100;
} }
.sound-controls {
position: fixed;
top: clamp(10px, 2vh, 20px);
left: clamp(10px, 2vw, 20px);
display: flex;
gap: clamp(8px, 1.5vw, 12px);
z-index: 100;
}
/* Sound Button Styling */
.btn-sound {
position: relative;
width: clamp(36px, 6vw, 48px);
height: clamp(36px, 6vw, 48px);
padding: 0;
background: rgba(30, 0, 50, 0.85);
backdrop-filter: blur(15px);
border: 2px solid rgba(0, 217, 255, 0.45);
border-radius: 12px;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
align-items: center;
justify-content: center;
box-shadow:
0 4px 18px rgba(0, 0, 0, 0.35),
0 0 20px rgba(0, 217, 255, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
}
.btn-sound svg {
width: clamp(18px, 3vw, 24px);
height: clamp(18px, 3vw, 24px);
color: rgba(0, 234, 255, 0.9);
transition: all 0.3s ease;
position: absolute;
}
/* BG Music Button - Purple */
#btn-sound-bg {
background: rgba(50, 0, 70, 0.85);
border-color: rgba(200, 100, 255, 0.45);
}
#btn-sound-bg svg {
color: rgba(200, 100, 255, 0.9);
}
#btn-sound-bg:hover {
background: rgba(200, 100, 255, 0.15);
border-color: rgba(200, 100, 255, 0.8);
box-shadow:
0 6px 25px rgba(0, 0, 0, 0.45),
0 0 35px rgba(200, 100, 255, 0.4),
inset 0 1px 0 rgba(255, 255, 255, 0.15);
}
#btn-sound-bg:hover svg {
color: rgba(200, 100, 255, 1);
}
/* Pop SFX Button - Cyan */
#btn-sound-pop {
background: rgba(0, 40, 50, 0.85);
border-color: rgba(0, 234, 255, 0.45);
}
#btn-sound-pop svg {
color: rgba(0, 234, 255, 0.9);
}
#btn-sound-pop:hover {
background: rgba(0, 234, 255, 0.15);
border-color: rgba(0, 234, 255, 0.8);
box-shadow:
0 6px 25px rgba(0, 0, 0, 0.45),
0 0 35px rgba(0, 234, 255, 0.4),
inset 0 1px 0 rgba(255, 255, 255, 0.15);
}
#btn-sound-pop:hover svg {
color: rgba(0, 234, 255, 1);
}
/* Merge SFX Button - Orange/Yellow */
#btn-sound-merge {
background: rgba(60, 30, 0, 0.85);
border-color: rgba(255, 170, 0, 0.45);
}
#btn-sound-merge svg {
color: rgba(255, 170, 0, 0.9);
}
#btn-sound-merge:hover {
background: rgba(255, 170, 0, 0.15);
border-color: rgba(255, 170, 0, 0.8);
box-shadow:
0 6px 25px rgba(0, 0, 0, 0.45),
0 0 35px rgba(255, 170, 0, 0.4),
inset 0 1px 0 rgba(255, 255, 255, 0.15);
}
#btn-sound-merge:hover svg {
color: rgba(255, 170, 0, 1);
}
/* Hover & Active States */
.btn-sound:hover {
transform: translateY(-2px);
}
.btn-sound:active {
transform: translateY(0);
box-shadow:
0 2px 12px rgba(0, 0, 0, 0.35),
0 0 20px rgba(0, 234, 255, 0.3),
inset 0 2px 6px rgba(0, 0, 0, 0.25);
}
/* Muted State - Red with X */
.btn-sound.muted {
background: rgba(60, 0, 10, 0.85) !important;
border-color: rgba(255, 50, 50, 0.6) !important;
box-shadow:
0 4px 18px rgba(0, 0, 0, 0.35),
0 0 20px rgba(255, 50, 50, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.1) !important;
}
.btn-sound.muted svg.sound-icon {
display: none !important;
}
.btn-sound.muted svg.mute-icon {
display: block !important;
color: rgba(255, 80, 80, 0.9) !important;
}
.btn-sound.muted:hover {
background: rgba(255, 50, 50, 0.2) !important;
border-color: rgba(255, 80, 80, 0.8) !important;
box-shadow:
0 6px 25px rgba(0, 0, 0, 0.45),
0 0 35px rgba(255, 50, 50, 0.5),
inset 0 1px 0 rgba(255, 255, 255, 0.15) !important;
}
.btn-sound.muted:hover svg.mute-icon {
color: rgba(255, 100, 100, 1) !important;
}
/* Icon Transitions */
.btn-sound svg.sound-icon,
.btn-sound svg.mute-icon {
transition: all 0.3s ease;
}
.btn-sound:hover svg {
transform: scale(1.1);
}
/* Mobile Responsive */
@media (max-width: 768px) {
.sound-controls {
flex-direction: column;
gap: clamp(6px, 1.2vw, 10px);
}
}
.icon-btn { .icon-btn {
width: clamp(36px, 6vw, 48px); width: clamp(36px, 6vw, 48px);
height: clamp(36px, 6vw, 48px); height: clamp(36px, 6vw, 48px);

View File

@ -15,6 +15,43 @@
<div class="starfield" aria-hidden="true"></div> <div class="starfield" aria-hidden="true"></div>
<div class="cursor-light" aria-hidden="true"></div> <div class="cursor-light" aria-hidden="true"></div>
<!-- SOUND CONTROLS - KIRI ATAS -->
<div class="sound-controls">
<!-- BG Music Toggle -->
<button class="icon-btn btn-sound" id="btn-sound-bg" data-sound="bg" title="Toggle Background Music">
<svg class="sound-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<path d="M9 18V5l12-2v13M9 18c0 1.657-1.343 3-3 3s-3-1.343-3-3 1.343-3 3-3 3 1.343 3 3zm12-2c0 1.657-1.343 3-3 3s-3-1.343-3-3 1.343-3 3-3 3 1.343 3 3z"/>
</svg>
<svg class="mute-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" style="display: none;">
<line x1="2" y1="2" x2="22" y2="22"/>
<path d="M9 18V5l12-2v13M9 18c0 1.657-1.343 3-3 3s-3-1.343-3-3 1.343-3 3-3 3 1.343 3 3zm12-2c0 1.657-1.343 3-3 3s-3-1.343-3-3 1.343-3 3-3 3 1.343 3 3z"/>
</svg>
</button>
<!-- Pop SFX Toggle -->
<button class="icon-btn btn-sound" id="btn-sound-pop" data-sound="pop" title="Toggle Pop Sound">
<svg class="sound-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"/>
<path d="M8 12h8M12 8v8"/>
</svg>
<svg class="mute-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="display: none;">
<line x1="2" y1="2" x2="22" y2="22"/>
<circle cx="12" cy="12" r="10"/>
</svg>
</button>
<!-- Merge SFX Toggle -->
<button class="icon-btn btn-sound" id="btn-sound-merge" data-sound="merge" title="Toggle Merge Sound">
<svg class="sound-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
</svg>
<svg class="mute-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" style="display: none;">
<line x1="2" y1="2" x2="22" y2="22"/>
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
</svg>
</button>
</div>
<!-- Top Right Controls --> <!-- Top Right Controls -->
<div class="top-controls"> <div class="top-controls">
<button class="icon-btn btn-tutorial" id="btn-tutorial" title="How to Play"> <button class="icon-btn btn-tutorial" id="btn-tutorial" title="How to Play">

119
2048.js
View File

@ -1,3 +1,5 @@
/* 2048.js — Complete Version with WASD + Interactive Effects + Sound Controls */
/* ------------------------ /* ------------------------
State & Variables State & Variables
------------------------ */ ------------------------ */
@ -16,15 +18,29 @@ const audio = {
pop: new Audio("Pop.mp3"), pop: new Audio("Pop.mp3"),
merge: new Audio("Merge.mp3") merge: new Audio("Merge.mp3")
}; };
audio.bg.volume = 0.25;
audio.pop.volume = 0.9; // Sound State (baca dari localStorage atau default ON)
audio.merge.volume = 1.0; let soundState = {
bg: localStorage.getItem('sound_bg') !== 'false',
pop: localStorage.getItem('sound_pop') !== '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; audio.bg.loop = true;
function tryPlayBg() { function tryPlayBg() {
if (!soundState.bg) return; // Jangan play kalau muted
audio.bg.play().catch(() => { audio.bg.play().catch(() => {
const unlock = () => { const unlock = () => {
audio.bg.play().catch(()=>{}); if (soundState.bg) audio.bg.play().catch(()=>{});
window.removeEventListener("keydown", unlock); window.removeEventListener("keydown", unlock);
window.removeEventListener("click", unlock); window.removeEventListener("click", unlock);
}; };
@ -41,6 +57,7 @@ document.addEventListener("DOMContentLoaded", () => {
setupBoard(); setupBoard();
addNewTile(); addNewTile();
addNewTile(); addNewTile();
updateAudioVolumes(); // Apply saved sound settings
tryPlayBg(); tryPlayBg();
document.addEventListener("keydown", handleKey); document.addEventListener("keydown", handleKey);
setupEventListeners(); setupEventListeners();
@ -94,12 +111,10 @@ function setupEventListeners() {
btnHome.addEventListener('click', goHome); btnHome.addEventListener('click', goHome);
} }
// Close button (X) di game over modal
if (gameOverClose) { if (gameOverClose) {
gameOverClose.addEventListener('click', hideGameOver); gameOverClose.addEventListener('click', hideGameOver);
} }
// Close game over when clicking outside modal
const gameOverOverlay = document.getElementById('game-over-overlay'); const gameOverOverlay = document.getElementById('game-over-overlay');
if (gameOverOverlay) { if (gameOverOverlay) {
gameOverOverlay.addEventListener('click', function(e) { gameOverOverlay.addEventListener('click', function(e) {
@ -108,6 +123,26 @@ function setupEventListeners() {
} }
}); });
} }
// 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);
}
} }
/* ------------------------ /* ------------------------
@ -137,7 +172,6 @@ function setupBoard() {
} }
} }
/* Update single tile visual */
function updateTile(row, col, num) { function updateTile(row, col, num) {
const tile = document.getElementById(`${row}-${col}`); const tile = document.getElementById(`${row}-${col}`);
if (!tile) return; if (!tile) return;
@ -152,7 +186,6 @@ function updateTile(row, col, num) {
} }
} }
/* Refresh whole board */
function refreshBoard() { function refreshBoard() {
for (let r = 0; r < 4; r++) { for (let r = 0; r < 4; r++) {
for (let c = 0; c < 4; c++) { for (let c = 0; c < 4; c++) {
@ -191,7 +224,7 @@ function resetScore() {
} }
/* ------------------------ /* ------------------------
Add New Tile - FIXED: Only play pop sound here Add New Tile
------------------------ */ ------------------------ */
function addNewTile() { function addNewTile() {
const empty = []; const empty = [];
@ -209,7 +242,6 @@ function addNewTile() {
const tile = document.getElementById(`${spot.r}-${spot.c}`); const tile = document.getElementById(`${spot.r}-${spot.c}`);
if (tile) { if (tile) {
tile.classList.add("new"); tile.classList.add("new");
// ✅ POP SOUND: Hanya main di sini (tile baru muncul)
playSound(audio.pop); playSound(audio.pop);
setTimeout(() => tile.classList.remove("new"), 300); setTimeout(() => tile.classList.remove("new"), 300);
} }
@ -217,16 +249,21 @@ function addNewTile() {
return true; return true;
} }
/* Safe playSound */ /* Safe playSound with mute check */
function playSound(soundObj) { function playSound(soundObj) {
try { 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.currentTime = 0;
soundObj.play().catch(() => {}); soundObj.play().catch(() => {});
} catch (e) {} } catch (e) {}
} }
/* ------------------------ /* ------------------------
Movement Logic - FIXED: Merge sound plays consistently Movement Logic
------------------------ */ ------------------------ */
function filterZero(row) { function filterZero(row) {
return row.filter(n => n !== 0); return row.filter(n => n !== 0);
@ -242,7 +279,6 @@ function slide(row) {
if (row[i] === row[i + 1]) { if (row[i] === row[i + 1]) {
row[i] = row[i] * 2; row[i] = row[i] * 2;
// ✅ MERGE SOUND & VIBRATION: Selalu main saat merge
playSound(audio.merge); playSound(audio.merge);
if (navigator.vibrate) { if (navigator.vibrate) {
@ -266,7 +302,7 @@ function arraysEqual(a, b) {
return a.length === b.length && a.every((v, i) => v === b[i]); return a.length === b.length && a.every((v, i) => v === b[i]);
} }
/* Move functions with COMBO DETECTION */ /* Move functions */
function moveLeft() { function moveLeft() {
let moved = false; let moved = false;
let mergedCells = []; let mergedCells = [];
@ -383,14 +419,13 @@ function moveDown() {
} }
/* ------------------------ /* ------------------------
Input Handling - WITH WASD SUPPORT Input Handling
------------------------ */ ------------------------ */
function handleKey(e) { function handleKey(e) {
if (isMoving) return; if (isMoving) return;
let moved = false; let moved = false;
// Arrow Keys
if (e.key === "ArrowLeft") { if (e.key === "ArrowLeft") {
e.preventDefault(); e.preventDefault();
moved = moveLeft(); moved = moveLeft();
@ -407,7 +442,6 @@ function handleKey(e) {
e.preventDefault(); e.preventDefault();
moved = moveDown(); moved = moveDown();
} }
// WASD Keys
else if (e.key === "a" || e.key === "A") { else if (e.key === "a" || e.key === "A") {
e.preventDefault(); e.preventDefault();
moved = moveLeft(); moved = moveLeft();
@ -443,7 +477,6 @@ function handleKey(e) {
} }
} }
/* Check if any move is possible */
function canMove() { function canMove() {
for (let r = 0; r < 4; r++) { for (let r = 0; r < 4; r++) {
for (let c = 0; c < 4; c++) { for (let c = 0; c < 4; c++) {
@ -573,12 +606,47 @@ function hideGameOver() {
} }
/* ============================================= /* =============================================
COMBO EFFECT HANDLER SOUND CONTROLS
============================================= */
function toggleSound(soundType) {
soundState[soundType] = !soundState[soundType];
localStorage.setItem('sound_' + soundType, soundState[soundType]);
updateAudioVolumes();
const button = document.getElementById('btn-sound-' + soundType);
if (button) {
updateSoundButtonState(button, soundState[soundType]);
}
if (soundType === 'bg') {
if (soundState.bg) {
tryPlayBg();
} else {
audio.bg.pause();
}
}
}
function updateSoundButtonState(button, isEnabled) {
if (!button) return;
if (isEnabled) {
button.classList.remove('muted');
button.querySelector('.sound-icon').style.display = 'block';
button.querySelector('.mute-icon').style.display = 'none';
} else {
button.classList.add('muted');
button.querySelector('.sound-icon').style.display = 'none';
button.querySelector('.mute-icon').style.display = 'block';
}
}
/* =============================================
COMBO EFFECTS
============================================= */ ============================================= */
function triggerComboEffect(mergedCells, comboCount) { function triggerComboEffect(mergedCells, comboCount) {
if (mergedCells.length === 0) return; if (mergedCells.length === 0) return;
// Trigger individual tile effects
mergedCells.forEach(cell => { mergedCells.forEach(cell => {
const tile = document.getElementById(`${cell.r}-${cell.c}`); const tile = document.getElementById(`${cell.r}-${cell.c}`);
if (!tile) return; if (!tile) return;
@ -593,7 +661,6 @@ function triggerComboEffect(mergedCells, comboCount) {
tile.style.boxShadow = ''; tile.style.boxShadow = '';
}, 300); }, 300);
// Individual score popup
const rect = tile.getBoundingClientRect(); const rect = tile.getBoundingClientRect();
const centerX = rect.left + rect.width / 2; const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2; const centerY = rect.top + rect.height / 2;
@ -601,15 +668,11 @@ function triggerComboEffect(mergedCells, comboCount) {
createScorePopup(centerX, centerY, tileValue); createScorePopup(centerX, centerY, tileValue);
}); });
// Show COMBO popup based on merge count
if (comboCount >= 2) { if (comboCount >= 2) {
showComboPopup(comboCount); showComboPopup(comboCount);
} }
} }
/* =============================================
COMBO POPUP
============================================= */
function showComboPopup(comboCount) { function showComboPopup(comboCount) {
const board = document.getElementById('board'); const board = document.getElementById('board');
if (!board) return; if (!board) return;
@ -630,7 +693,6 @@ function showComboPopup(comboCount) {
popup.style.textTransform = 'uppercase'; popup.style.textTransform = 'uppercase';
popup.style.letterSpacing = '3px'; popup.style.letterSpacing = '3px';
// Different text and color based on combo count
if (comboCount === 2) { if (comboCount === 2) {
popup.textContent = 'COMBO x2!'; popup.textContent = 'COMBO x2!';
popup.style.fontSize = '36px'; popup.style.fontSize = '36px';
@ -650,7 +712,6 @@ function showComboPopup(comboCount) {
document.body.appendChild(popup); document.body.appendChild(popup);
// Animate combo popup
popup.animate([ popup.animate([
{ {
transform: 'translate(-50%, -50%) scale(0.3) rotate(-10deg)', transform: 'translate(-50%, -50%) scale(0.3) rotate(-10deg)',
@ -676,9 +737,6 @@ function showComboPopup(comboCount) {
}).onfinish = () => popup.remove(); }).onfinish = () => popup.remove();
} }
/* =============================================
PARTICLE & SCORE EFFECTS
============================================= */
function createParticleBurst(tileElement) { function createParticleBurst(tileElement) {
const rect = tileElement.getBoundingClientRect(); const rect = tileElement.getBoundingClientRect();
const centerX = rect.left + rect.width / 2; const centerX = rect.left + rect.width / 2;
@ -772,3 +830,4 @@ function getTileColor(value) {
}; };
return colors[value] || '#00eaff'; return colors[value] || '#00eaff';
} }