- 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
378 lines
11 KiB
JavaScript
378 lines
11 KiB
JavaScript
// 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();
|
|
}
|
|
};
|