|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>Plants vs Zombies Clone</title> |
|
<style> |
|
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap'); |
|
|
|
* { |
|
margin: 0; |
|
padding: 0; |
|
box-sizing: border-box; |
|
} |
|
|
|
body { |
|
background-color: #2c3e50; |
|
font-family: 'Press Start 2P', cursive; |
|
overflow: hidden; |
|
color: #ecf0f1; |
|
} |
|
|
|
#game-container { |
|
width: 100vw; |
|
height: 100vh; |
|
position: relative; |
|
background-image: linear-gradient(#5da130, #3b7a1f); |
|
} |
|
|
|
#lawn { |
|
width: 100%; |
|
height: calc(100% - 120px); |
|
position: relative; |
|
display: grid; |
|
grid-template-columns: repeat(9, 1fr); |
|
grid-template-rows: repeat(5, 1fr); |
|
} |
|
|
|
.lawn-cell { |
|
border: 1px dashed rgba(255, 255, 255, 0.2); |
|
position: relative; |
|
} |
|
|
|
#ui-container { |
|
height: 120px; |
|
background-color: #4a2512; |
|
border-top: 4px solid #6d4724; |
|
display: flex; |
|
justify-content: space-between; |
|
padding: 10px; |
|
} |
|
|
|
#sun-counter { |
|
width: 100px; |
|
height: 100px; |
|
background-color: #f1c40f; |
|
border-radius: 50%; |
|
display: flex; |
|
justify-content: center; |
|
align-items: center; |
|
font-size: 24px; |
|
color: #e67e22; |
|
box-shadow: 0 0 10px rgba(241, 196, 15, 0.5); |
|
position: relative; |
|
} |
|
|
|
#plant-selection { |
|
display: flex; |
|
gap: 10px; |
|
} |
|
|
|
.plant-option { |
|
width: 80px; |
|
height: 100px; |
|
background-color: #3498db; |
|
border-radius: 10px; |
|
display: flex; |
|
flex-direction: column; |
|
justify-content: space-between; |
|
align-items: center; |
|
padding: 5px; |
|
cursor: pointer; |
|
transition: all 0.2s; |
|
border: 3px solid transparent; |
|
} |
|
|
|
.plant-option:hover { |
|
transform: scale(1.05); |
|
} |
|
|
|
.plant-option.selected { |
|
border-color: #f1c40f; |
|
box-shadow: 0 0 10px rgba(241, 196, 15, 0.8); |
|
} |
|
|
|
.plant-option .plant-icon { |
|
width: 50px; |
|
height: 50px; |
|
border-radius: 50%; |
|
background-size: contain; |
|
background-repeat: no-repeat; |
|
background-position: center; |
|
} |
|
|
|
.plant-option .cost { |
|
font-size: 12px; |
|
color: #f1c40f; |
|
} |
|
|
|
|
|
.plant { |
|
width: 80%; |
|
height: 80%; |
|
position: absolute; |
|
top: 10%; |
|
left: 10%; |
|
background-size: contain; |
|
background-repeat: no-repeat; |
|
background-position: center; |
|
z-index: 10; |
|
} |
|
|
|
.sunflower .plant { |
|
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50"><circle cx="25" cy="25" r="20" fill="yellow"/><circle cx="25" cy="25" r="10" fill="orange"/><path d="M10,25 Q25,10 40,25 Q25,40 10,25" fill="yellow"/><path d="M25,10 Q40,25 25,40 Q10,25 25,10" fill="yellow"/></svg>'); |
|
} |
|
|
|
.peashooter .plant { |
|
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50"><ellipse cx="25" cy="30" rx="15" ry="20" fill="green"/><circle cx="25" cy="15" r="10" fill="lime"/><circle cx="30" cy="12" r="3" fill="black"/></svg>'); |
|
} |
|
|
|
.wallnut .plant { |
|
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50"><circle cx="25" cy="25" r="20" fill="saddlebrown"/><path d="M15,15 Q25,10 35,15 Q40,25 35,35 Q25,40 15,35 Q10,25 15,15" fill="peru"/></svg>'); |
|
} |
|
|
|
|
|
.pea { |
|
width: 15px; |
|
height: 15px; |
|
background-color: lime; |
|
border-radius: 50%; |
|
position: absolute; |
|
z-index: 20; |
|
} |
|
|
|
|
|
.zombie { |
|
width: 60px; |
|
height: 100px; |
|
position: absolute; |
|
right: -60px; |
|
background-size: contain; |
|
background-repeat: no-repeat; |
|
background-position: center; |
|
z-index: 5; |
|
animation: zombie-walk 3s linear infinite; |
|
} |
|
|
|
@keyframes zombie-walk { |
|
0% { transform: translateX(0); } |
|
50% { transform: translateX(-5px) rotate(2deg); } |
|
100% { transform: translateX(0); } |
|
} |
|
|
|
.basic-zombie { |
|
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50"><circle cx="25" cy="15" r="10" fill="darkgreen"/><rect x="15" y="25" width="20" height="15" fill="green"/><circle cx="20" cy="12" r="2" fill="black"/><circle cx="30" cy="12" r="2" fill="black"/></svg>'); |
|
} |
|
|
|
.conehead-zombie { |
|
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50"><path d="M15,5 L35,5 L25,15 Z" fill="gray"/><circle cx="25" cy="15" r="10" fill="darkgreen"/><rect x="15" y="25" width="20" height="15" fill="green"/><circle cx="20" cy="12" r="2" fill="black"/><circle cx="30" cy="12" r="2" fill="black"/></svg>'); |
|
} |
|
|
|
.buckethead-zombie { |
|
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50"><circle cx="25" cy="15" r="10" fill="darkgreen"/><rect x="20" y="15" width="10" height="10" fill="silver"/><rect x="15" y="25" width="20" height="15" fill="green"/><circle cx="20" cy="12" r="2" fill="black"/><circle cx="30" cy="12" r="2" fill="black"/></svg>'); |
|
} |
|
|
|
|
|
.sun { |
|
width: 50px; |
|
height: 50px; |
|
position: absolute; |
|
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50"><circle cx="25" cy="25" r="15" fill="yellow"/></svg>'); |
|
background-size: contain; |
|
cursor: pointer; |
|
z-index: 30; |
|
animation: sun-fall 6s linear, sun-spin 2s linear infinite; |
|
} |
|
|
|
@keyframes sun-fall { |
|
0% { transform: translateY(-100px); opacity: 0; } |
|
10% { opacity: 1; } |
|
90% { opacity: 1; } |
|
100% { transform: translateY(calc(100vh - 170px)); opacity: 0; } |
|
} |
|
|
|
@keyframes sun-spin { |
|
from { transform: rotate(0deg); } |
|
to { transform: rotate(360deg); } |
|
} |
|
|
|
|
|
.game-message { |
|
position: absolute; |
|
top: 50%; |
|
left: 50%; |
|
transform: translate(-50%, -50%); |
|
font-size: 2rem; |
|
background-color: rgba(0, 0, 0, 0.7); |
|
padding: 20px 40px; |
|
border-radius: 10px; |
|
z-index: 100; |
|
display: none; |
|
} |
|
|
|
#start-screen { |
|
display: flex; |
|
flex-direction: column; |
|
align-items: center; |
|
justify-content: center; |
|
gap: 20px; |
|
} |
|
|
|
#start-screen h1 { |
|
font-size: 3rem; |
|
color: #f1c40f; |
|
text-shadow: 5px 5px #e67e22; |
|
} |
|
|
|
#start-button { |
|
padding: 15px 30px; |
|
background-color: #e74c3c; |
|
border: none; |
|
border-radius: 10px; |
|
font-family: 'Press Start 2P', cursive; |
|
font-size: 1.2rem; |
|
color: white; |
|
cursor: pointer; |
|
transition: all 0.2s; |
|
} |
|
|
|
#start-button:hover { |
|
transform: scale(1.1); |
|
background-color: #c0392b; |
|
} |
|
|
|
#level-select { |
|
display: flex; |
|
gap: 20px; |
|
} |
|
|
|
.level-button { |
|
padding: 10px 20px; |
|
background-color: #3498db; |
|
border: none; |
|
border-radius: 5px; |
|
font-family: 'Press Start 2P', cursive; |
|
font-size: 0.8rem; |
|
color: white; |
|
cursor: pointer; |
|
} |
|
|
|
.level-button:hover { |
|
background-color: #2980b9; |
|
} |
|
|
|
|
|
.health-bar { |
|
width: 100%; |
|
height: 5px; |
|
background-color: #2ecc71; |
|
position: absolute; |
|
bottom: -10px; |
|
left: 0; |
|
} |
|
|
|
#wave-indicator { |
|
position: absolute; |
|
top: 10px; |
|
right: 10px; |
|
background-color: rgba(0, 0, 0, 0.5); |
|
padding: 5px 10px; |
|
border-radius: 5px; |
|
z-index: 50; |
|
} |
|
|
|
|
|
.cooldown-overlay { |
|
position: absolute; |
|
bottom: 0; |
|
left: 0; |
|
width: 100%; |
|
background-color: rgba(0, 0, 0, 0.5); |
|
transition: height 0.5s linear; |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<div id="game-container"> |
|
<div id="start-screen" class="game-message"> |
|
<h1>PLANTS vs ZOMBIES</h1> |
|
<div id="level-select"> |
|
<button class="level-button" data-level="1">Backyard Day</button> |
|
<button class="level-button" data-level="2">Backyard Night</button> |
|
</div> |
|
<button id="start-button">START GAME</button> |
|
</div> |
|
|
|
<div id="game-over-message" class="game-message"> |
|
<h2>GAME OVER</h2> |
|
<p id="game-over-text"></p> |
|
<button id="restart-button">TRY AGAIN</button> |
|
</div> |
|
|
|
<div id="wave-indicator">Wave: 1</div> |
|
|
|
<div id="lawn"></div> |
|
|
|
<div id="ui-container"> |
|
<div id="sun-counter">0</div> |
|
<div id="plant-selection"> |
|
<div class="plant-option" data-plant="sunflower" data-cost="50"> |
|
<div class="plant-icon" style="background-image: url('data:image/svg+xml;utf8,<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 50 50\"><circle cx=\"25\" cy=\"25\" r=\"20\" fill=\"yellow\"/><circle cx=\"25\" cy=\"25\" r=\"10\" fill=\"orange\"/></svg>')"></div> |
|
<div class="cost">50</div> |
|
</div> |
|
<div class="plant-option" data-plant="peashooter" data-cost="100"> |
|
<div class="plant-icon" style="background-image: url('data:image/svg+xml;utf8,<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 50 50\"><ellipse cx=\"25\" cy=\"30\" rx=\"15\" ry=\"20\" fill=\"green\"/><circle cx=\"25\" cy=\"15\" r=\"10\" fill=\"lime\"/></svg>')"></div> |
|
<div class="cost">100</div> |
|
</div> |
|
<div class="plant-option" data-plant="wallnut" data-cost="50"> |
|
<div class="plant-icon" style="background-image: url('data:image/svg+xml;utf8,<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 50 50\"><circle cx=\"25\" cy=\"25\" r=\"20\" fill=\"saddlebrown\"/></svg>')"></div> |
|
<div class="cost">50</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<script> |
|
|
|
const gameState = { |
|
suns: 50, |
|
selectedPlant: null, |
|
plants: [], |
|
zombies: [], |
|
projectiles: [], |
|
sunDrops: [], |
|
currentWave: 0, |
|
totalWaves: 5, |
|
gameActive: false, |
|
currentLevel: 1, |
|
zombieSpeed: 0.5, |
|
gameClock: 0, |
|
lastSunProduced: 0, |
|
lastZombieSpawned: 0, |
|
plantCooldowns: { |
|
sunflower: 0, |
|
peashooter: 0, |
|
wallnut: 0 |
|
}, |
|
zombiesReachedEnd: 0 |
|
}; |
|
|
|
|
|
const plantProperties = { |
|
sunflower: { |
|
cost: 50, |
|
health: 100, |
|
cooldown: 10, |
|
produceSunInterval: 10, |
|
produceSunAmount: 25 |
|
}, |
|
peashooter: { |
|
cost: 100, |
|
health: 100, |
|
cooldown: 7.5, |
|
shootInterval: 3, |
|
damage: 20 |
|
}, |
|
wallnut: { |
|
cost: 50, |
|
health: 400, |
|
cooldown: 30, |
|
damage: 0 |
|
} |
|
}; |
|
|
|
|
|
const zombieProperties = { |
|
basic: { |
|
health: 100, |
|
damage: 1, |
|
speed: 0.5, |
|
value: 10 |
|
}, |
|
conehead: { |
|
health: 200, |
|
damage: 1, |
|
speed: 0.4, |
|
value: 20 |
|
}, |
|
buckethead: { |
|
health: 300, |
|
damage: 2, |
|
speed: 0.3, |
|
value: 30 |
|
} |
|
}; |
|
|
|
|
|
const levels = { |
|
1: { |
|
name: "Backyard Day", |
|
background: "#5da130", |
|
zombieTypes: ["basic", "conehead"], |
|
waves: [ |
|
{ delay: 5, zombies: 5, zombieTypeWeights: [0.8, 0.2] }, |
|
{ delay: 15, zombies: 8, zombieTypeWeights: [0.7, 0.3] }, |
|
{ delay: 25, zombies: 12, zombieTypeWeights: [0.6, 0.4] }, |
|
{ delay: 35, zombies: 15, zombieTypeWeights: [0.5, 0.5] }, |
|
{ delay: 45, zombies: 20, zombieTypeWeights: [0.4, 0.6] } |
|
] |
|
}, |
|
2: { |
|
name: "Backyard Night", |
|
background: "#1a2330", |
|
zombieTypes: ["basic", "conehead", "buckethead"], |
|
waves: [ |
|
{ delay: 5, zombies: 6, zombieTypeWeights: [0.6, 0.3, 0.1] }, |
|
{ delay: 15, zombies: 10, zombieTypeWeights: [0.5, 0.3, 0.2] }, |
|
{ delay: 25, zombies: 14, zombieTypeWeights: [0.4, 0.3, 0.3] }, |
|
{ delay: 35, zombies: 18, zombieTypeWeights: [0.3, 0.3, 0.4] }, |
|
{ delay: 45, zombies: 25, zombieTypeWeights: [0.2, 0.3, 0.5] } |
|
] |
|
} |
|
}; |
|
|
|
|
|
const gameContainer = document.getElementById('game-container'); |
|
const lawn = document.getElementById('lawn'); |
|
const sunCounter = document.getElementById('sun-counter'); |
|
const plantSelection = document.getElementById('plant-selection'); |
|
const plantOptions = document.querySelectorAll('.plant-option'); |
|
const startScreen = document.getElementById('start-screen'); |
|
const gameOverMessage = document.getElementById('game-over-message'); |
|
const gameOverText = document.getElementById('game-over-text'); |
|
const startButton = document.getElementById('start-button'); |
|
const restartButton = document.getElementById('restart-button'); |
|
const levelButtons = document.querySelectorAll('.level-button'); |
|
const waveIndicator = document.getElementById('wave-indicator'); |
|
|
|
|
|
function initLawn() { |
|
lawn.innerHTML = ''; |
|
for (let i = 0; i < 5; i++) { |
|
for (let j = 0; j < 9; j++) { |
|
const cell = document.createElement('div'); |
|
cell.className = 'lawn-cell'; |
|
cell.dataset.row = i; |
|
cell.dataset.col = j; |
|
cell.addEventListener('click', placePlant); |
|
lawn.appendChild(cell); |
|
} |
|
} |
|
} |
|
|
|
|
|
function initEventListeners() { |
|
plantOptions.forEach(option => { |
|
option.addEventListener('click', () => { |
|
const plantType = option.dataset.plant; |
|
const plantCost = parseInt(option.dataset.cost); |
|
|
|
if (gameState.plantCooldowns[plantType] > 0) return; |
|
|
|
if (gameState.selectedPlant === plantType) { |
|
gameState.selectedPlant = null; |
|
plantOptions.forEach(opt => opt.classList.remove('selected')); |
|
} else if (gameState.suns >= plantCost) { |
|
gameState.selectedPlant = plantType; |
|
plantOptions.forEach(opt => opt.classList.remove('selected')); |
|
option.classList.add('selected'); |
|
} |
|
}); |
|
}); |
|
|
|
document.addEventListener('click', (e) => { |
|
if (e.target === gameContainer || e.target === lawn) { |
|
gameState.selectedPlant = null; |
|
plantOptions.forEach(opt => opt.classList.remove('selected')); |
|
} |
|
}); |
|
|
|
startButton.addEventListener('click', startGame); |
|
restartButton.addEventListener('click', resetGame); |
|
|
|
levelButtons.forEach(button => { |
|
button.addEventListener('click', () => { |
|
const level = parseInt(button.dataset.level); |
|
selectLevel(level); |
|
}); |
|
}); |
|
} |
|
|
|
|
|
function selectLevel(level) { |
|
gameState.currentLevel = level; |
|
levelButtons.forEach(button => { |
|
button.style.backgroundColor = button.dataset.level == level ? '#2980b9' : '#3498db'; |
|
}); |
|
} |
|
|
|
|
|
function startGame() { |
|
if (gameState.gameActive) return; |
|
|
|
gameState.gameActive = true; |
|
gameState.suns = 50; |
|
gameState.plants = []; |
|
gameState.zombies = []; |
|
gameState.projectiles = []; |
|
gameState.sunDrops = []; |
|
gameState.currentWave = 0; |
|
gameState.gameClock = 0; |
|
gameState.lastSunProduced = 0; |
|
gameState.lastZombieSpawned = 0; |
|
gameState.zombiesReachedEnd = 0; |
|
|
|
|
|
for (const plantType in gameState.plantCooldowns) { |
|
gameState.plantCooldowns[plantType] = 0; |
|
} |
|
|
|
|
|
gameContainer.style.backgroundImage = `linear-gradient(${levels[gameState.currentLevel].background}, #3b7a1f)`; |
|
|
|
|
|
startScreen.style.display = 'none'; |
|
gameOverMessage.style.display = 'none'; |
|
|
|
|
|
gameLoop(); |
|
|
|
|
|
setInterval(createRandomSun, 10000); |
|
} |
|
|
|
|
|
function resetGame() { |
|
gameState.gameActive = false; |
|
|
|
|
|
document.querySelectorAll('.plant, .zombie, .pea, .sun').forEach(el => el.remove()); |
|
|
|
|
|
startScreen.style.display = 'flex'; |
|
gameOverMessage.style.display = 'none'; |
|
} |
|
|
|
|
|
function placePlant(e) { |
|
if (!gameState.selectedPlant || !gameState.gameActive) return; |
|
|
|
const row = parseInt(e.currentTarget.dataset.row); |
|
const col = parseInt(e.currentTarget.dataset.col); |
|
|
|
|
|
const cellOccupied = gameState.plants.some(plant => plant.row === row && plant.col === col); |
|
if (cellOccupied) return; |
|
|
|
const plantType = gameState.selectedPlant; |
|
const plantCost = plantProperties[plantType].cost; |
|
|
|
if (gameState.suns >= plantCost) { |
|
|
|
gameState.suns -= plantCost; |
|
updateSunCounter(); |
|
|
|
|
|
const plant = { |
|
type: plantType, |
|
row: row, |
|
col: col, |
|
health: plantProperties[plantType].health, |
|
lastActionTime: 0, |
|
element: createPlantElement(plantType, row, col) |
|
}; |
|
|
|
gameState.plants.push(plant); |
|
|
|
|
|
gameState.plantCooldowns[plantType] = plantProperties[plantType].cooldown; |
|
updateCooldownDisplay(plantType); |
|
|
|
|
|
gameState.selectedPlant = null; |
|
plantOptions.forEach(opt => opt.classList.remove('selected')); |
|
} |
|
} |
|
|
|
|
|
function createPlantElement(plantType, row, col) { |
|
const plant = document.createElement('div'); |
|
plant.className = `plant ${plantType}`; |
|
|
|
|
|
const healthBar = document.createElement('div'); |
|
healthBar.className = 'health-bar'; |
|
healthBar.style.width = '100%'; |
|
plant.appendChild(healthBar); |
|
|
|
|
|
const cell = document.querySelector(`.lawn-cell[data-row="${row}"][data-col="${col}"]`); |
|
cell.appendChild(plant); |
|
|
|
return plant; |
|
} |
|
|
|
|
|
function createZombie(type, row) { |
|
const zombie = document.createElement('div'); |
|
zombie.className = `zombie ${type}-zombie`; |
|
|
|
|
|
const healthBar = document.createElement('div'); |
|
healthBar.className = 'health-bar'; |
|
healthBar.style.width = '100%'; |
|
zombie.appendChild(healthBar); |
|
|
|
|
|
const cellHeight = lawn.clientHeight / 5; |
|
zombie.style.top = `${row * cellHeight + cellHeight / 2 - 50}px`; |
|
|
|
gameContainer.appendChild(zombie); |
|
|
|
|
|
const zombieObj = { |
|
type: type, |
|
row: row, |
|
x: gameContainer.clientWidth, |
|
element: zombie, |
|
health: zombieProperties[type].health, |
|
maxHealth: zombieProperties[type].health, |
|
speed: zombieProperties[type].speed * (gameState.currentLevel === 2 ? 0.8 : 1), |
|
damage: zombieProperties[type].damage, |
|
moving: true |
|
}; |
|
|
|
gameState.zombies.push(zombieObj); |
|
|
|
return zombieObj; |
|
} |
|
|
|
|
|
function createProjectile(x, y) { |
|
const pea = document.createElement('div'); |
|
pea.className = 'pea'; |
|
pea.style.left = `${x}px`; |
|
pea.style.top = `${y}px`; |
|
gameContainer.appendChild(pea); |
|
|
|
const projectile = { |
|
x: x, |
|
y: y, |
|
speed: 5, |
|
damage: 20, |
|
element: pea |
|
}; |
|
|
|
gameState.projectiles.push(projectile); |
|
return projectile; |
|
} |
|
|
|
|
|
function createSunDrop(x, y, autoCollect = false) { |
|
const sun = document.createElement('div'); |
|
sun.className = 'sun'; |
|
sun.style.left = `${x}px`; |
|
sun.style.top = `${y}px`; |
|
|
|
if (autoCollect) { |
|
|
|
sun.style.animation = 'sun-bounce 4s ease-in-out, sun-spin 2s linear infinite'; |
|
} else { |
|
|
|
sun.style.animation = 'sun-fall 6s linear, sun-spin 2s linear infinite'; |
|
} |
|
|
|
sun.addEventListener('click', collectSun); |
|
gameContainer.appendChild(sun); |
|
|
|
const sunDrop = { |
|
x: x, |
|
y: y, |
|
element: sun, |
|
autoCollect: autoCollect, |
|
collected: false, |
|
value: 25 |
|
}; |
|
|
|
gameState.sunDrops.push(sunDrop); |
|
|
|
if (autoCollect) { |
|
|
|
setTimeout(() => { |
|
if (!sunDrop.collected) { |
|
collectSun({ target: sun }); |
|
} |
|
}, 4000); |
|
} else { |
|
|
|
setTimeout(() => { |
|
if (!sunDrop.collected) { |
|
sun.remove(); |
|
gameState.sunDrops = gameState.sunDrops.filter(s => s !== sunDrop); |
|
} |
|
}, 6000); |
|
} |
|
|
|
return sunDrop; |
|
} |
|
|
|
|
|
function createRandomSun() { |
|
if (!gameState.gameActive) return; |
|
|
|
const x = Math.random() * (gameContainer.clientWidth - 100) + 50; |
|
createSunDrop(x, -50); |
|
} |
|
|
|
|
|
function collectSun(e) { |
|
e.stopPropagation(); |
|
|
|
const sunElement = e.target; |
|
const sunDrop = gameState.sunDrops.find(sun => sun.element === sunElement); |
|
|
|
if (sunDrop && !sunDrop.collected) { |
|
sunDrop.collected = true; |
|
gameState.suns += sunDrop.value; |
|
updateSunCounter(); |
|
|
|
|
|
sunElement.style.transition = 'all 0.3s ease-out'; |
|
sunElement.style.transform = 'translateY(-20px) scale(1.5)'; |
|
sunElement.style.opacity = '0'; |
|
|
|
|
|
setTimeout(() => { |
|
sunElement.remove(); |
|
gameState.sunDrops = gameState.sunDrops.filter(s => s !== sunDrop); |
|
}, 300); |
|
} |
|
} |
|
|
|
|
|
function updateSunCounter() { |
|
sunCounter.textContent = gameState.suns; |
|
|
|
|
|
plantOptions.forEach(option => { |
|
const plantType = option.dataset.plant; |
|
const plantCost = parseInt(option.dataset.cost); |
|
|
|
if (gameState.suns < plantCost || gameState.plantCooldowns[plantType] > 0) { |
|
option.style.opacity = '0.5'; |
|
} else { |
|
option.style.opacity = '1'; |
|
} |
|
}); |
|
} |
|
|
|
|
|
function updateCooldownDisplay(plantType) { |
|
const option = document.querySelector(`.plant-option[data-plant="${plantType}"]`); |
|
if (!option) return; |
|
|
|
|
|
const existingOverlay = option.querySelector('.cooldown-overlay'); |
|
if (existingOverlay) existingOverlay.remove(); |
|
|
|
|
|
const overlay = document.createElement('div'); |
|
overlay.className = 'cooldown-overlay'; |
|
overlay.style.height = '100%'; |
|
option.appendChild(overlay); |
|
|
|
|
|
const cooldown = plantProperties[plantType].cooldown; |
|
const startTime = Date.now(); |
|
|
|
const animateCooldown = () => { |
|
const elapsed = (Date.now() - startTime) / 1000; |
|
const remaining = Math.max(0, cooldown - elapsed); |
|
|
|
overlay.style.height = `${(remaining / cooldown) * 100}%`; |
|
|
|
if (remaining > 0) { |
|
gameState.plantCooldowns[plantType] = remaining; |
|
requestAnimationFrame(animateCooldown); |
|
} else { |
|
gameState.plantCooldowns[plantType] = 0; |
|
overlay.remove(); |
|
|
|
|
|
updateSunCounter(); |
|
} |
|
}; |
|
|
|
requestAnimationFrame(animateCooldown); |
|
} |
|
|
|
|
|
function gameLoop() { |
|
if (!gameState.gameActive) return; |
|
|
|
|
|
gameState.gameClock += 0.016; |
|
|
|
|
|
checkWaveCompletion(); |
|
|
|
|
|
updateZombies(); |
|
|
|
|
|
updatePlants(); |
|
|
|
|
|
updateProjectiles(); |
|
|
|
|
|
checkGameOver(); |
|
|
|
|
|
requestAnimationFrame(gameLoop); |
|
} |
|
|
|
|
|
function checkWaveCompletion() { |
|
const level = levels[gameState.currentLevel]; |
|
|
|
|
|
if (gameState.currentWave < level.waves.length) { |
|
const currentWaveData = level.waves[gameState.currentWave]; |
|
|
|
if (gameState.gameClock >= currentWaveData.delay && |
|
gameState.lastZombieSpawned + 1 < gameState.gameClock && |
|
gameState.zombies.length < 5 + gameState.currentWave) { |
|
|
|
|
|
const zombieType = getRandomZombieType(currentWaveData.zombieTypeWeights); |
|
const row = Math.floor(Math.random() * 5); |
|
createZombie(zombieType, row); |
|
|
|
|
|
gameState.lastZombieSpawned = gameState.gameClock; |
|
|
|
|
|
if (gameState.zombies.length === 1) { |
|
waveIndicator.textContent = `Wave: ${gameState.currentWave + 1}/${level.waves.length}`; |
|
} |
|
} |
|
|
|
|
|
const zombiesSpawnedThisWave = gameState.zombies.filter(z => |
|
z.spawnTime >= currentWaveData.delay - 1 |
|
).length; |
|
|
|
if (zombiesSpawnedThisWave >= currentWaveData.zombies && |
|
gameState.zombies.length === 0 && |
|
gameState.currentWave < level.waves.length - 1) { |
|
|
|
|
|
gameState.currentWave++; |
|
} |
|
} |
|
} |
|
|
|
|
|
function getRandomZombieType(weights) { |
|
const randomValue = Math.random(); |
|
let cumulativeWeight = 0; |
|
|
|
for (let i = 0; i < weights.length; i++) { |
|
cumulativeWeight += weights[i]; |
|
if (randomValue < cumulativeWeight) { |
|
return levels[gameState.currentLevel].zombieTypes[i]; |
|
} |
|
} |
|
|
|
return levels[gameState.currentLevel].zombieTypes[0]; |
|
} |
|
|
|
|
|
function updateZombies() { |
|
const cellWidth = lawn.clientWidth / 9; |
|
const cellHeight = lawn.clientHeight / 5; |
|
|
|
for (let i = gameState.zombies.length - 1; i >= 0; i--) { |
|
const zombie = gameState.zombies[i]; |
|
|
|
if (zombie.moving) { |
|
|
|
zombie.x -= zombie.speed; |
|
zombie.element.style.left = `${zombie.x}px`; |
|
|
|
|
|
if (zombie.x < -zombie.element.clientWidth) { |
|
gameState.zombiesReachedEnd++; |
|
zombie.element.remove(); |
|
gameState.zombies.splice(i, 1); |
|
continue; |
|
} |
|
|
|
|
|
const zombieRect = { |
|
left: zombie.x, |
|
right: zombie.x + zombie.element.clientWidth, |
|
top: parseInt(zombie.element.style.top), |
|
bottom: parseInt(zombie.element.style.top) + zombie.element.clientHeight |
|
}; |
|
|
|
|
|
const plantsInRow = gameState.plants.filter(p => p.row === zombie.row); |
|
|
|
for (const plant of plantsInRow) { |
|
const cell = document.querySelector(`.lawn-cell[data-row="${plant.row}"][data-col="${plant.col}"]`); |
|
const rect = cell.getBoundingClientRect(); |
|
|
|
const plantRect = { |
|
left: rect.left - gameContainer.getBoundingClientRect().left, |
|
right: rect.right - gameContainer.getBoundingClientRect().left, |
|
top: rect.top - gameContainer.getBoundingClientRect().top, |
|
bottom: rect.bottom - gameContainer.getBoundingClientRect().top |
|
}; |
|
|
|
|
|
if (zombieRect.right > plantRect.left && zombieRect.left < plantRect.right && |
|
zombieRect.bottom > plantRect.top && zombieRect.top < plantRect.bottom) { |
|
|
|
zombie.moving = false; |
|
|
|
|
|
plant.health -= zombie.damage / 60; |
|
updatePlantHealth(plant); |
|
|
|
|
|
if (Math.random() < 0.02) { |
|
zombie.element.style.transform = 'scaleX(1.1)'; |
|
setTimeout(() => { |
|
if (zombie.element) { |
|
zombie.element.style.transform = 'scaleX(1)'; |
|
} |
|
}, 100); |
|
} |
|
|
|
|
|
if (plant.health <= 0) { |
|
plant.element.remove(); |
|
gameState.plants = gameState.plants.filter(p => p !== plant); |
|
zombie.moving = true; |
|
} |
|
|
|
break; |
|
} |
|
} |
|
} |
|
|
|
|
|
updateZombieHealth(zombie); |
|
} |
|
} |
|
|
|
|
|
function updatePlants() { |
|
for (const plant of gameState.plants) { |
|
|
|
if (plant.type === 'sunflower') { |
|
if (gameState.gameClock - plant.lastActionTime >= plantProperties.sunflower.produceSunInterval) { |
|
plant.lastActionTime = gameState.gameClock; |
|
|
|
|
|
const cell = document.querySelector(`.lawn-cell[data-row="${plant.row}"][data-col="${plant.col}"]`); |
|
const rect = cell.getBoundingClientRect(); |
|
|
|
const x = rect.left - gameContainer.getBoundingClientRect().left + rect.width / 2; |
|
const y = rect.top - gameContainer.getBoundingClientRect().top + rect.height / 2; |
|
|
|
createSunDrop(x, y, true); |
|
} |
|
} |
|
|
|
|
|
if (plant.type === 'peashooter' && gameState.zombies.some(z => z.row === plant.row)) { |
|
if (gameState.gameClock - plant.lastActionTime >= plantProperties.peashooter.shootInterval) { |
|
plant.lastActionTime = gameState.gameClock; |
|
|
|
|
|
const cell = document.querySelector(`.lawn-cell[data-row="${plant.row}"][data-col="${plant.col}"]`); |
|
const rect = cell.getBoundingClientRect(); |
|
|
|
const x = rect.right - gameContainer.getBoundingClientRect().left; |
|
const y = rect.top - gameContainer.getBoundingClientRect().top + rect.height / 2; |
|
|
|
createProjectile(x, y); |
|
} |
|
} |
|
|
|
|
|
updatePlantHealth(plant); |
|
} |
|
} |
|
|
|
|
|
function updateProjectiles() { |
|
for (let i = gameState.projectiles.length - 1; i >= 0; i--) { |
|
const projectile = gameState.projectiles[i]; |
|
|
|
|
|
projectile.x += projectile.speed; |
|
projectile.element.style.left = `${projectile.x}px`; |
|
|
|
|
|
if (projectile.x > gameContainer.clientWidth) { |
|
projectile.element.remove(); |
|
gameState.projectiles.splice(i, 1); |
|
continue; |
|
} |
|
|
|
|
|
const peaRect = { |
|
left: projectile.x, |
|
right: projectile.x + projectile.element.clientWidth, |
|
top: parseInt(projectile.element.style.top), |
|
bottom: parseInt(projectile.element.style.top) + projectile.element.clientHeight |
|
}; |
|
|
|
for (const zombie of gameState.zombies) { |
|
const zombieRect = { |
|
left: zombie.x, |
|
right: zombie.x + zombie.element.clientWidth, |
|
top: parseInt(zombie.element.style.top), |
|
bottom: parseInt(zombie.element.style.top) + zombie.element.clientHeight |
|
}; |
|
|
|
|
|
if (peaRect.right > zombieRect.left && peaRect.left < zombieRect.right && |
|
peaRect.bottom > zombieRect.top && peaRect.top < zombieRect.bottom) { |
|
|
|
|
|
zombie.health -= projectile.damage; |
|
|
|
|
|
updateZombieHealth(zombie); |
|
|
|
|
|
projectile.element.remove(); |
|
gameState.projectiles.splice(i, 1); |
|
|
|
|
|
if (zombie.health <= 0) { |
|
|
|
gameState.suns += zombieProperties[zombie.type].value; |
|
updateSunCounter(); |
|
|
|
|
|
zombie.element.remove(); |
|
gameState.zombies = gameState.zombies.filter(z => z !== zombie); |
|
} |
|
|
|
break; |
|
} |
|
} |
|
} |
|
} |
|
|
|
|
|
function updatePlantHealth(plant) { |
|
const healthPercent = Math.max(0, plant.health / plantProperties[plant.type].health * 100); |
|
const healthBar = plant.element.querySelector('.health-bar'); |
|
|
|
if (healthBar) { |
|
healthBar.style.width = `${healthPercent}%`; |
|
healthBar.style.backgroundColor = healthPercent > 50 ? '#2ecc71' : |
|
healthPercent > 25 ? '#f39c12' : |
|
'#e74c3c'; |
|
} |
|
} |
|
|
|
|
|
function updateZombieHealth(zombie) { |
|
const healthPercent = Math.max(0, zombie.health / zombie.maxHealth * 100); |
|
const healthBar = zombie.element.querySelector('.health-bar'); |
|
|
|
if (healthBar) { |
|
healthBar.style.width = `${healthPercent}%`; |
|
|
|
|
|
if (healthPercent < 50) { |
|
zombie.element.style.filter = 'brightness(0.8)'; |
|
} |
|
if (healthPercent < 25) { |
|
zombie.element.style.filter = 'brightness(0.6) hue-rotate(30deg)'; |
|
} |
|
} |
|
} |
|
|
|
|
|
function checkGameOver() { |
|
|
|
if (gameState.zombiesReachedEnd >= 5) { |
|
endGame(false); |
|
return; |
|
} |
|
|
|
|
|
const level = levels[gameState.currentLevel]; |
|
if (gameState.currentWave >= level.waves.length - 1 && |
|
gameState.zombies.length === 0 && |
|
gameState.gameClock - gameState.lastZombieSpawned > 5) { |
|
|
|
endGame(true); |
|
return; |
|
} |
|
} |
|
|
|
|
|
function endGame(victory) { |
|
gameState.gameActive = false; |
|
|
|
gameOverText.textContent = victory ? |
|
`You survived the zombie attack! Final score: ${gameState.suns}` : |
|
`The zombies ate your brains! You survived ${gameState.currentWave + 1} waves.`; |
|
|
|
gameOverMessage.style.display = 'flex'; |
|
} |
|
|
|
|
|
function initGame() { |
|
initLawn(); |
|
initEventListeners(); |
|
|
|
|
|
selectLevel(1); |
|
|
|
|
|
startScreen.style.display = 'flex'; |
|
} |
|
|
|
|
|
window.onload = initGame; |
|
</script> |
|
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: absolute; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">This website has been generated by <a href="https://enzostvs-deepsite.hf.space" style="color: #fff;" target="_blank" >DeepSite</a> <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;"></p></body> |
|
</html> |