|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>Sea Creatures Memory Match</title> |
|
|
<style> |
|
|
* { |
|
|
margin: 0; |
|
|
padding: 0; |
|
|
box-sizing: border-box; |
|
|
} |
|
|
|
|
|
body { |
|
|
font-family: 'Arial', sans-serif; |
|
|
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%); |
|
|
min-height: 100vh; |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
color: white; |
|
|
padding: 20px; |
|
|
} |
|
|
|
|
|
.game-header { |
|
|
text-align: center; |
|
|
margin-bottom: 30px; |
|
|
} |
|
|
|
|
|
.game-title { |
|
|
font-size: 2.5rem; |
|
|
margin-bottom: 10px; |
|
|
text-shadow: 2px 2px 4px rgba(0,0,0,0.3); |
|
|
background: linear-gradient(45deg, #00d4ff, #ffffff); |
|
|
-webkit-background-clip: text; |
|
|
-webkit-text-fill-color: transparent; |
|
|
background-clip: text; |
|
|
} |
|
|
|
|
|
.game-stats { |
|
|
display: flex; |
|
|
gap: 30px; |
|
|
justify-content: center; |
|
|
margin-bottom: 20px; |
|
|
font-size: 1.2rem; |
|
|
} |
|
|
|
|
|
.stat-item { |
|
|
background: rgba(255,255,255,0.1); |
|
|
padding: 10px 20px; |
|
|
border-radius: 25px; |
|
|
backdrop-filter: blur(10px); |
|
|
border: 1px solid rgba(255,255,255,0.2); |
|
|
} |
|
|
|
|
|
.game-board { |
|
|
display: grid; |
|
|
grid-template-columns: repeat(4, 1fr); |
|
|
gap: 15px; |
|
|
max-width: 600px; |
|
|
margin: 0 auto; |
|
|
} |
|
|
|
|
|
.card { |
|
|
width: 120px; |
|
|
height: 120px; |
|
|
background: linear-gradient(145deg, #3498db, #2980b9); |
|
|
border-radius: 15px; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
cursor: pointer; |
|
|
transition: all 0.3s ease; |
|
|
transform-style: preserve-3d; |
|
|
position: relative; |
|
|
box-shadow: 0 8px 20px rgba(0,0,0,0.2); |
|
|
border: 2px solid rgba(255,255,255,0.1); |
|
|
} |
|
|
|
|
|
.card:hover { |
|
|
transform: translateY(-5px) scale(1.02); |
|
|
box-shadow: 0 12px 25px rgba(0,0,0,0.3); |
|
|
} |
|
|
|
|
|
.card.flipped { |
|
|
background: linear-gradient(145deg, #ffffff, #f0f0f0); |
|
|
color: #2c3e50; |
|
|
transform: rotateY(180deg); |
|
|
} |
|
|
|
|
|
.card.matched { |
|
|
background: linear-gradient(145deg, #2ecc71, #27ae60); |
|
|
color: white; |
|
|
animation: matchPulse 0.6s ease; |
|
|
} |
|
|
|
|
|
.card-content { |
|
|
font-size: 3rem; |
|
|
transition: opacity 0.3s ease; |
|
|
} |
|
|
|
|
|
.card:not(.flipped) .card-content { |
|
|
opacity: 0; |
|
|
} |
|
|
|
|
|
.card.flipped .card-content, |
|
|
.card.matched .card-content { |
|
|
opacity: 1; |
|
|
} |
|
|
|
|
|
.card-back { |
|
|
position: absolute; |
|
|
inset: 0; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
font-size: 2rem; |
|
|
color: rgba(255,255,255,0.8); |
|
|
transition: opacity 0.3s ease; |
|
|
} |
|
|
|
|
|
.card.flipped .card-back, |
|
|
.card.matched .card-back { |
|
|
opacity: 0; |
|
|
} |
|
|
|
|
|
@keyframes matchPulse { |
|
|
0%, 100% { transform: scale(1); } |
|
|
50% { transform: scale(1.1); } |
|
|
} |
|
|
|
|
|
.controls { |
|
|
margin-top: 30px; |
|
|
text-align: center; |
|
|
} |
|
|
|
|
|
.btn { |
|
|
background: linear-gradient(145deg, #e74c3c, #c0392b); |
|
|
color: white; |
|
|
border: none; |
|
|
padding: 12px 30px; |
|
|
border-radius: 25px; |
|
|
font-size: 1.1rem; |
|
|
cursor: pointer; |
|
|
transition: all 0.3s ease; |
|
|
box-shadow: 0 4px 15px rgba(231, 76, 60, 0.3); |
|
|
} |
|
|
|
|
|
.btn:hover { |
|
|
transform: translateY(-2px); |
|
|
box-shadow: 0 6px 20px rgba(231, 76, 60, 0.4); |
|
|
} |
|
|
|
|
|
.victory-message, .level-complete-message { |
|
|
position: fixed; |
|
|
top: 50%; |
|
|
left: 50%; |
|
|
transform: translate(-50%, -50%); |
|
|
background: linear-gradient(145deg, #2ecc71, #27ae60); |
|
|
color: white; |
|
|
padding: 30px; |
|
|
border-radius: 20px; |
|
|
text-align: center; |
|
|
font-size: 1.5rem; |
|
|
box-shadow: 0 20px 40px rgba(0,0,0,0.3); |
|
|
opacity: 0; |
|
|
visibility: hidden; |
|
|
transition: all 0.5s ease; |
|
|
z-index: 1000; |
|
|
} |
|
|
|
|
|
.level-complete-message { |
|
|
background: linear-gradient(145deg, #3498db, #2980b9); |
|
|
} |
|
|
|
|
|
.victory-message.show, .level-complete-message.show { |
|
|
opacity: 1; |
|
|
visibility: visible; |
|
|
animation: victoryBounce 0.6s ease; |
|
|
} |
|
|
|
|
|
.next-level-btn { |
|
|
background: linear-gradient(145deg, #f39c12, #e67e22); |
|
|
color: white; |
|
|
border: none; |
|
|
padding: 12px 30px; |
|
|
border-radius: 25px; |
|
|
font-size: 1.1rem; |
|
|
cursor: pointer; |
|
|
transition: all 0.3s ease; |
|
|
margin-top: 15px; |
|
|
} |
|
|
|
|
|
.next-level-btn:hover { |
|
|
transform: translateY(-2px); |
|
|
box-shadow: 0 6px 20px rgba(243, 156, 18, 0.4); |
|
|
} |
|
|
|
|
|
.how-to-play { |
|
|
background: rgba(255,255,255,0.1); |
|
|
backdrop-filter: blur(15px); |
|
|
border: 1px solid rgba(255,255,255,0.2); |
|
|
border-radius: 20px; |
|
|
padding: 25px; |
|
|
margin: 20px auto; |
|
|
max-width: 600px; |
|
|
text-align: left; |
|
|
transition: all 0.5s ease; |
|
|
} |
|
|
|
|
|
.how-to-play.hidden { |
|
|
opacity: 0; |
|
|
visibility: hidden; |
|
|
transform: translateY(-20px); |
|
|
} |
|
|
|
|
|
.how-to-play h3 { |
|
|
text-align: center; |
|
|
margin-bottom: 20px; |
|
|
font-size: 1.5rem; |
|
|
color: #00d4ff; |
|
|
} |
|
|
|
|
|
.how-to-play ul { |
|
|
list-style: none; |
|
|
padding: 0; |
|
|
} |
|
|
|
|
|
.how-to-play li { |
|
|
margin: 12px 0; |
|
|
padding: 8px 0; |
|
|
font-size: 1.1rem; |
|
|
line-height: 1.4; |
|
|
} |
|
|
|
|
|
.close-guide { |
|
|
background: linear-gradient(145deg, #00d4ff, #0099cc); |
|
|
color: white; |
|
|
border: none; |
|
|
padding: 10px 25px; |
|
|
border-radius: 20px; |
|
|
font-size: 1rem; |
|
|
cursor: pointer; |
|
|
display: block; |
|
|
margin: 20px auto 0; |
|
|
transition: all 0.3s ease; |
|
|
} |
|
|
|
|
|
.close-guide:hover { |
|
|
transform: translateY(-2px); |
|
|
box-shadow: 0 6px 20px rgba(0, 153, 204, 0.4); |
|
|
} |
|
|
|
|
|
.sound-controls { |
|
|
margin: 20px 0; |
|
|
} |
|
|
|
|
|
.sound-panel { |
|
|
background: rgba(255,255,255,0.1); |
|
|
backdrop-filter: blur(10px); |
|
|
border: 1px solid rgba(255,255,255,0.2); |
|
|
border-radius: 15px; |
|
|
padding: 20px; |
|
|
display: flex; |
|
|
gap: 20px; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
flex-wrap: wrap; |
|
|
} |
|
|
|
|
|
.sound-item { |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
align-items: center; |
|
|
gap: 8px; |
|
|
min-width: 120px; |
|
|
} |
|
|
|
|
|
.sound-item label { |
|
|
font-size: 0.9rem; |
|
|
font-weight: bold; |
|
|
text-align: center; |
|
|
} |
|
|
|
|
|
.sound-toggle { |
|
|
background: linear-gradient(145deg, #2ecc71, #27ae60); |
|
|
color: white; |
|
|
border: none; |
|
|
padding: 6px 16px; |
|
|
border-radius: 15px; |
|
|
font-size: 0.8rem; |
|
|
font-weight: bold; |
|
|
cursor: pointer; |
|
|
transition: all 0.3s ease; |
|
|
min-width: 50px; |
|
|
} |
|
|
|
|
|
.sound-toggle.off { |
|
|
background: linear-gradient(145deg, #e74c3c, #c0392b); |
|
|
} |
|
|
|
|
|
.sound-toggle:hover { |
|
|
transform: translateY(-1px); |
|
|
} |
|
|
|
|
|
.sound-item input[type="range"] { |
|
|
width: 80px; |
|
|
height: 6px; |
|
|
border-radius: 3px; |
|
|
background: rgba(255,255,255,0.3); |
|
|
outline: none; |
|
|
cursor: pointer; |
|
|
} |
|
|
|
|
|
.sound-item input[type="range"]::-webkit-slider-thumb { |
|
|
appearance: none; |
|
|
width: 16px; |
|
|
height: 16px; |
|
|
border-radius: 50%; |
|
|
background: #00d4ff; |
|
|
cursor: pointer; |
|
|
box-shadow: 0 2px 6px rgba(0,0,0,0.3); |
|
|
} |
|
|
|
|
|
.sound-item select { |
|
|
background: rgba(255,255,255,0.1); |
|
|
color: white; |
|
|
border: 1px solid rgba(255,255,255,0.3); |
|
|
border-radius: 8px; |
|
|
padding: 6px 12px; |
|
|
font-size: 0.9rem; |
|
|
cursor: pointer; |
|
|
} |
|
|
|
|
|
.sound-item select option { |
|
|
background: #2980b9; |
|
|
color: white; |
|
|
} |
|
|
|
|
|
.guide-btn { |
|
|
background: linear-gradient(145deg, #9b59b6, #8e44ad); |
|
|
margin-left: 15px; |
|
|
} |
|
|
|
|
|
.guide-btn:hover { |
|
|
box-shadow: 0 6px 20px rgba(155, 89, 182, 0.4); |
|
|
} |
|
|
|
|
|
@keyframes victoryBounce { |
|
|
0%, 100% { transform: translate(-50%, -50%) scale(1); } |
|
|
50% { transform: translate(-50%, -50%) scale(1.05); } |
|
|
} |
|
|
|
|
|
@media (max-width: 768px) { |
|
|
.game-board { |
|
|
grid-template-columns: repeat(3, 1fr); |
|
|
gap: 10px; |
|
|
} |
|
|
|
|
|
.card { |
|
|
width: 90px; |
|
|
height: 90px; |
|
|
} |
|
|
|
|
|
.card-content { |
|
|
font-size: 2rem; |
|
|
} |
|
|
|
|
|
.game-title { |
|
|
font-size: 2rem; |
|
|
} |
|
|
|
|
|
.game-stats { |
|
|
flex-direction: column; |
|
|
gap: 10px; |
|
|
} |
|
|
|
|
|
.sound-panel { |
|
|
flex-direction: column; |
|
|
gap: 15px; |
|
|
} |
|
|
|
|
|
.how-to-play { |
|
|
margin: 10px; |
|
|
padding: 20px; |
|
|
} |
|
|
|
|
|
.how-to-play li { |
|
|
font-size: 1rem; |
|
|
} |
|
|
|
|
|
.controls { |
|
|
flex-direction: column; |
|
|
gap: 10px; |
|
|
} |
|
|
|
|
|
.guide-btn { |
|
|
margin-left: 0; |
|
|
} |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<div class="game-header"> |
|
|
<h1 class="game-title">๐ Sea Creatures Memory Match ๐</h1> |
|
|
|
|
|
|
|
|
<div class="how-to-play" id="howToPlay"> |
|
|
<h3>๐ฏ How to Play</h3> |
|
|
<ul> |
|
|
<li>๐ฑ๏ธ Click any card to flip it and reveal a sea creature</li> |
|
|
<li>๐ Click a second card to find its matching pair</li> |
|
|
<li>โ
If they match, they stay revealed with a green glow!</li> |
|
|
<li>โ If they don't match, they flip back - remember their positions!</li> |
|
|
<li>๐ Find all 8 pairs to win the game</li> |
|
|
<li>โก Try to complete it in the fewest moves and fastest time!</li> |
|
|
</ul> |
|
|
<button class="close-guide" onclick="toggleGuide()">Got it! ๐</button> |
|
|
</div> |
|
|
|
|
|
<div class="game-stats"> |
|
|
<div class="stat-item"> |
|
|
<span>Level: </span><span id="level">1</span> |
|
|
</div> |
|
|
<div class="stat-item"> |
|
|
<span>Moves: </span><span id="moves">0</span> |
|
|
</div> |
|
|
<div class="stat-item"> |
|
|
<span>Matches: </span><span id="matches">0</span> |
|
|
</div> |
|
|
<div class="stat-item"> |
|
|
<span>Time: </span><span id="timer">00:00</span> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="sound-controls"> |
|
|
<div class="sound-panel"> |
|
|
<div class="sound-item"> |
|
|
<label>๐ต Background Music</label> |
|
|
<button class="sound-toggle" id="musicToggle" onclick="toggleMusic()">ON</button> |
|
|
<input type="range" id="musicVolume" min="0" max="100" value="30" onchange="setMusicVolume(this.value)"> |
|
|
</div> |
|
|
<div class="sound-item"> |
|
|
<label>๐ Sound Effects</label> |
|
|
<button class="sound-toggle" id="sfxToggle" onclick="toggleSFX()">ON</button> |
|
|
<input type="range" id="sfxVolume" min="0" max="100" value="50" onchange="setSFXVolume(this.value)"> |
|
|
</div> |
|
|
<div class="sound-item"> |
|
|
<label>โก Game Speed</label> |
|
|
<select id="gameSpeed" onchange="setGameSpeed(this.value)"> |
|
|
<option value="fast">Fast</option> |
|
|
<option value="normal" selected>Normal</option> |
|
|
<option value="slow">Slow</option> |
|
|
</select> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="game-board" id="gameBoard"></div> |
|
|
|
|
|
<div class="controls"> |
|
|
<button class="btn" onclick="resetGame()">๐ New Game</button> |
|
|
<button class="btn guide-btn" onclick="toggleGuide()">โ How to Play</button> |
|
|
</div> |
|
|
|
|
|
<div class="victory-message" id="victoryMessage"> |
|
|
<h2>๐ Congratulations! ๐</h2> |
|
|
<p>You found all the sea creature pairs!</p> |
|
|
<p id="finalStats"></p> |
|
|
</div> |
|
|
|
|
|
<div class="level-complete-message" id="levelCompleteMessage"> |
|
|
<h2>๐ Level Complete! ๐</h2> |
|
|
<p>Amazing! You've completed this level!</p> |
|
|
<p id="levelStats"></p> |
|
|
<button class="btn next-level-btn" onclick="nextLevel()">๐ Next Level</button> |
|
|
</div> |
|
|
|
|
|
<script> |
|
|
const seaCreatures = [ |
|
|
'๐ ', '๐', '๐ก', '๐ฆ', |
|
|
'๐', '๐ฆ', '๐', '๐ฆ', |
|
|
'๐', '๐ณ', '๐ฆ', '๐ข', |
|
|
'๐ฌ', '๐ฆญ', '๐ง', 'โญ' |
|
|
]; |
|
|
|
|
|
let gameBoard = []; |
|
|
let flippedCards = []; |
|
|
let matchedPairs = 0; |
|
|
let moves = 0; |
|
|
let startTime = null; |
|
|
let timerInterval = null; |
|
|
let gameActive = false; |
|
|
let gameSpeed = 'normal'; |
|
|
let musicEnabled = true; |
|
|
let sfxEnabled = true; |
|
|
let musicVolume = 0.3; |
|
|
let sfxVolume = 0.5; |
|
|
let currentLevel = 1; |
|
|
let totalMatches = 0; |
|
|
|
|
|
|
|
|
let audioContext; |
|
|
let backgroundMusic; |
|
|
let matchSound; |
|
|
let flipSound; |
|
|
|
|
|
|
|
|
function initAudio() { |
|
|
try { |
|
|
audioContext = new (window.AudioContext || window.webkitAudioContext)(); |
|
|
createBackgroundMusic(); |
|
|
createSoundEffects(); |
|
|
} catch (e) { |
|
|
console.log('Audio not supported'); |
|
|
} |
|
|
} |
|
|
|
|
|
function createBackgroundMusic() { |
|
|
if (!audioContext) return; |
|
|
|
|
|
|
|
|
if (backgroundMusic && backgroundMusic.oscillators) { |
|
|
backgroundMusic.oscillators.forEach(osc => { |
|
|
try { osc.stop(); } catch (e) {} |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
const masterGain = audioContext.createGain(); |
|
|
masterGain.gain.setValueAtTime(musicVolume * 0.15, audioContext.currentTime); |
|
|
masterGain.connect(audioContext.destination); |
|
|
|
|
|
const oscillators = []; |
|
|
|
|
|
|
|
|
const bass = audioContext.createOscillator(); |
|
|
const bassGain = audioContext.createGain(); |
|
|
bass.type = 'sine'; |
|
|
bass.frequency.setValueAtTime(55, audioContext.currentTime); |
|
|
bassGain.gain.setValueAtTime(0.3, audioContext.currentTime); |
|
|
bass.connect(bassGain); |
|
|
bassGain.connect(masterGain); |
|
|
oscillators.push(bass); |
|
|
|
|
|
|
|
|
const harmonics = [110, 165, 220, 330]; |
|
|
harmonics.forEach((freq, i) => { |
|
|
const osc = audioContext.createOscillator(); |
|
|
const gain = audioContext.createGain(); |
|
|
osc.type = 'triangle'; |
|
|
osc.frequency.setValueAtTime(freq, audioContext.currentTime); |
|
|
gain.gain.setValueAtTime(0.1 / (i + 1), audioContext.currentTime); |
|
|
osc.connect(gain); |
|
|
gain.connect(masterGain); |
|
|
oscillators.push(osc); |
|
|
}); |
|
|
|
|
|
|
|
|
const waveLFO = audioContext.createOscillator(); |
|
|
const waveGain = audioContext.createGain(); |
|
|
waveLFO.type = 'sine'; |
|
|
waveLFO.frequency.setValueAtTime(0.3, audioContext.currentTime); |
|
|
waveGain.gain.setValueAtTime(8, audioContext.currentTime); |
|
|
waveLFO.connect(waveGain); |
|
|
waveGain.connect(bass.frequency); |
|
|
oscillators.push(waveLFO); |
|
|
|
|
|
|
|
|
const bubbles = audioContext.createOscillator(); |
|
|
const bubblesGain = audioContext.createGain(); |
|
|
const bubblesLFO = audioContext.createOscillator(); |
|
|
const bubblesLFOGain = audioContext.createGain(); |
|
|
|
|
|
bubbles.type = 'sine'; |
|
|
bubbles.frequency.setValueAtTime(1760, audioContext.currentTime); |
|
|
bubblesGain.gain.setValueAtTime(0.02, audioContext.currentTime); |
|
|
|
|
|
bubblesLFO.type = 'sine'; |
|
|
bubblesLFO.frequency.setValueAtTime(0.7, audioContext.currentTime); |
|
|
bubblesLFOGain.gain.setValueAtTime(200, audioContext.currentTime); |
|
|
|
|
|
bubblesLFO.connect(bubblesLFOGain); |
|
|
bubblesLFOGain.connect(bubbles.frequency); |
|
|
bubbles.connect(bubblesGain); |
|
|
bubblesGain.connect(masterGain); |
|
|
|
|
|
oscillators.push(bubbles, bubblesLFO); |
|
|
|
|
|
|
|
|
const whale = audioContext.createOscillator(); |
|
|
const whaleGain = audioContext.createGain(); |
|
|
const whaleLFO = audioContext.createOscillator(); |
|
|
const whaleLFOGain = audioContext.createGain(); |
|
|
|
|
|
whale.type = 'sawtooth'; |
|
|
whale.frequency.setValueAtTime(80, audioContext.currentTime); |
|
|
whaleGain.gain.setValueAtTime(0.05, audioContext.currentTime); |
|
|
|
|
|
whaleLFO.type = 'sine'; |
|
|
whaleLFO.frequency.setValueAtTime(0.1, audioContext.currentTime); |
|
|
whaleLFOGain.gain.setValueAtTime(20, audioContext.currentTime); |
|
|
|
|
|
whaleLFO.connect(whaleLFOGain); |
|
|
whaleLFOGain.connect(whale.frequency); |
|
|
whale.connect(whaleGain); |
|
|
whaleGain.connect(masterGain); |
|
|
|
|
|
oscillators.push(whale, whaleLFO); |
|
|
|
|
|
backgroundMusic = { oscillators, masterGain }; |
|
|
|
|
|
if (musicEnabled) { |
|
|
try { |
|
|
oscillators.forEach(osc => osc.start()); |
|
|
} catch (e) { |
|
|
console.log('Could not start background music'); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
function createSoundEffects() { |
|
|
|
|
|
matchSound = () => { |
|
|
if (!audioContext || !sfxEnabled) return; |
|
|
|
|
|
const oscillator = audioContext.createOscillator(); |
|
|
const gainNode = audioContext.createGain(); |
|
|
|
|
|
oscillator.connect(gainNode); |
|
|
gainNode.connect(audioContext.destination); |
|
|
|
|
|
oscillator.type = 'triangle'; |
|
|
oscillator.frequency.setValueAtTime(523.25, audioContext.currentTime); |
|
|
oscillator.frequency.setValueAtTime(659.25, audioContext.currentTime + 0.1); |
|
|
oscillator.frequency.setValueAtTime(783.99, audioContext.currentTime + 0.2); |
|
|
|
|
|
gainNode.gain.setValueAtTime(sfxVolume * 0.3, audioContext.currentTime); |
|
|
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.5); |
|
|
|
|
|
oscillator.start(audioContext.currentTime); |
|
|
oscillator.stop(audioContext.currentTime + 0.5); |
|
|
}; |
|
|
|
|
|
|
|
|
flipSound = () => { |
|
|
if (!audioContext || !sfxEnabled) return; |
|
|
|
|
|
const oscillator = audioContext.createOscillator(); |
|
|
const gainNode = audioContext.createGain(); |
|
|
|
|
|
oscillator.connect(gainNode); |
|
|
gainNode.connect(audioContext.destination); |
|
|
|
|
|
oscillator.type = 'square'; |
|
|
oscillator.frequency.setValueAtTime(800, audioContext.currentTime); |
|
|
|
|
|
gainNode.gain.setValueAtTime(sfxVolume * 0.1, audioContext.currentTime); |
|
|
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.1); |
|
|
|
|
|
oscillator.start(audioContext.currentTime); |
|
|
oscillator.stop(audioContext.currentTime + 0.1); |
|
|
}; |
|
|
} |
|
|
|
|
|
function getSpeedDelay() { |
|
|
const speeds = { |
|
|
fast: { match: 300, flip: 600 }, |
|
|
normal: { match: 500, flip: 1000 }, |
|
|
slow: { match: 800, flip: 1500 } |
|
|
}; |
|
|
return speeds[gameSpeed]; |
|
|
} |
|
|
|
|
|
function initGame() { |
|
|
|
|
|
gameBoard = []; |
|
|
flippedCards = []; |
|
|
matchedPairs = 0; |
|
|
moves = 0; |
|
|
startTime = null; |
|
|
gameActive = true; |
|
|
|
|
|
if (timerInterval) { |
|
|
clearInterval(timerInterval); |
|
|
} |
|
|
|
|
|
|
|
|
const creatures = seaCreatures.slice(0, 8); |
|
|
const pairs = [...creatures, ...creatures]; |
|
|
gameBoard = shuffle(pairs); |
|
|
|
|
|
|
|
|
updateStats(); |
|
|
createBoard(); |
|
|
document.getElementById('victoryMessage').classList.remove('show'); |
|
|
document.getElementById('levelCompleteMessage').classList.remove('show'); |
|
|
} |
|
|
|
|
|
function shuffle(array) { |
|
|
const shuffled = [...array]; |
|
|
for (let i = shuffled.length - 1; i > 0; i--) { |
|
|
const j = Math.floor(Math.random() * (i + 1)); |
|
|
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; |
|
|
} |
|
|
return shuffled; |
|
|
} |
|
|
|
|
|
function createBoard() { |
|
|
const board = document.getElementById('gameBoard'); |
|
|
board.innerHTML = ''; |
|
|
|
|
|
gameBoard.forEach((creature, index) => { |
|
|
const card = document.createElement('div'); |
|
|
card.className = 'card'; |
|
|
card.dataset.index = index; |
|
|
card.innerHTML = ` |
|
|
<div class="card-back">๐</div> |
|
|
<div class="card-content">${creature}</div> |
|
|
`; |
|
|
card.addEventListener('click', () => flipCard(index)); |
|
|
board.appendChild(card); |
|
|
}); |
|
|
} |
|
|
|
|
|
function flipCard(index) { |
|
|
if (!gameActive) return; |
|
|
|
|
|
const card = document.querySelector(`[data-index="${index}"]`); |
|
|
|
|
|
|
|
|
if (card.classList.contains('flipped') || card.classList.contains('matched')) { |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
if (!startTime) { |
|
|
startTimer(); |
|
|
|
|
|
if (!audioContext) { |
|
|
initAudio(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (flipSound) flipSound(); |
|
|
|
|
|
|
|
|
card.classList.add('flipped'); |
|
|
flippedCards.push(index); |
|
|
|
|
|
|
|
|
if (flippedCards.length === 2) { |
|
|
moves++; |
|
|
updateStats(); |
|
|
checkForMatch(); |
|
|
} |
|
|
} |
|
|
|
|
|
function checkForMatch() { |
|
|
const [first, second] = flippedCards; |
|
|
const firstCard = document.querySelector(`[data-index="${first}"]`); |
|
|
const secondCard = document.querySelector(`[data-index="${second}"]`); |
|
|
const delays = getSpeedDelay(); |
|
|
|
|
|
if (gameBoard[first] === gameBoard[second]) { |
|
|
|
|
|
setTimeout(() => { |
|
|
|
|
|
if (matchSound) matchSound(); |
|
|
|
|
|
firstCard.classList.add('matched'); |
|
|
secondCard.classList.add('matched'); |
|
|
firstCard.classList.remove('flipped'); |
|
|
secondCard.classList.remove('flipped'); |
|
|
matchedPairs++; |
|
|
totalMatches++; |
|
|
updateStats(); |
|
|
flippedCards = []; |
|
|
|
|
|
|
|
|
if (matchedPairs === 8) { |
|
|
if (totalMatches >= 14) { |
|
|
gameWon(); |
|
|
} else { |
|
|
levelComplete(); |
|
|
} |
|
|
} |
|
|
}, delays.match); |
|
|
} else { |
|
|
|
|
|
setTimeout(() => { |
|
|
firstCard.classList.remove('flipped'); |
|
|
secondCard.classList.remove('flipped'); |
|
|
flippedCards = []; |
|
|
}, delays.flip); |
|
|
} |
|
|
} |
|
|
|
|
|
function startTimer() { |
|
|
startTime = Date.now(); |
|
|
timerInterval = setInterval(updateTimer, 1000); |
|
|
} |
|
|
|
|
|
function updateTimer() { |
|
|
if (!startTime) return; |
|
|
|
|
|
const elapsed = Math.floor((Date.now() - startTime) / 1000); |
|
|
const minutes = Math.floor(elapsed / 60).toString().padStart(2, '0'); |
|
|
const seconds = (elapsed % 60).toString().padStart(2, '0'); |
|
|
document.getElementById('timer').textContent = `${minutes}:${seconds}`; |
|
|
} |
|
|
|
|
|
function updateStats() { |
|
|
document.getElementById('level').textContent = currentLevel; |
|
|
document.getElementById('moves').textContent = moves; |
|
|
document.getElementById('matches').textContent = `${totalMatches}`; |
|
|
} |
|
|
|
|
|
function levelComplete() { |
|
|
gameActive = false; |
|
|
clearInterval(timerInterval); |
|
|
|
|
|
const finalTime = document.getElementById('timer').textContent; |
|
|
document.getElementById('levelStats').innerHTML = |
|
|
`Level ${currentLevel} Complete!<br>Time: ${finalTime}<br>Moves: ${moves}<br>Total Matches: ${totalMatches}`; |
|
|
|
|
|
setTimeout(() => { |
|
|
document.getElementById('levelCompleteMessage').classList.add('show'); |
|
|
}, 500); |
|
|
} |
|
|
|
|
|
function nextLevel() { |
|
|
currentLevel++; |
|
|
document.getElementById('levelCompleteMessage').classList.remove('show'); |
|
|
initGame(); |
|
|
} |
|
|
|
|
|
function gameWon() { |
|
|
gameActive = false; |
|
|
clearInterval(timerInterval); |
|
|
|
|
|
const finalTime = document.getElementById('timer').textContent; |
|
|
document.getElementById('finalStats').innerHTML = |
|
|
`๐ Game Complete! ๐<br>Total Levels: ${currentLevel}<br>Final Time: ${finalTime}<br>Total Moves: ${moves}<br>Total Matches: ${totalMatches}`; |
|
|
|
|
|
setTimeout(() => { |
|
|
document.getElementById('victoryMessage').classList.add('show'); |
|
|
}, 500); |
|
|
} |
|
|
|
|
|
function resetGame() { |
|
|
currentLevel = 1; |
|
|
totalMatches = 0; |
|
|
initGame(); |
|
|
} |
|
|
|
|
|
|
|
|
function toggleGuide() { |
|
|
const guide = document.getElementById('howToPlay'); |
|
|
guide.classList.toggle('hidden'); |
|
|
} |
|
|
|
|
|
function toggleMusic() { |
|
|
musicEnabled = !musicEnabled; |
|
|
const toggle = document.getElementById('musicToggle'); |
|
|
toggle.textContent = musicEnabled ? 'ON' : 'OFF'; |
|
|
toggle.classList.toggle('off', !musicEnabled); |
|
|
|
|
|
if (backgroundMusic && musicEnabled) { |
|
|
backgroundMusic.masterGain.gain.setValueAtTime( |
|
|
musicEnabled ? musicVolume * 0.15 : 0, |
|
|
audioContext.currentTime |
|
|
); |
|
|
} |
|
|
} |
|
|
|
|
|
function toggleSFX() { |
|
|
sfxEnabled = !sfxEnabled; |
|
|
const toggle = document.getElementById('sfxToggle'); |
|
|
toggle.textContent = sfxEnabled ? 'ON' : 'OFF'; |
|
|
toggle.classList.toggle('off', !sfxEnabled); |
|
|
} |
|
|
|
|
|
function setMusicVolume(value) { |
|
|
musicVolume = value / 100; |
|
|
if (backgroundMusic && musicEnabled) { |
|
|
backgroundMusic.masterGain.gain.setValueAtTime( |
|
|
musicVolume * 0.15, |
|
|
audioContext.currentTime |
|
|
); |
|
|
} |
|
|
} |
|
|
|
|
|
function setSFXVolume(value) { |
|
|
sfxVolume = value / 100; |
|
|
} |
|
|
|
|
|
function setGameSpeed(speed) { |
|
|
gameSpeed = speed; |
|
|
} |
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => { |
|
|
initGame(); |
|
|
|
|
|
document.getElementById('howToPlay').classList.add('hidden'); |
|
|
}); |
|
|
</script> |
|
|
</body> |
|
|
</html> |