feat: Initial commit Plants vs Zombies Web Edition v2.4
- 5 Biomes: Day, Night, Pool, Fog, Roof (25 Campaign levels) - Minigames: Wall-nut Bowling, I Zombie, Roguelike, Boss Rush & Dr. Zomboss - Systems: Zen Garden, Workshop Crafting, Almanac, Tree of Wisdom, Achievements - Backend: REST API + SQLite Accounts & Save Synchronization - Infrastructure: Nginx HTTPS & Docker deployment
This commit is contained in:
commit
6457a8904e
50 files changed
+14486
No files matched your search
@@ -0,0 +1,970 @@
|
||||
// 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 '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 '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;
|
||||
}
|
||||
this.health = this.maxHealth;
|
||||
}
|
||||
|
||||
update(dt, gameState) {
|
||||
this.animTime += dt;
|
||||
|
||||
if (this.health <= 0) {
|
||||
this.toRemove = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. Sunflower & Sun-shroom
|
||||
if (this.type === '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);
|
||||
|
||||
// Tier 3: Heal neighboring plants!
|
||||
if (this.level === 3) {
|
||||
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
|
||||
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;
|
||||
gameState.projectiles.push(new window.Projectile(this.x + 24, this.y - 12, this.row, deepFreeze ? 'freeze_snowpea' : 'snowpea', 0, this.damage));
|
||||
} else {
|
||||
const isCrit = this.critChance && Math.random() < this.critChance;
|
||||
const projType = this.type === 'snowpea' ? 'snowpea' : 'pea';
|
||||
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';
|
||||
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';
|
||||
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';
|
||||
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';
|
||||
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;
|
||||
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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 (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++;
|
||||
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 '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;
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
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 = this.health / this.maxHealth;
|
||||
ctx.fillStyle = '#8d6e63';
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(0, 2, 22, 28, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = '#a1887f';
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(-6, -4, 14, 20, -0.2, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
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();
|
||||
|
||||
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.stroke();
|
||||
|
||||
if (healthRatio <= 0.66) {
|
||||
ctx.strokeStyle = '#3e2723';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(12, -18);
|
||||
ctx.lineTo(6, -6);
|
||||
ctx.lineTo(14, 4);
|
||||
ctx.stroke();
|
||||
}
|
||||
if (healthRatio <= 0.33) {
|
||||
ctx.strokeStyle = '#3e2723';
|
||||
ctx.lineWidth = 2.5;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-14, -14);
|
||||
ctx.lineTo(-6, -2);
|
||||
ctx.lineTo(-12, 10);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,214 @@
|
||||
window.Projectile = class Projectile {
|
||||
constructor(x, y, row, type = 'pea', vy = 0, damage = 20, isCrit = false, targetX = 750) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.startX = x;
|
||||
this.baseY = y;
|
||||
this.targetX = targetX;
|
||||
this.row = row;
|
||||
this.type = type;
|
||||
this.vy = vy; // for bouncing bowling ball
|
||||
this.isCrit = isCrit;
|
||||
this.isLobbed = type === 'cabbage' || type === 'melon';
|
||||
this.apex = type === 'melon' ? 95 : (type === 'cabbage' ? 85 : 0);
|
||||
|
||||
let baseRadius = 9;
|
||||
let baseSpeed = 380;
|
||||
let baseDamage = damage;
|
||||
|
||||
if (type === 'bowling') {
|
||||
baseRadius = 22; baseSpeed = 440; baseDamage = 400;
|
||||
} else if (type === 'fume') {
|
||||
baseRadius = 16; baseSpeed = 280;
|
||||
} else if (type === 'plasma' || type === 'firepea') {
|
||||
baseRadius = 12; baseSpeed = 460;
|
||||
} else if (type === 'cabbage') {
|
||||
baseRadius = 13; baseSpeed = 340; baseDamage = 40;
|
||||
} else if (type === 'melon') {
|
||||
baseRadius = 17; baseSpeed = 320; baseDamage = 80;
|
||||
}
|
||||
|
||||
this.radius = isCrit ? baseRadius + 2 : baseRadius;
|
||||
this.speed = baseSpeed;
|
||||
this.damage = type === 'bowling' ? 400 : (isCrit ? baseDamage * 2 : baseDamage);
|
||||
this.splashDamage = type === 'melon' ? 26 : 0;
|
||||
this.splashRadius = type === 'melon' ? 80 : 0;
|
||||
this.slows = type === 'snowpea';
|
||||
this.freezes = type === 'freeze_snowpea';
|
||||
this.pierces = type === 'fume' || type === 'plasma';
|
||||
this.maxPierces = type === 'plasma' ? 2 : 999;
|
||||
this.pierceHits = new Set();
|
||||
this.lobProgress = 0;
|
||||
this.toRemove = false;
|
||||
this.animTime = 0;
|
||||
}
|
||||
|
||||
update(dt) {
|
||||
this.animTime += dt;
|
||||
this.x += this.speed * dt;
|
||||
|
||||
if (this.isLobbed) {
|
||||
const totalDist = Math.max(80, this.targetX - this.startX);
|
||||
const curDist = this.x - this.startX;
|
||||
const t = Math.max(0, Math.min(1, curDist / totalDist));
|
||||
this.lobProgress = t;
|
||||
this.y = this.baseY - 4 * this.apex * t * (1 - t);
|
||||
} else {
|
||||
this.y += this.vy * dt;
|
||||
}
|
||||
|
||||
// Fume fades out after ~340px
|
||||
if (this.type === 'fume' && (this.x - this.startX) > 340) {
|
||||
this.toRemove = true;
|
||||
}
|
||||
|
||||
// Bowling wall bounce
|
||||
if (this.type === 'bowling') {
|
||||
if (this.y < 120 && this.vy < 0) {
|
||||
this.vy = -this.vy;
|
||||
this.row = 0;
|
||||
} else if (this.y > 550 && this.vy > 0) {
|
||||
this.vy = -this.vy;
|
||||
this.row = 4;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.x > 960 || this.x < -40) {
|
||||
this.toRemove = true;
|
||||
}
|
||||
}
|
||||
|
||||
draw(ctx) {
|
||||
ctx.save();
|
||||
if (this.type === 'snowpea' || this.type === 'freeze_snowpea') {
|
||||
// Ice pea glow
|
||||
const grad = ctx.createRadialGradient(this.x - 2, this.y - 2, 2, this.x, this.y, this.radius);
|
||||
grad.addColorStop(0, '#e0f7fa');
|
||||
grad.addColorStop(0.6, '#4dd0e1');
|
||||
grad.addColorStop(1, '#00838f');
|
||||
ctx.fillStyle = grad;
|
||||
ctx.beginPath();
|
||||
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Ice sparkle trail
|
||||
ctx.fillStyle = 'rgba(178, 235, 242, 0.5)';
|
||||
ctx.beginPath();
|
||||
ctx.arc(this.x - 8, this.y, this.radius * 0.5, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
} else if (this.type === 'plasma') {
|
||||
// Tier 3 Plasma Peashooter
|
||||
const grad = ctx.createRadialGradient(this.x, this.y, 2, this.x, this.y, this.radius + 4);
|
||||
grad.addColorStop(0, '#ffffff');
|
||||
grad.addColorStop(0.4, '#00e5ff');
|
||||
grad.addColorStop(0.8, '#2979ff');
|
||||
grad.addColorStop(1, 'rgba(41, 121, 255, 0)');
|
||||
ctx.fillStyle = grad;
|
||||
ctx.beginPath();
|
||||
ctx.arc(this.x, this.y, this.radius + 4, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Electric spark trail
|
||||
ctx.strokeStyle = '#e0f7fa';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(this.x - 12, this.y);
|
||||
ctx.lineTo(this.x - 4, this.y + (Math.sin(this.animTime * 20) * 4));
|
||||
ctx.stroke();
|
||||
} else if (this.type === 'firepea') {
|
||||
// Tier 3 Fiery Repeater
|
||||
const grad = ctx.createRadialGradient(this.x, this.y, 2, this.x, this.y, this.radius + 4);
|
||||
grad.addColorStop(0, '#fff9c4');
|
||||
grad.addColorStop(0.4, '#ff9800');
|
||||
grad.addColorStop(0.8, '#d50000');
|
||||
grad.addColorStop(1, 'rgba(213, 0, 0, 0)');
|
||||
ctx.fillStyle = grad;
|
||||
ctx.beginPath();
|
||||
ctx.arc(this.x, this.y, this.radius + 4, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
} else if (this.type === 'fume') {
|
||||
// Purple spore cloud
|
||||
const alpha = Math.max(0.2, 1 - (this.x - this.startX) / 340);
|
||||
ctx.globalAlpha = alpha;
|
||||
const grad = ctx.createRadialGradient(this.x, this.y, 2, this.x, this.y, this.radius);
|
||||
grad.addColorStop(0, '#f3e5f5');
|
||||
grad.addColorStop(0.5, '#ab47bc');
|
||||
grad.addColorStop(1, '#6a1b9a');
|
||||
ctx.fillStyle = grad;
|
||||
ctx.beginPath();
|
||||
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Bubbles
|
||||
ctx.fillStyle = '#ea80fc';
|
||||
ctx.beginPath();
|
||||
ctx.arc(this.x - 6, this.y - 4, 4, 0, Math.PI * 2);
|
||||
ctx.arc(this.x + 5, this.y + 3, 3, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
} else if (this.type === 'cabbage') {
|
||||
// Tumbling green cabbage head
|
||||
ctx.translate(this.x, this.y);
|
||||
ctx.rotate(this.animTime * 7);
|
||||
ctx.fillStyle = '#66bb6a';
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, this.radius, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = '#2e7d32';
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.beginPath();
|
||||
ctx.arc(-2, -2, this.radius * 0.6, 0, Math.PI);
|
||||
ctx.arc(3, 1, this.radius * 0.5, 0, Math.PI);
|
||||
ctx.stroke();
|
||||
} else if (this.type === 'melon') {
|
||||
// Tumbling striped watermelon
|
||||
ctx.translate(this.x, this.y);
|
||||
ctx.rotate(this.animTime * 6);
|
||||
ctx.fillStyle = '#2e7d32';
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(0, 0, this.radius, this.radius * 0.8, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = '#a5d6a7';
|
||||
ctx.lineWidth = 2.5;
|
||||
for (let i = -1; i <= 1; i++) {
|
||||
ctx.beginPath();
|
||||
ctx.arc(i * 5, 0, this.radius * 0.7, -Math.PI / 2, Math.PI / 2);
|
||||
ctx.stroke();
|
||||
}
|
||||
} else if (this.type === 'bowling') {
|
||||
// Rolling Wall-nut
|
||||
ctx.translate(this.x, this.y);
|
||||
ctx.rotate(this.animTime * 10);
|
||||
ctx.fillStyle = '#8d6e63';
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, this.radius, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#5d4037';
|
||||
ctx.beginPath();
|
||||
ctx.arc(6, -6, 5, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
} else {
|
||||
// Standard or Critical pea
|
||||
const grad = ctx.createRadialGradient(this.x - 3, this.y - 3, 2, this.x, this.y, this.radius);
|
||||
if (this.isCrit) {
|
||||
grad.addColorStop(0, '#fff59d');
|
||||
grad.addColorStop(0.5, '#fbc02d');
|
||||
grad.addColorStop(1, '#f57f17');
|
||||
} else {
|
||||
grad.addColorStop(0, '#a5d6a7');
|
||||
grad.addColorStop(0.5, '#4caf50');
|
||||
grad.addColorStop(1, '#2e7d32');
|
||||
}
|
||||
ctx.fillStyle = grad;
|
||||
ctx.beginPath();
|
||||
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Highlight spot
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
|
||||
ctx.beginPath();
|
||||
ctx.arc(this.x - 3, this.y - 3, 2.5, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,446 @@
|
||||
// Zombie Entities (Normal, Conehead, Buckethead, Flag, PoleVaulter, Newspaper, Football, DuckyTube)
|
||||
window.Zombie = class Zombie {
|
||||
constructor(row, type = 'normal', startX = 940) {
|
||||
this.row = row;
|
||||
this.type = type;
|
||||
this.x = startX;
|
||||
this.y = 0;
|
||||
this.width = 60;
|
||||
this.height = 90;
|
||||
|
||||
this.baseSpeed = 22; // default walking speed
|
||||
this.eatingDps = 30;
|
||||
|
||||
this.isEating = false;
|
||||
this.targetPlant = null;
|
||||
this.eatTimer = 0;
|
||||
|
||||
this.slowTimer = 0;
|
||||
this.hitFlashTimer = 0;
|
||||
this.animTime = Math.random() * 5;
|
||||
this.toRemove = false;
|
||||
|
||||
// Type specifics
|
||||
this.hasPole = type === 'polevaulter';
|
||||
this.isVaulting = false;
|
||||
this.vaultTimer = 0;
|
||||
|
||||
this.hasPaper = type === 'newspaper';
|
||||
this.paperHealth = 100;
|
||||
this.isAngry = false;
|
||||
|
||||
switch (type) {
|
||||
case 'conehead':
|
||||
this.maxHealth = 200;
|
||||
this.helmetHealth = 100;
|
||||
break;
|
||||
case 'buckethead':
|
||||
this.maxHealth = 350;
|
||||
this.helmetHealth = 250;
|
||||
break;
|
||||
case 'football':
|
||||
this.maxHealth = 350;
|
||||
this.helmetHealth = 250;
|
||||
this.baseSpeed = 42;
|
||||
break;
|
||||
case 'polevaulter':
|
||||
this.maxHealth = 180;
|
||||
this.baseSpeed = 44; // Fast until vault
|
||||
break;
|
||||
case 'newspaper':
|
||||
this.maxHealth = 200;
|
||||
this.baseSpeed = 18;
|
||||
break;
|
||||
case 'duckytube':
|
||||
this.maxHealth = 120;
|
||||
this.baseSpeed = 24;
|
||||
break;
|
||||
case 'flag':
|
||||
this.maxHealth = 110;
|
||||
this.baseSpeed = 26;
|
||||
break;
|
||||
case 'normal':
|
||||
default:
|
||||
this.maxHealth = 100;
|
||||
this.helmetHealth = 0;
|
||||
break;
|
||||
}
|
||||
this.speed = this.baseSpeed;
|
||||
this.health = this.maxHealth;
|
||||
}
|
||||
|
||||
update(dt, gameState) {
|
||||
this.animTime += dt;
|
||||
|
||||
if (this.health <= 0) {
|
||||
this.toRemove = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.hitFlashTimer > 0) {
|
||||
this.hitFlashTimer -= dt;
|
||||
}
|
||||
|
||||
// Freeze / slow effect countdown
|
||||
if (this.slowTimer > 0) {
|
||||
this.slowTimer -= dt;
|
||||
this.speed = this.baseSpeed * 0.5;
|
||||
} else {
|
||||
this.speed = this.baseSpeed;
|
||||
}
|
||||
|
||||
// Pole Vaulter jump logic
|
||||
if (this.isVaulting) {
|
||||
this.vaultTimer += dt;
|
||||
// Arc jump over plant (travels ~90px leftwards in 0.6s)
|
||||
this.x -= 150 * dt;
|
||||
if (this.vaultTimer >= 0.6) {
|
||||
this.isVaulting = false;
|
||||
this.baseSpeed = 22; // Normal speed after jumping
|
||||
this.speed = 22;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if eating a plant
|
||||
if (this.targetPlant) {
|
||||
// Pole vaulter vaults over first encountered plant!
|
||||
if (this.hasPole && !this.isVaulting) {
|
||||
this.hasPole = false;
|
||||
this.isVaulting = true;
|
||||
this.vaultTimer = 0;
|
||||
this.targetPlant = null;
|
||||
this.isEating = false;
|
||||
if (window.PvZAudio) window.PvZAudio.playVault();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.targetPlant.toRemove || this.targetPlant.health <= 0) {
|
||||
this.targetPlant = null;
|
||||
this.isEating = false;
|
||||
} else {
|
||||
this.isEating = true;
|
||||
this.eatTimer += dt;
|
||||
if (this.eatTimer >= 0.35) {
|
||||
this.eatTimer = 0;
|
||||
this.targetPlant.takeDamage(this.eatingDps * 0.35);
|
||||
if (window.PvZAudio) window.PvZAudio.playChomp();
|
||||
if (this.targetPlant && this.targetPlant.type === 'wallnut' && gameState && gameState.achievements) {
|
||||
gameState.achievements.trigger('nut_wall');
|
||||
}
|
||||
if (this.targetPlant.toRemove || this.targetPlant.health <= 0) {
|
||||
this.targetPlant = null;
|
||||
this.isEating = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.isEating = false;
|
||||
}
|
||||
|
||||
// If not eating and not vaulting, walk forward
|
||||
if (!this.isEating) {
|
||||
this.x -= this.speed * dt;
|
||||
}
|
||||
}
|
||||
|
||||
takeDamage(amount, slows = false) {
|
||||
this.hitFlashTimer = 0.08;
|
||||
if (slows) {
|
||||
this.slowTimer = 4.0;
|
||||
}
|
||||
|
||||
// Newspaper mechanics
|
||||
if (this.type === 'newspaper' && this.hasPaper) {
|
||||
this.paperHealth -= amount;
|
||||
if (this.paperHealth <= 0) {
|
||||
this.hasPaper = false;
|
||||
this.isAngry = true;
|
||||
this.baseSpeed = 54; // Enraged run!
|
||||
this.speed = this.slowTimer > 0 ? 27 : 54;
|
||||
if (window.PvZAudio) window.PvZAudio.playStrike();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.health -= amount;
|
||||
if (this.health <= 0) {
|
||||
this.toRemove = true;
|
||||
}
|
||||
}
|
||||
|
||||
draw(ctx, gameState = null) {
|
||||
ctx.save();
|
||||
ctx.translate(this.x, this.y);
|
||||
|
||||
// Ground shadow
|
||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.25)';
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(0, 42, 22, 8, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
const isDancing = gameState && gameState.tree && gameState.tree.cheats && gameState.tree.cheats.dance;
|
||||
const danceBob = isDancing ? Math.sin(this.animTime * 12) * 6 : 0;
|
||||
const walkBob = (Math.sin(this.animTime * (this.isAngry ? 12 : 5)) * 2) + danceBob;
|
||||
const legAngle1 = Math.sin(this.animTime * (this.isAngry ? 10 : (isDancing ? 8 : 4))) * 0.35;
|
||||
const legAngle2 = -legAngle1;
|
||||
const armAngle = this.isEating
|
||||
? (Math.sin(this.animTime * 8) * 0.2 + 0.5)
|
||||
: (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;
|
||||
|
||||
let skinColor = this.isAngry ? '#b71c1c' : '#809973';
|
||||
let coatColor = '#5d4037';
|
||||
let pantsColor = '#263238';
|
||||
|
||||
if (this.type === 'football') {
|
||||
coatColor = '#b71c1c';
|
||||
pantsColor = '#f5f5f5';
|
||||
}
|
||||
|
||||
if (isSlow) {
|
||||
skinColor = '#81d4fa';
|
||||
coatColor = '#455a64';
|
||||
}
|
||||
if (isHit) {
|
||||
skinColor = '#ffffff';
|
||||
coatColor = '#e0e0e0';
|
||||
}
|
||||
|
||||
// Vault jump vertical offset
|
||||
if (this.isVaulting) {
|
||||
const jumpProgress = this.vaultTimer / 0.6;
|
||||
const jumpY = -Math.sin(jumpProgress * Math.PI) * 65;
|
||||
ctx.translate(0, jumpY);
|
||||
ctx.rotate(-0.3 + jumpProgress * 0.6);
|
||||
} else {
|
||||
ctx.translate(0, walkBob);
|
||||
}
|
||||
|
||||
// Water ripple for Ducky Tube
|
||||
if (this.type === 'duckytube') {
|
||||
ctx.strokeStyle = 'rgba(255, 255, 255, 0.6)';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(0, 30, 30 + Math.sin(this.animTime * 4) * 4, 12, 0, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// Back leg
|
||||
ctx.save();
|
||||
ctx.translate(6, 15);
|
||||
ctx.rotate(legAngle2);
|
||||
ctx.fillStyle = pantsColor;
|
||||
ctx.fillRect(-5, 0, 10, 24);
|
||||
ctx.fillStyle = '#1b1b1b';
|
||||
ctx.fillRect(-7, 24, 14, 6);
|
||||
ctx.restore();
|
||||
|
||||
// Front leg
|
||||
ctx.save();
|
||||
ctx.translate(-6, 15);
|
||||
ctx.rotate(legAngle1);
|
||||
ctx.fillStyle = pantsColor;
|
||||
ctx.fillRect(-5, 0, 10, 24);
|
||||
ctx.fillStyle = '#1b1b1b';
|
||||
ctx.fillRect(-7, 24, 14, 6);
|
||||
ctx.restore();
|
||||
|
||||
// Torso
|
||||
ctx.fillStyle = coatColor;
|
||||
ctx.fillRect(-14, -18, 26, 34);
|
||||
|
||||
// Tie
|
||||
ctx.fillStyle = '#b71c1c';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-1, -16);
|
||||
ctx.lineTo(3, -16);
|
||||
ctx.lineTo(2, -2);
|
||||
ctx.lineTo(1, 4);
|
||||
ctx.lineTo(0, -2);
|
||||
ctx.fill();
|
||||
|
||||
// Ducky Inner Tube
|
||||
if (this.type === 'duckytube') {
|
||||
ctx.fillStyle = '#fbc02d';
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(0, 16, 26, 14, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
// Duck beak
|
||||
ctx.fillStyle = '#ff9800';
|
||||
ctx.beginPath();
|
||||
ctx.arc(-22, 12, 6, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
// Arms & Items (Pole / Newspaper)
|
||||
ctx.save();
|
||||
ctx.translate(-8, -10);
|
||||
ctx.rotate(armAngle);
|
||||
ctx.fillStyle = coatColor;
|
||||
ctx.fillRect(-18, -4, 20, 8);
|
||||
ctx.fillStyle = skinColor;
|
||||
ctx.beginPath();
|
||||
ctx.arc(-20, 0, 5, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Held Item
|
||||
if (this.hasPole) {
|
||||
// Long vault pole
|
||||
ctx.strokeStyle = '#8d6e63';
|
||||
ctx.lineWidth = 5;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-20, 2);
|
||||
ctx.lineTo(-65, -35);
|
||||
ctx.stroke();
|
||||
} else if (this.hasPaper) {
|
||||
// Newspaper shield
|
||||
ctx.fillStyle = '#f5f5f5';
|
||||
ctx.strokeStyle = '#9e9e9e';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.fillRect(-34, -18, 16, 32);
|
||||
ctx.strokeRect(-34, -18, 16, 32);
|
||||
// Newspaper lines
|
||||
ctx.fillStyle = '#616161';
|
||||
ctx.fillRect(-32, -14, 12, 2);
|
||||
ctx.fillRect(-32, -8, 12, 2);
|
||||
ctx.fillRect(-32, -2, 12, 2);
|
||||
}
|
||||
ctx.restore();
|
||||
|
||||
// Head
|
||||
ctx.save();
|
||||
ctx.translate(-2, -28);
|
||||
const headTilt = this.isEating ? Math.sin(this.animTime * 12) * 0.15 : Math.sin(this.animTime * 3) * 0.05;
|
||||
ctx.rotate(headTilt);
|
||||
|
||||
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();
|
||||
|
||||
// Mouth
|
||||
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);
|
||||
|
||||
// Tree of Wisdom Visual Cheats
|
||||
if (gameState && gameState.tree && gameState.tree.cheats) {
|
||||
// 1. Future Sunglasses
|
||||
if (gameState.tree.cheats.future) {
|
||||
ctx.fillStyle = '#00e5ff';
|
||||
ctx.shadowColor = '#00e5ff';
|
||||
ctx.shadowBlur = 6;
|
||||
ctx.fillRect(-12, -7, 22, 6);
|
||||
ctx.fillStyle = '#18ffff';
|
||||
ctx.fillRect(-10, -5, 18, 2);
|
||||
ctx.shadowBlur = 0;
|
||||
}
|
||||
// 2. Mustache
|
||||
if (gameState.tree.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();
|
||||
}
|
||||
}
|
||||
|
||||
// Headwear
|
||||
if (this.type === 'conehead' && this.health > 100) {
|
||||
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' && this.health > 100) {
|
||||
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' && this.health > 100) {
|
||||
// Football Helmet
|
||||
ctx.fillStyle = '#b71c1c';
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, -10, 18, Math.PI, 0);
|
||||
ctx.fill();
|
||||
// White Stripe
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.fillRect(-3, -28, 6, 18);
|
||||
// Face grill
|
||||
ctx.strokeStyle = '#ffffff';
|
||||
ctx.lineWidth = 2.5;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-12, -4);
|
||||
ctx.lineTo(-2, 4);
|
||||
ctx.lineTo(8, -4);
|
||||
ctx.stroke();
|
||||
} else if (this.type === 'flag') {
|
||||
ctx.strokeStyle = '#8d6e63';
|
||||
ctx.lineWidth = 3;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(10, 10);
|
||||
ctx.lineTo(16, -55);
|
||||
ctx.stroke();
|
||||
|
||||
ctx.fillStyle = '#d32f2f';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(16, -55);
|
||||
ctx.lineTo(40, -45);
|
||||
ctx.lineTo(16, -35);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
|
||||
// Health bar if damaged
|
||||
if (this.health < this.maxHealth) {
|
||||
const pct = Math.max(0, this.health / this.maxHealth);
|
||||
ctx.fillStyle = 'rgba(0,0,0,0.6)';
|
||||
ctx.fillRect(-20, -56, 40, 5);
|
||||
ctx.fillStyle = pct > 0.4 ? '#8bc34a' : '#f44336';
|
||||
ctx.fillRect(-20, -56, 40 * pct, 5);
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
};
|
||||
Reference in new issue
Block a user