Files
ai-qpid-pvz/game/js/entities/zombie.js
T
Antigravity 6457a8904e feat: Initial commit Plants vs Zombies Web Edition v2.4
- 5 Biomes: Day, Night, Pool, Fog, Roof (25 Campaign levels)
- Minigames: Wall-nut Bowling, I Zombie, Roguelike, Boss Rush & Dr. Zomboss
- Systems: Zen Garden, Workshop Crafting, Almanac, Tree of Wisdom, Achievements
- Backend: REST API + SQLite Accounts & Save Synchronization
- Infrastructure: Nginx HTTPS & Docker deployment
2026-09-17 09:30:52 +00:00

447 lines
12 KiB
JavaScript

// Zombie Entities (Normal, Conehead, Buckethead, Flag, PoleVaulter, Newspaper, Football, DuckyTube)
window.Zombie = class Zombie {
constructor(row, type = 'normal', startX = 940) {
this.row = row;
this.type = type;
this.x = startX;
this.y = 0;
this.width = 60;
this.height = 90;
this.baseSpeed = 22; // default walking speed
this.eatingDps = 30;
this.isEating = false;
this.targetPlant = null;
this.eatTimer = 0;
this.slowTimer = 0;
this.hitFlashTimer = 0;
this.animTime = Math.random() * 5;
this.toRemove = false;
// Type specifics
this.hasPole = type === 'polevaulter';
this.isVaulting = false;
this.vaultTimer = 0;
this.hasPaper = type === 'newspaper';
this.paperHealth = 100;
this.isAngry = false;
switch (type) {
case 'conehead':
this.maxHealth = 200;
this.helmetHealth = 100;
break;
case 'buckethead':
this.maxHealth = 350;
this.helmetHealth = 250;
break;
case 'football':
this.maxHealth = 350;
this.helmetHealth = 250;
this.baseSpeed = 42;
break;
case 'polevaulter':
this.maxHealth = 180;
this.baseSpeed = 44; // Fast until vault
break;
case 'newspaper':
this.maxHealth = 200;
this.baseSpeed = 18;
break;
case 'duckytube':
this.maxHealth = 120;
this.baseSpeed = 24;
break;
case 'flag':
this.maxHealth = 110;
this.baseSpeed = 26;
break;
case 'normal':
default:
this.maxHealth = 100;
this.helmetHealth = 0;
break;
}
this.speed = this.baseSpeed;
this.health = this.maxHealth;
}
update(dt, gameState) {
this.animTime += dt;
if (this.health <= 0) {
this.toRemove = true;
return;
}
if (this.hitFlashTimer > 0) {
this.hitFlashTimer -= dt;
}
// Freeze / slow effect countdown
if (this.slowTimer > 0) {
this.slowTimer -= dt;
this.speed = this.baseSpeed * 0.5;
} else {
this.speed = this.baseSpeed;
}
// Pole Vaulter jump logic
if (this.isVaulting) {
this.vaultTimer += dt;
// Arc jump over plant (travels ~90px leftwards in 0.6s)
this.x -= 150 * dt;
if (this.vaultTimer >= 0.6) {
this.isVaulting = false;
this.baseSpeed = 22; // Normal speed after jumping
this.speed = 22;
}
return;
}
// Check if eating a plant
if (this.targetPlant) {
// Pole vaulter vaults over first encountered plant!
if (this.hasPole && !this.isVaulting) {
this.hasPole = false;
this.isVaulting = true;
this.vaultTimer = 0;
this.targetPlant = null;
this.isEating = false;
if (window.PvZAudio) window.PvZAudio.playVault();
return;
}
if (this.targetPlant.toRemove || this.targetPlant.health <= 0) {
this.targetPlant = null;
this.isEating = false;
} else {
this.isEating = true;
this.eatTimer += dt;
if (this.eatTimer >= 0.35) {
this.eatTimer = 0;
this.targetPlant.takeDamage(this.eatingDps * 0.35);
if (window.PvZAudio) window.PvZAudio.playChomp();
if (this.targetPlant && this.targetPlant.type === 'wallnut' && gameState && gameState.achievements) {
gameState.achievements.trigger('nut_wall');
}
if (this.targetPlant.toRemove || this.targetPlant.health <= 0) {
this.targetPlant = null;
this.isEating = false;
}
}
}
} else {
this.isEating = false;
}
// If not eating and not vaulting, walk forward
if (!this.isEating) {
this.x -= this.speed * dt;
}
}
takeDamage(amount, slows = false) {
this.hitFlashTimer = 0.08;
if (slows) {
this.slowTimer = 4.0;
}
// Newspaper mechanics
if (this.type === 'newspaper' && this.hasPaper) {
this.paperHealth -= amount;
if (this.paperHealth <= 0) {
this.hasPaper = false;
this.isAngry = true;
this.baseSpeed = 54; // Enraged run!
this.speed = this.slowTimer > 0 ? 27 : 54;
if (window.PvZAudio) window.PvZAudio.playStrike();
}
return;
}
this.health -= amount;
if (this.health <= 0) {
this.toRemove = true;
}
}
draw(ctx, gameState = null) {
ctx.save();
ctx.translate(this.x, this.y);
// Ground shadow
ctx.fillStyle = 'rgba(0, 0, 0, 0.25)';
ctx.beginPath();
ctx.ellipse(0, 42, 22, 8, 0, 0, Math.PI * 2);
ctx.fill();
const isDancing = gameState && gameState.tree && gameState.tree.cheats && gameState.tree.cheats.dance;
const danceBob = isDancing ? Math.sin(this.animTime * 12) * 6 : 0;
const walkBob = (Math.sin(this.animTime * (this.isAngry ? 12 : 5)) * 2) + danceBob;
const legAngle1 = Math.sin(this.animTime * (this.isAngry ? 10 : (isDancing ? 8 : 4))) * 0.35;
const legAngle2 = -legAngle1;
const armAngle = this.isEating
? (Math.sin(this.animTime * 8) * 0.2 + 0.5)
: (Math.sin(this.animTime * (this.isAngry ? 8 : (isDancing ? 8 : 3))) * 0.25 + (isDancing ? 0.9 : (this.isAngry ? 0.8 : 0.3)));
const isHit = this.hitFlashTimer > 0;
const isSlow = this.slowTimer > 0;
let skinColor = this.isAngry ? '#b71c1c' : '#809973';
let coatColor = '#5d4037';
let pantsColor = '#263238';
if (this.type === 'football') {
coatColor = '#b71c1c';
pantsColor = '#f5f5f5';
}
if (isSlow) {
skinColor = '#81d4fa';
coatColor = '#455a64';
}
if (isHit) {
skinColor = '#ffffff';
coatColor = '#e0e0e0';
}
// Vault jump vertical offset
if (this.isVaulting) {
const jumpProgress = this.vaultTimer / 0.6;
const jumpY = -Math.sin(jumpProgress * Math.PI) * 65;
ctx.translate(0, jumpY);
ctx.rotate(-0.3 + jumpProgress * 0.6);
} else {
ctx.translate(0, walkBob);
}
// Water ripple for Ducky Tube
if (this.type === 'duckytube') {
ctx.strokeStyle = 'rgba(255, 255, 255, 0.6)';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.ellipse(0, 30, 30 + Math.sin(this.animTime * 4) * 4, 12, 0, 0, Math.PI * 2);
ctx.stroke();
}
// Back leg
ctx.save();
ctx.translate(6, 15);
ctx.rotate(legAngle2);
ctx.fillStyle = pantsColor;
ctx.fillRect(-5, 0, 10, 24);
ctx.fillStyle = '#1b1b1b';
ctx.fillRect(-7, 24, 14, 6);
ctx.restore();
// Front leg
ctx.save();
ctx.translate(-6, 15);
ctx.rotate(legAngle1);
ctx.fillStyle = pantsColor;
ctx.fillRect(-5, 0, 10, 24);
ctx.fillStyle = '#1b1b1b';
ctx.fillRect(-7, 24, 14, 6);
ctx.restore();
// Torso
ctx.fillStyle = coatColor;
ctx.fillRect(-14, -18, 26, 34);
// Tie
ctx.fillStyle = '#b71c1c';
ctx.beginPath();
ctx.moveTo(-1, -16);
ctx.lineTo(3, -16);
ctx.lineTo(2, -2);
ctx.lineTo(1, 4);
ctx.lineTo(0, -2);
ctx.fill();
// Ducky Inner Tube
if (this.type === 'duckytube') {
ctx.fillStyle = '#fbc02d';
ctx.beginPath();
ctx.ellipse(0, 16, 26, 14, 0, 0, Math.PI * 2);
ctx.fill();
// Duck beak
ctx.fillStyle = '#ff9800';
ctx.beginPath();
ctx.arc(-22, 12, 6, 0, Math.PI * 2);
ctx.fill();
}
// Arms & Items (Pole / Newspaper)
ctx.save();
ctx.translate(-8, -10);
ctx.rotate(armAngle);
ctx.fillStyle = coatColor;
ctx.fillRect(-18, -4, 20, 8);
ctx.fillStyle = skinColor;
ctx.beginPath();
ctx.arc(-20, 0, 5, 0, Math.PI * 2);
ctx.fill();
// Held Item
if (this.hasPole) {
// Long vault pole
ctx.strokeStyle = '#8d6e63';
ctx.lineWidth = 5;
ctx.beginPath();
ctx.moveTo(-20, 2);
ctx.lineTo(-65, -35);
ctx.stroke();
} else if (this.hasPaper) {
// Newspaper shield
ctx.fillStyle = '#f5f5f5';
ctx.strokeStyle = '#9e9e9e';
ctx.lineWidth = 1;
ctx.fillRect(-34, -18, 16, 32);
ctx.strokeRect(-34, -18, 16, 32);
// Newspaper lines
ctx.fillStyle = '#616161';
ctx.fillRect(-32, -14, 12, 2);
ctx.fillRect(-32, -8, 12, 2);
ctx.fillRect(-32, -2, 12, 2);
}
ctx.restore();
// Head
ctx.save();
ctx.translate(-2, -28);
const headTilt = this.isEating ? Math.sin(this.animTime * 12) * 0.15 : Math.sin(this.animTime * 3) * 0.05;
ctx.rotate(headTilt);
ctx.fillStyle = skinColor;
ctx.beginPath();
ctx.roundRect(-14, -16, 28, 30, [10, 10, 6, 6]);
ctx.fill();
// Eyes
ctx.fillStyle = this.isAngry ? '#ff5252' : '#fff9c4';
ctx.beginPath();
ctx.arc(-8, -4, 4.5, 0, Math.PI * 2);
ctx.arc(3, -4, 5.5, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#000';
ctx.beginPath();
ctx.arc(-9, -4, 2, 0, Math.PI * 2);
ctx.arc(2, -4, 2, 0, Math.PI * 2);
ctx.fill();
// Mouth
ctx.fillStyle = '#212121';
ctx.fillRect(-10, 6, 18, 5);
ctx.fillStyle = '#fff';
ctx.fillRect(-9, 6, 3, 3);
ctx.fillRect(-4, 6, 3, 3);
ctx.fillRect(1, 8, 3, 3);
// Tree of Wisdom Visual Cheats
if (gameState && gameState.tree && gameState.tree.cheats) {
// 1. Future Sunglasses
if (gameState.tree.cheats.future) {
ctx.fillStyle = '#00e5ff';
ctx.shadowColor = '#00e5ff';
ctx.shadowBlur = 6;
ctx.fillRect(-12, -7, 22, 6);
ctx.fillStyle = '#18ffff';
ctx.fillRect(-10, -5, 18, 2);
ctx.shadowBlur = 0;
}
// 2. Mustache
if (gameState.tree.cheats.mustache) {
ctx.fillStyle = '#3e2723';
ctx.beginPath();
ctx.moveTo(-11, 4);
ctx.quadraticCurveTo(-4, 0, 0, 3);
ctx.quadraticCurveTo(4, 0, 9, 4);
ctx.quadraticCurveTo(0, 8, -11, 4);
ctx.fill();
}
}
// Headwear
if (this.type === 'conehead' && this.health > 100) {
ctx.fillStyle = '#ff6d00';
ctx.beginPath();
ctx.moveTo(-14, -14);
ctx.lineTo(14, -14);
ctx.lineTo(3, -48);
ctx.lineTo(-3, -48);
ctx.closePath();
ctx.fill();
ctx.fillStyle = '#ffffff';
ctx.beginPath();
ctx.moveTo(-8, -26);
ctx.lineTo(8, -26);
ctx.lineTo(6, -34);
ctx.lineTo(-6, -34);
ctx.closePath();
ctx.fill();
} else if (this.type === 'buckethead' && this.health > 100) {
ctx.fillStyle = '#90a4ae';
ctx.beginPath();
ctx.moveTo(-16, -14);
ctx.lineTo(16, -14);
ctx.lineTo(13, -44);
ctx.lineTo(-13, -44);
ctx.closePath();
ctx.fill();
ctx.strokeStyle = '#546e7a';
ctx.lineWidth = 2;
ctx.stroke();
} else if (this.type === 'football' && this.health > 100) {
// Football Helmet
ctx.fillStyle = '#b71c1c';
ctx.beginPath();
ctx.arc(0, -10, 18, Math.PI, 0);
ctx.fill();
// White Stripe
ctx.fillStyle = '#ffffff';
ctx.fillRect(-3, -28, 6, 18);
// Face grill
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 2.5;
ctx.beginPath();
ctx.moveTo(-12, -4);
ctx.lineTo(-2, 4);
ctx.lineTo(8, -4);
ctx.stroke();
} else if (this.type === 'flag') {
ctx.strokeStyle = '#8d6e63';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(10, 10);
ctx.lineTo(16, -55);
ctx.stroke();
ctx.fillStyle = '#d32f2f';
ctx.beginPath();
ctx.moveTo(16, -55);
ctx.lineTo(40, -45);
ctx.lineTo(16, -35);
ctx.closePath();
ctx.fill();
}
ctx.restore();
// Health bar if damaged
if (this.health < this.maxHealth) {
const pct = Math.max(0, this.health / this.maxHealth);
ctx.fillStyle = 'rgba(0,0,0,0.6)';
ctx.fillRect(-20, -56, 40, 5);
ctx.fillStyle = pct > 0.4 ? '#8bc34a' : '#f44336';
ctx.fillRect(-20, -56, 40 * pct, 5);
}
ctx.restore();
}
};