diff --git a/js/game.js b/js/game.js index ad8f6c0..bdd9ba4 100644 --- a/js/game.js +++ b/js/game.js @@ -63,6 +63,15 @@ function createInitialState() { let state = createInitialState(); let saveEligible = false; let scoreSavedThisRun = false; +let saveInFlight = false; + +const saveScoreBtnEl = saveScoreFormEl.querySelector('button[type="submit"]'); + +function setSaveFormBusy(isBusy) { + saveInFlight = isBusy; + playerNameEl.disabled = isBusy; + if (saveScoreBtnEl) saveScoreBtnEl.disabled = isBusy; +} window.addEventListener('keydown', (e) => { if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(e.key)) e.preventDefault(); @@ -81,23 +90,33 @@ restartBtnEl.addEventListener('click', () => { saveScoreFormEl.classList.add('hidden'); saveEligible = false; scoreSavedThisRun = false; + setSaveFormBusy(false); }); saveScoreFormEl.addEventListener('submit', async (e) => { e.preventDefault(); - if (!saveEligible) return; + if (!saveEligible || saveInFlight || scoreSavedThisRun) return; const name = playerNameEl.value.trim() || 'Anonymous'; - await fetch('api/scores.php', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ name, score: state.score }) - }); + setSaveFormBusy(true); + try { + const res = await fetch('api/scores.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name, score: state.score }) + }); - saveEligible = false; - scoreSavedThisRun = true; - saveScoreFormEl.classList.add('hidden'); - await populateLeaderboard(); + if (!res.ok) throw new Error('Score save failed'); + + saveEligible = false; + scoreSavedThisRun = true; + saveScoreFormEl.classList.add('hidden'); + await populateLeaderboard(); + } catch (err) { + console.error(err); + } finally { + setSaveFormBusy(false); + } }); function drawBackground() { @@ -177,7 +196,7 @@ async function populateLeaderboard() { }); const qualifies = scores.length < 10 || state.score > (scores[scores.length - 1]?.score ?? -1); - saveEligible = qualifies && !scoreSavedThisRun; + saveEligible = qualifies && !scoreSavedThisRun && !saveInFlight; saveScoreFormEl.classList.toggle('hidden', !saveEligible); } @@ -234,10 +253,23 @@ const PTERODACTYL_LANES = [ function getPterodactylHitbox(flyer) { return { - x: flyer.x + 6, - y: flyer.y + 5, - width: flyer.width - 12, - height: flyer.height - 10 + x: flyer.x + 8, + y: flyer.y + 6, + width: flyer.width - 16, + height: flyer.height - 12 + }; +} + +function getPlayerHitboxForAirHazards(player) { + const widthInset = player.ducking ? 8 : 6; + const topInset = player.ducking ? 8 : 4; + const bottomInset = 2; + + return { + x: player.x + widthInset, + y: player.y + topInset, + width: player.width - widthInset * 2, + height: player.height - topInset - bottomInset }; } @@ -258,9 +290,10 @@ function updatePterodactyls(dt) { } const p = state.player; + const playerHitbox = getPlayerHitboxForAirHazards(p); for (const flyer of state.pterodactyls) { flyer.x += flyer.vx * dt; - if (intersects(p, getPterodactylHitbox(flyer))) applyHit('Pterodactyl collision. Duck next time!'); + if (intersects(playerHitbox, getPterodactylHitbox(flyer))) applyHit('Pterodactyl collision. Duck next time!'); } state.pterodactyls = state.pterodactyls.filter((f) => f.x + f.width > state.cameraX - 150); }