feat(polish & stats): add wallnut degradation states, hit flash, decapitation particle, frost crystals, dave dialogues and lifetime player stats
This commit is contained in:
@@ -47,11 +47,175 @@ window.ParticlePool = class ParticlePool {
|
||||
}
|
||||
};
|
||||
|
||||
// Zombie Decapitation Flying Head Particle
|
||||
window.ZombieHeadParticle = class ZombieHeadParticle {
|
||||
constructor(x, y, options = {}) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.vx = (Math.random() * 50 + 40) * (Math.random() < 0.5 ? 1 : -0.7);
|
||||
this.vy = -(Math.random() * 60 + 130);
|
||||
this.gravity = 400;
|
||||
this.rotation = 0;
|
||||
this.rotSpeed = (Math.random() * 4 + 4) * (this.vx >= 0 ? 1 : -1);
|
||||
this.groundY = y + 62;
|
||||
this.bounces = 2;
|
||||
this.life = 2.2;
|
||||
this.maxLife = 2.2;
|
||||
this.type = options.type || 'normal';
|
||||
this.isAngry = !!options.isAngry;
|
||||
this.isSlow = !!options.isSlow;
|
||||
this.cheats = options.cheats || null;
|
||||
}
|
||||
|
||||
update(dt) {
|
||||
this.life -= dt;
|
||||
if (this.life <= 0) return;
|
||||
|
||||
this.vy += this.gravity * dt;
|
||||
this.x += this.vx * dt;
|
||||
this.y += this.vy * dt;
|
||||
this.rotation += this.rotSpeed * dt;
|
||||
|
||||
if (this.y >= this.groundY) {
|
||||
this.y = this.groundY;
|
||||
if (this.bounces > 0) {
|
||||
this.bounces--;
|
||||
this.vy = -this.vy * 0.42;
|
||||
this.vx *= 0.6;
|
||||
this.rotSpeed *= 0.5;
|
||||
} else {
|
||||
this.vy = 0;
|
||||
this.vx = 0;
|
||||
this.rotSpeed = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
draw(ctx) {
|
||||
if (this.life <= 0) return;
|
||||
ctx.save();
|
||||
|
||||
const alpha = this.life < 0.6 ? Math.max(0, this.life / 0.6) : 1;
|
||||
|
||||
// Ground shadow
|
||||
const distToGround = Math.max(0, this.groundY - this.y);
|
||||
if (distToGround < 65) {
|
||||
ctx.save();
|
||||
ctx.translate(this.x, this.groundY + 12);
|
||||
ctx.fillStyle = `rgba(0,0,0,${0.25 * (1 - distToGround / 65) * alpha})`;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(0, 0, 14, 5, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
ctx.translate(this.x, this.y);
|
||||
ctx.rotate(this.rotation);
|
||||
ctx.globalAlpha = alpha;
|
||||
|
||||
let skinColor = this.isAngry ? '#b71c1c' : '#809973';
|
||||
if (this.isSlow) skinColor = '#81d4fa';
|
||||
|
||||
// Head base
|
||||
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();
|
||||
|
||||
// Open mouth / teeth
|
||||
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);
|
||||
|
||||
// Cheats
|
||||
if (this.cheats) {
|
||||
if (this.cheats.future) {
|
||||
ctx.fillStyle = '#00e5ff';
|
||||
ctx.fillRect(-12, -7, 22, 6);
|
||||
}
|
||||
if (this.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();
|
||||
}
|
||||
}
|
||||
|
||||
// Hat / Helmet
|
||||
if (this.type === 'conehead') {
|
||||
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') {
|
||||
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') {
|
||||
ctx.fillStyle = '#b71c1c';
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, -10, 18, Math.PI, 0);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.fillRect(-3, -28, 6, 18);
|
||||
ctx.strokeStyle = '#ffffff';
|
||||
ctx.lineWidth = 2.5;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-12, -4);
|
||||
ctx.lineTo(-2, 4);
|
||||
ctx.lineTo(8, -4);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
};
|
||||
|
||||
// Combat & Collision Resolution System (with Bowling Ricochet & Coin Drops)
|
||||
window.CombatSystem = class CombatSystem {
|
||||
constructor() {
|
||||
this.particlePool = new window.ParticlePool(150);
|
||||
this.particles = [];
|
||||
this.heads = [];
|
||||
}
|
||||
|
||||
reset() {
|
||||
@@ -59,6 +223,12 @@ window.CombatSystem = class CombatSystem {
|
||||
this.particlePool.releaseAll(this.particles);
|
||||
}
|
||||
this.particles = [];
|
||||
this.heads = [];
|
||||
}
|
||||
|
||||
spawnDecapitatedHead(x, y, type = 'normal', isAngry = false, isSlow = false, cheats = null) {
|
||||
this.heads.push(new window.ZombieHeadParticle(x, y, { type, isAngry, isSlow, cheats }));
|
||||
this.createSplash(x, y + 10, '#689f38', 5);
|
||||
}
|
||||
|
||||
createSplash(x, y, color = '#8bc34a', count = 7) {
|
||||
@@ -110,6 +280,17 @@ window.CombatSystem = class CombatSystem {
|
||||
}
|
||||
this.particles.length = writeIdx;
|
||||
|
||||
// 1.5 Update Decapitated Heads
|
||||
let headWriteIdx = 0;
|
||||
for (let i = 0; i < this.heads.length; i++) {
|
||||
const h = this.heads[i];
|
||||
h.update(dt);
|
||||
if (h.life > 0) {
|
||||
this.heads[headWriteIdx++] = h;
|
||||
}
|
||||
}
|
||||
this.heads.length = headWriteIdx;
|
||||
|
||||
// 2. Projectiles vs Zombies
|
||||
for (const proj of gameState.projectiles) {
|
||||
if (proj.toRemove) continue;
|
||||
@@ -279,9 +460,24 @@ window.CombatSystem = class CombatSystem {
|
||||
}
|
||||
|
||||
handleZombieDeath(zombie, gameState) {
|
||||
if (!zombie.headSpawned) {
|
||||
zombie.headSpawned = true;
|
||||
this.spawnDecapitatedHead(
|
||||
zombie.x,
|
||||
zombie.y - 24,
|
||||
zombie.type,
|
||||
zombie.isAngry,
|
||||
zombie.slowTimer > 0 || (zombie.freezeTimer && zombie.freezeTimer > 0) || zombie.isSlowed,
|
||||
gameState && gameState.tree ? gameState.tree.cheats : null
|
||||
);
|
||||
}
|
||||
|
||||
const pts = zombie.type === 'buckethead' || zombie.type === 'football' ? 300 : (zombie.type === 'conehead' ? 200 : 100);
|
||||
gameState.addScore(pts);
|
||||
gameState.zombiesKilled++;
|
||||
if (typeof gameState.recordStat === 'function') {
|
||||
gameState.recordStat('zombiesKilled', 1);
|
||||
}
|
||||
|
||||
// Achievement: zombie_slayer
|
||||
if (gameState.achievements) {
|
||||
@@ -363,5 +559,8 @@ window.CombatSystem = class CombatSystem {
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
}
|
||||
for (const h of this.heads) {
|
||||
h.draw(ctx);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user