// BOSS 战系统:多阶段弹幕 / 冲撞 / 刺雨 import { clamp, sgn, rand, randInt, overlap } from './util.js'; import { sfx } from './audio.js'; import { TILE } from './util.js'; const DEFS = { guy: { hp: 42, w: 64, h: 74, name: 'THE GUY' }, mecha: { hp: 62, w: 78, h: 62, name: 'MECHA KID' }, devil: { hp: 84, w: 74, h: 90, name: 'THE DEVIL' }, }; export class Boss { constructor(level, e) { const d = DEFS[e.kind] || DEFS.guy; this.level = level; this.kind = e.kind; this.name = d.name; this.x = e.x; this.y = e.y; this.w = d.w; this.h = d.h; this.maxHp = e.hp || d.hp; this.hp = this.maxHp; this.flash = 0; this.dead = false; this.deathT = 0; this.bodyKill = true; this.t = 0; this.phase = 1; this.vx = 0; this.vy = 0; this.state = 'idle'; this.stateT = 0; // BOSS 贴地活动:保证玩家在地面平射就能命中 this.bounds = { x0: 40, x1: level.pxW - 40 - this.w, y0: 40, y1: level.pxH - 32 - this.h, // 底部贴地 }; this.homeY = clamp(e.y, this.bounds.y0, this.bounds.y1); this.y = this.homeY; this.homeX = e.x; this.wing = 0; } get rect() { return { x: this.x, y: this.y, w: this.w, h: this.h }; } shoot(level, x, y, vx, vy, col = '#ff5c7a', big = false) { level.ebullets.push({ x: x - 4, y: y - 4, w: big ? 12 : 8, h: big ? 12 : 8, vx, vy, dead: false, col, big, life: 500 }); } hit(dmg = 1) { if (this.dead || this.flash > 6) return; this.hp -= dmg; this.flash = 10; sfx.bossHit(); this.level.particles.spawn(this.x + this.w / 2, this.y + this.h / 2, 6, { colors: ['#fff', '#ffc63a'], speed: 2.6, life: 18 }); const r = this.hp / this.maxHp; this.phase = r > 0.6 ? 1 : r > 0.3 ? 2 : 3; if (this.hp <= 0) this.die(); } die() { this.dead = true; this.hp = 0; this.bodyKill = false; sfx.bossDie(); this.level.shake = 16; this.level.particles.burst(this.x + this.w / 2, this.y + this.h / 2, { n: 40, colors: ['#ff5c7a', '#ffc63a', '#fff', '#e03050'], speed: 5, life: 60, size: 4 }); } update(dt, player) { if (this.dead) { this.deathT++; if (this.deathT % 8 === 0 && this.deathT < 90) { this.level.particles.burst(this.x + rand(0, this.w), this.y + rand(0, this.h), { n: 6, colors: ['#ffc63a', '#ff5c7a', '#fff'], speed: 3, life: 30, size: 3 }); sfx.hit(); } if (this.deathT === 90) { if (this.onDefeat) this.onDefeat(); } return; } this.t++; this.stateT++; if (this.flash > 0) this.flash--; // 子类型行为 if (this.kind === 'guy') this.updateGuy(player); else if (this.kind === 'mecha') this.updateMecha(player); else this.updateDevil(player); this.x += this.vx; this.y += this.vy; this.x = clamp(this.x, this.bounds.x0, Math.max(this.bounds.x0, this.bounds.x1)); this.y = clamp(this.y, this.bounds.y0, Math.max(this.bounds.y0, this.bounds.y1)); // 被子弹击中 for (const b of this.level.bullets) { if (b.dead) continue; if (overlap(b, this.rect)) { b.dead = true; this.hit(1); } } } // ---------- THE GUY:飘移 + 扇形弹 + 砸地 ---------- updateGuy(player) { const L = this.level; if (this.phase === 1) { this.vx = Math.sin(this.t / 60) * 1.5; this.y = this.homeY + Math.sin(this.t / 45) * 8; if (this.t % 96 === 0) { const cx = this.x + this.w / 2, cy = this.y + this.h / 2; for (let i = -1; i <= 1; i++) { const a = Math.atan2(player.cy - cy, player.cx - cx) + i * 0.28; this.shoot(L, cx, cy, Math.cos(a) * 2.8, Math.sin(a) * 2.8); } sfx.shoot(); } } else { // 追踪 + 砸地 const dir = sgn(player.cx - (this.x + this.w / 2)); if (this.state === 'idle') { this.vx = dir * 1.9; this.y = this.homeY + Math.sin(this.t / 30) * 6; if (this.stateT > 70) { this.state = 'slam'; this.stateT = 0; this.vy = 0; } } else if (this.state === 'slam') { this.vx *= 0.9; this.vy = Math.min(this.vy + 0.7, 11); if (this.y + this.h >= this.bounds.y1 + this.h * 0.2 || this.y >= this.homeY + 150) { // 落地冲击:左右各飞出 3 发 this.state = 'idle'; this.stateT = 0; this.vy = -3; L.shake = 10; sfx.hit(); for (let i = 0; i < 4; i++) { this.shoot(L, this.x + this.w / 2, this.y + this.h - 6, -2.6 - i * 0.4, -1.2 - i * 0.15, '#ffc63a'); this.shoot(L, this.x + this.w / 2, this.y + this.h - 6, 2.6 + i * 0.4, -1.2 - i * 0.15, '#ffc63a'); } } } } } // ---------- MECHA KID:冲撞 + 刺雨 ---------- updateMecha(player) { const L = this.level; if (this.state === 'idle') { this.vx *= 0.92; this.y = this.homeY + Math.sin(this.t / 40) * 5; if (this.stateT > 100 - this.phase * 18) { this.state = 'dash'; this.stateT = 0; this.vx = sgn(player.cx - (this.x + this.w / 2)) * (5 + this.phase * 0.8); sfx.shoot(); } if (this.t % 150 === 0) { // 刺雨 for (let i = 0; i < 4 + this.phase; i++) { const tx = randInt(1, L.w - 2); L.entities.push({ type: 'dropspike', x: tx * TILE, y: -TILE, w: TILE, h: TILE, falling: true, vy: 0, homeY: -TILE, t: 0, temp: true, }); } L.showToast('刺雨来了!', 80); sfx.troll(); } } else if (this.state === 'dash') { this.vy = 0; if (this.x <= this.bounds.x0 + 1 || this.x >= this.bounds.x1 - 1 || this.stateT > 90) { this.state = 'idle'; this.stateT = 0; this.vx = 0; L.shake = 6; L.particles.spawn(this.x + this.w / 2, this.y + this.h / 2, 8, { colors: ['#9ad8ff', '#fff'], speed: 3, life: 20 }); } } } // ---------- THE DEVIL:追踪弹 / 激光 / 环形弹幕 ---------- updateDevil(player) { const L = this.level; this.wing += 0.16; const cx = this.x + this.w / 2, cy = this.y + this.h / 2; const p2 = this.phase; // 移动 if (p2 === 1) { this.vx = Math.sin(this.t / 70) * 1.6; this.y = this.homeY + Math.sin(this.t / 50) * 8; } else if (p2 === 2) { this.vx = Math.sin(this.t / 40) * 2.6; this.y = this.homeY + Math.sin(this.t / 28) * 11; } else { this.vx = Math.sin(this.t / 25) * 3.4; this.y = this.homeY + Math.sin(this.t / 20) * 14; } if (p2 === 1) { if (this.t % 60 === 0) { const a = Math.atan2(player.cy - cy, player.cx - cx); for (let i = -1; i <= 1; i++) this.shoot(L, cx, cy, Math.cos(a + i * 0.2) * 3, Math.sin(a + i * 0.2) * 3, '#ff9a3a'); sfx.shoot(); } } else if (p2 === 2) { if (this.t % 110 === 0) { this.state = 'laser'; this.stateT = 0; } if (this.state === 'laser' && this.stateT < 40 && this.stateT % 5 === 0) { // 横向激光弹幕(从上往下扫) const yy = 40 + (this.stateT / 40) * (L.pxH - 90); this.shoot(L, 8, yy, 6.5, 0, '#ff4060', true); this.shoot(L, L.pxW - 12, L.pxH - yy, -6.5, 0, '#ff4060', true); sfx.shoot(); } if (this.stateT > 44) this.state = 'idle'; } else { if (this.t % 34 === 0) { const n = 10; const off = this.t / 200; for (let i = 0; i < n; i++) { const a = off + (i / n) * Math.PI * 2; this.shoot(L, cx, cy, Math.cos(a) * 2.6, Math.sin(a) * 2.6, '#ff5c7a'); } sfx.shoot(); } if (this.t % 170 === 0) { const a = Math.atan2(player.cy - cy, player.cx - cx); this.shoot(L, cx, cy, Math.cos(a) * 4.4, Math.sin(a) * 4.4, '#ffc63a', true); } } } // ---------------- 绘制 ---------------- draw(ctx, t) { if (this.dead && this.deathT > 96) return; const x = this.x, y = this.y, w = this.w, h = this.h; const fl = this.flash > 0; ctx.save(); if (this.dead) ctx.globalAlpha = Math.max(0, 1 - this.deathT / 96); const body = fl ? '#fff' : null; if (this.kind === 'guy') this.drawGuy(ctx, x, y, w, h, body, t); else if (this.kind === 'mecha') this.drawMecha(ctx, x, y, w, h, body, t); else this.drawDevil(ctx, x, y, w, h, body, t); ctx.restore(); } drawGuy(ctx, x, y, w, h, fl, t) { const c = fl || '#2a1020', c2 = fl || '#5a1a34'; ctx.fillStyle = c; ctx.beginPath(); ctx.ellipse(x + w / 2, y + h * 0.62, w * 0.42, h * 0.34, 0, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = c2; ctx.beginPath(); ctx.ellipse(x + w / 2, y + h * 0.3, w * 0.36, h * 0.3, 0, 0, Math.PI * 2); ctx.fill(); // 角 ctx.fillStyle = fl || '#c8305a'; ctx.beginPath(); ctx.moveTo(x + w * 0.2, y + h * 0.16); ctx.lineTo(x + w * 0.05, y - h * 0.12); ctx.lineTo(x + w * 0.34, y + h * 0.06); ctx.closePath(); ctx.fill(); ctx.beginPath(); ctx.moveTo(x + w * 0.8, y + h * 0.16); ctx.lineTo(x + w * 0.95, y - h * 0.12); ctx.lineTo(x + w * 0.66, y + h * 0.06); ctx.closePath(); ctx.fill(); // 眼 ctx.fillStyle = fl ? '#000' : '#ffe14a'; ctx.fillRect(x + w * 0.3, y + h * 0.28, w * 0.14, h * 0.1); ctx.fillRect(x + w * 0.56, y + h * 0.28, w * 0.14, h * 0.1); // 嘴 ctx.fillStyle = fl ? '#000' : '#801025'; ctx.fillRect(x + w * 0.34, y + h * 0.46, w * 0.32, h * 0.06); if (this.phase >= 2) { ctx.strokeStyle = fl ? '#000' : '#ff4060'; ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(x + w / 2, y + h / 2, w * 0.62 + Math.sin(t / 90) * 3, 0, Math.PI * 2); ctx.stroke(); } } drawMecha(ctx, x, y, w, h, fl, t) { ctx.fillStyle = fl || '#3a4258'; ctx.fillRect(x + 4, y + 6, w - 8, h - 10); ctx.fillStyle = fl || '#68728e'; ctx.fillRect(x + 8, y + 10, w - 16, h - 20); // 眼 const blink = (Math.floor(t / 200) % 5) === 0; ctx.fillStyle = fl ? '#000' : (this.state === 'dash' ? '#ff4060' : '#7ae0ff'); ctx.fillRect(x + w * 0.22, y + h * 0.34, w * 0.18, h * 0.14); ctx.fillRect(x + w * 0.6, y + h * 0.34, w * 0.18, h * 0.14); // 炮口 ctx.fillStyle = fl || '#2a2f40'; ctx.fillRect(x + w * 0.42, y + h - 8, w * 0.16, 10); // 天线 ctx.fillStyle = fl || '#8a94ad'; ctx.fillRect(x + w / 2 - 1, y - 6, 2, 8); ctx.fillStyle = (Math.floor(t / 120) % 2) ? '#ff4060' : '#601020'; ctx.beginPath(); ctx.arc(x + w / 2, y - 8, 3, 0, Math.PI * 2); ctx.fill(); // 推进器 if (this.state === 'dash') { ctx.fillStyle = '#ffb040'; ctx.fillRect(x - 6, y + h * 0.4, 8, h * 0.2); ctx.fillRect(x + w - 2, y + h * 0.4, 8, h * 0.2); } } drawDevil(ctx, x, y, w, h, fl, t) { // 翅膀 const fl2 = Math.sin(this.wing) * 8; ctx.fillStyle = fl || '#4a0f22'; ctx.beginPath(); ctx.moveTo(x + 6, y + h * 0.4); ctx.quadraticCurveTo(x - 26, y + h * 0.1 + fl2, x - 4, y + h * 0.72 + fl2); ctx.closePath(); ctx.fill(); ctx.beginPath(); ctx.moveTo(x + w - 6, y + h * 0.4); ctx.quadraticCurveTo(x + w + 26, y + h * 0.1 + fl2, x + w + 4, y + h * 0.72 + fl2); ctx.closePath(); ctx.fill(); // 身体 ctx.fillStyle = fl || '#8e1030'; ctx.beginPath(); ctx.ellipse(x + w / 2, y + h * 0.6, w * 0.4, h * 0.33, 0, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = fl || '#c01a3a'; ctx.beginPath(); ctx.ellipse(x + w / 2, y + h * 0.32, w * 0.34, h * 0.29, 0, 0, Math.PI * 2); ctx.fill(); // 角 ctx.fillStyle = fl || '#ffc63a'; ctx.beginPath(); ctx.moveTo(x + w * 0.18, y + h * 0.14); ctx.lineTo(x - w * 0.06, y - h * 0.16); ctx.lineTo(x + w * 0.32, y + h * 0.04); ctx.closePath(); ctx.fill(); ctx.beginPath(); ctx.moveTo(x + w * 0.82, y + h * 0.14); ctx.lineTo(x + w * 1.06, y - h * 0.16); ctx.lineTo(x + w * 0.68, y + h * 0.04); ctx.closePath(); ctx.fill(); // 眼(阶段变色) const ec = this.phase === 3 ? '#7ae0ff' : this.phase === 2 ? '#ffb040' : '#ffe14a'; ctx.fillStyle = fl ? '#000' : ec; ctx.fillRect(x + w * 0.26, y + h * 0.26, w * 0.16, h * 0.12); ctx.fillRect(x + w * 0.58, y + h * 0.26, w * 0.16, h * 0.12); // 牙 ctx.fillStyle = fl ? '#000' : '#fff'; for (let i = 0; i < 4; i++) ctx.fillRect(x + w * 0.32 + i * w * 0.1, y + h * 0.46, 4, 6); } } export function bossName(kind) { return (DEFS[kind] || DEFS.guy).name; } export { DEFS as BOSS_DEFS };