| 
 | 1 | +<!DOCTYPE html>  | 
 | 2 | +<html lang="en">  | 
 | 3 | +<head>  | 
 | 4 | +<meta charset="UTF-8" />  | 
 | 5 | +<meta name="viewport" content="width=device-width, initial-scale=1.0"/>  | 
 | 6 | +<title>PvZ Browser Game</title>  | 
 | 7 | +<style>  | 
 | 8 | +  body { margin: 0; overflow: hidden; background: #222; color: white; font-family: Arial; }  | 
 | 9 | +  canvas { display: block; background: #333; margin: 0 auto; }  | 
 | 10 | +  #overlay {  | 
 | 11 | +    position: absolute; top: 10px; left: 10px;  | 
 | 12 | +    background: rgba(0,0,0,0.7); padding: 10px; border: 2px solid #fff;  | 
 | 13 | +  }  | 
 | 14 | +  #health-bar {  | 
 | 15 | +    width: 200px; height: 20px; background: #555; margin-bottom: 10px;  | 
 | 16 | +  }  | 
 | 17 | +  #health-fill { height: 100%; width: 100%; background: red; }  | 
 | 18 | +</style>  | 
 | 19 | +</head>  | 
 | 20 | +<body>  | 
 | 21 | + | 
 | 22 | +<div id="overlay">  | 
 | 23 | +  <div id="health-bar"><div id="health-fill"></div></div>  | 
 | 24 | +  <p>Wave: <span id="wave">1</span></p>  | 
 | 25 | +  <p>Coins: <span id="coins">0</span></p>  | 
 | 26 | +  <button onclick="startNextWave()">Start Wave</button>  | 
 | 27 | +</div>  | 
 | 28 | + | 
 | 29 | +<canvas id="game" width="800" height="600"></canvas>  | 
 | 30 | + | 
 | 31 | +<!-- Sound effects -->  | 
 | 32 | +<audio id="shoot-sfx" src="https://freesound.org/data/previews/341/341695_6263744-lq.mp3"></audio>  | 
 | 33 | +<audio id="zombie-dead-sfx" src="https://freesound.org/data/previews/341/341668_6263744-lq.mp3"></audio>  | 
 | 34 | + | 
 | 35 | +<script>  | 
 | 36 | +const canvas = document.getElementById("game"), ctx = canvas.getContext("2d");  | 
 | 37 | +const shootSfx = document.getElementById("shoot-sfx"),  | 
 | 38 | +      deadSfx = document.getElementById("zombie-dead-sfx");  | 
 | 39 | + | 
 | 40 | +let player = { x:400, y:300, size:20, speed:3, damage:1, maxHealth:100, health:100 };  | 
 | 41 | +let zombies = [], bullets = [];  | 
 | 42 | +let keys = {}, coins = 0, wave = 0, zombieSpeed = 1.0, waveActive = false;  | 
 | 43 | + | 
 | 44 | +// UI hooks  | 
 | 45 | +const healthFill = document.getElementById("health-fill");  | 
 | 46 | +const waveDisplay = document.getElementById("wave");  | 
 | 47 | +const coinsDisplay = document.getElementById("coins");  | 
 | 48 | + | 
 | 49 | +function updateUI(){  | 
 | 50 | +  healthFill.style.width = (player.health / player.maxHealth * 100) + "%";  | 
 | 51 | +  waveDisplay.textContent = wave;  | 
 | 52 | +  coinsDisplay.textContent = coins;  | 
 | 53 | +}  | 
 | 54 | + | 
 | 55 | +function spawnWave(){  | 
 | 56 | +  wave++;  | 
 | 57 | +  zombies = [];  | 
 | 58 | +  bullets = [];  | 
 | 59 | +  zombieSpeed = 1 + wave * 0.2;  | 
 | 60 | +  for(let i=0; i<wave*5; i++){  | 
 | 61 | +    let edge = Math.floor(Math.random()*4);  | 
 | 62 | +    let x = edge===0?0:edge===1?canvas.width:Math.random()*canvas.width;  | 
 | 63 | +    let y = edge===2?0:edge===3?canvas.height:Math.random()*canvas.height;  | 
 | 64 | +    zombies.push({ x, y, size:20, hp:3 + wave, id: Date.now()+i });  | 
 | 65 | +  }  | 
 | 66 | +  waveActive = true;  | 
 | 67 | +  updateUI();  | 
 | 68 | +}  | 
 | 69 | + | 
 | 70 | +function startNextWave(){ if(!waveActive) spawnWave(); }  | 
 | 71 | + | 
 | 72 | +function update(){  | 
 | 73 | +  if(player.health<=0){ waveActive = false; return; }  | 
 | 74 | +  if(keys.w) player.y -= player.speed;  | 
 | 75 | +  if(keys.s) player.y += player.speed;  | 
 | 76 | +  if(keys.a) player.x -= player.speed;  | 
 | 77 | +  if(keys.d) player.x += player.speed;  | 
 | 78 | +  player.x = Math.max(0,Math.min(canvas.width,player.x));  | 
 | 79 | +  player.y = Math.max(0,Math.min(canvas.height,player.y));  | 
 | 80 | + | 
 | 81 | +  zombies.forEach((z,i) => {  | 
 | 82 | +    let dx = player.x - z.x, dy = player.y - z.y;  | 
 | 83 | +    let dist = Math.hypot(dx,dy);  | 
 | 84 | +    if(dist < z.size + player.size){  | 
 | 85 | +      player.health -= 0.3;  | 
 | 86 | +      if(player.health < 0) player.health = 0;  | 
 | 87 | +    } else {  | 
 | 88 | +      z.x += (dx / dist)*zombieSpeed;  | 
 | 89 | +      z.y += (dy / dist)*zombieSpeed;  | 
 | 90 | +    }  | 
 | 91 | +    if(z.hp <= 0){  | 
 | 92 | +      zombies.splice(i,1);  | 
 | 93 | +      coins += 5;  | 
 | 94 | +      deadSfx.currentTime = 0;  | 
 | 95 | +      deadSfx.play();  | 
 | 96 | +      updateUI();  | 
 | 97 | +    }  | 
 | 98 | +  });  | 
 | 99 | + | 
 | 100 | +  bullets.forEach((b, bi) => {  | 
 | 101 | +    b.x += b.dx*5; b.y += b.dy*5;  | 
 | 102 | +    if(b.x <0||b.x>canvas.width||b.y<0||b.y>canvas.height){  | 
 | 103 | +      bullets.splice(bi,1);  | 
 | 104 | +      return;  | 
 | 105 | +    }  | 
 | 106 | +    zombies.forEach((z, zi) => {  | 
 | 107 | +      if(Math.hypot(b.x-z.x, b.y-z.y) < z.size){  | 
 | 108 | +        z.hp -= player.damage;  | 
 | 109 | +        bullets.splice(bi,1);  | 
 | 110 | +      }  | 
 | 111 | +    });  | 
 | 112 | +  });  | 
 | 113 | + | 
 | 114 | +  if(waveActive && zombies.length===0){  | 
 | 115 | +    waveActive = false;  | 
 | 116 | +  }  | 
 | 117 | +}  | 
 | 118 | + | 
 | 119 | +function draw(){  | 
 | 120 | +  ctx.clearRect(0,0,canvas.width,canvas.height);  | 
 | 121 | +  ctx.fillStyle = player.color || 'cyan';  | 
 | 122 | +  ctx.beginPath(); ctx.arc(player.x,player.y,player.size,0,2*Math.PI); ctx.fill();  | 
 | 123 | +  zombies.forEach(z => {  | 
 | 124 | +    ctx.fillStyle = "green";  | 
 | 125 | +    ctx.beginPath(); ctx.arc(z.x,z.y,z.size,0,2*Math.PI); ctx.fill();  | 
 | 126 | +  });  | 
 | 127 | +  bullets.forEach(b => {  | 
 | 128 | +    ctx.fillStyle = "yellow";  | 
 | 129 | +    ctx.beginPath(); ctx.arc(b.x,b.y,5,0,2*Math.PI); ctx.fill();  | 
 | 130 | +  });  | 
 | 131 | +}  | 
 | 132 | + | 
 | 133 | +function gameLoop(){  | 
 | 134 | +  update(); draw();  | 
 | 135 | +  if(player.health <= 0){  | 
 | 136 | +    ctx.fillStyle='red';  | 
 | 137 | +    ctx.font='48px sans-serif';  | 
 | 138 | +    ctx.fillText('Game Over', 300,300);  | 
 | 139 | +  } else {  | 
 | 140 | +    requestAnimationFrame(gameLoop);  | 
 | 141 | +  }  | 
 | 142 | +}  | 
 | 143 | +gameLoop();  | 
 | 144 | + | 
 | 145 | +document.addEventListener("keydown",e=>keys[e.key.toLowerCase()]=true);  | 
 | 146 | +document.addEventListener("keyup",e=>keys[e.key.toLowerCase()]=false);  | 
 | 147 | +canvas.addEventListener("click",e=>{  | 
 | 148 | +  if(!waveActive) return;  | 
 | 149 | +  let angle = Math.atan2(e.clientY-player.y, e.clientX-player.x);  | 
 | 150 | +  bullets.push({ x:player.x, y:player.y, dx:Math.cos(angle), dy:Math.sin(angle) });  | 
 | 151 | +  shootSfx.currentTime = 0; shootSfx.play();  | 
 | 152 | +});  | 
 | 153 | + | 
 | 154 | + | 
 | 155 | + | 
 | 156 | +updateUI();  | 
 | 157 | +</script>  | 
 | 158 | + | 
 | 159 | +</body>  | 
 | 160 | +</html>  | 
0 commit comments