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
+369 -33
View File
@@ -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) {