<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Retro Pop ブロック崩し ~Stage & Speed Up~</title>
  <style>
    /* プロ仕様のレトロポップデザイン */
    @import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');
    * { box-sizing: border-box; }
    body {
      margin: 0;
      background: linear-gradient(135deg, #2c3e50, #34495e);
      font-family: 'Press Start 2P', cursive;
      color: #ffea00;
      user-select: none;
    }
    #gameContainer {
      position: relative;
      width: 480px;
      margin: 20px auto;
      border: 4px solid #ffea00;
      background: #000;
      box-shadow: 0 0 20px rgba(0,0,0,0.8);
    }
    canvas {
      display: block;
      width: 100%;
      height: auto;
      image-rendering: pixelated;
    }
    #info {
      position: absolute;
      top: 5px;
      left: 5px;
      font-size: 10px;
      text-shadow: 1px 1px 0 #000;
    }
  </style>
</head>
<body>
  <div id="gameContainer">
    <div id="info"></div>
    <canvas id="gameCanvas" width="480" height="640"></canvas>
  </div>
  <script>
    // --------------------------------------------------
    // 基本設定・グローバル変数
    // --------------------------------------------------
    const canvas = document.getElementById('gameCanvas');
    const ctx = canvas.getContext('2d');
    const infoDiv = document.getElementById('info');

    // ゲーム基本情報
    let hp = 5;
    let score = 0;
    let currentStage = 0;
    // ステージ設定:各ステージはグリッドサイズ、余白、配置オフセット、セルの埋める確率(fillProbability)を持つ
    const stages = [
      {
        rows: 4,
        cols: 7,
        padding: 6,
        offsetTop: 50,
        offsetLeft: 30,
        blockWidth: 50,
        blockHeight: 20,
        fillProbability: 0.8
      },
      {
        rows: 5,
        cols: 9,
        padding: 5,
        offsetTop: 40,
        offsetLeft: 20,
        blockWidth: 45,
        blockHeight: 18,
        fillProbability: 0.9
      },
      {
        rows: 6,
        cols: 10,
        padding: 4,
        offsetTop: 30,
        offsetLeft: 15,
        blockWidth: 40,
        blockHeight: 16,
        fillProbability: 0.85
      }
    ];
    let stageBlocks = []; // 現在のステージのブロック群

    // ゲーム開始時間(弾速加速用)
    const gameStartTime = Date.now();
    const maxAccelTime = 5 * 60 * 1000; // 5分
    const maxSpeedMultiplier = 2.0;      // 最大2倍

    // Web Audio API(効果音&BGM用)
    const AudioContext = window.AudioContext || window.webkitAudioContext;
    const audioCtx = new AudioContext();

    // --------------------------------------------------
    // BGM(コード進行をループ再生)
    // --------------------------------------------------
    const bgmNotes = [
      // 例:Cメジャー→E→G→C5 / Aマイナー→C→E→G / Fメジャー→A→C5→G など
      { freq: 261.63, duration: 0.5 }, // C4
      { freq: 329.63, duration: 0.5 }, // E4
      { freq: 392.00, duration: 0.5 }, // G4
      { freq: 523.25, duration: 0.5 }, // C5
      { freq: 220.00, duration: 0.5 }, // A3
      { freq: 261.63, duration: 0.5 }, // C4
      { freq: 329.63, duration: 0.5 }, // E4
      { freq: 392.00, duration: 0.5 }, // G4
      { freq: 349.23, duration: 0.5 }, // F4
      { freq: 440.00, duration: 0.5 }, // A4
      { freq: 523.25, duration: 0.5 }, // C5
      { freq: 392.00, duration: 0.5 }  // G4
    ];
    let bgmIndex = 0;
    function playBGMSequence() {
      const note = bgmNotes[bgmIndex];
      const osc = audioCtx.createOscillator();
      const gain = audioCtx.createGain();
      osc.frequency.value = note.freq;
      osc.type = "triangle";
      gain.gain.value = 0.04;
      osc.connect(gain);
      gain.connect(audioCtx.destination);
      osc.start();
      osc.stop(audioCtx.currentTime + note.duration);
      bgmIndex = (bgmIndex + 1) % bgmNotes.length;
      setTimeout(playBGMSequence, note.duration * 1000);
    }
    window.addEventListener("click", () => {
      if (audioCtx.state === "suspended") audioCtx.resume();
    });
    playBGMSequence();

    // 効果音(シンプルビープ)
    // 通常は800Hz、ブロック衝突時などに使用。金属ブロックは高音(1200Hz)で鳴らす
    function playBeep(freq, duration = 0.1, vol = 0.15) {
      const osc = audioCtx.createOscillator();
      const gain = audioCtx.createGain();
      osc.frequency.value = freq;
      osc.type = "sine";
      gain.gain.value = vol;
      osc.connect(gain);
      gain.connect(audioCtx.destination);
      osc.start();
      osc.stop(audioCtx.currentTime + duration);
    }

    // --------------------------------------------------
    // オブジェクト群
    // --------------------------------------------------
    let balls = [];    // 複数ボール(各ボールは baseDx, baseDy を持つ)
    let items = [];    // 落下アイテム(従来の効果あり)
    let particles = []; // エフェクト用パーティクル

    // パドル(左右移動のみ)
    const paddleDefaultWidth = 80;
    let paddle = {
      x: (canvas.width - paddleDefaultWidth) / 2,
      y: canvas.height - 30,
      width: paddleDefaultWidth,
      height: 12,
      speed: 6,
      extendTimer: 0
    };

    // キー管理(左右キー / A, D)
    const keys = {};
    window.addEventListener("keydown", e => {
      keys[e.key.toLowerCase()] = true;
      if(["arrowleft","arrowright"].includes(e.key.toLowerCase())){
        e.preventDefault();
      }
    });
    window.addEventListener("keyup", e => {
      keys[e.key.toLowerCase()] = false;
    });

    // --------------------------------------------------
    // アイテム設定(従来通り "multiball", "extend", "bigball", "pierce")
    // --------------------------------------------------
    const ITEM_DROP_SPEED = 2;
    function spawnItem(x, y) {
      const types = ["multiball", "extend", "bigball", "pierce"];
      if (Math.random() < 0.3) {  // 30%の確率で発生
        const type = types[Math.floor(Math.random() * types.length)];
        items.push({
          type,
          x,
          y,
          dy: ITEM_DROP_SPEED,
          size: 16
        });
      }
    }

    // --------------------------------------------------
    // パーティクル(エフェクト)
    // --------------------------------------------------
    function spawnParticles(x, y, color) {
      for (let i = 0; i < 10; i++) {
        particles.push({
          x: x,
          y: y,
          dx: (Math.random()-0.5)*2,
          dy: (Math.random()-0.5)*2,
          life: 60,
          color: color
        });
      }
    }

    // --------------------------------------------------
    // ステージ初期化(ブロック配置:パールグローイング法)
    // --------------------------------------------------
    function initStage(stageIndex) {
      const stage = stages[stageIndex % stages.length];
      stageBlocks = [];
      const gridRows = stage.rows;
      const gridCols = stage.cols;
      const blockWidth = stage.blockWidth;
      const blockHeight = stage.blockHeight;
      const padding = stage.padding;
      const offsetLeft = stage.offsetLeft;
      const offsetTop = stage.offsetTop;
      const fillProbability = stage.fillProbability;
      // 各グリッドセルごとに、fillProbabilityの確率でブロックを配置
      for (let r = 0; r < gridRows; r++) {
        for (let c = 0; c < gridCols; c++) {
          if (Math.random() < fillProbability) {
            const x = offsetLeft + c * (blockWidth + padding);
            const y = offsetTop + r * (blockHeight + padding);
            let blockType, durability, maxDurability, color;
            // 15%の確率で壊せないメタルブロック
            if (Math.random() < 0.15) {
              blockType = "metal";
              durability = Infinity;
              maxDurability = Infinity;
              color = "#888888";  // メタルはグレー
            } else {
              blockType = "normal";
              maxDurability = Math.floor(Math.random() * 3) + 2; // 2~4
              durability = maxDurability;
              // ランダムなパステルカラー(可愛らしさ)
              const rVal = Math.floor(150 + Math.random()*105);
              const gVal = Math.floor(150 + Math.random()*105);
              const bVal = Math.floor(150 + Math.random()*105);
              color = `rgb(${rVal},${gVal},${bVal})`;
            }
            stageBlocks.push({
              x: x,
              y: y,
              width: blockWidth,
              height: blockHeight,
              type: blockType,
              durability: durability,
              maxDurability: maxDurability,
              status: 1,
              baseColor: color
            });
          }
        }
      }
      // 新ステージ開始時はアイテム・パーティクルをクリア
      items = [];
      particles = [];
      // 初期ボールのリセット(パドル上部から発射)
      balls = [];
      spawnBall(paddle.x + paddle.width/2, paddle.y - 10);
    }

    // --------------------------------------------------
    // ボール生成(baseDx, baseDy を保持)
    // --------------------------------------------------
    function spawnBall(x, y) {
      const angle = Math.random() * Math.PI/2 + Math.PI/4; // 上方向
      const speed = 3;
      balls.push({
        x: x,
        y: y,
        baseDx: speed * Math.cos(angle),
        baseDy: -speed * Math.sin(angle),
        radius: 8,
        piercing: false,
        bigTimer: 0,
        pierceTimer: 0
      });
    }

    // --------------------------------------------------
    // ユーティリティ:ランダムカラー(※ブロック生成用)
    // --------------------------------------------------
    function getRandomColor() {
      const letters = '89ABCDEF';
      let color = '#';
      for (let i = 0; i < 6; i++) {
        color += letters[Math.floor(Math.random()*letters.length)];
      }
      return color;
    }
    
    // --------------------------------------------------
    // 補助関数:色を暗くする(percentageは負の値で暗くする)
    // --------------------------------------------------
    function shadeColor(color, percent) {
      // colorは "rgb(r,g,b)" 形式と仮定
      const parts = color.match(/\d+/g);
      let r = parseInt(parts[0]), g = parseInt(parts[1]), b = parseInt(parts[2]);
      r = Math.max(Math.min(Math.floor(r * (100 + percent) / 100), 255), 0);
      g = Math.max(Math.min(Math.floor(g * (100 + percent) / 100), 255), 0);
      b = Math.max(Math.min(Math.floor(b * (100 + percent) / 100), 255), 0);
      return `rgb(${r},${g},${b})`;
    }

    // --------------------------------------------------
    // 初期化
    // --------------------------------------------------
    function initGame() {
      hp = 5;
      score = 0;
      currentStage = 0;
      paddle.width = paddleDefaultWidth;
      paddle.extendTimer = 0;
      initStage(currentStage);
    }
    initGame();

    // --------------------------------------------------
    // 衝突判定:円と長方形
    // --------------------------------------------------
    function circleRectCollision(cx, cy, radius, rx, ry, rw, rh) {
      const closestX = Math.max(rx, Math.min(cx, rx+rw));
      const closestY = Math.max(ry, Math.min(cy, ry+rh));
      const dx = cx - closestX;
      const dy = cy - closestY;
      return (dx*dx + dy*dy) <= radius*radius;
    }

    // --------------------------------------------------
    // メインループ(update/draw)
    // --------------------------------------------------
    let lastTime = performance.now();
    function gameLoop(time) {
      const deltaTime = time - lastTime;
      lastTime = time;
      update(deltaTime);
      draw();
      requestAnimationFrame(gameLoop);
    }
    requestAnimationFrame(gameLoop);

    // --------------------------------------------------
    // 更新処理
    // --------------------------------------------------
    function update(deltaTime) {
      // 経過時間による弾速倍率
      const elapsed = Date.now() - gameStartTime;
      const progress = Math.min(elapsed / maxAccelTime, 1);
      const speedMultiplier = 1 + progress * (maxSpeedMultiplier - 1);

      // パドル(左右移動のみ)の移動
      if (keys['arrowleft'] || keys['a']) { paddle.x -= paddle.speed; }
      if (keys['arrowright'] || keys['d']) { paddle.x += paddle.speed; }
      if (paddle.x < 0) paddle.x = 0;
      if (paddle.x + paddle.width > canvas.width) paddle.x = canvas.width - paddle.width;

      // パドル拡大効果タイマー
      if (paddle.extendTimer > 0) {
        paddle.extendTimer -= deltaTime;
        if (paddle.extendTimer <= 0) { paddle.width = paddleDefaultWidth; }
      }

      // 各ボールの更新
      for (let i = balls.length - 1; i >= 0; i--) {
        let ball = balls[i];
        // 効果タイマー更新
        if (ball.bigTimer > 0) {
          ball.bigTimer -= deltaTime;
          if (ball.bigTimer <= 0) { ball.radius = 8; }
        }
        if (ball.pierceTimer > 0) {
          ball.pierceTimer -= deltaTime;
          if (ball.pierceTimer <= 0) { ball.piercing = false; }
        }
        // 速度更新
        ball.x += ball.baseDx * speedMultiplier;
        ball.y += ball.baseDy * speedMultiplier;

        // 壁との衝突(左右・上)
        if (ball.x + ball.radius > canvas.width || ball.x - ball.radius < 0) {
          ball.baseDx = -ball.baseDx;
          playBeep(800, 0.05);
        }
        if (ball.y - ball.radius < 0) {
          ball.baseDy = -ball.baseDy;
          playBeep(800, 0.05);
        }

        // パドルとの衝突
        if (circleRectCollision(ball.x, ball.y, ball.radius, paddle.x, paddle.y, paddle.width, paddle.height)) {
          ball.baseDy = -Math.abs(ball.baseDy);
          playBeep(800, 0.05);
        }

        // ブロックとの衝突(各ブロックとの重なり判定)
        stageBlocks.forEach(block => {
          if (block.status === 1) {
            if (
              ball.x > block.x &&
              ball.x < block.x + block.width &&
              ball.y > block.y &&
              ball.y < block.y + block.height
            ) {
              // ブロックの種類によって処理を分岐
              if (block.type === "metal") {
                // メタルブロック:ダメージは入らず、キーンと高音
                playBeep(1200, 0.07, 0.18);
                // 反射は必ず起こす
                if (!ball.piercing) { ball.baseDy = -ball.baseDy; }
              } else {
                // 通常ブロック:耐久度を1減らす
                block.durability--;
                // ダメージを受けたら反射(貫通効果でなければ)
                if (!ball.piercing) { ball.baseDy = -ball.baseDy; }
                // まだ残っているなら、色をダメージ分だけ暗くする
                if (block.durability > 0) {
                  // 例:もとの色から (100 - (durability/maxDurability)*100)%分暗くする
                  const percent = -Math.floor(100 * (1 - block.durability / block.maxDurability));
                  block.baseColor = shadeColor(block.baseColor, percent);
                } else {
                  // 耐久度が0になったら破壊
                  block.status = 0;
                  score += 10;
                  spawnParticles(block.x + block.width/2, block.y + block.height/2, block.baseColor);
                  spawnItem(block.x + block.width/2, block.y + block.height/2);
                }
              }
            }
          }
        });

        // 画面下部(ボールが完全に消えた場合)の判定
        if (ball.y - ball.radius > canvas.height) {
          balls.splice(i, 1);
        }
      }

      // すべてのボールがなくなった場合:HPを減らして再射出
      if (balls.length === 0) {
        hp--;
        if (hp > 0) {
          spawnBall(paddle.x + paddle.width/2, paddle.y - 10);
        } else {
          alert("GAME OVER\nScore: " + score);
          document.location.reload();
          return;
        }
      }

      // アイテム更新
      for (let i = items.length - 1; i >= 0; i--) {
        let item = items[i];
        item.y += item.dy;
        if (
          item.x > paddle.x && item.x < paddle.x + paddle.width &&
          item.y > paddle.y && item.y < paddle.y + paddle.height
        ) {
          applyItemEffect(item.type);
          playBeep(1000, 0.1);
          items.splice(i, 1);
          continue;
        }
        if (item.y > canvas.height) { items.splice(i, 1); }
      }

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

      // ステージ進行:破壊可能なブロック(metal以外)がすべて破壊されたら次のステージへ
      if (stageBlocks.every(block => block.type === "metal" || block.status === 0)) {
        currentStage++;
        initStage(currentStage);
      }

      // 情報表示更新
      infoDiv.innerHTML = `HP: ${hp}  Score: ${score}  Stage: ${currentStage+1}<br>
      Speed: x${speedMultiplier.toFixed(2)}`;
    }

    // --------------------------------------------------
    // アイテム効果の適用(効果時間10秒、従来仕様)
    // --------------------------------------------------
    function applyItemEffect(type) {
      switch(type) {
        case "multiball":
          let currentBalls = balls.slice();
          currentBalls.forEach(ball => {
            for (let i = 0; i < 2; i++) {
              let angle = Math.atan2(ball.baseDy, ball.baseDx) + (Math.random()-0.5) * 0.5;
              const speed = Math.hypot(ball.baseDx, ball.baseDy);
              balls.push({
                x: ball.x,
                y: ball.y,
                baseDx: speed * Math.cos(angle),
                baseDy: speed * Math.sin(angle),
                radius: ball.radius,
                piercing: ball.piercing,
                bigTimer: ball.bigTimer,
                pierceTimer: ball.pierceTimer
              });
            }
          });
          break;
        case "extend":
          paddle.width = paddleDefaultWidth * 1.5;
          paddle.extendTimer = 10000;
          break;
        case "bigball":
          balls.forEach(ball => {
            ball.radius = 8 * 1.5;
            ball.bigTimer = 10000;
          });
          break;
        case "pierce":
          balls.forEach(ball => {
            ball.piercing = true;
            ball.pierceTimer = 10000;
          });
          break;
      }
    }

    // --------------------------------------------------
    // 描画処理
    // --------------------------------------------------
    function draw() {
      // 背景:統一感のあるグラデーション
      const grd = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
      grd.addColorStop(0, "#2c3e50");
      grd.addColorStop(1, "#34495e");
      ctx.fillStyle = grd;
      ctx.fillRect(0, 0, canvas.width, canvas.height);

      // ブロック描画
      stageBlocks.forEach(block => {
        if (block.status === 1) {
          // 通常ブロックは耐久度に応じた色で描画(ダメージが蓄積すると暗くなる)
          let drawColor = block.baseColor;
          ctx.fillStyle = drawColor;
          ctx.fillRect(block.x, block.y, block.width, block.height);
          ctx.strokeStyle = "#000";
          ctx.lineWidth = 2;
          ctx.strokeRect(block.x, block.y, block.width, block.height);
        }
      });

      // パドル描画
      ctx.fillStyle = "#00FF00";
      ctx.fillRect(paddle.x, paddle.y, paddle.width, paddle.height);
      ctx.strokeStyle = "#003300";
      ctx.lineWidth = 2;
      ctx.strokeRect(paddle.x, paddle.y, paddle.width, paddle.height);

      // ボール描画
      balls.forEach(ball => {
        ctx.beginPath();
        ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2);
        ctx.fillStyle = ball.piercing ? "#FFD700" : "#FFFFFF";
        ctx.fill();
        ctx.closePath();
      });

      // アイテム描画
      items.forEach(item => {
        ctx.beginPath();
        ctx.arc(item.x, item.y, item.size/2, 0, Math.PI * 2);
        switch(item.type) {
          case "multiball": ctx.fillStyle = "#FF69B4"; break;
          case "extend": ctx.fillStyle = "#00FFFF"; break;
          case "bigball": ctx.fillStyle = "#FFA500"; break;
          case "pierce": ctx.fillStyle = "#ADFF2F"; break;
          default: ctx.fillStyle = "#FFF"; break;
        }
        ctx.fill();
        ctx.closePath();
      });

      // パーティクル描画
      particles.forEach(p => {
        ctx.fillStyle = p.color;
        ctx.globalAlpha = Math.max(p.life/60, 0);
        ctx.fillRect(p.x, p.y, 2, 2);
        ctx.globalAlpha = 1;
      });
    }
  </script>
</body>
</html>

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.