feat(polish & stats): add wallnut degradation states, hit flash, decapitation particle, frost crystals, dave dialogues and lifetime player stats

This commit is contained in:
2026-09-17 13:28:33 +00:00
parent 23918dab1f
commit 643b0287c1
10 changed files with 1260 additions and 65 deletions
+110 -12
View File
@@ -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);