🔬 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>恐竜王 - リアル無限サバイバル</title>
  <script src="https://cdn.tailwindcss.com"></script>
  <script>
    tailwind.config = {
      theme: {
        extend: {
          animation: {
            'fade-in-up': 'fadeInUp 0.5s ease-out forwards',
            'fade-in': 'fadeIn 0.5s ease-out forwards',
          },
          keyframes: {
            fadeInUp: {
              '0%': { opacity: '0', transform: 'translate(-50%, calc(-50% + 20px))' },
              '100%': { opacity: '1', transform: 'translate(-50%, -50%)' },
            },
            fadeIn: {
              '0%': { opacity: '0' },
              '100%': { opacity: '1' },
            }
          }
        }
      }
    }
  </script>
  <style>
    body { margin: 0; overflow: hidden; background-color: #2a3d20; touch-action: none; }
    /* カスタムアニメーション用のクラス補正 */
    .centered-overlay { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); }
  </style>
</head>
<body class="font-sans select-none text-white">

  <div id="app" class="fixed inset-0 flex items-center justify-center">
    
    <!-- Game Canvas -->
    <canvas id="gameCanvas" class="absolute inset-0 z-0 transition-opacity duration-500 opacity-0 pointer-events-none"></canvas>

    <!-- UI: Boss Warning -->
    <div id="ui-boss-warning" class="centered-overlay z-40 flex flex-col items-center animate-pulse hidden">
      <svg class="text-red-600 mb-2 w-16 h-16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path></svg>
      <h2 class="text-4xl md:text-6xl font-black text-red-600 drop-shadow-[0_0_10px_rgba(220,38,38,0.8)] tracking-widest text-center">
        WARNING<br><span id="boss-name-text">Spinosaurus</span>
      </h2>
    </div>

    <!-- UI: Boss Defeated -->
    <div id="ui-boss-defeated" class="centered-overlay z-40 flex flex-col items-center hidden" style="animation: fadeInUp 0.5s ease-out forwards;">
      <svg class="text-amber-400 mb-2 w-16 h-16" fill="currentColor" viewBox="0 0 24 24"><path d="M21.5 5h-3.14a4.48 4.48 0 0 0-3.36-2H9a4.48 4.48 0 0 0-3.36 2H2.5A1.5 1.5 0 0 0 1 6.5v2.85a7 7 0 0 0 3.32 6.77L6 17.5V19a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2v-1.5l1.68-1.38A7 7 0 0 0 23 9.35V6.5A1.5 1.5 0 0 0 21.5 5zM4 14.28a5 5 0 0 1-1-4.93V7h2.64A12 12 0 0 0 5 11v3.28zM21 9.35a5 5 0 0 1-1 4.93v-3.28a12 12 0 0 0-.64-4H22v2.35z"/></svg>
      <h2 class="text-4xl md:text-6xl font-black text-amber-400 drop-shadow-[0_0_10px_rgba(251,191,36,0.8)] tracking-widest text-center">
        BOSS DEFEATED!<br>
        <span class="text-2xl text-white">見事な狩りだ</span>
      </h2>
    </div>

    <!-- UI: Stage Clear -->
    <div id="ui-stage-clear" class="absolute inset-0 flex flex-col items-center justify-center z-40 bg-stone-950/70 backdrop-blur-sm hidden animate-fade-in">
      <h2 class="text-5xl md:text-7xl font-black text-amber-500 drop-shadow-[0_0_15px_rgba(245,158,11,0.8)] tracking-widest text-center uppercase mb-4">
        Stage Cleared
      </h2>
      <p id="next-level-text" class="text-2xl text-stone-200 tracking-widest uppercase animate-pulse">
        Entering Level 2 ...
      </p>
    </div>

    <!-- UI: Start Screen -->
    <div id="ui-start" class="absolute inset-0 flex flex-col items-center justify-center p-4 bg-black/60 z-50">
      <div class="bg-stone-900 p-8 rounded-xl shadow-2xl border-2 border-stone-700 text-center max-w-lg w-full">
        <h1 class="text-5xl md:text-6xl font-black text-stone-300 mb-2 tracking-widest uppercase" style="font-family: Impact, sans-serif;">
          Jungle Apex
        </h1>
        <p class="text-stone-500 mb-8 font-bold tracking-widest text-sm uppercase">果てしない生存競争</p>
        
        <div class="text-left text-stone-400 bg-stone-950 p-6 rounded-lg mb-8 border border-stone-800">
          <ul class="space-y-4 text-sm">
            <li class="flex items-center">
              <span class="w-8 h-8 flex items-center justify-center bg-stone-800 rounded mr-3">W</span>
              <span><strong>移動:</strong> WASDキー または 矢印キー</span>
            </li>
            <li class="flex items-center">
              <span class="w-16 h-8 flex items-center justify-center bg-stone-800 rounded mr-3 text-xs">SPACE</span>
              <span><strong>噛み付く:</strong> スペースキー</span>
            </li>
            <li class="flex items-center">
              <span class="w-16 h-8 flex items-center justify-center bg-stone-800 rounded mr-3 text-xs">SHIFT</span>
              <span><strong>尻尾攻撃:</strong> Shiftキー または Cキー</span>
            </li>
            <li class="flex items-center text-amber-600/80">
              <span class="text-xl mr-3">🍖</span>
              <span>獲物を狩り、肉を喰らい、スコア1000を目指せ。</span>
            </li>
          </ul>
        </div>

        <button id="btn-start" class="w-full bg-stone-800 hover:bg-stone-700 text-stone-200 font-bold py-4 px-8 rounded text-xl transition-all border border-stone-600 flex items-center justify-center group uppercase tracking-widest">
          <svg class="mr-3 w-6 h-6" fill="currentColor" viewBox="0 0 24 24"><path d="M5 3l14 9-14 9V3z"/></svg>
          Hunt Begins
        </button>
      </div>
    </div>

    <!-- UI: Playing HUD -->
    <div id="ui-playing" class="absolute top-0 left-0 w-full p-4 flex justify-between items-start pointer-events-none z-10 hidden">
      <div class="bg-stone-950/80 p-3 rounded border border-stone-800 pointer-events-auto backdrop-blur-sm">
        <div class="flex items-center gap-2 mb-2">
          <svg class="text-stone-400 w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></svg>
          <span class="text-stone-400 font-bold text-sm tracking-widest uppercase">Health</span>
          <span id="hp-text" class="text-stone-300 font-mono font-bold ml-2">100</span>
        </div>
        <div class="w-32 md:w-48 bg-stone-900 h-2 overflow-hidden border border-stone-800">
          <div id="hp-bar" class="bg-stone-500 h-full transition-all duration-300" style="width: 100%;"></div>
        </div>
      </div>

      <div class="bg-stone-950/80 p-3 px-6 rounded border border-stone-800 pointer-events-auto flex flex-col items-center backdrop-blur-sm absolute left-1/2 transform -translate-x-1/2">
        <span class="text-amber-500/80 text-xs font-bold tracking-widest uppercase mb-1">Level</span>
        <span id="level-text" class="text-amber-400 font-mono font-black text-2xl leading-none">1</span>
      </div>

      <div class="bg-stone-950/80 p-3 px-6 rounded border border-stone-800 pointer-events-auto flex flex-col items-center backdrop-blur-sm">
        <span class="text-stone-500 text-xs font-bold tracking-widest uppercase mb-1">Score</span>
        <span id="score-text" class="text-stone-300 font-mono font-black text-2xl leading-none">0</span>
      </div>
    </div>

    <!-- UI: Game Over -->
    <div id="ui-gameover" class="absolute inset-0 flex items-center justify-center bg-stone-950/90 backdrop-blur-sm z-50 hidden">
      <div class="bg-stone-900 p-8 rounded-xl shadow-2xl border border-stone-800 text-center max-w-sm w-full" style="animation: fadeInUp 0.5s ease-out forwards;">
        <div class="text-stone-500 text-6xl mb-6 flex justify-center">💀</div>
        <h2 class="text-4xl font-black text-stone-400 mb-2 uppercase tracking-widest">Extinct</h2>
        <p class="text-stone-600 mb-8 text-sm">食物連鎖の敗者となった</p>
        
        <div class="bg-stone-950 p-4 rounded mb-8 border border-stone-800">
          <span class="text-stone-500 text-xs tracking-widest uppercase block mb-2">Final Score</span>
          <div id="final-score-text" class="flex justify-center items-center text-3xl font-mono font-bold text-stone-300">
            0
          </div>
        </div>

        <button id="btn-retry" class="w-full bg-stone-800 hover:bg-stone-700 text-stone-300 font-bold py-4 px-8 rounded transition-all flex items-center justify-center uppercase tracking-widest text-sm">
          <svg class="mr-3 w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"></path><path d="M3 3v5h5"></path></svg>
          Rebirth
        </button>
      </div>
    </div>

    <!-- UI: Mobile Controls -->
    <div id="ui-mobile-controls" class="md:hidden absolute bottom-0 left-0 w-full h-48 pointer-events-none z-20 hidden">
      <div class="absolute bottom-8 left-4 w-32 h-32 bg-stone-900/60 rounded-full border border-stone-700 pointer-events-auto backdrop-blur-md">
        <button id="btn-up" class="absolute top-0 left-1/2 -translate-x-1/2 w-10 h-10 bg-stone-500/20 rounded-t active:bg-stone-500/50"></button>
        <button id="btn-down" class="absolute bottom-0 left-1/2 -translate-x-1/2 w-10 h-10 bg-stone-500/20 rounded-b active:bg-stone-500/50"></button>
        <button id="btn-left" class="absolute left-0 top-1/2 -translate-y-1/2 w-10 h-10 bg-stone-500/20 rounded-l active:bg-stone-500/50"></button>
        <button id="btn-right" class="absolute right-0 top-1/2 -translate-y-1/2 w-10 h-10 bg-stone-500/20 rounded-r active:bg-stone-500/50"></button>
      </div>
      
      <button id="btn-tail" class="absolute bottom-10 right-28 w-16 h-16 bg-stone-700/80 border border-stone-500 rounded-full active:bg-stone-600 active:scale-95 pointer-events-auto backdrop-blur-md flex items-center justify-center uppercase text-xs tracking-widest text-stone-300 font-bold">
        Tail
      </button>

      <button id="btn-bite" class="absolute bottom-10 right-4 w-20 h-20 bg-stone-800/80 border border-stone-600 rounded-full active:bg-stone-700 active:scale-95 pointer-events-auto backdrop-blur-md flex items-center justify-center uppercase text-sm tracking-widest text-stone-300 font-bold">
        Bite
      </button>
    </div>

  </div>

  <script>
    // --- ゲーム定数 ---
    const TILE_SIZE = 150;
    const MAX_ENEMIES = 30;
    const SPAWN_RADIUS = 1200; 
    const DESPAWN_RADIUS = 1800; 
    const SCORE_TO_BOSS = 1000; 

    // --- サウンドエンジン ---
    let audioCtx = null;

    const initAudio = () => {
      try {
        if (!audioCtx && (window.AudioContext || window.webkitAudioContext)) {
          audioCtx = new (window.AudioContext || window.webkitAudioContext)();
        }
        if (audioCtx && audioCtx.state === 'suspended') {
          audioCtx.resume();
        }
      } catch (e) {
        console.error("Audio init error", e);
      }
    };

    const playSound = (type) => {
      try {
        if (!audioCtx) return;
        const t = audioCtx.currentTime;
        const osc = audioCtx.createOscillator();
        const gain = audioCtx.createGain();
        
        osc.connect(gain);
        gain.connect(audioCtx.destination);

        switch (type) {
          case 'bite': 
            osc.type = 'sawtooth'; osc.frequency.setValueAtTime(400, t); osc.frequency.exponentialRampToValueAtTime(100, t + 0.1);
            gain.gain.setValueAtTime(0.1, t); gain.gain.exponentialRampToValueAtTime(0.01, t + 0.1);
            osc.start(t); osc.stop(t + 0.1); break;
          case 'tail': 
            osc.type = 'sine'; osc.frequency.setValueAtTime(150, t); osc.frequency.exponentialRampToValueAtTime(40, t + 0.2);
            gain.gain.setValueAtTime(0.2, t); gain.gain.exponentialRampToValueAtTime(0.01, t + 0.2);
            osc.start(t); osc.stop(t + 0.2); break;
          case 'hit': 
            osc.type = 'square'; osc.frequency.setValueAtTime(150, t); osc.frequency.exponentialRampToValueAtTime(40, t + 0.15);
            gain.gain.setValueAtTime(0.2, t); gain.gain.exponentialRampToValueAtTime(0.01, t + 0.15);
            osc.start(t); osc.stop(t + 0.15); break;
          case 'damage': 
            osc.type = 'sawtooth'; osc.frequency.setValueAtTime(200, t); osc.frequency.linearRampToValueAtTime(50, t + 0.3);
            gain.gain.setValueAtTime(0.3, t); gain.gain.exponentialRampToValueAtTime(0.01, t + 0.3);
            const osc2 = audioCtx.createOscillator(); osc2.type = 'square'; osc2.frequency.setValueAtTime(100, t);
            osc2.connect(gain); osc2.start(t); osc2.stop(t + 0.3);
            osc.start(t); osc.stop(t + 0.3); break;
          case 'eat': 
            osc.type = 'sine'; osc.frequency.setValueAtTime(600, t); osc.frequency.linearRampToValueAtTime(800, t + 0.1);
            gain.gain.setValueAtTime(0.1, t); gain.gain.linearRampToValueAtTime(0, t + 0.1);
            osc.start(t); osc.stop(t + 0.1); break;
          case 'roar': 
            osc.type = 'sawtooth'; osc.frequency.setValueAtTime(80, t); osc.frequency.exponentialRampToValueAtTime(30, t + 2.5);
            const lfo = audioCtx.createOscillator(); lfo.type = 'sine'; lfo.frequency.value = 25; 
            const lfoGain = audioCtx.createGain(); lfoGain.gain.value = 30; lfo.connect(lfoGain); lfoGain.connect(osc.frequency);
            gain.gain.setValueAtTime(0, t); gain.gain.linearRampToValueAtTime(0.6, t + 0.2); gain.gain.linearRampToValueAtTime(0.4, t + 1.5); gain.gain.linearRampToValueAtTime(0, t + 2.5);
            lfo.start(t); osc.start(t); lfo.stop(t + 2.5); osc.stop(t + 2.5); break;
          case 'alert': 
            osc.type = 'square'; osc.frequency.setValueAtTime(400, t); osc.frequency.setValueAtTime(600, t + 0.5);
            osc.frequency.setValueAtTime(400, t + 1.0); osc.frequency.setValueAtTime(600, t + 1.5);
            gain.gain.setValueAtTime(0.15, t); gain.gain.linearRampToValueAtTime(0, t + 2.0);
            osc.start(t); osc.stop(t + 2.0); break;
          case 'levelup': 
            osc.type = 'sine'; osc.frequency.setValueAtTime(440, t); osc.frequency.setValueAtTime(554, t + 0.15);
            osc.frequency.setValueAtTime(659, t + 0.3); osc.frequency.setValueAtTime(880, t + 0.45);
            gain.gain.setValueAtTime(0, t); gain.gain.linearRampToValueAtTime(0.15, t + 0.1); gain.gain.linearRampToValueAtTime(0, t + 0.8);
            osc.start(t); osc.stop(t + 0.8); break;
          case 'step': 
            osc.type = 'sine'; osc.frequency.setValueAtTime(60, t); osc.frequency.exponentialRampToValueAtTime(20, t + 0.1);
            gain.gain.setValueAtTime(0.15, t); gain.gain.exponentialRampToValueAtTime(0.01, t + 0.1);
            osc.start(t); osc.stop(t + 0.1); break;
        }
      } catch(e) {}
    };

    // --- 色設定 ---
    const COLORS = {
      grass: '#5c8a3f', grassDark: '#456b2d', player: '#e68a35', playerStripe: '#8a4b18',
      raptor: '#7aa385', raptorStripe: '#4d6954', trike: '#b0b59a', trikeDetail: '#7a7d69',
      bossSpino: '#768a94', bossSpinoSail: '#de5454', bossAnkylo: '#877053', bossAnkyloArmor: '#4a3b2b',
      bossCarno: '#a34736', bossBrachio: '#697a6c', dust: '#c7b497', meat: '#e84f4f', bone: '#ffffff'
    };

    const randomRange = (min, max) => Math.random() * (max - min) + min;
    const distance = (x1, y1, x2, y2) => Math.hypot(x2 - x1, y2 - y1);
    const hashCoord = (x, y) => Math.abs(Math.sin(x * 12.9898 + y * 78.233) * 43758.5453);

    // --- ゲーム状態管理 ---
    const engine = {
      gameState: 'start', // start, playing, gameover
      keys: {},
      time: 0,
      level: 1,
      nextBossScore: SCORE_TO_BOSS,
      currentBossName: '',
      player: { x: 0, y: 0, radius: 25, speed: 7, hp: 100, maxHp: 100, dir: 1, isMoving: false, isAttacking: false, attackTimer: 0, isTailAttacking: false, tailAttackTimer: 0, isRoaring: false, roarTimer: 0 },
      enemies: [], obstacles: [], items: [], particles: [],
      camera: { x: 0, y: 0 },
      lastScoreUpdate: 0,
      bossSpawned: false, bossActive: false,
      isMobileTouch: { up: false, down: false, left: false, right: false, attack: false, tail: false }
    };

    let animationFrameId = null;
    const canvas = document.getElementById('gameCanvas');
    const ctx = canvas.getContext('2d');

    // UI制御関数
    function updateUI() {
      document.getElementById('ui-start').classList.toggle('hidden', engine.gameState !== 'start');
      document.getElementById('ui-playing').classList.toggle('hidden', engine.gameState !== 'playing');
      document.getElementById('ui-gameover').classList.toggle('hidden', engine.gameState !== 'gameover');
      
      const isMobile = window.innerWidth < 768;
      document.getElementById('ui-mobile-controls').classList.toggle('hidden', engine.gameState !== 'playing' || !isMobile);
      
      if (engine.gameState === 'start') {
        canvas.style.opacity = '0';
      } else {
        canvas.style.opacity = '1';
        document.getElementById('hp-text').innerText = Math.max(0, Math.floor(engine.player.hp));
        document.getElementById('hp-bar').style.width = Math.max(0, (engine.player.hp / engine.player.maxHp) * 100) + '%';
        document.getElementById('score-text').innerText = engine.lastScoreUpdate;
        document.getElementById('level-text').innerText = engine.level;
        document.getElementById('final-score-text').innerText = engine.lastScoreUpdate;
      }
    }

    // --- ゲーム初期化 ---
    function initGame() {
      initAudio(); 
      engine.gameState = 'playing';
      engine.level = 1;
      engine.nextBossScore = SCORE_TO_BOSS;
      engine.currentBossName = '';
      engine.player = { x: 0, y: 0, radius: 25, speed: 7, hp: 100, maxHp: 100, dir: 1, isMoving: false, isAttacking: false, attackTimer: 0, isTailAttacking: false, tailAttackTimer: 0, isRoaring: false, roarTimer: 0 };
      engine.enemies = []; engine.obstacles = []; engine.items = []; engine.particles = [];
      engine.time = 0; engine.bossSpawned = false; engine.bossActive = false; engine.lastScoreUpdate = 0;
      engine.camera = { x: 0, y: 0 };

      for (let i = 0; i < 150; i++) spawnObstacle(engine, 0, 0, SPAWN_RADIUS, 300);
      for (let i = 0; i < 15; i++) spawnEnemy(engine, 0, 0, SPAWN_RADIUS, 400);

      document.getElementById('ui-boss-warning').classList.add('hidden');
      document.getElementById('ui-boss-defeated').classList.add('hidden');
      document.getElementById('ui-stage-clear').classList.add('hidden');
      
      updateUI();
    }

    function spawnObstacle(state, px, py, maxRadius, minRadius = 0) {
      const angle = Math.random() * Math.PI * 2; const r = randomRange(minRadius, maxRadius);
      const type = Math.random() > 0.5 ? 'tree' : 'rock';
      state.obstacles.push({ x: px + Math.cos(angle) * r, y: py + Math.sin(angle) * r, radius: type === 'tree' ? randomRange(20, 50) : randomRange(15, 60), type: type });
    }

    function spawnEnemy(state, px, py, maxRadius, minRadius = 0) {
      const angle = Math.random() * Math.PI * 2; const r = randomRange(minRadius || maxRadius * 0.5, maxRadius);
      const type = Math.random() > 0.6 ? 'trike' : 'raptor';
      const hpMulti = 1 + (state.level - 1) * 0.2; const speedMulti = 1 + (state.level - 1) * 0.05;
      state.enemies.push({
        id: Math.random().toString(), type: type, x: px + Math.cos(angle) * r, y: py + Math.sin(angle) * r,
        radius: type === 'trike' ? 35 : 18, speed: (type === 'trike' ? 2.5 : 5.5) * speedMulti,
        hp: (type === 'trike' ? 60 : 30) * hpMulti, maxHp: (type === 'trike' ? 60 : 30) * hpMulti,
        dir: 1, isMoving: true, targetX: null, targetY: null, stateTimer: 0, isBoss: false
      });
    }

    function spawnBoss(state) {
      state.bossSpawned = true; state.bossActive = true;
      const bossIndex = (state.level - 1) % 4;
      let bossType = 'spino'; let radius = 55; let speed = 4.2; let hp = 350; let bossNameStr = 'Spinosaurus';

      if (bossIndex === 0) { bossType = 'spino'; radius = 55; speed = 4.2; hp = 350; bossNameStr = 'Spinosaurus'; } 
      else if (bossIndex === 1) { bossType = 'ankylosaurus'; radius = 48; speed = 2.5; hp = 600; bossNameStr = 'Ankylosaurus'; } 
      else if (bossIndex === 2) { bossType = 'carno'; radius = 40; speed = 6.8; hp = 250; bossNameStr = 'Carnotaurus'; } 
      else if (bossIndex === 3) { bossType = 'brachio'; radius = 80; speed = 1.5; hp = 1000; bossNameStr = 'Brachiosaurus'; }

      state.currentBossName = bossNameStr;
      document.getElementById('boss-name-text').innerText = bossNameStr;

      document.getElementById('ui-boss-warning').classList.remove('hidden');
      playSound('alert'); 
      setTimeout(() => document.getElementById('ui-boss-warning').classList.add('hidden'), 4000);

      const angle = Math.random() * Math.PI * 2; const r = SPAWN_RADIUS * 0.8;
      const hpMulti = 1 + (state.level - 1) * 0.4; const speedMulti = 1 + (state.level - 1) * 0.05;

      state.enemies.push({
        id: `boss_${bossType}_${Date.now()}`, type: bossType,
        x: state.player.x + Math.cos(angle) * r, y: state.player.y + Math.sin(angle) * r,
        radius: radius, speed: speed * speedMulti, hp: hp * hpMulti, maxHp: hp * hpMulti,
        dir: 1, isMoving: true, targetX: null, targetY: null, stateTimer: 0, isBoss: true
      });
    }

    function handleBossDefeat(state, p) {
      state.bossActive = false;
      p.isRoaring = true; p.roarTimer = 300; 
      playSound('roar'); 
      
      document.getElementById('ui-boss-defeated').classList.remove('hidden');
      
      setTimeout(() => {
        document.getElementById('ui-boss-defeated').classList.add('hidden');
        document.getElementById('next-level-text').innerText = `Entering Level ${state.level + 1} ...`;
        document.getElementById('ui-stage-clear').classList.remove('hidden');
        playSound('levelup'); 
        
        setTimeout(() => {
          document.getElementById('ui-stage-clear').classList.add('hidden');
          state.level += 1;
          state.nextBossScore = state.lastScoreUpdate + SCORE_TO_BOSS * state.level;
          state.bossSpawned = false;
          p.maxHp += 20; p.hp = p.maxHp;
          updateUI();
        }, 3000);
      }, 4000);
    }

    function spawnMeat(state, x, y, isBoss) {
      state.items.push({ x, y, radius: isBoss ? 20 : 10, type: 'meat', floatOffset: 0, heal: isBoss ? 100 : 25 });
    }

    function spawnDust(state, x, y, count) {
      for (let i = 0; i < count; i++) {
        state.particles.push({ x, y, vx: randomRange(-6, 6), vy: randomRange(-6, 6), life: 1.0, color: COLORS.dust });
      }
    }

    function tailAttack(state) {
      const p = state.player; p.isTailAttacking = true; p.tailAttackTimer = 20; playSound('tail'); 
      const attackRange = 70; const hitX = p.x - (p.dir * attackRange); const hitY = p.y; const hitRadius = 75;
      if (Math.random() > 0.3) spawnDust(state, hitX, hitY, 5);
      let isHit = false;

      state.obstacles = state.obstacles.filter(obs => {
        if (distance(hitX, hitY, obs.x, obs.y) < hitRadius + obs.radius) { spawnDust(state, obs.x, obs.y, 15); isHit = true; return false; }
        return true;
      });

      state.enemies.forEach(enemy => {
        if (distance(hitX, hitY, enemy.x, enemy.y) < hitRadius + enemy.radius) {
          enemy.hp -= 20; spawnDust(state, enemy.x, enemy.y, 15);
          const knockback = enemy.type === 'brachio' ? 5 : (enemy.isBoss ? 15 : 60);
          enemy.x -= p.dir * knockback; isHit = true;
          if (enemy.hp <= 0) {
            spawnMeat(state, enemy.x, enemy.y, enemy.isBoss);
            state.lastScoreUpdate += (enemy.isBoss ? 2000 : (enemy.type === 'trike' ? 50 : 30));
            if (enemy.isBoss) handleBossDefeat(state, p);
          }
        }
      });
      if (isHit) playSound('hit');
      updateUI();
    }

    function attack(state) {
      const p = state.player; p.isAttacking = true; p.attackTimer = 15; playSound('bite'); 
      const attackRange = 60; const hitX = p.x + (p.dir * attackRange); const hitY = p.y; const hitRadius = 55;
      if (Math.random() > 0.5) spawnDust(state, hitX, hitY, 3);
      let isHit = false;

      state.obstacles = state.obstacles.filter(obs => {
        if (distance(hitX, hitY, obs.x, obs.y) < hitRadius + obs.radius) { spawnDust(state, obs.x, obs.y, 15); isHit = true; return false; }
        return true;
      });

      state.enemies.forEach(enemy => {
        if (distance(hitX, hitY, enemy.x, enemy.y) < hitRadius + enemy.radius) {
          enemy.hp -= 25; spawnDust(state, enemy.x, enemy.y, 15);
          const knockback = enemy.type === 'brachio' ? 0 : (enemy.isBoss ? 5 : 30);
          enemy.x += p.dir * knockback; isHit = true;
          if (enemy.hp <= 0) {
            spawnMeat(state, enemy.x, enemy.y, enemy.isBoss);
            state.lastScoreUpdate += (enemy.isBoss ? 2000 : (enemy.type === 'trike' ? 50 : 30));
            if (enemy.isBoss) handleBossDefeat(state, p);
          }
        }
      });
      if (isHit) playSound('hit');
      updateUI();
    }

    // --- メインループ ---
    function gameLoop() {
      try {
        if (canvas.width !== window.innerWidth || canvas.height !== window.innerHeight) {
          canvas.width = window.innerWidth; canvas.height = window.innerHeight;
        }

        if (engine.gameState === 'playing') {
          const state = engine;
          const p = state.player;
          state.time++;

          let dx = 0; let dy = 0;
          const keys = state.keys; const touch = state.isMobileTouch;

          if (p.isRoaring) {
            p.roarTimer--; if (p.roarTimer <= 0) p.isRoaring = false;
          } else {
            if (keys['KeyW'] || keys['ArrowUp'] || touch.up) dy -= 1;
            if (keys['KeyS'] || keys['ArrowDown'] || touch.down) dy += 1;
            if (keys['KeyA'] || keys['ArrowLeft'] || touch.left) dx -= 1;
            if (keys['KeyD'] || keys['ArrowRight'] || touch.right) dx += 1;
          }

          if (dx !== 0 && dy !== 0) { const len = Math.hypot(dx, dy); dx /= len; dy /= len; }
          p.isMoving = (dx !== 0 || dy !== 0);
          if (dx > 0) p.dir = 1; if (dx < 0) p.dir = -1;

          if (p.isMoving && !p.isRoaring && state.time % 20 === 0) playSound('step');

          let nextX = p.x + dx * p.speed; let nextY = p.y + dy * p.speed;

          state.obstacles.forEach(obs => {
            const dist = distance(nextX, nextY, obs.x, obs.y);
            if (dist < p.radius + obs.radius) {
              const overlap = (p.radius + obs.radius) - dist;
              const angle = Math.atan2(nextY - obs.y, nextX - obs.x);
              nextX += Math.cos(angle) * overlap; nextY += Math.sin(angle) * overlap;
            }
          });

          p.x = nextX; p.y = nextY;

          if (p.isAttacking) { p.attackTimer--; if (p.attackTimer <= 0) p.isAttacking = false; }
          if (p.isTailAttacking) { p.tailAttackTimer--; if (p.tailAttackTimer <= 0) p.isTailAttacking = false; }

          if (state.lastScoreUpdate >= state.nextBossScore && !state.bossSpawned) spawnBoss(state);

          state.obstacles = state.obstacles.filter(o => distance(p.x, p.y, o.x, o.y) < DESPAWN_RADIUS);
          state.enemies = state.enemies.filter(e => distance(p.x, p.y, e.x, e.y) < DESPAWN_RADIUS || e.isBoss);
          state.items = state.items.filter(i => distance(p.x, p.y, i.x, i.y) < DESPAWN_RADIUS);

          while (state.obstacles.length < 250) spawnObstacle(state, p.x, p.y, SPAWN_RADIUS, SPAWN_RADIUS - 200);

          const currentMaxEnemies = MAX_ENEMIES + (state.level - 1) * 5;
          if (!state.bossActive && state.enemies.length < currentMaxEnemies && Math.random() < 0.05) spawnEnemy(state, p.x, p.y, SPAWN_RADIUS, SPAWN_RADIUS - 200);

          state.enemies = state.enemies.filter(e => e.hp > 0);
          state.enemies.forEach(enemy => {
            enemy.stateTimer--; let eDx = 0; let eDy = 0; const distToPlayer = distance(enemy.x, enemy.y, p.x, p.y);

            if (enemy.type === 'raptor' || enemy.type === 'spino' || enemy.type === 'carno') {
              const aggroRange = enemy.isBoss ? 1500 : 600;
              if (distToPlayer < aggroRange) {
                eDx = p.x - enemy.x; eDy = p.y - enemy.y;
                if (distToPlayer < p.radius + enemy.radius + 5 && state.time % (enemy.isBoss ? 40 : 30) === 0) {
                  const dmgMulti = 1 + (state.level - 1) * 0.2;
                  p.hp -= (enemy.isBoss ? 15 : 10) * dmgMulti;
                  spawnDust(state, p.x, p.y, 8); playSound('damage'); updateUI(); 
                }
              } else {
                if (enemy.stateTimer <= 0) {
                  enemy.targetX = enemy.x + randomRange(-150, 150); enemy.targetY = enemy.y + randomRange(-150, 150);
                  enemy.stateTimer = randomRange(40, 90);
                }
                if (enemy.targetX) { eDx = enemy.targetX - enemy.x; eDy = enemy.targetY - enemy.y; }
              }
            } else if (enemy.type === 'ankylosaurus' || enemy.type === 'brachio' || enemy.type === 'trike') {
              if (enemy.stateTimer <= 0) {
                 enemy.targetX = enemy.x + randomRange(-100, 100); enemy.targetY = enemy.y + randomRange(-100, 100);
                 enemy.stateTimer = randomRange(80, 150);
              }
              if (enemy.targetX) { eDx = enemy.targetX - enemy.x; eDy = enemy.targetY - enemy.y; }
              if (distToPlayer < p.radius + enemy.radius && state.time % 60 === 0) {
                  const dmgMulti = 1 + (state.level - 1) * 0.2;
                  p.hp -= (enemy.isBoss ? 25 : 15) * dmgMulti; 
                  spawnDust(state, p.x, p.y, 10); playSound('damage'); updateUI();
              }
            }

            if (enemy.targetX !== null) {
              const dx = enemy.targetX - enemy.x; const dy = enemy.targetY - enemy.y; const len = Math.hypot(dx, dy);
              if (len <= enemy.speed) {
                enemy.x = enemy.targetX; enemy.y = enemy.targetY; enemy.targetX = null; enemy.targetY = null; enemy.isMoving = false;
              } else {
                enemy.x += (dx / len) * enemy.speed; enemy.y += (dy / len) * enemy.speed;
                if (dx > 0) enemy.dir = 1; if (dx < 0) enemy.dir = -1; enemy.isMoving = true;
              }
            } else if (eDx !== 0 || eDy !== 0) {
               const len = Math.hypot(eDx, eDy);
               if (len > 0) {
                 enemy.x += (eDx / len) * enemy.speed; enemy.y += (eDy / len) * enemy.speed;
                 if (eDx > 0) enemy.dir = 1; if (eDx < 0) enemy.dir = -1; enemy.isMoving = true;
               }
            } else { enemy.isMoving = false; }
          });

          state.items.forEach((item, index) => {
            item.floatOffset = Math.sin(state.time * 0.1) * 4;
            if (distance(p.x, p.y, item.x, item.y) < p.radius + item.radius + 15) {
              p.hp = Math.min(p.maxHp, p.hp + item.heal); state.items.splice(index, 1); playSound('eat'); updateUI();
            }
          });

          state.particles.forEach(pt => { pt.x += pt.vx; pt.y += pt.vy; pt.life -= 0.04; });
          state.particles = state.particles.filter(pt => pt.life > 0);

          state.camera.x += (p.x - canvas.width / 2 - state.camera.x) * 0.1;
          state.camera.y += (p.y - canvas.height / 2 - state.camera.y) * 0.1;

          if (p.hp <= 0) { state.gameState = 'gameover'; updateUI(); }
          
          if (state.time % 10 === 0) updateUI(); // 定期的にUI更新(HPバーなど)
        }
        
        if (engine.gameState !== 'start') drawFrame(engine, ctx, canvas);

      } catch (e) { console.error("Game Loop Error:", e); }
      animationFrameId = requestAnimationFrame(gameLoop);
    }

    function drawFrame(state, ctx, canvas) {
      ctx.fillStyle = COLORS.grass; ctx.fillRect(0, 0, canvas.width, canvas.height);
      ctx.save();
      const camX = Math.floor(state.camera.x); const camY = Math.floor(state.camera.y);
      ctx.translate(-camX, -camY);

      const startX = Math.floor(camX / TILE_SIZE) * TILE_SIZE - TILE_SIZE;
      const startY = Math.floor(camY / TILE_SIZE) * TILE_SIZE - TILE_SIZE;
      const endX = camX + canvas.width + TILE_SIZE;
      const endY = camY + canvas.height + TILE_SIZE;

      ctx.fillStyle = COLORS.grassDark;
      for (let x = startX; x < endX; x += TILE_SIZE) {
        for (let y = startY; y < endY; y += TILE_SIZE) {
          const hash = hashCoord(x, y);
          if (hash % 1 > 0.6) {
            ctx.beginPath(); ctx.ellipse(x + (hash % 50), y + ((hash*3) % 50), 12 + (hash % 10), 8 + (hash % 5), hash, 0, Math.PI*2); ctx.fill();
          }
        }
      }

      state.items.forEach(item => {
        ctx.save(); ctx.translate(Math.floor(item.x), Math.floor(item.y + item.floatOffset));
        const s = item.type === 'meat' && item.heal > 50 ? 1.5 : 1; ctx.scale(s, s);
        ctx.fillStyle = COLORS.meat; ctx.beginPath(); ctx.ellipse(0, 0, 16, 10, 0, 0, Math.PI*2); ctx.fill();
        ctx.fillStyle = COLORS.bone; ctx.fillRect(-24, -3, 48, 6);
        ctx.beginPath(); ctx.arc(-24, -4, 5, 0, Math.PI*2); ctx.fill(); ctx.beginPath(); ctx.arc(-24, 4, 5, 0, Math.PI*2); ctx.fill();
        ctx.beginPath(); ctx.arc(24, -4, 5, 0, Math.PI*2); ctx.fill(); ctx.beginPath(); ctx.arc(24, 4, 5, 0, Math.PI*2); ctx.fill();
        ctx.restore();
      });

      const renderables = [ ...state.obstacles.map(o => ({...o, rType: 'obstacle'})), ...state.enemies.map(e => ({...e, rType: 'enemy'})), { ...state.player, rType: 'player' } ].sort((a, b) => a.y - b.y);

      renderables.forEach(obj => {
        const ex = Math.floor(obj.x); const ey = Math.floor(obj.y);
        if (obj.rType === 'obstacle') {
          ctx.fillStyle = 'rgba(0, 0, 0, 0.2)'; ctx.beginPath(); ctx.ellipse(ex + 10, ey + 10, obj.radius, obj.radius * 0.7, 0, 0, Math.PI * 2); ctx.fill();
          if (obj.type === 'rock') {
            ctx.fillStyle = '#7a7a7a'; ctx.beginPath(); ctx.arc(ex, ey, obj.radius, 0, Math.PI * 2); ctx.fill();
            ctx.fillStyle = '#5c5c5c'; ctx.beginPath(); ctx.arc(ex - obj.radius*0.2, ey + obj.radius*0.2, obj.radius * 0.7, 0, Math.PI * 2); ctx.fill();
            ctx.fillStyle = '#949494'; ctx.beginPath(); ctx.arc(ex + obj.radius*0.3, ey - obj.radius*0.3, obj.radius * 0.4, 0, Math.PI * 2); ctx.fill();
          } else {
            ctx.fillStyle = '#4a2e16'; ctx.fillRect(ex - 8, ey - 10, 16, obj.radius + 10);
            ctx.fillStyle = '#2d4d22'; ctx.beginPath(); ctx.arc(ex, ey - obj.radius * 0.3, obj.radius, 0, Math.PI * 2); ctx.fill();
            ctx.fillStyle = '#3f6b30'; ctx.beginPath(); ctx.arc(ex - obj.radius*0.2, ey - obj.radius * 0.5, obj.radius*0.7, 0, Math.PI * 2); ctx.fill();
          }
        } else if (obj.rType === 'player' || obj.rType === 'enemy') {
          drawDinosaur(ctx, obj, state.time);
        }
      });

      state.particles.forEach(p => {
        ctx.fillStyle = p.color; ctx.globalAlpha = Math.max(0, p.life);
        ctx.beginPath(); ctx.arc(Math.floor(p.x), Math.floor(p.y), 4 + (1 - p.life)*5, 0, Math.PI * 2); ctx.fill();
        ctx.globalAlpha = 1.0;
      });
      ctx.restore();
    }

    function drawDinosaur(ctx, entity, time) {
      ctx.save();
      const isHeavy = entity.type === 'trike' || entity.type === 'spino' || entity.type === 'ankylosaurus' || entity.type === 'brachio';
      const walkSpeed = isHeavy ? 0.15 : 0.3; const walkAmp = isHeavy ? 3 : 5;
      const breathOffset = entity.isMoving ? 0 : Math.floor(Math.sin(time * 0.05) * 1.5);
      const walkBob = entity.isMoving ? Math.floor(Math.sin(time * walkSpeed) * walkAmp) : 0;
      const walkTilt = entity.isMoving ? Math.sin(time * (walkSpeed/2)) * 0.05 : 0;
      const ex = Math.floor(entity.x); const ey = Math.floor(entity.y + walkBob + breathOffset);

      ctx.fillStyle = 'rgba(0,0,0,0.2)'; ctx.beginPath(); ctx.ellipse(ex + 8, Math.floor(entity.y) + 12, entity.radius * 1.2, entity.radius * 0.6, 0, 0, Math.PI * 2); ctx.fill();
      ctx.translate(ex, ey); ctx.scale(entity.dir, 1); ctx.rotate(walkTilt);

      if (entity.rType === 'player') {
        ctx.fillStyle = COLORS.player;
        let tailSwing = entity.isMoving ? Math.sin(time * walkSpeed) * 10 : Math.sin(time * 0.05) * 3;
        if (entity.isTailAttacking) {
          const t = 20 - entity.tailAttackTimer;
          if (t < 5) tailSwing = -15; else if (t < 12) tailSwing = 40; else tailSwing = 40 - (t - 12) * 5; 
          if (t > 3 && t < 15) { ctx.save(); ctx.beginPath(); ctx.arc(-35, 0, 50, Math.PI * 0.7, Math.PI * 1.3, false); ctx.lineWidth = 12; ctx.strokeStyle = `rgba(200, 200, 200, ${1 - t/15})`; ctx.stroke(); ctx.restore(); }
        }
        ctx.beginPath(); ctx.moveTo(-10, -5); ctx.quadraticCurveTo(-45, -5 + tailSwing, -80, -2 + tailSwing * 1.5); ctx.lineWidth = 14; ctx.strokeStyle = COLORS.player; ctx.lineCap = 'round'; ctx.stroke();
        ctx.beginPath(); ctx.moveTo(25, -12); ctx.lineTo(10, -18); ctx.lineTo(-20, -15); ctx.lineTo(-35, -8); ctx.lineTo(-25, 5); ctx.lineTo(0, 12); ctx.lineTo(20, 2); ctx.fill();
        ctx.strokeStyle = COLORS.playerStripe; ctx.lineWidth = 3; ctx.beginPath(); ctx.moveTo(-15, -15); ctx.lineTo(-10, -2); ctx.stroke(); ctx.beginPath(); ctx.moveTo(-5, -17); ctx.lineTo(0, 0); ctx.stroke(); ctx.beginPath(); ctx.moveTo(5, -15); ctx.lineTo(10, 2); ctx.stroke();
        const armBob = entity.isMoving ? Math.floor(Math.sin(time * walkSpeed) * 2) : 0;
        ctx.fillStyle = '#3a2e22'; ctx.beginPath(); ctx.moveTo(10, 5 + armBob); ctx.lineTo(13, 12 + armBob); ctx.lineTo(17, 14 + armBob); ctx.lineTo(15, 15 + armBob); ctx.lineTo(11, 12 + armBob); ctx.lineTo(8, 6 + armBob); ctx.fill();
        const legOffset = entity.isMoving ? Math.floor(Math.sin(time * walkSpeed) * 14) : 0;
        ctx.fillStyle = '#2c2219'; ctx.beginPath(); ctx.moveTo(-10 - legOffset * 0.5, 5); ctx.lineTo(-2 - legOffset, 18); ctx.lineTo(-8 - legOffset, 28); ctx.lineTo(2 - legOffset, 30); ctx.lineTo(-12 - legOffset, 28); ctx.lineTo(-18 - legOffset * 0.5, 5); ctx.fill();
        ctx.fillStyle = '#403224'; ctx.beginPath(); ctx.moveTo(6 + legOffset * 0.5, 5); ctx.lineTo(14 + legOffset, 20); ctx.lineTo(8 + legOffset, 30); ctx.lineTo(18 + legOffset, 32); ctx.lineTo(4 + legOffset, 30); ctx.lineTo(-2 + legOffset * 0.5, 5); ctx.fill();
        ctx.fillStyle = COLORS.player; const jawDrop = entity.isRoaring ? 25 : (entity.isAttacking ? 15 : 0);
        ctx.save(); if (entity.isRoaring) { ctx.translate(20, -5); ctx.rotate(-Math.PI / 4 + Math.sin(time * 2.0) * 0.05); ctx.translate(-20, 5); }
        ctx.beginPath(); ctx.moveTo(20, -12); ctx.lineTo(40, -8); ctx.lineTo(38, 2); ctx.lineTo(20, 4); ctx.fill();
        ctx.fillStyle = '#f5f5f4'; for(let i=0; i<5; i++) { ctx.beginPath(); ctx.moveTo(23 + i*4, 2); ctx.lineTo(25 + i*4, 10); ctx.lineTo(27 + i*4, 2); ctx.fill(); }
        ctx.fillStyle = '#eab308'; ctx.beginPath(); ctx.arc(28, -8, 4, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = 'black'; ctx.beginPath(); ctx.arc(29, -8, 2, 0, Math.PI*2); ctx.fill();
        ctx.strokeStyle = '#251d16'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(24, -13); ctx.lineTo(32, -10); ctx.stroke();
        ctx.save(); ctx.translate(20, 2); ctx.rotate(jawDrop * 0.05); ctx.fillStyle = COLORS.player; ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(18, 0); ctx.lineTo(15, 5); ctx.lineTo(0, 4); ctx.fill();
        ctx.fillStyle = '#f5f5f4'; if(entity.isAttacking || entity.isRoaring) { for(let i=0; i<4; i++) { ctx.beginPath(); ctx.moveTo(2 + i*4, 0); ctx.lineTo(4 + i*4, -8); ctx.lineTo(6 + i*4, 0); ctx.fill(); } }
        ctx.restore(); ctx.restore(); 

      } else if (entity.type === 'spino') {
        ctx.fillStyle = COLORS.bossSpino; const tailSwing = entity.isMoving ? Math.sin(time * walkSpeed) * 15 : Math.sin(time * 0.1) * 5;
        ctx.beginPath(); ctx.moveTo(-20, -5); ctx.quadraticCurveTo(-60, -5 + tailSwing, -100, tailSwing * 1.8); ctx.lineWidth = 18; ctx.strokeStyle = COLORS.bossSpino; ctx.lineCap = 'round'; ctx.stroke();
        ctx.beginPath(); ctx.moveTo(35, -10); ctx.lineTo(10, -15); ctx.lineTo(-25, -12); ctx.lineTo(-40, -5); ctx.lineTo(-20, 10); ctx.lineTo(15, 12); ctx.lineTo(30, 0); ctx.fill();
        ctx.fillStyle = COLORS.bossSpinoSail; ctx.beginPath(); ctx.moveTo(-25, -12); ctx.quadraticCurveTo(-10, -45, 5, -42); ctx.quadraticCurveTo(15, -35, 20, -12); ctx.fill();
        ctx.strokeStyle = '#4a1b1b'; ctx.lineWidth = 2; for(let i=-20; i<=15; i+=6) { const height = -12 - Math.max(0, Math.sin((i+25)/45 * Math.PI)) * 28; ctx.beginPath(); ctx.moveTo(i, -12); ctx.lineTo(i + 2, height); ctx.stroke(); }
        const armOffset = entity.isMoving ? Math.floor(Math.sin(time * walkSpeed + Math.PI) * 5) : 0;
        ctx.fillStyle = '#222729'; ctx.beginPath(); ctx.moveTo(15 + armOffset, 5); ctx.lineTo(20 + armOffset, 20); ctx.lineTo(30 + armOffset, 25); ctx.lineTo(35 + armOffset, 30); ctx.lineTo(30 + armOffset, 28); ctx.lineTo(37 + armOffset, 33); ctx.lineTo(28 + armOffset, 26); ctx.lineTo(18 + armOffset, 18); ctx.lineTo(10 + armOffset, 5); ctx.fill();
        const legOffset = entity.isMoving ? Math.floor(Math.sin(time * walkSpeed) * 16) : 0;
        ctx.fillStyle = '#1c2022'; ctx.beginPath(); ctx.moveTo(-10 - legOffset * 0.5, 5); ctx.lineTo(0 - legOffset, 20); ctx.lineTo(-6 - legOffset, 30); ctx.lineTo(6 - legOffset, 32); ctx.lineTo(-12 - legOffset, 30); ctx.lineTo(-20 - legOffset * 0.5, 5); ctx.fill();
        ctx.fillStyle = '#272d30'; ctx.beginPath(); ctx.moveTo(5 + legOffset * 0.5, 5); ctx.lineTo(15 + legOffset, 22); ctx.lineTo(6 + legOffset, 32); ctx.lineTo(18 + legOffset, 35); ctx.lineTo(0 + legOffset, 32); ctx.lineTo(-8 + legOffset * 0.5, 5); ctx.fill();
        ctx.fillStyle = COLORS.bossSpino; ctx.fillRect(30, -10, 32, 8); ctx.fillRect(30, -1, 30, 6);   
        ctx.beginPath(); ctx.arc(60, -6, 5, 0, Math.PI*2); ctx.fill();
        ctx.fillStyle = '#f5f5f4'; for(let i=0; i<8; i++) { ctx.beginPath(); ctx.moveTo(32 + i*4, -2); ctx.lineTo(34 + i*4, 4); ctx.lineTo(36 + i*4, -2); ctx.fill(); }
        ctx.fillStyle = '#fbbf24'; ctx.beginPath(); ctx.arc(40, -8, 3, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = 'black'; ctx.beginPath(); ctx.arc(41, -8, 1.5, 0, Math.PI*2); ctx.fill();

      } else if (entity.type === 'ankylosaurus') {
        ctx.fillStyle = COLORS.bossAnkylo; const tailSwing = entity.isMoving ? Math.sin(time * walkSpeed) * 12 : Math.sin(time * 0.1) * 3;
        ctx.beginPath(); ctx.moveTo(-20, 0); ctx.quadraticCurveTo(-40, tailSwing, -60, tailSwing * 1.5); ctx.lineWidth = 14; ctx.strokeStyle = COLORS.bossAnkylo; ctx.lineCap = 'round'; ctx.stroke();
        ctx.fillStyle = COLORS.bossAnkyloArmor; ctx.beginPath(); ctx.arc(-60, tailSwing * 1.5, 12, 0, Math.PI*2); ctx.fill();
        ctx.beginPath(); ctx.ellipse(0, 0, 35, 22, 0, 0, Math.PI * 2); ctx.fill();
        ctx.fillStyle = COLORS.bossAnkyloArmor;
        for(let i = -20; i <= 20; i += 12) { ctx.beginPath(); ctx.moveTo(i, -18); ctx.lineTo(i-5, -28); ctx.lineTo(i+5, -28); ctx.fill(); ctx.beginPath(); ctx.moveTo(i, 18); ctx.lineTo(i-5, 28); ctx.lineTo(i+5, 28); ctx.fill(); }
        const legOffset = entity.isMoving ? Math.floor(Math.sin(time * walkSpeed) * 10) : 0;
        ctx.fillStyle = '#4a3824'; ctx.fillRect(-20 - legOffset * 0.5, 10, 14, 18); ctx.fillRect(10 + legOffset * 0.5, 10, 14, 18);
        ctx.fillStyle = '#634e38'; ctx.fillRect(-10 + legOffset * 0.5, 12, 14, 18); ctx.fillRect(20 - legOffset * 0.5, 12, 14, 18);
        ctx.fillStyle = COLORS.bossAnkylo; ctx.beginPath(); ctx.ellipse(35, 0, 15, 12, 0, 0, Math.PI*2); ctx.fill();
        ctx.fillStyle = COLORS.bossAnkyloArmor; ctx.beginPath(); ctx.moveTo(40, -10); ctx.lineTo(35, -20); ctx.lineTo(45, -10); ctx.fill(); ctx.beginPath(); ctx.moveTo(40, 10); ctx.lineTo(35, 20); ctx.lineTo(45, 10); ctx.fill();
        ctx.fillStyle = '#eab308'; ctx.beginPath(); ctx.arc(42, -5, 2.5, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = 'black'; ctx.beginPath(); ctx.arc(42.5, -5, 1, 0, Math.PI*2); ctx.fill();

      } else if (entity.type === 'carno') {
        ctx.fillStyle = COLORS.bossCarno; const tailSwing = entity.isMoving ? Math.sin(time * 0.4) * 12 : Math.sin(time * 0.1) * 3;
        ctx.beginPath(); ctx.moveTo(-10, -5); ctx.quadraticCurveTo(-40, -5 + tailSwing, -70, -2 + tailSwing * 1.5); ctx.lineWidth = 12; ctx.strokeStyle = COLORS.bossCarno; ctx.lineCap = 'round'; ctx.stroke();
        ctx.beginPath(); ctx.moveTo(20, -10); ctx.lineTo(5, -15); ctx.lineTo(-15, -12); ctx.lineTo(-30, -8); ctx.lineTo(-20, 5); ctx.lineTo(0, 10); ctx.lineTo(15, 2); ctx.fill();
        const legOffset = entity.isMoving ? Math.floor(Math.sin(time * 0.4) * 16) : 0;
        ctx.fillStyle = '#5c2217'; ctx.beginPath(); ctx.moveTo(-10 - legOffset * 0.5, 5); ctx.lineTo(-2 - legOffset, 18); ctx.lineTo(-8 - legOffset, 28); ctx.lineTo(2 - legOffset, 30); ctx.lineTo(-12 - legOffset, 28); ctx.lineTo(-18 - legOffset * 0.5, 5); ctx.fill();
        ctx.fillStyle = '#7a2f20'; ctx.beginPath(); ctx.moveTo(6 + legOffset * 0.5, 5); ctx.lineTo(14 + legOffset, 20); ctx.lineTo(8 + legOffset, 30); ctx.lineTo(18 + legOffset, 32); ctx.lineTo(4 + legOffset, 30); ctx.lineTo(-2 + legOffset * 0.5, 5); ctx.fill();
        ctx.fillStyle = COLORS.bossCarno; ctx.beginPath(); ctx.moveTo(15, -10); ctx.lineTo(32, -6); ctx.lineTo(30, 4); ctx.lineTo(15, 4); ctx.fill();
        ctx.fillStyle = '#e8dcc5'; ctx.beginPath(); ctx.moveTo(22, -10); ctx.lineTo(28, -22); ctx.lineTo(28, -8); ctx.fill(); ctx.beginPath(); ctx.moveTo(18, -10); ctx.lineTo(24, -20); ctx.lineTo(24, -8); ctx.fill();
        ctx.fillStyle = '#f5f5f4'; for(let i=0; i<4; i++) { ctx.beginPath(); ctx.moveTo(20 + i*3, 2); ctx.lineTo(21 + i*3, 8); ctx.lineTo(23 + i*3, 2); ctx.fill(); }
        ctx.fillStyle = '#eab308'; ctx.beginPath(); ctx.arc(26, -4, 2.5, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = 'black'; ctx.beginPath(); ctx.arc(26.5, -4, 1.5, 0, Math.PI*2); ctx.fill();

      } else if (entity.type === 'brachio') {
        ctx.fillStyle = COLORS.bossBrachio; const tailSwing = entity.isMoving ? Math.sin(time * walkSpeed) * 8 : Math.sin(time * 0.05) * 2;
        ctx.beginPath(); ctx.moveTo(-30, -10); ctx.quadraticCurveTo(-70, -10 + tailSwing, -110, tailSwing); ctx.lineWidth = 16; ctx.strokeStyle = COLORS.bossBrachio; ctx.lineCap = 'round'; ctx.stroke();
        const neckBob = entity.isMoving ? Math.sin(time * walkSpeed) * 4 : Math.sin(time * 0.05) * 2;
        ctx.beginPath(); ctx.moveTo(30, -15); ctx.quadraticCurveTo(60, -40 + neckBob, 75, -70 + neckBob); ctx.lineWidth = 18; ctx.strokeStyle = COLORS.bossBrachio; ctx.lineCap = 'round'; ctx.stroke();
        ctx.beginPath(); ctx.ellipse(80, -75 + neckBob, 12, 8, Math.PI/6, 0, Math.PI*2); ctx.fill();
        ctx.fillStyle = '#eab308'; ctx.beginPath(); ctx.arc(84, -78 + neckBob, 2.5, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = 'black'; ctx.beginPath(); ctx.arc(84, -78 + neckBob, 1.5, 0, Math.PI*2); ctx.fill();
        ctx.fillStyle = COLORS.bossBrachio; ctx.beginPath(); ctx.ellipse(0, 0, 45, 35, 0, 0, Math.PI * 2); ctx.fill();
        const legOffset = entity.isMoving ? Math.floor(Math.sin(time * walkSpeed) * 10) : 0;
        ctx.fillStyle = '#3a453c'; ctx.fillRect(-25 - legOffset * 0.5, 15, 18, 30); ctx.fillRect(15 + legOffset * 0.5, 15, 18, 40);
        ctx.fillStyle = '#4b574d'; ctx.fillRect(-15 + legOffset * 0.5, 20, 18, 30); ctx.fillRect(25 - legOffset * 0.5, 20, 18, 40);

      } else if (entity.type === 'raptor') {
        ctx.fillStyle = COLORS.raptor; const tailSwing = entity.isMoving ? Math.sin(time * 0.5) * 12 : 0;
        ctx.beginPath(); ctx.moveTo(-5, -2); ctx.lineTo(-45, -2 + tailSwing); ctx.lineWidth = 6; ctx.strokeStyle = COLORS.raptor; ctx.stroke();
        ctx.beginPath(); ctx.moveTo(12, -8); ctx.lineTo(-10, -6); ctx.lineTo(-5, 4); ctx.lineTo(10, 2); ctx.fill();
        const legOffset = entity.isMoving ? Math.floor(Math.sin(time * 0.6) * 12) : 0;
        ctx.fillStyle = '#1e211b'; ctx.beginPath(); ctx.moveTo(-4 - legOffset * 0.5, 2); ctx.lineTo(2 - legOffset, 12); ctx.lineTo(-2 - legOffset, 18); ctx.lineTo(4 - legOffset, 20); ctx.lineTo(-6 - legOffset, 18); ctx.lineTo(-8 - legOffset * 0.5, 2); ctx.fill();
        ctx.fillStyle = '#292d25'; ctx.beginPath(); ctx.moveTo(8 + legOffset * 0.5, 2); ctx.lineTo(14 + legOffset, 12); ctx.lineTo(10 + legOffset, 18); ctx.lineTo(16 + legOffset, 20); ctx.lineTo(12 + legOffset, 14); ctx.lineTo(20 + legOffset, 16); ctx.lineTo(10 + legOffset, 18); ctx.lineTo(6 + legOffset, 18); ctx.lineTo(4 + legOffset * 0.5, 2); ctx.fill();
        ctx.fillRect(12, -10, 14, 7); 
        ctx.fillStyle = '#f5f5f4'; for(let i=0; i<3; i++) { ctx.beginPath(); ctx.moveTo(15 + i*4, -3); ctx.lineTo(17 + i*4, 2); ctx.lineTo(19 + i*4, -3); ctx.fill(); }
        ctx.fillStyle = '#ef4444'; ctx.beginPath(); ctx.arc(20, -7, 2.5, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = 'black'; ctx.beginPath(); ctx.arc(21, -7, 1, 0, Math.PI*2); ctx.fill();
        
      } else if (entity.type === 'trike') {
        ctx.fillStyle = COLORS.trike; ctx.beginPath(); ctx.moveTo(25, -12); ctx.lineTo(0, -20); ctx.lineTo(-30, -5); ctx.lineTo(-35, 8); ctx.lineTo(20, 12); ctx.fill();
        ctx.beginPath(); ctx.moveTo(-30, -5); ctx.lineTo(-45, 2); ctx.lineTo(-35, 8); ctx.fill();
        const legOffset = entity.isMoving ? Math.floor(Math.sin(time * walkSpeed) * 8) : 0;
        ctx.fillStyle = '#2b2b2b'; ctx.beginPath(); ctx.moveTo(-20 + legOffset, 8); ctx.lineTo(-14 + legOffset, 22); ctx.lineTo(-24 + legOffset, 22); ctx.lineTo(-26 + legOffset, 8); ctx.fill(); ctx.beginPath(); ctx.moveTo(15 + legOffset, 8); ctx.lineTo(21 + legOffset, 22); ctx.lineTo(11 + legOffset, 22); ctx.lineTo(9 + legOffset, 8); ctx.fill();
        ctx.fillStyle = '#3a3a3a'; ctx.beginPath(); ctx.moveTo(-6 - legOffset, 10); ctx.lineTo(-2 - legOffset, 26); ctx.lineTo(-14 - legOffset, 26); ctx.lineTo(-14 - legOffset, 10); ctx.fill(); ctx.beginPath(); ctx.moveTo(25 - legOffset, 10); ctx.lineTo(29 - legOffset, 26); ctx.lineTo(17 - legOffset, 26); ctx.lineTo(17 - legOffset, 10); ctx.fill();
        ctx.fillStyle = COLORS.trike; ctx.beginPath(); ctx.moveTo(20, -8); ctx.quadraticCurveTo(15, -35, 35, -30); ctx.quadraticCurveTo(40, -15, 30, 5); ctx.fill();
        ctx.fillStyle = '#8f8c83'; ctx.beginPath(); ctx.moveTo(42, -4); ctx.lineTo(48, 4); ctx.lineTo(38, 6); ctx.fill();
        ctx.fillStyle = COLORS.trike; ctx.beginPath(); ctx.moveTo(25, -12); ctx.lineTo(45, -5); ctx.lineTo(30, 8); ctx.fill();
        ctx.fillStyle = '#b5b2a8'; ctx.beginPath(); ctx.moveTo(35, -15); ctx.lineTo(60, -20); ctx.lineTo(38, -8); ctx.fill(); ctx.beginPath(); ctx.moveTo(32, -10); ctx.lineTo(50, -12); ctx.lineTo(35, -4); ctx.fill(); ctx.beginPath(); ctx.moveTo(42, 0); ctx.lineTo(52, -2); ctx.lineTo(42, 4); ctx.fill(); 
        ctx.fillStyle = '#eab308'; ctx.beginPath(); ctx.arc(36, -6, 2.5, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = 'black'; ctx.beginPath(); ctx.arc(36, -6, 1.5, 0, Math.PI*2); ctx.fill();
      }

      if (entity.rType === 'enemy' && entity.hp < entity.maxHp) {
        ctx.fillStyle = 'rgba(0,0,0,0.5)'; ctx.fillRect(-15, -35, 30, 4);
        ctx.fillStyle = entity.isBoss ? '#b33939' : '#8f8f8f'; ctx.fillRect(-15, -35, 30 * (entity.hp / entity.maxHp), 4);
      }
      ctx.restore();
    }

    // --- イベントリスナー設定 ---
    window.addEventListener('keydown', (e) => {
      engine.keys[e.code] = true;
      if (engine.gameState !== 'playing') return;
      const p = engine.player;
      if (e.code === 'Space' && !p.isAttacking && !p.isTailAttacking && !p.isRoaring) attack(engine);
      if ((e.code === 'ShiftLeft' || e.code === 'ShiftRight' || e.code === 'KeyC') && !p.isAttacking && !p.isTailAttacking && !p.isRoaring) tailAttack(engine);
    });
    window.addEventListener('keyup', (e) => { engine.keys[e.code] = false; });

    // UIボタン設定
    document.getElementById('btn-start').addEventListener('click', initGame);
    document.getElementById('btn-retry').addEventListener('click', initGame);

    // スマホタッチ設定
    const bindTouch = (id, key, actionFunc) => {
      const el = document.getElementById(id);
      if(!el) return;
      el.addEventListener('touchstart', (e) => {
        e.preventDefault(); 
        if(key) engine.isMobileTouch[key] = true;
        if(actionFunc && engine.gameState === 'playing') {
           const p = engine.player;
           if (!p.isAttacking && !p.isTailAttacking && !p.isRoaring) actionFunc(engine);
        }
      });
      el.addEventListener('touchend', (e) => { e.preventDefault(); if(key) engine.isMobileTouch[key] = false; });
      // PC用マウスクリック対応
      el.addEventListener('mousedown', (e) => {
        if(key) engine.isMobileTouch[key] = true;
        if(actionFunc && engine.gameState === 'playing') {
           const p = engine.player;
           if (!p.isAttacking && !p.isTailAttacking && !p.isRoaring) actionFunc(engine);
        }
      });
      el.addEventListener('mouseup', (e) => { if(key) engine.isMobileTouch[key] = false; });
      el.addEventListener('mouseleave', (e) => { if(key) engine.isMobileTouch[key] = false; });
    };

    bindTouch('btn-up', 'up'); bindTouch('btn-down', 'down'); bindTouch('btn-left', 'left'); bindTouch('btn-right', 'right');
    bindTouch('btn-bite', null, attack); bindTouch('btn-tail', null, tailAttack);

    // 起動
    animationFrameId = requestAnimationFrame(gameLoop);

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

恐竜王 - リアル無限サバイバル

恐竜世界を舞台にしたオリジナル2Dゲーム

👁 18 回閲覧
📅 投稿:2026年3月9日 🔄 更新:2026年4月11日
応援スタンプを押してみよう!