Auto commit: 2025-11-21T17:09:16.614Z

This commit is contained in:
Flatlogic Bot 2025-11-21 17:09:16 +00:00
parent a7de8d3e75
commit c7d1a28045
11 changed files with 551 additions and 182 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 325 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

View File

@ -27,14 +27,8 @@ ALLOWED_HOSTS = [
] ]
CSRF_TRUSTED_ORIGINS = [ CSRF_TRUSTED_ORIGINS = [
origin for origin in [ f"https://{os.getenv('HOST_FQDN', '')}",
os.getenv("HOST_FQDN", ""), "https://*.appwizzy.dev",
os.getenv("CSRF_TRUSTED_ORIGIN", "")
] if origin
]
CSRF_TRUSTED_ORIGINS = [
f"https://{host}" if not host.startswith(("http://", "https://")) else host
for host in CSRF_TRUSTED_ORIGINS
] ]
# Cookies must always be HTTPS-only; SameSite=Lax keeps CSRF working behind the proxy. # Cookies must always be HTTPS-only; SameSite=Lax keeps CSRF working behind the proxy.
@ -144,7 +138,8 @@ USE_TZ = True
# https://docs.djangoproject.com/en/5.2/howto/static-files/ # https://docs.djangoproject.com/en/5.2/howto/static-files/
STATIC_URL = 'static/' STATIC_URL = 'static/'
STATIC_ROOT = BASE_DIR / 'static' STATICFILES_DIRS = [BASE_DIR / 'static']
STATIC_ROOT = BASE_DIR / 'staticfiles'
# Email # Email
EMAIL_BACKEND = os.getenv( EMAIL_BACKEND = os.getenv(

View File

@ -14,23 +14,17 @@
</div> </div>
<div id="game-area" class="container game-container my-5"> <div id="game-area" class="container game-container my-5">
<div class="row"> <div class="game-wrapper">
<div class="col-lg-8 mx-auto"> <div class="instructions">
<div class="canvas-wrapper"> <h2 class="instructions-title">How to Play</h2>
<canvas id="gameCanvas" width="800" height="600"></canvas> <p>Use the <strong>left</strong> and <strong>right</strong> arrow keys to move your character.</p>
</div> <p>Catch as many oranges as you can before you run out of lives!</p>
</div>
<div class="canvas-wrapper">
<canvas id="gameCanvas" width="800" height="600"></canvas>
</div> </div>
</div> </div>
<div class="row mt-4"> </div>
<div class="col-lg-8 mx-auto text-center">
<div class="instructions">
<h2 class="instructions-title">How to Play</h2>
<p>Use the <strong>left</strong> and <strong>right</strong> arrow keys to move your character.</p>
<p>Catch as many oranges as you can before you run out of lives!</p>
</div>
</div>
</div>
</div></div>
<div class="container my-5"> <div class="container my-5">
<div class="row"> <div class="row">
@ -51,27 +45,4 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Name Submission Modal -->
<div class="modal fade" id="nameModal" tabindex="-1" aria-labelledby="nameModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="nameModalLabel">Game Over!</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>Your final score is: <strong id="finalScore"></strong></p>
<form id="nameForm">
{% csrf_token %}
<div class="mb-3">
<label for="playerName" class="form-label">Enter your name to save your score:</label>
<input type="text" class="form-control" id="playerName" required>
</div>
<button type="submit" class="btn btn-primary">Save Score</button>
</form>
</div>
</div>
</div>
</div>
{% endblock %} {% endblock %}

View File

@ -109,12 +109,14 @@ body {
border-radius: 10px; border-radius: 10px;
box-shadow: 0 0 30px rgba(255, 165, 0, 0.3); box-shadow: 0 0 30px rgba(255, 165, 0, 0.3);
overflow: hidden; overflow: hidden;
/* The canvas itself is 800x600 */
width: 800px;
height: 600px;
margin: 0 auto; /* Center the canvas */
} }
#gameCanvas { #gameCanvas {
display: block; display: block;
width: 100%;
height: auto;
} }
.instructions { .instructions {
@ -122,6 +124,8 @@ body {
padding: 2rem; padding: 2rem;
border-radius: 10px; border-radius: 10px;
color: var(--text-light); color: var(--text-light);
max-width: 800px; /* Match canvas width */
margin: 2rem auto; /* Center and add space */
} }
.instructions-title { .instructions-title {
@ -138,4 +142,14 @@ body {
.game-subtitle { .game-subtitle {
font-size: 1.2rem; font-size: 1.2rem;
} }
}
.canvas-wrapper {
width: 100%;
height: auto;
}
#gameCanvas {
width: 100%;
height: auto;
}
}

View File

@ -6,7 +6,7 @@ document.addEventListener('DOMContentLoaded', () => {
} }
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
// Use fixed canvas dimensions from HTML // Set fixed canvas size
canvas.width = 800; canvas.width = 800;
canvas.height = 600; canvas.height = 600;
@ -14,9 +14,9 @@ document.addEventListener('DOMContentLoaded', () => {
let score = 0; let score = 0;
let lives = 3; let lives = 3;
let gameOver = false; let gameOver = false;
let gameOverSoundPlayed = false; let gameStarted = false;
let orangeInterval;
// Player (catcher)
const player = { const player = {
width: 100, width: 100,
height: 50, height: 50,
@ -24,10 +24,8 @@ document.addEventListener('DOMContentLoaded', () => {
y: canvas.height - 60, y: canvas.height - 60,
speed: 500, speed: 500,
dx: 0, dx: 0,
// A simple representation of a character (e.g., a basket)
draw() { draw() {
// Basket body ctx.fillStyle = '#8B4513';
ctx.fillStyle = '#8B4513'; // SaddleBrown
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(this.x, this.y); ctx.moveTo(this.x, this.y);
ctx.lineTo(this.x + this.width, this.y); ctx.lineTo(this.x + this.width, this.y);
@ -35,9 +33,7 @@ document.addEventListener('DOMContentLoaded', () => {
ctx.lineTo(this.x + 15, this.y + this.height); ctx.lineTo(this.x + 15, this.y + this.height);
ctx.closePath(); ctx.closePath();
ctx.fill(); ctx.fill();
ctx.strokeStyle = '#5C2F0E';
// Basket rim
ctx.strokeStyle = '#5C2F0E'; // Darker brown
ctx.lineWidth = 8; ctx.lineWidth = 8;
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(this.x, this.y); ctx.moveTo(this.x, this.y);
@ -46,7 +42,6 @@ document.addEventListener('DOMContentLoaded', () => {
} }
}; };
// Orange
const orangeProps = { const orangeProps = {
size: 20, size: 20,
speed: 150, speed: 150,
@ -56,58 +51,49 @@ document.addEventListener('DOMContentLoaded', () => {
function createOrange() { function createOrange() {
oranges.push({ oranges.push({
x: Math.random() * (canvas.width - orangeProps.size), x: Math.random() * (canvas.width - orangeProps.size),
y: -orangeProps.size, // Start above the canvas y: -orangeProps.size,
size: orangeProps.size, size: orangeProps.size,
speed: orangeProps.speed + Math.random() * 100, speed: orangeProps.speed + Math.random() * 100,
draw() { draw() {
// Orange body
ctx.beginPath(); ctx.beginPath();
ctx.arc(this.x + this.size / 2, this.y + this.size / 2, this.size, 0, Math.PI * 2); ctx.arc(this.x + this.size / 2, this.y + this.size / 2, this.size, 0, Math.PI * 2);
ctx.fillStyle = '#FFA500'; // Orange color ctx.fillStyle = '#FFA500';
ctx.fill(); ctx.fill();
// Leaf
ctx.beginPath(); ctx.beginPath();
ctx.ellipse(this.x + this.size / 2 + 5, this.y, 8, 3, Math.PI / 4, 0, Math.PI * 2); ctx.ellipse(this.x + this.size / 2 + 5, this.y, 8, 3, Math.PI / 4, 0, Math.PI * 2);
ctx.fillStyle = '#228B22'; // ForestGreen ctx.fillStyle = '#228B22';
ctx.fill(); ctx.fill();
} }
}); });
} }
let lastTime = 0; let lastTime = 0;
// Game loop
function update(timestamp) { function update(timestamp) {
if (!gameStarted) return;
if (gameOver) { if (gameOver) {
drawGameOver(); drawGameOver();
return; return;
} }
const deltaTime = (timestamp - lastTime) / 1000; // Delta time in seconds const deltaTime = (timestamp - lastTime) / 1000;
lastTime = timestamp; lastTime = timestamp;
// Clear canvas
ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw background elements (like the tree trunk/branches if desired)
drawScene(); drawScene();
// Draw and move player
player.draw(); player.draw();
player.x += player.dx * player.speed * deltaTime; player.x += player.dx * player.speed * deltaTime;
// Wall detection for player
if (player.x < 0) player.x = 0; if (player.x < 0) player.x = 0;
if (player.x + player.width > canvas.width) { if (player.x + player.width > canvas.width) {
player.x = canvas.width - player.width; player.x = canvas.width - player.width;
} }
// Handle oranges
oranges.forEach((orange, index) => { oranges.forEach((orange, index) => {
orange.y += orange.speed * deltaTime; orange.y += orange.speed * deltaTime;
orange.draw(); orange.draw();
// Collision with player
if ( if (
orange.y + orange.size > player.y && orange.y + orange.size > player.y &&
orange.x < player.x + player.width && orange.x < player.x + player.width &&
@ -115,10 +101,9 @@ document.addEventListener('DOMContentLoaded', () => {
) { ) {
score++; score++;
playCatchSound(); playCatchSound();
oranges.splice(index, 1); // Remove caught orange oranges.splice(index, 1);
} }
// Orange missed (hit the ground)
if (orange.y > canvas.height) { if (orange.y > canvas.height) {
lives--; lives--;
oranges.splice(index, 1); oranges.splice(index, 1);
@ -127,84 +112,63 @@ document.addEventListener('DOMContentLoaded', () => {
} }
} }
}); });
// Draw score and lives
drawUI();
drawUI();
requestAnimationFrame(update); requestAnimationFrame(update);
} }
function drawScene() { function drawScene() {
// Simple ground ctx.fillStyle = '#0A192F';
ctx.fillStyle = '#228B22'; // ForestGreen ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#228B22';
ctx.fillRect(0, canvas.height - 20, canvas.width, 20); ctx.fillRect(0, canvas.height - 20, canvas.width, 20);
} }
function drawUI() { function drawUI() {
ctx.fillStyle = '#E6F1FF'; ctx.fillStyle = '#E6F1FF';
ctx.font = '24px Poppins, sans-serif'; ctx.font = '24px Poppins, sans-serif';
ctx.textAlign = 'left';
ctx.fillText(`Score: ${score}`, 20, 40); ctx.fillText(`Score: ${score}`, 20, 40);
ctx.fillText(`Lives: ${lives}`, canvas.width - 120, 40); ctx.textAlign = 'right';
ctx.fillText(`Lives: ${lives}`, canvas.width - 20, 40);
ctx.textAlign = 'center';
} }
function drawGameOver() { function drawGameOver() {
if (!gameOverSoundPlayed) { clearInterval(orangeInterval);
playGameOverSound(); ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
gameOverSoundPlayed = true;
document.getElementById('finalScore').textContent = score;
const nameModal = new bootstrap.Modal(document.getElementById('nameModal'));
nameModal.show();
}
ctx.fillStyle = 'rgba(10, 25, 47, 0.8)';
ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#E6F1FF'; ctx.fillStyle = '#E6F1FF';
ctx.font = '50px Poppins, sans-serif'; ctx.font = '50px Poppins, sans-serif';
ctx.textAlign = 'center'; ctx.textAlign = 'center';
ctx.fillText('GAME OVER', canvas.width / 2, canvas.height / 2 - 40); ctx.fillText('GAME OVER', canvas.width / 2, canvas.height / 2 - 40);
ctx.font = '30px Poppins, sans-serif'; ctx.font = '30px Poppins, sans-serif';
ctx.fillText(`Final Score: ${score}`, canvas.width / 2, canvas.height / 2 + 20); ctx.fillText(`Final Score: ${score}`, canvas.width / 2, canvas.height / 2 + 20);
ctx.font = '20px Poppins, sans-serif';
ctx.fillText('Click to Restart', canvas.width / 2, canvas.height / 2 + 70);
}
function drawInitialMessage() {
ctx.fillStyle = '#E6F1FF';
ctx.font = '30px Poppins, sans-serif';
ctx.textAlign = 'center';
ctx.fillText('Click "Play Now" to Start!', canvas.width / 2, canvas.height / 2);
} }
function playCatchSound() { function playCatchSound() {
if (!audioCtx) return; if (!audioCtx) return;
const oscillator = audioCtx.createOscillator(); const oscillator = audioCtx.createOscillator();
const gainNode = audioCtx.createGain(); const gainNode = audioCtx.createGain();
oscillator.connect(gainNode); oscillator.connect(gainNode);
gainNode.connect(audioCtx.destination); gainNode.connect(audioCtx.destination);
oscillator.type = 'sine'; oscillator.type = 'sine';
oscillator.frequency.setValueAtTime(880, audioCtx.currentTime); // A5 note oscillator.frequency.setValueAtTime(880, audioCtx.currentTime);
gainNode.gain.setValueAtTime(0.1, audioCtx.currentTime); gainNode.gain.setValueAtTime(0.1, audioCtx.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.00001, audioCtx.currentTime + 0.5); gainNode.gain.exponentialRampToValueAtTime(0.00001, audioCtx.currentTime + 0.5);
oscillator.start(audioCtx.currentTime); oscillator.start(audioCtx.currentTime);
oscillator.stop(audioCtx.currentTime + 0.5); oscillator.stop(audioCtx.currentTime + 0.5);
} }
function playGameOverSound() {
if (!audioCtx) return;
const oscillator = audioCtx.createOscillator();
const gainNode = audioCtx.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioCtx.destination);
oscillator.type = 'triangle';
gainNode.gain.setValueAtTime(0.1, audioCtx.currentTime);
const frequencies = [440, 220, 110]; // A4, A3, A2
frequencies.forEach((freq, i) => {
oscillator.frequency.setValueAtTime(freq, audioCtx.currentTime + i * 0.3);
});
gainNode.gain.exponentialRampToValueAtTime(0.00001, audioCtx.currentTime + 1);
oscillator.start(audioCtx.currentTime);
oscillator.stop(audioCtx.currentTime + 1);
}
async function updateLeaderboard() { async function updateLeaderboard() {
try { try {
const response = await fetch('/api/scores/'); const response = await fetch('/api/scores/');
@ -224,8 +188,8 @@ document.addEventListener('DOMContentLoaded', () => {
} }
} }
// Keyboard controls
function move(e) { function move(e) {
if (!gameStarted || gameOver) return;
if (e.key === 'ArrowRight' || e.key === 'd') { if (e.key === 'ArrowRight' || e.key === 'd') {
player.dx = 1; player.dx = 1;
} else if (e.key === 'ArrowLeft' || e.key === 'a') { } else if (e.key === 'ArrowLeft' || e.key === 'a') {
@ -238,66 +202,78 @@ document.addEventListener('DOMContentLoaded', () => {
player.dx = 0; player.dx = 0;
} }
} }
function initGame() {
score = 0;
lives = 3;
oranges = [];
gameOver = false;
gameStarted = false;
player.x = canvas.width / 2 - player.width / 2;
drawScene();
drawInitialMessage();
}
function restartGame() { function restartGame() {
score = 0; score = 0;
lives = 3; lives = 3;
oranges = []; oranges = [];
gameOver = false; gameOver = false;
gameOverSoundPlayed = false;
player.x = canvas.width / 2 - player.width / 2; player.x = canvas.width / 2 - player.width / 2;
const nameModal = bootstrap.Modal.getInstance(document.getElementById('nameModal')); startCountdown();
if (nameModal) {
nameModal.hide();
}
update(0);
} }
function getCookie(name) { function startCountdown() {
let cookieValue = null; let count = 3;
if (document.cookie && document.cookie !== '') { const countdown = setInterval(() => {
const cookies = document.cookie.split(';'); ctx.clearRect(0, 0, canvas.width, canvas.height);
for (let i = 0; i < cookies.length; i++) { drawScene();
const cookie = cookies[i].trim(); ctx.fillStyle = '#E6F1FF';
if (cookie.substring(0, name.length + 1) === (name + '=')) { ctx.font = '60px Poppins, sans-serif';
cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); ctx.textAlign = 'center';
break; if (count > 0) {
} ctx.fillText(count, canvas.width / 2, canvas.height / 2);
count--;
} else {
clearInterval(countdown);
ctx.fillText('Go!', canvas.width / 2, canvas.height / 2);
setTimeout(startGame, 500);
} }
} }, 1000);
return cookieValue; }
function startGame() {
gameStarted = true;
lastTime = performance.now();
if (orangeInterval) clearInterval(orangeInterval);
orangeInterval = setInterval(createOrange, 1200);
requestAnimationFrame(update);
} }
// Event Listeners
document.addEventListener('keydown', move); document.addEventListener('keydown', move);
document.addEventListener('keyup', stopMove); document.addEventListener('keyup', stopMove);
const playButton = document.querySelector('.btn-play');
const gameArea = document.getElementById('game-area');
if (playButton && gameArea) {
playButton.addEventListener('click', (e) => {
e.preventDefault();
gameArea.scrollIntoView({ behavior: 'smooth' });
if (!gameStarted && !gameOver) {
startCountdown();
}
});
}
const nameForm = document.getElementById('nameForm'); canvas.addEventListener('click', () => {
nameForm.addEventListener('submit', async (e) => { if (gameOver) {
e.preventDefault(); restartGame();
const playerName = document.getElementById('playerName').value;
const csrftoken = getCookie('csrftoken');
try {
await fetch('/api/scores/create/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrftoken,
},
body: JSON.stringify({ player_name: playerName, score: score }),
});
await updateLeaderboard();
} catch (error) {
console.error('Error saving score:', error);
} }
restartGame();
}); });
// Start creating oranges periodically // Initial Setup
setInterval(createOrange, 1200); initGame();
// Initial call to start the game loop and leaderboard
update(0);
updateLeaderboard(); updateLeaderboard();
}); });

View File

@ -1,21 +1,155 @@
/*
Orange Catcher - Custom Stylesheet
*/
:root { :root {
--bg-color-start: #6a11cb; --primary-orange: #FFA500;
--bg-color-end: #2575fc; --secondary-deep-blue: #0A192F;
--text-color: #ffffff; --accent-yellow: #FFD700;
--card-bg-color: rgba(255, 255, 255, 0.01); --text-light: #E6F1FF;
--card-border-color: rgba(255, 255, 255, 0.1); --background-dark-start: #0A192F;
--background-dark-end: #1C2D4A;
--font-headings: 'Poppins', sans-serif;
--font-body: 'Lato', sans-serif;
} }
html {
scroll-behavior: smooth;
}
body { body {
margin: 0; background-color: var(--background-dark-start);
font-family: 'Inter', sans-serif; color: var(--text-light);
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end)); font-family: var(--font-body);
color: var(--text-color);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
text-align: center;
overflow: hidden;
position: relative;
} }
/* Hero Section */
.hero-section {
position: relative;
height: 80vh;
min-height: 500px;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
background: radial-gradient(circle, var(--background-dark-end), var(--background-dark-start));
overflow: hidden;
padding: 0 15px;
}
.hero-section .content-wrapper {
z-index: 2;
}
.game-title {
font-family: var(--font-headings);
font-size: 4.5rem;
font-weight: 700;
color: white;
text-shadow: 0 4px 15px rgba(0,0,0,0.2);
}
.game-subtitle {
font-size: 1.5rem;
color: var(--text-light);
opacity: 0.9;
}
.btn-play {
background-color: var(--primary-orange);
color: var(--secondary-deep-blue);
font-family: var(--font-headings);
font-weight: 700;
font-size: 1.2rem;
padding: 12px 40px;
border-radius: 50px;
border: none;
margin-top: 20px;
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.btn-play:hover {
transform: translateY(-3px);
box-shadow: 0 10px 20px rgba(255, 165, 0, 0.2);
color: var(--secondary-deep-blue);
}
/* Decorative Shapes */
.hero-section .shape-1, .hero-section .shape-2 {
position: absolute;
border-radius: 50%;
background: var(--primary-orange);
opacity: 0.1;
z-index: 1;
}
.hero-section .shape-1 {
width: 200px;
height: 200px;
top: 10%;
left: 15%;
}
.hero-section .shape-2 {
width: 100px;
height: 100px;
bottom: 15%;
right: 20%;
}
/* Game Container */
.game-container {
padding-top: 4rem;
padding-bottom: 4rem;
}
.canvas-wrapper {
background-color: #000;
border: 4px solid var(--primary-orange);
border-radius: 10px;
box-shadow: 0 0 30px rgba(255, 165, 0, 0.3);
overflow: hidden;
/* The canvas itself is 800x600 */
width: 800px;
height: 600px;
margin: 0 auto; /* Center the canvas */
}
#gameCanvas {
display: block;
}
.instructions {
background-color: var(--background-dark-end);
padding: 2rem;
border-radius: 10px;
color: var(--text-light);
max-width: 800px; /* Match canvas width */
margin: 2rem auto; /* Center and add space */
}
.instructions-title {
font-family: var(--font-headings);
color: var(--primary-orange);
margin-bottom: 1rem;
}
/* Responsive */
@media (max-width: 768px) {
.game-title {
font-size: 3rem;
}
.game-subtitle {
font-size: 1.2rem;
}
.canvas-wrapper {
width: 100%;
height: auto;
}
#gameCanvas {
width: 100%;
height: auto;
}
}

279
staticfiles/js/game.js Normal file
View File

@ -0,0 +1,279 @@
document.addEventListener('DOMContentLoaded', () => {
const canvas = document.getElementById('gameCanvas');
if (!canvas) {
console.error('Canvas element not found!');
return;
}
const ctx = canvas.getContext('2d');
// Set fixed canvas size
canvas.width = 800;
canvas.height = 600;
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
let score = 0;
let lives = 3;
let gameOver = false;
let gameStarted = false;
let orangeInterval;
const player = {
width: 100,
height: 50,
x: canvas.width / 2 - 50,
y: canvas.height - 60,
speed: 500,
dx: 0,
draw() {
ctx.fillStyle = '#8B4513';
ctx.beginPath();
ctx.moveTo(this.x, this.y);
ctx.lineTo(this.x + this.width, this.y);
ctx.lineTo(this.x + this.width - 15, this.y + this.height);
ctx.lineTo(this.x + 15, this.y + this.height);
ctx.closePath();
ctx.fill();
ctx.strokeStyle = '#5C2F0E';
ctx.lineWidth = 8;
ctx.beginPath();
ctx.moveTo(this.x, this.y);
ctx.lineTo(this.x + this.width, this.y);
ctx.stroke();
}
};
const orangeProps = {
size: 20,
speed: 150,
};
let oranges = [];
function createOrange() {
oranges.push({
x: Math.random() * (canvas.width - orangeProps.size),
y: -orangeProps.size,
size: orangeProps.size,
speed: orangeProps.speed + Math.random() * 100,
draw() {
ctx.beginPath();
ctx.arc(this.x + this.size / 2, this.y + this.size / 2, this.size, 0, Math.PI * 2);
ctx.fillStyle = '#FFA500';
ctx.fill();
ctx.beginPath();
ctx.ellipse(this.x + this.size / 2 + 5, this.y, 8, 3, Math.PI / 4, 0, Math.PI * 2);
ctx.fillStyle = '#228B22';
ctx.fill();
}
});
}
let lastTime = 0;
function update(timestamp) {
if (!gameStarted) return;
if (gameOver) {
drawGameOver();
return;
}
const deltaTime = (timestamp - lastTime) / 1000;
lastTime = timestamp;
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawScene();
player.draw();
player.x += player.dx * player.speed * deltaTime;
if (player.x < 0) player.x = 0;
if (player.x + player.width > canvas.width) {
player.x = canvas.width - player.width;
}
oranges.forEach((orange, index) => {
orange.y += orange.speed * deltaTime;
orange.draw();
if (
orange.y + orange.size > player.y &&
orange.x < player.x + player.width &&
orange.x + orange.size > player.x
) {
score++;
playCatchSound();
oranges.splice(index, 1);
}
if (orange.y > canvas.height) {
lives--;
oranges.splice(index, 1);
if (lives <= 0) {
gameOver = true;
}
}
});
drawUI();
requestAnimationFrame(update);
}
function drawScene() {
ctx.fillStyle = '#0A192F';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#228B22';
ctx.fillRect(0, canvas.height - 20, canvas.width, 20);
}
function drawUI() {
ctx.fillStyle = '#E6F1FF';
ctx.font = '24px Poppins, sans-serif';
ctx.textAlign = 'left';
ctx.fillText(`Score: ${score}`, 20, 40);
ctx.textAlign = 'right';
ctx.fillText(`Lives: ${lives}`, canvas.width - 20, 40);
ctx.textAlign = 'center';
}
function drawGameOver() {
clearInterval(orangeInterval);
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#E6F1FF';
ctx.font = '50px Poppins, sans-serif';
ctx.textAlign = 'center';
ctx.fillText('GAME OVER', canvas.width / 2, canvas.height / 2 - 40);
ctx.font = '30px Poppins, sans-serif';
ctx.fillText(`Final Score: ${score}`, canvas.width / 2, canvas.height / 2 + 20);
ctx.font = '20px Poppins, sans-serif';
ctx.fillText('Click to Restart', canvas.width / 2, canvas.height / 2 + 70);
}
function drawInitialMessage() {
ctx.fillStyle = '#E6F1FF';
ctx.font = '30px Poppins, sans-serif';
ctx.textAlign = 'center';
ctx.fillText('Click "Play Now" to Start!', canvas.width / 2, canvas.height / 2);
}
function playCatchSound() {
if (!audioCtx) return;
const oscillator = audioCtx.createOscillator();
const gainNode = audioCtx.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioCtx.destination);
oscillator.type = 'sine';
oscillator.frequency.setValueAtTime(880, audioCtx.currentTime);
gainNode.gain.setValueAtTime(0.1, audioCtx.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.00001, audioCtx.currentTime + 0.5);
oscillator.start(audioCtx.currentTime);
oscillator.stop(audioCtx.currentTime + 0.5);
}
async function updateLeaderboard() {
try {
const response = await fetch('/api/scores/');
const scores = await response.json();
const leaderboardBody = document.getElementById('leaderboard-body');
leaderboardBody.innerHTML = '';
scores.forEach((score, index) => {
const row = `<tr>
<th scope="row">${index + 1}</th>
<td>${score.player_name}</td>
<td>${score.score}</td>
</tr>`;
leaderboardBody.innerHTML += row;
});
} catch (error) {
console.error('Error updating leaderboard:', error);
}
}
function move(e) {
if (!gameStarted || gameOver) return;
if (e.key === 'ArrowRight' || e.key === 'd') {
player.dx = 1;
} else if (e.key === 'ArrowLeft' || e.key === 'a') {
player.dx = -1;
}
}
function stopMove(e) {
if (['ArrowRight', 'd', 'ArrowLeft', 'a'].includes(e.key)) {
player.dx = 0;
}
}
function initGame() {
score = 0;
lives = 3;
oranges = [];
gameOver = false;
gameStarted = false;
player.x = canvas.width / 2 - player.width / 2;
drawScene();
drawInitialMessage();
}
function restartGame() {
score = 0;
lives = 3;
oranges = [];
gameOver = false;
player.x = canvas.width / 2 - player.width / 2;
startCountdown();
}
function startCountdown() {
let count = 3;
const countdown = setInterval(() => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawScene();
ctx.fillStyle = '#E6F1FF';
ctx.font = '60px Poppins, sans-serif';
ctx.textAlign = 'center';
if (count > 0) {
ctx.fillText(count, canvas.width / 2, canvas.height / 2);
count--;
} else {
clearInterval(countdown);
ctx.fillText('Go!', canvas.width / 2, canvas.height / 2);
setTimeout(startGame, 500);
}
}, 1000);
}
function startGame() {
gameStarted = true;
lastTime = performance.now();
if (orangeInterval) clearInterval(orangeInterval);
orangeInterval = setInterval(createOrange, 1200);
requestAnimationFrame(update);
}
// Event Listeners
document.addEventListener('keydown', move);
document.addEventListener('keyup', stopMove);
const playButton = document.querySelector('.btn-play');
const gameArea = document.getElementById('game-area');
if (playButton && gameArea) {
playButton.addEventListener('click', (e) => {
e.preventDefault();
gameArea.scrollIntoView({ behavior: 'smooth' });
if (!gameStarted && !gameOver) {
startCountdown();
}
});
}
canvas.addEventListener('click', () => {
if (gameOver) {
restartGame();
}
});
// Initial Setup
initGame();
updateLeaderboard();
});