Update fbird.html

added highscore and addjusted physics to make it same speed independent of FPS of device
This commit is contained in:
F. Javier "habiwan" Puig Diaz authored and GitHub committed 2026-06-28 00:02:10 +01:00
1 parent b7086b5f7b
commit 37bccfc53d
1 file changed
+367 -124
+367 -124
View File
@@ -2,69 +2,85 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content=" <meta name="viewport" content="
width=device-width, width=device-width,
initial-scale=1.0, initial-scale=1.0,
maximum-scale=1.0, maximum-scale=1.0,
user-scalable=no, user-scalable=no,
viewport-fit=cover"> viewport-fit=cover">
<link rel="shortcut icon" <link rel="shortcut icon"
href=" href="https://upload.wikimedia.org/wikipedia/commons/thumb/c/cf/UZ_road_sign_1.25.2.svg/960px-UZ_road_sign_1.25.2.svg.png"
https://upload.wikimedia.org/wikipedia/commons/thumb/c/cf/UZ_road_sign_1.25.2.svg/960px-UZ_road_sign_1.25.2.svg.png" type="image/svg+xml">
type="image/svg+xml">
<title>Fly through the bronXs</title> <title>famous bird</title>
<style> <style>
/* ==========================================
BASE LAYOUT & TYPOGRAPHY
========================================== */
body { body {
margin: 0; margin: 0;
padding: 0; padding: 0;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
min-height: 100dvh; /* 'dvh' respects iOS Safari's shrinking address bars */ min-height: 100dvh;
/* Background gradient outside the game container */
background: linear-gradient(to bottom, #64b3f4, #c2e59c); background: linear-gradient(to bottom, #64b3f4, #c2e59c);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
overflow: hidden; overflow: hidden;
/* CSS ANTI-ZOOM: Forcibly kills double-tap zoom and hold-to-highlight on mobile */ /* Prevents accidental text selection or zooming
while frantically tapping the screen */
touch-action: none; touch-action: none;
-webkit-touch-callout: none; -webkit-touch-callout: none;
-webkit-user-select: none; -webkit-user-select: none;
user-select: none; user-select: none;
} }
/* ==========================================
GAME CONTAINER & CANVAS
========================================== */
.game-container { .game-container {
position: relative; position: relative;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.6); box-shadow: 0 10px 40px rgba(0, 0, 0, 0.6);
overflow: hidden; overflow: hidden;
/* Force a strict 9:16 stretch to 90% of the absolute edges */ /* Maintains a standard portrait aspect ratio */
aspect-ratio: 360 / 640; aspect-ratio: 360 / 640;
width: 90vw; width: 90vw;
height: 90dvh; height: 90dvh;
max-width: 90%; max-width: 90%;
max-height: 90%; max-height: 90%;
border-radius: 16px; /* Give it a sleek, rounded "Arcade Cabinet" look */ border-radius: 16px;
/* Magic trigger: Tells text inside to scale relative to this box's width */
container-type: inline-size; container-type: inline-size;
} }
/* DESKTOP / TABLET CAP: If the screen is wider than a phone, stop growing! */
/* Adjusts sizing for desktop monitors */
@media (min-width: 600px) { @media (min-width: 600px) {
.game-container { .game-container {
height: 95dvh; /* Leave a nice breathing margin top and bottom */ height: 95dvh;
max-height: 95%; /* Hard ceiling: Never grow taller than 95% */ max-height: 95%;
width: auto; /* Let the 9:16 aspect ratio figure out the width */ width: auto;
border-radius: 16px; /* Give it a sleek, rounded "Arcade Cabinet" look */ border-radius: 16px;
} }
} }
canvas { canvas {
display: block; display: block;
width: 100%; /* Stretches canvas to match the responsive container */ width: 100%;
height: 100%; height: 100%;
background: #70c5ce; background: #70c5ce;
} }
/* ==========================================
USER INTERFACE (UI) OVERLAY
========================================== */
/* Acts as a transparent layer sitting on top of the
HTML5 canvas to hold our HTML text and buttons */
.ui-overlay { .ui-overlay {
position: absolute; position: absolute;
top: 0; top: 0;
@@ -77,12 +93,15 @@
align-items: center; align-items: center;
color: white; color: white;
text-align: center; text-align: center;
/* Allows clicks to pass through the overlay
and hit the canvas behind it */
pointer-events: none; pointer-events: none;
box-sizing: border-box; box-sizing: border-box;
padding: 4cqw; /* cqw = "Container Query Width" */ padding: 4cqw;
} }
/* Forces the pop-up cards into dead-center alignment */ /* Groups the different UI screens */
#startScreen, #gameOverScreen, #gameUI { #startScreen, #gameOverScreen, #gameUI {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -91,7 +110,6 @@
width: 100%; width: 100%;
} }
/* cqw = "Container Query Width". e.g. 10cqw means '10% of the game canvas width' */
.game-title { .game-title {
font-size: 13cqw; font-size: 13cqw;
font-weight: 800; font-weight: 800;
@@ -104,7 +122,7 @@
font-size: 16cqw; font-size: 16cqw;
font-weight: bold; font-weight: bold;
text-shadow: 0 0.6cqw 2cqw rgba(0, 0, 0, 0.5); text-shadow: 0 0.6cqw 2cqw rgba(0, 0, 0, 0.5);
margin-top: 3cqw; margin-top: 2cqw;
} }
.message { .message {
@@ -114,7 +132,7 @@
font-size: 6cqw; font-size: 6cqw;
width: 85%; width: 85%;
box-sizing: border-box; box-sizing: border-box;
margin-bottom: 6cqw; margin-bottom: 4cqw;
box-shadow: 0 1cqw 3cqw rgba(0, 0, 0, 0.3); box-shadow: 0 1cqw 3cqw rgba(0, 0, 0, 0.3);
border: 2px solid rgba(255,255,255,0.1); border: 2px solid rgba(255,255,255,0.1);
} }
@@ -124,8 +142,8 @@
padding: 3.5cqw 5cqw; padding: 3.5cqw 5cqw;
border-radius: 2cqw; border-radius: 2cqw;
font-size: 4.5cqw; font-size: 4.5cqw;
margin-top: 5cqw; margin-top: 3cqw;
width: 80%; width: 85%;
box-sizing: border-box; box-sizing: border-box;
} }
@@ -140,55 +158,110 @@
color: #2c3e50; color: #2c3e50;
box-shadow: 0 1.5cqw 4cqw rgba(0, 0, 0, 0.4); box-shadow: 0 1.5cqw 4cqw rgba(0, 0, 0, 0.4);
transition: transform 0.1s; transition: transform 0.1s;
/* Re-enables clicking specifically for buttons */
pointer-events: auto; pointer-events: auto;
margin-top: 2cqw; margin-top: 2cqw;
} }
/* Adds a slight physical "press" effect when clicked */
button:active { button:active {
transform: scale(0.92); transform: scale(0.92);
} }
/* ==========================================
LEADERBOARD & HIGHSCORE CSS
========================================== */
input[type="text"] {
background: rgba(255, 255, 255, 0.95);
border: 2px solid #2c3e50;
padding: 2.5cqw 4cqw;
border-radius: 2cqw;
font-size: 5cqw;
font-family: inherit;
font-weight: bold;
color: #2c3e50;
text-align: center;
width: 60%;
margin-bottom: 3cqw;
text-transform: uppercase;
outline: none;
/* Re-enables clicking and typing for the input box */
pointer-events: auto;
touch-action: manipulation;
}
input[type="text"]:focus {
border-color: #f1c40f;
box-shadow: 0 0 10px rgba(241, 196, 15, 0.5);
}
.leaderboard-row {
display: flex;
justify-content: space-between;
margin-bottom: 1.5cqw;
border-bottom: 1px solid rgba(255,255,255,0.2);
padding-bottom: 1cqw;
}
/* Utility class to easily hide UI elements */
.hidden { .hidden {
display: none !important; display: none !important;
} }
</style> </style>
</head> </head>
<body> <body>
<div class="game-container"> <div class="game-container">
<canvas id="gameCanvas" width="360" height="640"></canvas> <canvas id="gameCanvas" width="360" height="640"></canvas>
<div class="ui-overlay"> <div class="ui-overlay">
<div id="startScreen"> <div id="startScreen">
<h1 class="game-title"> <h1 class="game-title">famous bird</h1>
Fly through<br>
the bronXs
</h1>
<div class="message"> <div class="message">
Tap the screen or<br> Tap the screen or<br>
click the mouse or<br> click the mouse or<br>
hit spacebar to<br> hit spacebar to<br>
flap your wings "flap your wings"
</div>
<button id="startButton">
START GAME
</button>
<div class="instructions">
Avoid the pipes and<br>
don't hit the ground!
</div> </div>
<button id="startButton">START GAME</button>
</div> </div>
<div id="gameOverScreen" class="hidden"> <div id="gameOverScreen" class="hidden">
<div class="message" style="font-size: 8cqw; font-weight:bold; color: #ff4757;"> <div class="message"
style="font-size: 8cqw; font-weight:bold; color: #ff4757; margin-bottom: 2cqw;">
GAME OVER GAME OVER
</div> </div>
<div class="score-display"> <div class="score-display"
style="margin-top: 0; margin-bottom: 2cqw;">
Score: <span id="finalScore">0</span> Score: <span id="finalScore">0</span>
</div> </div>
<button id="restartButton">
TRY AGAIN <div id="highscoreEntry" class="hidden"
</button> style="display: flex; flex-direction: column; align-items: center; width: 100%;">
<div style="font-size: 5cqw; margin-bottom: 2cqw; color: #f1c40f; font-weight: bold; text-shadow: 0 2px 4px rgba(0,0,0,0.5);">
NEW HIGH SCORE!
</div>
<input type="text" id="playerName" placeholder="YOUR NAME" maxlength="13">
<button id="saveScoreButton" style="font-size: 5cqw; padding: 2.5cqw 8cqw;">
SAVE SCORE
</button>
</div>
<div id="leaderboard" class="instructions"
style="margin-top: 2cqw; font-size: 4.5cqw;">
<div style="font-weight: 900; margin-bottom: 3cqw; text-align: center; color: #f1c40f;">
TOP 5 SCORES
</div>
<div id="leaderboardList" style="text-align: left;">
</div>
</div>
<button id="restartButton" style="margin-top: 3cqw;">TRY AGAIN</button>
</div> </div>
<div id="gameUI" class="hidden"> <div id="gameUI" class="hidden">
<div class="score-display" id="score">0</div> <div class="score-display" id="score">0</div>
</div> </div>
@@ -196,7 +269,11 @@
</div> </div>
<script> <script>
// Game variables // ==========================================
// 1. DOM ELEMENT BINDINGS
// ==========================================
// Grabbing references to HTML elements so we can
// manipulate them via JavaScript later.
const canvas = document.getElementById('gameCanvas'); const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
const startScreen = document.getElementById('startScreen'); const startScreen = document.getElementById('startScreen');
@@ -207,34 +284,59 @@
const startButton = document.getElementById('startButton'); const startButton = document.getElementById('startButton');
const restartButton = document.getElementById('restartButton'); const restartButton = document.getElementById('restartButton');
// Game state // ==========================================
let gameState = 'start'; // 'start', 'playing', 'gameover' // 2. GLOBAL GAME STATE & TRACKERS
// ==========================================
// Tracks the current menu/game phase.
let gameState = 'start';
// Current session score.
let score = 0; let score = 0;
let frames = 0; // Array to hold {name, score} objects for the leaderboard.
let highscores = [];
// Time and Speed setup for smooth animations.
let lastTime = 0;
let pipeSpawnTimer = 0;
// Target pixel distance between pipes. Ensures that when
// the game speeds up, the horizontal gap stays identical.
const PIPE_DISTANCE = 220;
// Starting speed in pixels per second.
const BASE_SPEED = 120;
// How much faster to go every 10 points.
const SPEED_INCREMENT = 15;
// ==========================================
// 3. GAME ENTITIES (BIRD & PIPES)
// ==========================================
// Bird properties
const bird = { const bird = {
// Position and dimensions
x: 50, x: 50,
y: canvas.height / 2 - 10, y: canvas.height / 2 - 10,
width: 34, width: 34,
height: 24, height: 24,
gravity: 0.5,
// Physics variables tuned by the user
gravity: 20,
velocity: 0, velocity: 0,
jump: -10, jump: -500,
// Renders the bird using basic Canvas shapes
draw: function() { draw: function() {
// Body (Yellow Circle)
ctx.fillStyle = '#FFD700'; ctx.fillStyle = '#FFD700';
ctx.beginPath(); ctx.beginPath();
ctx.arc(this.x, this.y, 15, 0, Math.PI * 2); ctx.arc(this.x, this.y, 15, 0, Math.PI * 2);
ctx.fill(); ctx.fill();
// Draw eye // Eye (Black Dot)
ctx.fillStyle = 'black'; ctx.fillStyle = 'black';
ctx.beginPath(); ctx.beginPath();
ctx.arc(this.x + 8, this.y - 3, 4, 0, Math.PI * 2); ctx.arc(this.x + 8, this.y - 3, 4, 0, Math.PI * 2);
ctx.fill(); ctx.fill();
// Draw beak // Beak (Orange Triangle)
ctx.fillStyle = '#FF8C00'; ctx.fillStyle = '#FF8C00';
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(this.x + 15, this.y); ctx.moveTo(this.x + 15, this.y);
@@ -242,100 +344,128 @@
ctx.lineTo(this.x + 15, this.y + 8); ctx.lineTo(this.x + 15, this.y + 8);
ctx.fill(); ctx.fill();
// Draw wing // Wing (Darker Orange Ellipse)
ctx.fillStyle = '#FFA500'; ctx.fillStyle = '#FFA500';
ctx.beginPath(); ctx.beginPath();
ctx.ellipse(this.x - 5, this.y + 5, 10, 7, 0, 0, Math.PI * 2); ctx.ellipse(this.x - 5, this.y + 5, 10, 7, 0, 0, Math.PI * 2);
ctx.fill(); ctx.fill();
}, },
update: function() { // Applies physics to the bird's vertical position
update: function(dt) {
if (gameState === 'playing') { if (gameState === 'playing') {
this.velocity += this.gravity; // Apply gravity over time
this.y += this.velocity; this.velocity += this.gravity * dt * 60;
// Move the bird by the current velocity
this.y += this.velocity * dt;
// Floor collision // Floor collision check
if (this.y + this.height/2 >= canvas.height - groundHeight) { if (this.y + this.height/2 >= canvas.height - groundHeight) {
this.y = canvas.height - groundHeight - this.height/2; this.y = canvas.height - groundHeight - this.height/2;
if (gameState === 'playing') { if (gameState === 'playing') gameOver();
gameOver();
}
} }
// Ceiling collision // Ceiling collision check
if (this.y - this.height/2 <= 0) { if (this.y - this.height/2 <= 0) {
this.y = this.height/2; this.y = this.height/2;
// Hitting the ceiling stops upward momentum
this.velocity = 0; this.velocity = 0;
} }
} }
}, },
// Triggers a jump by applying negative velocity
flap: function() { flap: function() {
this.velocity = this.jump; this.velocity = this.jump;
}, },
// Resets bird position for a new game
reset: function() { reset: function() {
this.y = canvas.height / 2 - 10; this.y = canvas.height / 2 - 10;
this.velocity = 0; this.velocity = 0;
} }
}; };
// Pipes
const pipes = { const pipes = {
// Array to store active pipes currently on screen
position: [], position: [],
// Vertical gap between the top and bottom pipe
gap: 180, gap: 180,
// Defines how high the top pipe can randomly spawn
maxYPos: -150, maxYPos: -150,
dx: 2, // Current horizontal speed (dx)
dx: BASE_SPEED,
// Renders all active pipes in the array
draw: function() { draw: function() {
for (let i = 0; i < this.position.length; i++) { for (let i = 0; i < this.position.length; i++) {
let p = this.position[i]; let p = this.position[i];
// Top pipe // Draw Top Pipe
ctx.fillStyle = '#2ECC71'; ctx.fillStyle = '#2ECC71';
ctx.fillRect(p.x, p.y, p.width, p.height); ctx.fillRect(p.x, p.y, p.width, p.height);
// Pipe cap // Draw Top Pipe Cap
ctx.fillStyle = '#27AE60'; ctx.fillStyle = '#27AE60';
ctx.fillRect(p.x - 3, p.y + p.height - 20, p.width + 6, 20); ctx.fillRect(p.x - 3, p.y + p.height - 20, p.width + 6, 20);
// Bottom pipe // Draw Bottom Pipe
ctx.fillStyle = '#2ECC71'; ctx.fillStyle = '#2ECC71';
ctx.fillRect(p.x, p.y + p.height + this.gap, p.width, canvas.height); ctx.fillRect(p.x, p.y + p.height + this.gap, p.width, canvas.height);
// Pipe cap // Draw Bottom Pipe Cap
ctx.fillStyle = '#27AE60'; ctx.fillStyle = '#27AE60';
ctx.fillRect(p.x - 3, p.y + p.height + this.gap, p.width + 6, 20); ctx.fillRect(p.x - 3, p.y + p.height + this.gap, p.width + 6, 20);
} }
}, },
update: function() { // Handles spawning, moving, scoring, and pipe collision
update: function(dt) {
if (gameState !== 'playing') return; if (gameState !== 'playing') return;
if (frames % 100 === 0) { // Advance the timer by delta time
pipeSpawnTimer += dt;
// Calculate time required to travel the target distance
// at the current speed, keeping gaps consistent.
let currentSpawnInterval = PIPE_DISTANCE / this.dx;
// Spawn a new pipe if enough time has passed
if (pipeSpawnTimer >= currentSpawnInterval) {
this.position.push({ this.position.push({
x: canvas.width, x: canvas.width,
y: this.maxYPos * (Math.random() + 1), y: this.maxYPos * (Math.random() + 1),
width: 60, width: 60,
height: 300 height: 300
}); });
// Reset timer after spawning
pipeSpawnTimer = 0;
} }
// Update positions and check collisions
for (let i = 0; i < this.position.length; i++) { for (let i = 0; i < this.position.length; i++) {
let p = this.position[i]; let p = this.position[i];
// Move pipe to the left // Move pipe to the left
p.x -= this.dx; p.x -= this.dx * dt;
// If pipe moves off screen, remove it and add score // If pipe moves entirely off-screen to the left:
// Remove it, add a point, and check for speed up.
if (p.x + p.width <= 0) { if (p.x + p.width <= 0) {
this.position.shift(); this.position.splice(i, 1);
score++; score++;
scoreDisplay.innerHTML = score; scoreDisplay.innerHTML = score;
// Increase difficulty speed every 10 points
if (score > 0 && score % 10 === 0) {
this.dx += SPEED_INCREMENT;
}
// Move to the next pipe in the loop
continue;
} }
// Collision detection // COLLISION LOGIC
// Top pipe // Check collision with the TOP pipe
if ( if (
bird.x + bird.width/2 > p.x && bird.x + bird.width/2 > p.x &&
bird.x - bird.width/2 < p.x + p.width && bird.x - bird.width/2 < p.x + p.width &&
@@ -343,8 +473,7 @@
) { ) {
gameOver(); gameOver();
} }
// Check collision with the BOTTOM pipe
// Bottom pipe
if ( if (
bird.x + bird.width/2 > p.x && bird.x + bird.width/2 > p.x &&
bird.x - bird.width/2 < p.x + p.width && bird.x - bird.width/2 < p.x + p.width &&
@@ -355,46 +484,56 @@
} }
}, },
// Clears pipes and resets speed for a new game
reset: function() { reset: function() {
this.position = []; this.position = [];
this.dx = BASE_SPEED;
pipeSpawnTimer = 0;
} }
}; };
// Ground // ==========================================
// 4. ENVIRONMENT RENDERING
// ==========================================
const groundHeight = 80; const groundHeight = 80;
function drawGround() { function drawGround() {
// Main ground color
ctx.fillStyle = '#DAA520'; ctx.fillStyle = '#DAA520';
ctx.fillRect(0, canvas.height - groundHeight, canvas.width, groundHeight); ctx.fillRect(0, canvas.height - groundHeight, canvas.width, groundHeight);
// Draw grass // Green grass line on top of the ground
ctx.fillStyle = '#2ECC71'; ctx.fillStyle = '#2ECC71';
ctx.fillRect(0, canvas.height - groundHeight, canvas.width, 20); ctx.fillRect(0, canvas.height - groundHeight, canvas.width, 20);
// Draw some details // Small brown decorative dashes across the ground
ctx.fillStyle = '#8B4513'; ctx.fillStyle = '#8B4513';
for (let i = 0; i < canvas.width; i += 30) { for (let i = 0; i < canvas.width; i += 30) {
ctx.fillRect(i, canvas.height - groundHeight + 20, 15, 10); ctx.fillRect(i, canvas.height - groundHeight + 20, 15, 10);
} }
} }
// Background elements
function drawBackground() { function drawBackground() {
// Create a sky gradient from top to bottom
// Sky gradient
const skyGradient = ctx.createLinearGradient(0, 0, 0, canvas.height); const skyGradient = ctx.createLinearGradient(0, 0, 0, canvas.height);
skyGradient.addColorStop(0, '#70c5ce'); skyGradient.addColorStop(0, '#70c5ce');
skyGradient.addColorStop(1, '#e0f7fa'); skyGradient.addColorStop(1, '#e0f7fa');
// Fill background with gradient
ctx.fillStyle = skyGradient; ctx.fillStyle = skyGradient;
ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.fillRect(0, 0, canvas.width, canvas.height);
// Clouds // Draw decorative clouds using overlapping arcs
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)'; ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
// Cloud 1
ctx.beginPath(); ctx.beginPath();
ctx.arc(100, 80, 30, 0, Math.PI * 2); ctx.arc(100, 80, 30, 0, Math.PI * 2);
ctx.arc(130, 70, 35, 0, Math.PI * 2); ctx.arc(130, 70, 35, 0, Math.PI * 2);
ctx.arc(160, 80, 25, 0, Math.PI * 2); ctx.arc(160, 80, 25, 0, Math.PI * 2);
ctx.fill(); ctx.fill();
// Cloud 2
ctx.beginPath(); ctx.beginPath();
ctx.arc(300, 120, 30, 0, Math.PI * 2); ctx.arc(300, 120, 30, 0, Math.PI * 2);
ctx.arc(330, 110, 35, 0, Math.PI * 2); ctx.arc(330, 110, 35, 0, Math.PI * 2);
@@ -402,30 +541,115 @@
ctx.fill(); ctx.fill();
} }
// Game functions // ==========================================
// 5. HIGHSCORE & LEADERBOARD LOGIC
// ==========================================
// Iterates through the highscores array and updates the DOM
function renderLeaderboard() {
const list = document.getElementById('leaderboardList');
// Display empty state if no scores exist
if (highscores.length === 0) {
list.innerHTML = "<div style='text-align:center;'>No scores yet!</div>";
return;
}
// Map the array into HTML strings for display
list.innerHTML = highscores.map((entry, i) =>
`<div class="leaderboard-row">
<span>${i + 1}. ${entry.name}</span>
<span style="color: #f1c40f; font-weight: bold;">${entry.score}</span>
</div>`
).join('');
}
// Handles saving a new highscore when the user clicks submit
document.getElementById('saveScoreButton').addEventListener('click', () => {
const nameInput = document.getElementById('playerName');
// Get value, trim spaces, make uppercase.
// Fallback to 'ANONYMOUS' if left empty.
const name = nameInput.value.trim().toUpperCase() || 'ANONYMOUS';
// Push the new score object into the array
highscores.push({ name, score });
// Sort the array in descending order based on score
highscores.sort((a, b) => b.score - a.score);
// Enforce a maximum of 5 entries in the leaderboard
if (highscores.length > 5) {
highscores.pop();
}
// Hide the input form and show the try again button
document.getElementById('highscoreEntry').classList.add('hidden');
document.getElementById('restartButton').classList.remove('hidden');
// Clear the input box for the next session
nameInput.value = '';
// Re-render the visual list
renderLeaderboard();
});
// ==========================================
// 6. GAME FLOW CONTROLS
// ==========================================
// Triggers the end sequence when the bird crashes
function gameOver() { function gameOver() {
gameState = 'gameover'; gameState = 'gameover';
finalScoreDisplay.innerHTML = score; finalScoreDisplay.innerHTML = score;
// Check if the current score qualifies for the top 5
const isHighScore = score > 0 &&
(highscores.length < 5 || score > highscores[highscores.length - 1].score);
if (isHighScore) {
// Show input block, force user to save before restarting
document.getElementById('highscoreEntry').classList.remove('hidden');
document.getElementById('restartButton').classList.add('hidden');
} else {
// Skip input block, just show standard restart button
document.getElementById('highscoreEntry').classList.add('hidden');
document.getElementById('restartButton').classList.remove('hidden');
}
// Ensure leaderboard is updated visually
renderLeaderboard();
// Switch UI screens
gameOverScreen.classList.remove('hidden'); gameOverScreen.classList.remove('hidden');
gameUI.classList.add('hidden'); gameUI.classList.add('hidden');
} }
// Resets variables to start a fresh run
function resetGame() { function resetGame() {
score = 0; frames = 0; score = 0;
bird.reset(); bird.reset();
pipes.reset(); pipes.reset();
scoreDisplay.innerHTML = score; scoreDisplay.innerHTML = score;
} }
// Begins the active game loop
function startGame() { function startGame() {
gameState = 'playing'; gameState = 'playing';
// Hide menus, show in-game overlay
startScreen.classList.add('hidden'); startScreen.classList.add('hidden');
gameOverScreen.classList.add('hidden'); gameOverScreen.classList.add('hidden');
gameUI.classList.remove('hidden'); gameUI.classList.remove('hidden');
resetGame(); resetGame();
} }
// Draw everything // ==========================================
// 7. CORE RENDER & UPDATE LOOP
// ==========================================
// Master draw function that executes in visual order (back to front)
function draw() { function draw() {
drawBackground(); drawBackground();
pipes.draw(); pipes.draw();
@@ -433,66 +657,85 @@
bird.draw(); bird.draw();
} }
// Update game state // Master update function that processes physics
function update() { function update(dt) {
bird.update(); bird.update(dt);
pipes.update(); pipes.update(dt);
} }
// Game loop // The main game engine loop triggered by requestAnimationFrame
function loop() { function loop(timestamp) {
update(); // Calculate delta time (dt) in seconds.
draw(); // This ensures smooth movement regardless of framerate.
frames++; let dt = (timestamp - lastTime) / 1000;
lastTime = timestamp;
// Cap delta time to prevent physics glitches if the tab is backgrounded
if (dt > 0.1) dt = 0.1;
update(dt);
draw();
// Recursively call the loop for the next frame
requestAnimationFrame(loop); requestAnimationFrame(loop);
} }
// Event listeners // ==========================================
// 8. INPUT LISTENERS
// ==========================================
// Wrapper function to process bird flapping
function flapBird() { function flapBird() {
if (gameState === 'playing') { if (gameState === 'playing') {
bird.flap(); bird.flap();
} }
} }
/* ============================================================== // Touchscreen input processing
THE JAVASCRIPT BRUTE-FORCE ANTI-ZOOM & INSTANT TOUCH LISTENERS
============================================================== */
// Listen to raw touchstarts instead of 'click'.
// This removes mobile Safari's built-in 300ms "click delay", making flapping feel instant.
canvas.addEventListener('touchstart', function(e) { canvas.addEventListener('touchstart', function(e) {
e.preventDefault(); // Tells Safari: "I handled this touch, do NOT try to zoom." // Only prevent default zooming behavior during active gameplay.
flapBird(); // This allows the highscore form typing to work normally.
if (gameState === 'playing') {
e.preventDefault();
flapBird();
}
}, { passive: false }); }, { passive: false });
// Fallback for PC mouse users // Mouse click input processing
canvas.addEventListener('mousedown', flapBird); canvas.addEventListener('mousedown', flapBird);
// Keyboard spacebar input processing
document.addEventListener('keydown', function(e) { document.addEventListener('keydown', function(e) {
if (e.code === 'Space') { if (e.code === 'Space') {
flapBird(); // Prevent jumping if the user is currently typing their name
if (document.activeElement.id !== 'playerName') {
// Prevent space from scrolling page flapBird();
e.preventDefault(); e.preventDefault();
}
} }
}); });
// UI button bindings
startButton.addEventListener('click', startGame); startButton.addEventListener('click', startGame);
restartButton.addEventListener('click', startGame); restartButton.addEventListener('click', startGame);
// The Ultimate Safari Insurance Policy: Intercept all double-taps globally // Prevents double-tap zooming on iOS devices during active play
let lastTouchEnd = 0; let lastTouchEnd = 0;
document.addEventListener('touchend', function(e) { document.addEventListener('touchend', function(e) {
const now = (new Date()).getTime(); if (gameState === 'playing') {
if (now - lastTouchEnd <= 300) { const now = (new Date()).getTime();
e.preventDefault(); if (now - lastTouchEnd <= 300) {
e.preventDefault();
}
lastTouchEnd = now;
} }
lastTouchEnd = now;
}, { passive: false }); }, { passive: false });
// Start the game loop // Initializes the game loop upon loading the script
loop(); requestAnimationFrame((timestamp) => {
lastTime = timestamp;
requestAnimationFrame(loop);
});
</script> </script>
</body> </body>
</html> </html>