countdown works. normal mode feels better/faster. game-over buttons

This commit is contained in:
simon-ec
2026-07-28 10:41:43 -04:00
parent 366815d150
commit 6d3d7a0de8
+318 -115
View File
@@ -67,11 +67,21 @@
font-size: 2rem; font-size: 2rem;
} }
#title-container {
justify-self: center;
justify-content: center;
display: flex;
flex-direction: row;
width: 100%;
gap: 2rem;
}
#text-box { #text-box {
display: flex; display: flex;
flex-direction: row; flex-direction: column;
align-content: end; align-content: end;
justify-content: space-evenly; justify-content: space-evenly;
gap: 1rem;
text-align: center; text-align: center;
color: red; color: red;
width: 100%; width: 100%;
@@ -104,23 +114,26 @@
<div id="text-box"> <div id="text-box">
<!-- intro related --> <!-- intro related -->
<div style="display: flex; gap: 1rem;"> <div id="title-container">
<span id="intro1" class="hidden title">QUICK</span> <span id="title1" class="hidden title">QUICK</span>
<span id="intro2" class="hidden title">DRAW</span> <span id="title2" class="hidden title">DRAW</span>
</div> </div>
<!-- gamemode menu buttons--> <!-- gamemode menu buttons-->
<div id="game-modes" class="hidden fade"> <div id="game-modes" class="hidden fade">
<button id="normal" onclick="handleNormalMode()">normal</button> <button id="normal" onclick="handleNormalMode()">normal</button>
<button id="countdown">countdown</button> <button id="countdown" onclick="handleCountdownMode()">countdown</button>
<!-- <button id="infinite">infinite</button> --> <!-- <button id="infinite">infinite</button> -->
</div> </div>
<!-- pre-round text --> <!-- pre-round text -->
<span id="gameText" class="hidden fade"></span> <span id="game-text" class="hidden fade"></span>
<!-- post round text --> <!-- post game buttons -->
<span id="round-end" class="hidden fade"></span> <div id="game-over-buttons" class="hidden fade">
<button id="play-again" onclick="handlePlayAgain()">play again</button>
<button id="reset-game" onclick="handleReset()">back to main menu</button>
</div>
<!-- countdown text --> <!-- countdown text -->
<span id="ready" class="hidden title">READY?</span> <span id="ready" class="hidden title">READY?</span>
@@ -132,6 +145,7 @@
</div> </div>
<script> <script>
// SCREEN / SETUP VARIABLES
let gameBorderWidth = 1; let gameBorderWidth = 1;
let targetWidth = 50; let targetWidth = 50;
let targetHeight = 50; let targetHeight = 50;
@@ -150,38 +164,77 @@
let xFrameAdjustment; let xFrameAdjustment;
let yFrameAdjustment; let yFrameAdjustment;
let hovering = false; // UX HELPERS
let drawingCircle = false;
let needsResizeAdjustment = false;
let hoverCount = 0; // number of times the cursor has hovered over bullseye. used to cleanly handle round starting / cancelling // GAME STATE VARIABLES
let targetsClickedThisRound = 0;
let gamemode = ""; // can be: "normal", "countdown", "infinite" let gamemode = ""; // can be: "normal", "countdown", "infinite"
let roundmode = ""; // if gamemode is "countdown" or "infinite", can be: "rounds" or "frenzy" let countingDown = false;
let hovering = false;
let hoverCount = 0; // number of times the cursor has hovered over bullseye. used to cleanly handle round starting / cancelling
let gameIsOver = false;
let currentRound = 0; // GAMEPLAY VARIABLES
let countingDown = false; let targetsClicked = 0;
let totalTime = 0.0; // "NORMAL" MODE ONLY VARIABLES
let currentRound = 0;
let totalTime = 0.0;
let roundSeconds = 0.0;
let roundTimeStart = 0; let roundTimeStart = 0;
let roundTimeEnd = 0; let roundTimeEnd = 0;
let roundSeconds = 0.0; let bestNormalTime = 0;
let bestNormalTime = "none"; // "COUNTDOWN" MODE ONLY VARIABLES
let countdownTime = 0;
let bestCountdownScore = 0;
// menu / text elements
let titleContainer = document.getElementById("title-container");
let title1 = document.getElementById("title1");
let title2 = document.getElementById("title2");
let gameModes = document.getElementById("game-modes");
let gameText = document.getElementById("game-text");
let gameOverButtons = document.getElementById("game-over-buttons");
// text elements
let ready = document.getElementById("ready"); let ready = document.getElementById("ready");
let intro1 = document.getElementById("intro1");
let intro2 = document.getElementById("intro2");
let gameText = document.getElementById("gameText");
let one = document.getElementById("countdown1"); let one = document.getElementById("countdown1");
let two = document.getElementById("countdown2"); let two = document.getElementById("countdown2");
let three = document.getElementById("countdown3"); let three = document.getElementById("countdown3");
let draw = document.getElementById("draw"); let draw = document.getElementById("draw");
// setup initialization
window.addEventListener("load", initGame); window.addEventListener("load", initGame);
async function initGame() {
// determine screen parameters + listen for changes
setParameters();
window.addEventListener("resize", handlePageResize);
center = generateTarget(xFrameAdjustment, yFrameAdjustment);
center.id = "center";
document.body.insertAdjacentElement("afterbegin", center);
await drawTargetsInCircle(2);
if (needsResizeAdjustment) {
// auto-reset screen if page was resized during animation
handlePageResize();
}
await fadeMainMenuIn();
if (localStorage.getItem("bestNormalTime") != null) {
bestNormalTime = Number.parseFloat(localStorage.getItem("bestNormalTime"));
}
if (localStorage.getItem("bestCountdownScore") != null) {
bestCountdownScore = Number.parseInt(localStorage.getItem("bestCountdownScore"));
}
}
// helper function to determine screen-size + ratios for rendering targets
function setParameters(event) { function setParameters(event) {
pageHeight = window.innerHeight; pageHeight = window.innerHeight;
pageWidth = window.innerWidth; pageWidth = window.innerWidth;
@@ -203,45 +256,25 @@
console.log('frame: ' + frameWidth + ' x ' + frameHeight); console.log('frame: ' + frameWidth + ' x ' + frameHeight);
} }
async function initGame() { async function handlePlayAgain(e) {
setParameters(); gameIsOver = false;
center = generateTarget(xFrameAdjustment, yFrameAdjustment); // hide game over buttons
center.id = "center"; gameOverButtons.style.display = "none";
document.body.insertAdjacentElement("afterbegin", center); gameOverButtons.style.opacity = "0";
// await drawTargetsInCircle(2); // remove game text from DOM flow
await drawTargetsInCircle(false); gameText.style.display = "none";
gameText.style.opacity = "0";
// insert intro elements into document flow if (gamemode == "normal") {
intro1.style.display = "inline"; handleNormalMode(e);
intro2.style.display = "inline"; }
else if (gamemode == "countdown") {
// reveal intro elements handleCountdownMode(e);
intro1.style.opacity = "100"; }
await sleep(500); else {
intro2.style.opacity = "100"; alert("cannot play again, gamemode does not exit?");
await sleep(1500);
intro1.classList.add("fade");
intro2.classList.add("fade");
intro1.style.opacity = "0";
intro2.style.opacity = "0";
await sleep(1000);
intro1.style.display = "none";
intro2.style.display = "none";
window.addEventListener("resize", handlePageResize);
document.getElementById("game-modes").style.display = "inline";
await sleep(500);
document.getElementById("game-modes").style.opacity = "100";
if (localStorage.getItem("bestNormalTime") != null) {
bestNormalTime = Number.parseFloat(localStorage.getItem("bestNormalTime"));
} }
} }
@@ -249,42 +282,58 @@
gamemode = "normal"; gamemode = "normal";
console.log("game mode: " + gamemode); console.log("game mode: " + gamemode);
// setup mode parameters
currentRound = 1; currentRound = 1;
totalTime = 0; totalTime = 0;
center.addEventListener("mouseover", handleHoverStart); await fadeMainMenuOut();
center.addEventListener("mouseleave", handleHoverEnd);
document.getElementById("game-modes").style.opacity = "0";
await sleep(500);
document.getElementById("game-modes").style.display = "none";
intro1.style.display = "none";
intro2.style.display = "none";
// set game instruction text + fade it in
gameText.innerText = "hover on middle circle to start the countdown,\nthen click the targets as fast as possible!"; gameText.innerText = "hover on middle circle to start the countdown,\nthen click the targets as fast as possible!";
gameText.style.opacity = "0"; gameText.style.opacity = "0";
gameText.style.display = "inline"; gameText.style.display = "inline";
await sleep(500); await sleep(500);
gameText.style.opacity = "100"; gameText.style.opacity = "100";
// enable player to start first round
center.addEventListener("mouseover", handleHoverStart);
center.addEventListener("mouseleave", handleHoverEnd);
}
async function handleCountdownMode(e) {
gamemode = "countdown";
console.log("game mode: " + gamemode);
await fadeMainMenuOut();
// set game instruction text + fade it in
gameText.innerText = "hover on middle circle to start the countdown,\nthen click as many targets as possible!";
gameText.style.opacity = "0";
gameText.style.display = "inline";
await sleep(500);
gameText.style.opacity = "100";
// enable player to start game
center.addEventListener("mouseover", handleHoverStart);
center.addEventListener("mouseleave", handleHoverEnd);
} }
async function handleReset(e) { async function handleReset(e) {
gameIsOver = false;
gamemode = "";
currentRound = 0; currentRound = 0;
center.removeEventListener("mouseover", handleReset);
// hide buttons
gameOverButtons.style.display = "none";
gameOverButtons.style.opacity = "0";
// remove game text from DOM flow
gameText.style.display = "none"; gameText.style.display = "none";
gameText.style.opacity = "0"; gameText.style.opacity = "0";
intro1.style.display = "inline"; // reintroduce main menu elements into DOM flow
intro2.style.display = "inline"; await sleep(800);
intro1.style.opacity = "100";
intro2.style.opacity = "100";
document.getElementById("game-modes").style.display = "inline";
await sleep(500);
document.getElementById("game-modes").style.opacity = "100";
await drawTargetsInCircle(2); await drawTargetsInCircle(2);
await fadeMainMenuIn();
} }
async function handleHoverStart(e) { async function handleHoverStart(e) {
@@ -294,27 +343,91 @@
console.log('hover count: ' + hoverCount); console.log('hover count: ' + hoverCount);
console.log('current round: ' + currentRound); console.log('current round: ' + currentRound);
// remove all targets
frameElement.replaceChildren(); frameElement.replaceChildren();
// intro1.style.display = "none";
// intro2.style.display = "none";
gameText.style.display = "none"; gameText.style.display = "none";
if (gamemode == "normal") { if (gamemode == "normal") {
await startNormalRound(currentRound); await startNormalRound(currentRound);
} }
else if (gamemode == "countdown") {
await startCountdownMode();
}
// if (infinityMode) // if (infinityMode)
} }
async function startCountdownMode() {
// if user cancels hover countdown, don't start round
if (!await normalModeRoundCountdown(hoverCount)) {
console.log("player canceled coundown");
return;
}
// setup mode parameters
targetsClicked = 0;
tempTargetsClicked = 0;
countdownTime = 15.0;
// clear screen + render countdown / score
center.remove();
gameText.innerText = countdownTime.toFixed(3) + "s\ntargets clicked: " + targetsClicked;
gameText.style.display = "inline";
gameText.style.opacity = "100";
// begin actual game loop!
randomTargetGenerator(1);
while (countdownTime > 0.0) {
gameText.innerText = countdownTime.toFixed(3) + "s\ntargets clicked: " + targetsClicked;
// auto-reset screen if page was resized during animation
if (needsResizeAdjustment) {
handlePageResize();
}
// generate new target if last one was clicked
if (targetsClicked != tempTargetsClicked) {
randomTargetGenerator(1);
tempTargetsClicked++;
}
countdownTime = countdownTime - .01;
await sleep(10);
}
gameIsOver = true;
// ensure no targets are still on screen after timer ends
frameElement.replaceChildren();
// show end of round text. if high score, alert player and save
if (targetsClicked > bestCountdownScore || bestCountdownScore == 0) {
if (bestCountdownScore == 0) {
alert("NEW HIGHEST SCORE!!! congratulations cowboy.");
}
else {
alert("NEW HIGHEST SCORE!!! previous best: " + bestCountdownScore + " targets. congratulations cowboy.");
}
bestCountdownScore = targetsClicked;
localStorage.setItem("bestCountdownScore", bestCountdownScore);
}
gameText.innerText = "TARGETS CLICKED: " + targetsClicked +
"\nHIGH SCORE: " + bestCountdownScore +
"\nhover on circle to go back to main menu..";
// bring bullseye back to screen
center = generateTarget(xFrameAdjustment, yFrameAdjustment);
center.id = "center";
document.body.insertAdjacentElement("afterbegin", center);
gameOverButtons.style.display = "flex";
gameOverButtons.style.opacity = "100";
}
async function startNormalRound(roundNumber) { async function startNormalRound(roundNumber) {
if (roundNumber > 3) { if (roundNumber > 3) {
handleReset({}); handleReset({});
return; return;
} }
if (!await startCountdown(hoverCount)) { // if user cancels hover countdown, don't start round
// if user canceled hover, don't continue round if (!await normalModeRoundCountdown(hoverCount)) {
console.log("player canceled coundown"); console.log("player canceled coundown");
return; return;
} }
@@ -332,13 +445,13 @@
// effectively begin the round // effectively begin the round
console.log("starting round " + roundNumber); console.log("starting round " + roundNumber);
document.getElementById("center").remove(); center.remove();
targetsClickedThisRound = 0; targetsClicked = 0;
roundTimeStart = Date.now(); roundTimeStart = Date.now();
placeTarget(numTargets); randomTargetGenerator(numTargets);
while (targetsClickedThisRound < numTargets) { while (targetsClicked < numTargets) {
await sleep(10); await sleep(10);
if (targetsClickedThisRound == numTargets) break; if (targetsClicked == numTargets) break;
} }
roundTimeEnd = Date.now(); roundTimeEnd = Date.now();
roundSeconds = (roundTimeEnd - roundTimeStart) / 1000; roundSeconds = (roundTimeEnd - roundTimeStart) / 1000;
@@ -351,22 +464,29 @@
center.id = "center"; center.id = "center";
document.body.insertAdjacentElement("afterbegin", center); document.body.insertAdjacentElement("afterbegin", center);
// ensure draw isn't still lingering on screen // ensure "DRAW" isn't still lingering on screen after round
draw.style.display = "none"; draw.style.display = "none";
draw.style.opacity = "0"; draw.style.opacity = "0";
if (roundNumber == 3) { if (roundNumber == 3) {
// GAME OVER STUFF // GAME OVER STUFF
if (totalTime < bestNormalTime || bestNormalTime == "none") { gameIsOver = true;
alert("NEW FASTEST TIME!!! previous best: " + bestNormalTime + "s. congratulations cowboy."); if (totalTime < bestNormalTime || bestNormalTime == 0) {
if (bestNormalTime == 0) {
alert("NEW FASTEST TIME!!! congratulations cowboy.");
}
else {
alert("NEW FASTEST TIME!!! previous best: " + bestNormalTime.toFixed(3) + "s. congratulations cowboy.");
}
bestNormalTime = totalTime; bestNormalTime = totalTime;
localStorage.setItem("bestNormalTime", bestNormalTime); localStorage.setItem("bestNormalTime", bestNormalTime);
} }
gameText.innerText = "ROUND: " + roundSeconds.toFixed(3) + gameText.innerText = "ROUND: " + roundSeconds.toFixed(3) +
"s\nTOTAL: " + totalTime.toFixed(3) + "s\nTOTAL: " + totalTime.toFixed(3) +
"s\nBEST SCORE: " + bestNormalTime.toFixed(3) + "s\nBEST SCORE: " + bestNormalTime.toFixed(3);
"s\nhover on circle to go back to main menu..";
center.addEventListener("mouseover", handleReset); gameOverButtons.style.display = "flex";
gameOverButtons.style.opacity = "100";
} }
else { else {
console.log("ROUND: " + roundSeconds.toFixed(3) + "s\nTOTAL: " + totalTime.toFixed(3) + "\nhover on circle to start round " + currentRound); console.log("ROUND: " + roundSeconds.toFixed(3) + "s\nTOTAL: " + totalTime.toFixed(3) + "\nhover on circle to start round " + currentRound);
@@ -376,16 +496,16 @@
center.addEventListener("mouseleave", handleHoverEnd); center.addEventListener("mouseleave", handleHoverEnd);
} }
gameText.style.display = "inline"; gameText.style.display = "inline";
gameText.style.opacity = "100"; gameText.style.opacity = "100";
return; return;
} }
/**
* HELPER TO KICK-OFF "NORMAL MODE" ROUNDS
async function startCountdown(hoverNumber) { */
async function normalModeRoundCountdown(hoverNumber) {
gameText.style.display = "none"; gameText.style.display = "none";
countingDown = true; countingDown = true;
@@ -411,15 +531,15 @@
if (!hovering || hoverNumber != hoverCount) return false; if (!hovering || hoverNumber != hoverCount) return false;
three.style.opacity = "100"; three.style.opacity = "100";
await sleep(1000); await sleep(500);
if (!hovering || hoverNumber != hoverCount) return false; if (!hovering || hoverNumber != hoverCount) return false;
two.style.opacity = "100"; two.style.opacity = "100";
await sleep(1000); await sleep(500);
if (!hovering || hoverNumber != hoverCount) return false; if (!hovering || hoverNumber != hoverCount) return false;
one.style.opacity = "100"; one.style.opacity = "100";
await sleep(1000); await sleep(500);
if (!hovering || hoverNumber != hoverCount) return false; if (!hovering || hoverNumber != hoverCount) return false;
one.style.display = "none"; one.style.display = "none";
@@ -434,10 +554,12 @@
draw.style.opacity = "100"; draw.style.opacity = "100";
countingDown = false; countingDown = false;
// keep "DRAW" on screen for first second of the round
setTimeout(() => {draw.style.display = "none"; draw.style.opacity = "0"; }, 1000); setTimeout(() => {draw.style.display = "none"; draw.style.opacity = "0"; }, 1000);
return true; return true;
} }
function handleHoverEnd(e) { function handleHoverEnd(e) {
hovering = false; hovering = false;
@@ -455,46 +577,80 @@
draw.style.opacity = "0"; draw.style.opacity = "0";
if (currentRound <= 1) { if (gamemode == "normal") {
drawTargetsInCircle(); if (currentRound <= 1) {
gameText.innerText = "hover on middle circle to start the countdown,\nthen click the targets as fast as possible!"; drawTargetsInCircle(0);
gameText.innerText = "hover on middle circle to start the countdown,\nthen click the targets as fast as possible!";
}
else {
gameText.innerText = "ROUND: " + roundSeconds.toFixed(3) + "s\nTOTAL: " + totalTime.toFixed(3) + "s\nhover on circle to start round " + currentRound;
}
} }
else {
gameText.innerText = "ROUND: " + roundSeconds.toFixed(3) + "s\nTOTAL: " + totalTime.toFixed(3) + "s\nhover on circle to start round " + currentRound; else if (gamemode == "countdown") {
drawTargetsInCircle(0);
gameText.innerText = "hover on middle circle to start the countdown,\nthen click as many targets as possible!";
} }
gameText.style.display = "inline"; gameText.style.display = "inline";
countingDown = false; countingDown = false;
} }
} }
function handleTargetClick(e) { function handleTargetClick(e) {
targetsClickedThisRound++; targetsClicked++;
e.target.remove(); e.target.remove();
} }
function handlePageResize(event) { function handlePageResize(event) {
// take note if page was resized during animation or mid-countdown round to auto-reset at earliest convenience
if (!needsResizeAdjustment && (drawingCircle || countdownTime > 0)) {
needsResizeAdjustment = true; return;
}
/**
* if no game has started yet, re-draw circle
*
* no matter what, if targets on screen, do not reset, do not redraw middle circle, but do flag for reset
*
* if adding eventListener, need to redraw center circle!
*
* if we are playing normal mode:
* -- if round 1, redraw cirlce and addEventListener for hover
* -- if round 2/3, only addEventListener for hover
* -- if round 4, only addEvenListener for reset
*
* if we are playing countdown mode:
* -- if countdownTimer == 0,
* ----- need a way to know if this is before or after the game!
* ----- if before game, redraw cirlce and addEventListener for hover
* ----- if after game, only addEventListener for reset
* -- if countdownTimer > 0, flag for reset upon next target clicked, no event listeners
**/
frameElement.replaceChildren(); frameElement.replaceChildren();
setParameters(); setParameters();
document.getElementById("center").remove(); center.remove();
center = generateTarget(xFrameAdjustment, yFrameAdjustment); center = generateTarget(xFrameAdjustment, yFrameAdjustment);
center.id = "center"; center.id = "center";
document.body.insertAdjacentElement("afterbegin", center); document.body.insertAdjacentElement("afterbegin", center);
if (currentRound == 0 || (gamemode == "normal" && currentRound == 1)) { if (gamemode == "") {
drawTargetsInCircle(); drawTargetsInCircle(0);
} }
else if (gamemode != "") {
if (currentRound >= 1) {
center.addEventListener("mouseover", handleHoverStart); center.addEventListener("mouseover", handleHoverStart);
center.addEventListener("mouseleave", handleHoverEnd); center.addEventListener("mouseleave", handleHoverEnd);
} }
console.log('page: ' + pageWidth + ' x ' + pageHeight); console.log('page: ' + pageWidth + ' x ' + pageHeight);
console.log('frame: ' + frameWidth + ' x ' + frameHeight); console.log('frame: ' + frameWidth + ' x ' + frameHeight);
needsResizeAdjustment = false;
} }
async function drawTargetsInCircle(drawDurationSeconds) { async function drawTargetsInCircle(drawDurationSeconds) {
drawingCircle = true;
let r, x, y; let r, x, y;
let radians; let radians;
let target; let target;
@@ -507,8 +663,55 @@
target = generateTarget(x, y); target = generateTarget(x, y);
document.getElementById("frame").insertAdjacentElement("afterbegin", target); document.getElementById("frame").insertAdjacentElement("afterbegin", target);
if (drawDurationSeconds) await sleep((drawDurationSeconds * 1000) / 180); if (drawDurationSeconds) {
target.classList.add("fade");
await sleep((drawDurationSeconds * 1000) / 180);
}
} }
drawingCircle = false;
}
// helpers for rendering main menu stuff
async function fadeMainMenuIn() {
title1.classList.remove("fade");
title2.classList.remove("fade");
// add main menu elements to DOM flow
titleContainer.style.display = "flex";
title1.style.display = "inline";
title2.style.display = "inline";
gameModes.style.display = "inline";
// make title elements actually visible
title1.style.opacity = "100";
await sleep(500);
title2.style.opacity = "100";
await sleep(1500);
// add fade class for when elements are removed
title1.classList.add("fade");
title2.classList.add("fade");
// fade in gamemodes
await sleep(500);
gameModes.style.opacity = "100";
}
async function fadeMainMenuOut() {
let targets = document.getElementsByClassName("target");
for (let i = 0; i < targets.length; i++) {
console.log(targets[i]);
targets[i].style.opacity = 0;
};
title1.style.opacity = "0";
title2.style.opacity = "0";
gameModes.style.opacity = "0";
await sleep(500);
titleContainer.style.display = "none";
gameModes.style.display = "none";
title1.style.display = "none";
title2.style.display = "none";
} }
@@ -517,7 +720,7 @@
} }
// function to place a target somewhere random on the circle // function to place a target somewhere random on the circle
function placeTarget(numTargets) { function randomTargetGenerator(numTargets) {
let radians, radius let radians, radius
let x, y; let x, y;
let target; let target;