<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>PARRY & BLOCK TRAINER</title>
  <style>
    :root {
      --bg-dark: #070b12;
      --panel-bg: #0c131d;
      --border-cyan: #22d3ee;
      --text-cyan: #38bdf8;
      --text-main: #e2e8f0;
      --text-muted: #94a3b8;
      --danger-red: #f43f5e;
      --success-green: #10b981;
      --warn-yellow: #facc15;
    }

    * {
      box-sizing: border-box;
      user-select: none;
      margin: 0;
      padding: 0;
      font-family: 'Segoe UI', 'Hiragino Kaku Gothic ProN', 'Meiryo', sans-serif;
    }

    body {
      background-color: #030508;
      color: var(--text-main);
      display: flex;
      justify-content: center;
      align-items: center;
      min-height: 100vh;
      padding: 16px;
    }

    .app-frame {
      width: 100%;
      max-width: 1040px;
      background-color: var(--bg-dark);
      border: 2px solid var(--border-cyan);
      border-radius: 16px;
      box-shadow: 0 0 24px rgba(34, 211, 238, 0.25);
      padding: 18px 22px;
      display: flex;
      flex-direction: column;
      gap: 14px;
    }

    /* Header */
    .header {
      display: flex;
      justify-content: space-between;
      align-items: center;
    }

    .title {
      font-size: 1.5rem;
      font-weight: 800;
      letter-spacing: 1.5px;
      color: #67e8f9;
      text-shadow: 0 0 12px rgba(34, 211, 238, 0.7);
    }

    .menu-btn {
      background: rgba(15, 23, 42, 0.9);
      color: var(--text-main);
      border: 1.5px solid var(--border-cyan);
      border-radius: 6px;
      padding: 7px 14px;
      font-size: 0.85rem;
      font-weight: 700;
      cursor: pointer;
      transition: all 0.15s;
    }

    .menu-btn:hover {
      background: rgba(34, 211, 238, 0.15);
      box-shadow: 0 0 10px rgba(34, 211, 238, 0.4);
    }

    /* Canvas Container */
    .canvas-wrapper {
      position: relative;
      width: 100%;
      height: 440px;
      background-color: #06090f;
      border: 1px solid #1e293b;
      border-radius: 8px;
      overflow: hidden;
    }

    canvas {
      display: block;
      width: 100%;
      height: 100%;
    }

    /* Judgment Overlay */
    .judgment-banner {
      position: absolute;
      top: 34px;
      left: 50%;
      transform: translateX(-50%);
      font-size: 2.35rem;
      font-weight: 900;
      letter-spacing: 1px;
      pointer-events: none;
      white-space: nowrap;
      opacity: 0;
      transition: opacity 0.12s ease-out, transform 0.12s ease-out;
      text-shadow: 0 0 18px currentColor;
    }

    .judgment-banner.visible {
      opacity: 1;
    }

    /* Pause / Config Modal */
    .config-modal {
      position: absolute;
      top: 0;
      left: 50%;
      transform: translateX(-50%);
      width: 470px;
      max-width: 94%;
      height: 100%;
      background: rgba(11, 17, 27, 0.96);
      border-left: 1.5px solid var(--border-cyan);
      border-right: 1.5px solid var(--border-cyan);
      box-shadow: 0 0 30px rgba(0, 0, 0, 0.85);
      padding: 20px 26px;
      overflow-y: auto;
      display: none;
      flex-direction: column;
      gap: 14px;
      z-index: 20;
    }

    .config-modal.open {
      display: flex;
    }

    .config-modal h2 {
      text-align: center;
      color: #67e8f9;
      font-size: 1.5rem;
      letter-spacing: 1.5px;
      margin-bottom: 4px;
      text-shadow: 0 0 10px rgba(34, 211, 238, 0.5);
    }

    .config-check {
      display: flex;
      align-items: center;
      gap: 10px;
      font-size: 0.9rem;
      font-weight: 600;
      cursor: pointer;
    }

    .config-check input[type="checkbox"] {
      width: 16px;
      height: 16px;
      accent-color: #3b82f6;
      cursor: pointer;
    }

    .slider-group {
      display: flex;
      flex-direction: column;
      gap: 6px;
    }

    .slider-label {
      font-size: 0.88rem;
      font-weight: 600;
      color: #cbd5e1;
    }

    .slider-label span {
      font-weight: 800;
    }

    .val-red { color: #f43f5e; }
    .val-yellow { color: #facc15; }
    .val-cyan { color: #38bdf8; }

    input[type="range"] {
      width: 100%;
      accent-color: #3b82f6;
      cursor: pointer;
    }

    .keybind-section-title {
      font-size: 0.92rem;
      font-weight: 700;
      margin-top: 4px;
      color: #f8fafc;
    }

    .keybind-grid {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 12px;
    }

    .keybind-item {
      display: flex;
      flex-direction: column;
      gap: 5px;
      font-size: 0.85rem;
      color: #cbd5e1;
    }

    .keybind-btn {
      background: #0f172a;
      border: 1px solid #334155;
      border-radius: 6px;
      padding: 8px;
      color: #38bdf8;
      font-weight: 700;
      font-size: 0.9rem;
      text-align: center;
      cursor: pointer;
      transition: all 0.15s;
    }

    .keybind-btn:hover, .keybind-btn.listening {
      border-color: var(--border-cyan);
      background: #162238;
      color: #facc15;
    }

    .resume-btn {
      margin-top: 8px;
      background: #112432;
      border: 1px solid var(--border-cyan);
      color: #e2e8f0;
      padding: 10px;
      border-radius: 6px;
      font-weight: 700;
      font-size: 0.95rem;
      cursor: pointer;
      transition: background 0.15s;
    }

    .resume-btn:hover {
      background: #19374d;
    }

    /* Middle Cooldown & Action Row */
    .mid-controls {
      display: flex;
      justify-content: center;
      gap: 16px;
    }

    .cd-box {
      width: 210px;
      background: var(--panel-bg);
      border: 1px solid #1e293b;
      border-radius: 8px;
      padding: 10px 14px;
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 7px;
    }

    .cd-title {
      font-size: 0.78rem;
      font-weight: 700;
      color: var(--text-muted);
      letter-spacing: 0.5px;
    }

    .cd-bar-track {
      width: 100%;
      height: 6px;
      background: #1e293b;
      border-radius: 999px;
      overflow: hidden;
    }

    .cd-bar-fill {
      height: 100%;
      width: 100%;
      background: var(--border-cyan);
      box-shadow: 0 0 8px var(--border-cyan);
      transition: width 0.05s linear;
    }

    .action-btn {
      width: 100%;
      background: #1e293b;
      border: 1px solid #475569;
      border-radius: 5px;
      color: #f8fafc;
      font-size: 0.8rem;
      font-weight: 700;
      padding: 5px 0;
      cursor: pointer;
    }

    .action-btn.ready {
      border-color: var(--border-cyan);
      color: #67e8f9;
    }

    /* Bottom Status Panels */
    .bottom-panels {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 16px;
    }

    .stat-panel {
      background: var(--panel-bg);
      border: 1px solid #1e293b;
      border-radius: 8px;
      padding: 14px 18px;
      display: flex;
      flex-direction: column;
      gap: 10px;
    }

    .panel-heading {
      font-size: 0.82rem;
      font-weight: 700;
      color: var(--text-muted);
      letter-spacing: 0.5px;
    }

    .stat-row {
      display: flex;
      justify-content: space-between;
      align-items: center;
      font-size: 0.92rem;
      font-weight: 700;
    }

    .stat-val-cyan { color: #22d3ee; }
    .stat-val-purple { color: #a855f7; }
    .stat-val-yellow { color: #facc15; }
    .stat-val-red { color: #f43f5e; }
    .stat-val-orange { color: #fb923c; }

    .gauge-track {
      width: 100%;
      height: 8px;
      background: #1e293b;
      border-radius: 999px;
      overflow: hidden;
    }

    .gauge-stamina {
      height: 100%;
      width: 100%;
      background: #10b981;
      box-shadow: 0 0 10px rgba(16, 185, 129, 0.6);
      transition: width 0.08s linear, background-color 0.15s;
    }

    .gauge-stamina.exhausted {
      background: #f43f5e;
      box-shadow: 0 0 10px rgba(244, 63, 94, 0.6);
    }

    .gauge-enemy {
      height: 100%;
      width: 100%;
      background: #f43f5e;
      box-shadow: 0 0 10px rgba(244, 63, 94, 0.6);
      transition: width 0.1s linear;
    }

    .footer-note {
      text-align: center;
      font-size: 0.78rem;
      color: var(--text-muted);
      margin-top: 2px;
    }

    .footer-note strong {
      color: #cbd5e1;
    }
  </style>
</head>
<body>
  <div class="app-frame">
    <!-- Header -->
    <div class="header">
      <div class="title">PARRY &amp; BLOCK TRAINER</div>
      <button class="menu-btn" id="toggleMenuBtn">設定メニュー [ESC]</button>
    </div>

    <!-- Main Game Field -->
    <div class="canvas-wrapper">
      <canvas id="gameCanvas" width="992" height="440"></canvas>
      <div class="judgment-banner" id="judgmentBanner">HIT! (被弾)</div>

      <!-- Pause / Config Overlay -->
      <div class="config-modal" id="configModal">
        <h2>PAUSE / CONFIG</h2>

        <label class="config-check">
          <input type="checkbox" id="cfgEndless" checked />
          エンドレスモード (即時連戦・完全非停止)
        </label>

        <div class="slider-group">
          <div class="slider-label">Hitboxの長さ (リーチ): <span class="val-red" id="lblReach">55px</span></div>
          <input type="range" id="cfgReach" min="35" max="110" step="1" value="55" />
        </div>

        <div class="slider-group">
          <div class="slider-label">攻撃持続時間: <span class="val-red" id="lblActive">200ms</span></div>
          <input type="range" id="cfgActive" min="80" max="450" step="10" value="200" />
        </div>

        <div class="slider-group">
          <div class="slider-label">予備動作基準時間: <span class="val-yellow" id="lblWindup">0.20s</span></div>
          <input type="range" id="cfgWindup" min="0.10" max="0.80" step="0.02" value="0.20" />
        </div>

        <div class="slider-group">
          <div class="slider-label">敵の追尾速度: <span class="val-cyan" id="lblSpeed">4.2</span></div>
          <input type="range" id="cfgSpeed" min="1.5" max="7.5" step="0.1" value="4.2" />
        </div>

        <div class="slider-group">
          <div class="slider-label">ダッシュ速度: <span class="val-cyan" id="lblDashSpeed">360</span></div>
          <input type="range" id="cfgDashSpeed" min="250" max="650" step="10" value="360" />
        </div>

        <label class="config-check">
          <input type="checkbox" id="cfgFeint" checked />
          フェイント・BLOCK BAIT有効化 (OFFで完全単発直線攻撃)
        </label>

        <label class="config-check">
          <input type="checkbox" id="cfgCombo" />
          連続攻撃モード (2〜3連撃)
        </label>

        <label class="config-check">
          <input type="checkbox" id="cfgTelegraph" checked />
          予兆 (黄色発光)
        </label>

        <div class="keybind-section-title">キー割り当て設定</div>
        <div class="keybind-grid">
          <div class="keybind-item">
            <span>ガード:</span>
            <button class="keybind-btn" data-bind="block" id="bindBlockBtn">Space</button>
          </div>
          <div class="keybind-item">
            <span>パンチ/パリィ:</span>
            <button class="keybind-btn" data-bind="punch" id="bindPunchBtn">KeyR</button>
          </div>
          <div class="keybind-item">
            <span>ダッシュ:</span>
            <button class="keybind-btn" data-bind="dash" id="bindDashBtn">ShiftLeft</button>
          </div>
        </div>

        <button class="resume-btn" id="resumeBtn">ゲーム再開</button>
      </div>
    </div>

    <!-- Middle Cooldown & Action Bar -->
    <div class="mid-controls">
      <div class="cd-box">
        <div class="cd-title">BLOCK CD</div>
        <div class="cd-bar-track"><div class="cd-bar-fill" id="blockCdBar"></div></div>
      </div>
      <div class="cd-box">
        <div class="cd-title">PUNCH CD (15s)</div>
        <div class="cd-bar-track"><div class="cd-bar-fill" id="punchCdBar"></div></div>
      </div>
      <div class="cd-box">
        <div class="cd-title">ACTION</div>
        <button class="action-btn ready" id="actionPunchBtn">PUNCH [R]</button>
      </div>
    </div>

    <!-- Bottom Status Panels -->
    <div class="bottom-panels">
      <div class="stat-panel">
        <div class="panel-heading">自キャラ STAMINA &amp; REACTION</div>
        <div class="stat-row">
          <span>スタミナ:</span>
          <span class="stat-val-cyan" id="playerStaminaText">100 / 100</span>
        </div>
        <div class="gauge-track">
          <div class="gauge-stamina" id="playerStaminaBar"></div>
        </div>
        <div class="stat-row">
          <span>直近の反応時間:</span>
          <span class="stat-val-cyan" id="reactionTimeText">--- ms</span>
        </div>
        <div class="stat-row">
          <span>成功率:</span>
          <span class="stat-val-cyan" id="successRateText">0%</span>
        </div>
      </div>

      <div class="stat-panel">
        <div class="panel-heading">敵 AI BEHAVIOR &amp; STAMINA</div>
        <div class="stat-row">
          <span>AI状態:</span>
          <span class="stat-val-cyan" id="aiStateText">CHASING</span>
        </div>
        <div class="stat-row">
          <span>敵スタミナ:</span>
          <span class="stat-val-cyan" id="enemyStaminaText">100 / 100</span>
        </div>
        <div class="gauge-track">
          <div class="gauge-enemy" id="enemyStaminaBar"></div>
        </div>
      </div>
    </div>

    <div class="footer-note">
      ・💥 <strong>超極太波動砲アップデート</strong>: 壁めり込み3秒以上で発射される波動砲をさらに強化!直撃すると超ノックバックののち<strong>光の粒子となって蒸発し、初期位置へ戻されます</strong>。
    </div>
  </div>

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

    // UI Elements
    const judgmentBanner = document.getElementById('judgmentBanner');
    const configModal = document.getElementById('configModal');
    const toggleMenuBtn = document.getElementById('toggleMenuBtn');
    const resumeBtn = document.getElementById('resumeBtn');

    const blockCdBar = document.getElementById('blockCdBar');
    const punchCdBar = document.getElementById('punchCdBar');
    const actionPunchBtn = document.getElementById('actionPunchBtn');

    const playerStaminaText = document.getElementById('playerStaminaText');
    const playerStaminaBar = document.getElementById('playerStaminaBar');
    const reactionTimeText = document.getElementById('reactionTimeText');
    const successRateText = document.getElementById('successRateText');

    const aiStateText = document.getElementById('aiStateText');
    const enemyStaminaText = document.getElementById('enemyStaminaText');
    const enemyStaminaBar = document.getElementById('enemyStaminaBar');

    // Config Inputs
    const cfgEndless = document.getElementById('cfgEndless');
    const cfgReach = document.getElementById('cfgReach');
    const cfgActive = document.getElementById('cfgActive');
    const cfgWindup = document.getElementById('cfgWindup');
    const cfgSpeed = document.getElementById('cfgSpeed');
    const cfgDashSpeed = document.getElementById('cfgDashSpeed');
    const cfgFeint = document.getElementById('cfgFeint');
    const cfgCombo = document.getElementById('cfgCombo');
    const cfgTelegraph = document.getElementById('cfgTelegraph');

    const lblReach = document.getElementById('lblReach');
    const lblActive = document.getElementById('lblActive');
    const lblWindup = document.getElementById('lblWindup');
    const lblSpeed = document.getElementById('lblSpeed');
    const lblDashSpeed = document.getElementById('lblDashSpeed');

    const bindButtons = {
      block: document.getElementById('bindBlockBtn'),
      punch: document.getElementById('bindPunchBtn'),
      dash: document.getElementById('bindDashBtn')
    };

    // Game Settings & Keybinds
    const settings = {
      endless: true,
      reach: 55,
      activeMs: 200,
      windupSec: 0.20,
      enemySpeed: 4.2,
      dashSpeed: 360,
      feint: true,
      combo: false,
      telegraph: true
    };

    const keybinds = {
      block: 'Space',
      punch: 'KeyR',
      dash: 'ShiftLeft'
    };

    let listeningForBind = null;
    let isPaused = false;

    // World Bounds & Obstacles (2D Top-down Camera World)
    const WORLD = {
      minX: -650,
      maxX: 650,
      minY: -500,
      maxY: 500
    };

    const obstacles = [
      { x: 260, y: -60, w: 95, h: 120 },
      { x: -240, y: -260, w: 120, h: 140 },
      { x: 70, y: 120, w: 75, h: 170 },
      { x: -340, y: 160, w: 130, h: 90 }
    ];

    // Visual Effects System
    const particles = [];
    const shockwaves = [];
    const slashFlares = [];
    let screenShakeTimer = 0;
    let screenShakeIntensity = 0;
    let hitstopTimer = 0;
    let screenFlashAlpha = 0;
    let screenFlashColor = '#ffffff';

    function triggerScreenShake(intensity, duration) {
      screenShakeIntensity = Math.max(screenShakeIntensity, intensity);
      screenShakeTimer = Math.max(screenShakeTimer, duration);
    }

    function triggerScreenFlash(color, alpha) {
      screenFlashColor = color;
      screenFlashAlpha = alpha;
    }

    function spawnParticles(x, y, count, color, speedMin, speedMax, lifeMin, lifeMax, sizeMin = 2, sizeMax = 5, upwardDrift = 0) {
      for (let i = 0; i < count; i++) {
        const angle = Math.random() * Math.PI * 2;
        const speed = speedMin + Math.random() * (speedMax - speedMin);
        const life = lifeMin + Math.random() * (lifeMax - lifeMin);
        particles.push({
          x,
          y,
          vx: Math.cos(angle) * speed,
          vy: Math.sin(angle) * speed + upwardDrift,
          life,
          maxLife: life,
          color,
          size: sizeMin + Math.random() * (sizeMax - sizeMin)
        });
      }
    }

    function spawnDirectionalSparks(x, y, baseAngle, spread, count, color, speedMin, speedMax) {
      for (let i = 0; i < count; i++) {
        const angle = baseAngle + (Math.random() - 0.5) * spread;
        const speed = speedMin + Math.random() * (speedMax - speedMin);
        const life = 0.22 + Math.random() * 0.28;
        particles.push({
          x,
          y,
          vx: Math.cos(angle) * speed,
          vy: Math.sin(angle) * speed,
          life,
          maxLife: life,
          color,
          size: 2.5 + Math.random() * 4.0,
          isSpark: true
        });
      }
    }

    function spawnShockwave(x, y, startR, endR, color, duration, lineWidth = 4) {
      shockwaves.push({
        x,
        y,
        startR,
        endR,
        color,
        life: duration,
        maxLife: duration,
        lineWidth
      });
    }

    function spawnParryFlare(x, y, angle) {
      slashFlares.push({
        x,
        y,
        angle,
        life: 0.35,
        maxLife: 0.35
      });
    }

    // Hitbox Afterimages
    const hitboxTrails = [];
    let trailSpawnTimer = 0;

    function spawnHitboxTrail(x, y, angle, length, width, type) {
      hitboxTrails.push({
        x,
        y,
        angle,
        length,
        width,
        type,
        life: 0.45,
        maxLife: 0.45
      });
    }

    // Entities
    const PLAYER_SPAWN = { x: 0, y: 0 };

    const player = {
      x: PLAYER_SPAWN.x,
      y: PLAYER_SPAWN.y,
      radius: 17,
      angle: -Math.PI / 2,
      speed: 220,
      stamina: 100,
      maxStamina: 100,
      staminaExhausted: false,
      staminaRegenDelay: 0,
      isDashing: false,
      lungeVx: 0,
      lungeVy: 0,
      // Block & Parry State & Animation
      isBlocking: false,
      blockHoldTimer: 0,
      blockMaxHold: 1.5,
      blockCd: 0,
      blockCdMax: 2.2,
      blockStartTime: 0,
      blockDeployAnim: 0,
      blockImpactFlash: 0,
      parryWindowTimer: 0,
      // Punch & Parry Animation State
      punchCd: 0,
      punchCdMax: 15.0,
      punchActiveTimer: 0,
      punchVisualTimer: 0,
      punchVisualMax: 0.22,
      punchAngle: 0,
      punchIsParryAttempt: false,
      punchHitResolved: false,
      punchTrailTimer: 0,
      bodySquash: 0,
      // 隠し仕様:壁めり込み状態
      wallClipTimer: 0,
      isInsideObstacle: false,
      insideObstacleTimer: 0,
      // 波動砲直撃時の蒸発・リスポーン演出状態
      isVaporizing: false,
      vaporizeTimer: 0,
      vaporizeDuration: 0.85,
      vaporizeTrailTimer: 0,
      respawnBeamTimer: 0
    };

    const enemy = {
      x: 160,
      y: 0,
      radius: 17,
      angle: Math.PI,
      stamina: 100,
      maxStamina: 100,
      state: 'CHASING', // CHASING, BAITING, WINDUP, ATTACKING, STUNNED, DISARMED, CONFUSED, HADOU_CHARGE, HADOU_FIRE
      stateTimer: 0,
      currentWindupDuration: 0.20,
      attackCooldown: 0.6,
      attackTriggerDelay: -1,
      preferredAttackDistMult: 0.85,
      strafeSign: 1,
      strafeTimer: 1.5,
      attackAngle: 0,
      attackTactic: 'NORMAL',
      moveVx: 0,
      moveVy: 0,
      windupStartTime: 0,
      activeStartTime: 0,
      hasHitThisSwing: false,
      hasBeenBlockedThisSwing: false,
      feintPlanned: false,
      misdirectOffset: 0,
      comboRemaining: 0,
      knockbackVx: 0,
      knockbackVy: 0,
      avoidDirSign: 1,
      baitStrafeSign: 1,
      baitCooldown: 0,
      hitFlashTimer: 0,
      // 波動砲・頭上アイコン用
      hadouAngle: 0,
      hadouHitThisBeam: false,
      confusedLookTimer: 0
    };

    // Stats & Banner
    const stats = {
      attempts: 0,
      successes: 0,
      lastReactionMs: null
    };

    let bannerTimeout = null;
    function showJudgment(text, color) {
      judgmentBanner.textContent = text;
      judgmentBanner.style.color = color;
      judgmentBanner.classList.add('visible');
      if (bannerTimeout) clearTimeout(bannerTimeout);
      bannerTimeout = setTimeout(() => {
        judgmentBanner.classList.remove('visible');
      }, 1500);
    }

    // Input Tracking
    const keysDown = new Set();

    window.addEventListener('keydown', (e) => {
      if (listeningForBind) {
        e.preventDefault();
        keybinds[listeningForBind] = e.code;
        bindButtons[listeningForBind].textContent = e.code;
        bindButtons[listeningForBind].classList.remove('listening');
        if (listeningForBind === 'punch') {
          const shortKey = e.code.replace('Key', '');
          actionPunchBtn.textContent = `PUNCH [${shortKey}]`;
        }
        listeningForBind = null;
        return;
      }

      if (e.code === 'Escape') {
        e.preventDefault();
        togglePause();
        return;
      }

      if (isPaused || player.isVaporizing) return;

      if (!keysDown.has(e.code)) {
        keysDown.add(e.code);

        if (e.code === keybinds.block) {
          e.preventDefault();
          triggerBlockStart();
        } else if (e.code === keybinds.punch) {
          e.preventDefault();
          triggerPunch();
        } else if (e.code === keybinds.dash) {
          e.preventDefault();
        }
      }
    });

    window.addEventListener('keyup', (e) => {
      keysDown.delete(e.code);
    });

    actionPunchBtn.addEventListener('click', () => {
      if (!isPaused && !player.isVaporizing) triggerPunch();
    });

    // Config UI Listeners
    function togglePause() {
      isPaused = !isPaused;
      configModal.classList.toggle('open', isPaused);
    }

    toggleMenuBtn.addEventListener('click', togglePause);
    resumeBtn.addEventListener('click', togglePause);

    cfgEndless.addEventListener('change', () => settings.endless = cfgEndless.checked);
    cfgReach.addEventListener('input', () => {
      settings.reach = parseInt(cfgReach.value, 10);
      lblReach.textContent = `${settings.reach}px`;
    });
    cfgActive.addEventListener('input', () => {
      settings.activeMs = parseInt(cfgActive.value, 10);
      lblActive.textContent = `${settings.activeMs}ms`;
    });
    cfgWindup.addEventListener('input', () => {
      settings.windupSec = parseFloat(cfgWindup.value);
      lblWindup.textContent = `${settings.windupSec.toFixed(2)}s`;
    });
    cfgSpeed.addEventListener('input', () => {
      settings.enemySpeed = parseFloat(cfgSpeed.value);
      lblSpeed.textContent = settings.enemySpeed.toFixed(1);
    });
    cfgDashSpeed.addEventListener('input', () => {
      settings.dashSpeed = parseInt(cfgDashSpeed.value, 10);
      lblDashSpeed.textContent = `${settings.dashSpeed}`;
    });
    cfgFeint.addEventListener('change', () => settings.feint = cfgFeint.checked);
    cfgCombo.addEventListener('change', () => settings.combo = cfgCombo.checked);
    cfgTelegraph.addEventListener('change', () => settings.telegraph = cfgTelegraph.checked);

    Object.entries(bindButtons).forEach(([action, btn]) => {
      btn.addEventListener('click', () => {
        Object.values(bindButtons).forEach(b => b.classList.remove('listening'));
        listeningForBind = action;
        btn.classList.add('listening');
        btn.textContent = 'キーを押してください...';
      });
    });

    // Actions
    function triggerBlockStart() {
      if (player.blockCd > 0 || player.isBlocking || player.isVaporizing) return;
      player.isBlocking = true;
      player.blockHoldTimer = player.blockMaxHold;
      player.blockCd = player.blockCdMax;
      player.blockStartTime = performance.now();
      player.parryWindowTimer = 1.0;

      player.blockDeployAnim = 0.01;
      player.bodySquash = -0.22;
      spawnShockwave(player.x, player.y, player.radius * 0.6, player.radius + 20, 'rgba(16, 185, 129, 0.75)', 0.22, 3);
      spawnDirectionalSparks(
        player.x + Math.cos(player.angle) * 18,
        player.y + Math.sin(player.angle) * 18,
        player.angle,
        1.4,
        8,
        '#34d399',
        60,
        160
      );
    }

    function triggerPunch() {
      if (player.punchCd > 0 || player.isVaporizing) return;

      const isParry = player.parryWindowTimer > 0;
      const isDashingNow = player.isDashing || keysDown.has(keybinds.dash);

      player.punchCd = player.punchCdMax;
      player.punchActiveTimer = 0.16;
      player.punchVisualMax = isParry ? 0.26 : 0.20;
      player.punchVisualTimer = player.punchVisualMax;
      player.punchAngle = player.angle;
      player.punchIsParryAttempt = isParry;
      player.punchHitResolved = false;
      player.punchTrailTimer = 0.025;
      player.bodySquash = isParry ? 0.35 : 0.25;

      // 【隠しバグ仕様】ダッシュしながらパリィパンチすると一定時間障害物のすり抜けが発生!
      if (isParry && isDashingNow) {
        player.wallClipTimer = 0.24;
      }

      const stepSpeed = isParry ? 540 : 310;
      player.lungeVx = Math.cos(player.angle) * stepSpeed;
      player.lungeVy = Math.sin(player.angle) * stepSpeed;

      const muzzleX = player.x + Math.cos(player.angle) * 20;
      const muzzleY = player.y + Math.sin(player.angle) * 20;
      if (isParry) {
        spawnShockwave(muzzleX, muzzleY, 8, 52, 'rgba(250, 204, 21, 0.85)', 0.24, 3.5);
        spawnDirectionalSparks(muzzleX, muzzleY, player.angle, 0.9, 14, '#facc15', 140, 340);
      } else {
        spawnShockwave(muzzleX, muzzleY, 6, 38, 'rgba(56, 189, 248, 0.7)', 0.18, 2.5);
        spawnDirectionalSparks(muzzleX, muzzleY, player.angle, 0.7, 8, '#38bdf8', 110, 260);
      }

      spawnHitboxTrail(player.x, player.y, player.angle, isParry ? 62 : 58, isParry ? 44 : 42, 'player');
      evaluatePlayerPunchHit();
    }

    function evaluatePlayerPunchHit() {
      if (player.punchHitResolved) return;

      const punchReach = player.punchIsParryAttempt ? 62 : 58;
      const punchWidth = player.punchIsParryAttempt ? 44 : 42;
      const hitEnemy = checkRectCircleHit(
        player.x,
        player.y,
        player.punchAngle,
        punchReach,
        punchWidth,
        enemy.x,
        enemy.y,
        enemy.radius
      );

      if (hitEnemy) {
        player.punchHitResolved = true;
        const contactX = (player.x + enemy.x) / 2;
        const contactY = (player.y + enemy.y) / 2;

        if (player.punchIsParryAttempt || player.parryWindowTimer > 0) {
          executeParry(contactX, contactY);
        } else {
          enemy.state = 'DISARMED';
          enemy.stateTimer = 3.0;
          enemy.comboRemaining = 0;
          enemy.attackTriggerDelay = -1;
          enemy.moveVx = 0;
          enemy.moveVy = 0;
          enemy.knockbackVx = Math.cos(player.punchAngle) * 440;
          enemy.knockbackVy = Math.sin(player.punchAngle) * 440;
          enemy.stamina = Math.max(0, enemy.stamina - 15);
          enemy.hitFlashTimer = 0.22;

          hitstopTimer = 0.045;
          triggerScreenShake(7, 0.18);
          spawnShockwave(contactX, contactY, 10, 72, '#38bdf8', 0.28, 4);
          spawnParticles(contactX, contactY, 18, '#38bdf8', 120, 360, 0.2, 0.45, 2.5, 5);
          spawnDirectionalSparks(contactX, contactY, player.punchAngle, 1.3, 14, '#bae6fd', 180, 420);

          showJudgment('COUNTER PUNCH! (3s封じ)', '#38bdf8');
        }
      }
    }

    function executeParry(contactX, contactY) {
      player.parryWindowTimer = 0;
      player.isBlocking = false;
      player.blockHoldTimer = 0;

      enemy.state = 'STUNNED';
      enemy.stateTimer = 2.0;
      enemy.comboRemaining = 0;
      enemy.attackTriggerDelay = -1;
      enemy.moveVx = 0;
      enemy.moveVy = 0;
      enemy.knockbackVx = Math.cos(player.punchAngle) * 820;
      enemy.knockbackVy = Math.sin(player.punchAngle) * 820;
      enemy.stamina = Math.max(0, enemy.stamina - 25);
      enemy.hitFlashTimer = 0.35;

      hitstopTimer = 0.075;
      triggerScreenShake(13, 0.28);
      triggerScreenFlash('#fef08a', 0.22);

      spawnParryFlare(contactX, contactY, player.punchAngle);
      spawnShockwave(contactX, contactY, 12, 115, '#facc15', 0.36, 5.5);
      spawnShockwave(contactX, contactY, 6, 70, '#ffffff', 0.24, 3.0);
      spawnParticles(contactX, contactY, 32, '#facc15', 160, 520, 0.25, 0.55, 3, 6);
      spawnParticles(contactX, contactY, 16, '#ffffff', 120, 420, 0.18, 0.40, 2, 4.5);
      spawnDirectionalSparks(contactX, contactY, player.punchAngle, 1.8, 22, '#fef08a', 220, 560);

      let label = 'PREDICTION PARRY (0ms)';
      let color = '#facc15';
      let reactionMs = 0;

      if (enemy.activeStartTime > 0 && player.blockStartTime >= enemy.activeStartTime) {
        reactionMs = Math.round(player.blockStartTime - enemy.activeStartTime);
        label = `HITBOX DRAG (+${reactionMs}ms)`;
        color = '#10b981';
      } else if (enemy.windupStartTime > 0 && player.blockStartTime >= enemy.windupStartTime) {
        reactionMs = Math.round(player.blockStartTime - enemy.windupStartTime);
        label = `REACT PARRY (${reactionMs}ms)`;
        color = '#22d3ee';
      } else {
        reactionMs = enemy.windupStartTime > 0
          ? Math.round(player.blockStartTime - enemy.windupStartTime)
          : 0;
        label = `PREDICTION PARRY (0ms)`;
        color = '#facc15';
      }

      stats.attempts++;
      stats.successes++;
      stats.lastReactionMs = reactionMs;
      updateStatsUI();
      showJudgment(label, color);

      if (settings.endless && enemy.stamina <= 0) {
        enemy.stamina = enemy.maxStamina;
      }
    }

    // 波動砲直撃時の蒸発&初期位置リスポーン開始
    function triggerPlayerVaporize(beamAngle) {
      player.isBlocking = false;
      player.blockHoldTimer = 0;
      player.parryWindowTimer = 0;
      player.isInsideObstacle = false;
      player.insideObstacleTimer = 0;
      player.wallClipTimer = 0;

      player.isVaporizing = true;
      player.vaporizeTimer = player.vaporizeDuration;
      player.vaporizeTrailTimer = 0;

      // 超特大ノックバックで吹っ飛ばしながら消滅!
      player.lungeVx = Math.cos(beamAngle) * 1450;
      player.lungeVy = Math.sin(beamAngle) * 1450;

      hitstopTimer = 0.085;
      triggerScreenShake(26, 0.65);
      triggerScreenFlash('#ffffff', 0.55);

      // 大爆発&蒸発パーティクル
      spawnShockwave(player.x, player.y, 10, 180, '#ffffff', 0.45, 7);
      spawnShockwave(player.x, player.y, 15, 240, '#f43f5e', 0.55, 6);
      spawnShockwave(player.x, player.y, 8, 140, '#38bdf8', 0.50, 5);
      spawnParticles(player.x, player.y, 55, '#ffffff', 120, 620, 0.35, 0.85, 2.5, 6.5, -40);
      spawnParticles(player.x, player.y, 45, '#38bdf8', 100, 520, 0.40, 0.90, 2.5, 6.0, -60);
      spawnParticles(player.x, player.y, 35, '#f43f5e', 150, 680, 0.30, 0.75, 3.0, 7.0);
      spawnDirectionalSparks(player.x, player.y, beamAngle, 1.1, 40, '#fef08a', 320, 880);

      showJudgment('VAPORIZED!! (波動砲で蒸発)', '#f43f5e');
    }

    function checkRectCircleHit(ox, oy, angle, length, width, cx, cy, cr) {
      const dx = cx - ox;
      const dy = cy - oy;
      const cos = Math.cos(-angle);
      const sin = Math.sin(-angle);
      const localX = dx * cos - dy * sin;
      const localY = dx * sin + dy * cos;

      const minX = 0;
      const maxX = length;
      const minY = -width / 2;
      const maxY = width / 2;

      const closestX = Math.max(minX, Math.min(localX, maxX));
      const closestY = Math.max(minY, Math.min(localY, maxY));

      const distX = localX - closestX;
      const distY = localY - closestY;
      return (distX * distX + distY * distY) <= (cr * cr);
    }

    function checkPlayerInsideAnyObstacle() {
      for (const obs of obstacles) {
        if (
          player.x >= obs.x &&
          player.x <= obs.x + obs.w &&
          player.y >= obs.y &&
          player.y <= obs.y + obs.h
        ) {
          return true;
        }
      }
      return false;
    }

    function resolveCollisions(entity, isPlayer = false) {
      entity.x = Math.max(WORLD.minX + entity.radius, Math.min(WORLD.maxX - entity.radius, entity.x));
      entity.y = Math.max(WORLD.minY + entity.radius, Math.min(WORLD.maxY - entity.radius, entity.y));

      if (isPlayer) {
        const insideNow = checkPlayerInsideAnyObstacle();
        if (player.wallClipTimer > 0 || insideNow || player.isVaporizing) {
          return;
        }
      }

      for (const obs of obstacles) {
        const closestX = Math.max(obs.x, Math.min(entity.x, obs.x + obs.w));
        const closestY = Math.max(obs.y, Math.min(entity.y, obs.y + obs.h));
        const dx = entity.x - closestX;
        const dy = entity.y - closestY;
        const distSq = dx * dx + dy * dy;

        if (distSq < entity.radius * entity.radius) {
          const dist = Math.sqrt(distSq) || 0.001;
          const overlap = entity.radius - dist;
          entity.x += (dx / dist) * overlap;
          entity.y += (dy / dist) * overlap;
        }
      }
    }

    function isPointBlocked(px, py, pad) {
      if (px < WORLD.minX + pad || px > WORLD.maxX - pad || py < WORLD.minY + pad || py > WORLD.maxY - pad) {
        return true;
      }
      for (const obs of obstacles) {
        if (
          px > obs.x - pad &&
          px < obs.x + obs.w + pad &&
          py > obs.y - pad &&
          py < obs.y + obs.h + pad
        ) {
          return true;
        }
      }
      return false;
    }

    function getClearanceScore(x, y, angle, maxDist, pad) {
      const steps = 6;
      for (let i = 1; i <= steps; i++) {
        const d = (maxDist / steps) * i;
        if (isPointBlocked(x + Math.cos(angle) * d, y + Math.sin(angle) * d, pad)) {
          return (i - 1) / steps;
        }
      }
      return 1.0;
    }

    function getObstacleAvoidedAngle(fromX, fromY, desiredAngle) {
      const pad = enemy.radius + 8;
      const lookAhead = 76;

      if (getClearanceScore(fromX, fromY, desiredAngle, lookAhead, pad) >= 1.0) {
        return desiredAngle;
      }

      let bestAngle = desiredAngle;
      let bestScore = -999;
      const candidateOffsets = [
        0.35, -0.35,
        0.70, -0.70,
        1.05, -1.05,
        1.40, -1.40,
        1.85, -1.85,
        2.35, -2.35
      ];

      for (const offset of candidateOffsets) {
        const cand = desiredAngle + offset;
        const clearance = getClearanceScore(fromX, fromY, cand, lookAhead, pad);
        const alignment = Math.cos(offset);
        const sideBias = (Math.sign(offset) === enemy.avoidDirSign) ? 0.08 : 0;
        const score = clearance * 2.4 + alignment * 0.65 + sideBias;

        if (score > bestScore) {
          bestScore = score;
          bestAngle = cand;
        }
      }

      const diff = Math.atan2(Math.sin(bestAngle - desiredAngle), Math.cos(bestAngle - desiredAngle));
      if (Math.abs(diff) > 0.1) {
        enemy.avoidDirSign = Math.sign(diff);
      }
      return bestAngle;
    }

    function resetAttackPacing() {
      enemy.attackCooldown = 0.55 + Math.random() * 0.75;
      enemy.attackTriggerDelay = -1;
      enemy.preferredAttackDistMult = 0.72 + Math.random() * 0.28;
    }

    function updateStatsUI() {
      if (stats.lastReactionMs !== null) {
        reactionTimeText.textContent = `${stats.lastReactionMs} ms`;
      }
      const rate = stats.attempts > 0 ? Math.round((stats.successes / stats.attempts) * 100) : 0;
      successRateText.textContent = `${rate}%`;
    }

    function updateEffects(dt) {
      if (screenShakeTimer > 0) {
        screenShakeTimer = Math.max(0, screenShakeTimer - dt);
        if (screenShakeTimer <= 0) screenShakeIntensity = 0;
      }
      if (screenFlashAlpha > 0) {
        screenFlashAlpha = Math.max(0, screenFlashAlpha - dt * 1.4);
      }
      if (player.blockImpactFlash > 0) {
        player.blockImpactFlash = Math.max(0, player.blockImpactFlash - dt * 3.5);
      }
      if (player.respawnBeamTimer > 0) {
        player.respawnBeamTimer = Math.max(0, player.respawnBeamTimer - dt);
      }
      if (enemy.hitFlashTimer > 0) {
        enemy.hitFlashTimer = Math.max(0, enemy.hitFlashTimer - dt);
      }
      if (player.blockDeployAnim > 0 && player.blockDeployAnim < 1) {
        player.blockDeployAnim = Math.min(1, player.blockDeployAnim + dt * 9.5);
      }
      player.bodySquash *= Math.pow(0.0005, dt);

      for (let i = particles.length - 1; i >= 0; i--) {
        const p = particles[i];
        p.life -= dt;
        if (p.life <= 0) {
          particles.splice(i, 1);
          continue;
        }
        p.x += p.vx * dt;
        p.y += p.vy * dt;
        p.vx *= Math.pow(0.02, dt);
        p.vy *= Math.pow(0.02, dt);
      }

      for (let i = shockwaves.length - 1; i >= 0; i--) {
        const s = shockwaves[i];
        s.life -= dt;
        if (s.life <= 0) {
          shockwaves.splice(i, 1);
        }
      }

      for (let i = slashFlares.length - 1; i >= 0; i--) {
        const f = slashFlares[i];
        f.life -= dt;
        if (f.life <= 0) {
          slashFlares.splice(i, 1);
        }
      }
    }

    // Main Update Loop
    let lastTimestamp = performance.now();

    function update(dt) {
      if (isPaused) return;

      updateEffects(dt);

      if (hitstopTimer > 0) {
        hitstopTimer = Math.max(0, hitstopTimer - dt);
        return;
      }

      for (let i = hitboxTrails.length - 1; i >= 0; i--) {
        const t = hitboxTrails[i];
        t.life -= dt;
        if (t.life <= 0) {
          hitboxTrails.splice(i, 1);
        }
      }

      if (player.wallClipTimer > 0) player.wallClipTimer = Math.max(0, player.wallClipTimer - dt);
      if (player.blockCd > 0) player.blockCd = Math.max(0, player.blockCd - dt);
      if (player.punchCd > 0) player.punchCd = Math.max(0, player.punchCd - dt);
      if (player.parryWindowTimer > 0) player.parryWindowTimer = Math.max(0, player.parryWindowTimer - dt);
      if (player.punchVisualTimer > 0) player.punchVisualTimer = Math.max(0, player.punchVisualTimer - dt);
      if (player.staminaRegenDelay > 0) player.staminaRegenDelay = Math.max(0, player.staminaRegenDelay - dt);
      if (enemy.baitCooldown > 0) enemy.baitCooldown = Math.max(0, enemy.baitCooldown - dt);
      if (enemy.attackCooldown > 0) enemy.attackCooldown = Math.max(0, enemy.attackCooldown - dt);

      enemy.strafeTimer -= dt;
      if (enemy.strafeTimer <= 0) {
        enemy.strafeSign *= -1;
        enemy.strafeTimer = 1.2 + Math.random() * 1.6;
      }

      if (player.isBlocking) {
        player.blockHoldTimer -= dt;
        if (player.blockHoldTimer <= 0) {
          player.isBlocking = false;
          player.blockHoldTimer = 0;
          player.blockDeployAnim = 0;
        }
      } else {
        player.blockDeployAnim = 0;
      }

      // 蒸発中の処理(吹っ飛びながら粒子に分解 → 初期位置にリスポーン)
      if (player.isVaporizing) {
        player.vaporizeTimer -= dt;
        player.x += player.lungeVx * dt;
        player.y += player.lungeVy * dt;
        player.lungeVx *= Math.pow(0.015, dt);
        player.lungeVy *= Math.pow(0.015, dt);

        // 外周壁だけは超えないように制限
        player.x = Math.max(WORLD.minX + player.radius, Math.min(WORLD.maxX - player.radius, player.x));
        player.y = Math.max(WORLD.minY + player.radius, Math.min(WORLD.maxY - player.radius, player.y));

        // 吹っ飛び軌跡から蒸発する光の粒を撒き散らす
        player.vaporizeTrailTimer -= dt;
        if (player.vaporizeTrailTimer <= 0) {
          spawnParticles(player.x, player.y, 6, '#67e8f9', 30, 140, 0.25, 0.55, 2, 5, -55);
          spawnParticles(player.x, player.y, 4, '#ffffff', 20, 100, 0.20, 0.45, 2, 4, -70);
          player.vaporizeTrailTimer = 0.025;
        }

        if (player.vaporizeTimer <= 0) {
          // 初期位置 (0, 0) に復活!
          player.isVaporizing = false;
          player.x = PLAYER_SPAWN.x;
          player.y = PLAYER_SPAWN.y;
          player.lungeVx = 0;
          player.lungeVy = 0;
          player.stamina = player.maxStamina;
          player.staminaExhausted = false;
          player.respawnBeamTimer = 0.45;

          spawnShockwave(player.x, player.y, 65, 10, '#22d3ee', 0.35, 5);
          spawnShockwave(player.x, player.y, 8, 85, '#67e8f9', 0.40, 4);
          spawnParticles(player.x, player.y, 28, '#67e8f9', 80, 260, 0.25, 0.55, 2, 5, -45);
          showJudgment('RESPAWNED (初期位置へ帰還)', '#38bdf8');
        }
      } else {
        // 通常のプレイヤー移動処理
        let moveX = 0;
        let moveY = 0;
        if (keysDown.has('KeyW')) moveY -= 1;
        if (keysDown.has('KeyS')) moveY += 1;
        if (keysDown.has('KeyA')) moveX -= 1;
        if (keysDown.has('KeyD')) moveX += 1;

        const hasMoveInput = (moveX !== 0 || moveY !== 0);
        if (hasMoveInput) {
          const len = Math.hypot(moveX, moveY);
          moveX /= len;
          moveY /= len;
          player.angle = Math.atan2(moveY, moveX);
        }

        if (player.stamina <= 0) {
          player.stamina = 0;
          player.staminaExhausted = true;
        } else if (player.staminaExhausted && player.stamina >= 25) {
          player.staminaExhausted = false;
        }

        const dashKeyHeld = keysDown.has(keybinds.dash);
        const canDash = dashKeyHeld && !player.staminaExhausted && player.stamina > 0;

        if (canDash) {
          player.isDashing = true;
          player.stamina = Math.max(0, player.stamina - 36 * dt);
          player.staminaRegenDelay = 0.45;
          if (player.stamina <= 0) {
            player.staminaExhausted = true;
          }

          const dirX = hasMoveInput ? moveX : Math.cos(player.angle);
          const dirY = hasMoveInput ? moveY : Math.sin(player.angle);
          const blockSlow = player.isBlocking ? 0.7 : 1.0;

          player.x += dirX * settings.dashSpeed * blockSlow * dt;
          player.y += dirY * settings.dashSpeed * blockSlow * dt;
        } else {
          player.isDashing = false;
          if (player.staminaRegenDelay <= 0 && player.stamina < player.maxStamina) {
            player.stamina = Math.min(player.maxStamina, player.stamina + 24 * dt);
          }

          const moveMult = player.isBlocking ? 0.6 : 1.0;
          player.x += moveX * player.speed * moveMult * dt;
          player.y += moveY * player.speed * moveMult * dt;
        }

        if (Math.hypot(player.lungeVx, player.lungeVy) > 5) {
          player.x += player.lungeVx * dt;
          player.y += player.lungeVy * dt;
          player.lungeVx *= Math.pow(0.0001, dt);
          player.lungeVy *= Math.pow(0.0001, dt);
        }

        resolveCollisions(player, true);

        // 障害物へのめり込み状態&滞在時間の更新
        const wasInside = player.isInsideObstacle;
        player.isInsideObstacle = checkPlayerInsideAnyObstacle();
        if (player.isInsideObstacle) {
          if (!wasInside) {
            spawnParticles(player.x, player.y, 14, '#22d3ee', 80, 220, 0.2, 0.4, 2, 4);
            showJudgment('WALL CLIP! (壁めり込み発動)', '#67e8f9');
          }
          player.insideObstacleTimer += dt;
        } else {
          player.insideObstacleTimer = 0;
        }

        if (player.punchActiveTimer > 0) {
          player.punchActiveTimer = Math.max(0, player.punchActiveTimer - dt);
          player.punchTrailTimer -= dt;
          if (player.punchTrailTimer <= 0) {
            spawnHitboxTrail(
              player.x,
              player.y,
              player.punchAngle,
              player.punchIsParryAttempt ? 62 : 58,
              player.punchIsParryAttempt ? 44 : 42,
              'player'
            );
            player.punchTrailTimer = 0.025;
          }
          evaluatePlayerPunchHit();
        }
      }

      if (Math.hypot(enemy.knockbackVx, enemy.knockbackVy) > 5) {
        enemy.x += enemy.knockbackVx * dt;
        enemy.y += enemy.knockbackVy * dt;
        enemy.knockbackVx *= Math.pow(0.02, dt);
        enemy.knockbackVy *= Math.pow(0.02, dt);
      }

      // Enemy AI State Machine
      const toPlayerX = player.x - enemy.x;
      const toPlayerY = player.y - enemy.y;
      const distToPlayer = Math.hypot(toPlayerX, toPlayerY);
      const baseAngleToPlayer = Math.atan2(toPlayerY, toPlayerX);

      // プレイヤーが障害物の中にめり込んでいる場合、敵が困惑「?」状態へ移行
      if (
        !player.isVaporizing &&
        player.isInsideObstacle &&
        enemy.state !== 'CONFUSED' &&
        enemy.state !== 'HADOU_CHARGE' &&
        enemy.state !== 'HADOU_FIRE' &&
        enemy.state !== 'STUNNED'
      ) {
        enemy.state = 'CONFUSED';
        enemy.confusedLookTimer = 0.4;
        enemy.moveVx = 0;
        enemy.moveVy = 0;
      }

      const runSpeed = settings.enemySpeed * 60;
      const hasLineOfSight = getClearanceScore(enemy.x, enemy.y, baseAngleToPlayer, distToPlayer, enemy.radius * 0.65) >= 0.95;
      const maxAttackStartRange = settings.reach + runSpeed * (settings.windupSec + (settings.activeMs / 1000) * 0.65);
      const currentTriggerRange = maxAttackStartRange * enemy.preferredAttackDistMult;

      if (enemy.state === 'CONFUSED') {
        enemy.confusedLookTimer -= dt;
        if (enemy.confusedLookTimer <= 0) {
          enemy.confusedLookTimer = 0.45 + Math.random() * 0.35;
          enemy.angle = baseAngleToPlayer + (Math.random() - 0.5) * 1.4;
        }

        if (!player.isInsideObstacle) {
          resetAttackPacing();
          enemy.state = 'CHASING';
        } else if (player.insideObstacleTimer >= 3.0) {
          // 3秒以上とどまると「!?」を出して超極太波動砲チャージ開始!
          enemy.state = 'HADOU_CHARGE';
          enemy.stateTimer = 1.15;
          enemy.hadouAngle = baseAngleToPlayer;
          enemy.angle = enemy.hadouAngle;
          triggerScreenShake(7, 1.15);
          spawnShockwave(enemy.x, enemy.y, 85, 12, '#f43f5e', 0.45, 4);
          showJudgment('!? 敵が超高出力波動砲を溜めている!', '#f43f5e');
        }
      } else if (enemy.state === 'HADOU_CHARGE') {
        enemy.stateTimer -= dt;
        enemy.hadouAngle = baseAngleToPlayer;
        enemy.angle = enemy.hadouAngle;

        // 激しい収束エネルギー粒子&火花
        for (let k = 0; k < 2; k++) {
          const spawnAng = Math.random() * Math.PI * 2;
          const dist = 55 + Math.random() * 65;
          const px = enemy.x + Math.cos(spawnAng) * dist;
          const py = enemy.y + Math.sin(spawnAng) * dist;
          particles.push({
            x: px,
            y: py,
            vx: (enemy.x - px) * 4.6,
            vy: (enemy.y - py) * 4.6,
            life: 0.24,
            maxLife: 0.24,
            color: Math.random() < 0.4 ? '#ffffff' : (Math.random() < 0.5 ? '#f43f5e' : '#facc15'),
            size: 3 + Math.random() * 3.5
          });
        }

        if (enemy.stateTimer <= 0) {
          // 超極太波動砲発射!!
          enemy.state = 'HADOU_FIRE';
          enemy.stateTimer = 0.85;
          enemy.hadouHitThisBeam = false;

          // 発射反動で敵自身も少し後ろへノックバック
          enemy.knockbackVx = -Math.cos(enemy.hadouAngle) * 260;
          enemy.knockbackVy = -Math.sin(enemy.hadouAngle) * 260;

          triggerScreenShake(22, 0.80);
          triggerScreenFlash('#f43f5e', 0.42);
          spawnShockwave(enemy.x, enemy.y, 15, 210, '#f43f5e', 0.50, 8);
          spawnShockwave(enemy.x, enemy.y, 10, 140, '#facc15', 0.38, 5);
          spawnDirectionalSparks(enemy.x, enemy.y, enemy.hadouAngle, 0.9, 55, '#facc15', 280, 850);
          spawnDirectionalSparks(enemy.x, enemy.y, enemy.hadouAngle, 0.5, 35, '#ffffff', 350, 950);
          showJudgment('超極太波動砲!! (GIGA HADOUHOU!)', '#f43f5e');
        }
      } else if (enemy.state === 'HADOU_FIRE') {
        enemy.stateTimer -= dt;
        enemy.angle = enemy.hadouAngle;

        // 発射中もビーム周囲から火花が散り続ける
        if (Math.random() < 0.85) {
          const distAlong = 30 + Math.random() * 550;
          const bx = enemy.x + Math.cos(enemy.hadouAngle) * distAlong;
          const by = enemy.y + Math.sin(enemy.hadouAngle) * distAlong;
          spawnDirectionalSparks(bx, by, enemy.hadouAngle + (Math.random() < 0.5 ? 1.2 : -1.2), 0.7, 2, '#facc15', 140, 380);
        }

        if (!enemy.hadouHitThisBeam && !player.isVaporizing) {
          const hitBeam = checkRectCircleHit(
            enemy.x,
            enemy.y,
            enemy.hadouAngle,
            1500,
            110, // ビームの極太当たり判定(幅110px)
            player.x,
            player.y,
            player.radius
          );

          if (hitBeam) {
            enemy.hadouHitThisBeam = true;
            if (player.parryWindowTimer > 0 && player.punchActiveTimer > 0) {
              executeParry(player.x, player.y);
            } else {
              stats.attempts++;
              updateStatsUI();
              triggerPlayerVaporize(enemy.hadouAngle);
            }
          }
        }

        if (enemy.stateTimer <= 0) {
          resetAttackPacing();
          enemy.state = 'CHASING';
        }
      } else if (enemy.state === 'STUNNED') {
        enemy.stateTimer -= dt;
        if (enemy.stateTimer <= 0) {
          resetAttackPacing();
          enemy.state = 'CHASING';
        }
      } else if (enemy.state === 'DISARMED') {
        enemy.stateTimer -= dt;
        enemy.angle = baseAngleToPlayer;
        if (!player.isVaporizing && distToPlayer > settings.reach * 0.85) {
          const moveAngle = getObstacleAvoidedAngle(enemy.x, enemy.y, baseAngleToPlayer);
          enemy.x += Math.cos(moveAngle) * (runSpeed * 0.72) * dt;
          enemy.y += Math.sin(moveAngle) * (runSpeed * 0.72) * dt;
        }
        if (enemy.stateTimer <= 0) {
          resetAttackPacing();
          enemy.state = 'CHASING';
        }
      } else if (enemy.state === 'CHASING') {
        if (player.isVaporizing) {
          // プレイヤーが蒸発している間は中央付近を警戒して待機
          enemy.angle = baseAngleToPlayer;
        } else {
          if (
            settings.feint &&
            enemy.attackCooldown <= 0 &&
            enemy.baitCooldown <= 0 &&
            hasLineOfSight &&
            distToPlayer <= maxAttackStartRange * 1.05 &&
            distToPlayer > settings.reach * 0.95
          ) {
            if (Math.random() < 0.06 * dt) {
              enemy.state = 'BAITING';
              enemy.stateTimer = 0.20;
              enemy.baitCooldown = 10.0;
              enemy.baitStrafeSign = Math.random() < 0.5 ? 1 : -1;
              enemy.angle = baseAngleToPlayer;
              return;
            }
          }

          if (enemy.attackCooldown <= 0 && hasLineOfSight && distToPlayer <= currentTriggerRange) {
            if (enemy.attackTriggerDelay < 0) {
              enemy.attackTriggerDelay = Math.random() < 0.55 ? 0 : (0.08 + Math.random() * 0.35);
            }
            enemy.attackTriggerDelay -= dt;

            if (enemy.attackTriggerDelay <= 0) {
              enemy.attackTriggerDelay = -1;
              chooseAndStartAttack(baseAngleToPlayer);
              return;
            }
          } else if (distToPlayer > maxAttackStartRange * 1.15) {
            enemy.attackTriggerDelay = -1;
          }

          let desiredMoveAngle = baseAngleToPlayer;
          const closeOrbitDist = enemy.radius + player.radius + 8;

          if (distToPlayer <= closeOrbitDist) {
            desiredMoveAngle = baseAngleToPlayer + (Math.PI * 0.45) * enemy.strafeSign;
          }

          const steerAngle = getObstacleAvoidedAngle(enemy.x, enemy.y, desiredMoveAngle);
          enemy.angle = baseAngleToPlayer;
          if (distToPlayer > enemy.radius + player.radius + 3) {
            enemy.x += Math.cos(steerAngle) * runSpeed * dt;
            enemy.y += Math.sin(steerAngle) * runSpeed * dt;
          }
        }
      } else if (enemy.state === 'BAITING') {
        enemy.stateTimer -= dt;
        enemy.angle = baseAngleToPlayer;
        const baitAngle = baseAngleToPlayer + Math.PI * 0.55 * enemy.baitStrafeSign;
        const safeAngle = getObstacleAvoidedAngle(enemy.x, enemy.y, baitAngle);
        enemy.x += Math.cos(safeAngle) * (runSpeed * 0.85) * dt;
        enemy.y += Math.sin(safeAngle) * (runSpeed * 0.85) * dt;

        if (enemy.stateTimer <= 0) {
          if (!player.isVaporizing && Math.random() < 0.80 && distToPlayer <= maxAttackStartRange + 25) {
            chooseAndStartAttack(baseAngleToPlayer);
          } else {
            enemy.attackCooldown = 0.35 + Math.random() * 0.4;
            enemy.state = 'CHASING';
          }
        }
      } else if (enemy.state === 'WINDUP') {
        enemy.stateTimer -= dt;

        const steerAngle = getObstacleAvoidedAngle(enemy.x, enemy.y, baseAngleToPlayer);
        if (enemy.attackTactic === 'BACKSTEP') {
          const backAngle = getObstacleAvoidedAngle(enemy.x, enemy.y, baseAngleToPlayer + Math.PI);
          enemy.x += Math.cos(backAngle) * (runSpeed * 0.55) * dt;
          enemy.y += Math.sin(backAngle) * (runSpeed * 0.55) * dt;
        } else if (distToPlayer > enemy.radius + player.radius + 4) {
          enemy.x += Math.cos(steerAngle) * runSpeed * dt;
          enemy.y += Math.sin(steerAngle) * runSpeed * dt;
        }

        if (settings.feint) {
          if (enemy.attackTactic === 'MISDIRECT' || enemy.attackTactic === 'BAIT_WHIFF') {
            enemy.attackAngle = baseAngleToPlayer + enemy.misdirectOffset;
          } else {
            enemy.attackAngle = baseAngleToPlayer;
          }

          if (enemy.feintPlanned && enemy.stateTimer <= enemy.currentWindupDuration * 0.42) {
            enemy.feintPlanned = false;
            enemy.attackCooldown = 0.3 + Math.random() * 0.35;
            enemy.state = 'CHASING';
            return;
          }
        } else {
          enemy.attackAngle = baseAngleToPlayer;
        }
        enemy.angle = enemy.attackAngle;

        if (enemy.stateTimer <= 0) {
          enemy.state = 'ATTACKING';
          enemy.stateTimer = settings.activeMs / 1000;
          enemy.activeStartTime = performance.now();
          enemy.hasHitThisSwing = false;
          enemy.hasBeenBlockedThisSwing = false;
          trailSpawnTimer = 0.024;

          if (enemy.attackTactic === 'BACKSTEP') {
            enemy.moveVx = -Math.cos(enemy.attackAngle) * (runSpeed * 0.45);
            enemy.moveVy = -Math.sin(enemy.attackAngle) * (runSpeed * 0.45);
          } else if (enemy.attackTactic === 'BAIT_WHIFF') {
            enemy.moveVx = -Math.cos(enemy.attackAngle) * (runSpeed * 0.25);
            enemy.moveVy = -Math.sin(enemy.attackAngle) * (runSpeed * 0.25);
          } else {
            enemy.moveVx = Math.cos(enemy.attackAngle) * runSpeed;
            enemy.moveVy = Math.sin(enemy.attackAngle) * runSpeed;
          }

          spawnHitboxTrail(enemy.x, enemy.y, enemy.attackAngle, settings.reach, 34, 'enemy');
        }
      } else if (enemy.state === 'ATTACKING') {
        enemy.stateTimer -= dt;

        if (enemy.attackTactic === 'BACKSTEP' || enemy.attackTactic === 'BAIT_WHIFF' || distToPlayer > enemy.radius + player.radius + 4) {
          enemy.x += enemy.moveVx * dt;
          enemy.y += enemy.moveVy * dt;
        }

        trailSpawnTimer -= dt;
        if (trailSpawnTimer <= 0) {
          spawnHitboxTrail(enemy.x, enemy.y, enemy.attackAngle, settings.reach, 34, 'enemy');
          trailSpawnTimer = 0.024;
        }

        if (!enemy.hasHitThisSwing && !enemy.hasBeenBlockedThisSwing && !player.isVaporizing) {
          const hit = checkRectCircleHit(
            enemy.x,
            enemy.y,
            enemy.attackAngle,
            settings.reach,
            34,
            player.x,
            player.y,
            player.radius
          );

          if (hit) {
            if (player.isBlocking) {
              enemy.hasBeenBlockedThisSwing = true;
              player.punchCd = 0;
              player.parryWindowTimer = 1.0;

              player.blockImpactFlash = 1.0;
              player.bodySquash = -0.32;
              const shieldX = player.x + Math.cos(player.angle) * (player.radius + 10);
              const shieldY = player.y + Math.sin(player.angle) * (player.radius + 10);
              spawnShockwave(shieldX, shieldY, 10, 68, '#10b981', 0.28, 4.5);
              spawnParticles(shieldX, shieldY, 20, '#34d399', 100, 320, 0.2, 0.45, 2.5, 5);
              spawnDirectionalSparks(shieldX, shieldY, player.angle, 2.2, 16, '#a7f3d0', 140, 360);
              triggerScreenShake(6, 0.16);
              hitstopTimer = 0.035;

              enemy.knockbackVx = -Math.cos(enemy.attackAngle) * 180;
              enemy.knockbackVy = -Math.sin(enemy.attackAngle) * 180;

              const react = Math.max(0, Math.round(player.blockStartTime - enemy.windupStartTime));
              stats.lastReactionMs = react;
              updateStatsUI();
              showJudgment('BLOCKED! (1秒以内にパンチでパリィ)', '#10b981');
            } else {
              enemy.hasHitThisSwing = true;
              stats.attempts++;
              const react = Math.round(performance.now() - enemy.windupStartTime);
              stats.lastReactionMs = react;
              updateStatsUI();

              triggerScreenShake(9, 0.22);
              triggerScreenFlash('#f43f5e', 0.18);
              spawnParticles(player.x, player.y, 18, '#f43f5e', 120, 340, 0.2, 0.4, 3, 5);

              showJudgment('HIT! (被弾)', '#f43f5e');
            }
          }
        }

        if (enemy.stateTimer <= 0) {
          enemy.moveVx = 0;
          enemy.moveVy = 0;
          if (enemy.comboRemaining > 0 && !player.isVaporizing) {
            enemy.comboRemaining--;
            const nextAngle = Math.atan2(player.y - enemy.y, player.x - enemy.x);
            chooseAndStartAttack(nextAngle, true);
          } else {
            resetAttackPacing();
            enemy.state = 'CHASING';
          }
        }
      }

      resolveCollisions(enemy, false);
      updateHUD();
    }

    function chooseAndStartAttack(targetAngle, isFollowUpCombo = false) {
      enemy.state = 'WINDUP';
      const windupJitter = 0.75 + Math.random() * 0.50;
      enemy.currentWindupDuration = Math.max(0.10, settings.windupSec * windupJitter);
      enemy.stateTimer = enemy.currentWindupDuration;
      enemy.windupStartTime = performance.now();
      enemy.activeStartTime = 0;
      enemy.attackAngle = targetAngle;
      enemy.feintPlanned = false;
      enemy.misdirectOffset = 0;

      if (!isFollowUpCombo && settings.combo) {
        enemy.comboRemaining = Math.floor(Math.random() * 2) + 1;
      }

      if (!settings.feint) {
        enemy.attackTactic = 'NORMAL';
        return;
      }

      const roll = Math.random();
      if (roll < 0.86) {
        enemy.attackTactic = 'NORMAL';
      } else if (roll < 0.90) {
        enemy.attackTactic = 'BACKSTEP';
      } else if (roll < 0.95) {
        enemy.attackTactic = 'MISDIRECT';
        enemy.misdirectOffset = (Math.random() < 0.5 ? 1 : -1) * 0.42;
      } else if (roll < 0.98) {
        enemy.attackTactic = 'BAIT_WHIFF';
        enemy.misdirectOffset = (Math.random() < 0.5 ? 1 : -1) * 0.22;
      } else {
        enemy.attackTactic = 'NORMAL';
        enemy.feintPlanned = true;
      }
    }

    function updateHUD() {
      const blockRatio = 1 - (player.blockCd / player.blockCdMax);
      blockCdBar.style.width = `${Math.max(0, Math.min(100, blockRatio * 100))}%`;

      const punchRatio = 1 - (player.punchCd / player.punchCdMax);
      punchCdBar.style.width = `${Math.max(0, Math.min(100, punchRatio * 100))}%`;
      actionPunchBtn.classList.toggle('ready', player.punchCd <= 0 && !player.isVaporizing);

      const stamInt = Math.round(player.stamina);
      playerStaminaText.textContent = player.isVaporizing ? 'VAPORIZED...' : `${stamInt} / ${player.maxStamina}`;
      playerStaminaBar.style.width = player.isVaporizing ? '0%' : `${(player.stamina / player.maxStamina) * 100}%`;
      playerStaminaBar.classList.toggle('exhausted', player.staminaExhausted || player.isVaporizing);

      if (enemy.state === 'CONFUSED') {
        aiStateText.textContent = `CONFUSED (?) [${player.insideObstacleTimer.toFixed(1)}s]`;
        aiStateText.className = 'stat-val-yellow';
      } else if (enemy.state === 'HADOU_CHARGE') {
        aiStateText.textContent = 'CHARGING GIGA HADOU (!?)';
        aiStateText.className = 'stat-val-red';
      } else if (enemy.state === 'HADOU_FIRE') {
        aiStateText.textContent = 'GIGA HADOUHOU!!';
        aiStateText.className = 'stat-val-red';
      } else if (enemy.state === 'STUNNED') {
        aiStateText.textContent = 'STUNNED!';
        aiStateText.className = 'stat-val-purple';
      } else if (enemy.state === 'DISARMED') {
        aiStateText.textContent = 'NO ATTACK (3s)';
        aiStateText.className = 'stat-val-yellow';
      } else if (enemy.state === 'BAITING') {
        aiStateText.textContent = 'BLOCK BAIT!';
        aiStateText.className = 'stat-val-orange';
      } else if (enemy.state === 'WINDUP') {
        if (enemy.attackTactic === 'BACKSTEP') {
          aiStateText.textContent = 'BACKSTEP WINDUP!';
        } else if (enemy.attackTactic === 'BAIT_WHIFF' || enemy.attackTactic === 'MISDIRECT') {
          aiStateText.textContent = 'BAIT / FEINT!';
        } else {
          aiStateText.textContent = 'WINDUP!';
        }
        aiStateText.className = 'stat-val-yellow';
      } else if (enemy.state === 'ATTACKING') {
        if (enemy.attackTactic === 'BACKSTEP') {
          aiStateText.textContent = 'BACKSTEP ATK!';
        } else {
          aiStateText.textContent = 'ATTACKING!';
        }
        aiStateText.className = 'stat-val-red';
      } else {
        aiStateText.textContent = 'CHASING';
        aiStateText.className = 'stat-val-cyan';
      }

      const enemyStamInt = Math.round(enemy.stamina);
      enemyStaminaText.textContent = `${enemyStamInt} / ${enemy.maxStamina}`;
      enemyStaminaBar.style.width = `${(enemy.stamina / enemy.maxStamina) * 100}%`;
    }

    // Rendering (2D Camera centered on Player)
    function draw() {
      ctx.clearRect(0, 0, canvas.width, canvas.height);

      let shakeX = 0;
      let shakeY = 0;
      if (screenShakeTimer > 0) {
        shakeX = (Math.random() - 0.5) * 2 * screenShakeIntensity;
        shakeY = (Math.random() - 0.5) * 2 * screenShakeIntensity;
      }

      const camX = canvas.width / 2 - player.x + shakeX;
      const camY = canvas.height / 2 - player.y + shakeY;

      ctx.save();
      ctx.translate(camX, camY);

      // 1. Background Grid
      const gridSize = 64;
      const startX = Math.floor((player.x - canvas.width / 2) / gridSize) * gridSize;
      const endX = player.x + canvas.width / 2 + gridSize;
      const startY = Math.floor((player.y - canvas.height / 2) / gridSize) * gridSize;
      const endY = player.y + canvas.height / 2 + gridSize;

      ctx.strokeStyle = 'rgba(30, 41, 59, 0.45)';
      ctx.lineWidth = 1;
      ctx.beginPath();
      for (let gx = startX; gx <= endX; gx += gridSize) {
        if (gx >= WORLD.minX && gx <= WORLD.maxX) {
          ctx.moveTo(gx, Math.max(WORLD.minY, startY));
          ctx.lineTo(gx, Math.min(WORLD.maxY, endY));
        }
      }
      for (let gy = startY; gy <= endY; gy += gridSize) {
        if (gy >= WORLD.minY && gy <= WORLD.maxY) {
          ctx.moveTo(Math.max(WORLD.minX, startX), gy);
          ctx.lineTo(Math.min(WORLD.maxX, endX), gy);
        }
      }
      ctx.stroke();

      // 2. Red Out-of-Bounds Boundary Lines
      ctx.save();
      ctx.strokeStyle = '#f43f5e';
      ctx.lineWidth = 6;
      ctx.shadowColor = '#f43f5e';
      ctx.shadowBlur = 18;
      ctx.strokeRect(
        WORLD.minX,
        WORLD.minY,
        WORLD.maxX - WORLD.minX,
        WORLD.maxY - WORLD.minY
      );
      ctx.restore();

      // 3. Cyan Obstacles
      for (const obs of obstacles) {
        ctx.save();
        ctx.fillStyle = 'rgba(34, 211, 238, 0.08)';
        ctx.strokeStyle = '#22d3ee';
        ctx.lineWidth = 2;
        ctx.shadowColor = '#22d3ee';
        ctx.shadowBlur = 10;
        ctx.beginPath();
        ctx.roundRect(obs.x, obs.y, obs.w, obs.h, 8);
        ctx.fill();
        ctx.stroke();
        ctx.restore();
      }

      // 4. Hitbox Afterimages
      for (const t of hitboxTrails) {
        const ratio = Math.max(0, t.life / t.maxLife);
        const strokeAlpha = 0.42 * ratio;
        const fillAlpha = 0.08 * ratio;

        ctx.save();
        ctx.translate(t.x, t.y);
        ctx.rotate(t.angle);
        ctx.lineWidth = 1.0;
        ctx.setLineDash([2, 2]);
        if (t.type === 'enemy') {
          ctx.strokeStyle = `rgba(244, 63, 94, ${strokeAlpha})`;
          ctx.fillStyle = `rgba(244, 63, 94, ${fillAlpha})`;
        } else {
          ctx.strokeStyle = `rgba(56, 189, 248, ${strokeAlpha})`;
          ctx.fillStyle = `rgba(56, 189, 248, ${fillAlpha})`;
        }
        ctx.fillRect(0, -t.width / 2, t.length, t.width);
        ctx.strokeRect(0, -t.width / 2, t.length, t.width);
        ctx.restore();
      }

      // 5. Enemy Current Attack Hitbox & Hadouhou Laser
      if (enemy.state === 'ATTACKING') {
        drawAttackBox(enemy.x, enemy.y, enemy.attackAngle, settings.reach, 34, true);
      } else if (enemy.state === 'WINDUP') {
        drawAttackBox(enemy.x, enemy.y, enemy.attackAngle, settings.reach, 34, false);
      } else if (enemy.state === 'HADOU_CHARGE') {
        drawHadouCharge();
      } else if (enemy.state === 'HADOU_FIRE') {
        drawHadouBeam();
      }

      // 6. Player Punch & Parry Animated Strike
      if (player.punchVisualTimer > 0 && !player.isVaporizing) {
        drawPlayerPunchAnimation();
      }

      // 7. Enemy Entity & Overhead Icon (? / !?)
      drawEnemyEntity();

      // 8. Player Entity & Block Shield Animation (蒸発完了後は消滅)
      drawPlayerEntity();

      // 9. Shockwaves, Parry Flares & Particles
      drawVisualEffects();

      ctx.restore();

      // Screen Flash Overlay
      if (screenFlashAlpha > 0) {
        ctx.save();
        ctx.fillStyle = screenFlashColor;
        ctx.globalAlpha = screenFlashAlpha;
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        ctx.restore();
      }
    }

    function drawEnemyEntity() {
      ctx.save();
      let enemyColor = '#94a3b8';
      let enemyGlow = 'transparent';
      if (enemy.hitFlashTimer > 0) {
        enemyColor = '#ffffff';
        enemyGlow = '#facc15';
      } else if (enemy.state === 'HADOU_CHARGE' || enemy.state === 'HADOU_FIRE') {
        enemyColor = '#f43f5e';
        enemyGlow = '#f43f5e';
      } else if (enemy.state === 'CONFUSED') {
        enemyColor = '#facc15';
        enemyGlow = '#facc15';
      } else if (enemy.state === 'STUNNED') {
        enemyColor = '#9333ea';
        enemyGlow = '#a855f7';
      } else if (enemy.state === 'BAITING') {
        enemyColor = '#fb923c';
        enemyGlow = '#fb923c';
      } else if (enemy.state === 'WINDUP' && settings.telegraph) {
        enemyColor = '#facc15';
        enemyGlow = '#facc15';
      } else if (enemy.state === 'ATTACKING') {
        enemyColor = '#f43f5e';
        enemyGlow = '#f43f5e';
      } else if (enemy.state === 'DISARMED') {
        enemyColor = '#64748b';
      }

      ctx.fillStyle = enemyColor;
      ctx.shadowColor = enemyGlow;
      ctx.shadowBlur = enemyGlow !== 'transparent' ? 22 : 0;
      ctx.beginPath();
      ctx.arc(enemy.x, enemy.y, enemy.radius, 0, Math.PI * 2);
      ctx.fill();

      // 頭上の「?」および「!?」アイコン描画
      if (enemy.state === 'CONFUSED') {
        const bob = Math.sin(performance.now() * 0.012) * 3;
        ctx.font = '900 26px "Segoe UI", sans-serif';
        ctx.textAlign = 'center';
        ctx.textBaseline = 'bottom';
        ctx.fillStyle = '#facc15';
        ctx.shadowColor = '#facc15';
        ctx.shadowBlur = 14;
        ctx.fillText('?', enemy.x, enemy.y - enemy.radius - 8 + bob);
      } else if (enemy.state === 'HADOU_CHARGE' || enemy.state === 'HADOU_FIRE') {
        const shakeIconX = (Math.random() - 0.5) * 6;
        const shakeIconY = (Math.random() - 0.5) * 6;
        ctx.font = '900 34px "Segoe UI", sans-serif';
        ctx.textAlign = 'center';
        ctx.textBaseline = 'bottom';
        ctx.fillStyle = '#f43f5e';
        ctx.shadowColor = '#f43f5e';
        ctx.shadowBlur = 22;
        ctx.fillText('!?', enemy.x + shakeIconX, enemy.y - enemy.radius - 10 + shakeIconY);
      }

      ctx.restore();
    }

    function drawHadouCharge() {
      const progress = 1 - (enemy.stateTimer / 1.15);
      const now = performance.now();

      // 1. 敵の足元に広がる回転エネルギー魔法陣リング
      ctx.save();
      ctx.translate(enemy.x, enemy.y);
      ctx.rotate(now * 0.008);
      ctx.strokeStyle = `rgba(244, 63, 94, ${0.4 + progress * 0.6})`;
      ctx.lineWidth = 3;
      ctx.shadowColor = '#f43f5e';
      ctx.shadowBlur = 24;
      ctx.setLineDash([14, 8]);
      ctx.beginPath();
      ctx.arc(0, 0, 26 + progress * 34, 0, Math.PI * 2);
      ctx.stroke();

      ctx.rotate(-now * 0.016);
      ctx.strokeStyle = `rgba(250, 204, 21, ${0.4 + progress * 0.6})`;
      ctx.lineWidth = 2;
      ctx.setLineDash([8, 10]);
      ctx.beginPath();
      ctx.arc(0, 0, 18 + progress * 22, 0, Math.PI * 2);
      ctx.stroke();
      ctx.restore();

      // 2. 射線予測ライン(極太レーザーの予告帯)
      ctx.save();
      ctx.translate(enemy.x, enemy.y);
      ctx.rotate(enemy.hadouAngle);

      ctx.fillStyle = `rgba(244, 63, 94, ${0.06 + progress * 0.14})`;
      ctx.fillRect(20, -55, 1500, 110);

      ctx.strokeStyle = `rgba(244, 63, 94, ${0.35 + progress * 0.6})`;
      ctx.lineWidth = 2 + progress * 5;
      ctx.setLineDash([16, 10]);
      ctx.beginPath();
      ctx.moveTo(20, -55);
      ctx.lineTo(1500, -55);
      ctx.moveTo(20, 55);
      ctx.lineTo(1500, 55);
      ctx.stroke();
      ctx.setLineDash([]);

      // 3. 手元に溜まる超巨大な臨界エネルギー球
      const pulse = 1 + Math.sin(now * 0.06) * 0.15;
      const orbR = (10 + progress * 28) * pulse;
      ctx.fillStyle = '#f43f5e';
      ctx.shadowColor = '#f43f5e';
      ctx.shadowBlur = 35;
      ctx.beginPath();
      ctx.arc(24, 0, orbR, 0, Math.PI * 2);
      ctx.fill();

      ctx.fillStyle = '#facc15';
      ctx.beginPath();
      ctx.arc(24, 0, orbR * 0.72, 0, Math.PI * 2);
      ctx.fill();

      ctx.fillStyle = '#ffffff';
      ctx.beginPath();
      ctx.arc(24, 0, orbR * 0.45, 0, Math.PI * 2);
      ctx.fill();

      ctx.restore();
    }

    function drawHadouBeam() {
      const ratio = Math.max(0, enemy.stateTimer / 0.85);
      const pulse = 1 + Math.sin(performance.now() * 0.09) * 0.08;
      const beamWidth = 110 * (0.35 + 0.65 * ratio) * pulse;

      ctx.save();
      ctx.translate(enemy.x, enemy.y);
      ctx.rotate(enemy.hadouAngle);

      // 1. 最外周の広域真紅オーラ
      ctx.fillStyle = `rgba(244, 63, 94, ${0.55 * ratio})`;
      ctx.shadowColor = '#f43f5e';
      ctx.shadowBlur = 45;
      ctx.fillRect(16, -(beamWidth * 1.35) / 2, 1500, beamWidth * 1.35);

      // 2. メインの極太真紅ビーム層
      ctx.fillStyle = `rgba(244, 63, 94, ${0.92 * ratio})`;
      ctx.fillRect(16, -beamWidth / 2, 1500, beamWidth);

      // 3. 中間の黄金・オレンジエネルギー層
      ctx.fillStyle = `rgba(250, 204, 21, ${0.95 * ratio})`;
      ctx.fillRect(16, -(beamWidth * 0.62) / 2, 1500, beamWidth * 0.62);

      // 4. 中心の白熱プラズマコア
      ctx.fillStyle = `rgba(255, 255, 255, ${ratio})`;
      ctx.fillRect(16, -(beamWidth * 0.32) / 2, 1500, beamWidth * 0.32);

      // 5. ビームの周囲をうねる螺旋エネルギー波(2本のサイン波)
      ctx.strokeStyle = `rgba(255, 255, 255, ${0.85 * ratio})`;
      ctx.lineWidth = 4 * ratio;
      ctx.beginPath();
      const phase = performance.now() * 0.03;
      for (let x = 20; x <= 1400; x += 25) {
        const y = Math.sin(x * 0.025 - phase) * (beamWidth * 0.52);
        if (x === 20) ctx.moveTo(x, y);
        else ctx.lineTo(x, y);
      }
      ctx.stroke();

      ctx.strokeStyle = `rgba(250, 204, 21, ${0.85 * ratio})`;
      ctx.beginPath();
      for (let x = 20; x <= 1400; x += 25) {
        const y = -Math.sin(x * 0.025 - phase) * (beamWidth * 0.52);
        if (x === 20) ctx.moveTo(x, y);
        else ctx.lineTo(x, y);
      }
      ctx.stroke();

      // 6. 発射口の超巨大閃光球&衝撃リング
      ctx.fillStyle = '#ffffff';
      ctx.shadowColor = '#facc15';
      ctx.shadowBlur = 40;
      ctx.beginPath();
      ctx.arc(24, 0, beamWidth * 0.78, 0, Math.PI * 2);
      ctx.fill();

      ctx.strokeStyle = `rgba(255, 255, 255, ${ratio})`;
      ctx.lineWidth = 5;
      ctx.beginPath();
      ctx.ellipse(45, 0, 18, beamWidth * 0.85, 0, 0, Math.PI * 2);
      ctx.stroke();

      ctx.restore();
    }

    function drawPlayerPunchAnimation() {
      const progress = 1 - (player.punchVisualTimer / player.punchVisualMax);
      const extend = progress < 0.35
        ? (progress / 0.35)
        : Math.max(0, 1 - ((progress - 0.35) / 0.65));
      const alpha = Math.max(0, 1 - Math.pow(progress, 1.8));

      const isParry = player.punchIsParryAttempt;
      const w = isParry ? 44 : 42;
      const l = isParry ? 62 : 58;
      const fistDist = 14 + extend * (l - 12);

      ctx.save();
      ctx.translate(player.x, player.y);
      ctx.rotate(player.punchAngle);

      ctx.setLineDash([2, 2]);
      ctx.fillStyle = isParry
        ? `rgba(250, 204, 21, ${0.18 * alpha})`
        : `rgba(56, 189, 248, ${0.14 * alpha})`;
      ctx.strokeStyle = isParry
        ? `rgba(250, 204, 21, ${0.75 * alpha})`
        : `rgba(103, 232, 249, ${0.7 * alpha})`;
      ctx.lineWidth = 1.2;
      ctx.fillRect(0, -w / 2, l, w);
      ctx.strokeRect(0, -w / 2, l, w);
      ctx.setLineDash([]);

      const grad = ctx.createLinearGradient(8, 0, fistDist, 0);
      if (isParry) {
        grad.addColorStop(0, 'rgba(250, 204, 21, 0)');
        grad.addColorStop(0.5, `rgba(250, 204, 21, ${0.45 * alpha})`);
        grad.addColorStop(1, `rgba(255, 255, 255, ${0.9 * alpha})`);
      } else {
        grad.addColorStop(0, 'rgba(56, 189, 248, 0)');
        grad.addColorStop(0.5, `rgba(56, 189, 248, ${0.4 * alpha})`);
        grad.addColorStop(1, `rgba(186, 230, 253, ${0.85 * alpha})`);
      }

      ctx.fillStyle = grad;
      ctx.beginPath();
      ctx.moveTo(10, -5);
      ctx.lineTo(fistDist, -13);
      ctx.lineTo(fistDist + 6, 0);
      ctx.lineTo(fistDist, 13);
      ctx.lineTo(10, 5);
      ctx.closePath();
      ctx.fill();

      ctx.shadowColor = isParry ? '#facc15' : '#38bdf8';
      ctx.shadowBlur = isParry ? 20 : 14;
      ctx.fillStyle = isParry ? `rgba(254, 240, 138, ${alpha})` : `rgba(224, 242, 254, ${alpha})`;
      ctx.beginPath();
      ctx.arc(fistDist, 0, isParry ? 11 : 9, 0, Math.PI * 2);
      ctx.fill();

      if (isParry) {
        ctx.strokeStyle = `rgba(250, 204, 21, ${alpha})`;
        ctx.lineWidth = 3.5;
        ctx.beginPath();
        ctx.arc(fistDist - 8, 0, 24, -Math.PI * 0.42, Math.PI * 0.42);
        ctx.stroke();
      }

      ctx.restore();
    }

    function drawPlayerEntity() {
      // 蒸発中は開始直後だけ白く燃え尽きる残像を描画し、その後完全に消える
      if (player.isVaporizing) {
        const remainRatio = Math.max(0, player.vaporizeTimer / player.vaporizeDuration);
        if (remainRatio > 0.65) {
          const fade = (remainRatio - 0.65) / 0.35;
          ctx.save();
          ctx.translate(player.x, player.y);
          ctx.globalAlpha = fade;
          ctx.fillStyle = '#ffffff';
          ctx.shadowColor = '#38bdf8';
          ctx.shadowBlur = 30;
          ctx.beginPath();
          ctx.arc(0, 0, player.radius * (1.4 - fade * 0.4), 0, Math.PI * 2);
          ctx.fill();
          ctx.restore();
        }
        return;
      }

      ctx.save();
      ctx.translate(player.x, player.y);

      // リスポーン直後の光の柱リング演出
      if (player.respawnBeamTimer > 0) {
        const rRatio = player.respawnBeamTimer / 0.45;
        ctx.save();
        ctx.strokeStyle = `rgba(103, 232, 249, ${rRatio})`;
        ctx.lineWidth = 4 * rRatio;
        ctx.shadowColor = '#22d3ee';
        ctx.shadowBlur = 20;
        ctx.beginPath();
        ctx.arc(0, 0, player.radius + 12 * rRatio, 0, Math.PI * 2);
        ctx.stroke();
        ctx.restore();
      }

      if (player.isBlocking) {
        const remainRatio = Math.max(0, player.blockHoldTimer / player.blockMaxHold);
        const deployScale = 0.55 + 0.45 * player.blockDeployAnim;
        const impactBoost = player.blockImpactFlash;

        ctx.strokeStyle = impactBoost > 0 ? '#ffffff' : '#10b981';
        ctx.lineWidth = 3.5 + impactBoost * 2.5;
        ctx.shadowColor = '#10b981';
        ctx.shadowBlur = 16 + impactBoost * 18;
        ctx.beginPath();
        ctx.arc(0, 0, (player.radius + 7) * deployScale, -Math.PI / 2, -Math.PI / 2 + Math.PI * 2 * remainRatio);
        ctx.stroke();

        ctx.save();
        ctx.rotate(player.angle);
        const shieldRadius = (player.radius + 14) * deployScale + impactBoost * 5;
        const arcSpan = Math.PI * 0.68;

        ctx.strokeStyle = impactBoost > 0
          ? `rgba(255, 255, 255, ${0.95})`
          : `rgba(52, 211, 153, ${0.85 * player.blockDeployAnim})`;
        ctx.lineWidth = 5 + impactBoost * 4;
        ctx.shadowColor = impactBoost > 0 ? '#ffffff' : '#10b981';
        ctx.shadowBlur = 22;
        ctx.beginPath();
        ctx.arc(0, 0, shieldRadius, -arcSpan / 2, arcSpan / 2);
        ctx.stroke();

        ctx.strokeStyle = `rgba(167, 243, 208, ${0.55 * player.blockDeployAnim})`;
        ctx.lineWidth = 2;
        ctx.beginPath();
        ctx.arc(0, 0, shieldRadius - 5, -arcSpan * 0.42, arcSpan * 0.42);
        ctx.stroke();
        ctx.restore();

      } else if (player.parryWindowTimer > 0) {
        const spin = performance.now() * 0.005;
        const pulse = 1 + Math.sin(performance.now() * 0.025) * 0.08;
        ctx.save();
        ctx.rotate(spin);
        ctx.strokeStyle = 'rgba(250, 204, 21, 0.85)';
        ctx.lineWidth = 2.5;
        ctx.shadowColor = '#facc15';
        ctx.shadowBlur = 12;
        ctx.setLineDash([6, 5]);
        ctx.beginPath();
        ctx.arc(0, 0, (player.radius + 7) * pulse, 0, Math.PI * 2);
        ctx.stroke();
        ctx.restore();
      }

      if (player.isInsideObstacle) {
        ctx.globalAlpha = 0.72;
      }

      ctx.save();
      ctx.rotate(player.angle);
      const stretchX = 1 + player.bodySquash;
      const stretchY = 1 - player.bodySquash * 0.55;
      ctx.scale(stretchX, stretchY);

      ctx.fillStyle = player.blockImpactFlash > 0
        ? '#a7f3d0'
        : (player.isBlocking ? '#10b981' : '#06b6d4');
      ctx.shadowColor = player.isBlocking ? '#10b981' : '#22d3ee';
      ctx.shadowBlur = player.isBlocking ? 20 : 16;
      ctx.beginPath();
      ctx.arc(0, 0, player.radius, 0, Math.PI * 2);
      ctx.fill();

      ctx.fillStyle = '#facc15';
      ctx.shadowBlur = 0;
      ctx.beginPath();
      ctx.moveTo(player.radius + 9, 0);
      ctx.lineTo(player.radius + 1, -5);
      ctx.lineTo(player.radius + 1, 5);
      ctx.closePath();
      ctx.fill();
      ctx.restore();

      ctx.restore();
    }

    function drawVisualEffects() {
      for (const s of shockwaves) {
        const progress = 1 - (s.life / s.maxLife);
        const currentR = s.startR + (s.endR - s.startR) * (1 - Math.pow(1 - progress, 2.5));
        const alpha = Math.max(0, 1 - progress);

        ctx.save();
        ctx.strokeStyle = s.color;
        ctx.globalAlpha = alpha;
        ctx.lineWidth = s.lineWidth * (1 - progress * 0.6);
        ctx.shadowColor = s.color;
        ctx.shadowBlur = 12;
        ctx.beginPath();
        ctx.arc(s.x, s.y, currentR, 0, Math.PI * 2);
        ctx.stroke();
        ctx.restore();
      }

      for (const f of slashFlares) {
        const progress = 1 - (f.life / f.maxLife);
        const alpha = Math.max(0, 1 - Math.pow(progress, 1.5));
        const len = 85 * (0.4 + 0.6 * Math.sin(Math.min(1, progress * 2) * Math.PI * 0.5));
        const thick = 9 * (1 - progress);

        ctx.save();
        ctx.translate(f.x, f.y);
        ctx.rotate(f.angle + Math.PI / 4);
        ctx.globalAlpha = alpha;
        ctx.shadowColor = '#facc15';
        ctx.shadowBlur = 24;

        for (let k = 0; k < 2; k++) {
          ctx.rotate(Math.PI / 2);
          ctx.fillStyle = '#ffffff';
          ctx.beginPath();
          ctx.moveTo(-len, 0);
          ctx.quadraticCurveTo(0, -thick, len, 0);
          ctx.quadraticCurveTo(0, thick, -len, 0);
          ctx.fill();

          ctx.strokeStyle = '#facc15';
          ctx.lineWidth = 2;
          ctx.stroke();
        }
        ctx.restore();
      }

      for (const p of particles) {
        const alpha = Math.max(0, p.life / p.maxLife);
        ctx.save();
        ctx.globalAlpha = alpha;
        ctx.fillStyle = p.color;
        ctx.shadowColor = p.color;
        ctx.shadowBlur = 8;

        if (p.isSpark) {
          const speed = Math.hypot(p.vx, p.vy);
          const ang = Math.atan2(p.vy, p.vx);
          ctx.translate(p.x, p.y);
          ctx.rotate(ang);
          const streak = Math.min(22, speed * 0.045);
          ctx.fillRect(-streak / 2, -p.size * 0.4, streak, p.size * 0.8);
        } else {
          ctx.beginPath();
          ctx.arc(p.x, p.y, p.size * alpha, 0, Math.PI * 2);
          ctx.fill();
        }
        ctx.restore();
      }
    }

    function drawAttackBox(x, y, angle, length, width, isActive) {
      ctx.save();
      ctx.translate(x, y);
      ctx.rotate(angle);
      ctx.setLineDash([2, 2]);
      if (isActive) {
        ctx.lineWidth = 1.2;
        ctx.strokeStyle = 'rgba(244, 63, 94, 0.65)';
        ctx.fillStyle = 'rgba(244, 63, 94, 0.14)';
      } else {
        ctx.lineWidth = 1.0;
        ctx.strokeStyle = 'rgba(244, 63, 94, 0.30)';
        ctx.fillStyle = 'rgba(244, 63, 94, 0.04)';
      }
      ctx.fillRect(0, -width / 2, length, width);
      ctx.strokeRect(0, -width / 2, length, width);
      ctx.restore();
    }

    function loop(now) {
      const dt = Math.min(0.05, (now - lastTimestamp) / 1000);
      lastTimestamp = now;

      update(dt);
      draw();
      requestAnimationFrame(loop);
    }

    requestAnimationFrame(loop);
  </script>
</body>
</html>

Embed on website

To embed this project on your website, copy the following code and paste it into your website's HTML: