<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Bullet Hell STG Game</title>
<style>
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
background: radial-gradient(circle at top, #1f2b5c, #070915 70%);
display: flex;
align-items: center;
justify-content: center;
font-family: system-ui, sans-serif;
color: white;
overflow: hidden;
}
.wrap { text-align: center; }
h1 {
margin: 0 0 10px;
font-size: 28px;
letter-spacing: 0.08em;
}
canvas {
background: #050816;
border: 3px solid #ffffff33;
border-radius: 16px;
box-shadow: 0 20px 80px #000a;
display: block;
}
.info {
margin-top: 10px;
color: #dce6ff;
font-size: 14px;
}
.panel {
position: fixed;
top: 16px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 20px;
background: #0008;
border: 1px solid #fff2;
padding: 8px 16px;
border-radius: 999px;
backdrop-filter: blur(8px);
font-weight: 700;
}
</style>
</head>
<body>
<div class="panel">
<div>Score: <span id="score">0</span></div>
<div>HP: <span id="hp">5</span></div>
<div>Power: <span id="power">1</span></div>
</div>
<div class="wrap">
<h1>BULLET STORM</h1>
<canvas id="game" width="480" height="640"></canvas>
<div class="info">移動: WASD / 方向キー ショット: Space パワーアップを取ると弾が強化 リスタート: Enter</div>
</div>
<script>
const canvas = document.getElementById("game");
const ctx = canvas.getContext("2d");
const scoreEl = document.getElementById("score");
const hpEl = document.getElementById("hp");
const powerEl = document.getElementById("power");
const keys = {};
const player = {
x: canvas.width / 2,
y: canvas.height - 70,
w: 34,
h: 42,
speed: 5,
hp: 5,
power: 1,
shotCooldown: 0,
invincible: 0
};
let bullets = [];
let enemyBullets = [];
let enemies = [];
let items = [];
let particles = [];
let stars = [];
let score = 0;
let enemyTimer = 0;
let itemTimer = 0;
let gameOver = false;
let boss = null;
let bossTimer = 0;
let frame = 0;
for (let i = 0; i < 100; i++) {
stars.push({
x: Math.random() * canvas.width,
y: Math.random() * canvas.height,
r: Math.random() * 2 + 0.5,
speed: Math.random() * 2 + 1
});
}
window.addEventListener("keydown", (e) => {
keys[e.key.toLowerCase()] = true;
if (gameOver && e.key === "Enter") restart();
});
window.addEventListener("keyup", (e) => {
keys[e.key.toLowerCase()] = false;
});
function restart() {
player.x = canvas.width / 2;
player.y = canvas.height - 70;
player.hp = 5;
player.power = 1;
player.shotCooldown = 0;
player.invincible = 0;
bullets = [];
enemyBullets = [];
enemies = [];
items = [];
particles = [];
score = 0;
enemyTimer = 0;
itemTimer = 0;
frame = 0;
gameOver = false;
updateUI();
}
function updateUI() {
scoreEl.textContent = score;
hpEl.textContent = player.hp;
powerEl.textContent = player.power;
}
function addPlayerBullet(x, y, vx, vy, power = 1) {
bullets.push({ x, y, w: 6, h: 16, vx, vy, power });
}
function shoot() {
if (player.power === 1) {
addPlayerBullet(player.x, player.y - 25, 0, -10);
} else if (player.power === 2) {
addPlayerBullet(player.x - 9, player.y - 25, 0, -10);
addPlayerBullet(player.x + 9, player.y - 25, 0, -10);
} else if (player.power === 3) {
addPlayerBullet(player.x, player.y - 28, 0, -11);
addPlayerBullet(player.x - 16, player.y - 20, -1.2, -10);
addPlayerBullet(player.x + 16, player.y - 20, 1.2, -10);
} else {
addPlayerBullet(player.x, player.y - 30, 0, -12, 2);
addPlayerBullet(player.x - 14, player.y - 24, -0.8, -11);
addPlayerBullet(player.x + 14, player.y - 24, 0.8, -11);
addPlayerBullet(player.x - 24, player.y - 10, -1.7, -10);
addPlayerBullet(player.x + 24, player.y - 10, 1.7, -10);
}
player.shotCooldown = Math.max(4, 10 - player.power);
}
function spawnEnemy() {
if (boss) return;
const size = Math.random() * 16 + 28;
enemies.push({
x: Math.random() * (canvas.width - size) + size / 2,
y: -size,
w: size,
h: size,
speed: Math.random() * 1.4 + 1.4,
hp: size > 38 ? 4 : 2,
shotTimer: Math.floor(Math.random() * 50),
type: Math.random() < 0.35 ? "spread" : "normal"
});
}
function spawnPowerItem(x = Math.random() * (canvas.width - 40) + 20, y = -20) {
items.push({
x,
y,
w: 24,
h: 24,
speed: 2.2,
type: "power"
});
}
function enemyShoot(enemy) {
if (enemy.type === "spread") {
for (let i = -2; i <= 2; i++) {
enemyBullets.push({
x: enemy.x,
y: enemy.y + enemy.h / 2,
w: 8,
h: 8,
vx: i * 1.1,
vy: 3.2
});
}
} else {
const dx = player.x - enemy.x;
const dy = player.y - enemy.y;
const len = Math.hypot(dx, dy) || 1;
enemyBullets.push({
x: enemy.x,
y: enemy.y + enemy.h / 2,
w: 8,
h: 8,
vx: dx / len * 3.2,
vy: dy / len * 3.2
});
}
}
function createExplosion(x, y) {
for (let i = 0; i < 16; i++) {
particles.push({
x,
y,
vx: (Math.random() - 0.5) * 7,
vy: (Math.random() - 0.5) * 7,
life: 24,
r: Math.random() * 4 + 2
});
}
}
function isHit(a, b) {
return (
a.x - a.w / 2 < b.x + b.w / 2 &&
a.x + a.w / 2 > b.x - b.w / 2 &&
a.y - a.h / 2 < b.y + b.h / 2 &&
a.y + a.h / 2 > b.y - b.h / 2
);
}
function damagePlayer() {
if (player.invincible > 0) return;
player.hp--;
player.power = Math.max(1, player.power - 1);
player.invincible = 80;
createExplosion(player.x, player.y);
updateUI();
if (player.hp <= 0) gameOver = true;
}
function update() {
if (gameOver) return;
frame++;
// boss spawn
bossTimer++;
if (!boss && bossTimer > 2000) {
boss = {
x: canvas.width / 2,
y: 120,
w: 120,
h: 120,
hp: 200,
maxHp: 200,
shotTimer: 0
};
}
if (keys["arrowleft"] || keys["a"]) player.x -= player.speed;
if (keys["arrowright"] || keys["d"]) player.x += player.speed;
if (keys["arrowup"] || keys["w"]) player.y -= player.speed;
if (keys["arrowdown"] || keys["s"]) player.y += player.speed;
player.x = Math.max(player.w / 2, Math.min(canvas.width - player.w / 2, player.x));
player.y = Math.max(player.h / 2, Math.min(canvas.height - player.h / 2, player.y));
if (player.invincible > 0) player.invincible--;
if (player.shotCooldown > 0) player.shotCooldown--;
if ((keys[" "] || keys["space"]) && player.shotCooldown <= 0) shoot();
bullets.forEach((b) => {
b.x += b.vx;
b.y += b.vy;
});
bullets = bullets.filter((b) => b.y > -30 && b.x > -30 && b.x < canvas.width + 30);
enemyBullets.forEach((b) => {
b.x += b.vx;
b.y += b.vy;
});
enemyBullets = enemyBullets.filter((b) => b.y < canvas.height + 30 && b.x > -30 && b.x < canvas.width + 30);
enemyTimer++;
if (enemyTimer > Math.max(20, 40 - Math.floor(score / 1000))) {
spawnEnemy();
enemyTimer = 0;
}
itemTimer++;
if (itemTimer > 520) {
spawnPowerItem();
itemTimer = 0;
}
enemies.forEach((e) => {
e.y += e.speed;
e.shotTimer++;
if (e.shotTimer > 70) {
enemyShoot(e);
e.shotTimer = 0;
}
});
// boss behavior
if (boss) {
boss.shotTimer++;
if (boss.shotTimer % 40 === 0) {
for (let i = 0; i < 20; i++) {
const angle = (Math.PI * 2 / 20) * i + frame * 0.02;
enemyBullets.push({
x: boss.x,
y: boss.y,
w: 10,
h: 10,
vx: Math.cos(angle) * 3,
vy: Math.sin(angle) * 3
});
}
}
}
items.forEach((item) => {
item.y += item.speed;
item.x += Math.sin((frame + item.y) * 0.04) * 0.8;
});
items = items.filter((item) => item.y < canvas.height + 40);
for (let i = items.length - 1; i >= 0; i--) {
if (isHit(player, items[i])) {
player.power = Math.min(4, player.power + 1);
score += 300;
createExplosion(items[i].x, items[i].y);
items.splice(i, 1);
updateUI();
}
}
for (let i = enemies.length - 1; i >= 0; i--) {
const e = enemies[i];
if (isHit(player, e)) {
createExplosion(e.x, e.y);
enemies.splice(i, 1);
damagePlayer();
continue;
}
if (e.y > canvas.height + 50) {
enemies.splice(i, 1);
damagePlayer();
}
}
for (let i = enemyBullets.length - 1; i >= 0; i--) {
if (isHit(player, enemyBullets[i])) {
enemyBullets.splice(i, 1);
damagePlayer();
}
}
for (let i = enemies.length - 1; i >= 0; i--) {
for (let j = bullets.length - 1; j >= 0; j--) {
if (isHit(enemies[i], bullets[j])) {
enemies[i].hp -= bullets[j].power;
bullets.splice(j, 1);
if (enemies[i].hp <= 0) {
const drop = Math.random() < 0.18;
if (drop) spawnPowerItem(enemies[i].x, enemies[i].y);
createExplosion(enemies[i].x, enemies[i].y);
enemies.splice(i, 1);
score += 100;
updateUI();
}
break;
}
}
}
particles.forEach((p) => {
p.x += p.vx;
p.y += p.vy;
p.life--;
});
particles = particles.filter((p) => p.life > 0);
stars.forEach((s) => {
s.y += s.speed;
if (s.y > canvas.height) {
s.y = 0;
s.x = Math.random() * canvas.width;
}
});
}
function drawPlayer() {
if (player.invincible > 0 && Math.floor(frame / 5) % 2 === 0) return;
ctx.save();
ctx.translate(player.x, player.y);
// body
const grad = ctx.createLinearGradient(0, -20, 0, 30);
grad.addColorStop(0, "#7df9ff");
grad.addColorStop(1, "#0077ff");
ctx.fillStyle = grad;
ctx.beginPath();
ctx.moveTo(0, -26);
ctx.lineTo(20, 22);
ctx.lineTo(0, 12);
ctx.lineTo(-20, 22);
ctx.closePath();
ctx.fill();
// cockpit
ctx.fillStyle = "#ffffff";
ctx.beginPath();
ctx.arc(0, -6, 6, 0, Math.PI * 2);
ctx.fill();
// wings glow
ctx.fillStyle = "#00eaff";
ctx.globalAlpha = 0.5;
ctx.fillRect(-24, 0, 8, 10);
ctx.fillRect(16, 0, 8, 10);
ctx.globalAlpha = 1;
// engine flame
ctx.fillStyle = "#ffcf5a";
ctx.beginPath();
ctx.moveTo(-8, 24);
ctx.lineTo(0, 40 + Math.random() * 6);
ctx.lineTo(8, 24);
ctx.closePath();
ctx.fill();
ctx.restore();
}
function drawEnemy(e) {
ctx.save();
ctx.translate(e.x, e.y);
// core
const grad = ctx.createRadialGradient(0, 0, 4, 0, 0, e.w / 2);
grad.addColorStop(0, "#fff");
grad.addColorStop(1, e.type === "spread" ? "#ff00cc" : "#ff0000");
ctx.fillStyle = grad;
ctx.beginPath();
ctx.arc(0, 0, e.w / 2, 0, Math.PI * 2);
ctx.fill();
// spikes
ctx.strokeStyle = "#fff";
ctx.lineWidth = 2;
for (let i = 0; i < 6; i++) {
const angle = (Math.PI * 2 / 6) * i + frame * 0.01;
ctx.beginPath();
ctx.moveTo(Math.cos(angle) * 6, Math.sin(angle) * 6);
ctx.lineTo(Math.cos(angle) * (e.w / 2 + 8), Math.sin(angle) * (e.w / 2 + 8));
ctx.stroke();
}
ctx.restore();
}
function drawPowerItem(item) {
ctx.save();
ctx.translate(item.x, item.y);
ctx.rotate(frame * 0.05);
ctx.fillStyle = "#68ff7a";
ctx.fillRect(-12, -12, 24, 24);
ctx.fillStyle = "#052";
ctx.font = "bold 18px system-ui";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("P", 0, 1);
ctx.restore();
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "#050816";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "#ffffff";
stars.forEach((s) => {
ctx.globalAlpha = 0.4 + Math.random() * 0.5;
ctx.beginPath();
ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2);
ctx.fill();
});
ctx.globalAlpha = 1;
bullets.forEach((b) => {
ctx.fillStyle = b.power >= 2 ? "#fff36a" : "#8ffcff";
ctx.fillRect(b.x - b.w / 2, b.y - b.h / 2, b.w, b.h);
});
enemyBullets.forEach((b) => {
ctx.fillStyle = "#ff9a3b";
ctx.beginPath();
ctx.arc(b.x, b.y, b.w / 2, 0, Math.PI * 2);
ctx.fill();
});
items.forEach(drawPowerItem);
enemies.forEach(drawEnemy);
// draw boss
if (boss) {
ctx.save();
ctx.translate(boss.x, boss.y);
const grad = ctx.createRadialGradient(0, 0, 10, 0, 0, boss.w / 2);
grad.addColorStop(0, "#fff");
grad.addColorStop(1, "#ff00aa");
ctx.fillStyle = grad;
ctx.beginPath();
ctx.arc(0, 0, boss.w / 2, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
// HP bar
ctx.fillStyle = "#000";
ctx.fillRect(80, 20, 320, 16);
ctx.fillStyle = "#ff0066";
ctx.fillRect(80, 20, 320 * (boss.hp / boss.maxHp), 16);
}
drawPlayer();
particles.forEach((p) => {
ctx.globalAlpha = p.life / 24;
ctx.fillStyle = "#ffd35a";
ctx.beginPath();
ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
ctx.fill();
});
ctx.globalAlpha = 1;
if (gameOver) {
ctx.fillStyle = "#000b";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "white";
ctx.textAlign = "center";
ctx.font = "bold 42px system-ui";
ctx.fillText("GAME OVER", canvas.width / 2, canvas.height / 2 - 30);
ctx.font = "20px system-ui";
ctx.fillText("Score: " + score, canvas.width / 2, canvas.height / 2 + 10);
ctx.fillText("Enterでリスタート", canvas.width / 2, canvas.height / 2 + 48);
}
}
function loop() {
update();
draw();
requestAnimationFrame(loop);
}
updateUI();
loop();
</script>
</body>
</html>