<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>
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.