Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 26 additions & 4 deletions public/bonusFood.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,38 @@ export class BonusFood extends Food {
super(gridSize, canvasSize);
this.active = false;
this.expiresAt = 0;
this.glow = 1;
this._tween = null;
}

spawn(gridSize, canvasSize, lifetimeMs = 8000) {
this.respawn(gridSize, canvasSize);
this.active = true;
this.expiresAt = performance.now() + lifetimeMs;

if (this._tween) this._tween.kill();
this.glow = 1;
this._tween = gsap.to(this, {
glow: 1.5,
duration: 0.7,
ease: 'power1.inOut',
yoyo: true,
repeat: -1
});
}

deactivate() {
this.active = false;
if (this._tween) {
this._tween.kill();
this._tween = null;
}
this.glow = 1;
}

maybeExpire(now) {
if (this.active && now >= this.expiresAt) {
this.active = false;
this.deactivate();
}
}

Expand All @@ -28,14 +49,15 @@ export class BonusFood extends Food {
const size = gridSize - 4;

// Glow
const glowSize = size * 1.2 * this.glow;
const glow = ctx.createRadialGradient(
x + size/2 + 2, y + size/2 + 2, 0,
x + size/2 + 2, y + size/2 + 2, size * 1.6
x + size/2 + 2, y + size/2 + 2, glowSize
);
glow.addColorStop(0, 'rgba(255, 215, 0, 0.35)');
glow.addColorStop(0, 'rgba(255, 215, 0, 0.45)');
glow.addColorStop(1, 'rgba(255, 215, 0, 0)');
ctx.fillStyle = glow;
ctx.fillRect(x - size/2, y - size/2, size * 2, size * 2);
ctx.fillRect(x + size/2 + 2 - glowSize, y + size/2 + 2 - glowSize, glowSize * 2, glowSize * 2);

// Main golden square
const gradient = ctx.createLinearGradient(x, y, x + size, y + size);
Expand Down
103 changes: 54 additions & 49 deletions public/game.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ class Game {
closeLeaderboardBtn.addEventListener('click', () => {
leaderboardModal.style.display = 'none';
this.isPaused = false;
requestAnimationFrame(this.gameLoop);
});

if (closeHighScoreBtn) {
Expand Down Expand Up @@ -116,6 +115,7 @@ class Game {

this.gridSize = 20;
this.isPaused = false;
this.isGameOver = false;

// Create game objects
this.snake = new Snake();
Expand All @@ -137,6 +137,7 @@ class Game {
this.interval = 200; // update interval in ms
this.accumulator = 0;
this._lastFrameTs = 0;
this._lastGameTimestamp = 0;

// Intro animation (respect reduced motion)
if (!this.shouldReduceMotion()) {
Expand Down Expand Up @@ -194,7 +195,7 @@ class Game {
this.overlay.appendChild(this.pauseLayer);

this.scheduleBonus();
requestAnimationFrame(this.gameLoop);
gsap.ticker.add(this.gameLoop);
this.drawGrid();
// Mark player as playing now (presence)
this.onlinePlayersManager.updatePlayerStatus(true);
Expand All @@ -219,34 +220,27 @@ class Game {
}
}

gameLoop(timestamp) {
if (this.isPaused) return;
gameLoop(time, deltaTime, frame) {
if (this.isPaused && !this.isGameOver) return;

if (!this.lastTime) this.lastTime = timestamp;
const delta = timestamp - this.lastTime;
this.accumulator += delta;
const frameDelta = timestamp - (this._lastFrameTs || timestamp);
this._lastFrameTs = timestamp;

if (this.accumulator >= this.interval) {
// Update game logic at a fixed interval
if (!this.isGameOver && time - this._lastGameTimestamp >= this.interval) {
this.update();
this.accumulator = 0;
this._lastGameTimestamp = time;
}

// Update particles every frame
this.particles.update(frameDelta);
this.particles.update(deltaTime);
this.draw();
this.lastTime = timestamp;
requestAnimationFrame(this.gameLoop);
}

update() {
if (this.isPaused) return;

this.snake.update();
this.snake.update(this.interval);

// Check if snake ate normal food
if (this.snake.body[0].x === this.food.position.x && this.snake.body[0].y === this.food.position.y) {
const head = this.snake.body[0];
if (head.x === this.food.position.x && head.y === this.food.position.y) {
this.snake.grow();
this.scoreManager.incrementScore();
this.food.respawn(this.gridSize, { width: this.canvas.width, height: this.canvas.height });
Expand Down Expand Up @@ -274,7 +268,7 @@ class Game {
for (let i = 0; i < 5; i++) this.snake.grow();
this.scoreManager.score += 5;
this.updateScoreUI();
this.bonusFood.active = false;
this.bonusFood.deactivate();
if (this.vibrationEnabled && navigator.vibrate) navigator.vibrate([20, 40, 20]);
this.spawnEatParticles('#ffd700');
this.showFloatingText('+5', '#ffd700');
Expand Down Expand Up @@ -310,7 +304,6 @@ class Game {
if (e.key === ' ') {
this.isPaused = !this.isPaused;
if (this.pauseLayer) this.pauseLayer.style.display = this.isPaused ? 'flex' : 'none';
if (!this.isPaused) requestAnimationFrame(this.gameLoop);
return;
}

Expand Down Expand Up @@ -343,47 +336,59 @@ class Game {
}

async gameOver() {
if (this.isGameOver) return;
this.isGameOver = true;
this.isPaused = true;
// Update player status in presence
this.onlinePlayersManager.updatePlayerStatus(false);

if (!this.shouldReduceMotion()) {
gsap.to(this.canvas, {
duration: 0.5,
opacity: 0.5,
scale: 0.95,
ease: "power2.out"
});
}
if (this.vibrationEnabled && navigator.vibrate) navigator.vibrate(120);

// If needed, save high score
if (this.scoreManager.score > 0 && this.leaderboardManager.isHighScore(this.scoreManager.score)) {
try {
const rank = await this.leaderboardManager.saveScore(
this.leaderboardManager.currentPlayer,
this.scoreManager.score
);
if (rank) {
// Show non-blocking modal instead of alert
const modal = document.getElementById('highScoreModal');
const body = document.getElementById('highScoreBody');
if (modal && body) {
body.innerHTML = `#${rank} — ${this.escapeHtml(this.leaderboardManager.currentPlayerName || 'Player')} — ${this.scoreManager.score}`;
modal.style.display = 'flex';
const proceed = async () => {
// If needed, save high score
if (this.scoreManager.score > 0 && this.leaderboardManager.isHighScore(this.scoreManager.score)) {
try {
const rank = await this.leaderboardManager.saveScore(
this.leaderboardManager.currentPlayer,
this.scoreManager.score
);
if (rank) {
// Show non-blocking modal instead of alert
const modal = document.getElementById('highScoreModal');
const body = document.getElementById('highScoreBody');
if (modal && body) {
body.innerHTML = `#${rank} — ${this.escapeHtml(this.leaderboardManager.currentPlayerName || 'Player')} — ${this.scoreManager.score}`;
modal.style.display = 'flex';
} else {
this.handleRestart();
}
} else {
this.handleRestart();
}
} else {
} catch (error) {
console.error('Failed to save score:', error);
this.handleRestart();
}
} catch (error) {
console.error('Failed to save score:', error);
this.handleRestart();
} else {
// Otherwise restart silently after a delay
setTimeout(() => this.handleRestart(), 500);
}
};

if (this.shouldReduceMotion()) {
proceed();
} else {
// Otherwise restart silently
this.handleRestart();
this.snake.flash(() => {
this.snake.breakApart();
gsap.to(this.canvas, {
duration: 0.5,
opacity: 0.5,
scale: 0.95,
ease: "power2.out",
delay: 0.5,
onComplete: proceed
});
});
}
}

Expand All @@ -393,6 +398,7 @@ class Game {
this.food.respawn(this.gridSize, { width: this.canvas.width, height: this.canvas.height });
this.interval = 200;
this.isPaused = false;
this.isGameOver = false;
this.onlinePlayersManager.updatePlayerStatus(true);
if (!this.shouldReduceMotion()) {
gsap.to(this.canvas, {
Expand All @@ -404,7 +410,6 @@ class Game {
}
document.querySelector('.score-value').textContent = '0';
document.getElementById('leaderboardModal').style.display = 'none';
requestAnimationFrame(this.gameLoop);
}

updateScoreUI() {
Expand Down
18 changes: 10 additions & 8 deletions public/particles.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ export class Particle {
this.color = options.color || 'rgba(255,100,100,1)';
this.life = 0;
this.maxLife = options.maxLife || 600; // ms
this.gravity = options.gravity || 0.0015;
this.friction = options.friction || 0.98;
this.gravity = options.gravity || 0.0015 + Math.random() * 0.001;
this.friction = options.friction || 0.97 + Math.random() * 0.02;
}

update(dt) {
Expand Down Expand Up @@ -42,16 +42,18 @@ export class ParticleSystem {
this.particles = [];
}

spawnExplosion(x, y, count = 20, color = 'rgba(255,100,100,1)') {
spawnExplosion(x, y, count = 30, color = 'rgba(255,100,100,1)') {
for (let i = 0; i < count; i++) {
const angle = (i / count) * Math.PI * 2 + Math.random() * 0.5;
const speed = 0.7 + Math.random() * 2.5;
const angle = Math.random() * Math.PI * 2;
const speed = 0.5 + Math.random() * 3.5;
const colorVariation = `rgba(${Math.floor(Math.random() * 50 + 205)},${Math.floor(Math.random() * 50 + 80)},${Math.floor(Math.random() * 50 + 80)},1)`;

this.particles.push(new Particle(x, y, {
angle,
speed,
size: 1.5 + Math.random() * 2.5,
color,
maxLife: 500 + Math.random() * 500
size: 2 + Math.random() * 3,
color: i % 2 === 0 ? color : colorVariation,
maxLife: 600 + Math.random() * 600
}));
}
}
Expand Down
Loading