Remember, remember the fifth of November š! The bonfire is lit and the rockets are going up one after another: burst each one at just the right moment, while it glows inside the golden band near the top of the sky. Tap, click or press Space. A burst right in the middle of the band is perfect and scores double. Burst too early, too late, or let a rocket fall back and you lose one of your three lives. The rockets fly faster and the band gets thinner as your score climbs. How bright can you make this Bonfire Night show?
.fw-game {
text-align: center;
}
.fw-game .instructions {
max-width: 500px;
margin: 0 auto 15px;
font-size: 1em;
color: #555;
}
.fw-info {
display: flex;
justify-content: center;
gap: 18px;
margin-bottom: 10px;
font-size: 1.2em;
}
#fwCanvas {
display: block;
margin: 0 auto;
max-width: 100%;
border: 3px solid #f28c28;
border-radius: 10px;
background: #0b1026;
touch-action: manipulation;
cursor: pointer;
}
.fw-message {
margin-top: 15px;
font-weight: bold;
min-height: 1.5em;
}
.fw-btn {
margin-top: 15px;
padding: 10px 20px;
font-size: 1em;
background-color: #e0457b;
color: #fff;
font-weight: bold;
border: none;
border-radius: 5px;
cursor: pointer;
}
.fw-btn:hover {
background-color: #ef5b8f;
}
.fw-btn:disabled {
background-color: #ccc;
color: #666;
cursor: not-allowed;
}
<div class="fw-game">
<h1>š Firework Burst - Bonfire Night Game š„</h1>
<div class="instructions">
Tap, click or press Space to burst each rocket while it is inside the glowing band near the top. Too early, too late or let it fall and you lose a life. Rockets get faster and the band gets thinner!
</div>
<div class="fw-info">
<span>Score: <span id="fwScore">0</span></span>
<span>Lives: <span id="fwLives">ā¤ļøā¤ļøā¤ļø</span></span>
<span>Best: <span id="fwBest">0</span></span>
</div>
<canvas id="fwCanvas" width="320" height="480"></canvas>
<div class="fw-message" id="fwMessage">Press "Start" to light the first rocket...</div>
<button class="fw-btn" id="fwStart">Start</button>
</div>
const fwCanvas = document.getElementById('fwCanvas');
const fwCtx = fwCanvas.getContext('2d');
const fwScoreEl = document.getElementById('fwScore');
const fwLivesEl = document.getElementById('fwLives');
const fwBestEl = document.getElementById('fwBest');
const fwMessageEl = document.getElementById('fwMessage');
const fwStartBtn = document.getElementById('fwStart');
const FW_W = fwCanvas.width;
const FW_H = fwCanvas.height;
const FW_GRAVITY = 260; // px/s² pulling the rocket back down
const FW_START_LIVES = 3;
const FW_COLORS = ['#ff4d6d', '#ffd166', '#06d6a0', '#4cc9f0', '#f72585', '#ff9f1c'];
let fwRocket = null; // {x, y, vy, color}
let fwSparks = []; // burst particles: {x, y, vx, vy, life, color}
let fwScore = 0;
let fwLives = FW_START_LIVES;
let fwRunning = false;
let fwLastTime = 0;
let fwFrame = null;
let fwNextLaunchIn = 0;
let fwFlash = ''; // short text shown after each burst
let fwFlashLife = 0;
function fwReadBest() {
try {
return parseInt(localStorage.getItem('fireworkBurstBest') || '0', 10) || 0;
} catch (e) {
return 0;
}
}
function fwSaveBest(value) {
try {
localStorage.setItem('fireworkBurstBest', String(value));
} catch (e) {
// storage blocked: the best score only lasts for this visit
}
}
let fwBest = fwReadBest();
fwBestEl.textContent = fwBest;
function fwSetShare(visible) {
// The share buttons come after the game in the page, so look them up each time.
const share = document.querySelector('.share-buttons');
if (share) {
share.style.display = visible ? 'flex' : 'none';
}
}
function fwSetHidden(value) {
const hidden = document.getElementById('hiddenScore');
if (hidden) {
hidden.value = value;
}
}
function fwRenderLives() {
let hearts = '';
for (let i = 0; i < fwLives; i++) hearts += 'ā¤ļø';
fwLivesEl.textContent = hearts || 'ā';
}
// The glowing band around the top: it gets thinner as the score grows.
function fwBand() {
const height = Math.max(34, 90 - fwScore * 3);
const top = 70;
return {top: top, bottom: top + height};
}
function fwLaunch() {
// Faster rockets as the score grows; the peak always lands inside the band.
const band = fwBand();
const peakY = band.top + Math.random() * (band.bottom - band.top);
const startY = FW_H - 20;
const vy = -Math.sqrt(2 * FW_GRAVITY * (startY - peakY)) * (1 + Math.min(fwScore, 30) * 0.012);
const gravityScale = (vy * vy) / (2 * (startY - peakY)) / FW_GRAVITY;
fwRocket = {
x: 40 + Math.random() * (FW_W - 80),
y: startY,
vy: vy,
g: FW_GRAVITY * gravityScale,
color: FW_COLORS[Math.floor(Math.random() * FW_COLORS.length)],
};
}
function fwBurst(x, y, color, count) {
for (let i = 0; i < count; i++) {
const angle = (i / count) * Math.PI * 2;
const speed = 60 + Math.random() * 110;
fwSparks.push({x: x, y: y, vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed, life: 1, color: color});
}
}
function fwShowFlash(text) {
fwFlash = text;
fwFlashLife = 0.9;
}
function fwLoseLife(text) {
fwLives--;
fwRenderLives();
fwShowFlash(text);
if (fwLives <= 0) {
fwGameOver();
}
}
function fwTap() {
if (!fwRunning || !fwRocket) return;
const band = fwBand();
const rocket = fwRocket;
fwRocket = null;
fwNextLaunchIn = 0.45;
if (rocket.y >= band.top && rocket.y <= band.bottom) {
// Closer to the band's middle scores more.
const middle = (band.top + band.bottom) / 2;
const perfect = Math.abs(rocket.y - middle) < (band.bottom - band.top) / 4;
fwScore += perfect ? 2 : 1;
fwScoreEl.textContent = fwScore;
fwSetHidden(fwScore);
fwBurst(rocket.x, rocket.y, rocket.color, perfect ? 36 : 22);
fwShowFlash(perfect ? 'Perfect! +2' : 'Nice! +1');
} else {
fwBurst(rocket.x, rocket.y, '#777777', 8);
fwLoseLife(rocket.y > band.bottom ? 'Too early!' : 'Too late!');
}
}
function fwUpdate(dt) {
if (fwRocket) {
fwRocket.vy += fwRocket.g * dt;
fwRocket.y += fwRocket.vy * dt;
// Fell back below the band without being burst.
if (fwRocket.vy > 0 && fwRocket.y > fwBand().bottom + 40) {
fwBurst(fwRocket.x, fwRocket.y, '#777777', 6);
fwRocket = null;
fwNextLaunchIn = 0.6;
fwLoseLife('Missed it!');
}
} else if (fwRunning) {
fwNextLaunchIn -= dt;
if (fwNextLaunchIn <= 0) fwLaunch();
}
fwSparks.forEach(function (s) {
s.vy += 120 * dt;
s.x += s.vx * dt;
s.y += s.vy * dt;
s.life -= dt * 0.9;
});
fwSparks = fwSparks.filter(function (s) { return s.life > 0; });
if (fwFlashLife > 0) fwFlashLife -= dt;
}
function fwDraw() {
fwCtx.fillStyle = '#0b1026';
fwCtx.fillRect(0, 0, FW_W, FW_H);
// Stars
fwCtx.fillStyle = 'rgba(255,255,255,0.6)';
for (let i = 0; i < 30; i++) {
fwCtx.fillRect((i * 97) % FW_W, (i * 53) % (FW_H - 120), 2, 2);
}
// Target band
const band = fwBand();
fwCtx.fillStyle = 'rgba(255, 190, 60, 0.3)';
fwCtx.fillRect(0, band.top, FW_W, band.bottom - band.top);
fwCtx.strokeStyle = 'rgba(255, 200, 80, 0.95)';
fwCtx.setLineDash([6, 6]);
fwCtx.beginPath();
fwCtx.moveTo(0, band.top);
fwCtx.lineTo(FW_W, band.top);
fwCtx.moveTo(0, band.bottom);
fwCtx.lineTo(FW_W, band.bottom);
fwCtx.stroke();
fwCtx.setLineDash([]);
// Bonfire
fwCtx.fillStyle = '#5a3a22';
fwCtx.fillRect(FW_W / 2 - 40, FW_H - 18, 80, 10);
fwCtx.fillStyle = '#ff9f1c';
fwCtx.beginPath();
fwCtx.moveTo(FW_W / 2 - 30, FW_H - 18);
fwCtx.quadraticCurveTo(FW_W / 2, FW_H - 70, FW_W / 2 + 30, FW_H - 18);
fwCtx.fill();
// Rocket
if (fwRocket) {
fwCtx.fillStyle = fwRocket.color;
fwCtx.fillRect(fwRocket.x - 4, fwRocket.y - 14, 8, 18);
fwCtx.beginPath();
fwCtx.moveTo(fwRocket.x - 6, fwRocket.y - 14);
fwCtx.lineTo(fwRocket.x, fwRocket.y - 24);
fwCtx.lineTo(fwRocket.x + 6, fwRocket.y - 14);
fwCtx.fill();
fwCtx.fillStyle = 'rgba(255, 200, 80, 0.8)';
fwCtx.fillRect(fwRocket.x - 2, fwRocket.y + 4, 4, 10 + Math.random() * 8);
}
// Sparks
fwSparks.forEach(function (s) {
fwCtx.globalAlpha = Math.max(0, s.life);
fwCtx.fillStyle = s.color;
fwCtx.fillRect(s.x - 2, s.y - 2, 4, 4);
});
fwCtx.globalAlpha = 1;
if (fwFlashLife > 0) {
fwCtx.globalAlpha = Math.min(1, fwFlashLife * 2);
fwCtx.fillStyle = '#ffffff';
fwCtx.font = 'bold 26px sans-serif';
fwCtx.textAlign = 'center';
fwCtx.fillText(fwFlash, FW_W / 2, FW_H / 2);
fwCtx.globalAlpha = 1;
}
}
function fwLoop(now) {
const dt = Math.min(0.05, (now - fwLastTime) / 1000);
fwLastTime = now;
fwUpdate(dt);
fwDraw();
if (fwRunning || fwSparks.length || fwFlashLife > 0) {
fwFrame = requestAnimationFrame(fwLoop);
}
}
function fwGameOver() {
fwRunning = false;
fwRocket = null;
if (fwScore > fwBest) {
fwBest = fwScore;
fwSaveBest(fwBest);
fwBestEl.textContent = fwBest;
fwMessageEl.textContent = 'š Show over! New best: ' + fwScore + ' points š';
} else {
fwMessageEl.textContent = 'š Show over! You scored ' + fwScore + ' points.';
}
fwStartBtn.disabled = false;
fwStartBtn.textContent = 'Play again';
fwSetShare(true);
}
function fwStart() {
cancelAnimationFrame(fwFrame);
fwScore = 0;
fwLives = FW_START_LIVES;
fwSparks = [];
fwRocket = null;
fwNextLaunchIn = 0.6;
fwScoreEl.textContent = '0';
fwRenderLives();
fwSetHidden(0);
fwSetShare(false);
fwStartBtn.disabled = true;
fwMessageEl.textContent = 'Burst each rocket inside the golden band!';
fwRunning = true;
fwLastTime = performance.now();
fwFrame = requestAnimationFrame(fwLoop);
}
fwCanvas.addEventListener('touchstart', function (e) {
e.preventDefault();
fwTap();
}, {passive: false});
fwCanvas.addEventListener('mousedown', function (e) {
e.preventDefault();
fwTap();
});
document.addEventListener('keydown', function (e) {
if (e.code === 'Space' && fwRunning) {
e.preventDefault();
fwTap();
}
});
fwStartBtn.addEventListener('click', fwStart);
fwRenderLives();
fwDraw();