🔒 サンドボックス内で実行中 ⛶ 全画面で遊ぶ
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>怪獣ラン&デストロイ - 激闘編 -</title>
    <style>
        body, html {
            margin: 0;
            padding: 0;
            width: 100%;
            height: 100%;
            background-color: #050505;
            overflow: hidden;
            font-family: 'Courier New', Courier, monospace;
            touch-action: none;
            user-select: none;
            -webkit-user-select: none;
        }
        #gameCanvas {
            display: block;
            width: 100%;
            height: 100%;
        }
        #ui-layer {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            pointer-events: none;
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;
            color: white;
            text-shadow: 2px 2px 0 #000;
        }
        .hidden {
            display: none !important;
        }
        #start-screen, #game-over-screen {
            background: rgba(0, 0, 0, 0.85);
            padding: 30px;
            border: 2px solid #555;
            border-radius: 5px;
            text-align: center;
            pointer-events: auto;
            box-shadow: 0 0 20px rgba(0, 255, 255, 0.3);
            z-index: 10;
        }
        h1 {
            color: #00ffff;
            font-size: 2.5rem;
            margin-top: 0;
            text-transform: uppercase;
            letter-spacing: 5px;
            text-shadow: 0 0 15px #00ffff;
        }
        p {
            font-size: 1.1rem;
            line-height: 1.5;
            margin-bottom: 20px;
            color: #ddd;
        }
        .btn {
            background-color: #005555;
            color: white;
            border: 1px solid #00ffff;
            padding: 15px 40px;
            font-size: 1.5rem;
            font-weight: bold;
            cursor: pointer;
            border-radius: 3px;
            transition: all 0.2s;
            font-family: inherit;
            text-transform: uppercase;
            letter-spacing: 2px;
        }
        .btn:hover {
            background-color: #00ffff;
            color: black;
            box-shadow: 0 0 20px #00ffff;
        }
        .btn:active {
            transform: scale(0.95);
        }
        .controls-info {
            display: flex;
            justify-content: space-around;
            margin-top: 20px;
            font-size: 0.9rem;
            color: #aaa;
            text-align: left;
            gap: 20px;
        }
        .key {
            background: #222;
            padding: 5px 10px;
            border-radius: 3px;
            border: 1px solid #444;
            color: #fff;
            display: inline-block;
            margin-bottom: 5px;
        }
        #hud {
            position: absolute;
            top: 20px;
            left: 20px;
            font-size: 1.5rem;
            font-weight: bold;
            color: white;
            text-shadow: 2px 2px 0 #000;
            pointer-events: none;
            display: flex;
            justify-content: space-between;
            width: calc(100% - 40px);
            z-index: 5;
        }
        .hp-bar-container {
            width: 250px;
            height: 15px;
            background: #222;
            border: 2px solid #555;
            margin-top: 8px;
            box-shadow: 0 0 5px #000;
        }
        #hp-bar {
            width: 100%;
            height: 100%;
            background: #00ff00;
            transition: width 0.2s, background-color 0.2s;
        }
        .charge-bar-container {
            width: 250px;
            height: 8px;
            background: #222;
            border: 1px solid #444;
            margin-top: 5px;
            position: relative;
        }
        #charge-bar {
            width: 100%;
            height: 100%;
            background: #b000ff;
            box-shadow: 0 0 10px #b000ff;
        }
        .infinite-text {
            position: absolute;
            top: -2px;
            left: 5px;
            font-size: 0.6rem;
            color: black;
            font-weight: bold;
            text-shadow: none;
            letter-spacing: 2px;
        }
        /* スマホ用仮想ボタン */
        #mobile-controls {
            position: absolute;
            bottom: 30px;
            left: 0;
            width: 100%;
            display: flex;
            justify-content: space-between;
            align-items: flex-end;
            padding: 0 10px;
            box-sizing: border-box;
            pointer-events: none;
            z-index: 5;
        }
        .dpad {
            display: flex;
            flex-direction: column;
            align-items: center;
            gap: 5px;
        }
        .dpad-row {
            display: flex;
            gap: 5px;
        }
        .ctrl-group {
            display: flex;
            gap: 5px;
        }
        .ctrl-btn {
            pointer-events: auto;
            background: rgba(40, 40, 40, 0.7);
            border: 2px solid rgba(255, 255, 255, 0.5);
            color: white;
            font-size: 1.5rem;
            width: 50px;
            height: 50px;
            border-radius: 50%;
            display: flex;
            justify-content: center;
            align-items: center;
            touch-action: manipulation;
            box-shadow: 0 4px 6px rgba(0,0,0,0.5);
        }
        .ctrl-btn.active {
            background: rgba(0, 255, 255, 0.8);
            border-color: #fff;
            transform: scale(0.95);
            box-shadow: 0 1px 3px rgba(0,0,0,0.5);
        }
        .btn-text {
            font-size: 1rem;
            font-weight: bold;
        }
        @media (min-width: 768px) {
            #mobile-controls { display: none !important; }
        }
    </style>
</head>
<body>

    <canvas id="gameCanvas"></canvas>

    <div id="hud" class="hidden">
        <div>
            SCORE: <span id="score-display">0</span>
            <div class="hp-bar-container">
                <div id="hp-bar"></div>
            </div>
            <div class="charge-bar-container">
                <div id="charge-bar"></div>
                <div class="infinite-text">INFINITE BEAM</div>
            </div>
        </div>
    </div>

    <!-- スマホ向け仮想コントローラー -->
    <div id="mobile-controls" class="hidden">
        <div class="dpad">
            <div class="ctrl-btn" id="btn-up">🔼</div>
            <div class="dpad-row">
                <div class="ctrl-btn" id="btn-left">◀️</div>
                <div class="ctrl-btn" id="btn-down">🔽</div>
                <div class="ctrl-btn" id="btn-right">▶️</div>
            </div>
        </div>
        <div class="ctrl-group">
            <div class="ctrl-btn" id="btn-kick">🦵</div>
            <div class="ctrl-btn" id="btn-punch">👊</div>
            <div class="ctrl-btn" id="btn-beam">🔥</div>
            <div class="ctrl-btn btn-text" id="btn-jump">JUMP</div>
        </div>
    </div>

    <div id="ui-layer">
        <div id="start-screen">
            <h1>怪獣ラン&デストロイ</h1>
            <p>※音が出ます<br>ビームの角度を変えて敵を撃ち落とせ!</p>
            <div class="controls-info">
                <div>
                    <span class="key">←</span> <span class="key">→</span> : 移動<br>
                    <span class="key">↑</span> <span class="key">↓</span> : ビームの角度調整<br>
                    <span class="key">Space</span> : ジャンプ
                </div>
                <div>
                    <span class="key">X</span> : パンチ<br>
                    <span class="key">C</span> : キック<br>
                    <span class="key">Z</span> : 熱線 <span style="color:#b000ff; font-weight:bold;">(長押しで無限放射)</span>
                </div>
            </div>
            <br>
            <button class="btn" id="start-btn">破壊を開始する</button>
        </div>

        <div id="game-over-screen" class="hidden">
            <h1>GAME OVER</h1>
            <p>最終スコア: <span id="final-score">0</span></p>
            <button class="btn" id="restart-btn">再び目覚める</button>
        </div>
    </div>

<script>
// --- ... existing setup and audio code ... ---
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d', { alpha: false });

const startScreen = document.getElementById('start-screen');
const gameOverScreen = document.getElementById('game-over-screen');
const hud = document.getElementById('hud');
const scoreDisplay = document.getElementById('score-display');
const finalScoreDisplay = document.getElementById('final-score');
const hpBar = document.getElementById('hp-bar');
const chargeBar = document.getElementById('charge-bar');
const mobileControls = document.getElementById('mobile-controls');

let GAME_STATE = 'START';
let animationId;
let frames = 0;
let score = 0;
let gameSpeed = 2.0; 

// 入力状態の管理 (上下キーを追加)
const keys = {
    ArrowLeft: false,
    ArrowRight: false,
    ArrowUp: false,
    ArrowDown: false,
    Space: false,
    KeyZ: false,
    KeyX: false,
    KeyC: false
};

// --- Web Audio API ---
let audioCtx;

function initAudio() {
    if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
    if (audioCtx.state === 'suspended') audioCtx.resume();
}

function playRoarSound() {
    if (!audioCtx) return;
    const bufferSize = audioCtx.sampleRate * 1.5; 
    const buffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
    const data = buffer.getChannelData(0);
    for (let i = 0; i < bufferSize; i++) data[i] = Math.random() * 2 - 1;
    
    const noise = audioCtx.createBufferSource(); noise.buffer = buffer;
    const filter = audioCtx.createBiquadFilter(); filter.type = 'bandpass';
    filter.frequency.setValueAtTime(200, audioCtx.currentTime); // 400 -> 200に下げて重低音化
    filter.frequency.exponentialRampToValueAtTime(50, audioCtx.currentTime + 1.0);
    
    const osc = audioCtx.createOscillator(); osc.type = 'sawtooth'; 
    osc.frequency.setValueAtTime(50, audioCtx.currentTime); // 90 -> 50に下げる
    osc.frequency.exponentialRampToValueAtTime(15, audioCtx.currentTime + 1.0); // 30 -> 15
    
    const gain = audioCtx.createGain(); gain.gain.setValueAtTime(0, audioCtx.currentTime);
    gain.gain.linearRampToValueAtTime(1.0, audioCtx.currentTime + 0.1); 
    gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 1.5); 
    
    noise.connect(filter); filter.connect(gain); osc.connect(gain); gain.connect(audioCtx.destination);
    noise.start(); osc.start(); noise.stop(audioCtx.currentTime + 1.5); osc.stop(audioCtx.currentTime + 1.5);
}

function playDamageSound() {
    if (!audioCtx) return;
    const osc = audioCtx.createOscillator(); const gain = audioCtx.createGain();
    osc.type = 'square'; osc.frequency.setValueAtTime(60, audioCtx.currentTime); // 100 -> 60
    osc.frequency.exponentialRampToValueAtTime(5, audioCtx.currentTime + 0.3); // 10 -> 5
    gain.gain.setValueAtTime(0.8, audioCtx.currentTime); gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.3);
    osc.connect(gain); gain.connect(audioCtx.destination);
    osc.start(); osc.stop(audioCtx.currentTime + 0.3);
}

function playPunchSwingSound() {
    if (!audioCtx) return;
    const osc = audioCtx.createOscillator(); const gain = audioCtx.createGain();
    osc.type = 'triangle'; osc.frequency.setValueAtTime(80, audioCtx.currentTime); // 120 -> 80
    osc.frequency.exponentialRampToValueAtTime(20, audioCtx.currentTime + 0.15); // 40 -> 20
    gain.gain.setValueAtTime(0.6, audioCtx.currentTime); gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.15);
    osc.connect(gain); gain.connect(audioCtx.destination);
    osc.start(); osc.stop(audioCtx.currentTime + 0.15);
}

function playHitSound() {
    if (!audioCtx) return;
    const noise = audioCtx.createBufferSource();
    const bufferSize = audioCtx.sampleRate * 0.1;
    const buffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
    const data = buffer.getChannelData(0);
    for (let i = 0; i < bufferSize; i++) data[i] = (Math.random() * 2 - 1) * 0.8;
    noise.buffer = buffer;
    
    const filter = audioCtx.createBiquadFilter(); filter.type = 'lowpass'; filter.frequency.value = 400; // 800 -> 400 鈍い打撃音に
    const gain = audioCtx.createGain(); gain.gain.setValueAtTime(0.8, audioCtx.currentTime);
    gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.1);
    
    noise.connect(filter); filter.connect(gain); gain.connect(audioCtx.destination);
    noise.start();
}

function playShootSound() {
    if (!audioCtx) return;
    const osc = audioCtx.createOscillator(); const gain = audioCtx.createGain();
    osc.type = 'sine'; osc.frequency.setValueAtTime(300, audioCtx.currentTime); // 600 -> 300 敵の発射音を下げる
    osc.frequency.exponentialRampToValueAtTime(50, audioCtx.currentTime + 0.2); // 100 -> 50
    gain.gain.setValueAtTime(0.3, audioCtx.currentTime); gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.2);
    osc.connect(gain); gain.connect(audioCtx.destination);
    osc.start(); osc.stop(audioCtx.currentTime + 0.2);
}

// --- レーザーの連続音 ---
let laserOsc = null;
let laserGain = null;

function startLaserSound() {
    if (!audioCtx) return;
    if (laserOsc) return; // 既に鳴っている場合はスキップ
    laserOsc = audioCtx.createOscillator();
    laserGain = audioCtx.createGain();
    
    laserOsc.type = 'sine'; // やや低めの「ぴー」から「ぶー」に近い音へ
    laserOsc.frequency.setValueAtTime(400, audioCtx.currentTime); // 1000 -> 400
    laserOsc.frequency.linearRampToValueAtTime(450, audioCtx.currentTime + 0.1); // 1200 -> 450
    
    laserGain.gain.setValueAtTime(0, audioCtx.currentTime);
    laserGain.gain.linearRampToValueAtTime(0.3, audioCtx.currentTime + 0.05); // ボリュームを少しアップ
    
    laserOsc.connect(laserGain);
    laserGain.connect(audioCtx.destination);
    laserOsc.start();
}

function stopLaserSound() {
    if (laserOsc && laserGain) {
        laserGain.gain.linearRampToValueAtTime(0.01, audioCtx.currentTime + 0.05);
        laserOsc.stop(audioCtx.currentTime + 0.05);
        laserOsc = null;
        laserGain = null;
    }
}

function resizeCanvas() {
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;
}
window.addEventListener('resize', resizeCanvas);
resizeCanvas();

// --- 1. プレイヤー(怪獣) ---
class Player {
    constructor() {
        this.width = 110;
        this.height = 130;
        this.x = canvas.width * 0.1;
        this.y = canvas.height - this.height - 50;
        this.vy = 0;
        this.gravity = 0.85; 
        this.jumpPower = -15;
        this.isJumping = false;
        this.moveSpeed = 4.5;
        
        this.maxHp = 500; 
        this.hp = this.maxHp;
        
        this.isShooting = false;
        this.beamAngle = 0; // 首(ビーム)の角度
        
        this.isPunching = false;
        this.punchDuration = 15;
        this.punchTimer = 0;

        this.isKicking = false;
        this.kickDuration = 20;
        this.kickTimer = 0;

        this.groundY = canvas.height - 50;
    }

    update() {
        // 移動
        if (keys.ArrowLeft) this.x -= this.moveSpeed;
        if (keys.ArrowRight) this.x += this.moveSpeed;
        if (this.x < 0) this.x = 0;
        if (this.x > canvas.width - this.width) this.x = canvas.width - this.width;

        // ビーム角度調整
        if (keys.ArrowUp) this.beamAngle -= 0.04;
        if (keys.ArrowDown) this.beamAngle += 0.04;
        // 角度制限 (-45度 〜 45度)
        if (this.beamAngle < -Math.PI/4) this.beamAngle = -Math.PI/4;
        if (this.beamAngle > Math.PI/4) this.beamAngle = Math.PI/4;

        if (keys.Space && !this.isJumping) this.jump();
        
        if (keys.KeyZ) {
            if (!this.isShooting) startLaserSound(); // 撃ち始めに音を鳴らし始める
            this.isShooting = true;
            if (frames % 10 === 0) shakeScreen(2); 
        } else {
            if (this.isShooting) stopLaserSound(); // 撃ち終わりに音を止める
            this.isShooting = false;
        }
        
        if (this.isPunching) {
            this.punchTimer--;
            if (this.punchTimer <= 0) this.isPunching = false;
        }
        
        if (this.isKicking) {
            this.kickTimer--;
            if (this.kickTimer <= 0) this.isKicking = false;
        }

        // 物理
        this.vy += this.gravity;
        this.y += this.vy;

        if (this.y + this.height >= this.groundY) {
            this.y = this.groundY - this.height;
            if (this.vy > 5) {
                shakeScreen(Math.min(this.vy * 1.5, 15)); 
                createExplosion(this.x + this.width/2, this.groundY, '#222', 5); 
            }
            this.vy = 0;
            this.isJumping = false;
        }

        if (!this.isJumping && frames % 50 === 0 && (keys.ArrowLeft || keys.ArrowRight)) {
            shakeScreen(2); 
        }
    }

    // 攻撃の当たり判定領域を取得
    getAttackRects() {
        let rects = [];
        
        // 角度付きビームの当たり判定(円の連続で近似)
        if (this.isShooting) {
            let headBaseX = this.x + 35;
            let headBaseY = this.y + 10; 
            let startX = 55; // 口先の相対X
            let startY = 3;  // 口先の相対Y
            
            let cosA = Math.cos(this.beamAngle);
            let sinA = Math.sin(this.beamAngle);
            
            // ビーム発射の根本座標(絶対座標)
            let bx = headBaseX + startX * cosA - startY * sinA;
            let by = headBaseY + startX * sinA + startY * cosA;
            
            for (let d = 0; d < canvas.width; d += 40) {
                rects.push({
                    type: 'beam',
                    x: bx + cosA * d - 25, 
                    y: by + sinA * d - 10, // 高さを絞る
                    width: 50, 
                    height: 20, // 細いレーザーに合わせた当たり判定
                    damage: 1, 
                    hitDelay: 8
                });
            }
        }
        if (this.isPunching) {
            rects.push({
                type: 'punch',
                x: this.x + this.width - 20, y: this.y + 40,
                width: 80, height: 60,
                damage: 3, hitDelay: 15
            });
        }
        if (this.isKicking) {
            rects.push({
                type: 'kick',
                x: this.x + this.width - 20, y: this.y + 70,
                width: 100, height: 50,
                damage: 4, hitDelay: 20
            });
        }
        return rects;
    }

    draw() {
        ctx.fillStyle = 'rgba(0,0,0,0.8)';
        ctx.beginPath();
        ctx.ellipse(this.x + this.width/2, this.groundY, this.width/2 + 25, 12, 0, 0, Math.PI * 2);
        ctx.fill();

        const x = this.x;
        const y = this.y;
        const w = this.width;
        const h = this.height;

        let walkCycle = frames * 0.05; 
        let isMoving = (keys.ArrowLeft || keys.ArrowRight);
        let legAnim = (!this.isJumping && isMoving) ? Math.sin(walkCycle * 2) * 15 : 0;
        let armAnim = (!this.isJumping && isMoving) ? Math.cos(walkCycle * 2) * 5 : -5;
        let tailAnim = Math.sin(frames * 0.04) * 12; 
        let breathAnim = Math.sin(frames * 0.02) * 4; 

        let punchExtX = 0, punchExtY = 0;
        let kickExtX = 0, kickExtY = 0;

        if (this.isPunching && !this.isShooting) {
            let p = Math.sin((this.punchTimer / this.punchDuration) * Math.PI); 
            punchExtX = p * 50; punchExtY = p * 10; 
        }
        if (this.isKicking && !this.isShooting) {
            let p = Math.sin((this.kickTimer / this.kickDuration) * Math.PI);
            kickExtX = p * 70; kickExtY = -p * 20; 
        }

        const skinDark = '#0c0d10';
        const skinMid = '#1c1e24';
        const skinLight = '#2d313a';

        // --- 背びれ ---
        let finColor = skinLight;
        let finGlowColor = 'rgba(0, 0, 0, 0)';
        
        if (this.isShooting) {
            finColor = '#ffffff';
            finGlowColor = 'rgba(180, 0, 255, 1)'; // 紫色に発光
        } else {
            finColor = '#eabfff'; // 薄い紫
            let pulse = Math.sin(frames * 0.1) * 0.5 + 0.5;
            finGlowColor = `rgba(180, 0, 255, ${0.5 + pulse * 0.5})`;
        }

        ctx.shadowBlur = this.isShooting ? 30 : 15;
        ctx.shadowColor = finGlowColor;
        ctx.fillStyle = finColor;

        this.drawComplexSpike(x + 15, y + 25 + breathAnim, 15, 25, -15);
        this.drawComplexSpike(x + 35, y + 15 + breathAnim, 25, 45, -5); 
        this.drawComplexSpike(x + 55, y + 25 + breathAnim, 18, 30, 10);
        this.drawComplexSpike(x + 25, y + 55, 15, 25, -35); 
        this.drawComplexSpike(x + 5, y + 75, 12, 20, -50);  
        this.drawComplexSpike(x - 15, y + 85 + tailAnim * 0.2, 10, 15, -60); 
        ctx.shadowBlur = 0;

        // --- 奥の足 ---
        ctx.fillStyle = skinDark;
        ctx.beginPath();
        ctx.moveTo(x + 55, y + 70);
        ctx.bezierCurveTo(x + 80, y + 80, x + 75 + legAnim, y + h - 10, x + 65 + legAnim, y + h); 
        ctx.lineTo(x + 95 + legAnim, y + h); 
        ctx.lineTo(x + 75, y + 90);
        ctx.fill();

        // --- 尻尾 ---
        let tailGrad = ctx.createLinearGradient(x + 30, y + 70, x - 160, y + 90);
        tailGrad.addColorStop(0, skinMid);
        tailGrad.addColorStop(1, skinDark);
        ctx.fillStyle = tailGrad;
        
        ctx.beginPath();
        ctx.moveTo(x + 40, y + 65); 
        ctx.bezierCurveTo(x - 30, y + 95, x - 100, y + 85 + tailAnim*0.5, x - 160, y + 90 + tailAnim); 
        ctx.bezierCurveTo(x - 100, y + 120, x - 10, y + 110, x + 30, y + 115); 
        ctx.fill();
        
        ctx.fillStyle = finColor;
        this.drawComplexSpike(x - 45, y + 88 + tailAnim * 0.4, 10, 15, -70);
        this.drawComplexSpike(x - 80, y + 90 + tailAnim * 0.6, 8, 12, -80);
        this.drawComplexSpike(x - 120, y + 91 + tailAnim * 0.8, 6, 10, -85);

        // --- 胴体 ---
        let bodyGrad = ctx.createLinearGradient(x + 10, y, x + w - 20, y + h);
        bodyGrad.addColorStop(0, skinLight); 
        bodyGrad.addColorStop(0.3, skinMid); 
        bodyGrad.addColorStop(0.9, skinDark); 
        
        ctx.fillStyle = bodyGrad;
        ctx.beginPath();
        ctx.moveTo(x + 40, y + 15 + breathAnim); 
        ctx.bezierCurveTo(x + 70, y + 40, x + 70, y + 80, x + 50, y + 105); 
        ctx.bezierCurveTo(x + 20, y + 130, x + 5, y + 90, x + 15, y + 75); 
        ctx.bezierCurveTo(x + 5, y + 40, x + 40, y + 30, x + 35, y + 25 + breathAnim); 
        ctx.fill();

        ctx.strokeStyle = 'rgba(0,0,0,0.6)';
        ctx.lineWidth = 1.5;
        ctx.beginPath();
        ctx.moveTo(x + 35, y + 35 + breathAnim); ctx.quadraticCurveTo(x+45, y+38, x+52, y+30 + breathAnim);
        ctx.moveTo(x + 32, y + 45 + breathAnim); ctx.quadraticCurveTo(x+45, y+50, x+58, y+40 + breathAnim);
        ctx.moveTo(x + 30, y + 55 + breathAnim); ctx.quadraticCurveTo(x+40, y+60, x+55, y+52 + breathAnim);
        ctx.moveTo(x + 25, y + 70); ctx.quadraticCurveTo(x+40, y+80, x+60, y+75);
        ctx.moveTo(x + 20, y + 85); ctx.quadraticCurveTo(x+35, y+95, x+55, y+90);
        ctx.stroke();

        // --- 手前の足(キック反映) ---
        ctx.fillStyle = skinMid;
        ctx.beginPath();
        ctx.ellipse(x + 35 + kickExtX/2, y + 90 + kickExtY/2, 32, 25, Math.PI / 7, 0, Math.PI*2);
        ctx.fill();
        ctx.strokeStyle = skinDark; ctx.lineWidth = 5;
        ctx.beginPath(); ctx.ellipse(x + 35 + kickExtX/2, y + 90 + kickExtY/2, 22, 18, Math.PI / 7, Math.PI*0.4, Math.PI*1.6); ctx.stroke();

        let frontLegGrad = ctx.createLinearGradient(x + 20, y + 95, x + 40, y + h);
        frontLegGrad.addColorStop(0, skinMid);
        frontLegGrad.addColorStop(1, skinDark);
        ctx.fillStyle = frontLegGrad;
        
        ctx.beginPath();
        ctx.moveTo(x + 25 + kickExtX/2, y + 105 + kickExtY/2);
        ctx.quadraticCurveTo(x + 35 + kickExtX, y + 120 + kickExtY, x + 20 - legAnim + kickExtX, y + h + kickExtY); 
        ctx.lineTo(x + 60 - legAnim + kickExtX, y + h + kickExtY); 
        ctx.lineTo(x + 50 + kickExtX, y + 100 + kickExtY); 
        ctx.fill();
        
        ctx.fillStyle = '#8a8e91';
        let tx = x - legAnim + kickExtX, ty = y + h + kickExtY;
        this.drawClaw(tx + 60, ty, tx + 72, ty - 2, tx + 55, ty - 10);
        this.drawClaw(tx + 52, ty, tx + 64, ty - 2, tx + 47, ty - 10);
        this.drawClaw(tx + 44, ty, tx + 55, ty - 2, tx + 39, ty - 10);

        // --- 頭部とビーム(角度対応) ---
        let headBaseX = x + 35;
        let headBaseY = y + 10 + breathAnim;

        ctx.save();
        ctx.translate(headBaseX, headBaseY);
        ctx.rotate(this.beamAngle);

        let hX = 0;
        let hY = -15; // 首の付け根からの相対位置
        let jawOpen = this.isShooting ? 30 : 0; 

        ctx.fillStyle = skinMid;
        ctx.beginPath();
        ctx.moveTo(hX, hY + 8);
        ctx.bezierCurveTo(hX + 15, hY - 15, hX + 35, hY - 8, hX + 48, hY + 5); 
        ctx.lineTo(hX + 58, hY + 15); 
        ctx.lineTo(hX + 15, hY + 22); 
        ctx.fill();
        
        ctx.fillStyle = skinLight;
        ctx.beginPath(); ctx.arc(hX + 50, hY + 10, 4, 0, Math.PI*2); ctx.fill();
        ctx.beginPath(); ctx.arc(hX + 42, hY + 6, 3, 0, Math.PI*2); ctx.fill();
        
        ctx.fillStyle = skinDark;
        ctx.beginPath();
        ctx.moveTo(hX + 12, hY + 20);
        ctx.quadraticCurveTo(hX + 30, hY + 25 + jawOpen, hX + 55, hY + 17 + jawOpen); 
        ctx.lineTo(hX + 5, hY + 35 + jawOpen); 
        ctx.fill();

        ctx.fillStyle = '#c8cfb2'; 
        if (this.isShooting) {
            for(let i=0; i<5; i++) {
                ctx.beginPath(); ctx.moveTo(hX + 52 - i*7, hY + 14); ctx.lineTo(hX + 49 - i*7, hY + 23); ctx.lineTo(hX + 46 - i*7, hY + 15); ctx.fill();
            }
            for(let i=0; i<5; i++) {
                ctx.beginPath(); ctx.moveTo(hX + 52 - i*7, hY + 18 + jawOpen); ctx.lineTo(hX + 49 - i*7, hY + 9 + jawOpen); ctx.lineTo(hX + 46 - i*7, hY + 19 + jawOpen); ctx.fill();
            }
            ctx.fillStyle = '#ffffff';
            ctx.shadowBlur = 15; ctx.shadowColor = '#00ffff';
            ctx.beginPath(); ctx.ellipse(hX + 25, hY + 18 + jawOpen/2, 10, jawOpen/2 - 2, 0, 0, Math.PI*2); ctx.fill();
            ctx.shadowBlur = 0;
        } else {
            ctx.beginPath(); ctx.moveTo(hX + 52, hY + 15); ctx.lineTo(hX + 50, hY + 22); ctx.lineTo(hX + 48, hY + 15); ctx.fill();
            ctx.beginPath(); ctx.moveTo(hX + 44, hY + 16); ctx.lineTo(hX + 42, hY + 23); ctx.lineTo(hX + 40, hY + 16); ctx.fill();
            ctx.beginPath(); ctx.moveTo(hX + 36, hY + 17); ctx.lineTo(hX + 34, hY + 24); ctx.lineTo(hX + 32, hY + 17); ctx.fill();
        }

        ctx.fillStyle = this.hp < 100 ? '#ff0000' : '#ff8800'; 
        if (this.isShooting) ctx.fillStyle = '#ffffff';
        ctx.beginPath(); ctx.ellipse(hX + 30, hY + 10, 3.5, 1.5, Math.PI/8, 0, Math.PI*2); ctx.fill();
        
        ctx.fillStyle = skinDark;
        ctx.beginPath(); ctx.moveTo(hX + 22, hY + 4); ctx.lineTo(hX + 34, hY + 8); ctx.lineTo(hX + 28, hY + 13); ctx.fill();

        // 細い紫のレーザー描画(相対座標)
        if (this.isShooting) {
            let startX = hX + 55;
            let startY = hY + 18 + jawOpen/2;
            const beamLength = canvas.width;
            const beamFlicker = Math.random() * 4 - 2;

            ctx.fillStyle = 'rgba(180, 0, 255, 0.4)'; // 外側の紫
            ctx.fillRect(startX, startY - 15 + beamFlicker, beamLength, 30);
            ctx.fillStyle = 'rgba(150, 0, 255, 0.9)'; // 中間の濃い紫
            ctx.fillRect(startX, startY - 6 + beamFlicker/2, beamLength, 12);
            ctx.fillStyle = '#ffffff'; // 中心の鋭い光
            ctx.fillRect(startX, startY - 2 + beamFlicker/4, beamLength, 4);

            // 細かい螺旋のエネルギー
            ctx.strokeStyle = 'rgba(255, 255, 255, 0.9)';
            ctx.lineWidth = 2;
            ctx.beginPath();
            for(let i=0; i<beamLength; i+=15) {
                let yOffset = Math.sin((frames*2.5) + i*0.15) * 8;
                if(i===0) ctx.moveTo(startX + i, startY + yOffset);
                else ctx.lineTo(startX + i, startY + yOffset);
            }
            ctx.stroke();
            
            // ビームのパーティクル生成(絶対座標に変換して追加)
            let cosA = Math.cos(this.beamAngle);
            let sinA = Math.sin(this.beamAngle);
            for (let j=0; j<2; j++) { 
                let dist = startX + Math.random() * 150;
                let oy = startY + beamFlicker + (Math.random()*10-5); // パーティクルも細かく
                let px = headBaseX + dist * cosA - oy * sinA;
                let py = headBaseY + dist * sinA + oy * cosA;
                particles.push(new Particle(px, py, j===0?'#b000ff':'#ffffff', 10, 25*cosA + Math.random()*5, 25*sinA + Math.random()*5 - 2.5));
            }
        }
        ctx.restore(); // 頭部の回転終了

        // --- 手前の腕 ---
        ctx.fillStyle = skinMid;
        ctx.beginPath();
        ctx.moveTo(x + 40, y + 42 + breathAnim); 
        ctx.quadraticCurveTo(x + 65 + armAnim + punchExtX, y + 55 + punchExtY, x + 75 + armAnim + punchExtX, y + 60 + punchExtY); 
        ctx.lineTo(x + 55, y + 50 + breathAnim); 
        ctx.fill();
        
        ctx.strokeStyle = skinDark;
        ctx.lineWidth = 3;
        ctx.beginPath(); ctx.arc(x + 45, y + 45 + breathAnim, 10, Math.PI*0.3, Math.PI*1.5); ctx.stroke();
        
        ctx.fillStyle = '#8a8e91';
        this.drawClaw(x + 75 + armAnim + punchExtX, y + 60 + punchExtY, x + 88 + armAnim + punchExtX, y + 65 + punchExtY, x + 72 + armAnim + punchExtX, y + 55 + punchExtY);
        this.drawClaw(x + 72 + armAnim + punchExtX, y + 62 + punchExtY, x + 83 + armAnim + punchExtX, y + 68 + punchExtY, x + 68 + armAnim + punchExtX, y + 58 + punchExtY);
    }

    drawComplexSpike(bx, by, width, height, angleOffset) {
        ctx.save(); ctx.translate(bx, by); ctx.rotate(angleOffset * Math.PI / 180);
        ctx.beginPath(); ctx.moveTo(-width/2, 0); ctx.lineTo(-width*0.4, -height*0.2); ctx.lineTo(-width*0.5, -height*0.4); ctx.lineTo(-width*0.2, -height*0.6); ctx.lineTo(-width*0.3, -height*0.8); ctx.lineTo(0, -height); ctx.lineTo(width*0.1, -height*0.7); ctx.lineTo(width*0.4, -height*0.5); ctx.lineTo(width*0.2, -height*0.3); ctx.lineTo(width*0.5, -height*0.1); ctx.lineTo(width/2, 0); ctx.fill();
        ctx.strokeStyle = 'rgba(0,0,0,0.5)'; ctx.lineWidth = 1;
        ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(-width*0.1, -height*0.8); ctx.moveTo(-width*0.2, 0); ctx.lineTo(-width*0.3, -height*0.5); ctx.moveTo(width*0.2, 0); ctx.lineTo(width*0.1, -height*0.4); ctx.stroke();
        ctx.restore();
    }

    drawClaw(x1, y1, x2, y2, x3, y3) {
        ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.lineTo(x3, y3); ctx.fill();
    }

    jump() {
        this.vy = this.jumpPower; this.isJumping = true;
        shakeScreen(8); createExplosion(this.x + this.width/2, this.groundY, '#333', 15); 
    }
    
    punch() {
        if (!this.isPunching && !this.isShooting) {
            this.isPunching = true; this.punchTimer = this.punchDuration; playPunchSwingSound();
        }
    }

    kick() {
        if (!this.isKicking && !this.isShooting) {
            this.isKicking = true; this.kickTimer = this.kickDuration; playPunchSwingSound(); 
        }
    }

    takeDamage(amount) {
        this.hp -= amount;
        if (this.hp <= 0) {
            this.hp = 0; gameOver();
        } else {
            playDamageSound(); 
        }
        updateHUD();
        ctx.fillStyle = 'rgba(255, 0, 0, 0.4)'; ctx.fillRect(0, 0, canvas.width, canvas.height); shakeScreen(20);
    }
}

// --- 敵の遠距離攻撃(プロジェクタイル) ---
class EnemyProjectile {
    constructor(x, y, type) {
        this.x = x; this.y = y; this.type = type; this.markedForDeletion = false;
        
        if (this.type === 'mothra_beam') {
            this.vx = -6; this.vy = 0; this.width = 30; this.height = 30; this.damage = 15;
        } else if (this.type === 'ghidorah_beam') {
            this.vx = -8; this.vy = Math.random() * 2 - 1; this.width = 40; this.height = 10; this.damage = 20;
        } else if (this.type === 'mechagodzilla_missile') {
            this.vx = -10; this.vy = 0; this.width = 25; this.height = 10; this.damage = 25;
        }
    }

    update() {
        this.x += this.vx; this.y += this.vy;
        if (this.x + this.width < 0 || this.x > canvas.width || this.y > canvas.height || this.y < 0) this.markedForDeletion = true;
        if (this.type === 'mothra_beam') this.vy = Math.sin(frames * 0.2) * 2; 
    }

    draw() {
        if (this.type === 'mothra_beam') {
            ctx.strokeStyle = '#ffff00'; ctx.lineWidth = 4;
            ctx.beginPath(); ctx.arc(this.x + this.width/2, this.y + this.height/2, this.width/2, 0, Math.PI*2); ctx.stroke();
            ctx.fillStyle = 'rgba(255, 255, 0, 0.5)'; ctx.fill();
        } else if (this.type === 'ghidorah_beam') {
            ctx.strokeStyle = '#ffe600'; ctx.lineWidth = 6;
            ctx.beginPath(); ctx.moveTo(this.x + this.width, this.y + this.height/2); ctx.lineTo(this.x + this.width/2, this.y + this.height/2 + Math.sin(frames)*10); ctx.lineTo(this.x, this.y + this.height/2 - Math.sin(frames)*10); ctx.stroke();
            ctx.shadowBlur = 10; ctx.shadowColor = '#ffaa00'; ctx.stroke(); ctx.shadowBlur = 0;
        } else if (this.type === 'mechagodzilla_missile') {
            ctx.fillStyle = '#666'; ctx.fillRect(this.x, this.y, this.width, this.height);
            ctx.fillStyle = '#ff3300'; ctx.beginPath(); ctx.arc(this.x, this.y + this.height/2, this.height/2, 0, Math.PI*2); ctx.fill();
            ctx.fillStyle = '#ffaa00'; ctx.beginPath(); ctx.moveTo(this.x + this.width, this.y); ctx.lineTo(this.x + this.width + 15 + Math.random()*10, this.y + this.height/2); ctx.lineTo(this.x + this.width, this.y + this.height); ctx.fill();
        }
    }
}

// --- 2. 障害物・敵キャラクター ---
class Obstacle {
    constructor(type) {
        this.type = type; 
        this.x = canvas.width;
        this.markedForDeletion = false;
        this.hitDelay = 0; 
        this.attackTimer = 0; 
        
        if (this.type === 'building_small') {
            this.width = 70 + Math.random() * 50; this.height = 100 + Math.random() * 60;
            this.y = canvas.height - 50 - this.height; this.color = '#7a8b99'; this.hp = 1; this.damage = 15; this.scoreValue = 100;
        } else if (this.type === 'building_tall') {
            this.width = 90 + Math.random() * 60; this.height = 250 + Math.random() * 200; 
            this.y = canvas.height - 50 - this.height; this.color = '#5c6b7a'; this.hp = 2; this.damage = 25; this.scoreValue = 300;
        } else if (this.type === 'heli') {
            this.width = 80; this.height = 35; this.y = canvas.height - 150 - Math.random() * 200; this.color = '#3b4535'; 
            this.hp = 1; this.damage = 20; this.scoreValue = 200; this.sineOffset = Math.random() * Math.PI * 2;
        } else if (this.type === 'mothra') { 
            this.width = 120; this.height = 80; this.y = canvas.height - 250 - Math.random() * 150; this.color = '#ff8800'; 
            this.hp = 15; this.damage = 40; this.scoreValue = 800; this.sineOffset = Math.random() * Math.PI * 2;
        } else if (this.type === 'ghidorah') { 
            this.width = 180; this.height = 180; this.y = canvas.height - 220 - Math.random() * 50; this.color = '#ffd700'; 
            this.hp = 40; this.damage = 70; this.scoreValue = 2000; this.sineOffset = Math.random() * Math.PI * 2;
        } else if (this.type === 'mechagodzilla') { 
            this.width = 130; this.height = 150; this.y = canvas.height - 50 - this.height; this.color = '#a0a0a0';
            this.hp = 30; this.damage = 60; this.scoreValue = 1500;
        }
    }

    update() {
        if (this.hitDelay > 0) this.hitDelay--;

        if (this.type === 'heli') {
            this.y += Math.sin(frames * 0.05 + this.sineOffset) * 2; this.x -= gameSpeed * 0.6; 
        } else if (this.type === 'mothra') {
            this.y += Math.sin(frames * 0.08 + this.sineOffset) * 4; this.x -= gameSpeed * 1.2; 
            if (frames % 90 === 0 && this.x < canvas.width) { enemyProjectiles.push(new EnemyProjectile(this.x, this.y + 40, 'mothra_beam')); playShootSound(); }
        } else if (this.type === 'ghidorah') {
            this.y += Math.sin(frames * 0.04 + this.sineOffset) * 1.5; this.x -= gameSpeed * 0.4; 
            if (frames % 100 === 0 && this.x < canvas.width) {
                enemyProjectiles.push(new EnemyProjectile(this.x + 30, this.y + 60, 'ghidorah_beam'));
                enemyProjectiles.push(new EnemyProjectile(this.x + 70, this.y + 20, 'ghidorah_beam'));
                enemyProjectiles.push(new EnemyProjectile(this.x + 110, this.y + 50, 'ghidorah_beam')); playShootSound();
            }
        } else if (this.type === 'mechagodzilla') {
            this.x -= gameSpeed * 0.25; 
            if (frames % 80 === 0 && this.x < canvas.width) { enemyProjectiles.push(new EnemyProjectile(this.x + 10, this.y + 70, 'mechagodzilla_missile')); playShootSound(); }
        } else {
            this.x -= gameSpeed;
        }

        if (this.x + this.width < 0) this.markedForDeletion = true;
    }

    draw() {
        if (this.type.includes('building')) {
            ctx.fillStyle = this.hitDelay > 0 ? '#ffffff' : this.color; ctx.fillRect(this.x, this.y, this.width, this.height);
            ctx.fillStyle = 'rgba(0, 0, 0, 0.3)'; ctx.fillRect(this.x + this.width - 15, this.y, 15, this.height);
            ctx.fillStyle = '#aaddff'; 
            const cols = Math.floor(this.width / 18); const rows = Math.floor(this.height / 25);
            for (let i = 0; i < cols; i++) {
                for (let j = 0; j < rows; j++) {
                    if ((i + j * 3) % 4 !== 0) { ctx.globalAlpha = 0.8; ctx.fillRect(this.x + 8 + i * 18, this.y + 12 + j * 25, 8, 14); }
                }
            }
            ctx.globalAlpha = 1.0;
        } else if (this.type === 'heli') {
            ctx.fillStyle = this.hitDelay > 0 ? '#ffffff' : this.color; ctx.beginPath(); ctx.ellipse(this.x + this.width/2, this.y + this.height/2, this.width/2, this.height/2, 0, 0, Math.PI*2); ctx.fill();
            ctx.fillStyle = '#add8e6'; ctx.beginPath(); ctx.arc(this.x + 20, this.y + 18, 10, 0, Math.PI*2); ctx.fill();
            ctx.fillStyle = '#aaa'; const rotorWidth = (frames % 6 < 3) ? 100 : 10; ctx.fillRect(this.x + this.width/2 - rotorWidth/2, this.y - 5, rotorWidth, 3);
            ctx.fillStyle = this.hitDelay > 0 ? '#ffffff' : this.color; ctx.fillRect(this.x + this.width - 5, this.y + 10, 35, 8); ctx.fillRect(this.x + this.width + 25, this.y - 5, 8, 25);
        } else if (this.type === 'mothra') {
            this.drawMothra();
        } else if (this.type === 'ghidorah') {
            this.drawGhidorah();
        } else if (this.type === 'mechagodzilla') {
            this.drawMechagodzilla();
        }
    }

    drawMothra() {
        let bx = this.x; let by = this.y;
        let wingFlap = Math.sin(frames * 0.6) * 40; 
        let isHit = this.hitDelay > 0;
        let wingGrad = ctx.createLinearGradient(bx + 40, by + 40, bx + 100, by - 40 + wingFlap);
        wingGrad.addColorStop(0, isHit ? '#ffffff' : '#422'); wingGrad.addColorStop(0.5, isHit ? '#ffcccc' : '#d84'); wingGrad.addColorStop(1, isHit ? '#ffffff' : '#fa2');

        ctx.fillStyle = wingGrad;
        ctx.beginPath(); ctx.moveTo(bx + 50, by + 30); ctx.quadraticCurveTo(bx + 80, by - 50 + wingFlap, bx + 130, by - 30 + wingFlap); ctx.lineTo(bx + 110, by + 10 + wingFlap/2); ctx.fill();
        ctx.fillStyle = isHit ? '#fff' : '#00aaff'; ctx.beginPath(); ctx.arc(bx + 100, by - 15 + wingFlap * 0.8, 10, 0, Math.PI*2); ctx.fill();

        let bodyColor = isHit ? '#ffcccc' : '#531'; ctx.fillStyle = bodyColor;
        for(let i=0; i<5; i++) { ctx.beginPath(); ctx.arc(bx + 30 + i*12, by + 40 + Math.sin(i)*5, 12 - i, 0, Math.PI*2); ctx.fill(); }
        
        ctx.fillStyle = wingGrad;
        ctx.beginPath(); ctx.moveTo(bx + 40, by + 30); ctx.quadraticCurveTo(bx - 10, by - 40 + wingFlap, bx - 30, by - 20 + wingFlap); ctx.lineTo(bx + 10, by + 20 + wingFlap/2); ctx.fill();
        ctx.fillStyle = isHit ? '#fff' : '#00aaff'; ctx.beginPath(); ctx.arc(bx - 10, by - 10 + wingFlap * 0.8, 12, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = '#ff0000'; ctx.beginPath(); ctx.arc(bx - 10, by - 10 + wingFlap * 0.8, 4, 0, Math.PI*2); ctx.fill();
        ctx.fillStyle = '#321'; ctx.beginPath(); ctx.arc(bx + 20, by + 35, 15, 0, Math.PI*2); ctx.fill();
        
        let eyeGrad = ctx.createRadialGradient(bx + 15, by + 32, 2, bx + 15, by + 32, 8);
        eyeGrad.addColorStop(0, '#fff'); eyeGrad.addColorStop(1, '#00ffff'); ctx.fillStyle = eyeGrad; ctx.beginPath(); ctx.ellipse(bx + 15, by + 32, 6, 10, Math.PI/4, 0, Math.PI*2); ctx.fill();
        ctx.strokeStyle = '#fff'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(bx + 25, by + 25); ctx.quadraticCurveTo(bx + 30, by + 10, bx + 45, by + 5); ctx.stroke();
    }

    drawGhidorah() {
        let bx = this.x; let by = this.y; let isHit = this.hitDelay > 0;
        let gold1 = isHit ? '#ffffff' : '#ffe600'; let gold2 = isHit ? '#ffdddd' : '#b8860b'; let gold3 = isHit ? '#ffaaaa' : '#8b6508'; 
        let wingFlap = Math.sin(frames*0.15) * 30;

        ctx.fillStyle = gold3; ctx.beginPath(); ctx.moveTo(bx + 90, by + 60); ctx.lineTo(bx + 160, by - 40 + wingFlap); ctx.quadraticCurveTo(bx + 190, by + 10 + wingFlap/2, bx + 220, by + 60); ctx.lineTo(bx + 150, by + 90); ctx.fill();
        ctx.strokeStyle = gold2; ctx.lineWidth = 3; ctx.beginPath(); ctx.moveTo(bx+90, by+60); ctx.lineTo(bx+190, by+10 + wingFlap/2); ctx.stroke(); ctx.beginPath(); ctx.moveTo(bx+90, by+60); ctx.lineTo(bx+220, by+60); ctx.stroke();

        let bodyGrad = ctx.createLinearGradient(bx + 60, by + 50, bx + 120, by + 150); bodyGrad.addColorStop(0, gold1); bodyGrad.addColorStop(1, gold3); ctx.fillStyle = bodyGrad;
        ctx.beginPath(); ctx.ellipse(bx + 100, by + 100, 45, 55, Math.PI/8, 0, Math.PI*2); ctx.fill(); ctx.beginPath(); ctx.ellipse(bx + 80, by + 150, 20, 30, -Math.PI/6, 0, Math.PI*2); ctx.ellipse(bx + 120, by + 140, 20, 30, Math.PI/6, 0, Math.PI*2); ctx.fill();

        ctx.fillStyle = gold2; ctx.beginPath(); ctx.moveTo(bx + 70, by + 70); ctx.lineTo(bx - 30, by - 20 - wingFlap); ctx.quadraticCurveTo(bx - 50, by + 30 - wingFlap/2, bx - 60, by + 90); ctx.lineTo(bx + 50, by + 100); ctx.fill();
        ctx.strokeStyle = gold3; ctx.lineWidth = 4; ctx.beginPath(); ctx.moveTo(bx+70, by+70); ctx.lineTo(bx-50, by+30 - wingFlap/2); ctx.stroke(); ctx.beginPath(); ctx.moveTo(bx+70, by+70); ctx.lineTo(bx-60, by+90); ctx.stroke();

        let drawNeckAndHead = (sx, sy, cx, cy, ex, ey, headAngle) => {
            ctx.strokeStyle = bodyGrad; ctx.lineWidth = 18; ctx.lineCap = 'round'; ctx.beginPath(); ctx.moveTo(sx, sy); ctx.quadraticCurveTo(cx, cy, ex, ey); ctx.stroke();
            ctx.strokeStyle = gold3; ctx.lineWidth = 2; for(let i=0; i<=1; i+=0.1) { let px = (1-i)*(1-i)*sx + 2*(1-i)*i*cx + i*i*ex; let py = (1-i)*(1-i)*sy + 2*(1-i)*i*cy + i*i*ey; ctx.beginPath(); ctx.arc(px, py, 9, 0, Math.PI); ctx.stroke(); }
            ctx.save(); ctx.translate(ex, ey); ctx.rotate(headAngle); ctx.fillStyle = gold1; ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(-20, -10); ctx.lineTo(-25, 5); ctx.lineTo(10, 15); ctx.fill();
            ctx.fillStyle = gold3; ctx.beginPath(); ctx.moveTo(5, -5); ctx.lineTo(15, -20); ctx.lineTo(15, -5); ctx.fill(); ctx.beginPath(); ctx.moveTo(0, -8); ctx.lineTo(5, -25); ctx.lineTo(10, -5); ctx.fill(); 
            ctx.fillStyle = '#ff0000'; ctx.beginPath(); ctx.ellipse(-8, -3, 4, 2, -Math.PI/6, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.moveTo(-20, -2); ctx.lineTo(-15, 5); ctx.lineTo(-10, -2); ctx.fill(); ctx.restore();
        };
        let t = frames * 0.05;
        drawNeckAndHead(bx + 80, by + 60, bx + 30, by + 80, bx - 10 + Math.sin(t)*15, by + 10 + Math.cos(t)*15, Math.PI/8); 
        drawNeckAndHead(bx + 100, by + 50, bx + 70, by + 10, bx + 50 + Math.sin(t+2)*20, by - 30 + Math.cos(t+2)*10, 0); 
        drawNeckAndHead(bx + 120, by + 60, bx + 140, by + 20, bx + 130 + Math.sin(t+4)*15, by + 10 + Math.cos(t+4)*15, -Math.PI/8); 
    }

    drawMechagodzilla() {
        let bx = this.x; let by = this.y; let isHit = this.hitDelay > 0; let w = this.width; let h = this.height; let walkCycle = frames * 0.05;
        let silverGrad = ctx.createLinearGradient(bx, by, bx + w, by + h);
        silverGrad.addColorStop(0, isHit ? '#ffffff' : '#f0f0f0'); silverGrad.addColorStop(0.3, isHit ? '#ffdddd' : '#a0a5aa');
        silverGrad.addColorStop(0.7, isHit ? '#ff8888' : '#50555a'); silverGrad.addColorStop(1, isHit ? '#ff4444' : '#20252a');
        let darkMetal = isHit ? '#ffaaaa' : '#2a2e33'; let highlight = isHit ? '#ffffff' : '#ffffff';

        for(let i=0; i<6; i++) {
            let tailX = bx + 80 + i*16; let tailY = by + h - 30 + Math.sin(walkCycle*2 - i*0.5)*5 + i*5;
            ctx.fillStyle = darkMetal; ctx.beginPath(); ctx.ellipse(tailX, tailY, 14-i, 20-i, -Math.PI/4, 0, Math.PI*2); ctx.fill();
            ctx.fillStyle = silverGrad; ctx.beginPath(); ctx.ellipse(tailX-2, tailY-2, 12-i, 16-i, -Math.PI/4, 0, Math.PI*2); ctx.fill();
        }

        let backLegY = by + h - 30 + Math.sin(walkCycle + Math.PI)*15;
        ctx.fillStyle = darkMetal; ctx.beginPath(); ctx.ellipse(bx + 70, backLegY - 10, 20, 28, Math.PI/8, 0, Math.PI*2); ctx.fill();
        ctx.fillStyle = silverGrad; ctx.beginPath(); ctx.moveTo(bx+65, backLegY); ctx.lineTo(bx+85, backLegY+40); ctx.lineTo(bx+55, backLegY+40); ctx.fill();

        ctx.fillStyle = silverGrad; ctx.beginPath(); ctx.moveTo(bx + 45, by + 30); ctx.lineTo(bx + 90, by + 40); ctx.lineTo(bx + 100, by + 100); ctx.lineTo(bx + 65, by + 120); ctx.lineTo(bx + 25, by + 90); ctx.lineTo(bx + 30, by + 45); ctx.fill();
        ctx.strokeStyle = darkMetal; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(bx+30, by+60); ctx.lineTo(bx+85, by+70); ctx.stroke(); ctx.beginPath(); ctx.moveTo(bx+27, by+80); ctx.lineTo(bx+93, by+90); ctx.stroke();
        ctx.fillStyle = '#000'; for(let i=0; i<4; i++) { ctx.beginPath(); ctx.arc(bx+35+i*15, by+65+i*2, 2, 0, Math.PI*2); ctx.fill(); ctx.beginPath(); ctx.arc(bx+33+i*16, by+85+i*2, 2, 0, Math.PI*2); ctx.fill(); }

        ctx.fillStyle = darkMetal; ctx.beginPath(); ctx.arc(bx + 45, by + 50, 16, 0, Math.PI*2); ctx.fill();
        ctx.fillStyle = isHit ? '#ffffff' : '#ff8800'; ctx.beginPath(); ctx.arc(bx + 45, by + 50, 11, 0, Math.PI*2); ctx.fill();
        ctx.fillStyle = '#ffee00'; ctx.beginPath(); ctx.arc(bx + 45, by + 50, 6, 0, Math.PI*2); ctx.fill();

        ctx.fillStyle = silverGrad; ctx.strokeStyle = highlight; ctx.lineWidth = 1;
        let drawDorsal = (x, y, scale) => { ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x+20*scale, y-25*scale); ctx.lineTo(x+15*scale, y+10*scale); ctx.fill(); ctx.stroke(); };
        drawDorsal(bx+85, by+35, 1.2); drawDorsal(bx+95, by+60, 1.5); drawDorsal(bx+100, by+85, 1.0);

        let frontLegY = by + h - 40 + Math.sin(walkCycle)*15;
        ctx.fillStyle = silverGrad; ctx.beginPath(); ctx.ellipse(bx + 45, frontLegY, 24, 32, Math.PI/12, 0, Math.PI*2); ctx.fill();
        ctx.strokeStyle = darkMetal; ctx.lineWidth = 3; ctx.beginPath(); ctx.ellipse(bx+45, frontLegY, 16, 22, Math.PI/12, Math.PI*0.5, Math.PI*1.5); ctx.stroke();
        ctx.fillStyle = '#666'; ctx.fillRect(bx+35, frontLegY+20, 8, 25); ctx.fillStyle = '#ccc'; ctx.fillRect(bx+37, frontLegY+30, 4, 15);
        ctx.fillStyle = silverGrad; ctx.beginPath(); ctx.moveTo(bx+40, frontLegY+25); ctx.lineTo(bx+55, frontLegY+50); ctx.lineTo(bx+20, frontLegY+50); ctx.fill();
        ctx.fillStyle = darkMetal; ctx.beginPath(); ctx.moveTo(bx+20, frontLegY+50); ctx.lineTo(bx+10, frontLegY+55); ctx.lineTo(bx+20, frontLegY+45); ctx.fill(); ctx.beginPath(); ctx.moveTo(bx+30, frontLegY+50); ctx.lineTo(bx+20, frontLegY+55); ctx.lineTo(bx+30, frontLegY+45); ctx.fill();

        let armRot = Math.sin(walkCycle)*0.5;
        ctx.save(); ctx.translate(bx + 50, by + 45); ctx.rotate(armRot);
        ctx.fillStyle = darkMetal; ctx.beginPath(); ctx.arc(0, 0, 12, 0, Math.PI*2); ctx.fill();
        ctx.fillStyle = silverGrad; ctx.beginPath(); ctx.ellipse(0, 15, 8, 18, 0, 0, Math.PI*2); ctx.fill(); ctx.beginPath(); ctx.ellipse(-5, 35, 10, 15, Math.PI/6, 0, Math.PI*2); ctx.fill();
        ctx.fillStyle = '#333'; for(let i=0; i<4; i++) { ctx.beginPath(); ctx.ellipse(-15, 30 + i*4, 8, 2, 0, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = '#ff3300'; ctx.beginPath(); ctx.ellipse(-23, 30 + i*4, 2, 2, 0, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = '#333'; } ctx.restore();

        ctx.fillStyle = silverGrad; ctx.beginPath(); ctx.moveTo(bx + 50, by + 28); ctx.lineTo(bx + 25, by + 10); ctx.lineTo(bx + 5, by + 15); ctx.lineTo(bx, by + 25); ctx.lineTo(bx + 20, by + 35); ctx.fill();
        ctx.fillStyle = darkMetal; ctx.beginPath(); ctx.arc(bx+25, by+25, 8, 0, Math.PI*2); ctx.fill(); ctx.fillRect(bx+10, by+5, 3, 10); 
        let jawOpen = Math.sin(frames * 0.15) > 0 ? 8 : 0;
        ctx.fillStyle = silverGrad; ctx.beginPath(); ctx.moveTo(bx + 25, by + 35); ctx.lineTo(bx, by + 30 + jawOpen); ctx.lineTo(bx + 5, by + 40 + jawOpen); ctx.lineTo(bx + 30, by + 42); ctx.fill();
        ctx.fillStyle = isHit ? '#ffffff' : '#ffaa00'; ctx.shadowBlur = 15; ctx.shadowColor = '#ff0000'; ctx.beginPath(); ctx.moveTo(bx+17, by+20); ctx.lineTo(bx+9, by+22); ctx.lineTo(bx+15, by+25); ctx.fill(); ctx.shadowBlur = 0;
    }
}

// --- 3. 背景 ---
class Background {
    constructor() {
        this.buildings = [];
        this.darkness = 0; // 暗転用の変数
        this.init();
    }

    init() {
        for (let i = 0; i < 30; i++) {
            this.buildings.push({
                x: i * 60,
                width: 50 + Math.random() * 80,
                height: 100 + Math.random() * 300,
                color: `rgb(${100 + Math.random()*40}, ${110 + Math.random()*40}, ${130 + Math.random()*40})` 
            });
        }
    }

    update() {
        for (let i = 0; i < this.buildings.length; i++) {
            this.buildings[i].x -= gameSpeed * 0.2; 
        }

        if (this.buildings[0].x + this.buildings[0].width < 0) {
            let first = this.buildings.shift();
            first.x = this.buildings[this.buildings.length - 1].x + 60;
            first.height = 100 + Math.random() * 300;
            this.buildings.push(first);
        }

        // ビーム発射時に徐々に背景を暗くする
        if (player && player.isShooting) {
            this.darkness += 0.05;
            if (this.darkness > 1) this.darkness = 1;
        } else {
            this.darkness -= 0.05;
            if (this.darkness < 0) this.darkness = 0;
        }
    }

    draw() {
        let gradient = ctx.createLinearGradient(0, 0, 0, canvas.height);
        gradient.addColorStop(0, '#4facfe'); 
        gradient.addColorStop(1, '#00f2fe'); 
        ctx.fillStyle = gradient;
        ctx.fillRect(0, 0, canvas.width, canvas.height);

        ctx.fillStyle = '#ffde00';
        ctx.beginPath();
        ctx.arc(canvas.width - 150, 120, 60, 0, Math.PI * 2);
        ctx.fill();
        ctx.fillStyle = 'rgba(255, 222, 0, 0.3)';
        ctx.beginPath();
        ctx.arc(canvas.width - 150, 120, 80, 0, Math.PI * 2);
        ctx.fill();

        ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
        let cloudY1 = 150;
        let cloudY2 = 250;
        ctx.beginPath();
        for(let i=-100; i<canvas.width+100; i+=100) {
            ctx.arc(i - (frames*0.2%100), cloudY1 + Math.sin(i*0.01+frames*0.005)*20, 60, 0, Math.PI*2);
        }
        ctx.fill();
        
        ctx.fillStyle = 'rgba(255, 255, 255, 0.5)';
        ctx.beginPath();
        for(let i=-100; i<canvas.width+100; i+=80) {
            ctx.arc(i - (frames*0.4%80), cloudY2 + Math.cos(i*0.02+frames*0.01)*30, 50, 0, Math.PI*2);
        }
        ctx.fill();

        for (let b of this.buildings) {
            ctx.fillStyle = b.color;
            ctx.fillRect(b.x, canvas.height - 50 - b.height, b.width, b.height);
        }

        ctx.fillStyle = '#4a5d23'; 
        ctx.fillRect(0, canvas.height - 50, canvas.width, 50);
        
        ctx.fillStyle = '#3a4d13';
        for(let i=0; i<canvas.width; i+=40) {
            let offset = (frames * gameSpeed) % 40;
            ctx.fillRect(i - offset, canvas.height - 40, 20 + Math.random()*10, 5 + Math.random()*5);
        }

        // --- 暗転オーバーレイを描画 ---
        if (this.darkness > 0) {
            // 少し紫がかった暗闇で覆う (最大85%の不透明度)
            ctx.fillStyle = `rgba(15, 0, 30, ${this.darkness * 0.85})`;
            ctx.fillRect(0, 0, canvas.width, canvas.height);
        }
    }
}

// --- 4. パーティクル ---
class Particle {
    constructor(x, y, color, speed, vx = null, vy = null) {
        this.x = x; this.y = y; this.color = color;
        this.size = Math.random() * 10 + 5;
        this.vx = vx !== null ? vx : (Math.random() - 0.5) * speed * 2;
        this.vy = vy !== null ? vy : (Math.random() - 0.5) * speed * 2;
        this.life = 1.0; this.decay = Math.random() * 0.04 + 0.01;
    }

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

    draw() {
        ctx.save(); ctx.globalAlpha = this.life; ctx.fillStyle = this.color;
        ctx.beginPath(); ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); ctx.fill(); ctx.restore();
    }
}

let player; let obstacles = []; let enemyProjectiles = []; let particles = []; let background; let screenShake = 0;

function createExplosion(x, y, color, count) {
    for (let i = 0; i < count; i++) {
        particles.push(new Particle(x, y, color, 10));
        if (Math.random() > 0.4) particles.push(new Particle(x, y, '#ffaa00', 12)); 
    }
}

function shakeScreen(amount) { screenShake = amount; }

function isColliding(rect1, rect2) {
    let r1 = { x: rect1.x + rect1.width * 0.1, y: rect1.y + rect1.height * 0.1, width: rect1.width * 0.8, height: rect1.height * 0.8 };
    return r1.x < rect2.x + rect2.width && r1.x + r1.width > rect2.x && r1.y < rect2.y + rect2.height && r1.y + r1.height > rect2.y;
}

function isIntersecting(rect1, rect2) {
    return rect1.x < rect2.x + rect2.width && rect1.x + rect1.width > rect2.x && rect1.y < rect2.y + rect2.height && rect1.y + rect1.height > rect2.y;
}

function updateHUD() {
    scoreDisplay.innerText = Math.floor(score);
    let hpPercent = (player.hp / player.maxHp) * 100;
    hpBar.style.width = `${hpPercent}%`;
    if (hpPercent > 50) hpBar.style.backgroundColor = '#00ff00'; else if (hpPercent > 20) hpBar.style.backgroundColor = '#ffff00'; else hpBar.style.backgroundColor = '#ff0000';
}

function initGame() {
    initAudio(); player = new Player(); obstacles = []; enemyProjectiles = []; particles = []; background = new Background();
    score = 0; frames = 0; gameSpeed = 2.0; screenShake = 0;
    for(let key in keys) keys[key] = false;
    updateHUD();
}

function handleEnemyProjectiles() {
    let playerAttackRects = player.getAttackRects();

    for (let i = enemyProjectiles.length - 1; i >= 0; i--) {
        let proj = enemyProjectiles[i];
        proj.update(); proj.draw();

        // 相殺判定
        let neutralized = false;
        for (let atkRect of playerAttackRects) {
            if (isIntersecting(atkRect, proj)) {
                createExplosion(proj.x, proj.y + proj.height/2, '#fff', 5);
                playHitSound(); enemyProjectiles.splice(i, 1); score += 50; neutralized = true; break;
            }
        }
        if (neutralized) continue;

        if (isColliding(player, proj)) {
            player.takeDamage(proj.damage);
            score = Math.max(0, score - 50); // 敵の弾に当たるとスコア減少
            createExplosion(player.x + player.width/2, player.y + player.height/2, '#ff0000', 10);
            enemyProjectiles.splice(i, 1); continue;
        }

        if (proj.markedForDeletion) enemyProjectiles.splice(i, 1);
    }
}

function handleObstacles() {
    // 時間経過(frames)で敵の出現頻度が上がるように変更
    let spawnRate = Math.max(100, 250 - Math.floor(frames / 600) * 10);
    
    if (frames % spawnRate === 0 && frames > 100) {
        let rand = Math.random(); let type = 'building_small';
        // ライバル怪獣やビルの出現テーブルも時間経過ベースに変更
        if (frames > 3600 && rand < 0.15) type = 'ghidorah'; // 60秒後からギドラ
        else if (frames > 2400 && rand < 0.25) type = 'mechagodzilla'; // 40秒後からメカゴジラ
        else if (frames > 1200 && rand < 0.4) type = 'mothra'; // 20秒後からモスラ
        else if (frames > 600 && rand < 0.6) type = 'building_tall'; 
        else if (rand < 0.7) type = 'heli';
        obstacles.push(new Obstacle(type));
    }

    let playerAttackRects = player.getAttackRects();

    for (let i = obstacles.length - 1; i >= 0; i--) {
        let obs = obstacles[i];
        obs.update(); obs.draw();

        let hitByAttack = false;
        for (let atkRect of playerAttackRects) {
            if (obs.hitDelay <= 0 && isIntersecting(atkRect, obs)) {
                obs.hp -= atkRect.damage;
                obs.hitDelay = atkRect.hitDelay; 
                playHitSound();
                createExplosion(obs.x, obs.y + obs.height/2, atkRect.type === 'beam' ? '#b000ff' : '#fff', 8); // ビームヒット時は紫の火花
                shakeScreen(atkRect.type === 'beam' ? 2 : 5);
                if (obs.hp <= 0) {
                    createExplosion(obs.x + obs.width/2, obs.y + obs.height/2, obs.color, 40);
                    score += obs.scoreValue; // 倒した時にスコア増加
                    obstacles.splice(i, 1); hitByAttack = true;
                }
                break; 
            }
        }
        if (hitByAttack) continue;

        if (isColliding(player, obs) && obs.hitDelay <= 0) {
            let isMonster = obs.type === 'mothra' || obs.type === 'ghidorah' || obs.type === 'mechagodzilla';
            if (player.vy > 0 && player.y + player.height - player.vy <= obs.y + 20 && !isMonster) {
                createExplosion(obs.x + obs.width/2, obs.y, '#888', 30); player.vy = player.jumpPower * 0.6; score += obs.scoreValue / 2;
                shakeScreen(15); obstacles.splice(i, 1);
            } else {
                player.takeDamage(obs.damage); 
                score = Math.max(0, score - 100); // 障害物や敵に当たると大きくスコア減少
                createExplosion(player.x + player.width/2, player.y + player.height/2, '#ff0000', 15);
                if (!isMonster) obstacles.splice(i, 1); else obs.hitDelay = 30; 
            }
            continue;
        }

        if (obs.markedForDeletion) obstacles.splice(i, 1);
    }
}

function handleParticles() {
    for (let i = particles.length - 1; i >= 0; i--) {
        particles[i].update(); particles[i].draw();
        if (particles[i].life <= 0) particles.splice(i, 1);
    }
}

function animate() {
    if (GAME_STATE !== 'PLAYING') return;
    ctx.save();
    if (screenShake > 0) {
        ctx.translate((Math.random() - 0.5) * screenShake, (Math.random() - 0.5) * screenShake);
        screenShake *= 0.85; if (screenShake < 0.5) screenShake = 0;
    }
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    background.update(); background.draw();
    player.update(); player.draw();
    handleEnemyProjectiles(); handleObstacles(); handleParticles();
    ctx.restore();

    // 歩行によるスコア加算( score += 0.2; )を削除しました
    if (frames % 800 === 0) gameSpeed += 0.1; 
    if (frames % 5 === 0) updateHUD();

    frames++; animationId = requestAnimationFrame(animate);
}

function startGame() {
    GAME_STATE = 'PLAYING';
    startScreen.classList.add('hidden'); gameOverScreen.classList.add('hidden');
    hud.classList.remove('hidden'); mobileControls.classList.remove('hidden');
    initGame(); animate();
}

function gameOver() {
    GAME_STATE = 'GAMEOVER'; cancelAnimationFrame(animationId);
    stopLaserSound(); // 死んだ時に音が鳴り続けないようにする
    createExplosion(player.x + player.width/2, player.y + player.height/2, '#ff3300', 150); handleParticles(); playRoarSound(); 
    setTimeout(() => {
        hud.classList.add('hidden'); mobileControls.classList.add('hidden'); gameOverScreen.classList.remove('hidden');
        finalScoreDisplay.innerText = Math.floor(score);
    }, 1500); 
}

// --- キーボード入力 ---
window.addEventListener('keydown', (e) => {
    if (e.code in keys) keys[e.code] = true;
    if (e.code === 'Enter') keys.KeyZ = true;
    if (e.code === 'ShiftLeft' || e.code === 'ShiftRight') keys.KeyX = true;
    
    if ((e.code === 'KeyX' || e.code === 'ShiftLeft' || e.code === 'ShiftRight') && GAME_STATE === 'PLAYING') player.punch();
    if (e.code === 'KeyC' && GAME_STATE === 'PLAYING') player.kick();

    if (GAME_STATE === 'START' && (e.code === 'Space' || e.code === 'Enter')) startGame();
    if (GAME_STATE === 'GAMEOVER' && (e.code === 'Space' || e.code === 'Enter')) startGame();
});

window.addEventListener('keyup', (e) => {
    if (e.code in keys) keys[e.code] = false;
    if (e.code === 'Enter') keys.KeyZ = false;
    if (e.code === 'ShiftLeft' || e.code === 'ShiftRight') keys.KeyX = false;
});

// --- スマホ用仮想ボタン入力 ---
function bindTouchButton(id, keyName, triggerAction = null) {
    const btn = document.getElementById(id);
    const startAction = (e) => { e.preventDefault(); if (GAME_STATE === 'PLAYING') { keys[keyName] = true; if(triggerAction) triggerAction(); } btn.classList.add('active'); };
    const endAction = (e) => { e.preventDefault(); keys[keyName] = false; btn.classList.remove('active'); };
    btn.addEventListener('touchstart', startAction, {passive: false}); btn.addEventListener('touchend', endAction); btn.addEventListener('touchcancel', endAction);
    btn.addEventListener('mousedown', startAction); btn.addEventListener('mouseup', endAction); btn.addEventListener('mouseleave', endAction);
}

bindTouchButton('btn-left', 'ArrowLeft'); bindTouchButton('btn-right', 'ArrowRight');
bindTouchButton('btn-up', 'ArrowUp'); bindTouchButton('btn-down', 'ArrowDown');
bindTouchButton('btn-jump', 'Space'); bindTouchButton('btn-beam', 'KeyZ');
bindTouchButton('btn-punch', 'KeyX', () => player.punch()); bindTouchButton('btn-kick', 'KeyC', () => player.kick());

document.getElementById('start-btn').addEventListener('click', startGame);
document.getElementById('restart-btn').addEventListener('click', startGame);

background = new Background(); background.draw();

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

ゴジラ

村人からの願い 頑張ってください!! 怪獣全滅目指して!!

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