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:
2026-09-17 09:30:52 +00:00
commit 6457a8904e
50 changed files with 14486 additions and 0 deletions
+377
View File
@@ -0,0 +1,377 @@
// Dr. Zomboss Multi-Phase Boss Fight & Zombot Mechanics
window.BossSystem = class BossSystem {
constructor(gameEngine) {
this.engine = gameEngine;
this.isActive = false;
this.maxHealth = 6000;
this.health = 6000;
// Position & Visuals
this.x = 670;
this.y = 140;
this.w = 210;
this.h = 340;
this.animTimer = 0;
this.eyeColor = '#00e5ff';
// Attacks & Timers
this.stompTimer = 8.0;
this.ballTimer = 14.0;
this.vanTimer = 22.0;
this.summonTimer = 10.0;
// Active boss projectiles (Ice balls, Fire balls, RV vans)
this.hazards = [];
// Stomp animation
this.isStomping = false;
this.stompTarget = null;
this.stompAnimProgress = 0;
}
reset() {
this.isActive = false;
this.health = this.maxHealth;
this.stompTimer = 8.0;
this.ballTimer = 14.0;
this.vanTimer = 22.0;
this.summonTimer = 10.0;
this.hazards = [];
this.isStomping = false;
this.stompTarget = null;
}
startFight() {
this.isActive = true;
this.health = this.maxHealth;
this.hazards = [];
this.stompTimer = 6.0;
this.ballTimer = 12.0;
this.vanTimer = 25.0;
this.summonTimer = 8.0;
}
takeDamage(amount) {
if (!this.isActive || this.health <= 0) return;
this.health = Math.max(0, this.health - amount);
if (this.health <= 0) {
this.defeatBoss();
}
}
defeatBoss() {
this.isActive = false;
if (window.PvZAudio) {
window.PvZAudio.playExplosion();
window.PvZAudio.playWin();
}
// Huge reward
if (this.engine && this.engine.economy) {
this.engine.economy.addCoins(1000);
}
if (this.engine && this.engine.workshop) {
this.engine.workshop.addMaterial('crystals', 5);
this.engine.workshop.addMaterial('scrapMetal', 20);
}
if (this.engine && this.engine.leaderboards) {
this.engine.leaderboards.submitScore('boss', 1, 'Побежден Зомбот!');
}
// Explosion celebration
for (let i = 0; i < 60; i++) {
setTimeout(() => {
if (this.engine && this.engine.combat) {
const rx = this.x + Math.random() * this.w;
const ry = this.y + Math.random() * this.h;
this.engine.combat.createExplosionParticles(rx, ry);
}
}, i * 50);
}
setTimeout(() => {
if (this.engine) {
this.engine.triggerVictory();
}
}, 2500);
}
update(dt, gameState) {
if (!this.isActive) return;
this.animTimer += dt;
// Phase detection
const hpRatio = this.health / this.maxHealth;
const speedMult = hpRatio < 0.35 ? 1.5 : (hpRatio < 0.70 ? 1.25 : 1.0);
this.eyeColor = hpRatio < 0.35 ? '#ff1744' : (hpRatio < 0.70 ? '#ff9800' : '#00e5ff');
// 1. Attack: Stomp (Phase 1+)
this.stompTimer -= dt * speedMult;
if (this.stompTimer <= 0) {
this.stompTimer = 14.0;
this.triggerStomp(gameState);
}
// 2. Attack: Ice Ball / Fire Ball (Phase 2+)
if (hpRatio < 0.75) {
this.ballTimer -= dt * speedMult;
if (this.ballTimer <= 0) {
this.ballTimer = 16.0;
const ballType = Math.random() < 0.5 ? 'ice' : 'fire';
const row = Math.floor(Math.random() * gameState.grid.rows);
this.spawnBallHazard(row, ballType, gameState);
}
}
// 3. Attack: Van Throw (Phase 3 Enraged)
if (hpRatio < 0.35) {
this.vanTimer -= dt * speedMult;
if (this.vanTimer <= 0) {
this.vanTimer = 22.0;
this.throwVan(gameState);
}
}
// 4. Summons
this.summonTimer -= dt * speedMult;
if (this.summonTimer <= 0) {
this.summonTimer = 12.0;
const count = hpRatio < 0.35 ? 3 : 2;
for (let i = 0; i < count; i++) {
const types = ['conehead', 'buckethead', 'football', 'bungee', 'normal'];
const t = types[Math.floor(Math.random() * types.length)];
const r = Math.floor(Math.random() * gameState.grid.rows);
gameState.spawnZombie(r, t === 'bungee' ? 'buckethead' : t); // Bungee drop drops a durable zombie
}
}
// 5. Update Hazards
for (const h of this.hazards) {
if (h.type === 'ice' || h.type === 'fire') {
h.x -= h.speed * dt;
// Crush plants in path
for (const p of gameState.plants) {
if (p.row === h.row && Math.abs(p.x - h.x) < 30 && !p.toRemove) {
p.toRemove = true;
gameState.grid.removePlant(p.col, p.row);
gameState.combat.createSplash(p.x, p.y, h.type === 'ice' ? '#80deea' : '#ff5722');
}
}
// Jalapeno melts ice ball!
if (h.type === 'ice') {
for (const p of gameState.plants) {
if (p.type === 'jalapeno' && p.row === h.row && Math.abs(p.x - h.x) < 50) {
h.toRemove = true;
gameState.combat.createExplosionParticles(h.x, h.y);
}
}
}
// Snowpea / Ice slows and extinguishes fireball
if (h.type === 'fire') {
for (const proj of gameState.projectiles) {
if (proj.row === h.row && proj.slows && Math.abs(proj.x - h.x) < 30) {
h.toRemove = true;
proj.toRemove = true;
gameState.combat.createSplash(h.x, h.y, '#80deea', 16);
}
}
}
if (h.x < 30) {
h.toRemove = true;
}
}
}
this.hazards = this.hazards.filter(h => !h.toRemove);
}
triggerStomp(gameState) {
const alivePlants = gameState.plants.filter(p => !p.toRemove);
if (alivePlants.length === 0) return;
const victim = alivePlants[Math.floor(Math.random() * alivePlants.length)];
if (victim) {
victim.toRemove = true;
gameState.grid.removePlant(victim.col, victim.row);
gameState.combat.createExplosionParticles(victim.x, victim.y);
if (window.PvZAudio) window.PvZAudio.playStrike();
}
}
spawnBallHazard(row, type, gameState) {
const bounds = gameState.grid.getCellBounds(7, row);
this.hazards.push({
type,
row,
x: this.x - 20,
y: bounds.centerY,
speed: 130,
radius: 28,
toRemove: false
});
if (window.PvZAudio) window.PvZAudio.playHugeWave();
}
throwVan(gameState) {
// Pick 2x2 area to crush
const targetCol = 2 + Math.floor(Math.random() * 3);
const targetRow = Math.floor(Math.random() * (gameState.grid.rows - 1));
for (let r = targetRow; r <= targetRow + 1; r++) {
for (let c = targetCol; c <= targetCol + 1; c++) {
const p = gameState.grid.getPlant(c, r);
if (p) {
p.toRemove = true;
gameState.grid.removePlant(c, r);
}
const b = gameState.grid.getCellBounds(c, r);
gameState.combat.createExplosionParticles(b.centerX, b.centerY);
}
}
if (window.PvZAudio) window.PvZAudio.playExplosion();
}
draw(ctx) {
if (!this.isActive) return;
ctx.save();
// 1. Zombot Body & Legs
const bob = Math.sin(this.animTimer * 2.5) * 5;
// Shadow
ctx.fillStyle = 'rgba(0, 0, 0, 0.4)';
ctx.beginPath();
ctx.ellipse(this.x + 95, this.y + 320, 90, 24, 0, 0, Math.PI * 2);
ctx.fill();
// Robot Legs
ctx.fillStyle = '#424242';
ctx.fillRect(this.x + 30, this.y + 180 + bob, 35, 140);
ctx.fillRect(this.x + 125, this.y + 180 + bob, 35, 140);
// Feet
ctx.fillStyle = '#212121';
ctx.fillRect(this.x + 15, this.y + 300, 65, 24);
ctx.fillRect(this.x + 110, this.y + 300, 65, 24);
// Torso / Chassis
ctx.fillStyle = '#616161';
ctx.beginPath();
ctx.roundRect(this.x + 15, this.y + 70 + bob, 160, 130, 16);
ctx.fill();
ctx.strokeStyle = '#212121';
ctx.lineWidth = 4;
ctx.stroke();
// Cockpit Glass & Dr. Zomboss
ctx.fillStyle = 'rgba(33, 150, 243, 0.35)';
ctx.beginPath();
ctx.roundRect(this.x + 40, this.y + 15 + bob, 110, 65, 12);
ctx.fill();
ctx.strokeStyle = '#0d47a1';
ctx.lineWidth = 3;
ctx.stroke();
// Dr. Zomboss head inside
ctx.fillStyle = '#81c784'; // zombie green skin
ctx.beginPath();
ctx.arc(this.x + 95, this.y + 45 + bob, 22, 0, Math.PI * 2);
ctx.fill();
// Zomboss giant brain
ctx.fillStyle = '#f48fb1';
ctx.beginPath();
ctx.arc(this.x + 95, this.y + 34 + bob, 16, Math.PI, Math.PI * 2);
ctx.fill();
// Zomboss red eyes
ctx.fillStyle = '#d50000';
ctx.beginPath();
ctx.arc(this.x + 88, this.y + 45 + bob, 4, 0, Math.PI * 2);
ctx.arc(this.x + 102, this.y + 45 + bob, 4, 0, Math.PI * 2);
ctx.fill();
// Zombot Giant Robot Eyes
ctx.fillStyle = this.eyeColor;
ctx.shadowColor = this.eyeColor;
ctx.shadowBlur = 12;
ctx.beginPath();
ctx.arc(this.x + 60, this.y + 115 + bob, 14, 0, Math.PI * 2);
ctx.arc(this.x + 130, this.y + 115 + bob, 14, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
// Zombot Metal Jaw & Grille
ctx.fillStyle = '#212121';
ctx.fillRect(this.x + 45, this.y + 150 + bob, 100, 30);
ctx.strokeStyle = '#757575';
ctx.lineWidth = 2;
for (let i = 0; i < 6; i++) {
ctx.beginPath();
ctx.moveTo(this.x + 52 + i * 16, this.y + 150 + bob);
ctx.lineTo(this.x + 52 + i * 16, this.y + 180 + bob);
ctx.stroke();
}
// 2. Boss Health Bar (Top Center)
const barW = 340;
const barH = 22;
const barX = 280;
const barY = 16;
ctx.fillStyle = '#212121';
ctx.beginPath();
ctx.roundRect(barX, barY, barW, barH, 5);
ctx.fill();
ctx.strokeStyle = '#b71c1c';
ctx.lineWidth = 2.5;
ctx.stroke();
const hpRatioBar = Math.max(0, this.health / this.maxHealth);
ctx.fillStyle = hpRatioBar < 0.35 ? '#d50000' : (hpRatioBar < 0.70 ? '#ff9800' : '#4caf50');
ctx.beginPath();
ctx.roundRect(barX + 2, barY + 2, Math.max(0, (barW - 4) * hpRatioBar), barH - 4, 3);
ctx.fill();
ctx.fillStyle = '#fff';
ctx.font = 'bold 12px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(`👑 ДОКТОР ЗОМБОСС: ${Math.round(this.health)} / ${this.maxHealth} HP`, barX + barW / 2, barY + 15);
// 3. Draw Active Hazards (Ice/Fire Balls)
for (const h of this.hazards) {
ctx.save();
ctx.translate(h.x, h.y);
if (h.type === 'ice') {
ctx.fillStyle = '#80deea';
ctx.shadowColor = '#00e5ff';
ctx.shadowBlur = 10;
ctx.beginPath();
ctx.arc(0, 0, h.radius, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 3;
ctx.stroke();
} else {
ctx.fillStyle = '#ff5722';
ctx.shadowColor = '#ff1744';
ctx.shadowBlur = 12;
ctx.beginPath();
ctx.arc(0, 0, h.radius, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = '#ffeb3b';
ctx.lineWidth = 3;
ctx.stroke();
}
ctx.restore();
}
ctx.restore();
}
};