2048
This commit is contained in:
parent
c14ba859b8
commit
b08d1b5ce7
169
2048.css
169
2048.css
@ -120,6 +120,175 @@ h1 {
|
||||
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 {
|
||||
width: clamp(36px, 6vw, 48px);
|
||||
height: clamp(36px, 6vw, 48px);
|
||||
|
||||
37
2048.html
37
2048.html
@ -15,6 +15,43 @@
|
||||
<div class="starfield" 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 -->
|
||||
<div class="top-controls">
|
||||
<button class="icon-btn btn-tutorial" id="btn-tutorial" title="How to Play">
|
||||
|
||||
119
2048.js
119
2048.js
@ -1,3 +1,5 @@
|
||||
/* 2048.js — Complete Version with WASD + Interactive Effects + Sound Controls */
|
||||
|
||||
/* ------------------------
|
||||
State & Variables
|
||||
------------------------ */
|
||||
@ -16,15 +18,29 @@ const audio = {
|
||||
pop: new Audio("Pop.mp3"),
|
||||
merge: new Audio("Merge.mp3")
|
||||
};
|
||||
audio.bg.volume = 0.25;
|
||||
audio.pop.volume = 0.9;
|
||||
audio.merge.volume = 1.0;
|
||||
|
||||
// Sound State (baca dari localStorage atau default ON)
|
||||
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;
|
||||
|
||||
function tryPlayBg() {
|
||||
if (!soundState.bg) return; // Jangan play kalau muted
|
||||
|
||||
audio.bg.play().catch(() => {
|
||||
const unlock = () => {
|
||||
audio.bg.play().catch(()=>{});
|
||||
if (soundState.bg) audio.bg.play().catch(()=>{});
|
||||
window.removeEventListener("keydown", unlock);
|
||||
window.removeEventListener("click", unlock);
|
||||
};
|
||||
@ -41,6 +57,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
setupBoard();
|
||||
addNewTile();
|
||||
addNewTile();
|
||||
updateAudioVolumes(); // Apply saved sound settings
|
||||
tryPlayBg();
|
||||
document.addEventListener("keydown", handleKey);
|
||||
setupEventListeners();
|
||||
@ -94,12 +111,10 @@ function setupEventListeners() {
|
||||
btnHome.addEventListener('click', goHome);
|
||||
}
|
||||
|
||||
// Close button (X) di game over modal
|
||||
if (gameOverClose) {
|
||||
gameOverClose.addEventListener('click', hideGameOver);
|
||||
}
|
||||
|
||||
// Close game over when clicking outside modal
|
||||
const gameOverOverlay = document.getElementById('game-over-overlay');
|
||||
if (gameOverOverlay) {
|
||||
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) {
|
||||
const tile = document.getElementById(`${row}-${col}`);
|
||||
if (!tile) return;
|
||||
@ -152,7 +186,6 @@ function updateTile(row, col, num) {
|
||||
}
|
||||
}
|
||||
|
||||
/* Refresh whole board */
|
||||
function refreshBoard() {
|
||||
for (let r = 0; r < 4; r++) {
|
||||
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() {
|
||||
const empty = [];
|
||||
@ -209,7 +242,6 @@ function addNewTile() {
|
||||
const tile = document.getElementById(`${spot.r}-${spot.c}`);
|
||||
if (tile) {
|
||||
tile.classList.add("new");
|
||||
// ✅ POP SOUND: Hanya main di sini (tile baru muncul)
|
||||
playSound(audio.pop);
|
||||
setTimeout(() => tile.classList.remove("new"), 300);
|
||||
}
|
||||
@ -217,16 +249,21 @@ function addNewTile() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Safe playSound */
|
||||
/* 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 - FIXED: Merge sound plays consistently
|
||||
Movement Logic
|
||||
------------------------ */
|
||||
function filterZero(row) {
|
||||
return row.filter(n => n !== 0);
|
||||
@ -242,7 +279,6 @@ function slide(row) {
|
||||
if (row[i] === row[i + 1]) {
|
||||
row[i] = row[i] * 2;
|
||||
|
||||
// ✅ MERGE SOUND & VIBRATION: Selalu main saat merge
|
||||
playSound(audio.merge);
|
||||
|
||||
if (navigator.vibrate) {
|
||||
@ -266,7 +302,7 @@ function arraysEqual(a, b) {
|
||||
return a.length === b.length && a.every((v, i) => v === b[i]);
|
||||
}
|
||||
|
||||
/* Move functions with COMBO DETECTION */
|
||||
/* Move functions */
|
||||
function moveLeft() {
|
||||
let moved = false;
|
||||
let mergedCells = [];
|
||||
@ -383,14 +419,13 @@ function moveDown() {
|
||||
}
|
||||
|
||||
/* ------------------------
|
||||
Input Handling - WITH WASD SUPPORT
|
||||
Input Handling
|
||||
------------------------ */
|
||||
function handleKey(e) {
|
||||
if (isMoving) return;
|
||||
|
||||
let moved = false;
|
||||
|
||||
// Arrow Keys
|
||||
if (e.key === "ArrowLeft") {
|
||||
e.preventDefault();
|
||||
moved = moveLeft();
|
||||
@ -407,7 +442,6 @@ function handleKey(e) {
|
||||
e.preventDefault();
|
||||
moved = moveDown();
|
||||
}
|
||||
// WASD Keys
|
||||
else if (e.key === "a" || e.key === "A") {
|
||||
e.preventDefault();
|
||||
moved = moveLeft();
|
||||
@ -443,7 +477,6 @@ function handleKey(e) {
|
||||
}
|
||||
}
|
||||
|
||||
/* Check if any move is possible */
|
||||
function canMove() {
|
||||
for (let r = 0; r < 4; r++) {
|
||||
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) {
|
||||
if (mergedCells.length === 0) return;
|
||||
|
||||
// Trigger individual tile effects
|
||||
mergedCells.forEach(cell => {
|
||||
const tile = document.getElementById(`${cell.r}-${cell.c}`);
|
||||
if (!tile) return;
|
||||
@ -593,7 +661,6 @@ function triggerComboEffect(mergedCells, comboCount) {
|
||||
tile.style.boxShadow = '';
|
||||
}, 300);
|
||||
|
||||
// Individual score popup
|
||||
const rect = tile.getBoundingClientRect();
|
||||
const centerX = rect.left + rect.width / 2;
|
||||
const centerY = rect.top + rect.height / 2;
|
||||
@ -601,15 +668,11 @@ function triggerComboEffect(mergedCells, comboCount) {
|
||||
createScorePopup(centerX, centerY, tileValue);
|
||||
});
|
||||
|
||||
// Show COMBO popup based on merge count
|
||||
if (comboCount >= 2) {
|
||||
showComboPopup(comboCount);
|
||||
}
|
||||
}
|
||||
|
||||
/* =============================================
|
||||
COMBO POPUP
|
||||
============================================= */
|
||||
function showComboPopup(comboCount) {
|
||||
const board = document.getElementById('board');
|
||||
if (!board) return;
|
||||
@ -630,7 +693,6 @@ function showComboPopup(comboCount) {
|
||||
popup.style.textTransform = 'uppercase';
|
||||
popup.style.letterSpacing = '3px';
|
||||
|
||||
// Different text and color based on combo count
|
||||
if (comboCount === 2) {
|
||||
popup.textContent = 'COMBO x2!';
|
||||
popup.style.fontSize = '36px';
|
||||
@ -650,7 +712,6 @@ function showComboPopup(comboCount) {
|
||||
|
||||
document.body.appendChild(popup);
|
||||
|
||||
// Animate combo popup
|
||||
popup.animate([
|
||||
{
|
||||
transform: 'translate(-50%, -50%) scale(0.3) rotate(-10deg)',
|
||||
@ -676,9 +737,6 @@ function showComboPopup(comboCount) {
|
||||
}).onfinish = () => popup.remove();
|
||||
}
|
||||
|
||||
/* =============================================
|
||||
PARTICLE & SCORE EFFECTS
|
||||
============================================= */
|
||||
function createParticleBurst(tileElement) {
|
||||
const rect = tileElement.getBoundingClientRect();
|
||||
const centerX = rect.left + rect.width / 2;
|
||||
@ -772,3 +830,4 @@ function getTileColor(value) {
|
||||
};
|
||||
return colors[value] || '#00eaff';
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user