1
100%
$0
30 / 30
ZOMBIE DEFENSE
Thành phố đã thất thủ.
Hãy bảo vệ căn cứ trước từng đợt zombie.
Di chuột để ngắm
Giữ chuột để bắn
Thay đạn
Hạ zombie nhận tiền
UPGRADE
$0
DAMAGE
Tăng sát thương
$300
FIRE RATE
Bắn nhanh hơn
$400
BASE REPAIR
+30 HP căn cứ
$250
BASE DESTROYED
Bạn sống sót đến
Wave
1
LV.1
LV.1
(function(){
"use strict";
/* ========================================================= ELEMENTS ========================================================= */
const arena = document.getElementById("zdArena");
const zombieLayer = document.getElementById("zdZombieLayer");
const bulletLayer = document.getElementById("zdBulletLayer");
const effectLayer = document.getElementById("zdEffectLayer");
const gun = document.getElementById("zdGun");
const crosshair = document.getElementById("zdCrosshair");
const waveEl = document.getElementById("zdWave");
const moneyEl = document.getElementById("zdMoney");
const ammoEl = document.getElementById("zdAmmo");
const baseBar = document.getElementById("zdBaseBar");
const baseText = document.getElementById("zdBaseText");
const statusEl = document.getElementById("zdStatus");
const startScreen = document.getElementById("zdStart");
const shop = document.getElementById("zdShop");
const gameOver = document.getElementById("zdGameOver");
const startBtn = document.getElementById("zdStartBtn");
const restartBtn = document.getElementById("zdRestart");
const nextWaveBtn = document.getElementById("zdNextWave");
const reloadUI = document.getElementById("zdReload");
const reloadBar = document.getElementById("zdReloadBar");
/* ========================================================= GAME STATE ========================================================= */
let playing = false;
let wave = 1;
let money = 0;
let baseHP = 100;
let maxBaseHP = 100;
let damageLevel = 1;
let fireLevel = 1;
let damage = 25;
let fireDelay = 210;
let ammo = 30;
const maxAmmo = 30;
let reloading = false;
let shooting = false;
let zombies = [];
let bullets = [];
let zombieCount = 0;
let spawned = 0;
let killed = 0;
let lastSpawn = 0;
let lastShot = 0;
let lastTime = 0;
let mouseX = 0;
let mouseY = 0;
let gunAngle = 0;
let animationId;
/* ========================================================= UPDATE UI ========================================================= */
function updateUI(){
waveEl.textContent = wave;
moneyEl.textContent = "$"+ money.toLocaleString();
ammoEl.textContent = ammo+" / "+maxAmmo;
const hpPercent = Math.max( 0, baseHP/maxBaseHP*100 );
baseBar.style.width = hpPercent+"%";
baseText.textContent = Math.round(hpPercent)+"%";
if(hpPercent > 55){
baseBar.style.background = "#42d66b";
} else if(hpPercent > 25){
baseBar.style.background = "#e5b43d";
} else{
baseBar.style.background = "#d83c3c";
}
document.getElementById( "zdDamageLevel" ).textContent = "LV."+damageLevel;
document.getElementById( "zdFireLevel" ).textContent = "LV."+fireLevel;
}
/* ========================================================= AIM ========================================================= */
function aim(clientX,clientY){
if(!playing) return;
const rect = arena.getBoundingClientRect();
mouseX = clientX-rect.left;
mouseY = clientY-rect.top;
crosshair.style.display = "block";
crosshair.style.left = mouseX+"px";
crosshair.style.top = mouseY+"px";
const gunX = arena.clientWidth/2;
const gunY = arena.clientHeight-75;
const dx = mouseX-gunX;
const dy = mouseY-gunY;
let angle =
Math.atan2( dy, dx ) *180/Math.PI +90;
angle = Math.max( -75, Math.min( 75, angle ) );
gunAngle = angle;
gun.style.transform = "rotate(" +angle+ "deg)";
}
/* ========================================================= MOUSE ========================================================= */
arena.addEventListener( "mousemove", e=>{
aim( e.clientX, e.clientY );
});
arena.addEventListener( "mousedown", e=>{
if(!playing) return;
shooting = true;
});
window.addEventListener( "mouseup", ()=>{
shooting = false;
});
/* ========================================================= TOUCH ========================================================= */
arena.addEventListener( "touchstart", e=>{
if(!playing) return;
const t = e.touches[0];
aim( t.clientX, t.clientY );
shooting = true;
}, { passive:true });
arena.addEventListener( "touchmove", e=>{
if(!playing) return;
const t = e.touches[0];
aim( t.clientX, t.clientY );
}, { passive:true });
arena.addEventListener( "touchend", ()=>{
shooting = false;
});
/* ========================================================= RELOAD ========================================================= */
function reload(){
if( reloading || ammo===maxAmmo || !playing ) return;
reloading = true;
shooting = false;
reloadUI.style.display = "block";
reloadBar.style.width = "0%";
const start = performance.now();
const duration = 1300;
function reloadAnim(now){
const progress =
Math.min( 1, (now-start)/duration );
reloadBar.style.width = progress*100+"%";
if(progress < 1){ requestAnimationFrame( reloadAnim ); } else{ ammo = maxAmmo; reloading = false; reloadUI.style.display = "none"; updateUI(); } } requestAnimationFrame( reloadAnim ); } /* R KEY */ document.addEventListener( "keydown", e=>{
if( e.key.toLowerCase()==="r" ){
reload();
}
});
/* ========================================================= SHOOT ========================================================= */
function shoot(time){
if( reloading || ammo<=0 ){ if(ammo<=0) reload(); return; } if( time-lastShot < fireDelay ) return; lastShot = time; ammo--; updateUI(); const rad = ( gunAngle-90 ) * Math.PI/180; const startX = arena.clientWidth/2; const startY = arena.clientHeight-110; const speed = 16; const bulletEl = document.createElement( "div" ); bulletEl.className = "zd-bullet"; bulletLayer.appendChild( bulletEl ); bullets.push({ x:startX, y:startY, vx: Math.cos(rad)*speed, vy: Math.sin(rad)*speed, damage:damage, el:bulletEl }); gun.style.filter = "brightness(2)"; setTimeout( ()=>{
gun.style.filter = "";
}, 45 );
if(ammo<=0){ setTimeout( reload, 150 ); } } /* ========================================================= CREATE ZOMBIE HTML ========================================================= */ function zombieHTML(){ return `
`;
}
/* ========================================================= SPAWN ZOMBIE ========================================================= */
function spawnZombie(){
let type = "normal";
const random = Math.random();
if( wave>=3 && random>.72 ){
type="fast";
}
if( wave>=4 && random>.88 ){
type="tank";
}
if( wave%5===0 && spawned===zombieCount-1 ){
type="boss";
}
let hp; let speed; let reward; let damageBase; let cssClass="";
if(type==="normal"){
hp = 55 + wave*8;
speed = .35 + wave*.015;
reward = 55;
damageBase = 10;
}
if(type==="fast"){
hp = 45 + wave*6;
speed = .7 + wave*.02;
reward = 80;
damageBase = 8;
cssClass = " zd-fast";
}
if(type==="tank"){
hp = 180 + wave*18;
speed = .24;
reward = 180;
damageBase = 22;
cssClass = " zd-tank";
}
if(type==="boss"){
hp = 900 + wave*100;
speed = .18;
reward = 1000;
damageBase = 45;
cssClass = " zd-boss";
statusEl.textContent = "⚠ BOSS ZOMBIE ĐANG ĐẾN!";
}
const el = document.createElement( "div" );
el.className = "zd-zombie walking" +cssClass;
el.innerHTML = zombieHTML();
zombieLayer.appendChild( el );
let size = 1;
if(type==="fast") size=.85;
const x =
12 + Math.random()*76;
const zombie = {
type:type,
hp:hp,
maxHP:hp,
speed:speed,
reward:reward,
baseDamage: damageBase,
x: arena.clientWidth* (x/100),
y: type==="boss" ? -160 : -100,
scale:size,
el:el,
hpEl: el.querySelector( ".zd-z-hp span" )
};
zombies.push( zombie );
spawned++;
}
/* ========================================================= HIT TEST ========================================================= */
function collision( bullet, zombie ){
let width = 54*zombie.scale;
let height = 88*zombie.scale;
if(zombie.type==="tank"){
width=85; height=115;
}
if(zombie.type==="boss"){
width=130; height=150;
}
return(
bullet.x > zombie.x-width/2
&&
bullet.x < zombie.x+width/2 && bullet.y > zombie.y
&&
bullet.y < zombie.y+height ); } /* ========================================================= DAMAGE ========================================================= */ function damageZombie( zombie, bullet ){ let dealt = bullet.damage; /* HEADSHOT AREA */ const relativeY = bullet.y-zombie.y; if(relativeY < 32){ dealt *= 2; scoreText( bullet.x, bullet.y, "HEADSHOT!" ); } zombie.hp -= dealt; zombie.hpEl.style.width = Math.max( 0, zombie.hp/ zombie.maxHP* 100 ) +"%"; zombie.el.classList.add( "hit" ); setTimeout( ()=>{
if(zombie.el) zombie.el.classList.remove( "hit" );
}, 70 );
hitEffect( bullet.x, bullet.y );
bloodEffect( bullet.x, bullet.y );
if(zombie.hp<=0){ killZombie( zombie ); } } /* ========================================================= KILL ========================================================= */ function killZombie(zombie){ money += zombie.reward; killed++; scoreText( zombie.x, zombie.y, "+$"+zombie.reward ); zombie.el.remove(); const index = zombies.indexOf( zombie ); if(index!==-1){ zombies.splice( index, 1 ); } updateUI(); if( killed>=zombieCount && zombies.length===0 ){
setTimeout( waveComplete, 700 );
}
}
/* ========================================================= EFFECTS ========================================================= */
function hitEffect(x,y){
const el = document.createElement( "div" );
el.className = "zd-hit-effect";
el.style.left = x+"px";
el.style.top = y+"px";
effectLayer.appendChild( el );
setTimeout( ()=>el.remove(), 300 );
}
function bloodEffect(x,y){
const el = document.createElement( "div" );
el.className = "zd-blood";
el.style.left = x+"px";
el.style.top = y+"px";
effectLayer.appendChild( el );
setTimeout( ()=>el.remove(), 500 );
}
function scoreText( x, y, text ){
const el = document.createElement( "div" );
el.className = "zd-score";
el.textContent = text;
el.style.left = x+"px";
el.style.top = y+"px";
effectLayer.appendChild( el );
setTimeout( ()=>el.remove(), 850 );
}
/* ========================================================= BASE DAMAGE ========================================================= */
function zombieReachedBase( zombie ){
baseHP -= zombie.baseDamage;
baseHP = Math.max( 0, baseHP );
zombie.el.remove();
const index = zombies.indexOf( zombie );
if(index!==-1){
zombies.splice( index, 1 );
}
killed++;
statusEl.textContent = "⚠ Căn cứ đang bị tấn công!";
updateUI();
arena.style.filter = "brightness(.7) sepia(.4)";
setTimeout( ()=>{
arena.style.filter = "";
}, 100 );
if(baseHP<=0){ endGame(); return; } if( killed>=zombieCount && zombies.length===0 ){
setTimeout( waveComplete, 500 );
}
}
/* ========================================================= WAVE ========================================================= */
function beginWave(){
playing = true;
spawned = 0;
killed = 0;
zombieCount =
6 + wave*3;
if(wave%5===0){
zombieCount += 1;
}
lastSpawn = performance.now();
statusEl.textContent =
wave%5===0
? "⚠ BOSS WAVE!"
: "Zombie đang tiến đến...";
shop.style.display = "none";
crosshair.style.display = "block";
updateUI();
}
/* ========================================================= WAVE COMPLETE ========================================================= */
function waveComplete(){
if(!playing) return;
playing = false;
shooting = false;
crosshair.style.display = "none";
money += 150 + wave*50;
updateUI();
document.getElementById( "zdShopMoney" ).textContent =
"$"+ money.toLocaleString();
updateShop();
shop.style.display = "flex";
}
/* ========================================================= SHOP ========================================================= */
function damagePrice(){
return( 300 + (damageLevel-1)*250 );
}
function speedPrice(){
return( 400 + (fireLevel-1)*300 );
}
function updateShop(){
document.getElementById( "zdDamagePrice" ).textContent = "$"+damagePrice();
document.getElementById( "zdSpeedPrice" ).textContent = "$"+speedPrice();
document.getElementById( "zdShopMoney" ).textContent = "$"+ money.toLocaleString();
}
document.querySelectorAll( ".zd-upgrade" ).forEach( button=>{
button.addEventListener( "click", function(){
const type = this.dataset.upgrade;
if(type==="damage"){
const price = damagePrice();
if(money < price) return; money -= price; damageLevel++; damage += 12; } if(type==="speed"){ const price = speedPrice(); if(money < price) return; money -= price; fireLevel++; fireDelay = Math.max( 70, fireDelay-25 ); } if(type==="base"){ if(money<250) return; if(baseHP>=maxBaseHP) return;
money -= 250;
baseHP =
Math.min( maxBaseHP, baseHP+30 );
}
updateUI();
updateShop();
});
});
/* ========================================================= NEXT WAVE ========================================================= */
nextWaveBtn.addEventListener( "click", function(){
wave++;
ammo = maxAmmo;
updateUI();
beginWave();
});
/* ========================================================= GAME OVER ========================================================= */
function endGame(){
playing = false;
shooting = false;
crosshair.style.display = "none";
document.getElementById( "zdFinalWave" ).textContent = wave;
gameOver.style.display = "flex";
}
/* ========================================================= START GAME ========================================================= */
function startGame(){
cancelAnimationFrame( animationId );
zombieLayer.innerHTML = "";
bulletLayer.innerHTML = "";
effectLayer.innerHTML = "";
zombies=[]; bullets=[];
wave=1;
money=0;
baseHP=100; maxBaseHP=100;
damageLevel=1; fireLevel=1;
damage=25;
fireDelay=210;
ammo=30;
reloading=false;
startScreen.style.display = "none";
gameOver.style.display = "none";
shop.style.display = "none";
updateUI();
beginWave();
lastTime = performance.now();
animationId = requestAnimationFrame( loop );
}
/* ========================================================= LOOP ========================================================= */
function loop(time){
const dt =
Math.min( 2, ( time-lastTime )/16.67 || 1 );
lastTime = time;
if(playing){
/* AUTO FIRE */
if(shooting){
shoot(time);
}
/* SPAWN */
const spawnDelay =
Math.max( 450, 1150-wave*40 );
if( spawned < zombieCount && time-lastSpawn > spawnDelay ){
spawnZombie();
lastSpawn = time;
}
/* MOVE ZOMBIES */
for( let i=zombies.length-1; i>=0; i-- ){
const z = zombies[i];
z.y += z.speed*dt;
const sway =
Math.sin( time/300+i )*2;
z.el.style.left = ( z.x+sway ) +"px";
z.el.style.top = z.y+"px";
z.el.style.transform =
"translateX(-50%) scale(" +z.scale+ ")";
if( z.y > arena.clientHeight-150 ){
zombieReachedBase( z );
}
}
/* MOVE BULLETS */
for( let i=bullets.length-1; i>=0; i-- ){
const b = bullets[i];
b.x += b.vx*dt;
b.y += b.vy*dt;
b.el.style.left = b.x+"px";
b.el.style.top = b.y+"px";
let hit = false;
for( let j=zombies.length-1; j>=0; j-- ){
if( collision( b, zombies[j] ) ){
damageZombie( zombies[j], b );
hit = true;
break;
}
}
if(
hit
||
b.x < -30 || b.x > arena.clientWidth+30
||
b.y < -30 || b.y > arena.clientHeight+30
){
b.el.remove();
bullets.splice( i, 1 );
}
}
}
animationId = requestAnimationFrame( loop );
}
/* ========================================================= BUTTONS ========================================================= */
startBtn.addEventListener( "click", function(e){
e.stopPropagation();
startGame();
});
restartBtn.addEventListener( "click", function(){
startGame();
});
/* ========================================================= INITIAL UI ========================================================= */
updateUI();
})();
