【AIゲーム開発】大型ボス・パワーアップ追加・ChatGPTで本格シューティングゲームを作ってみた(2日目)

【AIゲーム開発】大型ボス・パワーアップ追加・ChatGPTで本格シューティングゲームを作ってみた(2日目)

ども。ROBO船長です。

前回1日目のつづきです。ChatGPTでシューティングゲームを作っていきたいと思います。

目指すのは本気で遊んでも楽しめる本格弾幕シューティングゲームです。

今回はパワーアップの実装や敵の弾の発射、パーティクルの調整、画面サイズの調整、音の追加、タイトル画面の追加などを実施していきます。
ボスも1種類追加したいと思います。

この記事では有料のChatGPT4を使用していますが、無料のChatGPT3.5でもほぼ同じことができます。
※生成されるソースコードの品質や指示に対する気の利き方はChatGPT4の方が当然上になります。

現時点では未完成のため同じボスが2回出た後それを倒したらそのあと敵が出なくなりますのでそれで終わりです。エンディングとかまだありませんのでご了承ください。

  1. 2日目時点のシューティングゲーム(実際に遊べる)
  2. ChatGPTにパワーアップ機能追加の指示をする(プロンプト)
    1. ChatGPTの回答(パワーアップの実装についての段取り)
      1. ステップ1: パワーアップレベルとタイプの追加
      2. ステップ2: パワーアップアイテムの取得と効果の適用
      3. ステップ3: 強化ビームの実装
      4. ステップ4: 強化ビームの当たり判定と効果
      5. ステップ5: パーティクル効果の追加
      6. ステップ6: テストと調整
      7. ステップ7: ドキュメントとリファクタリング
  3. ステップ1、ステップ2の実装指示(プロンプト)
    1. ChatGPTの回答(ソースコード ステップ1、ステップ2実装)
      1. ステップ1: パワーアップレベルとタイプの追加
      2. ステップ2: パワーアップアイテムの取得と効果の適用
  4. ChatGPTの誤りに対する指摘(プロンプト)
    1. ChatGPTの回答(ソースコード 指摘修正)
  5. ステップ3の実装指示(プロンプト)
    1. ChatGPTの回答(ソースコード ステップ3の実装)
    2. ステップ3の実装指針:
    3. コード例(概念的な説明):
  6. パワーアップしたビームを発射処理を完成させる(プロンプト)
    1. ChatGPTの回答(ソースコード パワーアップしたビーム発射)
      1. 完成したfireBeam関数の例:
    2. 追加指示 ビーム生成処理を完成させる(プロンプト)
    3. ChatGPTの回答(ソースコード ビーム生成処理)
      1. 完成したcreateBeam関数の例:
    4. 追加指示 ビーム更新処理を完成させる(プロンプト)
    5. ChatGPTの回答(ソースコード ビーム更新処理)
      1. updateBeams関数の例:
  7. ビームのパワーアップ、敵の弾発射の実装まとめ(プロンプト)
  8. 大型ボスの追加(プロンプト)
  9. タイトル画面の追加(プロンプト)
  10. BGMの追加(プロンプト)
  11. この時点のソースコード(game09_01)
      1. index.html(ブラウザでアクセスするページ)
      2. style.css(画面サイズなどのCSS定義)
      3. game.js (ゲームのメインプログラム)
      4. enemySpawnPatterns.js(敵の出現定義の定数宣言)
  12. 2日目 まとめ
    1. 今後の実装予定

2日目時点のシューティングゲーム(実際に遊べる)

2日目時点の実際のシューティングゲームはこちら

PC、スマホ(Android、iPhone)で動きます。
画面サイズ固定なので画面が広すぎるスマホやタブレット、PCだと小さく表示されます。

2日目時点で実装されているのは下記となります。

  • 自機の操作
  • 小型、中型の敵の出現と動作
  • 敵と自機の当たり判定
  • 敵と自機が発射したビームの当たり判定
  • 敵を倒したときやダメージを受けた時の爆発パーティクル
  • パワーアップアイテムの出現
  • GAME OVER表示
  • タイトル画面
  • 効果音、BGM追加
  • 画面サイズの変更(やや拡大)
  • アイテムを取ってパワーアップ(オレンジ:連射弾、青:貫通反射弾)
  • アイテムを取って自機の体力回復
  • 自機の体力表示(右下)
  • 大型ボス1
  • GAME OVER表示からの再スタート

一応、音がついてボスも出るようになったのでそれなりに楽しめるようになりましたが、まだボスが1種類なのと、ゲームクリアがない(ある程度すすむと敵が出なくなるところで実質終わり)のでそのあたりを今後追加します。

以下、2日目の開発内容です。

ほぼ、自分用の記録というかメモですが、ChatGPTでどうやってゲーム作っているの?と気になる方はプロンプトやソースコードも公開しているのでどうぞご覧ください。

ChatGPTにパワーアップ機能追加の指示をする(プロンプト)

1日目では敵を倒してパワーアップアイテムが表示されること、それを自機がとることができるところまで実装しました。

続いて、実際に自機の攻撃をパワーアップできるようにしたいと思います。ChatGPTが具体的なプログラムができるように、こちらの指示もできるだけ具体的にします。

また、いきなり全てを一気に実装させようとするとうまくいきませんので、まずは「段取り」だけ提案させます。

そのあと、各段取りの段階ごとに細かい指示を出して実際のプログラミングをしてもらいます。

パワーアップアイテムの効果について説明します。
段階的に機能を追加したいと思うので、実際のソースコードを変更する前に機能追加の段取りを提案してください。

初期状態はレベル0で、アイテムを取るたびにレベルが上がります。
アイテムを1個とった状態をレベル1としてレベル1から4まであがります。
別のアイテムを取った場合はレベルを維持したまま別の弾になります。
自機がダメージを受けるとレベルが1下がります。
レベル0(初期状態)より下には下がりません。

パワーアップ効果には以下の2種類があります。

1:連射弾強化
 ビームの形状が半径2の円形になり横方向に球数が増えます。
 レベルにより球数が3,5,7,9個と横方向に個数が増えます。
 個数が増えた連射弾は山形に並び同時に発射されます。
 個数と同じダメージを敵に与えます。
 自機がダメージを受けるとレベルが1つ下がります。
 レベル1でダメージを受けると、初期状態のビームに戻ります。
 連射弾の状態で貫通レーザーのアイテムを取ると、同じレベルの貫通レーザーになります。
 
2: 貫通反射弾強化
 貫通する青白く光る貫通反射弾(半径3の球)を発射します。
 敵に当たっても消えずに貫通し、画面外に出たら消えます。
 
 貫通反射弾には彗星の尾のようなパーティクルが付きます(パーティクルは当たり判定無し)
 球は左右の壁で反射します。
 レベルにより貫通反射弾の発射方向が増えます。
 レベル1:上のみ
 レベル2:上、右上、左上、
 レベル3:上、右上、左上、下
 レベル4:上、右上、左上、下、右下、左下

ChatGPTの回答(パワーアップの実装についての段取り)

パワーアップアイテムの効果を段階的に実装するための段取りを以下に提案します。この機能追加を行う前に、既存のゲームの設計やコードを十分に理解し、新しい機能が既存のシステムとどのように統合されるかを検討することが重要です。

ステップ1: パワーアップレベルとタイプの追加

  • 自機のデータ構造に、パワーアップのレベルとタイプを追跡するためのプロパティを追加します。例えば、powerUpLevelpowerUpTypeの2つのプロパティを持たせることができます。

ステップ2: パワーアップアイテムの取得と効果の適用

  • パワーアップアイテムを取得した際の処理を実装します。アイテムの種類に応じてpowerUpTypeを更新し、powerUpLevelを変更します。自機がダメージを受けた場合の処理もここで更新し、レベルが下がるようにします。

ステップ3: 強化ビームの実装

  • powerUpTypepowerUpLevelに応じて、ビームの発射方法を変更します。連射弾の場合はビームの数と形状を変え、貫通反射弾の場合は発射方向とパーティクルの追加を行います。

ステップ4: 強化ビームの当たり判定と効果

  • 各パワーアップレベルとタイプに応じたビームの当たり判定を実装します。連射弾の場合は横方向の球数に応じたダメージを敵に与え、貫通反射弾の場合はビームが敵を貫通し、壁で反射する処理を実装します。

ステップ5: パーティクル効果の追加

  • 貫通反射弾に彗星の尾のようなパーティクルを追加します。パーティクルはビームに付随して移動し、ビームが画面外に出た際に消去されるようにします。

ステップ6: テストと調整

  • 各機能を実装した後、ゲームのバランスやビームの見た目、動作をテストし、プレイヤーに適切なフィードバックが得られるように調整します。特に、新しいビームタイプが敵との相互作用にどのように影響するかを注意深く確認することが重要です。

ステップ7: ドキュメントとリファクタリング

  • 実装が完了したら、コードのドキュメントを更新し、必要に応じてリファクタリングを行います。新しい機能が既存のゲームプレイにどのように統合されるかを明確にすることで、将来の開発やメンテナンスが容易になります。

このプロセスは、新しい機能の導入を段階的に行い、各ステップでのテストと評価を通じて、ゲームの品質を保つことを目的としています。

ステップ1、ステップ2の実装指示(プロンプト)

ChatGPTの示した段取りを順番にこなしていきます。
とりあえず、ステップ1と2はパワーアップの枠組みだけの話なので、細かいことは言わずにまとめてプログラミングを進めてもらいます。

まずステップ1と2を実装してください

ChatGPTの回答(ソースコード ステップ1、ステップ2実装)

ステップ1とステップ2の実装に向けた基本的な概念を説明します。この説明に基づいて、ゲームの既存のコードを修正・追加していくことになります。

ステップ1: パワーアップレベルとタイプの追加

まず、自機のデータ構造にパワーアップのレベルとタイプを追跡するためのプロパティを追加する必要があります。これは、自機のオブジェクトに新たなプロパティを追加することで実現できます。

// 自機の初期化部分に追加
let powerUpLevel = 0; // パワーアップのレベル(0-4)
let powerUpType = null; // パワーアップのタイプ(null, 'rapidFire', 'piercing')

ステップ2: パワーアップアイテムの取得と効果の適用

パワーアップアイテムを取得した際の処理を実装します。これは、既存のcheckItemCollection関数内でアイテムのタイプに応じてpowerUpTypepowerUpLevelを更新することで実現できます。また、自機がダメージを受けた場合の処理も更新して、レベルが適切に下がるようにします。

function checkItemCollection() {
    for (let i = powerUpItems.length - 1; i >= 0; i--) {
        const item = powerUpItems[i];
        if (playerX < item.x + 32 &&
            playerX + playerImage.width > item.x &&
            playerY < item.y + 32 &&
            playerY + playerImage.height > item.y) {
            // アイテムを取得
            switch (item.type) {
                case 1: // 連射弾強化のアイテム
                    powerUpType = 'rapidFire';
                    if (powerUpLevel < 4) powerUpLevel++;
                    break;
                case 2: // 貫通反射弾強化のアイテム
                    powerUpType = 'piercing';
                    if (powerUpLevel < 4) powerUpLevel++;
                    break;
                // 他のアイテムタイプに応じたケースも追加可能
            }
            createParticles(item.x + 16, item.y + 16, '255, 255, 255', 2, 2);
            powerUpItems.splice(i, 1); // アイテムを削除
        }
    }
}

また、自機がダメージを受けた際にパワーアップレベルを下げる処理をcheckPlayerCollision関数内に追加します。

if (playerHealth <= 0) {
    // ここに自機がダメージを受けた際のパワーアップレベルの処理を追加
    if (powerUpLevel > 0) powerUpLevel--;
    else powerUpType = null; // パワーアップレベルが0になったらタイプもリセット
}

これらの変更を行うことで、パワーアップアイテムの取得とその効果の基本的なフレームワークがゲームに組み込まれます。これにより、後続のステップで具体的なパワーアップ効果(ビームの変化など)を実装する準備が整います。

ChatGPTの誤りに対する指摘(プロンプト)

ChatGPTの回答をよく見るとおかしな点があることがあります。

かなり精度の高いプログラムを組んでくれますが、指示の意図を取り違えたり、イメージと違う方向に進んでしまうこともありますし、単純に人のケアレスミスのようなことも起きます。

何も考えず、とりあえず出てきたソースコードを組み込んで動かして、その後に挙動がおかしいところやエラーが出るところを伝えてもいいのですが、実行する前に明らかに間違っていることに気がついたら先に指摘します。

下記について、if (playerHealth <= 0)だとすでに自機が爆発してゲームオーバーではありませんか?

if (playerHealth <= 0) {
    // ここに自機がダメージを受けた際のパワーアップレベルの処理を追加
    if (powerUpLevel > 0) powerUpLevel--;
    else powerUpType = null; // パワーアップレベルが0になったらタイプもリセット
}

ChatGPTの回答(ソースコード 指摘修正)

その通りです、私の説明に誤りがありました。自機がダメージを受けるたびにパワーアップレベルを下げる処理は、playerHealthが0になる前、つまり自機がダメージを受ける度に行われるべきです。自機のplayerHealthが0になったときには、ゲームオーバーの処理がすでに進行中です。したがって、正しくは自機がダメージを受けた際(playerHealthが減少する際)にパワーアップレベルを調整する必要があります。

以下のように修正します:

// 自機と敵の当たり判定処理内で、自機がダメージを受けた時の処理
if (distance < 8 + enemy.size / 2 && !invincible) {
    // 無敵状態ではないときのみ判定
    invincible = true; // 無敵状態にする
    playerHealth -= 1; // プレイヤーのヘルスを減少させる

    // パワーアップレベルを調整する処理
    if (powerUpLevel > 0) powerUpLevel--; // パワーアップレベルを下げる
    else powerUpType = null; // レベル0でダメージを受けた場合、パワーアップタイプをリセット

    // その他のダメージを受けた時の処理(パーティクル生成など)
}

この修正により、自機が敵に接触してダメージを受けるたびに、パワーアップレベルが適切に調整されます。自機のヘルスが0になる前、つまりゲームオーバーになる前にこの処理が行われることで、ゲームプレイのフローが保たれます。

ステップ3の実装指示(プロンプト)

ステップ1、2が問題なく実装されたので、続けてステップ3も実装してもらいます。

1、2とまとめてやってもよかったかもしれませんが、小分けにした方が丁寧な結果が返ってくるように思います。

ステップ3を実装して

ChatGPTの回答(ソースコード ステップ3の実装)

ステップ3では、連射弾と貫通反射弾の強化を実装します。これには、ビームの発射ロジックとビームの形状、挙動のカスタマイズが含まれます。具体的には、パワーアップレベルとタイプに応じてビームの発射方法を変更し、必要に応じて追加のビームを発射するようにします。また、貫通反射弾の場合は、ビームが敵を貫通し、壁で反射するように挙動を設定します。

ステップ3の実装指針:

  1. ビームの発射関数をカスタマイズする:
    • パワーアップタイプ(powerUpType)とレベル(powerUpLevel)に応じて、発射するビームの数、形状、挙動を変更します。
    • 連射弾の場合は、レベルに応じて横に広がるビームを発射します。例えば、レベル1では3つのビーム、レベル2では5つのビームとする。
    • 貫通反射弾の場合は、レベルに応じて異なる方向にビームを発射し、壁で反射するようにします。
  2. ビームオブジェクトのカスタマイズ:
    • ビームオブジェクトに新しいプロパティ(例:type, isPiercing)を追加して、ビームの挙動を制御します。
    • updateBeams関数内でこれらのプロパティをチェックし、ビームの挙動(貫通するか、反射するか)を決定します。
  3. ビームの描画方法の変更:
    • 貫通反射弾には彗星のような尾を描画するため、drawBeams関数(もしくは相当するビームを描画する関数)にパーティクルの描画ロジックを追加します。

コード例(概念的な説明):

// ビームを発射する関数のカスタマイズ
function fireBeam() {
    // パワーアップタイプとレベルに基づいてビームをカスタマイズ
    switch (powerUpType) {
        case "rapidFire":
            // レベルに応じて複数のビームを発射
            for (let i = 0; i < (powerUpLevel * 2 + 1); i++) {
                beams.push(createBeam(playerX, playerY, "rapid", i - powerUpLevel));
            }
            break;
        case "piercing":
            // レベルに応じた方向のビームを発射
            beams.push(createBeam(playerX, playerY, "piercing", 0)); // 上向き
            if (powerUpLevel >= 2) {
                beams.push(createBeam(playerX, playerY, "piercing", 1)); // 右上
                beams.push(createBeam(playerX, playerY, "piercing", -1)); // 左上
            }
            // さらにレベルが高い場合は追加の方向のビーム
            break;
    }
}

// 新しいビームオブジェクトを生成する関数
function createBeam(x, y, type, directionOffset) {
    // ビームの形状や挙動をカスタマイズするプロパティを設定
    return {
        x: x + directionOffset * 10, // 例: 方向オフセットに応じて発射位置を調整
        y: y,
        type: type,
        isPiercing: type === "piercing", // 貫通するかどうか
        // その他のビームプロパティ...
    };
}

// ビームの更新関数で貫通や反射の挙動を実装
function updateBeams() {
    // 各ビームについてループ
    beams.forEach(beam => {
        // 貫通反射弾の場合の特別な挙動
        if (beam.isPiercing) {
            // 壁で反射するロジック
            // ビームが壁に当たったかどうかをチェックし、必要に応じて方向を反転
        }
        // ビームの基本的な更新ロジック...
    });
}

この段階では、ビームの基本的な挙動(移動、敵との衝突判定など)をカスタマイズするための基盤を設定します。実際のゲームのロジックやパラメータは、ゲームのデザインや要件に応じて調整してください。

パワーアップしたビームを発射処理を完成させる(プロンプト)

提示されたソースコードを見ると「// さらにレベルが高い場合は追加の方向のビーム」といったコメントで細かい実装が省略されています。

そのため、関数名を指定して完成するように指示します。

function fireBeamを完成させて

ChatGPTの回答(ソースコード パワーアップしたビーム発射)

fireBeam関数を完成させるには、ビームの発射ロジックをパワーアップレベルとタイプに応じて変更する必要があります。ここでは、連射弾強化(rapidFire)と貫通反射弾強化(piercing)の2種類のパワーアップに対応したビームの発射ロジックを実装します。

完成したfireBeam関数の例:

// ビームを発射する関数
function fireBeam() {
    // パワーアップタイプに応じたビームの挙動
    switch (powerUpType) {
        case "rapidFire":
            // 連射弾強化: レベルに応じて横に広がるビームを発射
            const numberOfBeams = 2 * powerUpLevel + 1; // レベルに応じたビームの数
            for (let i = 0; i < numberOfBeams; i++) {
                const offset = (i - Math.floor(numberOfBeams / 2)) * 10; // 中心からのオフセット
                beams.push({
                    x: playerX + offset, // オフセットを加えた位置から発射
                    y: playerY,
                    width: 4, // ビームの幅
                    height: 20, // ビームの高さ
                    speed: 10, // ビームの速度
                    type: "rapidFire"
                });
            }
            break;
        case "piercing":
            // 貫通反射弾強化: レベルに応じた方向のビームを発射
            beams.push(createBeam(playerX, playerY - 20, "up")); // 常に上向きのビームを発射
            if (powerUpLevel > 1) {
                // レベル2以上で追加の方向のビーム
                beams.push(createBeam(playerX + 15, playerY - 15, "rightUp"));
                beams.push(createBeam(playerX - 15, playerY - 15, "leftUp"));
            }
            if (powerUpLevel > 2) {
                // レベル3以上で下向きのビーム
                beams.push(createBeam(playerX, playerY + 20, "down"));
            }
            break;
        default:
            // パワーアップがない場合は通常のビームを発射
            beams.push({
                x: playerX,
                y: playerY - 20,
                width: 4,
                height: 20,
                speed: 10
            });
            break;
    }
}

// 新しいビームオブジェクトを生成する補助関数
function createBeam(x, y, direction) {
    return {
        x: x,
        y: y,
        width: 4, // ビームの幅
        height: 20, // ビームの高さ
        speed: 10, // ビームの速度
        direction: direction, // ビームの方向
        type: "piercing" // ビームのタイプ
    };
}

このコードは、パワーアップのタイプとレベルに基づいて異なるビームを発射するロジックを実装しています。具体的には、連射弾強化では中心から横に広がるビームを、貫通反射弾強化ではレベルに応じて異なる方向のビームを発射します。また、createBeam関数を使用して、貫通反射弾のビームオブジェクトを生成しています。

ゲームの要件に応じて、ビームの形状、速度、方向などのパラメータを調整

追加指示 ビーム生成処理を完成させる(プロンプト)

さらに「// その他のビームプロパティ…」と省略されていた別の関数も完成してもらいます。

function createBeamを完成させて

ChatGPTの回答(ソースコード ビーム生成処理)

createBeam関数は、特定の方向に貫通反射弾を発射するためのビームオブジェクトを生成します。この関数は、ビームの位置、方向、およびその他の特性をカスタマイズするために使用されます。以下に、createBeam関数の実装例を示します。

この関数はビームの初期位置(x, y)、発射方向(direction)、およびビームのタイプ(type)をパラメータとして受け取り、それに基づいてビームオブジェクトを生成します。ビームの方向に応じて、ビームの移動方向や速度を変更することができます。

完成したcreateBeam関数の例:

// ビームオブジェクトを生成する関数
function createBeam(x, y, direction) {
    let velocityX = 0;
    let velocityY = -10; // 通常は上向きに発射

    // 方向に応じて速度を調整
    switch (direction) {
        case "up":
            velocityX = 0;
            velocityY = -10;
            break;
        case "rightUp":
            velocityX = 5;
            velocityY = -5;
            break;
        case "leftUp":
            velocityX = -5;
            velocityY = -5;
            break;
        case "down":
            velocityX = 0;
            velocityY = 10;
            break;
        case "rightDown":
            velocityX = 5;
            velocityY = 5;
            break;
        case "leftDown":
            velocityX = -5;
            velocityY = 5;
            break;
    }

    // 生成されるビームオブジェクト
    return {
        x: x,
        y: y,
        velocityX: velocityX,
        velocityY: velocityY,
        type: "piercing", // ここでは貫通タイプ固定
        radius: 3, // ビームの半径
    };
}

この関数は、ビームのtype"piercing"に固定していますが、必要に応じて他のタイプのビームを生成するためにtypeパラメータを関数の引数に追加することも可能です。また、velocityXvelocityYはビームの移動速度を制御します。この速度はビームの方向に応じて調整され、ビームが指定された方向に移動するようになります。

この例では、ビームが上向き、右上、左上、下向き、右下、左下の方向に発射されるように、各方向に対応する速度を設定しています。ビームの半径はradiusプロパティで指定され、これはビームの見た目や当たり判定のサイズに影響します。

追加指示 ビーム更新処理を完成させる(プロンプト)

もうひとつ「// ビームの基本的な更新ロジック…」と省略されていた別の関数も完成してもらいます。

function updateBeamsを完成させて

ChatGPTの回答(ソースコード ビーム更新処理)

updateBeams関数はゲーム内のビームの位置を更新し、ビームの描画を担当します。この関数は、ビームが敵に当たったか、画面外に出たかを判断し、必要に応じてビームを削除します。以下は、貫通反射弾を含めたビームの更新処理を行うupdateBeams関数の完成形です。

updateBeams関数の例:

function updateBeams() {
    for (let i = beams.length - 1; i >= 0; i--) {
        const beam = beams[i];

        // ビームの位置を更新
        beam.x += beam.velocityX;
        beam.y += beam.velocityY;

        // ビームの描画
        if (beam.type === "piercing") {
            // 貫通反射弾の描画
            ctx.fillStyle = '#00BFFF'; // 青白い色
            ctx.beginPath();
            ctx.arc(beam.x, beam.y, beam.radius, 0, Math.PI * 2);
            ctx.fill();

            // 彗星の尾のようなパーティクルを生成
            createParticles(beam.x, beam.y, '135,206,250', 1, 1); // 軽い青色のパーティクル

            // 壁での反射処理
            if (beam.x <= 0 || beam.x >= canvas.width) {
                beam.velocityX = -beam.velocityX; // X方向の速度を反転
            }
        } else {
            // 通常のビームの描画
            ctx.fillStyle = '#ff0'; // 黄色
            ctx.fillRect(beam.x - beam.width / 2, beam.y, beam.width, beam.height);
        }

        // 画面外に出たビームを配列から削除(貫通反射弾は画面外に出たら削除)
        if (beam.y < 0 || beam.y > canvas.height || (beam.type !== "piercing" && (beam.x < 0 || beam.x > canvas.width))) {
            beams.splice(i, 1);
        }
    }
}

この例では、ビームが"piercing"タイプの場合、青白い色で円形のビームを描画し、彗星の尾のようなパーティクルを生成しています。また、"piercing"タイプのビームは左右の壁で反射するように処理が追加されています。通常のビームは、黄色で四角形として描画されます。

ビームが画面外に出た場合の処理も含まれており、貫通反射弾の場合は上下方向に出た時のみ削除し、通常のビームは左右または上下方向に出た場合に削除します。これにより、ビームの種類に応じた挙動の違いを実装しています。

ビームのパワーアップ、敵の弾発射の実装まとめ(プロンプト)

その後もこういったやり取りを繰り返し、ビームのパワーアップ処理を完成させていきます。

単純にエラーが出る場合もあれば、エラーは出ないけれども自分が思っていたのと違う感じのビームになることもあります。

これは、他の人にプログラミングを頼むときに、どのように伝えるか説明の仕方によってイメージがうまく伝わることもあれば、伝わらないこともあるのに似ています。

連射弾と貫通反射弾を実装しましたが、貫通反射弾が強すぎて、連射弾が外れパワーアップ(気分的にはむしろパワーダウン)となってしまったため、威力を調整したり、連射弾にも一部貫通弾の要素を加えるなどしています。

こういった、実際にゲームを動かして、ゲーム性や難易度を調整するにはやっぱり人の間隔や判断が必要になってきます。

どのようなプロンプトを流しているのかの参考までに記載しますが、全部読む必要はありません。

また、途中でビームが敵に当たった時の効果音を追加しています。

そのあたりで、ブラウザで使用するChatGPTとは別にChatGPT搭載Visual Studio Code互換エディタ「Cursor」を併用し始めたため、そちらの中でChatGPTとやり取りしており、履歴を消してしまったため一部のプロンプトが飛んでいることをご了承ください。

※下記のプロンプトは実際は一度に流しているのではなく、行間ごとにChatGPTの回答がありますがそれは省略しています。

修正前の下記ソースをもとにして、updateBeamsの修正を適用してください。省略せずに全文書いてください。
(略 ChatGPTに参照してほしいソースコードの一部)

エラーです
Uncaught SyntaxError: Identifier 'createBeam' has already been declared (at game.js:244:1)

貫通反射弾の数が多すぎて処理がとても遅くなってしまいます。
貫通反射弾は一つの方向につき同時に1発だけとしてください。

その提案は採用せずに、もっとシンプルに弾数を減らしたいと思います。
通常ビームと連射弾はこれまでどおり setInterval(fireBeam, 200);
貫通反射弾は setInterval(fireBeam, 1000);
となるようにしてください

updateFireBeamInterval を完成させて、それを呼び出すソースコードも教えて

今のgame.jsを共有します。これをもとに修正をしてください
(略 game.js全文を貼り付け)

エラーです
game.js:861 Uncaught SyntaxError: Identifier 'scheduleBeamFiring' has already been declared (at game.js:861:1)

貫通反射弾の直径を10倍にして当たり判定もそれに合わせて、彗星の尾のようなパーティクルの直径も同様に大きくして。
また、そのパーティクルの数は10分の1に減らして。

game.jsをこちらで修正したので共有します。貫通反射弾に当たり判定がないようなので、このソースをもとに当たり判定を実装してください。弾の半径に合わせて当たり判定を適用してください。
(略 game.js全文を貼り付け)

通常弾が敵に当たらず素通りするようになりました。連射弾も同様かもしれません。修正してください。

貫通反射弾が敵にダメージを与えていないように見えます

パワーアップアイテムの当たり判定が見た目とずれているようです。
パワーアップアイテムよりも大きく左にそれた場所に自機があっても取ったことになっています。

自機の右側でしかパワーアップアイテムを取れません。左側はすり抜けます。調整してください。

やはり右下でしか取れません。当たり判定の範囲を確認するために当たり範囲が分かるように緑の線で囲ってください。

パワーアップアイテムの当たり判定の範囲はあっているようです。自機側にも同様に緑の線で囲ってください。

緑の線と実際の当たり判定にずれがあるようです。
自機の左側と上側の緑の線の中にアイテムの緑の線が入っても取れません。
自機の右側と下側は逆にアイテムの緑の線がまだ内側に入っていないうちに取れます。
自機の幅と高さともに1.5個分、右下に当たり判定がずれているように思われます。

このソースコードに修正を適用してください
(略 参照してほしいソースコード)

貫通反射弾レベル4の場合に右下、左下にも弾が発射されるようにしてください

反射貫通弾が下端でも反射するようにして

下端で反射するときとしない時があります

まだ下端で反射しないことがあります

createParticlesで life の上限、下限を引数で渡せるようにして

rapidFire のビームのY軸の開始位置を外側ほど低くして山型になるようにして

自機がゲーム画面外にはみ出さないようにして

エラー
Unchecked runtime.lastError: A listener indicated an asynchronous response by returning true, but the message channel closed before a response was received

ゲームオーバー時にタップで初期化してスタートしてください。
その際に、各種タイマーやリソースの解放を忘れずに。

ゲーム再開時にenemySpawnPatternsが正しく最初から読み直されていますか?
また、再開時に自機が点滅していました。
リセットされていないフラグや変数が他にもありませんか?

enemySpawnPatternsはgame.jsとは別のenemySpawnPatterns.jsにある
const enemySpawnPatterns のことですが、提示された修正方法で合っていますか?

下記もリセットしてください。ほかにリセットし忘れはないですか?
let currentTime = 0; // ゲーム開始からの経過時間
let gameOverWaitTime = 0.5; // 自機の耐久が0になってからゲームオーバー表示までの待機時間
let playerDeadTime = 0; // 自機の耐久が0になった時間(ゲーム開始からの経過時間を格納)

弾は敵の中心から生成して
弾が自機に当たる当たり判定の際、自機側の当たり判定は敵と当たるときの当たり判定の範囲と同じ自機の中心からの円にして

弾が敵の中心よりも左上から発生しているので中心から発生するように修正して
弾が画面外に消える時に当たってないのに自機に弾が当たった時のパーティクルが発生するのをやめて

弾が自機に当たったときは敵が自機に当たった時と同じ処理をして
一定時間点滅して無敵
衝突パーティクル発生(自機のヘルスが残っている時)
自機爆発パーティクル発生(自機のヘルスが0以下の時)

この修正により不要となり削除が必要な関数と宣言を教えて

弾が自機に当たりやすいと思います。
敵が自機に当たるのと同じ範囲で当たるようにしてください

当たり判定が視覚的に見えるように弾と自機それぞれに当たり判定の範囲を緑の線で描いてください

やはり敵の左上から弾が出現します。中央から出現するように調整して

ゲームオーバーのから再スタートするときに下記もリセットして
敵の弾
弾が自機に当たった時のパーティクル

shoots: 2 で敵の中心から16方向に時計回りで1周発射されるタイプの弾を実装して
弾の大きさは半径12
色は濃いオレンジ色

shoots: 1の弾が見えなくなったようです

エラー
Uncaught SyntaxError: Identifier 'speed' has already been declared (at game.js:477:19)

自機が無敵状態のときは弾が自機に当たっても弾を消さないで

大型ボスの追加(プロンプト)

やっぱりシューティングゲームにはボスが必要、ということで、大型ボスを追加します。

ChatGPTに単純に「大型ボスを追加して」だけでは伝わらないので、自分がイメージしている大型ボスの動きを具体的に説明します。

ファミコン時代の某有名横スクロールシューティングの1面のボスに近いイメージです。

【AIゲーム開発】大型ボス1
大型ボス1(large )の動きを実装する
大型ボスも他の敵と同様に @enemySpawnPatterns.js  にtimeで指定した時間に登場する。
ただし、大型ボスの shoots は 0: 弾を撃たない 1:弾を撃つ だけを表す。
複数種類の弾を撃つため、shootInterval は各弾種のインターバルのベースとする

例:
 弾1のインターバル = shootInterval * 2
 弾1のインターバル = shootInterval * 3

大型ボスは横長の体力ゲージをキャンバス内画面上部に設置する
体力ゲージは黄色のバーで表し、減った分は赤いバーで表す

ボスを撃破すると、パワーアップアイテムが種類ランダムで5個落ちる
落ちる場所もボスの中心座標周り半径128以内でランダムとする

大型ボス1は次の動きとします。

・通常モード(初期モード)
 ・上から100ぐらいまで下りてくる
 ・自機に向かって撃つ弾丸と16方向に時計回りで1周発射を定期的に発射
 ・発射間隔
  自機に向かって撃つ弾丸 = shootInterval  * 0.5
  16方向に時計回りで1周発射 = shootInterval  * 3

・追撃モード(ボスの体力が30%未満)
 ・上下左右に動きじわじわと自機に近づいてくる
 ・16方向に時計回りで1周発射を定期的に発射
 ・発射間隔
  16方向に時計回りで1周発射 = shootInterval  * 1

タイトル画面の追加(プロンプト)

これまでタイトル画面が無く、いきなりゲームが始まっていたのでタイトル画面を追加します。
タイトル画面は無くてもゲームは成り立つのですが、実はひとつ、実用的な理由があります。

実はJavaScriptで音を鳴らす場合、ユーザーが一度はブラウザの画面をクリックする必要があります。
恐らく、遷移先の画面でユーザーが意図せずいきなり大音量の音が鳴ったりしないようにするため、ブラウザによるセキュリティ上の処置であると思われます。

タイトル画面でスタートをクリック(タップ)してもらうことで、音を鳴らせるようにする目的も兼ねています。

初回起動時にタイトル画面を表示して
タイトル画面は背景画像を画面いっぱいに表示して
画面下部30%ぐらいのところに白字で START と表示して
STARTはゆっくり点滅させて
画面をタップするまではゲームをスタートしないで待機して
画面をタップしたらゲームをスタートして

さらに、タイトル画面からゲーム画面に遷移する際にパッと一瞬で切り替わるのではなく、トランジッション(画面の切り替えの際のアニメーション効果)をつけてみます。

フェードイン、フェードアウトや、単純なスライドでもいいのですが、少し凝ってブロックワイプにしてみます。

小さな四角(ブロック)が順番にめくれていくように画面が切り替わるトランジッションです。
これも、特別なプラグインやフレームワークを使わずに単純な計算とJavaScriptのロジックだけで実現しています。

いちから自力でプログラミングするなら面倒くさくて絶対にやりませんが、ChatGPTに頼めばこういった面倒なプログラムもあっという間に作ってくれます。

タイトル画面からゲーム画面に遷移するときにブロックワイプのトランジション効果をつけて
ブロックワイプの処理は2秒程度にしてください

BGMの追加(プロンプト)

BGMを追加します。
BGMもChatGPTで作りたかったのですが、ちょっと難しそうだったのでフリー素材のMP3を使用します。

こちらのフリー素材BGMを使用させていただきました。ありがとうございます。
Digital Transformation written by FLASH☆BEAT

ゲーム画面でBGMをループ再生したい
ゲームオーバー時にリソースの解放を忘れずに
ゲームオーバー画面やボス戦など途中でBGMが変わることも考慮して

BGMの音量調整できるように

バックグラウンドで音楽停止して

この時点のソースコード(game09_01)

この時点でのソースコードは次の状態でした。

index.html(ブラウザでアクセスするページ)

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Shooting Fighter</title>
    <link rel="stylesheet" href="style.css">
    <link rel="icon" type="image/png" href="images/player.png">
</head>
<body>
    <div id="game-container"></div>
    <script type="module" src="game.js"></script>
</body>
</html>

style.css(画面サイズなどのCSS定義)

html, body {
    height: 100%;
    margin: 0;
    display: flex;
    justify-content: center;
    align-items: center;
    background-color: #666; /* 背景色を設定、必要に応じて変更してください */
}

#game-container {
    width: 100%; /* コンテナの幅を画面幅に合わせる */
    max-width: 342px; /* キャンバスの最大幅を指定 */
    height: auto; /* 高さを自動調整 */
    aspect-ratio: 342 / 608; /* キャンバスのアスペクト比を維持 */
}

#game-container canvas {
    width: 100%; /* キャンバスの幅をコンテナの幅に合わせる */
    height: auto; /* 高さを自動調整してアスペクト比を維持 */
    image-rendering: pixelated; /* ピクセルの境界がはっきりするように */
}

game.js (ゲームのメインプログラム)

// キャンバスの設定
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
document.getElementById('game-container').appendChild(canvas);

canvas.width = 342;
canvas.height = 608;

// タイトル画面の背景画像を追加
const titleBackgroundImage = new Image();
titleBackgroundImage.src = 'images/title_background.png'; // タイトル画面の背景画像のパスを指定

// タイトル画面の表示フラグ
let showTitle = true;

// タイトル画面の点滅効果のための変数
let titleOpacity = 1;
let titleFadeDirection = -0.02;

// ブロックワイプのブロックサイズとブロックの配列を定義
const blockSize = 20;
const blocksWide = Math.ceil(canvas.width / blockSize);
const blocksHigh = Math.ceil(canvas.height / blockSize);
let blocks = [];

// BGMの設定
const bgm = new Audio('bgm/Digital_Transformation.mp3');
bgm.loop = true; // ループ再生を有効にする

// ヘルスアイコンの画像を読み込む
let healthIcon = new Image();
healthIcon.src = 'images/health_icon.png'; // ヘルスアイコンのパスを指定

// 敵の出現パターンの定義をインポート
import { originalEnemySpawnPatterns } from './enemySpawnPatterns.js';
let enemySpawnPatterns = []; // ゲーム中に使用するパターンのコピー

let currentTime = 0; // ゲーム開始からの経過時間
let invincible = false; // 無敵状態のフラグ
let invincibleTime = 0; // 無敵時間のカウンタ

let gameOverWaitTime = 0.5; // 自機の耐久が0になってからゲームオーバー表示までの待機時間
let playerDeadTime = 0; // 自機の耐久が0になった時間(ゲーム開始からの経過時間を格納)

// 自機の表示フラグ
let playerVisible = true;

// 自機の設定
const playerImage = new Image();
playerImage.src = 'images/player.png';
let playerX = canvas.width / 2;
let playerY = canvas.height - 50;

// 自機の耐久値を設定
let playerHealth = 5;
let gameOver = false; // ゲームオーバーフラグ

// 自機の初期化部分に追加
let powerUpLevel = 0; // パワーアップのレベル(0-6)
let powerUpType = null; // パワーアップのタイプ(null, 'rapidFire', 'piercing')

// ビームの設定
const beams = [];

// パワーアップアイテムのインスタンスを格納する配列
const powerUpItems = [];

// 弾丸を格納する配列
let bullets = [];

// 星の宣言
const stars = [];
const starCount = 30; // 生成する星の数

// パーティクル配列
const particles = [];

// 画像のプリロードを管理するためのオブジェクト
const loadedImages = {};

const spawnPositions = [57, 114, 171, 228, 285]; // 画面の横幅を5分割したX座標

// 音声ファイルを格納するオブジェクトを定義
const loadedAudioFiles = {};

// AudioContextを定義
const audioContext = new (window.AudioContext || window.webkitAudioContext)();

// 音声ソースを格納する配列を定義
const sources = [];


// ビーム発射のフラグ
let firing = false;

// ビーム発射の間隔を管理する変数
let fireBeamInterval = 200; // デフォルトは200ms
let beamIntervalId;

// パワーアップアイテムの画像パス
const itemTypes = {
    1: 'images/powerup_1.png',
    2: 'images/powerup_2.png',
};

// 効果音ファイルのパスをオブジェクトで管理する
const soundPaths = {
    beam_hit: 'sounds/beam_hit.mp3',
    game_over: 'sounds/game_over.mp3',
    hit_by_bullet: 'sounds/hit_by_bullet.mp3',
    hit_by_enemy: 'sounds/hit_by_enemy.mp3',
    large_explosion: 'sounds/large_explosion.mp3',
    medium_explosion: 'sounds/medium_explosion.mp3',
    power_up: 'sounds/power_up.mp3',
    ship_explosion: 'sounds/ship_explosion.mp3',
    small_explosion: 'sounds/small_explosion.mp3',
};

// 敵のインスタンスを保持する配列
let enemies = [];

// 敵キャラの定義
const enemyTypes = {
    small: [
        { size: 32, imageSrc: 'images/enemy_small_1.png', movement: 'linear', health: 1 },
        { size: 32, imageSrc: 'images/enemy_small_2.png', movement: 'zigzag', health: 3 },
        { size: 32, imageSrc: 'images/enemy_small_3.png', movement: 'looping', health: 8 },
        { size: 32, imageSrc: 'images/enemy_small_4.png', movement: 'dive', health: 6 },
        { size: 32, imageSrc: 'images/enemy_small_5.png', movement: 'verticalSweep', health: 10 },
        { size: 32, imageSrc: 'images/enemy_small_6.png', movement: 'backAndForth', health: 20 },
        { size: 32, imageSrc: 'images/enemy_small_7.png', movement: 'chase', health: 60 },
        { size: 32, imageSrc: 'images/enemy_small_8.png', movement: 'retreat', health: 40 }
    ],
    medium: [
        { size: 64, imageSrc: 'images/enemy_medium_1.png', movement: 'bounce', health: 200 },
        { size: 64, imageSrc: 'images/enemy_medium_2.png', movement: 'spiral', health: 100 }
    ],
    large: [
        {
            size: 128,
            imageSrc: 'images/enemy_large_boss_1.png',
            movement: 'bossPattern1',
            health: 2000, // ボスの体力を設定
            maxHealth: 2000, // 最大体力も設定(体力ゲージの表示に使用)
            boss: true, // ボスフラグ
            behavior: function(enemy) {
                // health プロパティの存在チェック
                if (typeof enemy.health === 'undefined') {
                    console.error('Enemy object is missing the health property');
                    return; // この敵の処理をスキップ
                }
                
                // 通常モード
                if (enemy.health >= enemy.maxHealth * 0.3) {
                    if (enemy.y >= 100) {
                        // 左右に揺れながら移動する
                        if (enemy.moveRight === undefined) {
                            enemy.moveRight = true; // 初期方向を右に設定
                        }

                        // 左または右端に到達したら方向転換する
                        if (enemy.x <= 0 || enemy.x + enemy.size >= canvas.width) {
                            enemy.moveRight = !enemy.moveRight;
                        }

                        // 移動方向に応じてX座標を更新する
                        enemy.x += enemy.moveRight ? 0.5 : -0.5;

                        // 画面端で反転するための境界値のチェックと修正
                        enemy.x = Math.max(0, Math.min(canvas.width - enemy.size, enemy.x));
                        
                        // 上下に揺れる動き(サイン波を使う)
                        enemy.y += Math.sin(Date.now() / 100) * 0.5;
                    } else {
                        // 初期の下降動作
                        enemy.y += Math.min(2, (128 - enemy.y) * 0.01); // 128までゆっくり下降
                    }
                    
                    // 弾の発射設定
                    enemy.shootInterval = 1.5;  // 発射間隔
                    enemy.shoots = 1; // 自機に向かって撃つ弾丸
                }
                // 追撃モード
                else {
                    enemy.x += playerX > enemy.x ? 0.3 : playerX < enemy.x ? -0.3 : 0;
                    enemy.y += playerY > enemy.y ? 0.3 : playerY < enemy.y ? -0.3 : 0;
                    
                    // 弾の発射設定
                    enemy.shootInterval = 3;  // 発射間隔
                    enemy.shoots = 2; // 16方向に時計回りで1周発射
                }
            }
        },
        // 他の大型ボスの定義...
    ],
    extraLarge: [
        {
            size: 256,
            imageSrc: 'images/enemy_extra_large_boss.png',
            movement: 'finalBossPattern',
            behavior: function() {
                // 超大型ラスボスの挙動
            }
        }
    ]
};

// 音声ファイルの事前読み込み関数
function preloadAudioFiles() {
    Object.values(soundPaths).forEach(soundPath => {
        fetch(soundPath)
            .then(response => response.arrayBuffer())
            .then(buffer => audioContext.decodeAudioData(buffer))
            .then(audioBuffer => {
                // 音声ファイルをデコードしてバッファに保存
                loadedAudioFiles[soundPath] = audioBuffer;
            })
            .catch(error => {
                console.error('音声ファイルの読み込みエラー:', error);
            });
    });
}

// AudioContextを使用して音声を再生する関数
function playSound(soundName, volume = 1) {
    const soundPath = soundPaths[soundName]; // 指定された効果音のパスを取得
    if (soundPath) {
        const audioBuffer = loadedAudioFiles[soundPath];
        if (audioBuffer) {
            const source = audioContext.createBufferSource();
            const gainNode = audioContext.createGain(); // 音量を制御するためのGainNodeを作成
            source.buffer = audioBuffer;
            source.connect(gainNode); // GainNodeに接続
            gainNode.connect(audioContext.destination);
            gainNode.gain.value = volume; // 音量を設定
            source.start(0); // 再生を即座に開始
        } else {
            console.error('音声ファイルが読み込まれていません');
        }
    } else {
        console.error('指定された効果音が見つかりません');
    }
}

// ゲームオーバー時に効果音のバッファやリソースを解放する関数
function releaseSoundResources() {
    sources.forEach(source => {
        source.stop(); // 音声を停止
    });
    sources.length = 0; // 音声ソース配列を空にする
    stopBgm();  // BGM停止
}

// BGMの再生を開始する関数
function startBgm() {
    bgm.play();
}

// BGMの再生を停止する関数
function stopBgm() {
    bgm.pause();
    bgm.currentTime = 0; // 再生位置を最初に戻す
}

// ボス戦開始時にBGMを切り替える例
function startBossBattle() {
    stopBgm(); // 現在のBGMを停止
    bgm.src = 'path/to/your/boss_battle_bgm.mp3'; // ボス戦用のBGMに切り替え
    startBgm(); // 新しいBGMを再生
}

// BGMの音量を調整する関数
function setBgmVolume(volume) {
    bgm.volume = volume;
}

// BGMのバックグラウンド停止のためページの可視性が変更されたときに呼び出される関数
function handleVisibilityChange() {
    if (document.hidden) {
        bgm.pause(); // ページがバックグラウンドになったら音楽を停止
    } else {
        bgm.play(); // ページがフォアグラウンドに戻ったら音楽を再開
    }
}

// BGMのバックグラウンド停止のためvisibilitychangeイベントリスナーを追加
document.addEventListener('visibilitychange', handleVisibilityChange);

// デバッグ用
console.log(itemTypes); // itemTypes オブジェクトの内容を確認

// ビームを発射する関数
function fireBeam() {
    if (firing) {
        // パワーアップタイプに応じたビームの挙動
        switch (powerUpType) {
            case "rapidFire":
                // 連射弾強化: レベルに応じて横に広がるビームを発射
                fireRapidFireBeam();
                if (powerUpLevel === 6) {
                    // 貫通反射 piercing ビームも同時に発射
                    firePiercingBeam();
                }
                break;
            case "piercing":
                // 貫通反射弾強化: レベルに応じた方向のビームを発射
                firePiercingBeam();
                if (powerUpLevel === 6) {
                    // 連射弾 rapidFire ビームも同時に発射
                    fireRapidFireBeam();
                }
                break;
            default:
                // パワーアップがない場合は通常のビームを発射
                fireNormalBeam();
                break;
        }
    }
}

// 連射弾 rapidFire ビームを発射する関数
function fireRapidFireBeam() {
    var numberOfBeams = 2 * powerUpLevel + 1; // レベルに応じたビームの数
    numberOfBeams = numberOfBeams > 11 ? 11 : numberOfBeams; // ビームが11以上なら11に固定
    for (let i = 0; i < numberOfBeams; i++) {
        const offset = (i - Math.floor(numberOfBeams / 2)) * 8; // 中心からのオフセット
        const yOffset = Math.abs(offset) * 0.5; // 外側ほど低い位置から発射するためのオフセット
        beams.push({
            x: playerX + offset, // オフセットを加えた位置から発射
            y: playerY + yOffset, // Y軸の開始位置を調整
            width: 4, // ビームの幅
            height: 20, // ビームの高さ
            speed: 10, // ビームの速度
            type: "rapidFire"
        });
    }
}

// 貫通反射弾 piercing ビームを発射する関数
function firePiercingBeam() {
    // レベル6の場合のためにビームの種類を判定して、連射弾 rapidFire の場合は発射しない
    if (powerUpType === "piercing") {
        beams.push(createBeam(playerX, playerY - 20, "up")); // 上向きのビームを発射
        if (powerUpLevel > 2) {
            // 追加の方向(右上、左上)のビーム
            beams.push(createBeam(playerX + 15, playerY - 15, "rightUp"));
            beams.push(createBeam(playerX - 15, playerY - 15, "leftUp"));
        }
        if (powerUpLevel > 3) {
            // 追加の方向(右下、左下)のビーム
            beams.push(createBeam(playerX + 15, playerY + 20, "rightDown"));
            beams.push(createBeam(playerX - 15, playerY + 20, "leftDown"));
        }
    }
    // レベル6の場合、連射弾 rapidFire でも発射する
    if (powerUpLevel > 1) {
        // 追加の方向(下)のビーム
        beams.push(createBeam(playerX, playerY + 20, "down"));
    }
}

// 通常のビームを発射する関数
function fireNormalBeam() {
    beams.push({
        x: playerX,
        y: playerY - 20,
        width: 4,
        height: 20,
        speed: 10
    });
}

// ビーム発射間隔を更新する関数
function updateFireBeamInterval() {
    if (powerUpType === "piercing") {
        fireBeamInterval = 500; // 貫通反射弾の場合、間隔を500msに設定
    } else {
        fireBeamInterval = 200; // それ以外の場合、間隔を200msに設定
    }

    scheduleBeamFiring(); // ビーム発射間隔を更新
}

// ビーム発射をスケジュールする関数(既存の関数を利用または修正が必要な場合)
function scheduleBeamFiring() {
    clearInterval(beamIntervalId); // 既存の間隔で設定されたビーム発射をクリア
    beamIntervalId = setInterval(fireBeam, fireBeamInterval); // 新しい間隔でビーム発射をスケジュール
}

// ビームオブジェクトを生成する関数
function createBeam(x, y, direction) {
    let velocityX = 0;
    let velocityY = -10; // 通常は上向きに発射

    // 方向に応じて速度を調整
    switch (direction) {
        case "up":
            velocityX = 0;
            velocityY = -10;
            break;
        case "rightUp":
            velocityX = 5;
            velocityY = -5;
            break;
        case "leftUp":
            velocityX = -5;
            velocityY = -5;
            break;
        case "down":
            velocityX = 0;
            velocityY = 10;
            break;
        case "rightDown":
            velocityX = 5;
            velocityY = 5;
            break;
        case "leftDown":
            velocityX = -5;
            velocityY = 5;
            break;
    }

    // 生成されるビームオブジェクト
    return {
        x: x,
        y: y,
        velocityX: velocityX,
        velocityY: velocityY,
        type: "piercing", // ここでは貫通タイプ固定
        radius: 8, // ビームの半径
    };
}


// 弾丸を生成する関数
function createBullet(x, y, velocityX, velocityY, radius = 6, color = 'rgba(255, 165, 0, 0.8)') {
    bullets.push({ x, y, velocityX, velocityY, radius, color });
}

// 敵が弾を撃つ処理
function enemyShoots(enemy) {
    const bulletX = enemy.x + enemy.size / 2; // 敵の中心X座標
    const bulletY = enemy.y + enemy.size / 2; // 敵の中心Y座標
    let speed = 5; // 弾丸の速度

    switch (enemy.shoots) {
        case 1: // 自機に向かって撃つ弾丸
            const dx = playerX - bulletX;
            const dy = playerY - bulletY;
            const distance = Math.sqrt(dx * dx + dy * dy);
            speed = 1.5; // 弾丸の速度
            const velocityX = (dx / distance) * speed;
            const velocityY = (dy / distance) * speed;
            createBullet(bulletX, bulletY, velocityX, velocityY);
            break;
        case 2: // 16方向に時計回りで1周発射
            speed = 1; // 弾丸の速度
            const angleIncrement = Math.PI * 2 / 16; // 16方向に分割するための角度の増分
            for (let i = 0; i < 16; i++) {
                const angle = i * angleIncrement;
                const velocityX = Math.cos(angle) * speed;
                const velocityY = Math.sin(angle) * speed;
                createBullet(bulletX, bulletY, velocityX, velocityY, 9, 'rgba(255, 140, 0, 0.8)'); // 濃いオレンジ色
            }
            break;
        // 他の弾丸やビームの種類に応じてケースを追加
    }
}


// 弾丸の更新と描画処理
function updateAndDrawBullets(ctx) {
    for (let i = bullets.length - 1; i >= 0; i--) {
        const bullet = bullets[i];
        bullet.x += bullet.velocityX;
        bullet.y += bullet.velocityY;

        // 弾丸が画面外に出たか、自機に当たったかをチェック
        if (bullet.x < 0 || bullet.x > canvas.width || bullet.y < 0 || bullet.y > canvas.height ||
            (checkCollisionWithPlayer(bullet.x, bullet.y, bullet.radius * 2, bullet.radius * 2) && !invincible)) {
            bullets.splice(i, 1); // 弾丸を配列から削除
        } else {
            // 弾丸を円として描画
            ctx.fillStyle = 'rgba(255, 165, 0, 0.8)';
            ctx.beginPath();
            ctx.arc(bullet.x, bullet.y, bullet.radius, 0, Math.PI * 2, false);
            ctx.fill();

            // 当たり判定の範囲を白の線で描画(縁取り)
            ctx.strokeStyle = 'white';
            ctx.beginPath();
            ctx.arc(bullet.x, bullet.y, bullet.radius, 0, Math.PI * 2, false);
            ctx.stroke();
        }
    }
}


// 自機がダメージを受けたときの処理
function handlePlayerHit() {
    if (!invincible) {
        invincible = true; // 無敵状態にする
        playerHealth -= 1;

        playSound('small_explosion', 3);  // 被弾爆発音(小)

        // パワーアップレベルを調整する処理
        if (powerUpLevel > 0) powerUpLevel--; // パワーアップレベルを下げる
        if (powerUpLevel <= 0) powerUpType = null; // レベル0になったとき、パワーアップタイプをリセット

        // 自機がダメージを受けた際にビーム発射間隔を更新する
        updateFireBeamInterval();

        // コンソールに状態を出力
        console.log(`Player Health: ${playerHealth}, PowerUp Level: ${powerUpLevel}, PowerUp Type: ${powerUpType}`);

        if(playerHealth > 0){
            // プレイヤーが生きているときの衝突用
            createParticles(playerX, playerY, "255,165,0", 3, 3); // 自機の衝突パーティクル(オレンジ)を生成
        }
        if (playerHealth <= 0) {
            if(playerDeadTime == 0){
                playerDeadTime = currentTime;   // プレイヤーがやられた時間を保存
                firing = false;
                playerVisible = false;
                createParticles(playerX, playerY, "255,0,0", 5, 8); // 自機の爆発パーティクル(赤)を生成

                // 効果音のリソースを解放
                releaseSoundResources();

                playSound('ship_explosion', 1);  // 自機爆発音
            }
        }
    }
}

// 自機との当たり判定
function checkCollisionWithPlayer(bulletX, bulletY, bulletWidth, bulletHeight) {
    // ここでは自機の位置とサイズ、弾丸の位置を使って簡易的な当たり判定を行う
    // 自機のサイズや位置に応じて調整が必要
    const playerHitBox = { x: playerX - 20, y: playerY - 20, width: 40, height: 40 }; // 自機のヒットボックスを仮定
    if (bulletX < playerHitBox.x + playerHitBox.width &&
        bulletX + bulletWidth > playerHitBox.x &&
        bulletY < playerHitBox.y + playerHitBox.height &&
        bulletY + bulletHeight > playerHitBox.y) {
        handlePlayerHit(); // 自機がダメージを受けたときの処理を実行
        updateFireBeamInterval();   // ビーム発射間隔を更新する
        return true; // 当たり判定があったことを示す
    }
    return false; // 当たり判定がなかったことを示す
}

// 自機の描写を更新
function updatePlayer() {
    // 自機の画像を描画
    // 自機のサイズが64x64ピクセルであると仮定して、playerX, playerYは自機の中心座標を表すようにします。

    if (playerVisible) {
        if (!invincible || Math.floor(Date.now() / 50) % 2) { // 無敵状態で点滅
            ctx.drawImage(playerImage, playerX - 32, playerY - 32, 64, 64);
        }
    }

/*     // 自機の当たり判定の範囲を緑の線で描画(デバッグ用)
    ctx.strokeStyle = 'green';
    ctx.beginPath();
    ctx.arc(playerX, playerY, 8, 0, Math.PI * 2);
    ctx.stroke(); */
    
/*     // 敵との当たり判定の円を描画(デバッグ用)
    // 自機の見た目の中央に円が来るように、playerX, playerYを直接使用します。
    ctx.beginPath();
    ctx.arc(playerX, playerY, 8, 0, Math.PI * 2);
    ctx.strokeStyle = 'red';
    ctx.stroke();

    // パワーアップアイテムとの当たり範囲を示す緑の線で囲む(デバッグ用)
    ctx.strokeStyle = 'green'; // 線の色を緑に設定
    ctx.strokeRect(playerX - 32, playerY - 32, 64, 64); // 自機のサイズに合わせて線を描画 */
}

// ビームと敵の当たり判定
function updateBeams() {
    for (let i = beams.length - 1; i >= 0; i--) {
        const beam = beams[i];

        // ビームの位置更新
        beam.x += beam.velocityX || 0;
        beam.y += beam.velocityY || -beam.speed; // 貫通反射弾の場合はvelocityYを使用

        // ビームのタイプに応じた描画
        if (beam.type === "piercing") {
            // 貫通反射弾の描画
            if(powerUpLevel < 5){
                ctx.fillStyle = '#00BFFF'; // 青白い色
            } else {
                if (powerUpType === "piercing") {
                    ctx.fillStyle = '#AA00FF'; // 青紫色(piercing 最大レベル)
                } else {
                    ctx.fillStyle = '#FF00AA'; // 赤紫色(rapidFire 最大レベル)
                }
                
            }
            ctx.beginPath();
            ctx.arc(beam.x, beam.y, beam.radius, 0, Math.PI * 2);
            ctx.fill();
            // 彗星の尾のようなパーティクルを生成
            createParticles(beam.x, beam.y, '135,206,250', 0.5, 5, 1, 0.3, 0.4);
            // 壁での反射処理
            if (beam.x <= 0 || beam.x >= canvas.width) {
                beam.velocityX = -beam.velocityX;
            }
            // レベル5以上で下端で反射
            if (beam.y + beam.radius >= canvas.height && beam.velocityY > 0 && powerUpLevel >= 5) {
                beam.velocityY = -beam.velocityY;
            }
        } else {
            // 連射弾・通常のビームの描画
            if(powerUpLevel < 5){
                ctx.fillStyle = '#ff0'; // 黄色
            } else {
                if (powerUpType === "rapidFire") {
                    ctx.fillStyle = '#fa0'; // オレンジ色(rapidFire  最大レベル)
                } else {
                    ctx.fillStyle = '#0af'; // 明るい青色(piercing 最大レベル)
                }
            }
            ctx.fillRect(beam.x - beam.width / 2, beam.y, beam.width, beam.height);
        }

        // 敵との当たり判定処理
        for (let j = enemies.length - 1; j >= 0; j--) {
            const enemy = enemies[j];
            // 貫通反射弾の場合、円形の当たり判定を使用
            if (beam.type === "piercing") {
                const dx = enemy.x + enemy.size / 2 - beam.x;
                const dy = enemy.y + enemy.size / 2 - beam.y;
                const distance = Math.sqrt(dx * dx + dy * dy);
                // 当たり判定:ビームの半径と敵の半径の和と中心点間の距離を比較
                if (distance < beam.radius + enemy.size / 2) {
                    enemy.health -= 1; // 敵のHPを減少させる
                    playSound('beam_hit', 0.06);  // 敵にビームヒット音
                    if (enemy.health <= 0) {
                        if (enemy.itemType && enemy.itemType > 0) {
                            dropItem(enemy.x + enemy.size / 2, enemy.y + enemy.size / 2, enemy.itemType);
                            // ボスの場合
                            if (enemy.boss) {
                                // ボス報酬パワーアップアイテム
                                dropBossItems(enemy);
                            }
                        }
                        enemies.splice(j, 1); // 敵を配列から削除
                        createParticles(enemy.x + enemy.size / 2, enemy.y + enemy.size / 2, "0, 255, 255", 2, 3);   // 敵の爆発パーティクル
                        playSound('medium_explosion', 3);  // 敵の爆発音(中)
                    }
                    // 貫通反射弾は敵に当たっても削除しない
                }
            } else {
                // 通常弾と連射弾の矩形の当たり判定
                if (beam.x < enemy.x + enemy.size && beam.x + beam.width > enemy.x &&
                    beam.y < enemy.y + enemy.size && beam.y + beam.height > enemy.y) {
                    enemy.health -= 1; // 敵のHPを減少させる
                    playSound('beam_hit', 0.06);  // 敵にビームヒット音
                    if (enemy.health <= 0) {
                        if (enemy.itemType && enemy.itemType > 0) {
                            dropItem(enemy.x + enemy.size / 2, enemy.y + enemy.size / 2, enemy.itemType);
                            // ボスの場合
                            if (enemy.boss) {
                                // ボス報酬パワーアップアイテム
                                dropBossItems(enemy);
                            }
                        }
                        enemies.splice(j, 1);
                        createParticles(enemy.x + enemy.size / 2, enemy.y + enemy.size / 2, "0, 255, 255", 2, 3);
                        playSound('medium_explosion', 3);  // 敵の爆発音(中)
                    }
                    beams.splice(i, 1); // 通常弾と連射弾は敵に当たったら削除
                    break; // 一つの敵に対して一度だけ効果を適用
                }
            }
        }

        // 画面外に出たビームを配列から削除(貫通反射弾は上下限のみ考慮)
        if (beam.y < 0 || beam.y > canvas.height || (beam.type !== "piercing" && (beam.x < 0 || beam.x > canvas.width))) {
            beams.splice(i, 1);
        }
    }
}


// 自機と敵の円形当たり判定
function checkPlayerCollision() {
    for (let i = enemies.length - 1; i >= 0; i--) {
        const enemy = enemies[i];
        const dx = enemy.x + enemy.size / 2 - playerX; // 敵の中心から自機の中心までのX距離
        const dy = enemy.y + enemy.size / 2 - playerY; // 敵の中心から自機の中心までのY距離
        const distance = Math.sqrt(dx * dx + dy * dy); // 中心点間の距離

        // 当たり判定: 自機の当たり判定の円(半径8ピクセル)と敵の中心との距離を比較
        if (distance < 8 + enemy.size / 2 && !invincible) { // 無敵状態ではないときのみ判定
            invincible = true; // 無敵状態にする
            // 当たり判定が成立した場合、処理を実行
            playerHealth -= 1;

            playSound('small_explosion', 3);  // 被弾爆発音(小)

            // パワーアップレベルを調整する処理
            if (powerUpLevel > 0) powerUpLevel--; // パワーアップレベルを下げる
            if (powerUpLevel <= 0) powerUpType = null; // レベル0になったとき、パワーアップタイプをリセット

            // 自機がダメージを受けた際にビーム発射間隔を更新する
            updateFireBeamInterval();

            // コンソールに状態を出力
            console.log(`Player Health: ${playerHealth}, PowerUp Level: ${powerUpLevel}, PowerUp Type: ${powerUpType}`);

            if(playerHealth > 0){
                // プレイヤーが生きているときの衝突用
                createParticles(playerX, playerY, "255,165,0", 3, 3); // 自機の衝突パーティクル(オレンジ)を生成
            }

            if (playerHealth <= 0) {
                if(playerDeadTime == 0){
                    playerDeadTime = currentTime;   // プレイヤーがやられた時間を保存
                    firing = false;
                    playerVisible = false;
                    createParticles(playerX, playerY, "255,0,0", 5, 8); // 自機の爆発パーティクル(赤)を生成

                    // 効果音のリソースを解放
                    releaseSoundResources();

                    playSound('ship_explosion', 1);  // 自機爆発音
                }
            }
            break; // 重複判定を避けるためにループから抜ける
        }
    }
}


// 敵の生成
function spawnEnemies() {
    enemySpawnPatterns.forEach((pattern, index) => {
        if (currentTime >= pattern.time) {
            // 敵のタイプとバリアントから適切な敵を選択
            const typeArray = enemyTypes[pattern.type];
            if (typeArray && typeArray.length > pattern.variant - 1) {
                const enemyDef = typeArray[pattern.variant - 1];
                const newEnemy = {
                    ...enemyDef,
                    x: spawnPositions[pattern.position - 1] - enemyDef.size / 2,
                    y: -enemyDef.size,
                    alive: true,
                    health: enemyDef.health, // 耐久値を設定
                    itemType: pattern.item, // ここで itemType を設定
                    shoots: pattern.shoots, // ここで shoots プロパティを設定
                    shootInterval: pattern.shootInterval, // ここで shootInterval を設定
                    shootTimer: pattern.shootInterval, // ここで shootTimer を設定
                };
                enemies.push(newEnemy);
            }
            // 出現した敵はスケジュールから削除
            enemySpawnPatterns.splice(index, 1);
        }
    });
}


// 敵の動作の更新処理
function updateEnemies(deltaTime) {
    enemies.forEach((enemy, index) => {
        switch (enemy.movement) {
            case 'linear':  // 1 直線的に進む。
                enemy.y += 1.5; // 直線的に下に移動
                break;

            case 'zigzag':  // ジグザグに進む。
                enemy.x += Math.cos(enemy.y / 15) * (5 + Math.sin(enemy.y / 50) * 5); // 振幅変動を加えたジグザグ
                enemy.y += 1; // やや遅い下降を維持
                break;


            case 'sineWave':    // 正弦波のような動きで進む。
                enemy.x += Math.sin(enemy.y / 10) * 5; // 振幅を大きくして、波の形を強調
                enemy.y += 2; // やや速い下降を維持
                break;

            case 'dive':    // 4 一定距離進んだ後、急降下する。
                if (enemy.y < 150) {
                    enemy.y += 2; // 一定距離まで直線的に移動
                } else {
                    enemy.y += 3.5; // 急降下
                }
                break;

            case 'looping': // ループや円を描くように動く。
                if (!enemy.loopCounter) enemy.loopCounter = 0; // ループカウンタの初期化
                enemy.loopCounter += 0.1; // カウンタをインクリメント
                enemy.x += Math.cos(enemy.loopCounter) * 4; // x軸方向の動きを円形に近づける
                enemy.y += Math.sin(enemy.loopCounter) * 4 + 2; // y軸はゆるやかに下降
                break;
            

            case 'random':  // 6 ランダムな方向に小刻みに動く。
                enemy.x += (Math.random() - 0.3) * 10; // ランダムに左右に動く
                enemy.y += 2;
                break;
            case 'chase':   // 7 プレイヤーの位置を追跡する。
                // ここではplayerXを追跡する簡単な例を示します。
                // 実際にはプレイヤーの位置をより正確に追跡するためのロジックが必要です。
                enemy.x += (playerX - enemy.x) * 0.02;
                enemy.y += 1;
                break;

            case 'retreat': // 8 一定距離まで直線的に下降移動した後、逆方向に退避する。
                // 敵の動きの状態を管理するフラグが未設定の場合、初期化する
                if (enemy.retreatState === undefined) {
                    enemy.retreatState = 'advancing'; // 'advancing' または 'retreating'
                    enemy.initialY = enemy.y; // 初期位置を設定
                }

                // 進行中の動き
                if (enemy.retreatState === 'advancing') {
                    enemy.y += 1.5; // 下に移動
                    // 一定距離(ここでは550ピクセル)に達したかチェック
                    if (enemy.y >= enemy.initialY + 550) {
                        enemy.retreatState = 'retreating'; // 状態を退避中に変更
                    }
                } 
                // 退避中の動き
                else if (enemy.retreatState === 'retreating') {
                    enemy.y -= 2.5; // 上に移動
                    // 初期位置よりも上に移動したかチェック
                    if (enemy.y <= enemy.initialY) {
                        enemy.retreatState = 'advancing'; // 再び進行中の状態に戻る
                        enemy.initialY = enemy.y; // 新しい初期位置を設定
                    }
                }
                break;
            case 'circular':    // 9 円運動をする。
                // 円運動の中心点を定義(例: 一定位置まで下りてきたらそこを中心にする)
                if (!enemy.center) {
                    enemy.center = { x: enemy.x, y: enemy.y };
                    enemy.angle = 0; // 初期角度
                }
                enemy.angle += 0.05; // 角度を増加させる(動きの速さを調整)
                enemy.x = enemy.center.x + Math.cos(enemy.angle) * 50; // 半径50の円運動
                enemy.y = enemy.center.y + Math.sin(enemy.angle) * 50;
                break;
            case 'spiral':  // 10 スパイラル状に進む。
                if (!enemy.center) {
                    enemy.center = { x: enemy.x, y: enemy.y };
                    enemy.angle = 0; // 初期角度
                    enemy.radius = 0; // 初期半径
                }
                enemy.angle += 0.03; // 角度を増加させる(動きの速さを調整)
                enemy.radius += 0.3; // 半径を徐々に増加させる(螺旋の広がりを調整)
                enemy.x = enemy.center.x + Math.cos(enemy.angle) * enemy.radius;
                enemy.y = enemy.center.y + Math.sin(enemy.angle) * enemy.radius;
                break;

            case 'backAndForth':    // 11 往復する動きをする。
                // 敵の動きの状態を管理するフラグが未設定の場合、初期化する
                if (enemy.retreatState === undefined) {
                    enemy.retreatState = 'advancing'; // 'advancing' または 'retreating'
                    enemy.initialY = enemy.y; // 初期位置を設定
                }

                // 進行中の動き
                if (enemy.retreatState === 'advancing') {
                    enemy.y += 2; // 下に移動
                    // 一定距離(ここでは300ピクセル)に達したかチェック
                    if (enemy.y >= enemy.initialY + 300) {
                        enemy.retreatState = 'retreating'; // 状態を進行後に変更
                    }
                } 

                // 進行後の動き
                else if (enemy.retreatState === 'retreating') {
                    if (!enemy.direction) enemy.direction = 1;
                    if (enemy.x > canvas.width - enemy.size || enemy.x < 0) {
                        enemy.direction *= -1; // 方向転換
                    }
                    enemy.x += 1.5 * enemy.direction; // 横方向に固定量で移動
                }
                break;

            case 'horizontalSweep':     // 12 水平に大きく移動しながら進む。
                enemy.x += Math.sin(enemy.y / 50) * 5; // 横方向に大きく移動
                enemy.y += 0.5; // 垂直方向に固定量で進む
                break;

            case 'verticalSweep':   // 13 垂直に大きく移動しながら進む。
                enemy.y += Math.sin(enemy.x / 50) * 5; // 垂直方向に大きく移動
                enemy.x += 1; // 横方向に固定量で進む
                break;

            case 'bounce':  // 14 壁や画面端で跳ね返るように動く。
                // 敵の動きの状態を管理するフラグが未設定の場合、初期化する
                if (enemy.retreatState === undefined) {
                    enemy.retreatState = 'advancing'; // 'advancing' または 'retreating'
                    enemy.initialY = enemy.y; // 初期位置を設定
                }

                // 進行中の動き
                if (enemy.retreatState === 'advancing') {
                    enemy.y += 2; // 下に移動
                    // 一定距離(ここでは100ピクセル)に達したかチェック
                    if (enemy.y >= enemy.initialY + 100) {
                        enemy.retreatState = 'retreating'; // 状態を進行後に変更
                    }
                } 

                // 進行後の動き
                else if (enemy.retreatState === 'retreating') {
                    // 壁や画面端で跳ね返るように動く。
                    if (!enemy.velocityX) enemy.velocityX = 1;
                    if (!enemy.velocityY) enemy.velocityY = 1;
                    if (enemy.x > canvas.width - enemy.size || enemy.x < 0) enemy.velocityX *= -1;
                    if (enemy.y > canvas.height - enemy.size || enemy.y < 0) enemy.velocityY *= -1;
                    enemy.x += enemy.velocityX;
                    enemy.y += enemy.velocityY;
                }
                break;

            case 'teleport':    // 15 短い間隔でランダムな位置にテレポートする。
                if (!enemy.teleportTimer) enemy.teleportTimer = 0;
                enemy.teleportTimer++;
                if (enemy.teleportTimer > 180) { // 180フレームごとにテレポート
                    enemy.x = Math.random() * (canvas.width - enemy.size);
                    enemy.y = Math.random() * (canvas.height - enemy.size);
                    enemy.teleportTimer = 0;
                }
                break;

            case 'hover':   // 16 一定の位置でホバリングし、突然速く動き出す。
                if (!enemy.hoverTime) enemy.hoverTime = 0;
                enemy.hoverTime++;
                if (enemy.hoverTime < 120) {
                    // 最初の120フレームはほとんど動かない
                    enemy.x += Math.random() * 2 - 1;
                    enemy.y += Math.random() * 2 - 1;
                } else {
                    // それ以降は速く動き出す
                    enemy.x += Math.random() * 20 - 10;
                    enemy.y += Math.random() * 20 - 10;
                }
                break;

            case 'bossPattern1':
                if (enemy.behavior) {
                    enemy.behavior(enemy);
                }
                break;
            case 'bossPattern2':
            case 'finalBossPattern':
                // ボス専用の動きや行動をここに実装
                if (enemy.behavior) {
                    enemy.behavior();
                }
                break;
            default:
                // 未定義の動きがあった場合の処理
                console.warn("Undefined movement pattern:", enemy.movement);
                break;
        }

        // ボスの挙動の更新
        if (enemy.movement.startsWith('bossPattern')) {
            if (enemy.behavior) {
                enemy.behavior(enemy);
            }
        }

        // 敵が弾を撃つ設定の場合
        if (enemy.shoots) {
            // 敵にshootTimerが未設定の場合、初期化する
            if (enemy.shootTimer === undefined) {
                enemy.shootTimer = enemy.shootInterval;
            }

            // タイマーを更新
            enemy.shootTimer -= deltaTime;

            // タイマーが0以下になったら弾を発射し、タイマーをリセット
            if (enemy.shootTimer <= 0) {
                enemyShoots(enemy);
                enemy.shootTimer = enemy.shootInterval;
            }
        }

        // 画面外に出た敵を削除
        if (enemy.y > canvas.height + enemy.size) {
            enemies.splice(index, 1);
        }
    });
}

// 敵の描画
function drawEnemies() {
    enemies.forEach(enemy => {
        const img = loadedImages[enemy.imageSrc]; // プリロードされた画像を取得
        if (img) {
            ctx.drawImage(img, enemy.x, enemy.y, enemy.size, enemy.size);
        }
    });
}

// イベントリスナーで座標の変換
function getCanvasRelativePosition(event) {
    const rect = canvas.getBoundingClientRect();
    return {
        x: event.clientX - rect.left,
        y: event.clientY - rect.top,
    };
}

// パワーアップアイテムの出現
function dropItem(x, y, itemType) {
//    console.log(`dropItem called with itemType: ${itemType}`); // itemType の値を確認
    if (itemType !== 0 && itemTypes[itemType]) { // アイテムをドロップする条件をチェック
        const itemSize = 32; // アイテムのサイズ
        powerUpItems.push({
            x: x - itemSize / 2, // アイテムの中心軸を調整
            y: y - itemSize / 2, // アイテムの中心軸を調整
            type: itemType,
            image: loadedImages[itemTypes[itemType]], // 事前にロードした画像
            speed: 1.5, // アイテムの下降速度
        });
    }
}

// パワーアップアイテムの描画と更新
function updatePowerUpItems() {
    console.log(`Updating ${powerUpItems.length} power-up items`); // アイテム更新時のログ
    for (let i = powerUpItems.length - 1; i >= 0; i--) {
        const item = powerUpItems[i];
        item.y += item.speed; // アイテムを下に移動

        // アイテムを描画(サイズを32x32に設定)
        ctx.drawImage(item.image, item.x, item.y, 32, 32);

        // 当たり範囲を示す緑の線で囲む(デバッグ用)
/*         ctx.strokeStyle = 'green'; // 線の色を緑に設定
        ctx.strokeRect(item.x, item.y, 32, 32); // アイテムのサイズに合わせて線を描画 */
        
        // 画面外に出たら配列から削除
        if (item.y > canvas.height) {
            powerUpItems.splice(i, 1);
        }
    }
}

// パワーアップアイテムの取得
function checkItemCollection() {
    for (let i = powerUpItems.length - 1; i >= 0; i--) {
        const item = powerUpItems[i];
        // アイテムの中心座標を計算
        const itemCenterX = item.x + 16; // アイテムの幅の半分
        const itemCenterY = item.y + 16; // アイテムの高さの半分
        
        // 自機の中心座標を計算(自機の画像が64x64pxと仮定)
        // 修正: 自機の中心座標の計算方法を修正。playerX, playerYはすでに自機の中心座標を指している
        const playerCenterX = playerX; // playerXは自機の中心座標
        const playerCenterY = playerY; // playerYは自機の中心座標

        // 中心点間の距離を計算
        const dx = itemCenterX - playerCenterX;
        const dy = itemCenterY - playerCenterY;
        const distance = Math.sqrt(dx * dx + dy * dy);

        // 当たり判定の範囲を決定(自機とアイテムのサイズに基づいて調整)
        if (distance < (32 + 16)) { // 自機の半径とアイテムの半径の和
            // アイテムを取得した場合の処理
            switch (item.type) {
                case 1: // 連射弾強化のアイテム
                    powerUpType = 'rapidFire';
                    if (powerUpLevel < 6) powerUpLevel++;
                    break;
                case 2: // 貫通反射弾強化のアイテム
                    powerUpType = 'piercing';
                    if (powerUpLevel < 6) powerUpLevel++;
                    break;
                // 他のアイテムタイプに応じた処理
            }

            if (playerHealth < 5) {
                playerHealth += 1; // 自機回復
            }

            // パワーアップアイテムの取得時にビーム発射間隔を更新する
            updateFireBeamInterval();

            // コンソールに状態を出力
            console.log(`Player Health: ${playerHealth}, PowerUp Level: ${powerUpLevel}, PowerUp Type: ${powerUpType}`);
            
            playSound('power_up', 0.5);  // パワーアップ音

            // アイテムを削除
            powerUpItems.splice(i, 1);
        }
    }
}

// ゲームの初期化
function initializeGame() {
    currentTime = 0;
    playerHealth = 5;
    gameOver = false;
    firing = true;
    playerVisible = true;
    playerX = canvas.width / 2;
    playerY = canvas.height - 50;
    powerUpLevel = 0;
    powerUpType = null;
    invincible = false; // 無敵状態を解除
    invincibleTime = 0; // 無敵時間をリセット
    currentTime = 0;
    playerDeadTime = 0;

    // 配列やリソースをクリアする
    beams.length = 0;
    enemies.length = 0;
    particles.length = 0;
    powerUpItems.length = 0;
    bullets.length = 0; // 敵の弾をクリア

    // ビーム発射のスケジュールをリセット
    clearInterval(beamIntervalId);
    beamIntervalId = setInterval(fireBeam, 200);

    // 敵の出現パターンをコピーしてリセット
    enemySpawnPatterns = originalEnemySpawnPatterns.map(pattern => ({ ...pattern }));

    // ゲーム開始時にBGMを再生
    setBgmVolume(0.03);  // BGMボリューム設定(0-1)
    startBgm(); // BGM再生
}

// ゲームオーバー時のテキスト表示
function showGameOver() {
    ctx.fillStyle = 'red';
    ctx.font = '48px Arial';
    ctx.textAlign = 'center';
    ctx.fillText('GAME OVER', canvas.width / 2, canvas.height / 2);
}

// ゲームオーバー時のイベントリスナー追加
function setupGameOverListener() {
    canvas.addEventListener('click', function restartGame() {
        if (gameOver) {
            initializeGame();
            // ゲームループを再開する
            requestAnimationFrame(gameLoop);
            // このリスナーが再度実行されることを防ぐためにリスナーを削除
            canvas.removeEventListener('click', restartGame);
        }
    });
}

// 自機の移動処理を行う部分(例: マウス移動イベント内)での調整
canvas.addEventListener('mousemove', event => {
    const pos = getCanvasRelativePosition(event);
    // 自機の中心がキャンバスの端を超えないように調整
    playerX = Math.min(Math.max(pos.x, 32), canvas.width - 32);
    playerY = Math.min(Math.max(pos.y - 50, 32), canvas.height - 32);
});


canvas.addEventListener('touchmove', event => {
    const touch = event.touches[0];
    const pos = getCanvasRelativePosition(touch);
    // タッチ操作に対しても同様の制約を適用
    playerX = Math.min(Math.max(pos.x, 32), canvas.width - 32);
    playerY = Math.min(Math.max(pos.y - 50, 32), canvas.height - 32);
    event.preventDefault();
}, { passive: false });

// 画像プリロード関数
function preloadImages(callback) {
    const imageSources = ["images/player.png"]; // 自機の画像もプリロード対象に含める
    let imagesLoaded = 0;

    // enemyTypesから画像パスを抽出してimageSourcesに追加
    Object.values(enemyTypes).forEach(group => {
        group.forEach(enemy => {
            if (!imageSources.includes(enemy.imageSrc)) {
                imageSources.push(enemy.imageSrc);
            }
        });
    });

    // パワーアップアイテムの画像パスもプリロード処理に追加
    Object.values(itemTypes).forEach(src => {
        if (!imageSources.includes(src)) {
            imageSources.push(src);
        }
    });
    
    // 各画像のプリロード
    imageSources.forEach(src => {
        const img = new Image();
        img.onload = () => {
            imagesLoaded++;
            loadedImages[src] = img; // ロード完了した画像をオブジェクトに格納
            if (imagesLoaded === imageSources.length) {
                callback(); // すべての画像のロードが完了したらコールバックを実行
            }
        };
        img.src = src;
    });
}

// 星のデータを生成する
function createStars() {
    for (let i = 0; i < starCount; i++) {
        stars.push({
            x: Math.random() * canvas.width, // 星の初期X座標
            y: Math.random() * canvas.height, // 星の初期Y座標
            size: Math.random() * 3, // 星の大きさ(1pxから3px)
            speed: Math.random() * 5 + 3, // 星の速度(3から5の範囲でランダム)
            opacity: Math.random() * 0.1 + 0.5, // 星の透明度(0.1から0.6の範囲でランダム)
        });
    }
}

// 星を描画し、位置を更新する
function updateStars() {
    ctx.fillStyle = 'black';
    ctx.fillRect(0, 0, canvas.width, canvas.height); // 背景を黒で塗りつぶす

    stars.forEach(star => {
        ctx.beginPath();
        ctx.arc(star.x, star.y, star.size, 0, Math.PI * 2);
        ctx.closePath();

        // 星の透明度と大きさで遠近感を表現
        ctx.fillStyle = `rgba(128, 128, 128, ${star.opacity})`;
        ctx.fill();

        // 星を下に移動させる
        star.y += star.speed;

        // 星が画面下端を超えたら上に戻す
        if (star.y > canvas.height) {
            star.y = 0;
            star.x = Math.random() * canvas.width;
        }
    });
}

// ヘルスアイコンを描画する関数
function drawHealthIcons(health, context, canvas) {
    const iconSize = 12; // アイコンのサイズ
    const padding = 4; // アイコン間のパディング
    const startX = canvas.width - iconSize - padding; // 描画開始位置(X座標)
    const startY = canvas.height - (iconSize + padding) * health; // 描画開始位置(Y座標)

    for (let i = 0; i < health; i++) {
        // アイコンを縦に並べて描画
        context.drawImage(healthIcon, startX, startY + (iconSize + padding) * i, iconSize, iconSize);
    }
}

// パーティクルの作成
function createParticles(x, y, color, velocityRange, size, count = 30, lifeMin = 0.5, lifeMax = 1.5) {
    for (let i = 0; i < count; i++) {
        const life = Math.random() * (lifeMax - lifeMin) + lifeMin; // lifeMin から lifeMax の範囲でランダムに設定
        particles.push({
            x: x,
            y: y,
            velocityX: (Math.random() - 0.5) * velocityRange,
            velocityY: (Math.random() - 0.5) * velocityRange,
            color: color,
            size: size,
            life: life,
        });
    }
}

// パーティクルの更新
function updateParticles() {
    for (let i = particles.length - 1; i >= 0; i--) {
        const p = particles[i];
        p.x += p.velocityX;
        p.y += p.velocityY;
        p.life -= 0.02; // パーティクルの生存期間を減少させる

        if (p.life <= 0) {
            particles.splice(i, 1); // パーティクルの寿命が尽きたら削除
        }
    }
}

// パーティクルの描画
function drawParticles() {
    particles.forEach(p => {
        ctx.fillStyle = `rgba(${p.color},${p.life})`;
        ctx.beginPath();
        ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
        ctx.fill();
    });
}

// タイトル画面を表示する関数
function showTitleScreen() {
    ctx.drawImage(titleBackgroundImage, 0, 0, canvas.width, canvas.height); // 背景画像を表示
    ctx.fillStyle = `rgba(255, 255, 255, ${titleOpacity})`;
    ctx.font = '48px Arial';
    ctx.textAlign = 'center';
    ctx.fillText('START', canvas.width / 2, canvas.height * 0.8);

    // 点滅効果
    titleOpacity += titleFadeDirection;
    if (titleOpacity <= 0.2 || titleOpacity >= 1) {
        titleFadeDirection *= -1;
    }
}

// タイトル画面でのタップイベントを処理
canvas.addEventListener('click', () => {
    if (showTitle) {
        showTitle = false;
        // ブロックワイプアニメーションのためのブロック配列をリセット
        for (let y = 0; y < blocksHigh; y++) {
            for (let x = 0; x < blocksWide; x++) {
                blocks.push({ x: x * blockSize, y: y * blockSize, visible: false });
            }
        }
    }
});

// ブロックワイプアニメーションを実行する関数
function blockWipeTransition() {
    // 一度に表示するブロックの数を増やす
    let blocksToShow = Math.min(10, blocks.filter(block => !block.visible).length);
    for (let i = 0; i < blocksToShow; i++) {
        let hiddenBlocks = blocks.filter(block => !block.visible);
        let blockToShow = hiddenBlocks[Math.floor(Math.random() * hiddenBlocks.length)];
        blockToShow.visible = true;
    }

    // すべてのブロックが表示されたらブロックをリセット
    if (blocks.every(block => block.visible)) {
        initializeGame();   // ゲーム初期化
        blocks = [];
    }
}

// ゲームの更新処理を行う関数
function updateGame() {
    const deltaTime = 1 / 60;
    currentTime += deltaTime;

    if (invincible) {
        invincibleTime += deltaTime;
        if (invincibleTime >= 2) {
            invincible = false;
            invincibleTime = 0;
        }
    }

    if (playerDeadTime !== 0 && playerDeadTime + gameOverWaitTime < currentTime) {
        gameOver = true;
    }

    if (!gameOver) {
        updateStars();
        if (!gameOver) {
            updatePlayer();
            updateBeams();
        }

        updateAndDrawBullets(ctx);
        spawnEnemies();
        drawEnemies();
        updateEnemies(deltaTime);
        checkPlayerCollision();
        updateParticles();
        drawParticles();
        updatePowerUpItems();
        checkItemCollection();
        drawHealthIcons(playerHealth, ctx, canvas);
        drawBossHealthBar();    // ボスの体力ゲージを描画
    }
}

// ゲームループを更新する関数
function gameLoop() {
    if (showTitle) {
        showTitleScreen();
    } else {
        if (blocks.length > 0) {
            blockWipeTransition();
            blocks.forEach(block => {
                if (block.visible) {
                    ctx.fillStyle = 'black';
                    ctx.fillRect(block.x, block.y, blockSize, blockSize);
                }
            });
        } else {
            updateGame();
        }
    }

    if (gameOver) {
        showGameOver();
        setupGameOverListener();
        return;
    }

    requestAnimationFrame(gameLoop);
}

// ゲームループを開始する関数
function startGameLoop() {
    gameLoop();
}

// ゲームの初期化と開始
preloadImages(() => {
    preloadAudioFiles();
    startGameLoop();
});

// 一定間隔でビームを発射
beamIntervalId = setInterval(fireBeam, 200);

// 星の初期化
createStars();

/////////////////////////////////////////////////////////////////////

// ボスの体力ゲージの描画
function drawBossHealthBar() {
    const boss = enemies.find(enemy => enemy.movement.startsWith('bossPattern'));
    if (boss) {
        const barWidth = 200;
        const barHeight = 6;
        const x = (canvas.width - barWidth) / 2;
        const y = 20;

        // 背景(赤いバー)
        ctx.fillStyle = 'red';
        ctx.fillRect(x, y, barWidth, barHeight);

        // 体力(黄色いバー)
        ctx.fillStyle = 'yellow';
        const healthWidth = (boss.health / boss.maxHealth) * barWidth;
        ctx.fillRect(x, y, healthWidth, barHeight);
    }
}

// ボス撃破時のアイテムドロップ処理
function dropBossItems(boss) {
    for (let i = 0; i < 5; i++) {
        const itemType = Math.floor(Math.random() * 2) + 1;
        const angle = Math.random() * Math.PI * 2;
        const distance = Math.random() * 128;
        const x = boss.x + boss.size / 2 + Math.cos(angle) * distance;
        const y = boss.y + boss.size / 2 + Math.sin(angle) * distance;
        dropItem(x, y, itemType);
        updatePowerUpItems();
    }
}

enemySpawnPatterns.js(敵の出現定義の定数宣言)

この定数宣言を書き換えることで、出てくる敵の種類やタイミングを変更することができます。

// 敵の出現パターンの定義
export const originalEnemySpawnPatterns = [
    { time: 1, type: 'small', variant: 1, position: 1, item: 0, shoots: 0, shootInterval: 3 },
    { time: 2, type: 'small', variant: 1, position: 2, item: 0, shoots: 0, shootInterval: 3 },
    { time: 3, type: 'small', variant: 1, position: 3, item: 0, shoots: 0, shootInterval: 3 },
    { time: 4, type: 'small', variant: 1, position: 4, item: 0, shoots: 0, shootInterval: 3 },
    { time: 5, type: 'small', variant: 1, position: 5, item: 0, shoots: 0, shootInterval: 3 },
    { time: 7, type: 'small', variant: 4, position: 2, item: 1, shoots: 0, shootInterval: 3 },
    { time: 7, type: 'small', variant: 4, position: 4, item: 2, shoots: 0, shootInterval: 3 },
    { time: 8, type: 'small', variant: 1, position: 5, item: 0, shoots: 0, shootInterval: 3 },
    { time: 9, type: 'small', variant: 1, position: 4, item: 0, shoots: 0, shootInterval: 3 },
    { time: 10, type: 'small', variant: 1, position: 3, item: 0, shoots: 0, shootInterval: 3 },
    { time: 11, type: 'small', variant: 1, position: 2, item: 0, shoots: 0, shootInterval: 3 },
    { time: 12, type: 'small', variant: 1, position: 1, item: 0, shoots: 0, shootInterval: 3 },
    { time: 14, type: 'small', variant: 4, position: 2, item: 2, shoots: 0, shootInterval: 3 },
    { time: 14, type: 'small', variant: 4, position: 4, item: 1, shoots: 0, shootInterval: 3 },
    { time: 15, type: 'small', variant: 1, position: 1, item: 0, shoots: 0, shootInterval: 3 },
    { time: 15, type: 'small', variant: 1, position: 5, item: 0, shoots: 0, shootInterval: 3 },
    { time: 16, type: 'small', variant: 2, position: 2, item: 0, shoots: 0, shootInterval: 3 },
    { time: 16, type: 'small', variant: 2, position: 4, item: 0, shoots: 0, shootInterval: 3 },
    { time: 17, type: 'small', variant: 1, position: 3, item: 0, shoots: 0, shootInterval: 3 },
    { time: 18, type: 'small', variant: 1, position: 4, item: 0, shoots: 0, shootInterval: 3 },
    { time: 18, type: 'small', variant: 1, position: 2, item: 0, shoots: 0, shootInterval: 3 },
    { time: 19, type: 'small', variant: 1, position: 5, item: 0, shoots: 0, shootInterval: 3 },
    { time: 19, type: 'small', variant: 2, position: 1, item: 0, shoots: 0, shootInterval: 3 },
    { time: 22, type: 'small', variant: 1, position: 2, item: 0, shoots: 0, shootInterval: 3 },
    { time: 22, type: 'small', variant: 2, position: 3, item: 0, shoots: 0, shootInterval: 3 },
    { time: 22, type: 'small', variant: 1, position: 4, item: 0, shoots: 0, shootInterval: 3 },
    { time: 22, type: 'small', variant: 2, position: 5, item: 0, shoots: 0, shootInterval: 3 },
    { time: 23, type: 'small', variant: 1, position: 5, item: 0, shoots: 0, shootInterval: 3 },
    { time: 24, type: 'small', variant: 1, position: 1, item: 0, shoots: 0, shootInterval: 3 },
    { time: 25, type: 'small', variant: 1, position: 5, item: 0, shoots: 0, shootInterval: 3 },
    { time: 26, type: 'small', variant: 1, position: 3, item: 0, shoots: 0, shootInterval: 3 },
    { time: 27, type: 'small', variant: 3, position: 2, item: 0, shoots: 0, shootInterval: 3 },
    { time: 27, type: 'small', variant: 3, position: 4, item: 0, shoots: 0, shootInterval: 3 },
    { time: 28, type: 'small', variant: 1, position: 3, item: 0, shoots: 0, shootInterval: 3 },
    { time: 30, type: 'small', variant: 6, position: 1, item: 1, shoots: 1, shootInterval: 3 },
    { time: 30, type: 'small', variant: 6, position: 5, item: 2, shoots: 1, shootInterval: 3 },
    { time: 31, type: 'small', variant: 1, position: 1, item: 0, shoots: 0, shootInterval: 3 },
    { time: 32, type: 'small', variant: 1, position: 5, item: 0, shoots: 0, shootInterval: 3 },
    { time: 33, type: 'small', variant: 1, position: 3, item: 0, shoots: 0, shootInterval: 3 },
    { time: 34, type: 'small', variant: 2, position: 1, item: 0, shoots: 0, shootInterval: 3 },
    { time: 34, type: 'small', variant: 1, position: 2, item: 0, shoots: 0, shootInterval: 3 },
    { time: 34, type: 'small', variant: 2, position: 3, item: 0, shoots: 0, shootInterval: 3 },
    { time: 34, type: 'small', variant: 1, position: 4, item: 0, shoots: 0, shootInterval: 3 },
    { time: 34, type: 'small', variant: 2, position: 5, item: 0, shoots: 0, shootInterval: 3 },
    { time: 35, type: 'small', variant: 1, position: 3, item: 0, shoots: 0, shootInterval: 3 },
    { time: 36, type: 'small', variant: 1, position: 2, item: 0, shoots: 0, shootInterval: 3 },
    { time: 37, type: 'small', variant: 1, position: 4, item: 0, shoots: 0, shootInterval: 3 },
    { time: 38, type: 'small', variant: 1, position: 3, item: 0, shoots: 0, shootInterval: 3 },
    { time: 42, type: 'small', variant: 4, position: 5, item: 1, shoots: 0, shootInterval: 3 },
    { time: 42, type: 'small', variant: 4, position: 1, item: 2, shoots: 0, shootInterval: 3 },
    { time: 44, type: 'small', variant: 1, position: 4, item: 0, shoots: 0, shootInterval: 3 },
    { time: 45, type: 'small', variant: 1, position: 2, item: 0, shoots: 0, shootInterval: 3 },
    { time: 46, type: 'small', variant: 1, position: 3, item: 0, shoots: 0, shootInterval: 3 },
    { time: 49, type: 'small', variant: 5, position: 1, item: 2, shoots: 1, shootInterval: 3 },
    { time: 51, type: 'small', variant: 3, position: 3, item: 0, shoots: 0, shootInterval: 3 },
    { time: 54, type: 'small', variant: 5, position: 5, item: 2, shoots: 1, shootInterval: 3 },
    { time: 59, type: 'medium', variant: 1, position: 3, item: 1, shoots: 1, shootInterval: 3 },
    { time: 59, type: 'small', variant: 1, position: 1, item: 0, shoots: 0, shootInterval: 3 },
    { time: 60, type: 'small', variant: 1, position: 2, item: 0, shoots: 0, shootInterval: 3 },
    { time: 61, type: 'small', variant: 1, position: 3, item: 0, shoots: 0, shootInterval: 3 },
    { time: 62, type: 'small', variant: 1, position: 4, item: 0, shoots: 0, shootInterval: 3 },
    { time: 63, type: 'small', variant: 1, position: 5, item: 0, shoots: 0, shootInterval: 3 },
    { time: 64, type: 'small', variant: 5, position: 2, item: 1, shoots: 1, shootInterval: 3 },
    { time: 64, type: 'small', variant: 4, position: 1, item: 1, shoots: 0, shootInterval: 3 },
    { time: 64, type: 'small', variant: 4, position: 5, item: 2, shoots: 0, shootInterval: 3 },
    { time: 64, type: 'small', variant: 1, position: 5, item: 0, shoots: 0, shootInterval: 3 },
    { time: 65, type: 'small', variant: 1, position: 4, item: 0, shoots: 0, shootInterval: 3 },
    { time: 66, type: 'small', variant: 1, position: 3, item: 0, shoots: 0, shootInterval: 3 },
    { time: 67, type: 'small', variant: 1, position: 2, item: 0, shoots: 0, shootInterval: 3 },
    { time: 68, type: 'small', variant: 1, position: 1, item: 0, shoots: 0, shootInterval: 3 },
    { time: 69, type: 'small', variant: 5, position: 4, item: 2, shoots: 1, shootInterval: 3 },
    { time: 69, type: 'small', variant: 4, position: 1, item: 2, shoots: 0, shootInterval: 3 },
    { time: 69, type: 'small', variant: 4, position: 5, item: 1, shoots: 0, shootInterval: 3 },
    { time: 69, type: 'small', variant: 1, position: 1, item: 0, shoots: 0, shootInterval: 3 },
    { time: 69, type: 'small', variant: 1, position: 5, item: 0, shoots: 0, shootInterval: 3 },
    { time: 70, type: 'small', variant: 1, position: 2, item: 0, shoots: 0, shootInterval: 3 },
    { time: 70, type: 'small', variant: 1, position: 4, item: 0, shoots: 0, shootInterval: 3 },
    { time: 71, type: 'small', variant: 1, position: 3, item: 0, shoots: 0, shootInterval: 3 },
    { time: 82, type: 'large', variant: 1, position: 3, item: 2, shoots: 1, shootInterval: 1 },
    { time: 83, type: 'small', variant: 1, position: 5, item: 0, shoots: 0, shootInterval: 3 },
    { time: 83, type: 'small', variant: 1, position: 1, item: 0, shoots: 0, shootInterval: 3 },
    { time: 83, type: 'small', variant: 1, position: 5, item: 0, shoots: 0, shootInterval: 3 },
    { time: 83, type: 'small', variant: 1, position: 3, item: 0, shoots: 0, shootInterval: 3 },
    { time: 90, type: 'small', variant: 3, position: 2, item: 0, shoots: 0, shootInterval: 3 },
    { time: 90, type: 'small', variant: 3, position: 4, item: 0, shoots: 0, shootInterval: 3 },
    { time: 93, type: 'small', variant: 1, position: 3, item: 0, shoots: 0, shootInterval: 3 },
    { time: 95, type: 'small', variant: 6, position: 1, item: 1, shoots: 1, shootInterval: 3 },
    { time: 98, type: 'small', variant: 6, position: 5, item: 2, shoots: 1, shootInterval: 3 },
    { time: 99, type: 'small', variant: 1, position: 1, item: 0, shoots: 0, shootInterval: 3 },
    { time: 100, type: 'small', variant: 1, position: 5, item: 0, shoots: 0, shootInterval: 3 },
    { time: 101, type: 'small', variant: 1, position: 3, item: 0, shoots: 0, shootInterval: 3 },
    { time: 102, type: 'small', variant: 1, position: 4, item: 0, shoots: 0, shootInterval: 3 },
    { time: 103, type: 'small', variant: 1, position: 2, item: 0, shoots: 0, shootInterval: 3 },
    { time: 104, type: 'small', variant: 1, position: 5, item: 0, shoots: 0, shootInterval: 3 },
    { time: 105, type: 'small', variant: 1, position: 1, item: 0, shoots: 0, shootInterval: 3 },
    { time: 105, type: 'small', variant: 6, position: 3, item: 1, shoots: 1, shootInterval: 3 },
    { time: 120, type: 'small', variant: 5, position: 2, item: 2, shoots: 1, shootInterval: 3 },
    { time: 125, type: 'small', variant: 4, position: 1, item: 1, shoots: 0, shootInterval: 3 },
    { time: 130, type: 'small', variant: 4, position: 5, item: 2, shoots: 0, shootInterval: 3 },
    { time: 135, type: 'small', variant: 1, position: 1, item: 0, shoots: 0, shootInterval: 3 },
    { time: 135, type: 'small', variant: 1, position: 2, item: 0, shoots: 0, shootInterval: 3 },
    { time: 135, type: 'small', variant: 1, position: 3, item: 0, shoots: 0, shootInterval: 3 },
    { time: 135, type: 'small', variant: 1, position: 4, item: 0, shoots: 0, shootInterval: 3 },
    { time: 135, type: 'small', variant: 1, position: 5, item: 0, shoots: 0, shootInterval: 3 },
    { time: 138, type: 'small', variant: 7, position: 1, item: 0, shoots: 0, shootInterval: 3 },
    { time: 138, type: 'small', variant: 7, position: 3, item: 0, shoots: 0, shootInterval: 3 },
    { time: 138, type: 'small', variant: 7, position: 5, item: 0, shoots: 0, shootInterval: 3 },
    { time: 139, type: 'small', variant: 4, position: 5, item: 1, shoots: 0, shootInterval: 3 },
    { time: 140, type: 'small', variant: 7, position: 2, item: 0, shoots: 0, shootInterval: 3 },
    { time: 140, type: 'small', variant: 7, position: 4, item: 0, shoots: 0, shootInterval: 3 },
    { time: 141, type: 'small', variant: 4, position: 1, item: 2, shoots: 0, shootInterval: 3 },
    { time: 142, type: 'small', variant: 7, position: 1, item: 0, shoots: 0, shootInterval: 3 },
    { time: 142, type: 'small', variant: 7, position: 3, item: 0, shoots: 0, shootInterval: 3 },
    { time: 142, type: 'small', variant: 7, position: 5, item: 0, shoots: 0, shootInterval: 3 },
    { time: 143, type: 'small', variant: 2, position: 4, item: 0, shoots: 1, shootInterval: 3 },
    { time: 144, type: 'small', variant: 7, position: 2, item: 0, shoots: 0, shootInterval: 3 },
    { time: 144, type: 'small', variant: 7, position: 4, item: 0, shoots: 0, shootInterval: 3 },
    { time: 145, type: 'small', variant: 2, position: 2, item: 0, shoots: 1, shootInterval: 3 },
    { time: 146, type: 'small', variant: 7, position: 1, item: 0, shoots: 0, shootInterval: 3 },
    { time: 146, type: 'small', variant: 7, position: 3, item: 0, shoots: 0, shootInterval: 3 },
    { time: 146, type: 'small', variant: 7, position: 5, item: 0, shoots: 0, shootInterval: 3 },
    { time: 146, type: 'small', variant: 8, position: 1, item: 0, shoots: 1, shootInterval: 10 },
    { time: 147, type: 'small', variant: 4, position: 3, item: 1, shoots: 0, shootInterval: 3 },
    { time: 148, type: 'small', variant: 7, position: 1, item: 0, shoots: 0, shootInterval: 3 },
    { time: 148, type: 'small', variant: 7, position: 3, item: 0, shoots: 0, shootInterval: 3 },
    { time: 148, type: 'small', variant: 7, position: 5, item: 0, shoots: 0, shootInterval: 3 },
    { time: 148, type: 'small', variant: 8, position: 3, item: 0, shoots: 1, shootInterval: 10 },
    { time: 149, type: 'small', variant: 4, position: 1, item: 2, shoots: 0, shootInterval: 3 },
    { time: 150, type: 'small', variant: 7, position: 1, item: 0, shoots: 0, shootInterval: 3 },
    { time: 150, type: 'small', variant: 7, position: 3, item: 0, shoots: 0, shootInterval: 3 },
    { time: 150, type: 'small', variant: 7, position: 5, item: 0, shoots: 0, shootInterval: 3 },
    { time: 150, type: 'small', variant: 8, position: 5, item: 0, shoots: 1, shootInterval: 10 },
    { time: 151, type: 'small', variant: 3, position: 3, item: 0, shoots: 0, shootInterval: 3 },
    { time: 152, type: 'small', variant: 7, position: 3, item: 0, shoots: 0, shootInterval: 3 },
    { time: 153, type: 'small', variant: 3, position: 5, item: 0, shoots: 0, shootInterval: 3 },
    { time: 155, type: 'small', variant: 6, position: 1, item: 2, shoots: 1, shootInterval: 3 },
    { time: 155, type: 'small', variant: 2, position: 2, item: 0, shoots: 0, shootInterval: 3 },
    { time: 155, type: 'small', variant: 6, position: 3, item: 1, shoots: 1, shootInterval: 3 },
    { time: 155, type: 'small', variant: 2, position: 4, item: 0, shoots: 0, shootInterval: 3 },
    { time: 155, type: 'small', variant: 6, position: 5, item: 2, shoots: 1, shootInterval: 3 },
    { time: 160, type: 'small', variant: 6, position: 1, item: 1, shoots: 1, shootInterval: 3 },
    { time: 160, type: 'small', variant: 2, position: 2, item: 0, shoots: 0, shootInterval: 3 },
    { time: 160, type: 'small', variant: 6, position: 3, item: 2, shoots: 1, shootInterval: 3 },
    { time: 160, type: 'small', variant: 2, position: 4, item: 0, shoots: 0, shootInterval: 3 },
    { time: 160, type: 'small', variant: 6, position: 5, item: 1, shoots: 1, shootInterval: 3 },
    { time: 160, type: 'medium', variant: 1, position: 3, item: 2, shoots: 1, shootInterval: 3 },
    { time: 163, type: 'medium', variant: 2, position: 3, item: 2, shoots: 2, shootInterval: 3 },
    { time: 165, type: 'small', variant: 3, position: 1, item: 0, shoots: 0, shootInterval: 3 },
    { time: 171, type: 'small', variant: 2, position: 2, item: 0, shoots: 0, shootInterval: 3 },
    { time: 172, type: 'small', variant: 4, position: 3, item: 1, shoots: 0, shootInterval: 3 },
    { time: 173, type: 'small', variant: 2, position: 4, item: 0, shoots: 0, shootInterval: 3 },
    { time: 174, type: 'small', variant: 3, position: 5, item: 0, shoots: 0, shootInterval: 3 },
    { time: 175, type: 'small', variant: 1, position: 1, item: 0, shoots: 0, shootInterval: 3 },
    { time: 176, type: 'small', variant: 1, position: 2, item: 0, shoots: 0, shootInterval: 3 },
    { time: 177, type: 'small', variant: 1, position: 3, item: 0, shoots: 0, shootInterval: 3 },
    { time: 178, type: 'small', variant: 1, position: 4, item: 0, shoots: 0, shootInterval: 3 },
    { time: 179, type: 'small', variant: 1, position: 5, item: 0, shoots: 0, shootInterval: 3 },
    { time: 180, type: 'small', variant: 5, position: 2, item: 2, shoots: 1, shootInterval: 3 },
    { time: 181, type: 'small', variant: 1, position: 5, item: 0, shoots: 0, shootInterval: 3 },
    { time: 182, type: 'small', variant: 1, position: 4, item: 0, shoots: 0, shootInterval: 3 },
    { time: 183, type: 'small', variant: 1, position: 3, item: 0, shoots: 0, shootInterval: 3 },
    { time: 184, type: 'small', variant: 1, position: 2, item: 0, shoots: 0, shootInterval: 3 },
    { time: 185, type: 'small', variant: 1, position: 1, item: 0, shoots: 0, shootInterval: 3 },
    { time: 186, type: 'small', variant: 5, position: 4, item: 1, shoots: 1, shootInterval: 3 },
    { time: 188, type: 'medium', variant: 1, position: 1, item: 2, shoots: 1, shootInterval: 3 },
    { time: 188, type: 'medium', variant: 1, position: 5, item: 1, shoots: 1, shootInterval: 3 },
    { time: 188, type: 'medium', variant: 2, position: 3, item: 2, shoots: 2, shootInterval: 3 },
    { time: 188, type: 'small', variant: 8, position: 1, item: 0, shoots: 1, shootInterval: 10 },
    { time: 189, type: 'small', variant: 8, position: 5, item: 0, shoots: 1, shootInterval: 10 },
    { time: 190, type: 'small', variant: 4, position: 2, item: 1, shoots: 0, shootInterval: 3 },
    { time: 191, type: 'small', variant: 4, position: 4, item: 2, shoots: 0, shootInterval: 3 },
    { time: 192, type: 'small', variant: 8, position: 3, item: 0, shoots: 1, shootInterval: 10 },
    { time: 193, type: 'small', variant: 7, position: 1, item: 0, shoots: 0, shootInterval: 3 },
    { time: 194, type: 'small', variant: 7, position: 5, item: 0, shoots: 0, shootInterval: 3 },
    { time: 195, type: 'small', variant: 2, position: 2, item: 0, shoots: 0, shootInterval: 3 },
    { time: 196, type: 'small', variant: 2, position: 4, item: 0, shoots: 0, shootInterval: 3 },
    { time: 197, type: 'small', variant: 7, position: 3, item: 0, shoots: 0, shootInterval: 3 },
    { time: 198, type: 'small', variant: 4, position: 2, item: 1, shoots: 0, shootInterval: 3 },
    { time: 199, type: 'medium', variant: 2, position: 3, item: 2, shoots: 2, shootInterval: 3 },
    { time: 202, type: 'small', variant: 5, position: 1, item: 1, shoots: 1, shootInterval: 3 },
    { time: 204, type: 'small', variant: 5, position: 5, item: 2, shoots: 1, shootInterval: 3 },
    { time: 206, type: 'small', variant: 5, position: 2, item: 1, shoots: 1, shootInterval: 3 },
    { time: 208, type: 'small', variant: 5, position: 4, item: 2, shoots: 1, shootInterval: 3 },
    { time: 210, type: 'small', variant: 5, position: 3, item: 1, shoots: 1, shootInterval: 3 },
    { time: 212, type: 'small', variant: 2, position: 1, item: 0, shoots: 0, shootInterval: 3 },
    { time: 214, type: 'small', variant: 2, position: 2, item: 0, shoots: 0, shootInterval: 3 },
    { time: 216, type: 'small', variant: 2, position: 3, item: 0, shoots: 0, shootInterval: 3 },
    { time: 218, type: 'small', variant: 2, position: 4, item: 0, shoots: 0, shootInterval: 3 },
    { time: 220, type: 'small', variant: 2, position: 5, item: 0, shoots: 0, shootInterval: 3 },
    { time: 223, type: 'small', variant: 4, position: 1, item: 1, shoots: 0, shootInterval: 3 },
    { time: 223, type: 'small', variant: 4, position: 5, item: 1, shoots: 0, shootInterval: 3 },
    { time: 226, type: 'small', variant: 8, position: 2, item: 0, shoots: 1, shootInterval: 10 },
    { time: 226, type: 'small', variant: 8, position: 4, item: 0, shoots: 1, shootInterval: 10 },
    { time: 230, type: 'small', variant: 4, position: 3, item: 2, shoots: 0, shootInterval: 3 },
    { time: 232, type: 'small', variant: 6, position: 1, item: 2, shoots: 1, shootInterval: 3 },
    { time: 232, type: 'small', variant: 6, position: 5, item: 1, shoots: 1, shootInterval: 3 },
    { time: 235, type: 'small', variant: 2, position: 1, item: 0, shoots: 0, shootInterval: 3 },
    { time: 235, type: 'small', variant: 2, position: 2, item: 0, shoots: 0, shootInterval: 3 },
    { time: 235, type: 'small', variant: 2, position: 3, item: 0, shoots: 0, shootInterval: 3 },
    { time: 235, type: 'small', variant: 2, position: 4, item: 0, shoots: 0, shootInterval: 3 },
    { time: 235, type: 'small', variant: 2, position: 5, item: 0, shoots: 0, shootInterval: 3 },
    { time: 238, type: 'small', variant: 7, position: 1, item: 0, shoots: 0, shootInterval: 3 },
    { time: 238, type: 'small', variant: 7, position: 5, item: 0, shoots: 0, shootInterval: 3 },
    { time: 238, type: 'small', variant: 7, position: 2, item: 0, shoots: 0, shootInterval: 3 },
    { time: 238, type: 'small', variant: 7, position: 4, item: 0, shoots: 0, shootInterval: 3 },
    { time: 238, type: 'small', variant: 7, position: 3, item: 0, shoots: 0, shootInterval: 3 },
    { time: 240, type: 'small', variant: 8, position: 1, item: 0, shoots: 1, shootInterval: 10 },
    { time: 240, type: 'small', variant: 8, position: 5, item: 0, shoots: 1, shootInterval: 10 },
    { time: 240, type: 'small', variant: 2, position: 2, item: 1, shoots: 0, shootInterval: 3 },
    { time: 240, type: 'small', variant: 2, position: 4, item: 2, shoots: 0, shootInterval: 3 },
    { time: 240, type: 'small', variant: 8, position: 3, item: 0, shoots: 1, shootInterval: 10 },
    { time: 241, type: 'small', variant: 2, position: 1, item: 0, shoots: 0, shootInterval: 3 },
    { time: 241, type: 'small', variant: 2, position: 2, item: 0, shoots: 0, shootInterval: 3 },
    { time: 241, type: 'small', variant: 2, position: 3, item: 0, shoots: 0, shootInterval: 3 },
    { time: 241, type: 'small', variant: 2, position: 4, item: 0, shoots: 0, shootInterval: 3 },
    { time: 241, type: 'small', variant: 2, position: 5, item: 0, shoots: 0, shootInterval: 3 },
    { time: 242, type: 'small', variant: 6, position: 5, item: 1, shoots: 1, shootInterval: 3 },
    { time: 242, type: 'small', variant: 6, position: 1, item: 2, shoots: 1, shootInterval: 3 },
    { time: 243, type: 'small', variant: 5, position: 1, item: 1, shoots: 1, shootInterval: 3 },
    { time: 244, type: 'small', variant: 5, position: 5, item: 2, shoots: 1, shootInterval: 3 },
    { time: 245, type: 'small', variant: 5, position: 2, item: 1, shoots: 1, shootInterval: 3 },
    { time: 246, type: 'small', variant: 5, position: 4, item: 2, shoots: 1, shootInterval: 3 },
    { time: 247, type: 'small', variant: 5, position: 3, item: 1, shoots: 1, shootInterval: 3 },
    { time: 248, type: 'small', variant: 2, position: 5, item: 0, shoots: 0, shootInterval: 3 },
    { time: 250, type: 'small', variant: 4, position: 2, item: 2, shoots: 0, shootInterval: 3 },
    { time: 250, type: 'small', variant: 4, position: 3, item: 1, shoots: 0, shootInterval: 3 },
    { time: 250, type: 'small', variant: 4, position: 4, item: 2, shoots: 0, shootInterval: 3 },
    { time: 252, type: 'small', variant: 2, position: 1, item: 0, shoots: 0, shootInterval: 3 },
    { time: 255, type: 'large', variant: 1, position: 2, item: 2, shoots: 1, shootInterval: 1 },
    { time: 260, type: 'small', variant: 6, position: 4, item: 1, shoots: 1, shootInterval: 3 },
    { time: 270, type: 'large', variant: 1, position: 4, item: 1, shoots: 1, shootInterval: 1 },
    { time: 275, type: 'small', variant: 6, position: 2, item: 2, shoots: 1, shootInterval: 3 },
    { time: 280, type: 'small', variant: 5, position: 1, item: 2, shoots: 1, shootInterval: 3 },
    { time: 285, type: 'small', variant: 5, position: 5, item: 1, shoots: 1, shootInterval: 3 },
    { time: 290, type: 'small', variant: 5, position: 3, item: 2, shoots: 1, shootInterval: 3 },
    { time: 295, type: 'small', variant: 6, position: 4, item: 1, shoots: 1, shootInterval: 3 },
    { time: 300, type: 'small', variant: 6, position: 2, item: 2, shoots: 1, shootInterval: 3 },
    // 以下、必要に応じてさらに敵の出現パターンを追加
];

2日目 まとめ

2日目の追加分はこのような内容でした。

  • タイトル画面追加
  • 効果音、BGM追加
  • 画面サイズの変更(やや拡大)
  • アイテムを取ってパワーアップ(オレンジ:連射弾、青:貫通反射弾)
  • アイテムを取って自機の体力回復
  • 自機の体力表示(右下)
  • 大型ボス1追加
  • GAME OVER表示からの再スタート

一応、音がついてボスも出るようになったのでそれなりに楽しめるようになりましたが、まだボスが1種類なのと、ゲームクリアがない(ある程度すすむと敵が出なくなるところで実質終わり)のでそのあたりを今後追加します。

今後の実装予定

  • 大型ボス2追加
  • 大型ボス3(ラスボス)追加
  • 追加のパワーアップとして追従ドローン(グラディ〇スのオプションや、ガン〇ムのファン〇ルのようなもの)※敵の弾を消せる予定
  • ゲームバランス調整(難しいけど何度も挑戦したらぎりぎりクリアできる、爽快感がありつつ達成感もあるライン)
  • ゲームクリアの判定(ラスボスを倒したらクリア)
  • ゲームの進行状況の表示(現在地点表記 or ラスボス登場までの時間表記など)

以上、ROBO船長でした。では、また次回!

コメント

タイトルとURLをコピーしました