Auto commit: 2025-11-21T17:09:16.614Z
This commit is contained in:
parent
a7de8d3e75
commit
c7d1a28045
BIN
assets/pasted-20251121-165555-7485ce01.png
Normal file
BIN
assets/pasted-20251121-165555-7485ce01.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 325 KiB |
BIN
assets/pasted-20251121-170044-1e327a41.png
Normal file
BIN
assets/pasted-20251121-170044-1e327a41.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 251 KiB |
BIN
assets/pasted-20251121-170343-31e8648f.png
Normal file
BIN
assets/pasted-20251121-170343-31e8648f.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 188 KiB |
BIN
assets/pasted-20251121-170801-c7b6de55.png
Normal file
BIN
assets/pasted-20251121-170801-c7b6de55.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 199 KiB |
Binary file not shown.
@ -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(
|
||||||
|
|||||||
@ -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 %}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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();
|
||||||
});
|
});
|
||||||
@ -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
279
staticfiles/js/game.js
Normal 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();
|
||||||
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user