Update fbird.html
added highscore and addjusted physics to make it same speed independent of FPS of device
This commit is contained in:
1 parent
b7086b5f7b
commit
37bccfc53d
1 file changed
+350
-107
+350
-107
@@ -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"
|
||||||
|
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>
|
</button>
|
||||||
</div>
|
</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 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,
|
|
||||||
velocity: 0,
|
|
||||||
jump: -10,
|
|
||||||
|
|
||||||
|
// Physics variables tuned by the user
|
||||||
|
gravity: 20,
|
||||||
|
velocity: 0,
|
||||||
|
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.
|
||||||
|
// This allows the highscore form typing to work normally.
|
||||||
|
if (gameState === 'playing') {
|
||||||
|
e.preventDefault();
|
||||||
flapBird();
|
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') {
|
||||||
|
// Prevent jumping if the user is currently typing their name
|
||||||
|
if (document.activeElement.id !== 'playerName') {
|
||||||
flapBird();
|
flapBird();
|
||||||
|
|
||||||
// Prevent space from scrolling page
|
|
||||||
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) {
|
||||||
|
if (gameState === 'playing') {
|
||||||
const now = (new Date()).getTime();
|
const now = (new Date()).getTime();
|
||||||
if (now - lastTouchEnd <= 300) {
|
if (now - lastTouchEnd <= 300) {
|
||||||
e.preventDefault();
|
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>
|
||||||
Reference in new issue
Block a user