🔬 AI発明ギャラリー syunsuke カービー
🔒 サンドボックス内で実行中 ⛶ 全画面で遊ぶ
HTML / CSS / JS
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
    <title>Kirby Ultra Realistic Adventure</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <style>
        @import url('https://fonts.googleapis.com/css2?family=M+PLUS+Rounded+1c:wght@700;800&display=swap');
        
        body {
            margin: 0;
            background-color: #000;
            font-family: 'M PLUS Rounded 1c', sans-serif;
            overflow: hidden;
            touch-action: none;
        }

        #game-wrapper {
            position: relative;
            width: 100vw;
            height: 100vh;
            display: flex;
            justify-content: center;
            align-items: center;
            background: radial-gradient(circle at center, #1a1a2e 0%, #000000 100%);
        }

        #canvas-container {
            position: relative;
            width: 100%;
            max-width: 1024px;
            aspect-ratio: 16 / 9;
            background: #000;
            box-shadow: 0 0 50px rgba(0, 0, 0, 1), 0 0 100px rgba(56, 189, 248, 0.2);
            border: 2px solid #1e293b;
            border-radius: 12px;
            overflow: hidden;
        }

        /* 究極のリアルさを出すためのポストプロセッシング(ビネット&微細なノイズ) */
        #canvas-container::after {
            content: "";
            position: absolute;
            inset: 0;
            background: radial-gradient(circle, rgba(0,0,0,0) 50%, rgba(0,0,0,0.6) 100%);
            box-shadow: inset 0 0 20px rgba(255,255,255,0.05);
            pointer-events: none;
            z-index: 10;
        }

        canvas {
            width: 100%;
            height: 100%;
            image-rendering: auto;
            filter: contrast(1.1) saturate(1.1); /* コントラストを高めてリッチに */
        }

        /* UI Overlay */
        #hud {
            position: absolute;
            bottom: 0;
            left: 0;
            width: 100%;
            height: 80px;
            background: linear-gradient(to top, rgba(0, 0, 0, 0.8), rgba(0,0,0,0));
            display: flex;
            align-items: flex-end;
            padding: 15px 25px;
            pointer-events: none;
            display: none;
            z-index: 20;
        }

        .hp-bar-bg {
            width: 200px;
            height: 18px;
            background: rgba(0,0,0,0.6);
            border: 2px solid rgba(255,255,255,0.8);
            border-radius: 9px;
            overflow: hidden;
            box-shadow: 0 0 10px rgba(0,0,0,0.5);
        }

        .hp-bar-fill {
            height: 100%;
            background: linear-gradient(to right, #fb7185, #e11d48);
            width: 100%;
            transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
            box-shadow: inset 0 0 5px rgba(255,255,255,0.5);
        }

        #title-screen {
            position: absolute;
            inset: 0;
            background: radial-gradient(circle at top, #334155 0%, #0f172a 100%);
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;
            z-index: 50;
        }

        .start-btn {
            background: linear-gradient(to bottom, #f8fafc, #cbd5e1);
            color: #0f172a;
            padding: 1rem 4rem;
            border-radius: 9999px;
            font-size: 1.5rem;
            font-weight: 800;
            box-shadow: 0 10px 20px rgba(0,0,0,0.5), inset 0 -3px 0 rgba(0,0,0,0.2);
            transition: all 0.1s;
            cursor: pointer;
            margin-top: 3rem;
            pointer-events: auto;
            text-shadow: 0 1px 1px rgba(255,255,255,0.8);
        }

        .start-btn:active {
            transform: translateY(4px);
            box-shadow: 0 6px 10px rgba(0,0,0,0.5), inset 0 -1px 0 rgba(0,0,0,0.2);
        }

        .char-btn {
            width: 60px;
            height: 60px;
            border-radius: 50%;
            cursor: pointer;
            border: 3px solid rgba(255,255,255,0.3);
            transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
            box-shadow: 0 10px 15px rgba(0,0,0,0.5);
        }
        .char-btn.selected {
            border: 4px solid #fff;
            transform: scale(1.2) translateY(-10px);
            box-shadow: 0 15px 25px rgba(0,0,0,0.6), 0 0 20px rgba(255,255,255,0.4);
        }

        #touch-ui {
            position: absolute;
            bottom: 20px;
            width: 100%;
            display: none;
            justify-content: space-between;
            padding: 0 40px;
            pointer-events: none;
            z-index: 100;
        }

        .touch-btn {
            background: rgba(255, 255, 255, 0.15);
            border: 2px solid rgba(255, 255, 255, 0.3);
            border-radius: 50%;
            display: flex;
            justify-content: center;
            align-items: center;
            pointer-events: auto;
            color: rgba(255,255,255,0.9);
            font-weight: bold;
            user-select: none;
            backdrop-filter: blur(4px);
            box-shadow: 0 5px 15px rgba(0,0,0,0.3);
            transition: background 0.1s;
        }
        .touch-btn:active {
            background: rgba(255, 255, 255, 0.3);
        }

        /* 縦持ち(スマートフォン)対応のレイアウト最適化 */
        @media (orientation: portrait) {
            #game-wrapper {
                flex-direction: column;
                justify-content: flex-start;
                padding-top: 10vh; /* 上部にゲーム画面を配置 */
            }
            #canvas-container {
                border-radius: 0;
                border-left: none;
                border-right: none;
            }
            /* コントローラーを画面下部の黒帯部分に固定配置 */
            #touch-ui {
                position: fixed;
                bottom: 8vh;
                left: 0;
                width: 100vw;
                padding: 0 5vw;
                box-sizing: border-box;
                z-index: 100;
            }
            /* ボタンを親指で押しやすいサイズに相対調整 */
            .touch-btn {
                width: 16vw !important;
                height: 16vw !important;
                font-size: 6vw !important;
            }
            #t-shift {
                width: 14vw !important;
                height: 14vw !important;
                font-size: 3vw !important;
            }
            #t-x, #t-z {
                width: 20vw !important;
                height: 20vw !important;
                font-size: 8vw !important;
            }
            
            /* タイトル画面の文字サイズ・余白調整(縦画面でもボタンが隠れないようにする) */
            #title-screen h1 { font-size: 14vw; line-height: 1; margin-top: -2vw; }
            #title-screen p { font-size: 3vw; margin-top: 0 !important; }
            #title-screen > div.mt-12 { margin-top: 0.5rem !important; margin-bottom: 0.25rem !important; font-size: 2.5vw; }
            #char-select { gap: 10px !important; }
            .char-btn { width: 8vw; height: 8vw; border-width: 2px; }
            .char-btn.selected { transform: scale(1.2) translateY(-5px); border-width: 3px; }
            .start-btn { font-size: 4vw; padding: 0.5rem 1.5rem; margin-top: 1rem !important; box-shadow: 0 5px 10px rgba(0,0,0,0.5), inset 0 -2px 0 rgba(0,0,0,0.2); }
            
            /* HUD(UI)の縮小調整 */
            #hud {
                padding: 10px;
                height: auto;
            }
            #ability-icon { width: 40px; height: 40px; }
            #ability-text { font-size: 8px; }
            .hp-bar-bg { width: 100px; height: 12px; }
            .mb-1 { margin-bottom: 2px; }
            #stage-text { font-size: 1.2rem; }
            #score-text { font-size: 1rem; }
        }

    </style>
</head>
<body>

<div id="game-wrapper">
    <div id="canvas-container">
        <canvas id="gameCanvas" width="1024" height="576"></canvas>
        
        <div id="title-screen">
            <h1 class="text-7xl text-transparent bg-clip-text bg-gradient-to-b from-white to-gray-400 drop-shadow-[0_0_15px_rgba(255,255,255,0.5)] font-black italic tracking-tighter">KIRBY</h1>
            <p class="text-blue-300 text-2xl mt-2 font-bold tracking-[0.3em] drop-shadow-md">ULTRA REALISTIC</p>
            
            <div class="mt-12 text-white/80 font-bold mb-4 tracking-widest text-sm">SELECT PLAYER COLOR</div>
            <div id="char-select" class="flex gap-6 pointer-events-auto">
                <div class="char-btn selected" style="background: radial-gradient(circle at 30% 30%, #ffb7c5, #e11d48);" data-color="pink" onclick="selectChar('pink')"></div>
                <div class="char-btn" style="background: radial-gradient(circle at 30% 30%, #fef08a, #d97706);" data-color="yellow" onclick="selectChar('yellow')"></div>
                <div class="char-btn" style="background: radial-gradient(circle at 30% 30%, #bae6fd, #0284c7);" data-color="blue" onclick="selectChar('blue')"></div>
                <div class="char-btn" style="background: radial-gradient(circle at 30% 30%, #bbf7d0, #166534);" data-color="green" onclick="selectChar('green')"></div>
            </div>

            <div class="start-btn" onclick="startGame()">ENTER THE WORLD</div>
        </div>

        <div id="hud">
            <div class="flex items-end gap-5 flex-wrap w-full">
                <div id="ability-icon" class="w-16 h-16 bg-black/40 rounded-2xl flex items-center justify-center border-2 border-white/20 backdrop-blur-md shadow-[0_0_15px_rgba(0,0,0,0.5)]">
                    <span id="ability-text" class="text-xs font-bold text-white tracking-wider">NORMAL</span>
                </div>
                <div class="mb-1">
                    <div class="text-white/80 text-[10px] mb-1 font-bold tracking-widest">HP</div>
                    <div class="hp-bar-bg">
                        <div id="hp-fill" class="hp-bar-fill"></div>
                    </div>
                </div>
                <div class="mb-1">
                    <div class="text-white/80 text-[10px] mb-1 font-bold tracking-widest">WING STAMINA</div>
                    <div style="width: 120px; height: 12px; background: rgba(0,0,0,0.6); border: 2px solid rgba(255,255,255,0.8); border-radius: 6px; overflow: hidden; box-shadow: 0 0 10px rgba(0,0,0,0.5);">
                        <div id="wing-fill" style="height: 100%; background: linear-gradient(to right, #38bdf8, #818cf8); width: 100%; transition: width 0.1s; box-shadow: inset 0 0 5px rgba(255,255,255,0.8);"></div>
                    </div>
                </div>
                
                <div id="boss-hp-container" class="ml-auto mb-1" style="display: none; width: 30%;">
                    <div class="text-yellow-500 text-[10px] mb-1 font-bold tracking-widest text-right">BOSS</div>
                    <div style="width: 100%; height: 18px; background: rgba(0,0,0,0.8); border: 2px solid #eab308; border-radius: 9px; overflow: hidden; box-shadow: 0 0 15px rgba(234, 179, 8, 0.4);">
                        <div id="boss-hp-fill" style="height: 100%; background: linear-gradient(to right, #facc15, #b45309); width: 100%; transition: width 0.1s; box-shadow: inset 0 0 10px rgba(255,255,255,0.5);"></div>
                    </div>
                </div>
            </div>
            
            <div class="absolute top-5 right-5 text-right z-20">
                <div class="text-white text-3xl font-black drop-shadow-[0_2px_4px_rgba(0,0,0,0.8)] tracking-tighter" id="stage-text">STAGE 1</div>
                <div id="score-text" class="text-white/90 text-lg font-bold drop-shadow-md tracking-wider font-mono">0000000</div>
                <div class="text-white/60 text-xs font-bold drop-shadow-md mt-1">ONLINE: <span id="online-count">1</span></div>
            </div>
        </div>

        <div id="stage-clear-screen" class="absolute inset-0 bg-white/10 backdrop-blur-lg flex flex-col items-center justify-center hidden z-40">
            <h2 class="text-7xl font-black text-transparent bg-clip-text bg-gradient-to-b from-yellow-300 to-yellow-600 drop-shadow-[0_0_20px_rgba(250,204,21,0.6)] italic tracking-tighter">STAGE CLEAR</h2>
        </div>

        <div id="game-clear-screen" class="absolute inset-0 bg-black/80 backdrop-blur-xl flex flex-col items-center justify-center hidden z-50">
            <h2 class="text-8xl font-black text-transparent bg-clip-text bg-gradient-to-b from-white to-blue-400 drop-shadow-[0_0_30px_rgba(255,255,255,0.8)] tracking-tighter">ALL CLEAR</h2>
            <div class="start-btn" onclick="location.reload()">RETURN TO WORLD</div>
        </div>

        <div id="touch-ui">
            <div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px;">
                <div class="touch-btn" id="t-up" style="grid-column: 2; grid-row: 1; width:65px; height:65px;">↑</div>
                <div class="touch-btn" id="t-left" style="grid-column: 1; grid-row: 2; width:65px; height:65px;">←</div>
                <div class="touch-btn" id="t-right" style="grid-column: 3; grid-row: 2; width:65px; height:65px;">→</div>
                <div class="touch-btn" id="t-down" style="grid-column: 2; grid-row: 3; width:65px; height:65px;">↓</div>
            </div>
            <div class="flex items-end gap-4 mb-4">
                <div class="touch-btn text-xs flex-col" id="t-shift" style="width:70px; height:70px; background: rgba(168, 85, 247, 0.4); border-color: rgba(216, 180, 254, 0.6);"><span>WING</span></div>
                <div class="touch-btn" id="t-x" style="width:80px; height:80px; font-size: 28px; background: rgba(239, 68, 68, 0.4); border-color: rgba(252, 165, 165, 0.6);">B</div>
                <div class="touch-btn" id="t-z" style="width:80px; height:80px; font-size: 28px; background: rgba(59, 130, 246, 0.4); border-color: rgba(147, 197, 253, 0.6);">A</div>
            </div>
        </div>
    </div>
</div>

<script type="module">
import { initializeApp } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-app.js";
import { getAuth, signInWithCustomToken, signInAnonymously, onAuthStateChanged } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-auth.js";
import { getFirestore, doc, setDoc, onSnapshot, collection } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-firestore.js";

const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const GRAVITY = 0.45;
const FRICTION = 0.82;
const JUMP_FORCE = -10;
const ACCEL = 0.8;
const MAX_SPEED = 5.5;

const CHAR_COLORS = {
    'pink': { body: '#ffb7c5', cheek: '#f43f5e', feet: '#e11d48', shadow: '#9f1239' },
    'yellow': { body: '#fef08a', cheek: '#f59e0b', feet: '#ea580c', shadow: '#9a3412' },
    'blue': { body: '#bae6fd', cheek: '#38bdf8', feet: '#0284c7', shadow: '#075985' },
    'green': { body: '#bbf7d0', cheek: '#22c55e', feet: '#166534', shadow: '#14532d' }
};

let gameState = 'TITLE'; 
const keys = {};

// 超リアルな翼の描画(透過と重なりを表現)
function drawRealisticWing(ctx, flapAngle, isForeground) {
    ctx.save();
    if (isForeground) {
        ctx.translate(-8, 0);
        ctx.rotate((-flapAngle - 20) * Math.PI / 180);
        ctx.fillStyle = 'rgba(255, 255, 255, 0.9)';
    } else {
        ctx.translate(8, -5);
        ctx.rotate((flapAngle + 20) * Math.PI / 180);
        ctx.fillStyle = 'rgba(226, 232, 240, 0.8)'; // 奥は少し暗く透過
    }

    ctx.strokeStyle = 'rgba(148, 163, 184, 0.5)';
    ctx.lineWidth = 1;
    
    for (let i = 0; i < 5; i++) {
        ctx.save();
        ctx.rotate((-20 + i * 15) * Math.PI / 180);
        
        // 羽のグラデーション
        let grad = ctx.createLinearGradient(0, 0, -30, 0);
        grad.addColorStop(0, ctx.fillStyle);
        grad.addColorStop(1, 'rgba(255,255,255,0)');
        
        ctx.fillStyle = grad;
        ctx.beginPath();
        ctx.ellipse(-14 - i * 2, -2 - i, 16 + i * 2, 5 + i * 0.5, 0, 0, Math.PI * 2);
        ctx.fill(); ctx.stroke();
        
        // 羽の芯
        ctx.strokeStyle = 'rgba(255,255,255,0.8)';
        ctx.beginPath(); ctx.moveTo(-2, -2 - i); ctx.lineTo(-26 - i*4, -2 - i); ctx.stroke();
        ctx.restore();
    }
    ctx.restore();
}

class Camera {
    constructor() {
        this.baseX = 0; this.baseY = 0;
        this.x = 0; this.y = 0; 
        this.lerp = 0.08; // カメラ追従を少し滑らかに
        this.shakeX = 0; this.shakeY = 0;
        this.shakeTimer = 0; this.shakeIntensity = 0;
    }
    shake(intensity, duration) {
        this.shakeIntensity = intensity;
        this.shakeTimer = duration;
    }
    update(targetX, targetY, mapW, mapH, boss) {
        let tx = targetX - canvas.width / 2;
        let ty = targetY - canvas.height / 2 - 20; // プレイヤーを少し下寄りに配置して空を広く
        
        if (boss && boss.hp > 0) {
            let bossAreaStartX = boss.x - canvas.width + 200;
            if (tx > bossAreaStartX) tx = bossAreaStartX;
        }

        this.baseX += (tx - this.baseX) * this.lerp;
        this.baseY += (ty - this.baseY) * this.lerp;
        this.baseX = Math.max(0, Math.min(this.baseX, mapW - canvas.width));
        this.baseY = Math.max(-200, Math.min(this.baseY, mapH - canvas.height)); // 上空にも少し行けるように

        if (this.shakeTimer > 0) {
            this.shakeX = (Math.random() - 0.5) * this.shakeIntensity;
            this.shakeY = (Math.random() - 0.5) * this.shakeIntensity;
            this.shakeTimer--;
            this.shakeIntensity *= 0.85; 
        } else {
            this.shakeX = 0; this.shakeY = 0;
        }

        this.x = this.baseX + this.shakeX;
        this.y = this.baseY + this.shakeY;
    }
}

class Kirby {
    constructor(x, y, colorType = 'pink') {
        this.x = x; this.y = y;
        this.vx = 0; this.vy = 0;
        this.w = 40; this.h = 40;
        this.hp = 6; this.maxHp = 6;
        this.facing = 1;
        this.state = 'IDLE'; 
        this.ability = 'NORMAL';
        this.invincibility = 0;
        this.onGround = false;
        this.frame = 0;
        this.swallowedType = null;
        this.colorType = colorType;
        this.isWinged = false;
        this.prevShift = false;
        this.wingStamina = 600; 
        this.maxWingStamina = 600;
        this.aimY = 0; 
    }

    update() {
        this.frame++;
        if (this.invincibility > 0) this.invincibility--;

        let moveX = 0; let moveY = 0;
        if (keys['ArrowLeft'] || keys['t-left']) moveX = -1;
        if (keys['ArrowRight'] || keys['t-right']) moveX = 1;
        if (keys['ArrowUp'] || keys['t-up']) moveY = -1;
        if (keys['ArrowDown'] || keys['t-down']) moveY = 1;

        const jumpTrigger = (keys['z'] || keys['t-z']) && !this.prevZ;
        const actionTrigger = (keys['x'] || keys['t-x']) && !this.prevX;
        const shiftTrigger = (keys['Shift'] || keys['t-shift']) && !this.prevShift;
        
        this.prevZ = !!(keys['z'] || keys['t-z']);
        this.prevX = !!(keys['x'] || keys['t-x']);
        this.prevShift = !!(keys['Shift'] || keys['t-shift']);

        let attackDirX = this.facing;
        let attackDirY = 0;
        this.aimY = 0;
        if (keys['ArrowUp'] || keys['t-up']) {
            attackDirX = 0; attackDirY = -1; this.aimY = -1;
        }

        if (shiftTrigger && this.wingStamina > 0) {
            this.isWinged = !this.isWinged;
            game.particles.push(new Particle(this.x+20, this.y+20, 'COPY_FLASH'));
        }

        if (this.isWinged) {
            this.wingStamina -= 1;
            if (this.wingStamina <= 0) {
                this.wingStamina = 0; this.isWinged = false;
                game.particles.push(new Particle(this.x+20, this.y+20, 'PUFF'));
            }

            if (moveX !== 0) {
                this.vx += moveX * ACCEL; this.facing = moveX;
            } else this.vx *= FRICTION;
            
            if (moveY !== 0) this.vy += moveY * ACCEL;
            else this.vy *= FRICTION;
            
            this.state = 'HOVER';
            
            if (actionTrigger) game.spawnBullet(this.x + 20, this.y + 20, attackDirX, attackDirY, 'AIR');
        } else {
            if (this.state === 'INHALE') {
                this.vx *= 0.7;
                if (!(keys['x'] || keys['t-x'])) this.state = 'IDLE';
                if (this.frame % 3 === 0) {
                    if (this.aimY === -1) game.particles.push(new Particle(this.x + 20 + (Math.random()-0.5)*20, this.y - 30, 'INHALE_WIND', 0, -8));
                    else game.particles.push(new Particle(this.x + 20 + this.facing * 50, this.y + 20 + (Math.random()-0.5)*20, 'INHALE_WIND', -this.facing * 8, 0));
                }
            } else if (this.state === 'ATTACK') {
                this.vx *= 0.8;
                if (this.ability === 'FIRE' || this.ability === 'ICE' || this.ability === 'SPARK') {
                    if (!(keys['x'] || keys['t-x'])) this.state = 'IDLE';
                    if (this.ability === 'FIRE' && this.frame % 4 === 0) game.spawnBullet(this.x + 20, this.y + 20, attackDirX, attackDirY, 'FIRE');
                    else if (this.ability === 'ICE' && this.frame % 4 === 0) game.spawnBullet(this.x + 20, this.y + 20, attackDirX, attackDirY, 'ICE');
                    else if (this.ability === 'SPARK' && this.frame % 2 === 0) {
                        let sx = this.x + 20 + (Math.random() - 0.5) * 60;
                        let sy = this.y + 20 + (Math.random() - 0.5) * 60;
                        game.spawnBullet(sx, sy, attackDirX, attackDirY, 'SPARK');
                    }
                } else {
                    this.attackTimer--;
                    if (this.attackTimer <= 0) this.state = 'IDLE';
                }
            } else {
                if (moveX !== 0) {
                    this.vx += moveX * ACCEL; this.facing = moveX;
                    if (this.onGround && this.state !== 'FULL' && this.state !== 'HOVER') this.state = 'RUN';
                } else {
                    this.vx *= FRICTION;
                    if (this.onGround && this.state !== 'FULL' && this.state !== 'HOVER') this.state = 'IDLE';
                }

                if (jumpTrigger) {
                    if (this.onGround) {
                        this.vy = JUMP_FORCE; this.onGround = false;
                    } else {
                        if (this.state === 'FULL') {
                            game.spawnBullet(this.x + 20, this.y + 20, attackDirX, attackDirY, 'STAR');
                            this.swallowedType = null;
                        }
                        this.state = 'HOVER'; this.vy = -5;
                        game.particles.push(new Particle(this.x+20, this.y+40, 'PUFF'));
                    }
                }

                if (actionTrigger) {
                    if (this.state === 'FULL') {
                        game.spawnBullet(this.x + 20, this.y + 20, attackDirX, attackDirY, 'STAR');
                        this.state = 'IDLE'; this.swallowedType = null;
                        game.camera.shake(4, 5); // 星を吐いた時の軽い振動
                    } else if (this.state === 'HOVER') {
                        game.spawnBullet(this.x + 20, this.y + 20, attackDirX, attackDirY, 'AIR');
                        this.state = 'IDLE';
                    } else if (this.ability !== 'NORMAL') {
                        this.state = 'ATTACK';
                        if (this.ability === 'SWORD') {
                            this.attackTimer = 15;
                            game.spawnBullet(this.x + 20, this.y + 20, attackDirX, attackDirY, 'SWORD_WAVE');
                            game.camera.shake(3, 5);
                        } else if (this.ability === 'CUTTER') {
                            this.attackTimer = 15;
                            game.spawnBullet(this.x + 20, this.y + 20, attackDirX, attackDirY, 'CUTTER');
                        } else if (this.ability === 'SUPER') {
                            this.attackTimer = 20; 
                            game.spawnBullet(this.x + 20, this.y + 20, attackDirX, attackDirY, 'SUPER_STAR');
                            this.vx = -this.facing * 3;
                            game.camera.shake(12, 15); // スーパー弾の大振動
                        }
                    } else {
                        this.state = 'INHALE';
                    }
                }

                if (this.state === 'FULL' && (keys['ArrowDown'] || keys['t-down'])) {
                    if (this.swallowedType && this.swallowedType !== 'NORMAL') {
                        this.ability = this.swallowedType;
                        game.particles.push(new Particle(this.x+20, this.y+20, 'COPY_FLASH'));
                        game.camera.shake(5, 10);
                    } else {
                        this.ability = 'NORMAL';
                    }
                    updateAbilityUI(this.ability);
                    this.state = 'IDLE';
                    this.swallowedType = null;
                }
            }
        }

        if (!this.isWinged) {
            let grav = (this.state === 'HOVER') ? GRAVITY * 0.3 : GRAVITY;
            this.vy += grav;
        }

        if (!this.isWinged && this.onGround) {
            this.wingStamina += 4;
            if (this.wingStamina > this.maxWingStamina) this.wingStamina = this.maxWingStamina;
        }

        if (this.vx > MAX_SPEED) this.vx = MAX_SPEED;
        if (this.vx < -MAX_SPEED) this.vx = -MAX_SPEED;
        if (this.vy > MAX_SPEED * 1.5) this.vy = MAX_SPEED * 1.5;
        if (this.vy < -MAX_SPEED * 1.5) this.vy = -MAX_SPEED * 1.5;

        this.x += this.vx;
        this.y += this.vy;

        if (game.boss && game.boss.hp > 0) {
            let camLeft = game.camera.x;
            if (this.x < camLeft) this.x = camLeft;
        }

        let wasOnGround = this.onGround;
        this.onGround = false;
        for (let b of game.map.blocks) {
            if (this.checkCollision(b)) {
                if (this.vy > 0 && this.y + this.h - this.vy <= b.y + 10) {
                    this.y = b.y - this.h;
                    this.vy = 0;
                    this.onGround = true;
                    if (this.state === 'HOVER') this.state = 'IDLE';
                    if (!wasOnGround && this.vy > 5) game.particles.push(new Particle(this.x+20, this.y+40, 'PUFF')); // 着地土煙
                } else if (this.vy < 0 && this.y - this.vy >= b.y + b.h - 10) {
                    this.y = b.y + b.h; this.vy = 0;
                } else {
                    if (this.vx > 0) this.x = b.x - this.w;
                    else if (this.vx < 0) this.x = b.x + b.w;
                    this.vx = 0;
                }
            }
        }

        game.enemies.forEach(e => e.isBeingInhaled = false);
        if (this.state === 'INHALE') {
            game.enemies.forEach(e => {
                let dx = (this.x + 20) - (e.x + 15);
                let dy = (this.y + 20) - (e.y + 15);
                let dist = Math.sqrt(dx*dx + dy*dy);
                
                let validAngle = false;
                if (this.aimY === -1) {
                    if (dy > 0 && Math.abs(dx) < dy) validAngle = true;
                } else {
                    if (Math.sign(-dx) === this.facing && Math.abs(dy) < Math.abs(dx)) validAngle = true;
                }

                if (dist < 180 && (validAngle || dist < 40)) { // 吸い込み範囲を少し拡大
                    e.isBeingInhaled = true;
                    let pullStrength = (e.ability === 'NORMAL') ? 0.15 : 0.02;
                    e.x += (this.x + 20 - 15 - e.x) * pullStrength;
                    e.y += (this.y + 20 - 15 - e.y) * pullStrength;
                    
                    if (dist < 30) {
                        e.hp = 0; this.state = 'FULL'; this.swallowedType = e.ability;
                        game.score += 100;
                    }
                }
            });
        }

        if (this.y > 650) this.takeDamage(this.hp); 
        
        document.getElementById('hp-fill').style.width = `${(this.hp / this.maxHp) * 100}%`;
        document.getElementById('wing-fill').style.width = `${(this.wingStamina / this.maxWingStamina) * 100}%`;
        document.getElementById('score-text').innerText = String(game.score).padStart(7, '0');
    }

    checkCollision(rect) {
        return this.x < rect.x + rect.w && this.x + this.w > rect.x && this.y < rect.y + rect.h && this.y + this.h > rect.y;
    }

    takeDamage(n) {
        if (this.invincibility > 0) return;
        this.hp -= n;
        this.invincibility = 60;
        this.vy = -7;
        this.vx = -this.facing * 6;
        this.ability = 'NORMAL';
        this.state = 'IDLE';
        this.swallowedType = null;
        updateAbilityUI('NORMAL');
        game.camera.shake(15, 20); 
        // ダメージパーティクル(星が飛び散る)
        for(let i=0; i<8; i++) game.particles.push(new Particle(this.x+20, this.y+20, 'STAR_PIECE', (Math.random()-0.5)*10, (Math.random()-1)*10));
        
        if (this.hp <= 0) game.reset();
    }

    draw(ctx, cam) {
        if (this.invincibility > 0 && (this.frame % 6 < 3)) return;
        
        ctx.save();
        ctx.translate(this.x - cam.x + 20, this.y - cam.y + 20);

        // リアルなドロップシャドウ(影)
        ctx.fillStyle = 'rgba(0,0,0,0.6)';
        ctx.filter = 'blur(2px)'; // 影をぼかす
        ctx.beginPath();
        let shadowScale = this.onGround ? 1 : Math.max(0.3, 1 - (cam.y+20 - this.y)/100);
        ctx.ellipse(0, 25 + (cam.y+20 > this.y ? 0 : this.y - cam.y), 20 * shadowScale, 6 * shadowScale, 0, 0, Math.PI*2);
        ctx.fill();
        ctx.filter = 'none';

        if (this.facing === -1) ctx.scale(-1, 1);

        if (this.isWinged) {
            let flap = Math.sin(this.frame * 0.4) * 30;
            drawRealisticWing(ctx, flap, false);
        }

        const colors = CHAR_COLORS[this.colorType] || CHAR_COLORS['pink'];
        let walkCycle = 0;
        if (this.onGround && Math.abs(this.vx) > 0.5) walkCycle = Math.sin(this.frame * 0.3) * 5;
        else if (!this.onGround) walkCycle = -4;

        // 足(リアルな立体感)
        let drawFoot = (x, y) => {
            let gradF = ctx.createRadialGradient(x, y-2, 2, x, y, 8);
            gradF.addColorStop(0, 'rgba(255,255,255,0.7)'); 
            gradF.addColorStop(0.3, colors.feet);
            gradF.addColorStop(1, colors.shadow);       
            ctx.fillStyle = gradF;
            ctx.beginPath(); ctx.ellipse(x, y, 9, 6, 0, 0, Math.PI*2); ctx.fill();
        };
        drawFoot(-8, 16 + walkCycle);
        drawFoot(8, 16 - walkCycle);

        let r = (this.state === 'FULL' || this.state === 'HOVER') ? 25 : 20;
        
        // 体(究極の球体ライティング)
        let gradBody = ctx.createRadialGradient(-r*0.3, -r*0.3, r*0.1, 0, 0, r*1.1);
        gradBody.addColorStop(0, '#ffffff'); // ハイライトコア
        gradBody.addColorStop(0.1, 'rgba(255,255,255,0.8)'); 
        gradBody.addColorStop(0.4, colors.body); // ベースカラー
        gradBody.addColorStop(0.8, colors.shadow); // コアシャドウ
        gradBody.addColorStop(1, colors.body); // 環境光の反射(リムライト)

        ctx.fillStyle = gradBody; 
        ctx.beginPath(); ctx.arc(0, 0, r, 0, Math.PI*2); ctx.fill();

        let faceOffsetY = 0;
        if (this.aimY === -1 && (this.state === 'INHALE' || this.state === 'ATTACK')) faceOffsetY = -8;

        ctx.fillStyle = colors.cheek; 
        ctx.beginPath(); ctx.ellipse(-10, faceOffsetY, 4, 2.5, -0.1, 0, Math.PI*2); ctx.fill();
        ctx.beginPath(); ctx.ellipse(10, faceOffsetY, 4, 2.5, 0.1, 0, Math.PI*2); ctx.fill();

        ctx.fillStyle = '#1e293b'; // 目を少しネイビー寄りの黒に
        if (this.state === 'INHALE' || this.state === 'ATTACK') {
             ctx.beginPath(); ctx.ellipse(-5, -6 + faceOffsetY, 2.5, 7, Math.PI/8, 0, Math.PI*2); ctx.fill();
             ctx.beginPath(); ctx.ellipse(5, -6 + faceOffsetY, 2.5, 7, -Math.PI/8, 0, Math.PI*2); ctx.fill();
        } else {
             ctx.beginPath(); ctx.ellipse(-5, -6 + faceOffsetY, 2.5, 7, 0, 0, Math.PI*2); ctx.fill();
             ctx.beginPath(); ctx.ellipse(5, -6 + faceOffsetY, 2.5, 7, 0, 0, Math.PI*2); ctx.fill();
             ctx.fillStyle = '#fff';
             ctx.beginPath(); ctx.arc(-5, -9 + faceOffsetY, 1.5, 0, Math.PI*2); ctx.fill();
             ctx.beginPath(); ctx.arc(5, -9 + faceOffsetY, 1.5, 0, Math.PI*2); ctx.fill();
             ctx.fillStyle = '#38bdf8'; // 瞳孔の下部反射
             ctx.beginPath(); ctx.arc(-5, -4 + faceOffsetY, 1, 0, Math.PI*2); ctx.fill();
             ctx.beginPath(); ctx.arc(5, -4 + faceOffsetY, 1, 0, Math.PI*2); ctx.fill();
        }

        if (this.state === 'INHALE' || this.state === 'ATTACK') {
            ctx.fillStyle = '#450a0a';
            ctx.beginPath(); ctx.arc(0, 4 + faceOffsetY, 8, 0, Math.PI*2); ctx.fill();
            // 口の中の奥の暗闇
            ctx.fillStyle = '#000';
            ctx.beginPath(); ctx.arc(0, 4 + faceOffsetY, 5, 0, Math.PI*2); ctx.fill();
        } else if (this.state === 'FULL' || this.state === 'HOVER') {
            ctx.strokeStyle = '#1e293b'; ctx.lineWidth = 1.5;
            ctx.beginPath(); ctx.moveTo(-4, 6 + faceOffsetY); ctx.lineTo(4, 6 + faceOffsetY); ctx.stroke();
        } else {
            ctx.strokeStyle = '#1e293b'; ctx.lineWidth = 1.5;
            ctx.beginPath(); ctx.arc(0, 3 + faceOffsetY, 3, 0.1*Math.PI, 0.9*Math.PI); ctx.stroke();
            // 笑った時の口の中
            if (!this.onGround && this.vy > 0) {
                ctx.fillStyle = '#e11d48';
                ctx.beginPath(); ctx.arc(0, 5 + faceOffsetY, 2, 0, Math.PI*2); ctx.fill();
            }
        }

        let armRot = 0;
        if (this.state === 'INHALE' || this.state === 'ATTACK') armRot = (this.aimY === -1) ? -Math.PI / 1.5 : -Math.PI / 4;
        else if (this.state === 'HOVER' && this.vy < 0) armRot = Math.PI / 2;
        else if (!this.onGround) armRot = Math.PI / 4;
        else armRot = Math.sin(this.frame * 0.1) * 0.2;

        let drawArm = (x, y, rot) => {
            ctx.save(); ctx.translate(x, y); ctx.rotate(rot);
            let gradA = ctx.createRadialGradient(-2, -2, 1, 0, 0, 7);
            gradA.addColorStop(0, 'rgba(255,255,255,0.7)'); gradA.addColorStop(0.5, colors.body); gradA.addColorStop(1, colors.shadow);
            ctx.fillStyle = gradA;
            ctx.beginPath(); ctx.ellipse(0, 0, 8, 6, 0, 0, Math.PI*2); ctx.fill();
            ctx.restore();
        };
        
        ctx.save(); ctx.translate(12, 5); 
        if (this.ability === 'SWORD' && this.state !== 'FULL' && this.state !== 'INHALE' && this.state !== 'HOVER') {
            let swordRot = armRot;
            if (this.state === 'ATTACK') swordRot = (this.aimY === -1) ? -Math.PI / 2 : Math.PI / 2; 
            ctx.save();
            ctx.rotate(swordRot);
            ctx.fillStyle = '#f59e0b'; ctx.fillRect(0, -5, 12, 4);
            // 剣の金属の質感(グラデーション)
            let gradSword = ctx.createLinearGradient(12, -7, 12, 1);
            gradSword.addColorStop(0, '#f8fafc'); gradSword.addColorStop(0.5, '#cbd5e1'); gradSword.addColorStop(1, '#64748b');
            ctx.fillStyle = gradSword; 
            ctx.beginPath(); ctx.moveTo(12, -7); ctx.lineTo(38, -3); ctx.lineTo(12, 1); ctx.closePath(); ctx.fill(); 
            // 剣の輝き
            ctx.strokeStyle = '#fff'; ctx.lineWidth = 1;
            ctx.beginPath(); ctx.moveTo(12, -6); ctx.lineTo(35, -3); ctx.stroke();
            ctx.restore();
        }
        ctx.restore();
        
        drawArm(14, 5, armRot); // 手前
        drawArm(-14, 5, -armRot); // 奥

        if (this.isWinged) {
            let flap = Math.sin(this.frame * 0.4) * 30;
            drawRealisticWing(ctx, flap, true);
        }

        // コピー能力の帽子のリッチ化
        if (this.ability === 'FIRE') {
            // 燃えるようなグラデーション
            let gradHat = ctx.createLinearGradient(0, -35, 0, -15);
            gradHat.addColorStop(0, '#fef08a'); gradHat.addColorStop(0.5, '#f97316'); gradHat.addColorStop(1, '#be123c');
            ctx.fillStyle = gradHat;
            ctx.beginPath(); ctx.moveTo(-15, -15); ctx.quadraticCurveTo(0, -20, 5, -40); ctx.quadraticCurveTo(5, -20, 15, -15); ctx.fill(); 
        } else if (this.ability === 'SWORD') {
            ctx.fillStyle = '#166534';
            ctx.beginPath(); ctx.moveTo(-16, -12); ctx.lineTo(-12, -45); ctx.lineTo(20, -15); ctx.fill(); 
            ctx.strokeStyle = '#facc15'; ctx.lineWidth = 3;
            ctx.beginPath(); ctx.moveTo(-18, -12); ctx.lineTo(18, -12); ctx.stroke();
        } else if (this.ability === 'ICE') {
            // 氷の結晶のような半透明
            ctx.fillStyle = 'rgba(186, 230, 253, 0.8)';
            ctx.beginPath(); ctx.moveTo(-15, -15); ctx.lineTo(-10, -35); ctx.lineTo(-5, -20); ctx.lineTo(0, -40); ctx.lineTo(5, -20); ctx.lineTo(10, -35); ctx.lineTo(15, -15); ctx.fill(); 
            ctx.strokeStyle = '#fff'; ctx.lineWidth = 1; ctx.stroke();
        } else if (this.ability === 'CUTTER') {
            ctx.fillStyle = '#facc15'; ctx.beginPath(); ctx.arc(0, -15, 16, Math.PI, 0); ctx.fill(); 
            let gradCutter = ctx.createLinearGradient(0, -50, 0, -31);
            gradCutter.addColorStop(0, '#fff'); gradCutter.addColorStop(1, '#94a3b8');
            ctx.fillStyle = gradCutter;
            ctx.beginPath(); ctx.moveTo(-5, -31); ctx.lineTo(0, -55); ctx.lineTo(5, -31); ctx.fill(); 
        } else if (this.ability === 'SPARK') {
            ctx.fillStyle = '#a3e635';
            ctx.beginPath(); ctx.moveTo(-15, -15); ctx.lineTo(-20, -25); ctx.lineTo(-10, -20); ctx.lineTo(0, -40); ctx.lineTo(10, -20); ctx.lineTo(20, -25); ctx.lineTo(15, -15); ctx.fill(); 
            // バチバチのエフェクト
            if(Math.random() < 0.3) { ctx.strokeStyle = '#fff'; ctx.beginPath(); ctx.moveTo(0, -40); ctx.lineTo((Math.random()-0.5)*20, -50); ctx.stroke(); }
        } else if (this.ability === 'SUPER') {
            // グローする虹色の王冠
            ctx.shadowBlur = 10; ctx.shadowColor = '#fff';
            ctx.fillStyle = `hsl(${this.frame * 15 % 360}, 100%, 60%)`;
            ctx.beginPath(); ctx.moveTo(-15, -15); ctx.lineTo(-10, -30); ctx.lineTo(0, -15); ctx.lineTo(10, -30); ctx.lineTo(15, -15); ctx.fill(); 
            ctx.shadowBlur = 0;
        }

        ctx.restore();
    }
}

class Enemy {
    constructor(x, y, type) {
        this.x = x; this.y = y; this.startY = y;
        this.w = 30; this.h = 30;
        this.type = type;
        const abilityMap = {
            'FIRE_DUDE': 'FIRE', 'SWORD_DUDE': 'SWORD', 'ICE_DUDE': 'ICE', 'CUTTER_DUDE': 'CUTTER',
            'SPARK_DUDE': 'SPARK', 'FLYING_DUDE': 'NORMAL', 'WADDLE': 'NORMAL', 'SUPER_DUDE': 'SUPER'
        };
        this.ability = abilityMap[type] || 'NORMAL';
        this.hp = (type === 'SUPER_DUDE') ? 3 : 1; 
        this.vx = -1.2; this.vy = 0;
        this.frame = 0;
        this.isBeingInhaled = false;
        this.isFlying = (type === 'FLYING_DUDE');
    }
    update() {
        if (this.isBeingInhaled) return;
        this.frame++;
        
        if (this.isFlying) {
            this.x += this.vx;
            this.y = this.startY + Math.sin(this.frame * 0.05) * 40;
            for (let b of game.map.blocks) {
                if (this.x < b.x + b.w && this.x + this.w > b.x && this.y < b.y + b.h && this.y + this.h > b.y) {
                    this.x -= this.vx; this.vx *= -1; 
                }
            }
            return;
        }

        this.vy += GRAVITY;
        if (this.vy > 10) this.vy = 10;
        this.y += this.vy;
        
        for (let b of game.map.blocks) {
            if (this.x < b.x + b.w && this.x + this.w > b.x && this.y < b.y + b.h && this.y + this.h > b.y) {
                if (this.vy > 0 && this.y + this.h - this.vy <= b.y + 15) { this.y = b.y - this.h; this.vy = 0; } 
                else if (this.vy < 0 && this.y - this.vy >= b.y + b.h - 15) { this.y = b.y + b.h; this.vy = 0; }
            }
        }

        this.x += this.vx;
        for (let b of game.map.blocks) {
            if (this.x < b.x + b.w && this.x + this.w > b.x && this.y < b.y + b.h && this.y + this.h > b.y) {
                if (this.vx > 0) { this.x = b.x - this.w; this.vx *= -1; } 
                else if (this.vx < 0) { this.x = b.x + b.w; this.vx *= -1; }
            }
        }
    }
    draw(ctx, cam) {
        ctx.save();
        ctx.translate(this.x - cam.x + 15, this.y - cam.y + 15);
        
        ctx.fillStyle = 'rgba(0,0,0,0.5)';
        ctx.filter = 'blur(2px)';
        ctx.beginPath();
        let shadowScale = this.isFlying ? 0.5 : 1;
        ctx.ellipse(0, 20 + (this.isFlying ? 30 : 0), 16 * shadowScale, 4 * shadowScale, 0, 0, Math.PI*2);
        ctx.fill();
        ctx.filter = 'none';

        let walkCycle = Math.sin(this.frame * 0.2) * 3;
        
        if (this.isFlying) {
            let flap = Math.sin(this.frame * 0.6) * 20;
            ctx.save(); ctx.scale(0.7, 0.7); 
            drawRealisticWing(ctx, flap, false);
            ctx.restore();
        }

        let gradFootE = ctx.createRadialGradient(0, 10, 1, 0, 12, 8);
        gradFootE.addColorStop(0, 'rgba(255,255,255,0.7)'); gradFootE.addColorStop(1, 'rgba(0,0,0,0.6)');

        if (!this.isFlying) {
            ctx.fillStyle = gradFootE;
            ctx.beginPath(); ctx.ellipse(-5, 12 + walkCycle, 6, 4, 0, 0, Math.PI*2); ctx.fill(); 
            ctx.beginPath(); ctx.ellipse(5, 12 - walkCycle, 6, 4, 0, 0, Math.PI*2); ctx.fill(); 
        } else {
            ctx.fillStyle = gradFootE;
            ctx.beginPath(); ctx.ellipse(-5, 12, 6, 4, -Math.PI/8, 0, Math.PI*2); ctx.fill(); 
            ctx.beginPath(); ctx.ellipse(5, 12, 6, 4, Math.PI/8, 0, Math.PI*2); ctx.fill(); 
        }

        const colorMap = {
            'FIRE': {b:'#f97316', s:'#9a3412'}, 'SWORD': {b:'#16a34a', s:'#14532d'},
            'ICE': {b:'#0ea5e9', s:'#075985'}, 'CUTTER': {b:'#94a3b8', s:'#334155'},
            'SPARK': {b:'#eab308', s:'#854d0e'}, 'NORMAL': {b: this.isFlying?'#fbcfe8':'#fb923c', s: this.isFlying?'#9d174d':'#9a3412'}
        };
        let rE = 15;
        
        let eColor = colorMap[this.ability] || colorMap['NORMAL'];
        if (this.ability === 'SUPER') {
            eColor = {b: `hsl(${this.frame * 5 % 360}, 100%, 60%)`, s: '#000'};
            ctx.shadowBlur = 15; ctx.shadowColor = eColor.b;
        }
        
        let gradEnemy = ctx.createRadialGradient(-rE*0.3, -rE*0.3, rE*0.1, 0, 0, rE*1.1);
        gradEnemy.addColorStop(0, '#fff');
        gradEnemy.addColorStop(0.2, eColor.b);
        gradEnemy.addColorStop(0.8, eColor.s);
        gradEnemy.addColorStop(1, eColor.b);

        ctx.fillStyle = gradEnemy;
        ctx.beginPath(); ctx.arc(0,0,rE,0,Math.PI*2); ctx.fill(); 
        ctx.shadowBlur = 0;

        if (this.isFlying) {
            let flap = Math.sin(this.frame * 0.6) * 20;
            ctx.save(); ctx.scale(0.7, 0.7);
            drawRealisticWing(ctx, flap, true);
            ctx.restore();
        }

        ctx.fillStyle = '#ffedd5';
        ctx.beginPath(); ctx.ellipse(-2, 2, 10, 9, 0, 0, Math.PI*2); ctx.fill();

        ctx.fillStyle = '#1e293b';
        ctx.beginPath(); ctx.ellipse(-4, 0, 1.5, 4, 0, 0, Math.PI*2); ctx.fill();
        ctx.beginPath(); ctx.ellipse(2, 0, 1.5, 4, 0, 0, Math.PI*2); ctx.fill();

        ctx.restore();
    }
}

class Boss {
    constructor(x, y, stageNum) {
        this.x = x; this.y = y;
        this.w = 120; this.h = 120; // ボスのサイズを大きく
        this.stage = stageNum;
        this.maxHp = 40 * stageNum; // ボスのHPさらにアップ
        this.hp = this.maxHp;
        this.vx = 0; this.vy = 0;
        this.frame = 0;
        this.actionTimer = 0;
        this.invincibility = 0;
    }
    update() {
        if (this.hp <= 0) return;
        this.frame++; this.actionTimer++;
        if (this.invincibility > 0) this.invincibility--;

        let distToPlayer = game.player.x - this.x;
        let dirToPlayer = Math.sign(distToPlayer) || -1;

        if (this.stage === 1) {
            this.vy += GRAVITY; if (this.y > 330) { this.y = 330; this.vy = 0; }
            if (this.actionTimer > 90) {
                game.spawnBullet(this.x + 60, this.y + 60, dirToPlayer, 0, 'FIRE', true);
                this.actionTimer = 0;
            }
        } else if (this.stage === 2) {
            this.y = 120 + Math.sin(this.frame * 0.04) * 60;
            if (this.actionTimer > 70) {
                game.spawnBullet(this.x + 60, this.y + 120, 0, 1.5, 'ICE', true);
                this.actionTimer = 0;
            }
        } else if (this.stage === 3) {
            this.vy += GRAVITY; if (this.y > 330) { this.y = 330; this.vy = 0; }
            if (this.actionTimer > 50) {
                game.spawnBullet(this.x + 60, this.y + 60, dirToPlayer, 0.2, 'CUTTER', true);
                game.spawnBullet(this.x + 60, this.y + 60, dirToPlayer, -0.7, 'CUTTER', true);
                this.actionTimer = 0;
            }
        } else if (this.stage === 4) {
            this.vy += GRAVITY; if (this.y > 330) { this.y = 330; this.vy = 0; }
            if (this.actionTimer > 60) {
                for(let i=-1; i<=1; i++) game.spawnBullet(this.x + 60, this.y + 60, dirToPlayer, i*0.8, 'SPARK', true);
                this.actionTimer = 0;
            }
        } else if (this.stage === 5) {
            this.y = 80 + Math.sin(this.frame * 0.02) * 100;
            if (this.actionTimer > 40) {
                let r = Math.random();
                if (r < 0.4) {
                    game.spawnBullet(this.x + 60, this.y + 60, dirToPlayer, 0.3, 'SUPER_STAR', true);
                    game.camera.shake(8, 15); 
                } else if (r < 0.7) {
                    for(let i=-1.5; i<=1.5; i+=0.5) game.spawnBullet(this.x + 60, this.y + 60, dirToPlayer, i, 'FIRE', true);
                } else {
                    game.spawnBullet(this.x + 60, this.y + 60, dirToPlayer, 0.8, 'CUTTER', true);
                    game.spawnBullet(this.x + 60, this.y + 60, dirToPlayer, 0, 'CUTTER', true);
                }
                this.actionTimer = 0;
            }
        }

        if (this.x < game.camera.x + 50) this.x = game.camera.x + 50;
        if (this.x > game.map.w - 200) this.x = game.map.w - 200;
        this.y += this.vy;
    }
    takeDamage(n) {
        if (this.invincibility > 0) return;
        this.hp -= n;
        this.invincibility = 10;
        game.particles.push(new Particle(this.x + 60, this.y + 60, 'HIT'));
        game.camera.shake(12, 10); 
        if (this.hp <= 0) {
            game.score += 10000;
            for(let i=0; i<40; i++) game.particles.push(new Particle(this.x + 60, this.y + 60, 'STAR_PIECE', (Math.random()-0.5)*15, (Math.random()-0.5)*15));
            game.camera.shake(30, 40); // ボス撃破時の超特大揺れ
        }
    }
    draw(ctx, cam) {
        if (this.hp <= 0) return;
        if (this.invincibility > 0 && this.frame % 4 < 2) {
            ctx.globalCompositeOperation = 'lighter'; // ダメージ時フラッシュ
        }

        ctx.save();
        ctx.translate(this.x - cam.x + 60, this.y - cam.y + 60);

        ctx.fillStyle = 'rgba(0,0,0,0.6)';
        ctx.filter = 'blur(4px)';
        ctx.beginPath(); ctx.ellipse(0, 65, 55, 15, 0, 0, Math.PI*2); ctx.fill();
        ctx.filter = 'none';

        let scale = 1.2 + Math.sin(this.frame * 0.1) * 0.05;
        if (this.stage === 5) scale *= 1.6; 
        ctx.scale(scale, scale);

        let colors = [
            {b:'#f43f5e', s:'#881337'}, {b:'#38bdf8', s:'#082f49'}, {b:'#22c55e', s:'#14532d'}, 
            {b:'#eab308', s:'#713f12'}, {b:'#111827', s:'#000000'}
        ];
        let color = colors[this.stage - 1];

        if (this.stage >= 3) {
            ctx.shadowColor = (this.stage === 5) ? '#ef4444' : color.b;
            ctx.shadowBlur = 30;
        }

        // リアルなドラゴンの関節尻尾
        ctx.shadowBlur = 0;
        let tailBaseX = 0, tailBaseY = 30;
        for (let i = 1; i <= 10; i++) {
            let tX = tailBaseX + Math.sin(this.frame * 0.05 - i * 0.3) * (20 + i*5);
            let tY = tailBaseY + i * 14;
            let r = Math.max(22 - i * 2, 3);
            
            let gradT = ctx.createRadialGradient(tX-r*0.3, tY-r*0.3, r*0.1, tX, tY, r);
            gradT.addColorStop(0, color.b); gradT.addColorStop(1, color.s);
            ctx.fillStyle = gradT;
            ctx.beginPath(); ctx.arc(tX, tY, r, 0, Math.PI*2); ctx.fill();
            
            if (i % 2 === 0) {
                ctx.fillStyle = '#facc15'; // 尻尾のトゲ
                ctx.beginPath(); ctx.moveTo(tX - r, tY); ctx.lineTo(tX - r - 12, tY - 12); ctx.lineTo(tX, tY - r); ctx.fill();
                ctx.beginPath(); ctx.moveTo(tX + r, tY); ctx.lineTo(tX + r + 12, tY - 12); ctx.lineTo(tX, tY - r); ctx.fill();
            }
        }

        // 超絶リアルなドラゴンの翼
        ctx.fillStyle = (this.stage === 5) ? '#450a0a' : '#1e293b'; 
        ctx.strokeStyle = (this.stage === 5) ? '#fca5a5' : '#94a3b8'; 
        let flap = Math.sin(this.frame * 0.12) * 20;
        
        let drawDragonWing = (dir) => {
            ctx.save(); ctx.scale(dir, 1);
            ctx.lineWidth = 5;
            ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(-50, -50 - flap); ctx.lineTo(-120, -40 - flap * 1.5); ctx.stroke(); 
            ctx.beginPath();
            ctx.moveTo(0, 0); ctx.lineTo(-50, -50 - flap); ctx.lineTo(-120, -40 - flap * 1.5);
            ctx.quadraticCurveTo(-100, 10, -110, 40); ctx.quadraticCurveTo(-80, 30, -60, 50); ctx.quadraticCurveTo(-40, 30, 0, 20);
            ctx.fill();
            ctx.lineWidth = 2; // 翼の骨格(被膜の筋)
            ctx.beginPath(); ctx.moveTo(-50, -50 - flap); ctx.lineTo(-100, 10); ctx.stroke();
            ctx.beginPath(); ctx.moveTo(-50, -50 - flap); ctx.lineTo(-80, 30); ctx.stroke();
            ctx.beginPath(); ctx.moveTo(-50, -50 - flap); ctx.lineTo(-40, 30); ctx.stroke();
            ctx.restore();
        };
        drawDragonWing(1); drawDragonWing(-1);

        // 体(重厚なグラデーション)
        let gradB = ctx.createRadialGradient(-15, -15, 10, 0, 0, 50);
        gradB.addColorStop(0, '#fff'); gradB.addColorStop(0.2, color.b); gradB.addColorStop(1, color.s);
        ctx.fillStyle = gradB;
        ctx.beginPath(); ctx.ellipse(0, 15, 35, 50, 0, 0, Math.PI*2); ctx.fill();

        // 腹の鱗
        ctx.fillStyle = '#fef08a';
        ctx.beginPath(); ctx.ellipse(0, 20, 22, 40, 0, 0, Math.PI*2); ctx.fill();
        ctx.strokeStyle = '#b45309'; ctx.lineWidth = 2;
        for(let i=0; i<6; i++) {
            ctx.beginPath(); ctx.moveTo(-18 + i*2, 5 + i*12); ctx.lineTo(18 - i*2, 5 + i*12); ctx.stroke();
        }

        // 首
        let neckBaseX = 0, neckBaseY = -15;
        let headY = -55 + Math.sin(this.frame * 0.1) * 10;
        let headX = Math.cos(this.frame * 0.05) * 15;
        for(let i=1; i<=4; i++) {
            let nx = neckBaseX + (headX - neckBaseX) * (i/5);
            let ny = neckBaseY + (headY - neckBaseY) * (i/5);
            let rN = 20 - i*2;
            let gradN = ctx.createRadialGradient(nx-rN*0.3, ny-rN*0.3, rN*0.1, nx, ny, rN);
            gradN.addColorStop(0, color.b); gradN.addColorStop(1, color.s);
            ctx.fillStyle = gradN;
            ctx.beginPath(); ctx.arc(nx, ny, rN, 0, Math.PI*2); ctx.fill();
        }

        // 頭
        ctx.save();
        ctx.translate(headX, headY);
        
        let gradHead = ctx.createRadialGradient(-5, -5, 5, 0, 0, 25);
        gradHead.addColorStop(0, color.b); gradHead.addColorStop(1, color.s);
        ctx.fillStyle = gradHead;
        ctx.beginPath();
        ctx.moveTo(-18, -12); ctx.lineTo(18, -12); ctx.lineTo(28, 12); ctx.lineTo(12, 28);
        ctx.lineTo(-12, 28); ctx.lineTo(-28, 12); ctx.closePath();
        ctx.fill();
        
        ctx.fillStyle = (this.stage === 5) ? '#fca5a5' : '#fed7aa';
        ctx.beginPath(); ctx.ellipse(0, 18, 20, 15, 0, 0, Math.PI*2); ctx.fill();
        
        ctx.fillStyle = '#000'; // 鼻
        ctx.beginPath(); ctx.arc(-8, 22, 3, 0, Math.PI*2); ctx.fill();
        ctx.beginPath(); ctx.arc(8, 22, 3, 0, Math.PI*2); ctx.fill();
        
        ctx.fillStyle = '#fff'; // 鋭い牙
        ctx.beginPath(); ctx.moveTo(-14, 30); ctx.lineTo(-8, 40); ctx.lineTo(-2, 30); ctx.fill();
        ctx.beginPath(); ctx.moveTo(14, 30); ctx.lineTo(8, 40); ctx.lineTo(2, 30); ctx.fill();

        ctx.fillStyle = '#facc15'; // 巨大な角
        ctx.beginPath(); ctx.moveTo(-18, -12); ctx.quadraticCurveTo(-40, -40, -55, -25); ctx.quadraticCurveTo(-30, -10, -25, 5); ctx.fill();
        ctx.beginPath(); ctx.moveTo(18, -12); ctx.quadraticCurveTo(40, -40, 55, -25); ctx.quadraticCurveTo(30, -10, 25, 5); ctx.fill();
        if(this.stage === 5) { // 第3の角
            ctx.beginPath(); ctx.moveTo(0, -18); ctx.lineTo(-8, -45); ctx.lineTo(8, -45); ctx.fill();
        }

        // 発光する目
        ctx.shadowBlur = 15; ctx.shadowColor = (this.stage === 5) ? '#ef4444' : '#fff';
        ctx.fillStyle = (this.stage === 5) ? '#ef4444' : '#fff';
        ctx.beginPath(); ctx.ellipse(-12, -2, 8, 5, -Math.PI/6, 0, Math.PI*2); ctx.fill();
        ctx.beginPath(); ctx.ellipse(12, -2, 8, 5, Math.PI/6, 0, Math.PI*2); ctx.fill();
        
        ctx.shadowBlur = 0; ctx.fillStyle = '#000'; // 瞳孔
        ctx.beginPath(); ctx.ellipse(-12, -2, 2, 4, 0, 0, Math.PI*2); ctx.fill();
        ctx.beginPath(); ctx.ellipse(12, -2, 2, 4, 0, 0, Math.PI*2); ctx.fill();
        
        ctx.restore(); 
        ctx.restore();
        ctx.globalCompositeOperation = 'source-over';
    }
}

class Bullet {
    constructor(x, y, dirX, dirY, type, isEnemy = false) {
        this.x = x; this.y = y; this.dirX = dirX; this.dirY = dirY; this.type = type;
        this.isEnemy = isEnemy;
        this.life = 40;
        this.dead = false;
        this.timer = 0;
        
        if (type === 'STAR') this.speed = 12;
        else if (type === 'FIRE' || type === 'ICE') this.speed = 8;
        else if (type === 'CUTTER') this.speed = 15;
        else if (type === 'SPARK') this.speed = 0;
        else if (type === 'SUPER_STAR') this.speed = 14; 
        else this.speed = 10; 
        
        this.vx = this.dirX * this.speed;
        this.vy = this.dirY * this.speed;

        if (type === 'FIRE' || type === 'ICE') {
            this.vx += (Math.random() - 0.5) * 3;
            this.vy += (Math.random() - 0.5) * 3;
        }
        if (type === 'SPARK') this.life = 12;
        if (type === 'SUPER_STAR') this.life = 100;
    }
    update() {
        this.timer++;
        
        if (this.type === 'CUTTER') {
            if (this.dirX !== 0) this.vx -= this.dirX * 0.5; 
            if (this.dirY !== 0) this.vy -= this.dirY * 0.5; 
            this.x += this.vx; this.y += this.vy;
            if (this.timer > 15 && Math.abs(this.x - (game.player.x + 20)) < 30 && Math.abs(this.y - (game.player.y + 20)) < 30) this.dead = true;
            this.life = 100;
            if (this.timer > 100) this.dead = true;
        } else {
            this.x += this.vx; this.y += this.vy;
            this.life--;
            if (this.life <= 0) this.dead = true;
        }

        if (this.isEnemy) {
            let hitbox = (this.type === 'SUPER_STAR') ? 50 : 30;
            if (Math.abs(this.x - (game.player.x + 20)) < hitbox && Math.abs(this.y - (game.player.y + 20)) < hitbox) {
                game.player.takeDamage((this.type === 'SUPER_STAR') ? 2 : 1);
                this.dead = true;
            }
        } else {
            game.enemies.forEach(e => {
                if (e.hp > 0 && Math.abs(this.x - (e.x + 15)) < 40 && Math.abs(this.y - (e.y + 15)) < 40) {
                    e.hp -= (this.type === 'SUPER_STAR') ? 10 : 1;
                    if (this.type !== 'SWORD_WAVE' && this.type !== 'STAR' && this.type !== 'CUTTER' && this.type !== 'SUPER_STAR') this.dead = true;
                    if (e.hp <= 0) {
                        game.score += 200;
                        for(let i=0; i<5; i++) game.particles.push(new Particle(e.x+15, e.y+15, 'STAR_PIECE', (Math.random()-0.5)*10, (Math.random()-0.5)*10));
                    }
                }
            });

            if (game.boss && game.boss.hp > 0) {
                let hitboxSize = (this.type === 'SUPER_STAR') ? 90 : 60;
                if (Math.abs(this.x - (game.boss.x + 60)) < hitboxSize && Math.abs(this.y - (game.boss.y + 60)) < hitboxSize) {
                    game.boss.takeDamage((this.type === 'SUPER_STAR') ? 5 : 1);
                    if (this.type !== 'SWORD_WAVE' && this.type !== 'STAR' && this.type !== 'CUTTER' && this.type !== 'SUPER_STAR') this.dead = true;
                }
            }
        }
    }
    draw(ctx, cam) {
        ctx.save();
        ctx.translate(this.x - cam.x, this.y - cam.y);
        let angle = Math.atan2(this.vy, this.vx);

        // 圧倒的な光(ブルーム)を加算合成で表現
        ctx.globalCompositeOperation = 'lighter';
        
        if (this.type === 'STAR' || this.type === 'SUPER_STAR') {
            ctx.rotate(this.life * 0.4 * (this.vx < 0 ? -1 : 1));
            
            if (this.type === 'SUPER_STAR') {
                ctx.scale(3, 3); 
                let hue = this.life * 20 % 360;
                ctx.fillStyle = `hsl(${hue}, 100%, 70%)`; 
                // 巨大な光のオーラ
                let gradG = ctx.createRadialGradient(0,0,5, 0,0,25);
                gradG.addColorStop(0, `hsl(${hue}, 100%, 80%)`); gradG.addColorStop(1, 'rgba(0,0,0,0)');
                ctx.fillStyle = gradG; ctx.beginPath(); ctx.arc(0,0,25,0,Math.PI*2); ctx.fill();
                ctx.fillStyle = '#fff';
            } else {
                ctx.fillStyle = '#fef08a';
                let gradG = ctx.createRadialGradient(0,0,5, 0,0,20);
                gradG.addColorStop(0, '#facc15'); gradG.addColorStop(1, 'rgba(0,0,0,0)');
                ctx.fillStyle = gradG; ctx.beginPath(); ctx.arc(0,0,20,0,Math.PI*2); ctx.fill();
                ctx.fillStyle = '#fff';
            }
            
            ctx.beginPath();
            for (let i = 0; i < 5; i++) {
                ctx.lineTo(Math.cos((18 + i * 72) / 180 * Math.PI) * 16, -Math.sin((18 + i * 72) / 180 * Math.PI) * 16);
                ctx.lineTo(Math.cos((54 + i * 72) / 180 * Math.PI) * 8, -Math.sin((54 + i * 72) / 180 * Math.PI) * 8);
            }
            ctx.closePath(); ctx.fill();
            
        } else if (this.type === 'CUTTER') {
            ctx.rotate(this.timer * 0.6 * (this.vx < 0 ? -1 : 1));
            let gradG = ctx.createRadialGradient(0,0,5, 0,0,20);
            gradG.addColorStop(0, 'rgba(255,255,255,0.8)'); gradG.addColorStop(1, 'rgba(0,0,0,0)');
            ctx.fillStyle = gradG; ctx.beginPath(); ctx.arc(0,0,20,0,Math.PI*2); ctx.fill();

            ctx.fillStyle = '#e2e8f0'; ctx.lineWidth = 3; ctx.strokeStyle = '#fff';
            ctx.beginPath(); ctx.arc(0, 0, 14, 0, Math.PI, false); ctx.lineTo(0, 6); ctx.closePath();
            ctx.fill(); ctx.stroke();
        } else if (this.type === 'SPARK') {
            ctx.rotate(Math.random() * Math.PI * 2);
            let gradG = ctx.createRadialGradient(0,0,5, 0,0,30);
            gradG.addColorStop(0, '#fef08a'); gradG.addColorStop(1, 'rgba(0,0,0,0)');
            ctx.fillStyle = gradG; ctx.beginPath(); ctx.arc(0,0,30,0,Math.PI*2); ctx.fill();

            ctx.fillStyle = '#fff';
            ctx.beginPath(); ctx.moveTo(0, -20); ctx.lineTo(6, -6); ctx.lineTo(20, 0); ctx.lineTo(6, 6);
            ctx.lineTo(0, 20); ctx.lineTo(-6, 6); ctx.lineTo(-20, 0); ctx.lineTo(-6, -6); ctx.closePath(); ctx.fill();
        } else if (this.type === 'SWORD_WAVE') {
            ctx.rotate(angle);
            let gradG = ctx.createRadialGradient(0,0,10, 0,0,30);
            gradG.addColorStop(0, '#4ade80'); gradG.addColorStop(1, 'rgba(0,0,0,0)');
            ctx.fillStyle = gradG; ctx.beginPath(); ctx.arc(0,0,30,0,Math.PI*2); ctx.fill();

            ctx.fillStyle = '#fff';
            ctx.beginPath(); ctx.arc(0, 0, 25, -Math.PI/2, Math.PI/2, false); ctx.quadraticCurveTo(15, 0, 0, -25); ctx.fill();
        } else if (this.type === 'FIRE' || this.type === 'ICE') {
            let color = (this.type === 'FIRE') ? '#f43f5e' : '#38bdf8';
            let gradG = ctx.createRadialGradient(0,0,5, 0,0,25);
            gradG.addColorStop(0, color); gradG.addColorStop(1, 'rgba(0,0,0,0)');
            ctx.fillStyle = gradG; ctx.beginPath(); ctx.arc(0,0,25,0,Math.PI*2); ctx.fill();
            ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.arc(0,0, 10, 0, Math.PI*2); ctx.fill();
        } else {
            ctx.fillStyle = 'rgba(255,255,255,0.8)';
            ctx.beginPath(); ctx.arc(0,0, 12, 0, Math.PI*2); ctx.fill();
        }
        ctx.restore();
    }
}

class Particle {
    constructor(x, y, type, vx = 0, vy = 0) {
        this.x = x; this.y = y; this.type = type; 
        this.life = (type === 'ENV') ? Math.random()*100 + 50 : 25; 
        this.maxLife = this.life;
        this.vx = vx; this.vy = vy;
        // 環境パーティクル用
        if (type === 'ENV') {
            this.vx = (Math.random() - 0.5) * 2;
            this.vy = (Math.random() - 0.5) * 2 - 1; // やや上に昇る
            this.size = Math.random() * 4 + 1;
        }
    }
    update() { 
        this.life--; 
        this.x += this.vx; this.y += this.vy; 
        if (this.type === 'STAR_PIECE') {
            this.vy += GRAVITY * 0.5; // 重力
            this.vx *= 0.95; // 空気抵抗
        }
    }
    draw(ctx, cam) {
        let alpha = this.life / this.maxLife;
        ctx.save();
        ctx.globalAlpha = alpha;
        ctx.translate(this.x - cam.x, this.y - cam.y);
        ctx.globalCompositeOperation = 'lighter';
        
        if (this.type === 'HIT') {
            ctx.rotate(this.life * 0.4);
            ctx.fillStyle = '#fde047';
            ctx.beginPath(); ctx.moveTo(0, -20); ctx.lineTo(5, -5); ctx.lineTo(20, 0); ctx.lineTo(5, 5);
            ctx.lineTo(0, 20); ctx.lineTo(-5, 5); ctx.lineTo(-20, 0); ctx.lineTo(-5, -5); ctx.fill();
        } else if (this.type === 'STAR_PIECE') {
            ctx.rotate(this.life * 0.5);
            ctx.fillStyle = `hsl(${Math.random()*60 + 30}, 100%, 70%)`; // 黄色〜オレンジ
            ctx.beginPath();
            for (let i = 0; i < 5; i++) {
                ctx.lineTo(Math.cos((18 + i * 72) / 180 * Math.PI) * 8, -Math.sin((18 + i * 72) / 180 * Math.PI) * 8);
                ctx.lineTo(Math.cos((54 + i * 72) / 180 * Math.PI) * 4, -Math.sin((54 + i * 72) / 180 * Math.PI) * 4);
            }
            ctx.closePath(); ctx.fill();
        } else if (this.type === 'ENV') {
            // ステージに応じた環境光の粒(STAGE 5は火の粉、他はホコリ/妖精の粉)
            ctx.fillStyle = (game.stage === 5) ? '#ef4444' : '#7dd3fc';
            ctx.beginPath(); ctx.arc(0, 0, this.size, 0, Math.PI*2); ctx.fill();
            let grad = ctx.createRadialGradient(0,0,0, 0,0,this.size*3);
            grad.addColorStop(0, ctx.fillStyle); grad.addColorStop(1, 'rgba(0,0,0,0)');
            ctx.fillStyle = grad; ctx.beginPath(); ctx.arc(0, 0, this.size*3, 0, Math.PI*2); ctx.fill();
        } else {
            ctx.fillStyle = (this.type === 'COPY_FLASH') ? '#fff' : 'rgba(255,255,255,0.6)';
            let size = (this.type === 'COPY_FLASH') ? (25-this.life)*30 : 6;
            ctx.beginPath(); ctx.arc(0, 0, size, 0, Math.PI*2); ctx.fill();
        }
        ctx.restore();
    }
}

class Item {
    constructor(x, y, ability) {
        this.x = x; this.y = y; this.w = 30; this.h = 30;
        this.ability = ability; this.frame = 0;
    }
    update() {
        this.frame++;
        if (Math.abs(this.x + 15 - (game.player.x + 20)) < 30 && Math.abs(this.y + 15 - (game.player.y + 20)) < 30) {
            if (keys['ArrowDown'] || keys['t-down']) {
                if (game.player.ability !== this.ability) {
                    game.player.ability = this.ability;
                    updateAbilityUI(this.ability);
                    game.particles.push(new Particle(game.player.x + 20, game.player.y + 20, 'COPY_FLASH'));
                    game.camera.shake(6, 10);
                }
            }
        }
    }
    draw(ctx, cam) {
        ctx.save();
        let floatY = Math.sin(this.frame * 0.08) * 5;
        ctx.translate(this.x - cam.x + 15, this.y - cam.y + 15 + floatY);
        
        // リッチな台座
        ctx.fillStyle = '#64748b'; ctx.fillRect(-14, 10 - floatY, 28, 8);
        ctx.fillStyle = '#334155'; ctx.fillRect(-18, 18 - floatY, 36, 6);
        ctx.strokeStyle = '#94a3b8'; ctx.lineWidth=2; ctx.strokeRect(-18, 18 - floatY, 36, 6);

        let color = '#fff';
        if (this.ability === 'FIRE') color = '#f43f5e';
        else if (this.ability === 'SWORD') color = '#22c55e';
        else if (this.ability === 'ICE') color = '#38bdf8';
        else if (this.ability === 'CUTTER') color = '#facc15';
        else if (this.ability === 'SPARK') color = '#a3e635';
        else if (this.ability === 'SUPER') color = `hsl(${this.frame * 15 % 360}, 100%, 60%)`;

        // 発光するアイコン
        ctx.globalCompositeOperation = 'lighter';
        let grad = ctx.createRadialGradient(0,0,5, 0,0,25);
        grad.addColorStop(0, color); grad.addColorStop(1, 'rgba(0,0,0,0)');
        ctx.fillStyle = grad; ctx.beginPath(); ctx.arc(0,0,25,0,Math.PI*2); ctx.fill();
        ctx.globalCompositeOperation = 'source-over';

        ctx.fillStyle = color;
        ctx.beginPath(); ctx.arc(0, 0, 12, 0, Math.PI*2); ctx.fill();
        ctx.lineWidth = 2; ctx.strokeStyle = '#fff'; ctx.stroke();
        
        ctx.fillStyle = '#fff';
        ctx.beginPath();
        for (let i = 0; i < 5; i++) {
            ctx.lineTo(Math.cos((18 + i * 72) / 180 * Math.PI) * 6, -Math.sin((18 + i * 72) / 180 * Math.PI) * 6);
            ctx.lineTo(Math.cos((54 + i * 72) / 180 * Math.PI) * 3, -Math.sin((54 + i * 72) / 180 * Math.PI) * 3);
        }
        ctx.closePath(); ctx.fill();

        ctx.fillStyle = 'rgba(255,255,255,0.9)';
        ctx.font = '900 18px sans-serif'; ctx.textAlign = 'center';
        ctx.fillText("↓", 0, -20);

        ctx.restore();
    }
}

const game = {
    player: new Kirby(100, 300),
    camera: new Camera(),
    score: 0,
    stage: 1,
    maxStage: 5, 
    goal: null,
    boss: null,
    map: { w: 10000, h: 540, blocks: [] },
    enemies: [], bullets: [], particles: [], items: [], envParticles: [],
    remotePlayers: {}, userId: null, db: null,
    appId: typeof __app_id !== 'undefined' ? __app_id : 'modern-kirby-123',

    init() {
        this.stage = 1; this.score = 0;
        this.player = new Kirby(100, 100, this.selectedColor || 'pink');
        this.loadStage(this.stage);
        this.initMultiplayer();
    },

    loadStage(stageNum) {
        this.map.blocks = []; this.enemies = []; this.bullets = []; 
        this.particles = []; this.items = []; this.envParticles = [];
        this.boss = null;
        this.player.x = 100; this.player.y = 100; this.player.vx = 0; this.player.vy = 0;
        
        if (stageNum !== 5) this.map.blocks.push({ x: 0, y: 450, w: 20000, h: 100 }); 
        else this.map.blocks.push({ x: 0, y: 600, w: 20000, h: 50 }); 
        
        let mapLength = 3000 + stageNum * 1500;
        this.map.w = mapLength + 1000;
        
        // 環境パーティクルの初期化
        for(let i=0; i<50; i++) {
            this.envParticles.push(new Particle(Math.random()*this.map.w, Math.random()*this.map.h, 'ENV'));
        }
        
        const enemyTypes = ['WADDLE', 'WADDLE', 'WADDLE', 'WADDLE', 'WADDLE', 'FIRE_DUDE', 'SWORD_DUDE', 'ICE_DUDE', 'CUTTER_DUDE', 'SPARK_DUDE'];
        
        if (stageNum === 1) {
            for(let i=1; i<mapLength/600; i++) {
                let cx = i * 600;
                if (i % 2 === 0) this.map.blocks.push({ x: cx, y: 330, w: 150, h: 30 });
                else this.map.blocks.push({ x: cx, y: 380, w: 100, h: 70 });
                this.enemies.push(new Enemy(cx, 100, enemyTypes[Math.floor(Math.random() * enemyTypes.length)]));
                if (Math.random() < 0.3) this.enemies.push(new Enemy(cx + 100, 150 + Math.random() * 100, 'FLYING_DUDE'));
                if (Math.random() < 0.1) this.enemies.push(new Enemy(cx + 200, 100, 'SUPER_DUDE'));
            }
        } else if (stageNum === 2) {
            for(let i=1; i<mapLength/500; i++) {
                let cx = i * 500;
                this.map.blocks.push({ x: cx, y: 350 + (i%3)*40 - 50, w: 120, h: 30 });
                this.map.blocks.push({ x: cx + 250, y: 250 + (i%2)*50, w: 100, h: 30 });
                this.enemies.push(new Enemy(cx, 100, enemyTypes[Math.floor(Math.random() * enemyTypes.length)]));
                if(i%3===0) this.enemies.push(new Enemy(cx+250, 100, enemyTypes[Math.floor(Math.random() * enemyTypes.length)]));
                if (Math.random() < 0.4) this.enemies.push(new Enemy(cx + 150, 100 + Math.random() * 100, 'FLYING_DUDE'));
                if (Math.random() < 0.15) this.enemies.push(new Enemy(cx + 300, 100, 'SUPER_DUDE'));
            }
        } else if (stageNum === 3) {
            for(let i=1; i<mapLength/400; i++) {
                let cx = i * 400;
                if(i%4===0) this.map.blocks.push({ x: cx, y: 200, w: 60, h: 250 }); 
                else this.map.blocks.push({ x: cx, y: 300, w: 150, h: 30 });
                this.enemies.push(new Enemy(cx + 50, 100, enemyTypes[Math.floor(Math.random() * enemyTypes.length)]));
                this.enemies.push(new Enemy(cx + 250, 100, enemyTypes[Math.floor(Math.random() * enemyTypes.length)]));
                if (Math.random() < 0.5) this.enemies.push(new Enemy(cx + 100, 150 + Math.random() * 150, 'FLYING_DUDE'));
                if (Math.random() < 0.1) this.enemies.push(new Enemy(cx + 150, 100, 'SUPER_DUDE'));
            }
        } else if (stageNum === 4) {
            for(let i=1; i<mapLength/600; i++) {
                let cx = i * 600;
                this.map.blocks.push({ x: cx, y: 350 + (i%2)*50, w: 120, h: 30 });
                this.map.blocks.push({ x: cx + 300, y: 200 + (i%3)*50, w: 100, h: 30 });
                this.enemies.push(new Enemy(cx, 100, enemyTypes[Math.floor(Math.random() * enemyTypes.length)]));
                if (Math.random() < 0.5) this.enemies.push(new Enemy(cx + 300, 100, 'FLYING_DUDE'));
                if (Math.random() < 0.2) this.enemies.push(new Enemy(cx + 150, 100, 'SUPER_DUDE'));
            }
        } else if (stageNum === 5) {
            for(let i=1; i<mapLength/400; i++) {
                let cx = i * 400;
                this.map.blocks.push({ x: cx, y: 350 + (i%3)*40, w: 140, h: 30 });
                this.enemies.push(new Enemy(cx + 50, 100, enemyTypes[Math.floor(Math.random() * enemyTypes.length)]));
                if (Math.random() < 0.6) this.enemies.push(new Enemy(cx + 100, 100, 'FLYING_DUDE'));
                if (Math.random() < 0.2) this.enemies.push(new Enemy(cx + 50, 100, 'SUPER_DUDE'));
            }
            this.map.blocks.push({ x: mapLength - 800, y: 350, w: 1000, h: 200 });
            
            this.items.push(new Item(mapLength - 750, 300, 'FIRE'));
            this.items.push(new Item(mapLength - 650, 300, 'ICE'));
            this.items.push(new Item(mapLength - 550, 300, 'SWORD'));
            this.items.push(new Item(mapLength - 450, 300, 'SPARK'));
            this.items.push(new Item(mapLength - 350, 300, 'CUTTER'));
            this.items.push(new Item(mapLength - 250, 300, 'SUPER')); 
        }
        
        this.goal = { x: mapLength, y: 350, w: 80, h: 100 };
        this.boss = new Boss(mapLength - 200, 200, stageNum);
        document.getElementById('stage-text').innerText = `STAGE ${stageNum}`;
    },

    async initMultiplayer() {
        if (typeof __firebase_config === 'undefined') return;
        const app = initializeApp(JSON.parse(__firebase_config));
        const auth = getAuth(app); this.db = getFirestore(app);
        
        if (typeof __initial_auth_token !== 'undefined' && __initial_auth_token) await signInWithCustomToken(auth, __initial_auth_token);
        else await signInAnonymously(auth);

        onAuthStateChanged(auth, (user) => {
            if (user) { this.userId = user.uid; this.listenPlayers(); }
        });
    },

    listenPlayers() {
        const ref = collection(this.db, 'artifacts', this.appId, 'public', 'data', 'players');
        onSnapshot(ref, (snap) => {
            snap.docChanges().forEach(change => {
                if (change.doc.id === this.userId) return;
                if (change.type === 'removed') delete this.remotePlayers[change.doc.id];
                else this.remotePlayers[change.doc.id] = change.doc.data();
            });
            let count = 1;
            for(let id in this.remotePlayers) if(this.remotePlayers[id].stage === this.stage) count++;
            const onlineCountEl = document.getElementById('online-count');
            if (onlineCountEl) onlineCountEl.innerText = count;
        }, (error) => {
            console.error("Firebase Snapshot Error:", error);
        });
    },

    sync() {
        if (!this.userId) return;
        const ref = doc(this.db, 'artifacts', this.appId, 'public', 'data', 'players', this.userId);
        setDoc(ref, {
            x: this.player.x, y: this.player.y, ability: this.player.ability, facing: this.player.facing,
            stage: this.stage, colorType: this.player.colorType, t: Date.now()
        }, { merge: true });
    },

    spawnBullet(x, y, dirX, dirY, type, isEnemy = false) {
        this.bullets.push(new Bullet(x, y, dirX, dirY, type, isEnemy));
    },

    reset() {
        this.player.hp = 6; this.player.ability = 'NORMAL';
        updateAbilityUI('NORMAL'); this.loadStage(this.stage); 
    },

    handleStageClear() {
        gameState = 'CLEAR';
        if (this.stage >= this.maxStage) {
            document.getElementById('game-clear-screen').style.display = 'flex';
            document.getElementById('touch-ui').style.display = 'none';
        } else {
            const clearScreen = document.getElementById('stage-clear-screen');
            clearScreen.style.display = 'flex';
            setTimeout(() => {
                this.stage++; this.player.hp = this.player.maxHp;
                this.loadStage(this.stage);
                clearScreen.style.display = 'none'; gameState = 'PLAYING';
            }, 3000);
        }
    },

    loop() {
        if (gameState !== 'PLAYING') {
            if (gameState === 'CLEAR') {
                this.player.vx *= 0.8; this.player.x += this.player.vx;
                this.draw(); requestAnimationFrame(() => this.loop());
            }
            return;
        }

        if (this.goal && (!this.boss || this.boss.hp <= 0) && this.player.x > this.goal.x && this.player.x < this.goal.x + this.goal.w && this.player.onGround) {
            this.handleStageClear(); requestAnimationFrame(() => this.loop()); return;
        }

        this.player.update();
        this.camera.update(this.player.x, this.player.y, this.map.w, this.map.h, this.boss);

        if (this.boss) {
            this.boss.update();
            if (this.boss.hp > 0 && Math.abs((this.boss.x + 60) - (this.player.x + 20)) < 70 && Math.abs((this.boss.y + 60) - (this.player.y + 20)) < 70) this.player.takeDamage(1);
        }

        this.enemies = this.enemies.filter(e => e.hp > 0);
        this.enemies.forEach(e => {
            e.update();
            if (Math.abs(e.x - this.player.x) < 30 && Math.abs(e.y - this.player.y) < 30) this.player.takeDamage(1);
        });

        this.bullets = this.bullets.filter(b => !b.dead);
        this.bullets.forEach(b => b.update());

        this.particles = this.particles.filter(p => p.life > 0);
        this.particles.forEach(p => p.update());

        // 環境パーティクルの補充
        this.envParticles = this.envParticles.filter(p => p.life > 0);
        this.envParticles.forEach(p => { p.update(); p.x += this.camera.shakeX * 0.5; }); // カメラ揺れに少し追従
        while(this.envParticles.length < 50) {
            this.envParticles.push(new Particle(this.camera.x + Math.random()*canvas.width, this.camera.y + Math.random()*canvas.height, 'ENV'));
        }

        this.items.forEach(i => i.update()); 

        if (this.player.frame % 10 === 0) this.sync();

        if (this.boss && this.boss.hp > 0) {
            document.getElementById('boss-hp-container').style.display = 'block';
            document.getElementById('boss-hp-fill').style.width = `${(this.boss.hp / this.boss.maxHp) * 100}%`;
        } else {
            document.getElementById('boss-hp-container').style.display = 'none';
        }

        this.draw();
        requestAnimationFrame(() => this.loop());
    },

    draw() {
        ctx.clearRect(0,0, canvas.width, canvas.height);

        if (this.stage === 5) {
            let gradBg = ctx.createLinearGradient(0, 0, 0, canvas.height);
            gradBg.addColorStop(0, '#1a0505'); gradBg.addColorStop(1, '#7f1d1d'); 
            ctx.fillStyle = gradBg; ctx.fillRect(0,0, canvas.width, canvas.height);
            
            // 超絶リアルな活火山(多重スクロール)
            ctx.fillStyle = '#290000';
            for(let i=0; i<15; i++) {
                let mx = (i * 300 - this.camera.baseX * 0.15) % 3000;
                if(mx < -400) mx += 3000;
                ctx.beginPath(); ctx.moveTo(mx, 600 - this.camera.y); ctx.lineTo(mx + 150, 100 - this.camera.y + (i%3)*50); ctx.lineTo(mx + 300, 600 - this.camera.y); ctx.fill();
                ctx.strokeStyle = '#dc2626'; ctx.lineWidth = 4;
                ctx.beginPath(); ctx.moveTo(mx+150, 100 - this.camera.y + (i%3)*50); ctx.lineTo(mx+130+i*10, 300 - this.camera.y); ctx.stroke();
                // 火山の煙
                ctx.globalCompositeOperation = 'lighter';
                ctx.fillStyle = 'rgba(239,68,68,0.1)';
                ctx.beginPath(); ctx.arc(mx+150, 50 - this.camera.y + (i%3)*50, 80, 0, Math.PI*2); ctx.fill();
                ctx.globalCompositeOperation = 'source-over';
            }

            // マグマの海(ブルーム効果付き)
            ctx.fillStyle = '#991b1b'; ctx.fillRect(0, 550 - this.camera.y, canvas.width, canvas.height);
            ctx.fillStyle = '#f97316'; ctx.shadowBlur = 30; ctx.shadowColor = '#facc15';
            for(let i=0; i<30; i++) {
                let wx = (i * 60 - this.camera.x * 0.6) % 1500;
                if (wx < -100) wx += 1500;
                ctx.beginPath(); ctx.ellipse(wx, 550 - this.camera.y + Math.sin(this.player.frame*0.05 + i)*15, 60, 20, 0, Math.PI, false); ctx.fill();
            }
            ctx.shadowBlur = 0;
            
            // マグマの照り返しフォグ
            let gradFog = ctx.createLinearGradient(0, canvas.height, 0, canvas.height - 300);
            gradFog.addColorStop(0, 'rgba(239,68,68,0.3)'); gradFog.addColorStop(1, 'rgba(239,68,68,0)');
            ctx.fillStyle = gradFog; ctx.fillRect(0,0, canvas.width, canvas.height);

        } else {
            let gradBg = ctx.createLinearGradient(0, 0, 0, canvas.height);
            gradBg.addColorStop(0, '#38bdf8'); gradBg.addColorStop(1, '#f0f9ff');
            ctx.fillStyle = gradBg; ctx.fillRect(0,0, canvas.width, canvas.height);

            // 美しい連なる山脈と大気フォグ
            for(let layer=0; layer<2; layer++) {
                ctx.fillStyle = layer===0 ? '#94a3b8' : '#cbd5e1';
                let speed = layer===0 ? 0.05 : 0.1;
                for(let i=0; i<20; i++) {
                    let mx = (i * 200 - this.camera.baseX * speed) % 3000;
                    if(mx < -300) mx += 3000;
                    ctx.beginPath(); ctx.moveTo(mx, 500 - this.camera.y); ctx.lineTo(mx + 150, 200 - this.camera.y + layer*50 + (i%4)*40); ctx.lineTo(mx + 300, 500 - this.camera.y); ctx.fill();
                }
                // レイヤー間の空気の層(フォグ)
                ctx.fillStyle = 'rgba(240,249,255,0.4)'; ctx.fillRect(0,0, canvas.width, canvas.height);
            }

            ctx.fillStyle = 'rgba(255,255,255,0.9)'; ctx.shadowBlur = 20; ctx.shadowColor = '#fff';
            for(let i=0; i<15; i++) {
                let cx = (i * 250 - this.camera.x * 0.2) % 2000;
                if(cx < -100) cx += 2000;
                ctx.beginPath(); ctx.arc(cx, 100 + (i%3)*40, 40, 0, Math.PI*2); ctx.arc(cx+40, 120 + (i%3)*40, 30, 0, Math.PI*2); ctx.arc(cx-40, 120 + (i%3)*40, 30, 0, Math.PI*2); ctx.fill();
            }
            ctx.shadowBlur = 0;
        }
        
        // 環境パーティクルの描画(奥)
        this.envParticles.forEach(p => p.draw(ctx, this.camera));

        // マップブロックの超リアル描画
        this.map.blocks.forEach(b => {
            let bx = b.x - this.camera.x; let by = b.y - this.camera.y;
            if (this.stage === 5) {
                ctx.fillStyle = '#0f0a0a'; ctx.fillRect(bx, by, b.w, b.h);
                let gradEdge = ctx.createLinearGradient(0, by, 0, by + 10);
                gradEdge.addColorStop(0, '#f87171'); gradEdge.addColorStop(1, '#7f1d1d');
                ctx.fillStyle = gradEdge; ctx.fillRect(bx, by, b.w, 10);
                ctx.strokeStyle = '#dc2626'; ctx.lineWidth = 2; ctx.strokeRect(bx, by, b.w, b.h);
            } else {
                ctx.fillStyle = '#713f12'; ctx.fillRect(bx, by, b.w, b.h);
                // リアルな草のグラデーション
                let gradGrass = ctx.createLinearGradient(0, by, 0, by + 20);
                gradGrass.addColorStop(0, '#86efac'); gradGrass.addColorStop(0.5, '#22c55e'); gradGrass.addColorStop(1, '#14532d');
                ctx.fillStyle = gradGrass; ctx.fillRect(bx, by, b.w, 20);
                ctx.strokeStyle = '#000'; ctx.lineWidth = 3; ctx.strokeRect(bx, by, b.w, b.h);
            }
        });

        if (this.goal && (!this.boss || this.boss.hp <= 0)) {
            let gx = this.goal.x - this.camera.x; let gy = this.goal.y - this.camera.y;
            ctx.fillStyle = '#0f172a'; ctx.fillRect(gx, gy, this.goal.w, this.goal.h);
            ctx.fillStyle = '#020617'; ctx.fillRect(gx + 10, gy + 10, this.goal.w - 20, this.goal.h - 10);
            
            ctx.save(); ctx.translate(gx + this.goal.w/2, gy + this.goal.h/2);
            ctx.shadowBlur = 20; ctx.shadowColor = '#facc15';
            ctx.fillStyle = '#fef08a'; ctx.beginPath();
            for (let i = 0; i < 5; i++) {
                ctx.lineTo(Math.cos((18 + i * 72) / 180 * Math.PI) * 20, -Math.sin((18 + i * 72) / 180 * Math.PI) * 20);
                ctx.lineTo(Math.cos((54 + i * 72) / 180 * Math.PI) * 10, -Math.sin((54 + i * 72) / 180 * Math.PI) * 10);
            }
            ctx.closePath(); ctx.fill(); ctx.restore();
        }

        if (this.boss) this.boss.draw(ctx, this.camera);
        this.enemies.forEach(e => e.draw(ctx, this.camera));
        this.items.forEach(i => i.draw(ctx, this.camera)); 
        
        for (let id in this.remotePlayers) {
            let p = this.remotePlayers[id];
            if (p.stage !== this.stage) continue; 
            const cColor = CHAR_COLORS[p.colorType] || CHAR_COLORS['pink'];
            ctx.save(); ctx.globalAlpha = 0.5; ctx.translate(p.x - this.camera.x + 20, p.y - this.camera.y + 20);
            ctx.fillStyle = cColor.body; ctx.beginPath(); ctx.arc(0,0,20,0,Math.PI*2); ctx.fill();
            ctx.fillStyle = cColor.feet; ctx.beginPath(); ctx.ellipse(-8, 16, 9, 5, 0, 0, Math.PI*2); ctx.fill();
            ctx.beginPath(); ctx.ellipse(8, 16, 9, 5, 0, 0, Math.PI*2); ctx.fill();
            ctx.restore();
        }

        this.player.draw(ctx, this.camera);
        this.bullets.forEach(b => b.draw(ctx, this.camera));
        this.particles.forEach(p => p.draw(ctx, this.camera));
    }
};

window.selectChar = (color) => {
    document.querySelectorAll('.char-btn').forEach(b => b.classList.remove('selected'));
    document.querySelector(`.char-btn[data-color="${color}"]`).classList.add('selected');
};

window.startGame = () => {
    const selectedColor = document.querySelector('.char-btn.selected').dataset.color;
    game.selectedColor = selectedColor;
    document.getElementById('title-screen').style.display = 'none';
    document.getElementById('hud').style.display = 'flex';
    
    // 修正: タッチ対応端末であれば、画面の幅(縦持ち・横持ち)に関わらずコントローラーを表示する
    if ('ontouchstart' in window || navigator.maxTouchPoints > 0) {
        document.getElementById('touch-ui').style.display = 'flex';
    }
    
    gameState = 'PLAYING';
    game.init(); game.loop();
};

function updateAbilityUI(ability) {
    const text = document.getElementById('ability-text');
    const icon = document.getElementById('ability-icon');
    text.innerText = ability;
    if (ability === 'FIRE') icon.style.background = 'radial-gradient(circle, #f43f5e, #9f1239)';
    else if (ability === 'SWORD') icon.style.background = 'radial-gradient(circle, #22c55e, #14532d)';
    else if (ability === 'ICE') icon.style.background = 'radial-gradient(circle, #38bdf8, #075985)';
    else if (ability === 'CUTTER') icon.style.background = 'radial-gradient(circle, #cbd5e1, #334155)';
    else if (ability === 'SPARK') icon.style.background = 'radial-gradient(circle, #eab308, #854d0e)';
    else if (ability === 'SUPER') icon.style.background = 'linear-gradient(45deg, #f87171, #facc15, #4ade80, #38bdf8, #c084fc)';
    else icon.style.background = 'rgba(0,0,0,0.4)';
}

window.addEventListener('keydown', e => {
    keys[e.key] = true;
    if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', ' ', 'Shift'].includes(e.key)) e.preventDefault(); 
});
window.addEventListener('keyup', e => keys[e.key] = false);

const touchBtns = ['t-left', 't-right', 't-up', 't-down', 't-z', 't-x', 't-shift'];
touchBtns.forEach(id => {
    const el = document.getElementById(id);
    if (!el) return;
    el.addEventListener('touchstart', e => { e.preventDefault(); keys[id] = true; }, { passive: false });
    el.addEventListener('touchend', e => { e.preventDefault(); keys[id] = false; }, { passive: false });
    // 修正: 指がボタンの外にスライドして離れた場合などにも確実に入力をリセットする
    el.addEventListener('touchcancel', e => { e.preventDefault(); keys[id] = false; }, { passive: false });
});

</script>
</body>
</html>

カービー

カービー

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