flap / index.html
omnificat's picture
Add 2 files
d3e103c verified
Raw
History Blame Contribute Delete
34.3 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Flappy City: Day & Night Adventure</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Poppins', 'Arial', sans-serif;
touch-action: manipulation;
}
.game-container {
width: 100%;
max-width: 500px;
height: 700px;
margin: 0 auto;
}
#game-canvas {
width: 100%;
height: 100%;
border-radius: 14px;
box-shadow: 0 15px 30px rgba(0,0,0,0.25);
overflow: hidden;
image-rendering: crisp-edges;
}
.pipe {
position: absolute;
width: 80px;
background-color: #4d9a17;
border: 4px solid #356811;
box-shadow: inset 0 0 10px rgba(0,0,0,0.2);
border-radius: 8px;
z-index: 9;
}
.bird-shade {
position: absolute;
background: rgba(0,0,0,0.15);
border-radius: 50%;
filter: blur(4px);
z-index: 18;
}
.bird-shade-animate {
animation: shadow-pulse 0.5s infinite alternate;
}
@keyframes fadeIn {
0% { opacity: 0; }
100% { opacity: 1; }
}
@keyframes shadow-pulse {
0% { transform: scale(0.9); opacity: 0.7; }
100% { transform: scale(1.1); opacity: 0.9; }
}
#bird {
position: absolute;
z-index: 20;
transform-style: preserve-3d;
}
.bird-shadow {
position: absolute;
background: rgba(0,0,0,0.15);
border-radius: 50%;
filter: blur(4px);
z-index: 18;
}
.bird-wing {
transform-origin: center center;
}
.building {
position: absolute;
background-color: #222;
border-left: 4px solid rgba(0,0,0,0.4);
border-right: 4px solid rgba(0,0,0,0.4);
box-shadow: 0 8px 15px -5px rgba(0,0,0,0.5);
border-top-left-radius: 8px;
border-top-right-radius: 8px;
z-index: 4;
}
.building-side {
position: absolute;
background-color: #1a1a1a;
width: 8px;
height: 100%;
}
/* Settings button animation */
.settings-btn-pulse {
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.1); }
100% { transform: scale(1); }
}
</style>
</head>
<body class="bg-gray-900 min-h-screen flex items-center justify-center p-4">
<div class="game-container relative">
<canvas id="game-canvas" class="bg-sky-500"></canvas>
<div id="start-screen" class="absolute inset-0 flex flex-col items-center justify-center bg-slate-700/50 backdrop-blur-sm p-6">
<div class="bg-gradient-to-br from-blue-500 to-purple-600 rounded-2xl p-8 w-full max-w-xs text-center shadow-2xl">
<div class="flex justify-center mb-6">
<div class="bg-yellow-300 w-24 h-24 rounded-full flex items-center justify-center shadow-lg">
<i class="fa-solid fa-dove text-5xl text-blue-600"></i>
</div>
</div>
<h2 class="text-white text-2xl font-bold mb-2">Flappy City</h2>
<p class="text-white/90 mb-6">Fly through the cityscape!</p>
<button id="start-button" class="bg-gradient-to-r from-yellow-400 to-yellow-500 hover:from-yellow-500 hover:to-yellow-600 text-blue-900 font-bold py-3 px-6 rounded-full text-lg w-full transform transition-all duration-300 hover:scale-105 focus:outline-none shadow-lg active:scale-95">
<span class="drop-shadow">Start Flight</span>
</button>
<div class="mt-6">
<p class="text-sm text-white/80"><i class="fa-solid fa-hand-pointer mr-1"></i> Tap to flap</p>
<p class="text-xs text-white/60 mt-2">Spacebar: Jump | P: Pause</p>
</div>
</div>
</div>
<div id="settings-screen" class="absolute inset-0 flex flex-col items-center justify-center bg-slate-800/80 backdrop-blur-sm hidden">
<div class="bg-gradient-to-br from-indigo-600 to-purple-700 rounded-2xl p-8 w-full max-w-xs">
<h2 class="text-white text-2xl font-bold mb-6 text-center">Game Settings</h2>
<div class="mb-6">
<label class="flex items-center cursor-pointer">
<div class="relative">
<input id="sound-toggle" type="checkbox" class="sr-only" checked>
<div class="block bg-gray-600 w-14 h-8 rounded-full"></div>
<div class="dot absolute left-1 top-1 bg-white w-6 h-6 rounded-full transition transform"></div>
</div>
<div class="ml-3 text-white font-medium">Sound Effects</div>
</label>
</div>
<div class="mb-6">
<label class="text-white block mb-3 font-medium">Pipe Gap Size:</label>
<div class="flex items-center">
<span class="text-white mr-2 text-sm">Easy</span>
<input id="gap-slider" type="range" min="150" max="250" value="200" class="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer">
<span class="text-white ml-2 text-sm">Hard</span>
</div>
</div>
<div class="mt-8">
<button id="settings-back-button" class="w-full bg-gradient-to-r from-blue-500 to-indigo-600 hover:from-blue-600 hover:to-indigo-700 text-white font-bold py-3 px-6 rounded-full mb-3 transition-all">
Back to Game
</button>
<button id="settings-restart-button" class="w-full bg-gradient-to-r from-green-500 to-green-600 hover:from-green-600 hover:to-green-700 text-white font-bold py-3 px-6 rounded-full transition-all">
Restart Game
</button>
</div>
</div>
</div>
<div id="game-over" class="absolute inset-0 flex flex-col items-center justify-center bg-slate-800/80 backdrop-blur-sm hidden">
<div class="bg-gradient-to-br from-rose-600 to-rose-800 rounded-2xl p-8 max-w-xs w-full text-center shadow-xl">
<h2 class="text-white text-2xl font-bold mb-2">Flight Over!</h2>
<p class="text-white/90 mb-2">Score: <span id="final-score" class="text-yellow-300 font-bold">0</span></p>
<p class="text-white/90 mb-6">Best: <span id="best-score" class="text-yellow-300 font-bold">0</span></p>
<button id="restart-button" class="bg-gradient-to-r from-green-400 to-green-500 hover:from-green-500 hover:to-green-600 text-white font-bold py-3 px-6 rounded-full text-lg w-full transition duration-300 transform hover:scale-105 focus:outline-none mb-4 shadow-lg active:scale-95">
Fly Again
</button>
<button id="menu-button" class="bg-gray-700 hover:bg-gray-600 text-white font-bold py-3 px-6 rounded-full text-lg w-full transition duration-300 shadow active:bg-gray-500">
Main Menu
</button>
<button id="settings-button" class="bg-indigo-600 hover:bg-indigo-500 text-white font-bold py-3 px-6 rounded-full text-lg w-full mt-4 transition duration-300 shadow active:bg-indigo-700 settings-btn-pulse">
<i class="fas fa-cog mr-2"></i>Settings
</button>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const canvas = document.getElementById('game-canvas');
const ctx = canvas.getContext('2d');
const startScreen = document.getElementById('start-screen');
const startButton = document.getElementById('start-button');
const gameOverScreen = document.getElementById('game-over');
const restartButton = document.getElementById('restart-button');
const menuButton = document.getElementById('menu-button');
const settingsButton = document.getElementById('settings-button');
const settingsScreen = document.getElementById('settings-screen');
const settingsBackButton = document.getElementById('settings-back-button');
const settingsRestartButton = document.getElementById('settings-restart-button');
const gapSlider = document.getElementById('gap-slider');
const soundToggle = document.getElementById('sound-toggle');
const finalScoreElement = document.getElementById('final-score');
const bestScoreElement = document.getElementById('best-score');
// Set canvas dimensions
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
// Game variables
let gameState = 'stopped';
let frames = 0;
let score = 0;
let bestScore = localStorage.getItem('flappyBestScore') || 0;
let isDayTime = true;
let gravity = 0.25;
let animationFrame;
let pipeGap = 200;
let birdVerticalOffset = 0;
let wingRotation = 0;
let birdAngle = 0;
// Fixed pipe interval
const pipeInterval = 120; // Consistent spacing between pipes
// Events
window.addEventListener('resize', resizeCanvas);
document.addEventListener('keydown', handleKeyPress);
startButton.addEventListener('click', startGame);
restartButton.addEventListener('click', startGame);
menuButton.addEventListener('click', showStartScreen);
settingsButton.addEventListener('click', showSettings);
settingsBackButton.addEventListener('click', hideSettings);
settingsRestartButton.addEventListener('click', () => {
hideSettings();
startGame();
});
gapSlider.addEventListener('input', updatePipeGap);
soundToggle.addEventListener('change', toggleSound);
// Mobile touch controls
canvas.addEventListener('touchstart', function(e) {
e.preventDefault();
if (gameState === 'stopped') {
startGame();
} else if (gameState === 'started') {
bird.jump();
}
}, { passive: false });
canvas.addEventListener('click', function() {
if (gameState === 'stopped') {
startGame();
} else if (gameState === 'started') {
bird.jump();
}
});
function resizeCanvas() {
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
buildingsCache = []; // Rebuild on resize
drawBackground();
}
function showSettings() {
settingsScreen.classList.remove('hidden');
}
function hideSettings() {
settingsScreen.classList.add('hidden');
}
function updatePipeGap() {
pipeGap = parseInt(gapSlider.value);
}
function toggleSound() {
console.log('Sound toggled:', soundToggle.checked);
}
function handleKeyPress(e) {
if (e.code === 'Space') {
if (gameState === 'stopped') {
startGame();
} else if (gameState === 'started') {
bird.jump();
}
} else if (e.code === 'KeyR') {
startGame();
} else if (e.code === 'KeyP') {
togglePause();
} else if (e.code === 'KeyS') {
showSettings();
}
}
// Bird object with 2.5D effects
const bird = {
x: 100,
y: canvas.height / 2,
radius: 22,
velocity: 0,
jumpHeight: 7.2,
update: function() {
// Physics
if (gameState === 'started') {
this.velocity += gravity;
this.y += this.velocity;
// Bird flapping offset
birdVerticalOffset = Math.sin(Date.now() / 100) * 3;
// Wing movement
wingRotation = Math.sin(Date.now() / 100) * 30;
// Bird angle based on velocity
birdAngle = this.velocity * 3;
if (birdAngle > 25) birdAngle = 25;
if (birdAngle < -25) birdAngle = -25;
}
// Boundary collision (except the ground - ground is handled by pipes/buildings)
if (this.y - this.radius <= 10) {
this.y = this.radius + 10;
this.velocity = 0;
}
},
jump: function() {
this.velocity = -this.jumpHeight;
},
draw: function() {
const centerX = this.x;
const centerY = this.y + birdVerticalOffset;
// Bird body with perspective
ctx.save();
ctx.translate(centerX, centerY);
ctx.rotate(birdAngle * Math.PI / 180);
// Bird body (yellow)
ctx.fillStyle = '#FFD32B';
ctx.beginPath();
ctx.ellipse(0, 0, this.radius, this.radius, 0, 0, Math.PI * 2);
ctx.fill();
// Bird wing
ctx.fillStyle = '#FF9839';
ctx.save();
ctx.translate(0, 7);
ctx.rotate(wingRotation * Math.PI / 180);
ctx.beginPath();
ctx.ellipse(0, 0, 12, 16, 0, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
// Bird detail
ctx.fillStyle = '#000000';
ctx.beginPath();
ctx.ellipse(0, -5, 5, 2, 0, 0, Math.PI * 2);
ctx.fill();
// Bird eye
ctx.fillStyle = '#FFFFFF';
ctx.beginPath();
ctx.ellipse(6, -5, 4, 4, 0, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#000000';
ctx.beginPath();
ctx.ellipse(6, -5, 1.5, 1.5, 0, 0, Math.PI * 2);
ctx.fill();
// Bird beak
ctx.fillStyle = '#FF7839';
ctx.beginPath();
ctx.moveTo(16, -2);
ctx.lineTo(22, 0);
ctx.lineTo(16, 4);
ctx.fill();
ctx.restore();
// Draw bird shadow
ctx.fillStyle = '#FCE49B';
ctx.fillRect(centerX - 10, centerY + this.radius, 20, 8);
},
reset: function() {
this.y = canvas.height / 2;
this.velocity = 0;
birdAngle = 0;
}
};
// Pipes array
const pipes = [];
// Enhanced Pipe object with 2.5D effect
function Pipe() {
this.topHeight = Math.floor(Math.random() * 180) + 60;
this.bottomHeight = canvas.height - this.topHeight - pipeGap - 80;
this.x = canvas.width + 80; // Start off-screen
this.width = 80;
this.speed = 3.2;
this.color = '#4d9a17';
this.highlight = false;
this.passed = false;
this.update = function() {
this.x -= this.speed;
// Pipe collision
if (
bird.x + bird.radius > this.x &&
bird.x - bird.radius < this.x + this.width &&
(bird.y - bird.radius < this.topHeight ||
bird.y + bird.radius > canvas.height - this.bottomHeight - 80)
) {
endGame();
}
// Score counting
if (!this.passed && this.x < bird.x - this.width/2) {
score++;
this.passed = true;
this.highlight = true;
// Toggle day/night every 5 points
if (score % 5 === 0) {
isDayTime = !isDayTime;
}
}
};
this.draw = function() {
// Top pipe
ctx.fillStyle = '#356811';
ctx.fillRect(this.x - 4, 0, this.width + 8, this.topHeight);
ctx.fillStyle = this.color;
ctx.fillRect(this.x, 0, this.width, this.topHeight);
// Top pipe cap
ctx.fillStyle = '#3d8617';
ctx.fillRect(this.x - 4, this.topHeight - 10, this.width + 8, 14);
ctx.fillRect(this.x - 4, this.topHeight - 14, this.width + 8, 4);
// Bottom pipe
ctx.fillStyle = '#356811';
ctx.fillRect(
this.x - 4,
canvas.height - this.bottomHeight - 80,
this.width + 8,
this.bottomHeight
);
ctx.fillStyle = this.color;
ctx.fillRect(
this.x,
canvas.height - this.bottomHeight - 80,
this.width,
this.bottomHeight
);
// Bottom pipe cap
ctx.fillStyle = '#3d8617';
ctx.fillRect(
this.x - 4,
canvas.height - this.bottomHeight - 80,
this.width + 8,
14
);
if (this.highlight) {
ctx.fillStyle = 'rgba(255, 215, 0, 0.4)';
ctx.fillRect(this.x, 0, this.width, this.topHeight);
ctx.fillRect(
this.x,
canvas.height - this.bottomHeight - 80,
this.width,
this.bottomHeight
);
this.highlight = false;
}
};
}
// Create and cache buildings
let buildingsCache = [];
function createBuildingCache() {
buildingsCache = [];
const buildingColors = [
{main: '#333', edge: '#2a2a2a'},
{main: '#2a2a2a', edge: '#222'},
{main: '#222', edge: '#1c1c1c'},
{main: '#1c1c1c', edge: '#161616'}
];
let xPos = 0;
while (xPos < canvas.width) {
const width = Math.floor(Math.random() * 100) + 60;
const height = Math.floor(canvas.height * (0.05 + Math.random() * 0.2)) + 80;
const colorIndex = Math.floor(Math.random() * buildingColors.length);
buildingsCache.push({
x: xPos,
width: width,
height: height,
color: buildingColors[colorIndex].main,
edgeColor: buildingColors[colorIndex].edge,
windows: []
});
xPos += width + 15;
}
}
// Draw buildings with 2.5D effect and better windows
function drawBuildings() {
// Create building cache if empty
if (buildingsCache.length === 0) {
createBuildingCache();
}
// Draw base floor
ctx.fillStyle = 'rgba(85, 85, 85, 0.95)';
ctx.fillRect(0, canvas.height - 80, canvas.width, 80);
ctx.fillStyle = '#666';
ctx.fillRect(0, canvas.height - 80, canvas.width, 4);
// Draw street markings
for (let i = 0; i < canvas.width; i += 50) {
ctx.fillStyle = '#ffc107';
ctx.fillRect(i, canvas.height - 55, 30, 8);
}
buildingsCache.forEach(building => {
const buildingY = canvas.height - building.height - 80;
// Draw building silhouette (3D effect)
ctx.fillStyle = '#000';
ctx.fillRect(
building.x - 8,
buildingY - 10,
building.width + 16,
building.height + 10
);
// Draw main building body
ctx.fillStyle = building.color;
ctx.fillRect(
building.x,
buildingY,
building.width,
building.height
);
// Draw building right edge for 3D
ctx.fillStyle = building.edgeColor;
ctx.fillRect(
building.x + building.width - 8,
buildingY,
8,
building.height
);
// Draw building top ledge
ctx.fillStyle = '#111';
ctx.fillRect(
building.x - 8,
buildingY - 10,
building.width + 16,
10
);
// Draw building roof
ctx.fillStyle = '#333';
ctx.fillRect(
building.x - 15,
buildingY - 30,
building.width + 30,
20
);
// Draw windows within canvas
const windowRows = Math.floor(building.height / 25);
const windowCols = Math.floor(building.width / 15);
const rowSpacing = building.height / windowRows;
const colSpacing = building.width / windowCols;
for (let row = 1; row < windowRows; row++) {
for (let col = 1; col < windowCols; col++) {
if (Math.random() > 0.25) { // Only place windows 75% of the time
const windowX = building.x + (col * colSpacing) - 4;
const windowY = buildingY + (row * rowSpacing) - 6;
const isLit = isDayTime || Math.random() > 0.6;
// Draw the window on canvas
ctx.fillStyle = isLit ?
(isDayTime ? '#f6d186' : '#FFF59D') :
(isDayTime ? '#9fc4d4' : '#1a1a1a');
ctx.fillRect(windowX, windowY, 8, 12);
// Add reflection if lit
if (isLit) {
ctx.fillStyle = 'rgba(255, 255, 255, 0.5)';
ctx.beginPath();
ctx.ellipse(windowX + 5, windowY + 3, 1.5, 1.5, 0, 0, Math.PI * 2);
ctx.fill();
}
}
}
}
});
}
// Draw background with day/night effects
function drawBackground() {
// Sky gradient
if (isDayTime) {
// Day gradient
const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height);
gradient.addColorStop(0, '#70d1ff');
gradient.addColorStop(1, '#48a0ff');
ctx.fillStyle = gradient;
} else {
// Night gradient
const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height);
gradient.addColorStop(0, '#0f2f6e');
gradient.addColorStop(1, '#1c1554');
ctx.fillStyle = gradient;
}
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Draw clouds during day
if (isDayTime) {
ctx.fillStyle = 'rgba(255,255,255,0.8)';
// Cloud positions
const cloudPositions = [
{x: frames/15 % (canvas.width + 150) - 50, y: 100},
{x: frames/20 % (canvas.width + 200) - 100, y: 180},
{x: frames/25 % (canvas.width + 100) - 20, y: 240}
];
cloudPositions.forEach(cloud => {
ctx.beginPath();
ctx.arc(cloud.x, cloud.y, 30, 0, Math.PI * 2);
ctx.arc(cloud.x + 25, cloud.y - 10, 20, 0, Math.PI * 2);
ctx.arc(cloud.x + 15, cloud.y + 15, 25, 0, Math.PI * 2);
ctx.fill();
});
} else {
// Draw stars at night
ctx.fillStyle = 'rgba(255,255,255,0.8)';
for (let i = 0; i < 30; i++) {
const starX = (frames + i * 100) % (canvas.width + 20) - 10;
const starY = Math.sin(i * 123) * 100 + 200;
if (starX > 0 && starX < canvas.width) {
ctx.beginPath();
ctx.arc(starX, starY, Math.random() * 1.2 + 0.3, 0, Math.PI * 2);
ctx.fill();
}
}
}
// Render city buildings
drawBuildings();
}
function createPipes() {
// Add pipes with consistent spacing
if (frames % pipeInterval === 0) {
pipes.push(new Pipe());
// Limit pipes on screen to 3 max for performance
if (pipes.length > 3) {
pipes.shift();
}
}
}
function updatePipes() {
for (let i = 0; i < pipes.length; i++) {
pipes[i].update();
pipes[i].draw();
if (pipes[i].x + pipes[i].width < 0) {
pipes.splice(i, 1);
i--;
}
}
}
function drawScore() {
ctx.font = 'bold 36px "Poppins", sans-serif';
ctx.fillStyle = 'rgba(255, 255, 255, 0.85)';
ctx.textAlign = 'center';
ctx.strokeStyle = '#0f172a';
ctx.lineWidth = 6;
ctx.strokeText(score, canvas.width / 2, 60);
ctx.fillText(score, canvas.width / 2, 60);
}
function endGame() {
gameState = 'over';
gameOverScreen.classList.remove('hidden');
finalScoreElement.textContent = score;
// Update best score
if (score > bestScore) {
bestScore = score;
localStorage.setItem('flappyBestScore', bestScore);
}
bestScoreElement.textContent = bestScore;
}
function togglePause() {
if (gameState === 'started') {
gameState = 'paused';
} else if (gameState === 'paused') {
gameState = 'started';
gameLoop();
}
}
function startGame() {
gameState = 'started';
startScreen.classList.add('hidden');
gameOverScreen.classList.add('hidden');
settingsScreen.classList.add('hidden');
// Reset game values
score = 0;
pipes.length = 0;
frames = 0;
isDayTime = true;
birdVerticalOffset = 0;
bird.reset();
createBuildingCache();
// Start the game loop
if (animationFrame) cancelAnimationFrame(animationFrame);
gameLoop();
}
function showStartScreen() {
gameState = 'stopped';
gameOverScreen.classList.add('hidden');
settingsScreen.classList.add('hidden');
startScreen.classList.remove('hidden');
}
// Main game loop
function gameLoop() {
// Clear canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw background
drawBackground();
// Game animation update
if (gameState === 'started') {
// Update and draw game objects
if (pipes.length === 0 || pipes[pipes.length-1].x < canvas.width - 250) {
createPipes();
}
updatePipes();
bird.update();
bird.draw();
}
// Draw score last for layering
drawScore();
frames++;
// Continue game loop if game is active
if (gameState === 'started') {
animationFrame = requestAnimationFrame(gameLoop);
}
}
// Initialize the game
showStartScreen();
});
</script>
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=omnificat/flap" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
</html>