// 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(); } };