🔬 AI発明ギャラリー momoka2 メンダコ・ブレイド
🔒 サンドボックス内で実行中 ⛶ 全画面で遊ぶ
HTML / CSS / JS
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>メンダコ・ブレイド</title>
    <style>
        body {
            margin: 0;
            padding: 0;
            background-color: #111;
            display: flex;
            flex-direction: column; /* 縦並びを基本にする */
            justify-content: center;
            align-items: center;
            height: 100vh;
            height: 100dvh; /* スマホのアドレスバー対策 */
            font-family: 'Courier New', Courier, monospace;
            overflow: hidden;
            /* スワイプによる更新などを防止 */
            touch-action: none; 
        }

        /* ゲーム画面全体を囲み、レイアウトを調整するラッパー */
        #gameWrapper {
            flex: 1; /* 残りの空間をすべて使う */
            width: 100%;
            display: flex;
            justify-content: center;
            align-items: center;
            min-height: 0; /* はみ出し防止に必須 */
            padding: 10px;
            box-sizing: border-box;
        }

        #gameContainer {
            position: relative;
            /* アスペクト比 4:3 を維持しつつ親要素内で最大化(はみ出さない) */
            aspect-ratio: 4 / 3;
            width: 800px; /* 基準幅。max-width/heightで自動縮小される */
            max-width: 100%;
            max-height: 100%;
        }

        canvas {
            width: 100%;
            height: 100%;
            background-color: #2ca3e6; /* 明るい海の色に変更 */
            /* ドット絵をくっきり表示させるための設定 */
            image-rendering: -moz-crisp-edges;
            image-rendering: -webkit-crisp-edges;
            image-rendering: pixelated;
            image-rendering: crisp-edges;
            box-shadow: 0 0 20px rgba(0,0,0,0.8);
        }

        .ui-screen {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;
            color: white;
            text-shadow: 3px 3px 0 #000;
            background: rgba(0, 0, 0, 0.4);
        }

        .hidden {
            display: none !important;
        }

        h1 {
            font-size: 4rem;
            margin: 0 0 20px 0;
            color: #e43b44;
            letter-spacing: 5px;
        }

        p {
            font-size: 1.5rem;
            margin: 10px 0;
            animation: blink 1.5s infinite;
        }

        .instructions {
            font-size: 1.2rem;
            color: #9badb7;
            text-align: center;
            line-height: 1.8;
            margin-top: 40px;
            animation: none;
        }

        .score-text {
            font-size: 2.5rem;
            color: #ffee83;
            animation: none;
        }

        @keyframes blink {
            0%, 100% { opacity: 1; }
            50% { opacity: 0.3; }
        }

        /* --- タッチ操作用UI --- */
        .controls-wrapper {
            flex: 0 0 auto;
            width: 100%;
            max-width: 600px;
            display: flex;
            justify-content: space-between;
            align-items: flex-end;
            padding: 10px 20px;
            /* iPhoneの下部バー(ホームインジケータ)被り対策 */
            padding-bottom: calc(10px + env(safe-area-inset-bottom));
            box-sizing: border-box;
        }

        .controls-wrapper.hidden {
            display: none !important;
        }

        .dpad {
            display: flex;
            flex-direction: column;
            align-items: center;
            -webkit-touch-callout: none;
        }

        .dpad-row {
            display: flex;
        }

        .dpad-btn {
            width: 50px;
            height: 50px;
            margin: 2px;
            background: rgba(255, 255, 255, 0.2);
            border: 2px solid rgba(255, 255, 255, 0.5);
            border-radius: 10px;
            color: white;
            font-size: 20px;
            font-weight: bold;
            display: flex;
            justify-content: center;
            align-items: center;
            user-select: none;
            -webkit-user-select: none;
            touch-action: none;
            cursor: pointer;
        }

        .dpad-center {
            width: 50px;
            height: 50px;
            margin: 2px;
        }

        .action-pad {
            display: flex;
            align-items: flex-end;
            pointer-events: auto;
        }

        .action-btn {
            width: 70px;
            height: 70px;
            margin-bottom: 10px;
            background: rgba(228, 59, 68, 0.5);
            border: 2px solid rgba(228, 59, 68, 0.8);
            border-radius: 50%;
            color: white;
            font-size: 20px;
            font-weight: bold;
            display: flex;
            justify-content: center;
            align-items: center;
            user-select: none;
            -webkit-user-select: none;
            touch-action: none;
            cursor: pointer;
        }

        .dpad-btn.active {
            background: rgba(255, 255, 255, 0.6);
            border-color: white;
        }

        .action-btn.active {
            background: rgba(228, 59, 68, 0.9);
            border-color: white;
            transform: scale(0.95);
        }

        /* スマホ向けのサイズ調整 */
        @media (max-width: 600px) {
            .dpad-btn, .dpad-center {
                width: 45px;
                height: 45px;
                font-size: 16px;
            }
            .action-btn {
                width: 65px;
                height: 65px;
                font-size: 16px;
            }
            h1 { font-size: 2.2rem; }
            .score-text { font-size: 1.5rem; }
            .instructions { font-size: 1rem; margin-top: 20px; }
            .controls-wrapper {
                padding: 10px 10px;
                padding-bottom: calc(10px + env(safe-area-inset-bottom));
            }
        }

        /* スマホ横画面対策: ゲーム画面と重ならないように左右に配置 */
        @media (orientation: landscape) and (max-height: 500px) {
            body {
                flex-direction: row;
            }
            #gameWrapper {
                padding: 10px; /* 左右の余白は固定せず自然に任せる */
            }
            .controls-wrapper {
                position: absolute;
                bottom: 10px;
                left: 0;
                width: 100%;
                max-width: none;
                padding: 0;
                /* スマホのノッチ(カメラの出っ張り)避け */
                padding-left: calc(20px + env(safe-area-inset-left));
                padding-right: calc(20px + env(safe-area-inset-right));
                padding-bottom: calc(10px + env(safe-area-inset-bottom));
                pointer-events: none; /* コンテナはタッチを透過 */
            }
            .dpad, .action-pad {
                pointer-events: auto; /* ボタン部分はタッチを受け付ける */
            }
        }
    </style>
</head>
<body>

    <div id="gameWrapper">
        <div id="gameContainer">
            <!-- 論理サイズは400x300に設定し、CSSで800x600に拡大してレトロ感を出す -->
            <canvas id="gameCanvas" width="400" height="300"></canvas>
            
            <div id="uiStart" class="ui-screen">
                <h1>メンダコ・ブレイド</h1>
                <p>SPACEキーでスタート</p>
                <div class="instructions">
                    移動: WASD または 矢印キー<br>
                    攻撃: SPACEキー
                </div>
            </div>

            <div id="uiGameOver" class="ui-screen hidden">
                <h1>GAME OVER</h1>
                <div class="score-text">SCORE: <span id="finalScore">0</span></div>
                <p>SPACEキーでリトライ</p>
            </div>
        </div>
    </div>

    <!-- 仮想コントローラー (ゲーム画面と重ならないよう分離) -->
    <div id="onScreenControls" class="controls-wrapper hidden">
        <div class="dpad">
            <div class="dpad-row">
                <button class="dpad-btn up" data-key="ArrowUp">▲</button>
            </div>
            <div class="dpad-row">
                <button class="dpad-btn left" data-key="ArrowLeft">◀</button>
                <div class="dpad-center"></div>
                <button class="dpad-btn right" data-key="ArrowRight">▶</button>
            </div>
            <div class="dpad-row">
                <button class="dpad-btn down" data-key="ArrowDown">▼</button>
            </div>
        </div>
        <div class="action-pad">
            <button class="action-btn attack" data-key=" ">剣</button>
        </div>
    </div>

    <script>
        const canvas = document.getElementById('gameCanvas');
        const ctx = canvas.getContext('2d');
        const uiStart = document.getElementById('uiStart');
        const uiGameOver = document.getElementById('uiGameOver');
        const finalScoreEl = document.getElementById('finalScore');

        // --- パレット (レトロ風カラー) ---
        const palette = {
            0: null,
            1: '#e43b44', // 赤 (メンダコ)
            2: '#ffffff', // 白
            3: '#181425', // 黒 (縁取り・影)
            4: '#ffee83', // 黄色 (剣の柄)
            5: '#9badb7', // 銀色 (剣の刃)
            6: '#2ce8f5', // 水色 (魚)
            7: '#0099db', // 青 (魚の影)
        };

        // --- ピクセルアート配列 ---
        // メンダコ
        const mendakoArt = [
            [0,0,0,0,0,1,1,1,1,0,0,0,0,0],
            [0,1,1,0,0,1,1,1,1,0,0,1,1,0], // 耳(ヒレ)を追加
            [0,1,1,1,1,1,1,1,1,1,1,1,1,0],
            [0,0,1,1,1,1,1,1,1,1,1,1,0,0],
            [0,0,1,1,3,3,1,1,3,3,1,1,0,0],
            [0,0,1,1,3,3,1,1,3,3,1,1,0,0],
            [0,1,1,1,1,1,1,1,1,1,1,1,1,0],
            [1,1,1,1,1,1,1,1,1,1,1,1,1,1],
            [0,1,0,1,0,0,1,1,0,0,1,0,1,0],
            [0,1,1,0,0,0,1,1,0,0,0,1,1,0]
        ];

        // 剣
        const swordArt = [
            [0,0,0,0,0,0,0,5,5],
            [0,0,0,0,0,0,5,5,5],
            [0,0,0,0,0,5,5,5,0],
            [0,0,0,0,5,5,5,0,0],
            [0,0,0,5,5,5,0,0,0],
            [0,0,5,5,5,0,0,0,0],
            [0,5,5,5,0,0,0,0,0],
            [5,5,4,4,0,0,0,0,0],
            [5,4,4,3,0,0,0,0,0],
            [0,5,3,3,0,0,0,0,0]
        ];

        // 魚(敵)
        const fishArt = [
            [0,0,0,0,0,0,6,6,0,0,0,0],
            [0,0,0,0,0,6,6,6,6,0,0,0],
            [0,0,0,0,6,6,6,6,6,6,0,0],
            [6,0,0,6,6,6,3,3,6,6,6,0],
            [6,6,6,6,6,6,3,3,6,6,6,0],
            [6,6,6,6,6,6,6,6,6,6,0,0],
            [6,0,0,6,6,6,6,6,6,0,0,0],
            [0,0,0,0,0,6,7,0,0,0,0,0]
        ];

        // ハート(アイテム)
        const heartArt = [
            [0,1,1,0,1,1,0],
            [1,1,1,1,1,1,1],
            [1,1,1,1,1,1,1],
            [0,1,1,1,1,1,0],
            [0,0,1,1,1,0,0],
            [0,0,0,1,0,0,0]
        ];

        const scale = 2; // 1ピクセルを2x2で描画

        // アート描画関数
        function drawArt(ctx, art, x, y, scale) {
            for (let r = 0; r < art.length; r++) {
                for (let c = 0; c < art[r].length; c++) {
                    let colorIndex = art[r][c];
                    if (colorIndex !== 0) {
                        ctx.fillStyle = palette[colorIndex];
                        ctx.fillRect(x + c * scale, y + r * scale, scale, scale);
                    }
                }
            }
        }

        // --- ゲームクラス ---

        class Player {
            constructor() {
                this.w = 14 * scale; // ヒレを追加した分、横幅を広げる
                this.h = 10 * scale;
                this.x = 50;
                this.y = 150;
                this.speed = 2.5;
                this.hp = 3;
                this.maxHp = 3;
                this.isAttacking = false;
                this.attackTimer = 0;
                this.attackDuration = 12; // 攻撃のフレーム数
                this.isInvincible = false;
                this.invincibleTimer = 0;
                this.invincibleDuration = 60; // 被ダメージ時の無敵フレーム数
                this.swordScale = 1.0; // 剣のサイズ
            }

            powerUp() {
                // HP回復と剣の巨大化
                if (this.hp < this.maxHp) this.hp++;
                if (this.swordScale < 2.5) {
                    this.swordScale += 0.5;
                }
            }

            update(keys) {
                // 移動
                if (keys['ArrowUp'] || keys['w']) this.y -= this.speed;
                if (keys['ArrowDown'] || keys['s']) this.y += this.speed;
                if (keys['ArrowLeft'] || keys['a']) this.x -= this.speed;
                if (keys['ArrowRight'] || keys['d']) this.x += this.speed;

                // 画面端の制限
                if (this.x < 0) this.x = 0;
                if (this.y < 20) this.y = 20; // UI領域を避ける
                if (this.x > canvas.width - this.w) this.x = canvas.width - this.w;
                if (this.y > canvas.height - this.h - 30) this.y = canvas.height - this.h - 30; // 30は海底の高さ

                // 攻撃
                if (keys[' '] && !this.isAttacking && this.attackTimer === 0) {
                    this.isAttacking = true;
                    this.attackTimer = this.attackDuration;
                }

                if (this.attackTimer > 0) {
                    this.attackTimer--;
                    if (this.attackTimer === 0) {
                        this.isAttacking = false;
                    }
                }

                // 無敵時間
                if (this.invincibleTimer > 0) {
                    this.invincibleTimer--;
                    if (this.invincibleTimer === 0) {
                        this.isInvincible = false;
                    }
                }
            }

            draw(ctx) {
                // 無敵時の点滅エフェクト
                if (this.isInvincible && Math.floor(Date.now() / 100) % 2 === 0) {
                    // 描画スキップで点滅を表現
                } else {
                    drawArt(ctx, mendakoArt, this.x, this.y, scale);
                }

                // 剣の描画とアニメーション
                ctx.save();
                // 剣の回転軸をメンダコの右側中央あたりに設定
                ctx.translate(this.x + this.w - 4, this.y + this.h / 2);
                
                let angle = -Math.PI / 4; // 待機時の角度 (上向き)
                if (this.attackTimer > 0) {
                    // 攻撃時は振り下ろすアニメーション
                    let progress = 1 - (this.attackTimer / this.attackDuration);
                    angle = -Math.PI / 4 + progress * (Math.PI * 0.8);
                }
                ctx.rotate(angle);
                drawArt(ctx, swordArt, 0, -8 * scale * this.swordScale, scale * this.swordScale);
                ctx.restore();

                // デバッグ用: 攻撃判定の可視化
                /*
                if (this.isAttacking) {
                    let hHit = this.getAttackHitbox();
                    ctx.strokeStyle = 'red';
                    ctx.strokeRect(hHit.x, hHit.y, hHit.w, hHit.h);
                }
                */
            }

            getHitbox() {
                // 見た目より少し小さめの当たり判定
                return { x: this.x + 4, y: this.y + 4, w: this.w - 8, h: this.h - 8 };
            }

            getAttackHitbox() {
                if (!this.isAttacking) return null;
                // メンダコの右側に攻撃判定を発生させる
                return {
                    x: this.x + this.w - 10,
                    y: this.y - 15 * this.swordScale,
                    w: 35 * this.swordScale,
                    h: (this.h + 30) * this.swordScale
                };
            }
        }

        class Enemy {
            constructor(speedMultiplier) {
                this.w = 12 * scale;
                this.h = 8 * scale;
                this.x = canvas.width;
                this.y = Math.random() * (canvas.height - this.h - 40 - 30) + 30; // 海底を避ける
                this.speed = (1.5 + Math.random() * 1.5) * speedMultiplier;
                this.baseY = this.y;
                this.waveTimer = Math.random() * Math.PI * 2;
                this.waveSpeed = 0.03 + Math.random() * 0.05;
                this.waveAmp = 10 + Math.random() * 20;
                this.markedForDeletion = false;
            }

            update() {
                this.x -= this.speed;
                this.waveTimer += this.waveSpeed;
                this.y = this.baseY + Math.sin(this.waveTimer) * this.waveAmp;

                if (this.x < -this.w) {
                    this.markedForDeletion = true;
                }
            }

            draw(ctx) {
                drawArt(ctx, fishArt, this.x, this.y, scale);
            }

            getHitbox() {
                return { x: this.x + 2, y: this.y + 2, w: this.w - 4, h: this.h - 4 };
            }
        }

        class Item {
            constructor() {
                this.w = 7 * scale;
                this.h = 6 * scale;
                this.x = canvas.width;
                this.y = Math.random() * (canvas.height - this.h - 60 - 30) + 30; // 海底を避ける
                this.speed = 1.5;
                this.markedForDeletion = false;
                this.waveTimer = 0;
            }

            update() {
                this.x -= this.speed;
                this.waveTimer += 0.05;
                this.y += Math.sin(this.waveTimer) * 0.5; // ふわふわ揺れる

                if (this.x < -this.w) {
                    this.markedForDeletion = true;
                }
            }

            draw(ctx) {
                // 少し光る演出
                ctx.shadowBlur = 10;
                ctx.shadowColor = '#ffb8b8';
                drawArt(ctx, heartArt, this.x, this.y, scale);
                ctx.shadowBlur = 0;
            }

            getHitbox() {
                return { x: this.x, y: this.y, w: this.w, h: this.h };
            }
        }

        class Particle {
            constructor(x, y, color) {
                this.x = x;
                this.y = y;
                this.vx = (Math.random() - 0.5) * 8;
                this.vy = (Math.random() - 0.5) * 8;
                this.life = 20 + Math.random() * 10;
                this.color = color;
                this.size = scale * (1 + Math.random());
            }

            update() {
                this.x += this.vx;
                this.y += this.vy;
                this.life--;
            }

            draw(ctx) {
                ctx.fillStyle = this.color;
                ctx.globalAlpha = Math.max(0, this.life / 30);
                ctx.fillRect(this.x, this.y, this.size, this.size);
                ctx.globalAlpha = 1;
            }
        }

        class Bubble {
            constructor() {
                this.x = Math.random() * canvas.width;
                this.y = canvas.height - 30; // 初期位置も海底から
                this.speed = 0.5 + Math.random();
                this.size = scale + Math.random() * scale * 2;
            }
            update() {
                // 上に昇りつつ、スクロールに合わせて左に流れる
                this.y -= this.speed;
                this.x -= (1.5 + Math.sin(this.y * 0.02) * 0.5);
                
                if (this.y < -10 || this.x < -10) {
                    this.y = canvas.height - 30; // 海底から湧き出る
                    this.x = canvas.width + Math.random() * 50;
                }
            }
            draw(ctx) {
                ctx.strokeStyle = 'rgba(255, 255, 255, 0.4)'; // 背景が明るくなったので泡を少し濃く
                ctx.lineWidth = 1;
                ctx.strokeRect(this.x, this.y, this.size, this.size);
            }
        }

        // --- ゲーム状態と変数 ---
        let gameState = 'start'; // 'start', 'playing', 'gameover'
        let player;
        let enemies = [];
        let particles = [];
        let bubbles = [];
        let items = [];
        let score = 0;
        let killCount = 0;
        let frameCount = 0;
        let keys = {};
        let bgScroll = 0; // スクロール位置

        // --- 入力処理 ---
        window.addEventListener('keydown', (e) => {
            keys[e.key] = true;
            // スペースキー押下時の状態遷移
            if (e.key === ' ' || e.code === 'Space') {
                if (gameState === 'start') {
                    startGame();
                } else if (gameState === 'gameover') {
                    // 少しディレイを入れて連打防止
                    if (frameCount > 30) {
                        startGame();
                    }
                }
            }
        });

        window.addEventListener('keyup', (e) => {
            keys[e.key] = false;
        });

        // --- タッチ操作対応 ---
        const isTouchDevice = ('ontouchstart' in window) || (navigator.maxTouchPoints > 0);
        
        if (isTouchDevice) {
            document.getElementById('onScreenControls').classList.remove('hidden');
            // UIの文言をタッチ端末向けに変更
            document.querySelector('#uiStart p').innerText = '画面タップでスタート';
            document.querySelector('#uiStart .instructions').innerHTML = '左下: 移動ボタン<br>右下: 攻撃ボタン';
            document.querySelector('#uiGameOver p').innerText = '画面タップでリトライ';
        }

        // タップでスタート・リトライ
        function handleStartTap(e) {
            // ボタン自体をタップした時は反応させない
            if (e.target.closest('.on-screen-controls')) return;
            
            e.preventDefault();
            if (gameState === 'start') {
                startGame();
            } else if (gameState === 'gameover') {
                if (frameCount > 30) {
                    startGame();
                }
            }
        }

        uiStart.addEventListener('touchstart', handleStartTap);
        uiStart.addEventListener('click', handleStartTap);
        uiGameOver.addEventListener('touchstart', handleStartTap);
        uiGameOver.addEventListener('click', handleStartTap);

        // 仮想ボタンのイベント処理
        const touchBtns = document.querySelectorAll('.dpad-btn, .action-btn');
        touchBtns.forEach(btn => {
            const key = btn.getAttribute('data-key');
            
            const pressBtn = (e) => {
                e.preventDefault();
                keys[key] = true;
                btn.classList.add('active');
            };

            const releaseBtn = (e) => {
                e.preventDefault();
                keys[key] = false;
                btn.classList.remove('active');
            };

            btn.addEventListener('touchstart', pressBtn);
            btn.addEventListener('mousedown', pressBtn); // PCのマウスでもテスト可能に
            
            btn.addEventListener('touchend', releaseBtn);
            btn.addEventListener('touchcancel', releaseBtn);
            btn.addEventListener('mouseup', releaseBtn);
            btn.addEventListener('mouseleave', releaseBtn);
        });

        // 衝突判定ヘルパー
        function checkCollision(rect1, rect2) {
            return rect1.x < rect2.x + rect2.w &&
                   rect1.x + rect1.w > rect2.x &&
                   rect1.y < rect2.y + rect2.h &&
                   rect1.y + rect1.h > rect2.y;
        }

        // パーティクル生成
        function createExplosion(x, y, colorArr) {
            for (let i = 0; i < 15; i++) {
                let color = colorArr[Math.floor(Math.random() * colorArr.length)];
                particles.push(new Particle(x, y, color));
            }
        }

        // 進行度に応じた背景色を取得 (深く進むほど暗くなる)
        function getBackgroundColor(kills) {
            // 100キルで一番暗い深海色になるように補間
            let ratio = Math.min(kills / 100, 1);
            let r = Math.floor(44 + (5 - 44) * ratio);   // 44 -> 5
            let g = Math.floor(163 + (10 - 163) * ratio); // 163 -> 10
            let b = Math.floor(230 + (31 - 230) * ratio); // 230 -> 31
            return `rgb(${r}, ${g}, ${b})`;
        }

        // 遠景(岩山)の描画
        function drawBackground(ctx) {
            let ratio = Math.min(killCount / 100, 1);
            let r = Math.floor(34 + (3 - 34) * ratio);
            let g = Math.floor(143 + (6 - 143) * ratio);
            let b = Math.floor(210 + (21 - 210) * ratio);
            ctx.fillStyle = `rgb(${r}, ${g}, ${b})`;

            let patternWidth = 150;
            let offset = (bgScroll * 0.3) % patternWidth; // 遠景はゆっくりスクロール
            
            ctx.beginPath();
            ctx.moveTo(0, canvas.height - 30);
            for (let x = -offset; x <= canvas.width + patternWidth; x += patternWidth) {
                ctx.lineTo(x, canvas.height - 30);
                ctx.lineTo(x + 50, canvas.height - 100);
                ctx.lineTo(x + 100, canvas.height - 60);
                ctx.lineTo(x + 150, canvas.height - 30);
            }
            ctx.lineTo(canvas.width, canvas.height - 30);
            ctx.fill();
        }

        // 海底の描画
        function drawFloor(ctx) {
            let floorY = canvas.height - 30;
            let ratio = Math.min(killCount / 100, 1);
            
            // 砂の色
            let r = Math.floor(194 + (20 - 194) * ratio);
            let g = Math.floor(178 + (20 - 178) * ratio);
            let b = Math.floor(128 + (20 - 128) * ratio);
            ctx.fillStyle = `rgb(${r}, ${g}, ${b})`;
            ctx.fillRect(0, floorY, canvas.width, 30);

            // 地面の模様(岩や海草のシルエット)
            ctx.fillStyle = `rgba(0, 0, 0, 0.2)`;
            let patternWidth = 60;
            let offset = bgScroll % patternWidth; // 近景は速くスクロール
            for(let x = -offset; x < canvas.width; x += patternWidth) {
                ctx.fillRect(x + 10, floorY + 5, 10, 5);
                ctx.fillRect(x + 35, floorY + 15, 15, 8);
            }
        }

        // ゲーム初期化
        function startGame() {
            player = new Player();
            enemies = [];
            particles = [];
            items = [];
            score = 0;
            killCount = 0;
            frameCount = 0;
            bgScroll = 0;
            gameState = 'playing';
            
            uiStart.classList.add('hidden');
            uiGameOver.classList.add('hidden');

            // 泡の初期化
            bubbles = [];
            for(let i=0; i<15; i++) {
                let b = new Bubble();
                b.y = Math.random() * canvas.height;
                b.x = Math.random() * canvas.width;
                bubbles.push(b);
            }
        }

        // メインループ
        function gameLoop() {
            update();
            draw();
            requestAnimationFrame(gameLoop);
        }

        // 更新処理
        function update() {
            if (gameState !== 'playing') {
                // 背景の泡とスクロールだけ動かす
                bgScroll += 1.5;
                bubbles.forEach(b => b.update());
                if (gameState === 'gameover') frameCount++;
                return;
            }

            frameCount++;
            bgScroll += 1.5; // スクロール進行
            player.update(keys);
            bubbles.forEach(b => b.update());

            // 敵のスポーン制御 (進む=キル数が増えるほど頻度・数・速度が増す)
            let spawnRate = Math.max(15, 70 - Math.floor(killCount / 5) * 5);
            let speedMultiplier = 1 + (killCount / 30);
            let spawnCount = 1 + Math.floor(killCount / 25); // 25匹倒すごとに同時に出る数が増える

            if (frameCount % spawnRate === 0) {
                for(let j=0; j<spawnCount; j++) {
                    let enemy = new Enemy(speedMultiplier);
                    // 複数出る場合は重ならないようにY軸をずらす
                    enemy.y += (j * 25);
                    if (enemy.y > canvas.height - enemy.h - 10) enemy.y = canvas.height - enemy.h - 10;
                    enemies.push(enemy);
                }
            }

            // 攻撃判定の取得
            let attackHitbox = player.getAttackHitbox();
            let playerHitbox = player.getHitbox();

            // アイテムの更新と判定
            for (let i = items.length - 1; i >= 0; i--) {
                let item = items[i];
                item.update();
                
                if (checkCollision(playerHitbox, item.getHitbox())) {
                    player.powerUp();
                    score += 500;
                    createExplosion(item.x + item.w/2, item.y + item.h/2, ['#ff0000', '#ffffff', '#ffb8b8']);
                    item.markedForDeletion = true;
                }

                if (item.markedForDeletion) {
                    items.splice(i, 1);
                }
            }

            // 敵の更新と判定
            for (let i = enemies.length - 1; i >= 0; i--) {
                let enemy = enemies[i];
                enemy.update();

                let enemyHitbox = enemy.getHitbox();

                // プレイヤーの攻撃が敵に当たったか
                if (attackHitbox && checkCollision(attackHitbox, enemyHitbox)) {
                    enemy.markedForDeletion = true;
                    score += 100;
                    killCount++; // キル数をカウント(進行度)
                    
                    // 水色と青のパーティクル
                    createExplosion(enemy.x + enemy.w/2, enemy.y + enemy.h/2, [palette[6], palette[7], '#ffffff']);
                    
                    // 20匹倒すごとにアイテム出現
                    if (killCount % 20 === 0) {
                        items.push(new Item());
                    }
                    continue; // 倒したので以下の被ダメージ判定はスキップ
                }

                // 敵がプレイヤーに当たったか
                if (!player.isInvincible && checkCollision(playerHitbox, enemyHitbox)) {
                    player.hp--;
                    player.isInvincible = true;
                    player.invincibleTimer = player.invincibleDuration;
                    
                    // 赤いダメージパーティクル
                    createExplosion(player.x + player.w/2, player.y + player.h/2, [palette[1], '#ff0044', '#ffffff']);

                    if (player.hp <= 0) {
                        gameState = 'gameover';
                        finalScoreEl.innerText = score;
                        uiGameOver.classList.remove('hidden');
                        frameCount = 0; // リトライ遅延用
                    }
                }

                if (enemy.markedForDeletion) {
                    enemies.splice(i, 1);
                }
            }

            // パーティクルの更新
            for (let i = particles.length - 1; i >= 0; i--) {
                particles[i].update();
                if (particles[i].life <= 0) {
                    particles.splice(i, 1);
                }
            }
        }

        // 描画処理
        function draw() {
            // 背景クリア (進行度に応じて暗くなる)
            ctx.fillStyle = getBackgroundColor(killCount); 
            ctx.fillRect(0, 0, canvas.width, canvas.height);

            // 遠景(岩山)の描画
            drawBackground(ctx);

            // 泡の描画
            bubbles.forEach(b => b.draw(ctx));

            // 海底の描画
            drawFloor(ctx);

            if (gameState === 'start') {
                return; // UIが表示されているので描画は背景のみ
            }

            // エンティティの描画
            items.forEach(i => i.draw(ctx));
            particles.forEach(p => p.draw(ctx));
            enemies.forEach(e => e.draw(ctx));
            
            if (gameState === 'playing' || (gameState === 'gameover' && player.hp > 0)) {
                player.draw(ctx);
            }

            // UI (スコアとHP)
            ctx.fillStyle = 'white';
            ctx.font = '16px monospace';
            ctx.textAlign = 'left';
            ctx.fillText(`SCORE: ${score}`, 10, 20);
            
            // HPをハートで描画
            let hpText = 'HP: ';
            for(let i=0; i<player.maxHp; i++) {
                hpText += (i < player.hp) ? '♥' : '♡';
            }
            ctx.fillStyle = '#e43b44';
            ctx.fillText(hpText, 10, 40);
        }

        // 初期泡の生成とループ開始
        for(let i=0; i<15; i++) {
            let b = new Bubble();
            b.y = Math.random() * canvas.height;
            bubbles.push(b);
        }
        requestAnimationFrame(gameLoop);

    </script>
</body>
</html>
✨ ピックアップ

メンダコ・ブレイド

by momoka2

メンダコが剣を使って魚を倒すゲームです。 20匹倒すごとに敵が増えたり、アイテムが大きくなります。

🤖 使用したAI
Gemini
👁 51 回閲覧
📅 投稿:2026年4月11日 🔄 更新:2026年8月13日
応援スタンプを押してみよう!