ソースコード解説・目次

CHAPTER 15 · 全23項目

大型戦艦のボス戦

部位ごとの耐久値、コアの保護、予告位置を固定する攻撃と、登場するたびに上がるボスのレベルを実装します。

主な参照先:makeBoss / coreExposed / damageBossPart / startBossAttack / fireBossVolley / updateBoss / BOSS_SWAY

ボスを3つの部位に分ける

game.html · L1103–1113ソースを見る ↗
function makeBoss() {
  const level = wave / 5, hpLevel = Math.min(level, BOSS_HP_LEVEL_MAX);
  boss = {
    level, x: W / 2, y: 112, t: 0, attackT: 2, attackN: 0, warning: 0, laserT: 0, lanes: [],
    parts: [
      { kind: "gun", offset: -54, hp: 7 + hpLevel * 2, max: 7 + hpLevel * 2, flash: 0 },
      { kind: "gun", offset: 54, hp: 7 + hpLevel * 2, max: 7 + hpLevel * 2, flash: 0 },
      { kind: "core", offset: 0, hp: 20 + hpLevel * 8, max: 20 + hpLevel * 8, flash: 0 },
    ],
  };
}
game.html · L820–821ソースを見る ↗
const BOSS_HP_LEVEL_MAX = 4; // ボスの耐久力は Lv.4(ウェーブ 20)で頭打ち。以降は攻撃の速さで手強くなる
const BOSS_TWIN_GAP = 50;    // Lv.2 からの挟み撃ちレーザー: 自機の左右この距離に 2 本。動かなければ間に収まる(画面端では内側へずれる)

5ウェーブごとに大型戦艦を作ります。左右の砲台と中央コアを parts に入れ、各部位に現在HPと最大HPを持たせます。level はウェーブ数÷5で、5ウェーブ目がLv.1、10ウェーブ目がLv.2です。耐久値は hpLevel、つまり level を BOSS_HP_LEVEL_MAX の4で止めた値から求め、砲台は 7 + hpLevel × 2、コアは 20 + hpLevel × 8 です。5ウェーブ目なら砲台は各9、コアは28、10ウェーブ目なら砲台は各11、コアは36、20ウェーブ目以降は砲台が各15、コアが52のまま増えません。後半はボス戦が長くなるのではなく、攻撃が激しくなる作りです。自機の弾は通常弾も連結砲の弾も1発で1ダメージなので、5ウェーブ目の砲台は9発で壊れます。部位の画面座標はボス本体の座標に offset を足して求めます。

左右の砲台を壊すまでコアを保護する

game.html · L1119–1119ソースを見る ↗
function coreExposed() { return boss && boss.parts.slice(0, 2).every(p => p.hp <= 0); }
game.html · L1121–1128ソースを見る ↗
function damageBossPart(part, amount, source = "shot") {
  if (!boss || part.hp <= 0 || amount <= 0) return;
  if (part.kind === "core" && !coreExposed()) return;
  const r = bossPartRect(part), damage = Math.min(part.hp, amount);
  part.hp = Math.max(0, part.hp - amount); part.flash = 0.08;
  if (source === "shot") gainCharge(damage * 1.5);
  if (part.hp > 0) return;
  spawnBurst(r.x + 14, r.y + 15, "#ff9a5c", 28, 180);

コアへの攻撃を受けた時点で、両方の砲台のHPが0以下かを調べます。通常弾だけでなくレーザーやボムも同じ関数を通すため、このルールを共用できます。通常射撃のダメージだけがゲージ充電につながります。

予告、発射、待機の3段階

  1. 予告1.1秒。狙うx座標を固定
  2. 発射0.6秒。幅36 pxの危険範囲
  3. 待機横移動し、次の攻撃を待つ

待機中に attackT が0になると startBossAttack() が呼ばれ、その瞬間の player.x から攻撃する列を決めて boss.lanes に保存します。予告が終わっても位置を追い直さないため、プレイヤーは予告を見て逃げられます。

game.html · L1148–1165ソースを見る ↗
// 次の攻撃までの時間。コアが露出すると速まり、Lv.4〜6 では 1 段ごとに 0.15 秒ずつ縮む。
function bossAttackInterval() {
  return (coreExposed() ? 1.5 : 2.2) - clamp(boss.level - 3, 0, 3) * 0.15;
}

// 攻撃の予告を始める。レーザーの位置(lanes)はこの時点で固定し、予告と同じ場所に撃つ。
// Lv.1 は自機を狙う 1 本。Lv.2 からは 1 回おきに、自機の左右へ 2 本の挟み撃ち(動かなければ間に収まる)。
function startBossAttack() {
  const twin = boss.level >= 2 && boss.attackN % 2 === 1;
  if (twin) {
    const x = clamp(player.x, 22 + BOSS_TWIN_GAP, W - 22 - BOSS_TWIN_GAP);
    boss.lanes = [x - BOSS_TWIN_GAP, x + BOSS_TWIN_GAP];
  } else boss.lanes = [clamp(player.x, 22, W - 22)];
  boss.attackN++;
  boss.warning = 1.1;
  boss.attackT = bossAttackInterval();
  beep({ f0: 740, dur: 0.12, type: "square", vol: 0.07 });
}

Lv.1は自機を狙う1本だけです。Lv.2からは attackN が奇数の回、つまり1回おきに、自機の左右へ BOSS_TWIN_GAP の50 pxずつ離れた2本で挟み撃ちにします。危険範囲の幅は36 pxなので、2本の内側の端は自機の中心から32 px、自機の半分の幅は13 pxで、動かなければ間に収まります。画面の端では2本とも画面に収まるよう中心を内側へずらすため、端にいると逃げる必要があります。

Lv.2のボスが自機の左右に2本のレーザーを予告している。自機は2本の間にいる。
Lv.2の挟み撃ち。2本の予告の間にとどまれば当たらない。

次の攻撃までの時間は bossAttackInterval() で決めます。コアが露出すると2.2秒から1.5秒に縮み、Lv.4〜6では1段ごとにさらに0.15秒縮みます。

game.html · L1167–1181ソースを見る ↗
// 予告が終わった瞬間の射撃。残っている砲台は 3 方向弾、Lv.3 からは船体中央が自機へ扇状の 5 発も撃つ。
function fireBossVolley() {
  const vy = 180 + Math.min(wave * 3, 90);
  for (const part of boss.parts) {
    if (part.hp <= 0 || part.kind !== "gun") continue;
    for (const vx of [-75, 0, 75]) eBullets.push({ x: boss.x + part.offset, y: boss.y + 40, w: 3, h: 9, vx, vy });
  }
  if (boss.level >= 3) {
    const x = boss.x, y = boss.y + 44, aim = Math.atan2(PLAYER_Y - y, player.x - x);
    for (const da of [-0.36, -0.18, 0, 0.18, 0.36]) {
      const a = aim + da;
      eBullets.push({ x: x - 1.5, y, w: 3, h: 9, vx: Math.cos(a) * vy, vy: Math.sin(a) * vy, aimed: true });
    }
  }
}

予告が終わってレーザーを撃つ瞬間に fireBossVolley() が弾も撃ちます。生きている砲台は3方向に広がる弾、Lv.3からは船体の中央が自機を狙った扇状の5発です。扇は自機への角度 aim を Math.atan2() で求め、±0.18・±0.36ラジアンずらした向きに同じ速さで飛ばします。狙い撃ちの弾には aimed を付け、画面では赤く描きます。

game.html · L1183–1188ソースを見る ↗
function inBossLane(rect) {
  for (const x of boss.lanes) {
    if (hit(rect, { x: x - 18, y: boss.y + 40, w: 36, h: GROUND_Y - boss.y - 40 })) return true;
  }
  return false;
}

発射中は inBossLane() で、自機がどれかの列に重なっていないかを毎フレーム調べます。列が1本でも2本でも同じ処理で判定できます。

ボスのレーザー攻撃。予告時に確定した列を攻撃する。
ボスのレーザー攻撃。予告時に確定した列を攻撃する。

動いている間だけ時計を進める

game.html · L818–818ソースを見る ↗
const BOSS_SWAY = 0.45;      // ボスが左右に揺れる速さ(角速度)。振れ幅は ±130px
game.html · L1203–1207ソースを見る ↗
  } else {
    // 予告・照射中は止まるので、移動中だけ時間を進める(止まっている間も進めると攻撃後に瞬間移動する)。
    boss.t += dt;
    boss.x = W / 2 + Math.sin(boss.t * BOSS_SWAY) * 130;
    boss.attackT -= dt;

待機中のボスは W / 2 + Math.sin(boss.t * BOSS_SWAY) * 130 の位置にいて、画面中央から左右に最大130 px揺れます。BOSS_SWAY は揺れる速さで、0.45なら1往復に約14秒かかります。

boss.t += dt を待機中の分岐の中に置いているのが大切な点です。予告と発射の間 (合わせて1.7秒) はボスが止まっているので、時計も止めておきます。もし分岐の外で毎フレーム時計を進めると、止まっている間も boss.t だけが進み、動き出した瞬間に Math.sin() の値が飛んで位置がずれます。以前のバージョンはこの書き方 (揺れの速さは0.65) で、攻撃のあとにボスが最大140 pxほど瞬間移動していました。部位の当たり判定も boss.x から求めるので、瞬間移動すると狙って撃った弾まで外れてしまいます。移動中だけ時計を進めれば、ボスは止まった位置からなめらかに動き出します。

撃破と攻撃中断

コアを破壊するとボスと敵弾を消し、得点を加え、ボムを1個補充します。ボムを使う場合は warning と laserT を0にし、次の攻撃までの待ち時間を設定するため、予告中も発射中も中断できます。

ボス戦では通常弾を5発まで撃てる

game.html · L1609–1610ソースを見る ↗
  // ボスは遠く弾の到達に時間がかかるので、ボス戦では画面内の通常弾の上限を緩める。
  const maxB = (active.rapid > 0 || active.wide > 0) ? 12 : boss ? 5 : 3;

画面内に同時に出せる通常弾の数は、ふだんは3発、WかRが有効なら12発です。ボス戦ではW・Rがなくても上限を5発に緩めています。ボスは画面上部の遠い位置にいて、弾が届くまでに時間がかかるためです。連結砲の弾はこの上限に数えず、1門につき1発までです。