// プレイヤー衝突 if (!state.over && circleHit(en.x, en.y, en.r, player.x, player.y, player.r)) { enemies.splice(i, 1); hitPlayer(); continue; } // 弾との衝突 for (let j = bullets.length - 1; j >= 0; j--) { const b = bullets[j]; if (circleHit(en.x, en.y, en.r, b.x, b.y, b.r)) { bullets.splice(j, 1); en.hp -= b.dmg; boom(b.x, b.y, 0.4); if (en.hp <= 0) { enemies.splice(i, 1); const add = (en.kind === 'elite') ? 250 : 100; state.score += add; uiScore.textContent = String(state.score); boom(en.x, en.y, (en.kind === 'elite') ? 1.8 : 1.1); } break; } } } // パーティクル for (let i = particles.length - 1; i >= 0; i--) { const p = particles[i]; p.t += dt; p.x += p.vx * dt; p.y += p.vy * dt; p.vx *= Math.pow(0.02, dt); p.vy *= Math.pow(0.02, dt); if (p.t >= p.life) particles.splice(i, 1); } // wave進行 if (enemies.length === 0) { state.wave += 1; uiWave.textContent = String(state.wave); spawnWave(state.wave); // wave上昇で少し回復(やりすぎ防止) if (state.wave % 3 === 0 && state.life < 5) { state.life += 1; uiLife.textContent = String(state.life); } } // 画面揺れ if (state.shake > 0) state.shake -= dt * 16; if (state.shake < 0) state.shake = 0; } // ====== 描画(8bit風) ====== function draw() { // 画面揺れ const sx = (Math.random() - 0.5) * state.shake; const sy = (Math.random() - 0.5) * state.shake; ctx.save(); ctx.clearRect(0, 0, W, H); ctx.translate(sx, sy); // 背景グラデ const g = ctx.createLinearGradient(0, 0, 0, H); g.addColorStop(0, '#05070b'); g.addColorStop(1, '#070b14'); ctx.fillStyle = g; ctx.fillRect(0, 0, W, H); // 星 ctx.fillStyle = 'rgba(255,255,255,0.85)'; for (const s of stars) { ctx.globalAlpha = clamp(s.s / 1.8, 0.25, 0.9); ctx.fillRect(Math.round(s.x), Math.round(s.y), Math.max(1, Math.round(s.s)), Math.max(1, Math.round(s.s))); } ctx.globalAlpha = 1; // スクロールライン(雰囲気) ctx.strokeStyle = 'rgba(255,255,255,0.05)'; ctx.lineWidth = 1; for (let y = 0; y < H; y += 40) { const yy = (y + (state.t * 120) % 40); ctx.beginPath(); ctx.moveTo(0, yy); ctx.lineTo(W, yy); ctx.stroke(); } // 弾 ctx.fillStyle = '#d8f7ff'; for (const b of bullets) { ctx.beginPath();