feat(polish & stats): add wallnut degradation states, hit flash, decapitation particle, frost crystals, dave dialogues and lifetime player stats
This commit is contained in:
+369
-33
@@ -49,6 +49,11 @@ window.Plant = class Plant {
|
||||
this.armorReduction = this.level >= 2 ? 0.15 : 0;
|
||||
this.recoilDps = this.level === 3 ? 18 : 0; // Thorns on Tier 3!
|
||||
break;
|
||||
case 'tallnut':
|
||||
this.maxHealth = this.level === 3 ? 4800 : (this.level === 2 ? 3600 : 2400);
|
||||
this.armorReduction = this.level >= 2 ? 0.2 : 0.05;
|
||||
this.recoilDps = this.level === 3 ? 25 : 0;
|
||||
break;
|
||||
case 'cherrybomb':
|
||||
this.fuse = 1.2;
|
||||
this.fuseTimer = 0;
|
||||
@@ -262,6 +267,7 @@ window.Plant = class Plant {
|
||||
z.takeDamage(1800);
|
||||
gameState.addScore(250);
|
||||
gameState.zombiesKilled++;
|
||||
if (typeof gameState.recordStat === 'function') gameState.recordStat('zombiesKilled', 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -284,6 +290,7 @@ window.Plant = class Plant {
|
||||
victim.takeDamage(9999);
|
||||
gameState.addScore(200);
|
||||
gameState.zombiesKilled++;
|
||||
if (typeof gameState.recordStat === 'function') gameState.recordStat('zombiesKilled', 1);
|
||||
if (window.PvZAudio) window.PvZAudio.playGulp();
|
||||
|
||||
if (this.level === 3 && this.bitesLeft > 1) {
|
||||
@@ -324,6 +331,7 @@ window.Plant = class Plant {
|
||||
z.takeDamage(1800);
|
||||
gameState.addScore(250);
|
||||
gameState.zombiesKilled++;
|
||||
if (typeof gameState.recordStat === 'function') gameState.recordStat('zombiesKilled', 1);
|
||||
killedAny = true;
|
||||
}
|
||||
}
|
||||
@@ -382,6 +390,9 @@ window.Plant = class Plant {
|
||||
case 'wallnut':
|
||||
this.drawWallnut(ctx);
|
||||
break;
|
||||
case 'tallnut':
|
||||
this.drawTallnut(ctx);
|
||||
break;
|
||||
case 'cherrybomb':
|
||||
this.drawCherryBomb(ctx);
|
||||
break;
|
||||
@@ -546,60 +557,385 @@ window.Plant = class Plant {
|
||||
}
|
||||
|
||||
drawWallnut(ctx) {
|
||||
const healthRatio = this.health / this.maxHealth;
|
||||
const healthRatio = Math.max(0, Math.min(1, this.health / this.maxHealth));
|
||||
|
||||
// Base nut body (Outer shell)
|
||||
ctx.fillStyle = '#8d6e63';
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(0, 2, 22, 28, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Inner walnut tone / lighting
|
||||
ctx.fillStyle = '#a1887f';
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(-6, -4, 14, 20, -0.2, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = '#ffffff';
|
||||
// Subtle nut shell texture creases
|
||||
ctx.strokeStyle = 'rgba(93, 64, 55, 0.4)';
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(-8, -8, 5, 7, 0, 0, Math.PI * 2);
|
||||
ctx.ellipse(8, -8, 5, 7, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = '#3e2723';
|
||||
ctx.beginPath();
|
||||
ctx.arc(-6, -8, 2.5, 0, Math.PI * 2);
|
||||
ctx.arc(10, -8, 2.5, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
ctx.strokeStyle = '#3e2723';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
if (healthRatio > 0.66) {
|
||||
ctx.arc(0, 6, 6, 0.2, Math.PI - 0.2);
|
||||
} else if (healthRatio > 0.33) {
|
||||
ctx.moveTo(-6, 8);
|
||||
ctx.lineTo(6, 8);
|
||||
} else {
|
||||
ctx.arc(0, 12, 6, Math.PI + 0.2, Math.PI * 2 - 0.2);
|
||||
}
|
||||
ctx.arc(0, 4, 18, 0.6, 2.5);
|
||||
ctx.stroke();
|
||||
|
||||
if (healthRatio <= 0.66) {
|
||||
// Degradation State 2 & 3: Chipped Shell / Bite Marks
|
||||
if (healthRatio <= 0.66 && healthRatio > 0.33) {
|
||||
// Degraded: chipped shell notch on upper right
|
||||
ctx.fillStyle = '#5d4037';
|
||||
ctx.beginPath();
|
||||
ctx.arc(19, -12, 5, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = '#3e2723';
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.stroke();
|
||||
} else if (healthRatio <= 0.33) {
|
||||
// Heavy Damaged: Large bite marks (deep chunks missing)
|
||||
ctx.fillStyle = '#4e342e';
|
||||
// Upper right large chunk
|
||||
ctx.beginPath();
|
||||
ctx.arc(17, -14, 9, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = '#2b1b17';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.stroke();
|
||||
|
||||
// Lower left bite chunk
|
||||
ctx.fillStyle = '#4e342e';
|
||||
ctx.beginPath();
|
||||
ctx.arc(-18, 10, 7, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = '#2b1b17';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.stroke();
|
||||
|
||||
// Exposed nut pulp inside bite marks
|
||||
ctx.fillStyle = '#d7ccc8';
|
||||
ctx.beginPath();
|
||||
ctx.arc(16, -14, 4, 0, Math.PI * 2);
|
||||
ctx.arc(-17, 10, 3, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
// Degradation State 2 & 3: Cracks
|
||||
if (healthRatio <= 0.66 && healthRatio > 0.33) {
|
||||
// Moderate cracks
|
||||
ctx.strokeStyle = '#3e2723';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(12, -18);
|
||||
ctx.lineTo(6, -6);
|
||||
ctx.lineTo(14, 4);
|
||||
// Upper crack
|
||||
ctx.moveTo(14, -20);
|
||||
ctx.lineTo(8, -10);
|
||||
ctx.lineTo(13, -2);
|
||||
// Forehead crack
|
||||
ctx.moveTo(-10, -18);
|
||||
ctx.lineTo(-5, -11);
|
||||
ctx.stroke();
|
||||
}
|
||||
if (healthRatio <= 0.33) {
|
||||
ctx.strokeStyle = '#3e2723';
|
||||
} else if (healthRatio <= 0.33) {
|
||||
// Severe deep cracks
|
||||
ctx.strokeStyle = '#2b1b17';
|
||||
ctx.lineWidth = 2.5;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-14, -14);
|
||||
ctx.lineTo(-6, -2);
|
||||
ctx.lineTo(-12, 10);
|
||||
// Right heavy fissure
|
||||
ctx.moveTo(16, -22);
|
||||
ctx.lineTo(7, -10);
|
||||
ctx.lineTo(12, 0);
|
||||
ctx.lineTo(5, 14);
|
||||
// Left heavy fissure
|
||||
ctx.moveTo(-16, -16);
|
||||
ctx.lineTo(-7, -4);
|
||||
ctx.lineTo(-14, 6);
|
||||
ctx.lineTo(-8, 18);
|
||||
// Center branching crack
|
||||
ctx.moveTo(-2, -16);
|
||||
ctx.lineTo(2, -6);
|
||||
ctx.lineTo(-3, 4);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// Eyes & Expressions
|
||||
if (healthRatio > 0.66) {
|
||||
// 100%-66%: Smiling fresh walnut, happy sparkling eyes
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(-8, -8, 5, 7, 0, 0, Math.PI * 2);
|
||||
ctx.ellipse(8, -8, 5, 7, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Cute shiny pupils
|
||||
ctx.fillStyle = '#3e2723';
|
||||
ctx.beginPath();
|
||||
ctx.arc(-6, -8, 2.5, 0, Math.PI * 2);
|
||||
ctx.arc(10, -8, 2.5, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Specular glint
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.beginPath();
|
||||
ctx.arc(-7, -9.5, 1.2, 0, Math.PI * 2);
|
||||
ctx.arc(9, -9.5, 1.2, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Rosy cheeks
|
||||
ctx.fillStyle = 'rgba(255, 138, 128, 0.4)';
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(-14, 0, 4, 2.5, 0, 0, Math.PI * 2);
|
||||
ctx.ellipse(14, 0, 4, 2.5, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Happy Smiling Mouth
|
||||
ctx.strokeStyle = '#3e2723';
|
||||
ctx.lineWidth = 2.2;
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 6, 6.5, 0.2, Math.PI - 0.2);
|
||||
ctx.stroke();
|
||||
|
||||
} else if (healthRatio > 0.33) {
|
||||
// 66%-33%: Degraded state - nervous / concerned eyes, sweat drop, straight tense mouth
|
||||
// Stressed eyes
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(-8, -7, 5, 6, 0, 0, Math.PI * 2);
|
||||
ctx.ellipse(8, -7, 5, 6, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Looking sideways nervously
|
||||
ctx.fillStyle = '#3e2723';
|
||||
ctx.beginPath();
|
||||
ctx.arc(-9, -7, 2.2, 0, Math.PI * 2);
|
||||
ctx.arc(7, -7, 2.2, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Worried slanted eyebrows
|
||||
ctx.strokeStyle = '#3e2723';
|
||||
ctx.lineWidth = 1.8;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-12, -15);
|
||||
ctx.lineTo(-4, -13);
|
||||
ctx.moveTo(12, -15);
|
||||
ctx.lineTo(4, -13);
|
||||
ctx.stroke();
|
||||
|
||||
// Sweat drop on forehead / temple
|
||||
const sweatBob = Math.sin(this.animTime * 6) * 1.5;
|
||||
ctx.fillStyle = '#4fc3f7';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(14, -17 + sweatBob);
|
||||
ctx.quadraticCurveTo(17, -13 + sweatBob, 14, -10 + sweatBob);
|
||||
ctx.quadraticCurveTo(11, -13 + sweatBob, 14, -17 + sweatBob);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#e1f5fe';
|
||||
ctx.beginPath();
|
||||
ctx.arc(13.5, -12 + sweatBob, 0.9, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Grimace / tense mouth
|
||||
ctx.strokeStyle = '#3e2723';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-6, 8);
|
||||
ctx.lineTo(6, 8);
|
||||
ctx.stroke();
|
||||
|
||||
} else {
|
||||
// <33%: Heavy damaged state - crying / panicked eyes, large tears, wailing mouth
|
||||
// Wide panicked eye whites
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(-8, -6, 6, 7.5, 0, 0, Math.PI * 2);
|
||||
ctx.ellipse(8, -6, 6, 7.5, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Trembling dilated pupils
|
||||
const tremX = Math.sin(this.animTime * 20) * 0.8;
|
||||
ctx.fillStyle = '#3e2723';
|
||||
ctx.beginPath();
|
||||
ctx.arc(-8 + tremX, -6, 3, 0, Math.PI * 2);
|
||||
ctx.arc(8 + tremX, -6, 3, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Severe panicked eyebrows
|
||||
ctx.strokeStyle = '#2b1b17';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-13, -13);
|
||||
ctx.lineTo(-4, -17);
|
||||
ctx.moveTo(13, -13);
|
||||
ctx.lineTo(4, -17);
|
||||
ctx.stroke();
|
||||
|
||||
// Streaming Tears down cheeks (animated)
|
||||
const tearPhase = (this.animTime * 5) % 1;
|
||||
ctx.fillStyle = '#29b6f6';
|
||||
// Left tear stream & droplet
|
||||
ctx.fillRect(-9, 1, 2.5, 10);
|
||||
ctx.beginPath();
|
||||
ctx.arc(-7.8, 12 + tearPhase * 6, 2.2, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Right tear stream & droplet
|
||||
ctx.fillRect(7, 1, 2.5, 10);
|
||||
ctx.beginPath();
|
||||
ctx.arc(8.2, 12 + tearPhase * 6, 2.2, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Crying tear pools in eye corners
|
||||
ctx.fillStyle = '#81d4fa';
|
||||
ctx.beginPath();
|
||||
ctx.arc(-8, 0, 3, 0, Math.PI);
|
||||
ctx.arc(8, 0, 3, 0, Math.PI);
|
||||
ctx.fill();
|
||||
|
||||
// Open wailing panicked mouth
|
||||
ctx.fillStyle = '#212121';
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(0, 10, 5.5, 7, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = '#2b1b17';
|
||||
ctx.lineWidth = 1.8;
|
||||
ctx.stroke();
|
||||
|
||||
// Trembling lower lip
|
||||
ctx.fillStyle = '#ff8a80';
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 13, 3, 0, Math.PI);
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
|
||||
drawTallnut(ctx) {
|
||||
const healthRatio = Math.max(0, Math.min(1, this.health / this.maxHealth));
|
||||
|
||||
// Tallnut body (Taller silhouette)
|
||||
ctx.fillStyle = '#6d4c41';
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(0, -2, 22, 36, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = '#8d6e63';
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(-6, -8, 14, 26, -0.15, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Texture ridge
|
||||
ctx.strokeStyle = 'rgba(62, 39, 35, 0.4)';
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 2, 24, 0.6, 2.5);
|
||||
ctx.stroke();
|
||||
|
||||
// Bite marks & chips
|
||||
if (healthRatio <= 0.66 && healthRatio > 0.33) {
|
||||
ctx.fillStyle = '#4e342e';
|
||||
ctx.beginPath();
|
||||
ctx.arc(19, -18, 6, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
} else if (healthRatio <= 0.33) {
|
||||
ctx.fillStyle = '#3e2723';
|
||||
ctx.beginPath();
|
||||
ctx.arc(18, -20, 10, 0, Math.PI * 2);
|
||||
ctx.arc(-18, 8, 8, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#d7ccc8';
|
||||
ctx.beginPath();
|
||||
ctx.arc(17, -20, 4, 0, Math.PI * 2);
|
||||
ctx.arc(-17, 8, 3, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
// Cracks
|
||||
if (healthRatio <= 0.66 && healthRatio > 0.33) {
|
||||
ctx.strokeStyle = '#3e2723';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(14, -28);
|
||||
ctx.lineTo(7, -16);
|
||||
ctx.lineTo(12, -4);
|
||||
ctx.stroke();
|
||||
} else if (healthRatio <= 0.33) {
|
||||
ctx.strokeStyle = '#2b1b17';
|
||||
ctx.lineWidth = 2.5;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(15, -30);
|
||||
ctx.lineTo(6, -14);
|
||||
ctx.lineTo(12, 2);
|
||||
ctx.lineTo(4, 20);
|
||||
ctx.moveTo(-15, -20);
|
||||
ctx.lineTo(-6, -6);
|
||||
ctx.lineTo(-14, 10);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// Face / Expression
|
||||
if (healthRatio > 0.66) {
|
||||
// Stoic / confident tallnut eyes
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(-8, -14, 5, 7, 0, 0, Math.PI * 2);
|
||||
ctx.ellipse(8, -14, 5, 7, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#3e2723';
|
||||
ctx.beginPath();
|
||||
ctx.arc(-6, -14, 2.5, 0, Math.PI * 2);
|
||||
ctx.arc(10, -14, 2.5, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
// Confident smirk
|
||||
ctx.strokeStyle = '#3e2723';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, 6, 0.2, Math.PI - 0.2);
|
||||
ctx.stroke();
|
||||
} else if (healthRatio > 0.33) {
|
||||
// Determined / grimace
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(-8, -13, 5, 6, 0, 0, Math.PI * 2);
|
||||
ctx.ellipse(8, -13, 5, 6, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#3e2723';
|
||||
ctx.beginPath();
|
||||
ctx.arc(-9, -13, 2.2, 0, Math.PI * 2);
|
||||
ctx.arc(7, -13, 2.2, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
// Slanted heavy eyebrows
|
||||
ctx.strokeStyle = '#2b1b17';
|
||||
ctx.lineWidth = 2.2;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-13, -22);
|
||||
ctx.lineTo(-4, -19);
|
||||
ctx.moveTo(13, -22);
|
||||
ctx.lineTo(4, -19);
|
||||
ctx.stroke();
|
||||
// Tight mouth
|
||||
ctx.moveTo(-6, 2);
|
||||
ctx.lineTo(6, 2);
|
||||
ctx.stroke();
|
||||
// Sweat drop
|
||||
ctx.fillStyle = '#4fc3f7';
|
||||
ctx.beginPath();
|
||||
ctx.arc(14, -22, 2, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
} else {
|
||||
// Panicked / crying
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(-8, -12, 6, 7.5, 0, 0, Math.PI * 2);
|
||||
ctx.ellipse(8, -12, 6, 7.5, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#3e2723';
|
||||
ctx.beginPath();
|
||||
ctx.arc(-8, -12, 3, 0, Math.PI * 2);
|
||||
ctx.arc(8, -12, 3, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
// Crying tears
|
||||
ctx.fillStyle = '#29b6f6';
|
||||
ctx.fillRect(-9, -4, 2.5, 14);
|
||||
ctx.fillRect(7, -4, 2.5, 14);
|
||||
// Panicked open mouth
|
||||
ctx.fillStyle = '#212121';
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(0, 4, 6, 8, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
|
||||
drawPotatoMine(ctx) {
|
||||
|
||||
+110
-12
@@ -16,7 +16,9 @@ window.Zombie = class Zombie {
|
||||
this.eatTimer = 0;
|
||||
|
||||
this.slowTimer = 0;
|
||||
this.freezeTimer = 0;
|
||||
this.hitFlashTimer = 0;
|
||||
this.headSpawned = false;
|
||||
this.animTime = Math.random() * 5;
|
||||
this.toRemove = false;
|
||||
|
||||
@@ -69,22 +71,52 @@ window.Zombie = class Zombie {
|
||||
this.health = this.maxHealth;
|
||||
}
|
||||
|
||||
get isSlowed() {
|
||||
return this.slowTimer > 0 || this.freezeTimer > 0;
|
||||
}
|
||||
|
||||
set isSlowed(val) {
|
||||
if (val && this.slowTimer <= 0) {
|
||||
this.slowTimer = 4.0;
|
||||
} else if (!val) {
|
||||
this.slowTimer = 0;
|
||||
this.freezeTimer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
update(dt, gameState) {
|
||||
this.animTime += dt;
|
||||
|
||||
if (this.health <= 0) {
|
||||
if (!this.headSpawned && gameState && gameState.combat) {
|
||||
this.headSpawned = true;
|
||||
gameState.combat.spawnDecapitatedHead(
|
||||
this.x,
|
||||
this.y - 24,
|
||||
this.type,
|
||||
this.isAngry,
|
||||
this.isSlowed,
|
||||
gameState.tree ? gameState.tree.cheats : null
|
||||
);
|
||||
}
|
||||
this.toRemove = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.hitFlashTimer > 0) {
|
||||
this.hitFlashTimer -= dt;
|
||||
if (this.hitFlashTimer < 0) this.hitFlashTimer = 0;
|
||||
}
|
||||
|
||||
if (this.freezeTimer > 0) {
|
||||
this.freezeTimer -= dt;
|
||||
if (this.freezeTimer < 0) this.freezeTimer = 0;
|
||||
}
|
||||
|
||||
// Freeze / slow effect countdown
|
||||
if (this.slowTimer > 0) {
|
||||
this.slowTimer -= dt;
|
||||
this.speed = this.baseSpeed * 0.5;
|
||||
if (this.slowTimer > 0 || this.freezeTimer > 0) {
|
||||
if (this.slowTimer > 0) this.slowTimer -= dt;
|
||||
this.speed = this.baseSpeed * (this.freezeTimer > 0 ? 0.35 : 0.5);
|
||||
} else {
|
||||
this.speed = this.baseSpeed;
|
||||
}
|
||||
@@ -157,7 +189,7 @@ window.Zombie = class Zombie {
|
||||
this.hasPaper = false;
|
||||
this.isAngry = true;
|
||||
this.baseSpeed = 54; // Enraged run!
|
||||
this.speed = this.slowTimer > 0 ? 27 : 54;
|
||||
this.speed = this.isSlowed ? 27 : 54;
|
||||
if (window.PvZAudio) window.PvZAudio.playStrike();
|
||||
}
|
||||
return;
|
||||
@@ -169,6 +201,51 @@ window.Zombie = class Zombie {
|
||||
}
|
||||
}
|
||||
|
||||
drawFrostCrystal(ctx, cx, cy, size, pulse) {
|
||||
ctx.save();
|
||||
ctx.translate(cx, cy);
|
||||
ctx.rotate(pulse * 0.4);
|
||||
ctx.strokeStyle = 'rgba(224, 247, 250, 0.9)';
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.lineWidth = 1.2;
|
||||
for (let i = 0; i < 3; i++) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-size, 0);
|
||||
ctx.lineTo(size, 0);
|
||||
ctx.stroke();
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-size * 0.5, -size * 0.35);
|
||||
ctx.lineTo(-size * 0.5, size * 0.35);
|
||||
ctx.moveTo(size * 0.5, -size * 0.35);
|
||||
ctx.lineTo(size * 0.5, size * 0.35);
|
||||
ctx.stroke();
|
||||
ctx.rotate(Math.PI / 3);
|
||||
}
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, 1.2, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
drawFrostCrystals(ctx) {
|
||||
ctx.save();
|
||||
const pulse = Math.sin(this.animTime * 3);
|
||||
const crys = [
|
||||
{ x: -18, y: -26, s: 4.5 },
|
||||
{ x: 16, y: -22, s: 5.0 },
|
||||
{ x: -16, y: 4, s: 4.0 },
|
||||
{ x: 14, y: 0, s: 4.5 },
|
||||
{ x: -8, y: 24, s: 3.8 },
|
||||
{ x: 10, y: 22, s: 4.0 }
|
||||
];
|
||||
for (let i = 0; i < crys.length; i++) {
|
||||
const c = crys[i];
|
||||
const p = pulse + i;
|
||||
this.drawFrostCrystal(ctx, c.x, c.y, c.s, p);
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
draw(ctx, gameState = null) {
|
||||
ctx.save();
|
||||
ctx.translate(this.x, this.y);
|
||||
@@ -189,7 +266,7 @@ window.Zombie = class Zombie {
|
||||
: (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;
|
||||
const isSlow = this.isSlowed;
|
||||
|
||||
let skinColor = this.isAngry ? '#b71c1c' : '#809973';
|
||||
let coatColor = '#5d4037';
|
||||
@@ -203,10 +280,14 @@ window.Zombie = class Zombie {
|
||||
if (isSlow) {
|
||||
skinColor = '#81d4fa';
|
||||
coatColor = '#455a64';
|
||||
pantsColor = '#263238';
|
||||
}
|
||||
if (isHit) {
|
||||
skinColor = '#ffffff';
|
||||
coatColor = '#e0e0e0';
|
||||
coatColor = '#ffffff';
|
||||
pantsColor = '#ffffff';
|
||||
ctx.shadowColor = '#ffffff';
|
||||
ctx.shadowBlur = 12;
|
||||
}
|
||||
|
||||
// Vault jump vertical offset
|
||||
@@ -368,7 +449,7 @@ window.Zombie = class Zombie {
|
||||
|
||||
// Headwear
|
||||
if (this.type === 'conehead' && this.health > 100) {
|
||||
ctx.fillStyle = '#ff6d00';
|
||||
ctx.fillStyle = isHit ? '#ffffff' : '#ff6d00';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-14, -14);
|
||||
ctx.lineTo(14, -14);
|
||||
@@ -385,7 +466,7 @@ window.Zombie = class Zombie {
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
} else if (this.type === 'buckethead' && this.health > 100) {
|
||||
ctx.fillStyle = '#90a4ae';
|
||||
ctx.fillStyle = isHit ? '#ffffff' : '#90a4ae';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-16, -14);
|
||||
ctx.lineTo(16, -14);
|
||||
@@ -393,12 +474,12 @@ window.Zombie = class Zombie {
|
||||
ctx.lineTo(-13, -44);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = '#546e7a';
|
||||
ctx.strokeStyle = isHit ? '#ffffff' : '#546e7a';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.stroke();
|
||||
} else if (this.type === 'football' && this.health > 100) {
|
||||
// Football Helmet
|
||||
ctx.fillStyle = '#b71c1c';
|
||||
ctx.fillStyle = isHit ? '#ffffff' : '#b71c1c';
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, -10, 18, Math.PI, 0);
|
||||
ctx.fill();
|
||||
@@ -414,14 +495,14 @@ window.Zombie = class Zombie {
|
||||
ctx.lineTo(8, -4);
|
||||
ctx.stroke();
|
||||
} else if (this.type === 'flag') {
|
||||
ctx.strokeStyle = '#8d6e63';
|
||||
ctx.strokeStyle = isHit ? '#ffffff' : '#8d6e63';
|
||||
ctx.lineWidth = 3;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(10, 10);
|
||||
ctx.lineTo(16, -55);
|
||||
ctx.stroke();
|
||||
|
||||
ctx.fillStyle = '#d32f2f';
|
||||
ctx.fillStyle = isHit ? '#ffffff' : '#d32f2f';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(16, -55);
|
||||
ctx.lineTo(40, -45);
|
||||
@@ -432,6 +513,23 @@ window.Zombie = class Zombie {
|
||||
|
||||
ctx.restore();
|
||||
|
||||
// Frost Crystals visual effect around slowed / frozen zombie
|
||||
if (isSlow && !isHit) {
|
||||
this.drawFrostCrystals(ctx);
|
||||
}
|
||||
|
||||
// Bright white highlight overlay filter when hit
|
||||
if (isHit) {
|
||||
ctx.save();
|
||||
const flashAlpha = Math.min(1, this.hitFlashTimer / 0.08) * 0.75;
|
||||
ctx.globalAlpha = flashAlpha;
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(0, -6, 22, 44, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// Health bar if damaged
|
||||
if (this.health < this.maxHealth) {
|
||||
const pct = Math.max(0, this.health / this.maxHealth);
|
||||
|
||||
Reference in New Issue
Block a user