|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>AvaRush - Adventure Match Game</title> |
|
|
<style> |
|
|
* { |
|
|
margin: 0; |
|
|
padding: 0; |
|
|
box-sizing: border-box; |
|
|
} |
|
|
|
|
|
body { |
|
|
font-family: 'Arial', sans-serif; |
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
|
|
min-height: 100vh; |
|
|
overflow-x: hidden; |
|
|
} |
|
|
|
|
|
.game-container { |
|
|
max-width: 1200px; |
|
|
margin: 0 auto; |
|
|
padding: 10px; |
|
|
} |
|
|
|
|
|
.header { |
|
|
display: flex; |
|
|
justify-content: space-between; |
|
|
align-items: center; |
|
|
background: rgba(255, 255, 255, 0.1); |
|
|
backdrop-filter: blur(10px); |
|
|
padding: 15px 20px; |
|
|
border-radius: 15px; |
|
|
margin-bottom: 10px; |
|
|
} |
|
|
|
|
|
.logo { |
|
|
font-size: 2.5em; |
|
|
font-weight: bold; |
|
|
background: linear-gradient(45deg, #ff6b6b, #4ecdc4); |
|
|
-webkit-background-clip: text; |
|
|
-webkit-text-fill-color: transparent; |
|
|
text-shadow: 2px 2px 4px rgba(0,0,0,0.3); |
|
|
} |
|
|
|
|
|
.nav-buttons { |
|
|
display: flex; |
|
|
gap: 10px; |
|
|
} |
|
|
|
|
|
.nav-btn { |
|
|
padding: 10px 20px; |
|
|
background: linear-gradient(45deg, #ff6b6b, #ee5a52); |
|
|
color: white; |
|
|
border: none; |
|
|
border-radius: 25px; |
|
|
cursor: pointer; |
|
|
font-weight: bold; |
|
|
transition: all 0.3s ease; |
|
|
} |
|
|
|
|
|
.nav-btn:hover { |
|
|
transform: translateY(-2px); |
|
|
box-shadow: 0 5px 15px rgba(0,0,0,0.3); |
|
|
} |
|
|
|
|
|
.game-area { |
|
|
display: flex; |
|
|
gap: 20px; |
|
|
height: 600px; |
|
|
} |
|
|
|
|
|
.left-panel, .right-panel { |
|
|
width: 200px; |
|
|
background: rgba(255, 255, 255, 0.1); |
|
|
backdrop-filter: blur(10px); |
|
|
border-radius: 15px; |
|
|
padding: 20px; |
|
|
color: white; |
|
|
} |
|
|
|
|
|
.game-board { |
|
|
flex: 1; |
|
|
background: rgba(255, 255, 255, 0.1); |
|
|
backdrop-filter: blur(10px); |
|
|
border-radius: 15px; |
|
|
padding: 20px; |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
align-items: center; |
|
|
} |
|
|
|
|
|
.score-info { |
|
|
text-align: right; |
|
|
margin-bottom: 20px; |
|
|
} |
|
|
|
|
|
.score-item { |
|
|
background: rgba(255, 255, 255, 0.2); |
|
|
padding: 10px; |
|
|
border-radius: 10px; |
|
|
margin-bottom: 10px; |
|
|
} |
|
|
|
|
|
.moves-info { |
|
|
text-align: left; |
|
|
margin-bottom: 20px; |
|
|
} |
|
|
|
|
|
.canvas-container { |
|
|
position: relative; |
|
|
border-radius: 15px; |
|
|
overflow: hidden; |
|
|
box-shadow: 0 10px 30px rgba(0,0,0,0.3); |
|
|
} |
|
|
|
|
|
canvas { |
|
|
display: block; |
|
|
background: #2c3e50; |
|
|
} |
|
|
|
|
|
.game-controls { |
|
|
display: flex; |
|
|
gap: 10px; |
|
|
margin-top: 20px; |
|
|
} |
|
|
|
|
|
.control-btn { |
|
|
padding: 15px 30px; |
|
|
font-size: 18px; |
|
|
border: none; |
|
|
border-radius: 25px; |
|
|
cursor: pointer; |
|
|
font-weight: bold; |
|
|
transition: all 0.3s ease; |
|
|
} |
|
|
|
|
|
.play-btn { |
|
|
background: linear-gradient(45deg, #4ecdc4, #44a08d); |
|
|
color: white; |
|
|
} |
|
|
|
|
|
.pause-btn { |
|
|
background: linear-gradient(45deg, #ffa726, #ff7043); |
|
|
color: white; |
|
|
} |
|
|
|
|
|
.control-btn:hover { |
|
|
transform: scale(1.05); |
|
|
} |
|
|
|
|
|
.objective-info { |
|
|
background: rgba(255, 255, 255, 0.2); |
|
|
padding: 15px; |
|
|
border-radius: 10px; |
|
|
text-align: center; |
|
|
color: white; |
|
|
font-weight: bold; |
|
|
margin-bottom: 20px; |
|
|
} |
|
|
|
|
|
.level-info { |
|
|
text-align: center; |
|
|
margin: 20px 0; |
|
|
} |
|
|
|
|
|
.modal { |
|
|
display: none; |
|
|
position: fixed; |
|
|
top: 0; |
|
|
left: 0; |
|
|
width: 100%; |
|
|
height: 100%; |
|
|
background: rgba(0,0,0,0.8); |
|
|
z-index: 1000; |
|
|
} |
|
|
|
|
|
.modal-content { |
|
|
position: absolute; |
|
|
top: 50%; |
|
|
left: 50%; |
|
|
transform: translate(-50%, -50%); |
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
|
|
padding: 30px; |
|
|
border-radius: 20px; |
|
|
color: white; |
|
|
text-align: center; |
|
|
min-width: 300px; |
|
|
} |
|
|
|
|
|
.toast { |
|
|
position: fixed; |
|
|
top: 20px; |
|
|
right: 20px; |
|
|
background: linear-gradient(45deg, #4ecdc4, #44a08d); |
|
|
color: white; |
|
|
padding: 15px 25px; |
|
|
border-radius: 10px; |
|
|
z-index: 1001; |
|
|
transform: translateX(400px); |
|
|
transition: transform 0.3s ease; |
|
|
} |
|
|
|
|
|
.toast.show { |
|
|
transform: translateX(0); |
|
|
} |
|
|
|
|
|
.gem { |
|
|
width: 40px; |
|
|
height: 40px; |
|
|
border-radius: 8px; |
|
|
display: inline-block; |
|
|
margin: 2px; |
|
|
cursor: pointer; |
|
|
transition: all 0.2s ease; |
|
|
} |
|
|
|
|
|
.gem:hover { |
|
|
transform: scale(1.1); |
|
|
} |
|
|
|
|
|
.gem-red { background: linear-gradient(45deg, #ff6b6b, #ee5a52); } |
|
|
.gem-blue { background: linear-gradient(45deg, #4ecdc4, #44a08d); } |
|
|
.gem-green { background: linear-gradient(45deg, #a8e6cf, #88d8a3); } |
|
|
.gem-yellow { background: linear-gradient(45deg, #ffd93d, #ffb74d); } |
|
|
.gem-purple { background: linear-gradient(45deg, #b19cd9, #9b59b6); } |
|
|
.gem-ice { background: linear-gradient(45deg, #e3f2fd, #bbdefb); border: 2px solid #2196f3; } |
|
|
|
|
|
.modal-buttons { |
|
|
display: flex; |
|
|
gap: 15px; |
|
|
justify-content: center; |
|
|
margin-top: 20px; |
|
|
} |
|
|
|
|
|
.modal-btn { |
|
|
padding: 10px 20px; |
|
|
border: none; |
|
|
border-radius: 20px; |
|
|
cursor: pointer; |
|
|
font-weight: bold; |
|
|
transition: all 0.3s ease; |
|
|
} |
|
|
|
|
|
.close-btn { |
|
|
background: #e74c3c; |
|
|
color: white; |
|
|
} |
|
|
|
|
|
.continue-btn { |
|
|
background: #27ae60; |
|
|
color: white; |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<div class="game-container"> |
|
|
<div class="header"> |
|
|
<div class="logo">AvaRush</div> |
|
|
<div class="nav-buttons"> |
|
|
<button class="nav-btn" onclick="showModal('shop')">Shop</button> |
|
|
<button class="nav-btn" onclick="showModal('equip')">Equip</button> |
|
|
<button class="nav-btn" onclick="showModal('friends')">Friends</button> |
|
|
<button class="nav-btn" onclick="showModal('tasks')">Tasks</button> |
|
|
<button class="nav-btn" onclick="showModal('rating')">Rating</button> |
|
|
<button class="nav-btn" onclick="showModal('boosters')">Boosters</button> |
|
|
<button class="nav-btn" onclick="showModal('skins')">Skins</button> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="game-area"> |
|
|
<div class="left-panel"> |
|
|
<div class="moves-info"> |
|
|
<h3>Moves</h3> |
|
|
<div class="score-item"> |
|
|
<div style="font-size: 2em; color: #4ecdc4;" id="moves-count">25</div> |
|
|
</div> |
|
|
</div> |
|
|
<div class="objective-info"> |
|
|
<div>Crush Ice</div> |
|
|
<div style="font-size: 1.5em; color: #4ecdc4;" id="ice-count">0/28</div> |
|
|
</div> |
|
|
<div class="level-info"> |
|
|
<h3>Level</h3> |
|
|
<div style="font-size: 2em; color: #ffd93d;" id="player-level">1</div> |
|
|
<div style="font-size: 0.9em; margin-top: 5px;" id="map-name">Forest Valley</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="game-board"> |
|
|
<div class="canvas-container"> |
|
|
<canvas id="gameCanvas" width="480" height="480"></canvas> |
|
|
</div> |
|
|
<div class="game-controls"> |
|
|
<button class="control-btn pause-btn" onclick="pauseGame()" id="pauseBtn">Pause</button> |
|
|
<button class="control-btn play-btn" onclick="playGame()" id="playBtn" style="display: none;">Play</button> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="right-panel"> |
|
|
<div class="score-info"> |
|
|
<h3>Score</h3> |
|
|
<div class="score-item"> |
|
|
<div style="font-size: 2em; color: #ff6b6b;" id="score">0</div> |
|
|
</div> |
|
|
<div class="score-item"> |
|
|
<div>High Score</div> |
|
|
<div style="color: #ffd93d;" id="high-score">1250</div> |
|
|
</div> |
|
|
<div class="score-item"> |
|
|
<div>Level Progress</div> |
|
|
<div style="color: #4ecdc4;" id="level-progress">Level 1/30</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div id="modal" class="modal"> |
|
|
<div class="modal-content"> |
|
|
<div id="modal-title"></div> |
|
|
<div id="modal-body"></div> |
|
|
<div class="modal-buttons"> |
|
|
<button class="modal-btn close-btn" onclick="closeModal()">Close</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div id="toast" class="toast"></div> |
|
|
|
|
|
<script> |
|
|
|
|
|
let gameState = { |
|
|
score: 0, |
|
|
moves: 25, |
|
|
level: 1, |
|
|
iceCount: 0, |
|
|
iceTarget: 28, |
|
|
isPaused: false, |
|
|
currentMap: 'forest', |
|
|
gems: [], |
|
|
selectedGem: null, |
|
|
animations: [], |
|
|
fallingGems: [], |
|
|
isAnimating: false |
|
|
}; |
|
|
|
|
|
|
|
|
let audioContext; |
|
|
|
|
|
function initAudio() { |
|
|
try { |
|
|
audioContext = new (window.AudioContext || window.webkitAudioContext)(); |
|
|
} catch (e) { |
|
|
console.log('Audio not supported'); |
|
|
} |
|
|
} |
|
|
|
|
|
function playSound(frequency, duration, type = 'sine') { |
|
|
if (!audioContext) return; |
|
|
|
|
|
const oscillator = audioContext.createOscillator(); |
|
|
const gainNode = audioContext.createGain(); |
|
|
|
|
|
oscillator.connect(gainNode); |
|
|
gainNode.connect(audioContext.destination); |
|
|
|
|
|
oscillator.frequency.setValueAtTime(frequency, audioContext.currentTime); |
|
|
oscillator.type = type; |
|
|
|
|
|
gainNode.gain.setValueAtTime(0.1, audioContext.currentTime); |
|
|
gainNode.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime + duration); |
|
|
|
|
|
oscillator.start(audioContext.currentTime); |
|
|
oscillator.stop(audioContext.currentTime + duration); |
|
|
} |
|
|
|
|
|
function playMatchSound() { |
|
|
|
|
|
playSound(523.25, 0.2); |
|
|
setTimeout(() => playSound(659.25, 0.2), 100); |
|
|
setTimeout(() => playSound(783.99, 0.3), 200); |
|
|
} |
|
|
|
|
|
function playDropSound() { |
|
|
|
|
|
playSound(220, 0.15, 'triangle'); |
|
|
} |
|
|
|
|
|
function playIceBreakSound() { |
|
|
|
|
|
playSound(1000, 0.1, 'square'); |
|
|
setTimeout(() => playSound(800, 0.1, 'square'), 50); |
|
|
setTimeout(() => playSound(600, 0.1, 'square'), 100); |
|
|
} |
|
|
|
|
|
|
|
|
const maps = { |
|
|
forest: { name: 'Forest Valley', bg: '#2d5016', levels: [1,2,3,4,5,6,7,8,9,10] }, |
|
|
desert: { name: 'Desert Oasis', bg: '#8b4513', levels: [11,12,13,14,15,16,17,18,19,20] }, |
|
|
skylands: { name: 'Sky Realm', bg: '#4169e1', levels: [21,22,23,24,25,26,27,28,29,30] } |
|
|
}; |
|
|
|
|
|
|
|
|
const canvas = document.getElementById('gameCanvas'); |
|
|
const ctx = canvas.getContext('2d'); |
|
|
const gridSize = 8; |
|
|
const gemSize = 60; |
|
|
|
|
|
|
|
|
const gemTypes = ['wood', 'water', 'snake', 'bee', 'whale', 'fish']; |
|
|
const gemColors = { |
|
|
wood: '#8B4513', |
|
|
water: '#4682B4', |
|
|
snake: '#228B22', |
|
|
bee: '#FFD700', |
|
|
whale: '#4169E1', |
|
|
fish: '#FF6347' |
|
|
}; |
|
|
|
|
|
|
|
|
function initGame() { |
|
|
createGemGrid(); |
|
|
updateUI(); |
|
|
updateMapTheme(); |
|
|
gameLoop(); |
|
|
} |
|
|
|
|
|
function createGemGrid() { |
|
|
gameState.gems = []; |
|
|
for (let row = 0; row < gridSize; row++) { |
|
|
gameState.gems[row] = []; |
|
|
for (let col = 0; col < gridSize; col++) { |
|
|
gameState.gems[row][col] = { |
|
|
type: gemTypes[Math.floor(Math.random() * gemTypes.length)], |
|
|
x: col * gemSize, |
|
|
y: row * gemSize, |
|
|
isIce: Math.random() < 0.1 |
|
|
}; |
|
|
} |
|
|
} |
|
|
|
|
|
addIceGems(); |
|
|
} |
|
|
|
|
|
function addIceGems() { |
|
|
let iceAdded = 0; |
|
|
while (iceAdded < gameState.iceTarget && iceAdded < 15) { |
|
|
const row = Math.floor(Math.random() * gridSize); |
|
|
const col = Math.floor(Math.random() * gridSize); |
|
|
if (!gameState.gems[row][col].isIce) { |
|
|
gameState.gems[row][col].isIce = true; |
|
|
iceAdded++; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
function drawGame() { |
|
|
|
|
|
const currentMapData = Object.values(maps).find(map => |
|
|
map.levels.includes(gameState.level) |
|
|
) || maps.forest; |
|
|
|
|
|
ctx.fillStyle = currentMapData.bg; |
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height); |
|
|
|
|
|
|
|
|
ctx.strokeStyle = 'rgba(255,255,255,0.1)'; |
|
|
ctx.lineWidth = 1; |
|
|
for (let i = 0; i <= gridSize; i++) { |
|
|
ctx.beginPath(); |
|
|
ctx.moveTo(i * gemSize, 0); |
|
|
ctx.lineTo(i * gemSize, canvas.height); |
|
|
ctx.stroke(); |
|
|
|
|
|
ctx.beginPath(); |
|
|
ctx.moveTo(0, i * gemSize); |
|
|
ctx.lineTo(canvas.width, i * gemSize); |
|
|
ctx.stroke(); |
|
|
} |
|
|
|
|
|
|
|
|
for (let row = 0; row < gridSize; row++) { |
|
|
for (let col = 0; col < gridSize; col++) { |
|
|
const gem = gameState.gems[row][col]; |
|
|
if (gem && !gem.isMatched) { |
|
|
drawGem(gem, col * gemSize, row * gemSize); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
gameState.fallingGems.forEach(fallingGem => { |
|
|
drawGem(fallingGem.gem, fallingGem.x, fallingGem.y); |
|
|
}); |
|
|
|
|
|
|
|
|
gameState.animations.forEach(animation => { |
|
|
drawMatchAnimation(animation); |
|
|
}); |
|
|
|
|
|
|
|
|
if (gameState.selectedGem && !gameState.isAnimating) { |
|
|
const { row, col } = gameState.selectedGem; |
|
|
ctx.strokeStyle = '#fff'; |
|
|
ctx.lineWidth = 3; |
|
|
ctx.strokeRect(col * gemSize + 2, row * gemSize + 2, gemSize - 4, gemSize - 4); |
|
|
} |
|
|
} |
|
|
|
|
|
function drawMatchAnimation(animation) { |
|
|
const progress = (Date.now() - animation.startTime) / animation.duration; |
|
|
if (progress >= 1) return; |
|
|
|
|
|
const alpha = 1 - progress; |
|
|
const scale = 1 + progress * 0.5; |
|
|
|
|
|
ctx.save(); |
|
|
ctx.globalAlpha = alpha; |
|
|
ctx.translate(animation.x + gemSize/2, animation.y + gemSize/2); |
|
|
ctx.scale(scale, scale); |
|
|
ctx.translate(-gemSize/2, -gemSize/2); |
|
|
|
|
|
|
|
|
ctx.fillStyle = '#FFD700'; |
|
|
for (let i = 0; i < 8; i++) { |
|
|
const angle = (i / 8) * Math.PI * 2; |
|
|
const x = Math.cos(angle) * 20 * progress; |
|
|
const y = Math.sin(angle) * 20 * progress; |
|
|
ctx.beginPath(); |
|
|
ctx.arc(gemSize/2 + x, gemSize/2 + y, 3, 0, Math.PI * 2); |
|
|
ctx.fill(); |
|
|
} |
|
|
|
|
|
ctx.restore(); |
|
|
} |
|
|
|
|
|
function drawGem(gem, x, y) { |
|
|
const padding = 4; |
|
|
const size = gemSize - padding * 2; |
|
|
const centerX = x + padding + size/2; |
|
|
const centerY = y + padding + size/2; |
|
|
|
|
|
ctx.save(); |
|
|
|
|
|
|
|
|
switch(gem.type) { |
|
|
case 'wood': |
|
|
drawWood(centerX, centerY, size); |
|
|
break; |
|
|
case 'water': |
|
|
drawWaterDrop(centerX, centerY, size); |
|
|
break; |
|
|
case 'snake': |
|
|
drawSnake(centerX, centerY, size); |
|
|
break; |
|
|
case 'bee': |
|
|
drawBee(centerX, centerY, size); |
|
|
break; |
|
|
case 'whale': |
|
|
drawWhale(centerX, centerY, size); |
|
|
break; |
|
|
case 'fish': |
|
|
drawFish(centerX, centerY, size); |
|
|
break; |
|
|
} |
|
|
|
|
|
|
|
|
if (gem.isIce) { |
|
|
ctx.strokeStyle = '#87CEEB'; |
|
|
ctx.lineWidth = 3; |
|
|
ctx.strokeRect(x + padding, y + padding, size, size); |
|
|
|
|
|
|
|
|
ctx.strokeStyle = 'rgba(135, 206, 235, 0.7)'; |
|
|
ctx.lineWidth = 2; |
|
|
ctx.beginPath(); |
|
|
ctx.moveTo(centerX, y + padding); |
|
|
ctx.lineTo(centerX, y + padding + size); |
|
|
ctx.moveTo(x + padding, centerY); |
|
|
ctx.lineTo(x + padding + size, centerY); |
|
|
ctx.moveTo(x + padding + size/4, y + padding + size/4); |
|
|
ctx.lineTo(x + padding + 3*size/4, y + padding + 3*size/4); |
|
|
ctx.moveTo(x + padding + 3*size/4, y + padding + size/4); |
|
|
ctx.lineTo(x + padding + size/4, y + padding + 3*size/4); |
|
|
ctx.stroke(); |
|
|
} |
|
|
|
|
|
ctx.restore(); |
|
|
} |
|
|
|
|
|
function drawWood(centerX, centerY, size) { |
|
|
const radius = size * 0.4; |
|
|
|
|
|
|
|
|
const gradient = ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, radius); |
|
|
gradient.addColorStop(0, '#D2691E'); |
|
|
gradient.addColorStop(1, '#8B4513'); |
|
|
ctx.fillStyle = gradient; |
|
|
ctx.beginPath(); |
|
|
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2); |
|
|
ctx.fill(); |
|
|
|
|
|
|
|
|
ctx.strokeStyle = '#654321'; |
|
|
ctx.lineWidth = 2; |
|
|
for(let i = 1; i <= 3; i++) { |
|
|
ctx.beginPath(); |
|
|
ctx.arc(centerX, centerY, radius * 0.3 * i, 0, Math.PI * 2); |
|
|
ctx.stroke(); |
|
|
} |
|
|
} |
|
|
|
|
|
function drawWaterDrop(centerX, centerY, size) { |
|
|
const radius = size * 0.4; |
|
|
|
|
|
|
|
|
const gradient = ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, radius); |
|
|
gradient.addColorStop(0, '#87CEEB'); |
|
|
gradient.addColorStop(1, '#4682B4'); |
|
|
ctx.fillStyle = gradient; |
|
|
|
|
|
ctx.beginPath(); |
|
|
ctx.arc(centerX, centerY + radius * 0.2, radius * 0.8, 0, Math.PI * 2); |
|
|
ctx.moveTo(centerX, centerY - radius); |
|
|
ctx.quadraticCurveTo(centerX - radius * 0.5, centerY, centerX, centerY + radius * 0.2); |
|
|
ctx.quadraticCurveTo(centerX + radius * 0.5, centerY, centerX, centerY - radius); |
|
|
ctx.fill(); |
|
|
|
|
|
|
|
|
ctx.fillStyle = 'rgba(255, 255, 255, 0.5)'; |
|
|
ctx.beginPath(); |
|
|
ctx.arc(centerX - radius * 0.3, centerY - radius * 0.3, radius * 0.2, 0, Math.PI * 2); |
|
|
ctx.fill(); |
|
|
} |
|
|
|
|
|
function drawSnake(centerX, centerY, size) { |
|
|
const radius = size * 0.15; |
|
|
|
|
|
|
|
|
ctx.strokeStyle = '#228B22'; |
|
|
ctx.lineWidth = radius; |
|
|
ctx.lineCap = 'round'; |
|
|
ctx.beginPath(); |
|
|
ctx.moveTo(centerX - size * 0.3, centerY - size * 0.2); |
|
|
ctx.quadraticCurveTo(centerX + size * 0.2, centerY - size * 0.1, centerX, centerY); |
|
|
ctx.quadraticCurveTo(centerX - size * 0.2, centerY + size * 0.1, centerX + size * 0.3, centerY + size * 0.2); |
|
|
ctx.stroke(); |
|
|
|
|
|
|
|
|
ctx.fillStyle = '#32CD32'; |
|
|
ctx.beginPath(); |
|
|
ctx.arc(centerX + size * 0.3, centerY + size * 0.2, radius * 1.2, 0, Math.PI * 2); |
|
|
ctx.fill(); |
|
|
|
|
|
|
|
|
ctx.fillStyle = '#000'; |
|
|
ctx.beginPath(); |
|
|
ctx.arc(centerX + size * 0.25, centerY + size * 0.15, 2, 0, Math.PI * 2); |
|
|
ctx.arc(centerX + size * 0.35, centerY + size * 0.15, 2, 0, Math.PI * 2); |
|
|
ctx.fill(); |
|
|
} |
|
|
|
|
|
function drawBee(centerX, centerY, size) { |
|
|
const bodyWidth = size * 0.6; |
|
|
const bodyHeight = size * 0.3; |
|
|
|
|
|
|
|
|
ctx.fillStyle = '#FFD700'; |
|
|
ctx.beginPath(); |
|
|
ctx.ellipse(centerX, centerY, bodyWidth/2, bodyHeight/2, 0, 0, Math.PI * 2); |
|
|
ctx.fill(); |
|
|
|
|
|
|
|
|
ctx.fillStyle = '#000'; |
|
|
for(let i = 0; i < 3; i++) { |
|
|
ctx.fillRect(centerX - bodyWidth/2 + i * bodyWidth/3, centerY - bodyHeight/2, bodyWidth/6, bodyHeight); |
|
|
} |
|
|
|
|
|
|
|
|
ctx.fillStyle = 'rgba(255, 255, 255, 0.7)'; |
|
|
ctx.beginPath(); |
|
|
ctx.ellipse(centerX - bodyWidth/4, centerY - bodyHeight/2, bodyWidth/3, bodyHeight/3, -Math.PI/6, 0, Math.PI * 2); |
|
|
ctx.ellipse(centerX + bodyWidth/4, centerY - bodyHeight/2, bodyWidth/3, bodyHeight/3, Math.PI/6, 0, Math.PI * 2); |
|
|
ctx.fill(); |
|
|
|
|
|
|
|
|
ctx.fillStyle = '#FFD700'; |
|
|
ctx.beginPath(); |
|
|
ctx.arc(centerX, centerY - bodyHeight/2, bodyHeight/3, 0, Math.PI * 2); |
|
|
ctx.fill(); |
|
|
} |
|
|
|
|
|
function drawWhale(centerX, centerY, size) { |
|
|
const bodyWidth = size * 0.7; |
|
|
const bodyHeight = size * 0.4; |
|
|
|
|
|
|
|
|
const gradient = ctx.createLinearGradient(centerX - bodyWidth/2, centerY, centerX + bodyWidth/2, centerY); |
|
|
gradient.addColorStop(0, '#4169E1'); |
|
|
gradient.addColorStop(1, '#191970'); |
|
|
ctx.fillStyle = gradient; |
|
|
|
|
|
ctx.beginPath(); |
|
|
ctx.ellipse(centerX, centerY, bodyWidth/2, bodyHeight/2, 0, 0, Math.PI * 2); |
|
|
ctx.fill(); |
|
|
|
|
|
|
|
|
ctx.beginPath(); |
|
|
ctx.moveTo(centerX + bodyWidth/2, centerY); |
|
|
ctx.lineTo(centerX + bodyWidth/2 + size * 0.2, centerY - size * 0.15); |
|
|
ctx.lineTo(centerX + bodyWidth/2 + size * 0.2, centerY + size * 0.15); |
|
|
ctx.closePath(); |
|
|
ctx.fill(); |
|
|
|
|
|
|
|
|
ctx.fillStyle = '#FFF'; |
|
|
ctx.beginPath(); |
|
|
ctx.arc(centerX - bodyWidth/4, centerY - bodyHeight/4, 4, 0, Math.PI * 2); |
|
|
ctx.fill(); |
|
|
ctx.fillStyle = '#000'; |
|
|
ctx.beginPath(); |
|
|
ctx.arc(centerX - bodyWidth/4, centerY - bodyHeight/4, 2, 0, Math.PI * 2); |
|
|
ctx.fill(); |
|
|
|
|
|
|
|
|
ctx.strokeStyle = '#87CEEB'; |
|
|
ctx.lineWidth = 3; |
|
|
ctx.beginPath(); |
|
|
ctx.moveTo(centerX, centerY - bodyHeight/2); |
|
|
ctx.lineTo(centerX, centerY - bodyHeight/2 - size * 0.2); |
|
|
ctx.stroke(); |
|
|
} |
|
|
|
|
|
function drawFish(centerX, centerY, size) { |
|
|
const bodyWidth = size * 0.6; |
|
|
const bodyHeight = size * 0.4; |
|
|
|
|
|
|
|
|
const gradient = ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, bodyWidth/2); |
|
|
gradient.addColorStop(0, '#FFA500'); |
|
|
gradient.addColorStop(1, '#FF6347'); |
|
|
ctx.fillStyle = gradient; |
|
|
|
|
|
ctx.beginPath(); |
|
|
ctx.ellipse(centerX, centerY, bodyWidth/2, bodyHeight/2, 0, 0, Math.PI * 2); |
|
|
ctx.fill(); |
|
|
|
|
|
|
|
|
ctx.beginPath(); |
|
|
ctx.moveTo(centerX + bodyWidth/2, centerY); |
|
|
ctx.lineTo(centerX + bodyWidth/2 + size * 0.25, centerY - size * 0.2); |
|
|
ctx.lineTo(centerX + bodyWidth/2 + size * 0.15, centerY); |
|
|
ctx.lineTo(centerX + bodyWidth/2 + size * 0.25, centerY + size * 0.2); |
|
|
ctx.closePath(); |
|
|
ctx.fill(); |
|
|
|
|
|
|
|
|
ctx.beginPath(); |
|
|
ctx.moveTo(centerX - bodyWidth/4, centerY + bodyHeight/2); |
|
|
ctx.lineTo(centerX - bodyWidth/4 - size * 0.1, centerY + bodyHeight/2 + size * 0.15); |
|
|
ctx.lineTo(centerX, centerY + bodyHeight/2); |
|
|
ctx.closePath(); |
|
|
ctx.fill(); |
|
|
|
|
|
|
|
|
ctx.fillStyle = '#FFF'; |
|
|
ctx.beginPath(); |
|
|
ctx.arc(centerX - bodyWidth/4, centerY - bodyHeight/6, 5, 0, Math.PI * 2); |
|
|
ctx.fill(); |
|
|
ctx.fillStyle = '#000'; |
|
|
ctx.beginPath(); |
|
|
ctx.arc(centerX - bodyWidth/4, centerY - bodyHeight/6, 3, 0, Math.PI * 2); |
|
|
ctx.fill(); |
|
|
|
|
|
|
|
|
ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)'; |
|
|
ctx.lineWidth = 1; |
|
|
for(let i = 0; i < 3; i++) { |
|
|
ctx.beginPath(); |
|
|
ctx.arc(centerX - bodyWidth/4 + i * bodyWidth/6, centerY, bodyWidth/8, 0, Math.PI * 2); |
|
|
ctx.stroke(); |
|
|
} |
|
|
} |
|
|
|
|
|
function darkenColor(color, amount) { |
|
|
const num = parseInt(color.replace("#", ""), 16); |
|
|
const amt = Math.round(2.55 * amount * 100); |
|
|
const R = (num >> 16) - amt; |
|
|
const G = (num >> 8 & 0x00FF) - amt; |
|
|
const B = (num & 0x0000FF) - amt; |
|
|
return "#" + (0x1000000 + (R < 255 ? R < 1 ? 0 : R : 255) * 0x10000 + |
|
|
(G < 255 ? G < 1 ? 0 : G : 255) * 0x100 + |
|
|
(B < 255 ? B < 1 ? 0 : B : 255)).toString(16).slice(1); |
|
|
} |
|
|
|
|
|
|
|
|
function gameLoop() { |
|
|
if (!gameState.isPaused) { |
|
|
updateAnimations(); |
|
|
drawGame(); |
|
|
} |
|
|
requestAnimationFrame(gameLoop); |
|
|
} |
|
|
|
|
|
function updateAnimations() { |
|
|
|
|
|
gameState.fallingGems = gameState.fallingGems.filter(fallingGem => { |
|
|
fallingGem.y += fallingGem.speed; |
|
|
fallingGem.speed += 0.5; |
|
|
|
|
|
const targetY = fallingGem.targetRow * gemSize; |
|
|
if (fallingGem.y >= targetY) { |
|
|
fallingGem.y = targetY; |
|
|
gameState.gems[fallingGem.targetRow][fallingGem.col] = fallingGem.gem; |
|
|
playDropSound(); |
|
|
return false; |
|
|
} |
|
|
return true; |
|
|
}); |
|
|
|
|
|
|
|
|
gameState.animations = gameState.animations.filter(animation => { |
|
|
return (Date.now() - animation.startTime) < animation.duration; |
|
|
}); |
|
|
|
|
|
|
|
|
if (gameState.fallingGems.length === 0 && gameState.animations.length === 0 && gameState.isAnimating) { |
|
|
gameState.isAnimating = false; |
|
|
|
|
|
setTimeout(() => { |
|
|
const newMatches = findMatches(); |
|
|
if (newMatches.length > 0) { |
|
|
processMatches(newMatches); |
|
|
} |
|
|
}, 200); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
canvas.addEventListener('click', function(e) { |
|
|
if (gameState.isPaused) return; |
|
|
|
|
|
const rect = canvas.getBoundingClientRect(); |
|
|
const x = e.clientX - rect.left; |
|
|
const y = e.clientY - rect.top; |
|
|
|
|
|
const col = Math.floor(x / gemSize); |
|
|
const row = Math.floor(y / gemSize); |
|
|
|
|
|
if (row >= 0 && row < gridSize && col >= 0 && col < gridSize) { |
|
|
handleGemClick(row, col); |
|
|
} |
|
|
}); |
|
|
|
|
|
function handleGemClick(row, col) { |
|
|
if (gameState.isAnimating) return; |
|
|
|
|
|
if (!gameState.selectedGem) { |
|
|
gameState.selectedGem = { row, col }; |
|
|
} else { |
|
|
const selected = gameState.selectedGem; |
|
|
if (selected.row === row && selected.col === col) { |
|
|
gameState.selectedGem = null; |
|
|
} else if (isAdjacent(selected, { row, col })) { |
|
|
swapGems(selected, { row, col }); |
|
|
gameState.selectedGem = null; |
|
|
} else { |
|
|
gameState.selectedGem = { row, col }; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
function isAdjacent(gem1, gem2) { |
|
|
const rowDiff = Math.abs(gem1.row - gem2.row); |
|
|
const colDiff = Math.abs(gem1.col - gem2.col); |
|
|
return (rowDiff === 1 && colDiff === 0) || (rowDiff === 0 && colDiff === 1); |
|
|
} |
|
|
|
|
|
function swapGems(gem1, gem2) { |
|
|
const temp = gameState.gems[gem1.row][gem1.col]; |
|
|
gameState.gems[gem1.row][gem1.col] = gameState.gems[gem2.row][gem2.col]; |
|
|
gameState.gems[gem2.row][gem2.col] = temp; |
|
|
|
|
|
const matches = findMatches(); |
|
|
if (matches.length > 0) { |
|
|
gameState.moves--; |
|
|
processMatches(matches); |
|
|
updateUI(); |
|
|
showToast('Great match!'); |
|
|
} else { |
|
|
|
|
|
const temp = gameState.gems[gem1.row][gem1.col]; |
|
|
gameState.gems[gem1.row][gem1.col] = gameState.gems[gem2.row][gem2.col]; |
|
|
gameState.gems[gem2.row][gem2.col] = temp; |
|
|
showToast('No matches found!'); |
|
|
} |
|
|
} |
|
|
|
|
|
function findMatches() { |
|
|
const matches = []; |
|
|
|
|
|
|
|
|
for (let row = 0; row < gridSize; row++) { |
|
|
let count = 1; |
|
|
let currentType = gameState.gems[row][0].type; |
|
|
for (let col = 1; col < gridSize; col++) { |
|
|
if (gameState.gems[row][col].type === currentType) { |
|
|
count++; |
|
|
} else { |
|
|
if (count >= 3) { |
|
|
for (let i = col - count; i < col; i++) { |
|
|
matches.push({ row, col: i }); |
|
|
} |
|
|
} |
|
|
count = 1; |
|
|
currentType = gameState.gems[row][col].type; |
|
|
} |
|
|
} |
|
|
if (count >= 3) { |
|
|
for (let i = gridSize - count; i < gridSize; i++) { |
|
|
matches.push({ row, col: i }); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
for (let col = 0; col < gridSize; col++) { |
|
|
let count = 1; |
|
|
let currentType = gameState.gems[0][col].type; |
|
|
for (let row = 1; row < gridSize; row++) { |
|
|
if (gameState.gems[row][col].type === currentType) { |
|
|
count++; |
|
|
} else { |
|
|
if (count >= 3) { |
|
|
for (let i = row - count; i < row; i++) { |
|
|
matches.push({ row: i, col }); |
|
|
} |
|
|
} |
|
|
count = 1; |
|
|
currentType = gameState.gems[row][col].type; |
|
|
} |
|
|
} |
|
|
if (count >= 3) { |
|
|
for (let i = gridSize - count; i < gridSize; i++) { |
|
|
matches.push({ row: i, col }); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
return matches; |
|
|
} |
|
|
|
|
|
function processMatches(matches) { |
|
|
let iceDestroyed = 0; |
|
|
gameState.isAnimating = true; |
|
|
|
|
|
|
|
|
matches.forEach(match => { |
|
|
const gem = gameState.gems[match.row][match.col]; |
|
|
if (gem.isIce) { |
|
|
iceDestroyed++; |
|
|
playIceBreakSound(); |
|
|
} |
|
|
|
|
|
|
|
|
gameState.animations.push({ |
|
|
x: match.col * gemSize, |
|
|
y: match.row * gemSize, |
|
|
startTime: Date.now(), |
|
|
duration: 500 |
|
|
}); |
|
|
|
|
|
|
|
|
gameState.gems[match.row][match.col].isMatched = true; |
|
|
}); |
|
|
|
|
|
|
|
|
playMatchSound(); |
|
|
|
|
|
|
|
|
gameState.iceCount += iceDestroyed; |
|
|
gameState.score += matches.length * 100 + iceDestroyed * 200; |
|
|
|
|
|
|
|
|
setTimeout(() => { |
|
|
applyGravity(); |
|
|
fillEmptySpaces(); |
|
|
}, 300); |
|
|
|
|
|
if (gameState.iceCount >= gameState.iceTarget) { |
|
|
setTimeout(() => { |
|
|
levelComplete(); |
|
|
}, 1000); |
|
|
} |
|
|
} |
|
|
|
|
|
function applyGravity() { |
|
|
|
|
|
for (let col = 0; col < gridSize; col++) { |
|
|
const column = []; |
|
|
|
|
|
|
|
|
for (let row = gridSize - 1; row >= 0; row--) { |
|
|
if (!gameState.gems[row][col].isMatched) { |
|
|
column.push(gameState.gems[row][col]); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
for (let row = 0; row < gridSize; row++) { |
|
|
gameState.gems[row][col] = null; |
|
|
} |
|
|
|
|
|
|
|
|
column.forEach((gem, index) => { |
|
|
const newRow = gridSize - 1 - index; |
|
|
const currentRow = findGemCurrentRow(gem, col); |
|
|
|
|
|
if (currentRow !== newRow && currentRow !== -1) { |
|
|
gameState.fallingGems.push({ |
|
|
gem: gem, |
|
|
x: col * gemSize, |
|
|
y: currentRow * gemSize, |
|
|
targetRow: newRow, |
|
|
col: col, |
|
|
speed: 2 |
|
|
}); |
|
|
} else { |
|
|
gameState.gems[newRow][col] = gem; |
|
|
} |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
function findGemCurrentRow(targetGem, col) { |
|
|
for (let row = 0; row < gridSize; row++) { |
|
|
if (gameState.gems[row][col] === targetGem) { |
|
|
return row; |
|
|
} |
|
|
} |
|
|
return -1; |
|
|
} |
|
|
|
|
|
function fillEmptySpaces() { |
|
|
|
|
|
for (let col = 0; col < gridSize; col++) { |
|
|
for (let row = 0; row < gridSize; row++) { |
|
|
if (!gameState.gems[row][col]) { |
|
|
const newGem = { |
|
|
type: gemTypes[Math.floor(Math.random() * gemTypes.length)], |
|
|
isIce: false, |
|
|
isMatched: false |
|
|
}; |
|
|
|
|
|
|
|
|
gameState.fallingGems.push({ |
|
|
gem: newGem, |
|
|
x: col * gemSize, |
|
|
y: -gemSize * (gridSize - row), |
|
|
targetRow: row, |
|
|
col: col, |
|
|
speed: 2 |
|
|
}); |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
function levelComplete() { |
|
|
showToast('Level Complete!'); |
|
|
gameState.level++; |
|
|
if (gameState.level > 30) { |
|
|
showToast('Congratulations! Game Complete!'); |
|
|
gameState.level = 30; |
|
|
} else { |
|
|
setTimeout(() => { |
|
|
initializeNewLevel(); |
|
|
}, 2000); |
|
|
} |
|
|
} |
|
|
|
|
|
function initializeNewLevel() { |
|
|
gameState.moves = 25; |
|
|
gameState.iceCount = 0; |
|
|
gameState.iceTarget = Math.min(28 + gameState.level * 2, 50); |
|
|
updateMapTheme(); |
|
|
createGemGrid(); |
|
|
updateUI(); |
|
|
} |
|
|
|
|
|
function updateMapTheme() { |
|
|
let currentMap = 'forest'; |
|
|
if (gameState.level >= 11 && gameState.level <= 20) { |
|
|
currentMap = 'desert'; |
|
|
} else if (gameState.level >= 21) { |
|
|
currentMap = 'skylands'; |
|
|
} |
|
|
|
|
|
gameState.currentMap = currentMap; |
|
|
document.getElementById('map-name').textContent = maps[currentMap].name; |
|
|
} |
|
|
|
|
|
|
|
|
function updateUI() { |
|
|
document.getElementById('score').textContent = gameState.score; |
|
|
document.getElementById('moves-count').textContent = gameState.moves; |
|
|
document.getElementById('ice-count').textContent = `${gameState.iceCount}/${gameState.iceTarget}`; |
|
|
document.getElementById('player-level').textContent = gameState.level; |
|
|
document.getElementById('level-progress').textContent = `Level ${gameState.level}/30`; |
|
|
} |
|
|
|
|
|
|
|
|
function pauseGame() { |
|
|
gameState.isPaused = true; |
|
|
document.getElementById('pauseBtn').style.display = 'none'; |
|
|
document.getElementById('playBtn').style.display = 'block'; |
|
|
showToast('Game Paused'); |
|
|
} |
|
|
|
|
|
function playGame() { |
|
|
gameState.isPaused = false; |
|
|
document.getElementById('pauseBtn').style.display = 'block'; |
|
|
document.getElementById('playBtn').style.display = 'none'; |
|
|
showToast('Game Resumed'); |
|
|
} |
|
|
|
|
|
|
|
|
function showModal(type) { |
|
|
const modal = document.getElementById('modal'); |
|
|
const title = document.getElementById('modal-title'); |
|
|
const body = document.getElementById('modal-body'); |
|
|
|
|
|
const modalContent = { |
|
|
shop: { |
|
|
title: 'ποΈ Shop', |
|
|
body: '<p>Buy power-ups and boosters!</p><div style="margin: 20px 0;"><div>π Gems: 150</div><div>β‘ Energy Boost - 50 gems</div><div>π₯ Fire Boost - 75 gems</div><div>βοΈ Ice Breaker - 100 gems</div></div>' |
|
|
}, |
|
|
equip: { |
|
|
title: 'βοΈ Equipment', |
|
|
body: '<p>Manage your power-ups and equipment!</p><div style="margin: 20px 0;"><div>π‘οΈ Equipped: Basic Hammer</div><div>β‘ Energy Boost: Ready</div><div>π₯ Fire Power: Cooldown 2 moves</div></div>' |
|
|
}, |
|
|
friends: { |
|
|
title: 'π₯ Friends', |
|
|
body: '<p>Connect with friends and send lives!</p><div style="margin: 20px 0;"><div>π’ Alex_Gamer - Online</div><div>π‘ Sarah_123 - 5 min ago</div><div>π΄ Mike_Pro - 1 hour ago</div><button style="margin-top: 10px; padding: 5px 15px; background: #4ecdc4; border: none; border-radius: 15px; color: white;">Invite Friends</button></div>' |
|
|
}, |
|
|
tasks: { |
|
|
title: 'π Daily Tasks', |
|
|
body: '<p>Complete tasks for rewards!</p><div style="margin: 20px 0;"><div>β
Play 3 levels - Completed</div><div>β³ Crush 50 ice blocks - 28/50</div><div>β³ Score 5000 points - 0/5000</div><div>β³ Use 5 power-ups - 0/5</div></div>' |
|
|
}, |
|
|
rating: { |
|
|
title: 'π Leaderboard', |
|
|
body: '<p>Global Rankings</p><div style="margin: 20px 0;"><div>π₯ ProGamer99 - 45,230</div><div>π₯ MatchMaster - 42,100</div><div>π₯ GemCrusher - 38,950</div><div style="color: #4ecdc4;">π
You - 0 (Rank: 15,432)</div></div>' |
|
|
}, |
|
|
boosters: { |
|
|
title: 'π Boosters', |
|
|
body: '<p>Activate powerful boosters!</p><div style="margin: 20px 0;"><div>β‘ Color Bomb - 3 left</div><div>πͺοΈ Line Blast - 2 left</div><div>βοΈ Ice Breaker - 1 left</div><div>π₯ Fire Storm - 0 left</div></div>' |
|
|
}, |
|
|
skins: { |
|
|
title: 'π¨ Skins & Themes', |
|
|
body: '<p>Customize your game appearance!</p><div style="margin: 20px 0;"><div>π² Forest Theme - Active</div><div>ποΈ Desert Theme - Unlocked</div><div>βοΈ Sky Theme - Locked</div><div>π Galaxy Theme - Locked</div></div>' |
|
|
} |
|
|
}; |
|
|
|
|
|
const content = modalContent[type]; |
|
|
title.innerHTML = `<h2>${content.title}</h2>`; |
|
|
body.innerHTML = content.body; |
|
|
modal.style.display = 'block'; |
|
|
} |
|
|
|
|
|
function closeModal() { |
|
|
document.getElementById('modal').style.display = 'none'; |
|
|
} |
|
|
|
|
|
|
|
|
function showToast(message) { |
|
|
const toast = document.getElementById('toast'); |
|
|
toast.textContent = message; |
|
|
toast.classList.add('show'); |
|
|
|
|
|
setTimeout(() => { |
|
|
toast.classList.remove('show'); |
|
|
}, 3000); |
|
|
} |
|
|
|
|
|
|
|
|
window.onclick = function(event) { |
|
|
const modal = document.getElementById('modal'); |
|
|
if (event.target === modal) { |
|
|
closeModal(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
initGame(); |
|
|
initAudio(); |
|
|
showToast('Welcome to AvaRush!'); |
|
|
</script> |
|
|
</body> |
|
|
</html> |