Files

1491 lines
43 KiB
JavaScript

// Plant Entities (Sunflower, Peashooter, SnowPea, Wallnut, CherryBomb, PotatoMine, Repeater, Chomper, Jalapeno, Sunshroom, Fumeshroom, Lilypad, Gravebuster)
window.Plant = class Plant {
constructor(type, col, row, gridX, gridY, tileSizeX, tileSizeY, level = 1) {
this.type = type;
this.col = col;
this.row = row;
this.x = gridX + tileSizeX / 2;
this.y = gridY + tileSizeY / 2;
this.width = tileSizeX;
this.height = tileSizeY;
this.level = level || 1;
this.timer = 0;
this.animTime = Math.random() * 10;
this.toRemove = false;
// Attributes by type & level
this.maxHealth = 300;
this.damage = 20;
this.armorReduction = 0;
this.recoilDps = 0;
switch (type) {
case 'mega_sunflower':
this.maxHealth = 5000;
this.sunInterval = 5;
this.sunAmount = 100;
this.sunTimer = 2;
this.isRaidBoss = true;
break;
case 'sunflower':
this.sunInterval = this.level === 3 ? 9 : (this.level === 2 ? 10 : 13);
this.sunAmount = this.level >= 2 ? 35 : 25;
this.sunTimer = 3;
break;
case 'peashooter':
this.shootInterval = this.level === 3 ? 1.05 : (this.level === 2 ? 1.15 : 1.4);
this.damage = this.level === 3 ? 30 : (this.level === 2 ? 25 : 20);
this.critChance = this.level === 2 ? 0.15 : 0;
this.shootTimer = 0.5;
break;
case 'snowpea':
this.shootInterval = this.level >= 2 ? 1.25 : 1.4;
this.damage = this.level >= 2 ? 25 : 20;
this.shootTimer = 0.5;
break;
case 'repeater':
this.shootInterval = 1.35;
this.damage = 20;
this.shootTimer = 0.5;
this.repeaterSecondPea = 0;
this.repeaterThirdPea = 0;
break;
case 'wallnut':
this.maxHealth = this.level === 3 ? 2400 : (this.level === 2 ? 1800 : 1200);
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;
break;
case 'potatomine':
this.armTime = this.level === 3 ? 4.0 : (this.level === 2 ? 6.0 : 14.0);
this.armTimer = 0;
this.isArmed = false;
break;
case 'chomper':
this.maxHealth = 450;
this.isChewing = false;
this.chewTimer = 0;
this.chewDuration = this.level >= 2 ? 14.0 : 25.0;
this.bitesLeft = this.level === 3 ? 2 : 1;
break;
case 'jalapeno':
this.fuse = 0.9;
this.fuseTimer = 0;
break;
case 'sunshroom':
this.age = 0;
this.sunInterval = 12;
this.sunTimer = 3;
this.isBig = false;
break;
case 'fumeshroom':
this.shootInterval = 1.5;
this.shootTimer = 0.5;
break;
case 'lilypad':
this.maxHealth = 300;
this.isLilypad = true;
break;
case 'gravebuster':
this.maxHealth = 300;
this.bustTimer = 0;
this.bustDuration = 5.0;
break;
case 'flowerpot':
this.maxHealth = 300;
this.isPot = true;
break;
case 'cabbagepult':
this.damage = 40;
this.shootInterval = 2.0;
this.shootTimer = 0.8;
break;
case 'melonpult':
this.damage = 80;
this.shootInterval = 2.4;
this.shootTimer = 1.0;
break;
}
if (window.PvZConfig) {
const hpMult = window.PvZConfig.getPlantHpMult();
const dmgMult = window.PvZConfig.getPlantDamageMult();
const sunIntMult = window.PvZConfig.getSunIntervalMult();
this.maxHealth = Math.round(this.maxHealth * hpMult);
this.damage = Math.round(this.damage * dmgMult);
if (this.sunInterval) this.sunInterval = this.sunInterval * sunIntMult;
}
this.health = this.maxHealth;
}
update(dt, gameState) {
this.animTime += dt;
if (this.health <= 0) {
this.toRemove = true;
return;
}
// 1. Sunflower, Mega Sunflower & Sun-shroom
if (this.type === 'sunflower' || this.type === 'mega_sunflower') {
this.sunTimer += dt;
if (this.sunTimer >= this.sunInterval) {
this.sunTimer = 0;
gameState.spawnSun(this.x + (Math.random() * 20 - 10), this.y + (Math.random() * 20 - 10), this.sunAmount || 25, false);
if (this.type === 'mega_sunflower') {
// Raid Boss solar pulse: heal all plants on the lawn
for (const p of gameState.plants) {
if (p !== this && p.health > 0) {
p.health = Math.min(p.maxHealth, p.health + 25);
if (gameState.combat && gameState.combat.createSplash) {
gameState.combat.createSplash(p.x, p.y, '#ffd54f', 4);
}
}
}
} else if (this.level === 3) {
// Tier 3: Heal neighboring plants!
for (const p of gameState.plants) {
if (p !== this && Math.abs(p.col - this.col) <= 1 && Math.abs(p.row - this.row) <= 1 && p.health > 0) {
p.health = Math.min(p.maxHealth, p.health + 30);
gameState.combat.createSplash(p.x, p.y, '#80deea', 6);
}
}
}
}
} else if (this.type === 'sunshroom') {
this.age += dt;
if (this.age > 50 && !this.isBig) {
this.isBig = true;
}
this.sunTimer += dt;
if (this.sunTimer >= this.sunInterval) {
this.sunTimer = 0;
const val = this.isBig ? 25 : 15;
gameState.spawnSun(this.x + (Math.random() * 16 - 8), this.y + (Math.random() * 16 - 8), val, false);
}
}
// 2. Peashooter & SnowPea
if (this.type === 'peashooter' || this.type === 'snowpea') {
this.shootTimer += dt;
if (this.shootTimer >= this.shootInterval) {
const hasZombie = gameState.zombies.some(z => z.row === this.row && z.x > this.x && z.health > 0 && !z.toRemove);
if (hasZombie) {
this.shootTimer = 0;
if (this.type === 'peashooter' && this.level === 3) {
// Tier 3: Plasma Peashooter
if (gameState.spawnProjectile) gameState.spawnProjectile(this.x + 24, this.y - 12, this.row, 'plasma', 0, this.damage);
else gameState.projectiles.push(new window.Projectile(this.x + 24, this.y - 12, this.row, 'plasma', 0, this.damage));
} else if (this.type === 'snowpea' && this.level === 3) {
// Tier 3: Freeze Snowpea
const deepFreeze = Math.random() < 0.2;
const pType = deepFreeze ? 'freeze_snowpea' : 'snowpea';
if (gameState.spawnProjectile) gameState.spawnProjectile(this.x + 24, this.y - 12, this.row, pType, 0, this.damage);
else gameState.projectiles.push(new window.Projectile(this.x + 24, this.y - 12, this.row, pType, 0, this.damage));
} else {
const isCrit = this.critChance && Math.random() < this.critChance;
const projType = this.type === 'snowpea' ? 'snowpea' : 'pea';
if (gameState.spawnProjectile) gameState.spawnProjectile(this.x + 24, this.y - 12, this.row, projType, 0, this.damage, isCrit);
else gameState.projectiles.push(new window.Projectile(this.x + 24, this.y - 12, this.row, projType, 0, this.damage, isCrit));
}
if (window.PvZAudio) window.PvZAudio.playShoot();
}
}
}
// 2.5 Cabbage-pult & Melon-pult (Lobbed Arc Shooters)
if (this.type === 'cabbagepult' || this.type === 'melonpult') {
this.shootTimer += dt;
if (this.shootTimer >= this.shootInterval) {
const targetZombie = gameState.zombies.find(z => z.row === this.row && z.x > this.x && z.health > 0 && !z.toRemove);
const hasBoss = gameState.boss && gameState.boss.isActive;
if (targetZombie || hasBoss) {
this.shootTimer = 0;
const targetX = targetZombie ? targetZombie.x : (hasBoss ? gameState.boss.x : 750);
const projType = this.type === 'melonpult' ? 'melon' : 'cabbage';
if (gameState.spawnProjectile) gameState.spawnProjectile(this.x + 15, this.y - 10, this.row, projType, 0, this.damage, false, targetX);
else gameState.projectiles.push(new window.Projectile(this.x + 15, this.y - 10, this.row, projType, 0, this.damage, false, targetX));
if (window.PvZAudio) window.PvZAudio.playShoot();
}
}
}
// 3. Repeater (Double Pea)
if (this.type === 'repeater') {
this.shootTimer += dt;
if (this.repeaterThirdPea > 0) {
this.repeaterThirdPea -= dt;
if (this.repeaterThirdPea <= 0) {
const pType = this.level === 3 ? 'firepea' : 'pea';
if (gameState.spawnProjectile) gameState.spawnProjectile(this.x + 24, this.y - 12, this.row, pType, 0, 22);
else gameState.projectiles.push(new window.Projectile(this.x + 24, this.y - 12, this.row, pType, 0, 22));
if (window.PvZAudio) window.PvZAudio.playShoot();
}
} else if (this.repeaterSecondPea > 0) {
this.repeaterSecondPea -= dt;
if (this.repeaterSecondPea <= 0) {
const pType = this.level === 3 ? 'firepea' : 'pea';
if (gameState.spawnProjectile) gameState.spawnProjectile(this.x + 24, this.y - 12, this.row, pType, 0, 22);
else gameState.projectiles.push(new window.Projectile(this.x + 24, this.y - 12, this.row, pType, 0, 22));
if (window.PvZAudio) window.PvZAudio.playShoot();
}
} else if (this.shootTimer >= this.shootInterval) {
const hasZombie = gameState.zombies.some(z => z.row === this.row && z.x > this.x && z.health > 0 && !z.toRemove);
if (hasZombie) {
this.shootTimer = 0;
const pType = this.level === 3 ? 'firepea' : 'pea';
if (gameState.spawnProjectile) gameState.spawnProjectile(this.x + 24, this.y - 12, this.row, pType, 0, 22);
else gameState.projectiles.push(new window.Projectile(this.x + 24, this.y - 12, this.row, pType, 0, 22));
this.repeaterSecondPea = 0.16;
if (this.level >= 2 && Math.random() < 0.25) {
this.repeaterThirdPea = 0.32; // Tier 2+ bonus 3rd pea!
}
if (window.PvZAudio) window.PvZAudio.playShoot();
}
}
}
// 4. Fume-shroom (Piercing Spore Cloud)
if (this.type === 'fumeshroom') {
this.shootTimer += dt;
if (this.shootTimer >= this.shootInterval) {
const hasZombie = gameState.zombies.some(z => z.row === this.row && z.x > this.x && (z.x - this.x) < 330 && z.health > 0 && !z.toRemove);
if (hasZombie) {
this.shootTimer = 0;
if (gameState.spawnProjectile) gameState.spawnProjectile(this.x + 20, this.y - 8, this.row, 'fume');
else gameState.projectiles.push(new window.Projectile(this.x + 20, this.y - 8, this.row, 'fume'));
if (window.PvZAudio) window.PvZAudio.playShoot();
}
}
}
// 5. Potato Mine
if (this.type === 'potatomine') {
if (!this.isArmed) {
this.armTimer += dt;
if (this.armTimer >= this.armTime) {
this.isArmed = true;
}
} else {
// Explode if zombie steps on it
const victim = gameState.zombies.find(z => z.row === this.row && Math.abs(z.x - this.x) < 35 && z.health > 0 && !z.toRemove);
if (victim) {
this.toRemove = true;
gameState.combat.createExplosionParticles(this.x, this.y);
if (window.PvZAudio) window.PvZAudio.playMine();
if (gameState.achievements) gameState.achievements.trigger('spudow_master');
if (this.level === 3) {
// Tier 3: 3x3 Explosion!
gameState.explodeArea(this.col, this.row, 3);
} else {
for (const z of gameState.zombies) {
if (z.row === this.row && Math.abs(z.x - this.x) < 45 && z.health > 0 && !z.toRemove) {
z.takeDamage(1800);
gameState.addScore(250);
gameState.zombiesKilled++;
if (typeof gameState.recordStat === 'function') gameState.recordStat('zombiesKilled', 1);
}
}
}
}
}
}
// 6. Chomper (Instant Swallow)
if (this.type === 'chomper') {
if (this.isChewing) {
this.chewTimer += dt;
if (this.chewTimer >= this.chewDuration) {
this.isChewing = false;
this.chewTimer = 0;
this.bitesLeft = this.level === 3 ? 2 : 1;
}
} else {
const victim = gameState.zombies.find(z => z.row === this.row && z.x > this.x && (z.x - this.x) < 110 && z.health > 0 && !z.toRemove);
if (victim) {
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) {
this.bitesLeft--;
// Still ready for another bite!
} else {
this.isChewing = true;
this.chewTimer = 0;
}
}
}
}
// 7. Cherry Bomb
if (this.type === 'cherrybomb') {
this.fuseTimer += dt;
if (this.fuseTimer >= this.fuse) {
this.toRemove = true;
gameState.explodeArea(this.col, this.row, 3);
if (window.PvZAudio) window.PvZAudio.playExplosion();
}
}
// 8. Jalapeno (Full Row Sizzle)
if (this.type === 'jalapeno') {
this.fuseTimer += dt;
if (this.fuseTimer >= this.fuse) {
this.toRemove = true;
if (window.PvZAudio) window.PvZAudio.playSizzle();
// Fiery blast along entire row
for (let c = 0; c < gameState.grid.cols; c++) {
const bounds = gameState.grid.getCellBounds(c, this.row);
gameState.combat.createExplosionParticles(bounds.centerX, bounds.centerY);
}
let killedAny = false;
for (const z of gameState.zombies) {
if (z.row === this.row && z.health > 0 && !z.toRemove) {
z.takeDamage(1800);
gameState.addScore(250);
gameState.zombiesKilled++;
if (typeof gameState.recordStat === 'function') gameState.recordStat('zombiesKilled', 1);
killedAny = true;
}
}
if (killedAny && gameState.achievements) {
gameState.achievements.trigger('fire_jalapeno');
}
}
}
// 9. Grave Buster
if (this.type === 'gravebuster') {
this.bustTimer += dt;
if (this.bustTimer >= this.bustDuration) {
this.toRemove = true;
gameState.grid.removeGrave(this.col, this.row);
gameState.spawnCoin(this.x, this.y, 'gold');
if (window.PvZAudio) window.PvZAudio.playCoin();
}
}
}
takeDamage(amount) {
const reduced = amount * (1 - (this.armorReduction || 0));
this.health -= reduced;
if (this.health <= 0) {
this.toRemove = true;
}
}
draw(ctx) {
ctx.save();
ctx.translate(this.x, this.y);
// Ground shadow
ctx.fillStyle = 'rgba(0, 0, 0, 0.25)';
ctx.beginPath();
ctx.ellipse(0, 32, 28, 10, 0, 0, Math.PI * 2);
ctx.fill();
const sway = Math.sin(this.animTime * 3) * 0.05;
ctx.rotate(sway);
switch (this.type) {
case 'sunflower':
this.drawSunflower(ctx);
break;
case 'peashooter':
this.drawPeashooter(ctx, false);
break;
case 'snowpea':
this.drawPeashooter(ctx, true);
break;
case 'repeater':
this.drawRepeater(ctx);
break;
case 'wallnut':
this.drawWallnut(ctx);
break;
case 'tallnut':
this.drawTallnut(ctx);
break;
case 'cherrybomb':
this.drawCherryBomb(ctx);
break;
case 'potatomine':
this.drawPotatoMine(ctx);
break;
case 'chomper':
this.drawChomper(ctx);
break;
case 'jalapeno':
this.drawJalapeno(ctx);
break;
case 'sunshroom':
this.drawSunshroom(ctx);
break;
case 'fumeshroom':
this.drawFumeshroom(ctx);
break;
case 'lilypad':
this.drawLilypad(ctx);
break;
case 'gravebuster':
this.drawGravebuster(ctx);
break;
case 'flowerpot':
this.drawFlowerPot(ctx);
break;
case 'cabbagepult':
this.drawCabbagepult(ctx);
break;
case 'melonpult':
this.drawMelonpult(ctx);
break;
case 'mega_sunflower':
this.drawMegaSunflower(ctx);
break;
}
// Tier Stars (Level 2 & 3)
if (this.level > 1) {
ctx.font = 'bold 12px sans-serif';
ctx.textAlign = 'center';
ctx.fillStyle = this.level === 3 ? '#00e5ff' : '#ffd54f';
ctx.shadowColor = this.level === 3 ? 'rgba(0, 229, 255, 0.8)' : 'rgba(255, 213, 79, 0.8)';
ctx.shadowBlur = 4;
const stars = this.level === 3 ? '★★★' : '★★';
ctx.fillText(stars, 0, -42);
ctx.shadowBlur = 0;
}
ctx.restore();
}
drawSunflower(ctx) {
ctx.fillStyle = '#4caf50';
ctx.beginPath();
ctx.ellipse(-18, 18, 16, 7, -0.4, 0, Math.PI * 2);
ctx.ellipse(18, 18, 16, 7, 0.4, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = '#2e7d32';
ctx.lineWidth = 6;
ctx.beginPath();
ctx.moveTo(0, 30);
ctx.quadraticCurveTo(-4, 10, 0, -5);
ctx.stroke();
const petalCount = 12;
const petalPulse = 1 + Math.sin(this.animTime * 4) * 0.06;
const isAboutToSpawn = (this.sunInterval - this.sunTimer) < 1.5;
ctx.save();
ctx.translate(0, -10);
for (let i = 0; i < petalCount; i++) {
const angle = (i * (Math.PI * 2) / petalCount) + this.animTime * 0.4;
ctx.save();
ctx.rotate(angle);
ctx.fillStyle = isAboutToSpawn ? '#fff59d' : '#fbc02d';
ctx.beginPath();
ctx.ellipse(0, -22 * petalPulse, 8, 14 * petalPulse, 0, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
ctx.fillStyle = isAboutToSpawn ? '#8d6e63' : '#5d4037';
ctx.beginPath();
ctx.arc(0, 0, 18, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#212121';
ctx.beginPath();
ctx.arc(-6, -2, 2.5, 0, Math.PI * 2);
ctx.arc(6, -2, 2.5, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = '#212121';
ctx.lineWidth = 1.8;
ctx.beginPath();
ctx.arc(0, 2, 8, 0.2, Math.PI - 0.2);
ctx.stroke();
ctx.restore();
}
drawMegaSunflower(ctx) {
// Grand glowing aura
const glowGradient = ctx.createRadialGradient(0, -10, 10, 0, -10, 60);
glowGradient.addColorStop(0, 'rgba(255, 215, 0, 0.6)');
glowGradient.addColorStop(0.7, 'rgba(255, 152, 0, 0.3)');
glowGradient.addColorStop(1, 'rgba(255, 100, 0, 0)');
ctx.fillStyle = glowGradient;
ctx.beginPath();
ctx.arc(0, -10, 60, 0, Math.PI * 2);
ctx.fill();
// Leaves / base
ctx.fillStyle = '#2e7d32';
ctx.beginPath();
ctx.ellipse(-26, 22, 22, 10, -0.4, 0, Math.PI * 2);
ctx.ellipse(26, 22, 22, 10, 0.4, 0, Math.PI * 2);
ctx.fill();
// Big stem
ctx.strokeStyle = '#1b5e20';
ctx.lineWidth = 10;
ctx.beginPath();
ctx.moveTo(0, 32);
ctx.quadraticCurveTo(-5, 12, 0, -5);
ctx.stroke();
// 16 Petals with pulse and slow rotation
const petalCount = 16;
const pulse = 1 + Math.sin(this.animTime * 3) * 0.08;
ctx.save();
ctx.translate(0, -10);
ctx.scale(pulse, pulse);
for (let i = 0; i < petalCount; i++) {
const angle = (i * (Math.PI * 2) / petalCount) + this.animTime * 0.3;
ctx.save();
ctx.rotate(angle);
// Outer petal
ctx.fillStyle = i % 2 === 0 ? '#ffca28' : '#ffa000';
ctx.beginPath();
ctx.ellipse(0, -32, 11, 20, 0, 0, Math.PI * 2);
ctx.fill();
// Inner petal highlight
ctx.fillStyle = '#fff59d';
ctx.beginPath();
ctx.ellipse(0, -30, 5, 12, 0, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
// Flower Center
ctx.fillStyle = '#5d4037';
ctx.beginPath();
ctx.arc(0, 0, 24, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = '#8d6e63';
ctx.lineWidth = 3;
ctx.stroke();
// Smiling Face & Eyes
ctx.fillStyle = '#212121';
ctx.beginPath();
ctx.arc(-8, -4, 4, 0, Math.PI * 2);
ctx.arc(8, -4, 4, 0, Math.PI * 2);
ctx.fill();
// Eye sparkles
ctx.fillStyle = '#ffffff';
ctx.beginPath();
ctx.arc(-9, -6, 1.5, 0, Math.PI * 2);
ctx.arc(7, -6, 1.5, 0, Math.PI * 2);
ctx.fill();
// Big Royal Smile
ctx.strokeStyle = '#3e2723';
ctx.lineWidth = 2.5;
ctx.beginPath();
ctx.arc(0, 5, 9, 0.2, Math.PI - 0.2);
ctx.stroke();
// Royal Crown
ctx.fillStyle = '#ffd700';
ctx.strokeStyle = '#e65100';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(-16, -22);
ctx.lineTo(-14, -36);
ctx.lineTo(-6, -26);
ctx.lineTo(0, -40);
ctx.lineTo(6, -26);
ctx.lineTo(14, -36);
ctx.lineTo(16, -22);
ctx.closePath();
ctx.fill();
ctx.stroke();
// Crown rubies
ctx.fillStyle = '#e91e63';
ctx.beginPath();
ctx.arc(0, -32, 2.5, 0, Math.PI * 2);
ctx.arc(-10, -28, 2, 0, Math.PI * 2);
ctx.arc(10, -28, 2, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
// Raid Boss Overhead Health Bar & Label
const barWidth = 68;
const barHeight = 8;
const barX = -barWidth / 2;
const barY = -54;
const hpRatio = Math.max(0, this.health / this.maxHealth);
// Dark background
ctx.fillStyle = 'rgba(0, 0, 0, 0.75)';
ctx.fillRect(barX - 1, barY - 1, barWidth + 2, barHeight + 2);
// Health bar with gradient
const hpGrad = ctx.createLinearGradient(barX, barY, barX + barWidth, barY);
hpGrad.addColorStop(0, '#ffd700');
hpGrad.addColorStop(1, '#ff3d00');
ctx.fillStyle = hpGrad;
ctx.fillRect(barX, barY, barWidth * hpRatio, barHeight);
// Border
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 1;
ctx.strokeRect(barX - 1, barY - 1, barWidth + 2, barHeight + 2);
// Boss Title
ctx.font = 'bold 9px sans-serif';
ctx.textAlign = 'center';
ctx.fillStyle = '#ffe082';
ctx.shadowColor = 'rgba(0, 0, 0, 0.8)';
ctx.shadowBlur = 3;
ctx.fillText(`👑 MEGA SUNFLOWER [${Math.ceil(this.health)}]`, 0, barY - 4);
ctx.shadowBlur = 0;
}
drawPeashooter(ctx, isSnow) {
const mainColor = isSnow ? '#29b6f6' : '#4caf50';
const darkColor = isSnow ? '#0288d1' : '#2e7d32';
ctx.fillStyle = darkColor;
ctx.beginPath();
ctx.ellipse(-20, 22, 16, 8, -0.3, 0, Math.PI * 2);
ctx.ellipse(20, 22, 16, 8, 0.3, 0, Math.PI * 2);
ctx.ellipse(0, 24, 18, 8, 0, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = darkColor;
ctx.lineWidth = 7;
ctx.beginPath();
ctx.moveTo(0, 24);
ctx.quadraticCurveTo(-10, 4, -4, -10);
ctx.stroke();
ctx.save();
ctx.translate(-4, -14);
const headSquish = Math.sin(this.animTime * 5) * 0.05;
ctx.scale(1 + headSquish, 1 - headSquish);
ctx.fillStyle = mainColor;
ctx.beginPath();
ctx.arc(0, 0, 20, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.ellipse(18, 0, 10, 13, 0, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = darkColor;
ctx.beginPath();
ctx.ellipse(22, 0, 5, 9, 0, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#ffffff';
ctx.beginPath();
ctx.arc(6, -8, 5, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#000000';
ctx.beginPath();
ctx.arc(8, -8, 2.5, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
drawRepeater(ctx) {
this.drawPeashooter(ctx, false);
// Extra top leaves/eyebrow
ctx.save();
ctx.translate(-4, -14);
ctx.fillStyle = '#1b5e20';
ctx.beginPath();
ctx.ellipse(-16, -12, 12, 6, -0.6, 0, Math.PI * 2);
ctx.ellipse(-14, 0, 10, 5, -0.2, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
drawWallnut(ctx) {
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();
// Subtle nut shell texture creases
ctx.strokeStyle = 'rgba(93, 64, 55, 0.4)';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.arc(0, 4, 18, 0.6, 2.5);
ctx.stroke();
// 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();
// 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();
} else if (healthRatio <= 0.33) {
// Severe deep cracks
ctx.strokeStyle = '#2b1b17';
ctx.lineWidth = 2.5;
ctx.beginPath();
// 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) {
if (!this.isArmed) {
// Underground sprout
ctx.fillStyle = '#795548';
ctx.beginPath();
ctx.ellipse(0, 28, 14, 6, 0, 0, Math.PI * 2);
ctx.fill();
// Little green antenna
ctx.strokeStyle = '#4caf50';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(0, 28);
ctx.lineTo(0, 14);
ctx.stroke();
ctx.fillStyle = '#f44336';
ctx.beginPath();
ctx.arc(0, 14, 4, 0, Math.PI * 2);
ctx.fill();
} else {
// Emerged Potato
ctx.fillStyle = '#a1887f';
ctx.beginPath();
ctx.ellipse(0, 16, 20, 16, 0, 0, Math.PI * 2);
ctx.fill();
// Flashing red beacon
const flash = Math.sin(this.animTime * 12) > 0;
ctx.strokeStyle = '#3e2723';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(0, 2);
ctx.lineTo(0, -6);
ctx.stroke();
ctx.fillStyle = flash ? '#ff1744' : '#b71c1c';
ctx.beginPath();
ctx.arc(0, -6, 5, 0, Math.PI * 2);
ctx.fill();
// Eyes
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.arc(-6, 12, 4, 0, Math.PI * 2);
ctx.arc(6, 12, 4, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#000';
ctx.beginPath();
ctx.arc(-5, 12, 2, 0, Math.PI * 2);
ctx.arc(7, 12, 2, 0, Math.PI * 2);
ctx.fill();
}
}
drawChomper(ctx) {
// Stem
ctx.strokeStyle = '#2e7d32';
ctx.lineWidth = 8;
ctx.beginPath();
ctx.moveTo(0, 28);
ctx.quadraticCurveTo(-14, 0, -4, -12);
ctx.stroke();
// Purple Head
ctx.save();
ctx.translate(-4, -16);
const jawOpen = this.isChewing ? Math.sin(this.animTime * 10) * 0.15 + 0.1 : 0.45;
// Upper head
ctx.save();
ctx.rotate(-jawOpen);
ctx.fillStyle = '#7b1fa2';
ctx.beginPath();
ctx.arc(0, 0, 22, Math.PI, 0);
ctx.lineTo(26, 0);
ctx.fill();
// Teeth
ctx.fillStyle = '#fff';
for (let i = 0; i < 4; i++) {
ctx.beginPath();
ctx.moveTo(i * 6, 0);
ctx.lineTo(i * 6 + 3, 6);
ctx.lineTo(i * 6 + 6, 0);
ctx.fill();
}
ctx.restore();
// Lower jaw
ctx.save();
ctx.rotate(jawOpen);
ctx.fillStyle = '#4a148c';
ctx.beginPath();
ctx.arc(0, 4, 18, 0, Math.PI);
ctx.fill();
ctx.restore();
ctx.restore();
}
drawJalapeno(ctx) {
const pulse = 1 + Math.sin(this.animTime * 15) * 0.1;
ctx.scale(pulse, pulse);
// Green cap
ctx.fillStyle = '#2e7d32';
ctx.beginPath();
ctx.arc(0, -26, 8, Math.PI, 0);
ctx.fill();
// Red pepper body
ctx.fillStyle = '#d50000';
ctx.beginPath();
ctx.moveTo(-10, -22);
ctx.quadraticCurveTo(-14, 10, 0, 28);
ctx.quadraticCurveTo(14, 10, 10, -22);
ctx.closePath();
ctx.fill();
// Furious Face
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.arc(-4, -6, 3, 0, Math.PI * 2);
ctx.arc(4, -6, 3, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#000';
ctx.beginPath();
ctx.arc(-4, -6, 1.5, 0, Math.PI * 2);
ctx.arc(4, -6, 1.5, 0, Math.PI * 2);
ctx.fill();
}
drawSunshroom(ctx) {
const scale = this.isBig ? 1.0 : 0.65;
ctx.scale(scale, scale);
// Stalk
ctx.fillStyle = '#fff9c4';
ctx.fillRect(-6, 4, 12, 22);
// Glowing Cap
ctx.fillStyle = '#fbc02d';
ctx.beginPath();
ctx.arc(0, 6, 18, Math.PI, 0);
ctx.fill();
// Orange spots
ctx.fillStyle = '#f57f17';
ctx.beginPath();
ctx.arc(-6, -2, 4, 0, Math.PI * 2);
ctx.arc(6, -2, 4, 0, Math.PI * 2);
ctx.fill();
}
drawFumeshroom(ctx) {
// Stalk
ctx.fillStyle = '#e1bee7';
ctx.fillRect(-7, 4, 14, 22);
// Purple Cap
ctx.fillStyle = '#8e24aa';
ctx.beginPath();
ctx.arc(0, 4, 20, Math.PI, 0);
ctx.fill();
// Spout snout
ctx.fillStyle = '#6a1b9a';
ctx.beginPath();
ctx.ellipse(16, 2, 7, 10, 0, 0, Math.PI * 2);
ctx.fill();
}
drawLilypad(ctx) {
ctx.fillStyle = '#2e7d32';
ctx.beginPath();
ctx.ellipse(0, 24, 32, 16, 0, 0.4, Math.PI * 2 - 0.4);
ctx.lineTo(0, 24);
ctx.closePath();
ctx.fill();
ctx.strokeStyle = '#1b5e20';
ctx.lineWidth = 2;
ctx.stroke();
}
drawGravebuster(ctx) {
// Green roots wrapping around grave
ctx.strokeStyle = '#33691e';
ctx.lineWidth = 5;
ctx.beginPath();
ctx.arc(0, 0, 24, 0, Math.PI * 2);
ctx.stroke();
}
drawCherryBomb(ctx) {
const pulse = 1 + (this.fuseTimer / this.fuse) * 0.4 + Math.sin(this.animTime * 15) * 0.1;
ctx.scale(pulse, pulse);
ctx.strokeStyle = '#2e7d32';
ctx.lineWidth = 4;
ctx.beginPath();
ctx.moveTo(0, -22);
ctx.lineTo(-14, 0);
ctx.moveTo(0, -22);
ctx.lineTo(14, 0);
ctx.stroke();
const flash = Math.sin(this.animTime * 20) > 0;
const cherryColor = flash ? '#ff1744' : '#b71c1c';
ctx.fillStyle = cherryColor;
ctx.beginPath();
ctx.arc(-14, 4, 16, 0, Math.PI * 2);
ctx.arc(14, 4, 16, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.arc(-16, 2, 4, 0, Math.PI * 2);
ctx.arc(12, 2, 4, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#000';
ctx.beginPath();
ctx.arc(-15, 2, 2, 0, Math.PI * 2);
ctx.arc(13, 2, 2, 0, Math.PI * 2);
ctx.fill();
}
drawFlowerPot(ctx) {
// Terracotta clay pot
ctx.fillStyle = '#bf360c';
ctx.beginPath();
ctx.moveTo(-24, 8);
ctx.lineTo(24, 8);
ctx.lineTo(18, 32);
ctx.lineTo(-18, 32);
ctx.closePath();
ctx.fill();
// Rim
ctx.fillStyle = '#d84315';
ctx.beginPath();
ctx.roundRect(-26, 2, 52, 9, 3);
ctx.fill();
// Dark soil inside
ctx.fillStyle = '#3e2723';
ctx.beginPath();
ctx.ellipse(0, 5, 20, 4, 0, 0, Math.PI * 2);
ctx.fill();
// Little green leaf sprout on pot edge
ctx.fillStyle = '#66bb6a';
ctx.beginPath();
ctx.ellipse(14, 2, 6, 3, -0.5, 0, Math.PI * 2);
ctx.fill();
}
drawCabbagepult(ctx) {
const bob = Math.sin(this.animTime * 3) * 1.5;
// Wooden catapult basket frame
ctx.fillStyle = '#5d4037';
ctx.fillRect(-16, 12, 32, 10);
ctx.fillStyle = '#8d6e63';
ctx.beginPath();
ctx.roundRect(-12, 0, 24, 14, 3);
ctx.fill();
// Catapult Arm
const armAngle = -0.4 + Math.sin(this.animTime * 4) * 0.1;
ctx.save();
ctx.translate(-4, 6);
ctx.rotate(armAngle);
ctx.strokeStyle = '#4e342e';
ctx.lineWidth = 4;
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(-24, -18);
ctx.stroke();
// Spoon basket
ctx.fillStyle = '#8d6e63';
ctx.beginPath();
ctx.arc(-26, -18, 10, 0, Math.PI * 2);
ctx.fill();
// Cabbage inside basket
ctx.fillStyle = '#4caf50';
ctx.beginPath();
ctx.arc(-26, -20, 8, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
// Main Cabbage Head Face
ctx.fillStyle = '#81c784';
ctx.beginPath();
ctx.arc(6, -10 + bob, 18, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = '#388e3c';
ctx.lineWidth = 2;
ctx.stroke();
// Eyes
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.arc(2, -12 + bob, 4, 0, Math.PI * 2);
ctx.arc(12, -12 + bob, 4, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#000';
ctx.beginPath();
ctx.arc(3, -12 + bob, 2, 0, Math.PI * 2);
ctx.arc(13, -12 + bob, 2, 0, Math.PI * 2);
ctx.fill();
}
drawMelonpult(ctx) {
const bob = Math.sin(this.animTime * 2.5) * 2;
// Heavy wooden frame
ctx.fillStyle = '#3e2723';
ctx.fillRect(-22, 10, 44, 14);
ctx.fillStyle = '#5d4037';
ctx.beginPath();
ctx.roundRect(-16, -2, 32, 16, 4);
ctx.fill();
// Heavy catapult lever
const armAngle = -0.5 + Math.sin(this.animTime * 3) * 0.1;
ctx.save();
ctx.translate(-6, 4);
ctx.rotate(armAngle);
ctx.strokeStyle = '#263238';
ctx.lineWidth = 6;
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(-28, -22);
ctx.stroke();
// Heavy steel basket
ctx.fillStyle = '#455a64';
ctx.beginPath();
ctx.arc(-30, -24, 14, 0, Math.PI * 2);
ctx.fill();
// Watermelon inside basket
ctx.fillStyle = '#2e7d32';
ctx.beginPath();
ctx.ellipse(-30, -26, 12, 9, 0, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = '#a5d6a7';
ctx.lineWidth = 2;
ctx.stroke();
ctx.restore();
// Melonpult Head Face
ctx.fillStyle = '#388e3c';
ctx.beginPath();
ctx.ellipse(8, -14 + bob, 22, 17, 0, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = '#a5d6a7';
ctx.lineWidth = 2.5;
ctx.stroke();
// Eyes
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.arc(4, -16 + bob, 4.5, 0, Math.PI * 2);
ctx.arc(15, -16 + bob, 4.5, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#000';
ctx.beginPath();
ctx.arc(5, -16 + bob, 2, 0, Math.PI * 2);
ctx.arc(16, -16 + bob, 2, 0, Math.PI * 2);
ctx.fill();
}
};