diff options
| author | simon-ec <simon@eustaceconsulting.com> | 2026-07-28 10:41:43 -0400 |
|---|---|---|
| committer | simon-ec <simon@eustaceconsulting.com> | 2026-07-28 10:41:43 -0400 |
| commit | 6d3d7a0de86abf5e6a40297b052cabd8f358ba3e (patch) | |
| tree | 98d07b4e4654cebb95ab627e53999eaa34c3aca7 | |
| parent | 366815d1504bea64387364f70227ab37ea00caf8 (diff) | |
countdown works. normal mode feels better/faster. game-over buttons
| -rw-r--r-- | quickdraw-game.html | 433 |
1 files changed, 318 insertions, 115 deletions
diff --git a/quickdraw-game.html b/quickdraw-game.html index 4742d7d..a2f92c1 100644 --- a/quickdraw-game.html +++ b/quickdraw-game.html @@ -67,11 +67,21 @@ font-size: 2rem; } + #title-container { + justify-self: center; + justify-content: center; + display: flex; + flex-direction: row; + width: 100%; + gap: 2rem; + } + #text-box { display: flex; - flex-direction: row; + flex-direction: column; align-content: end; justify-content: space-evenly; + gap: 1rem; text-align: center; color: red; width: 100%; @@ -104,23 +114,26 @@ <div id="text-box"> <!-- intro related --> - <div style="display: flex; gap: 1rem;"> - <span id="intro1" class="hidden title">QUICK</span> - <span id="intro2" class="hidden title">DRAW</span> + <div id="title-container"> + <span id="title1" class="hidden title">QUICK</span> + <span id="title2" class="hidden title">DRAW</span> </div> <!-- gamemode menu buttons--> <div id="game-modes" class="hidden fade"> <button id="normal" onclick="handleNormalMode()">normal</button> - <button id="countdown">countdown</button> + <button id="countdown" onclick="handleCountdownMode()">countdown</button> <!-- <button id="infinite">infinite</button> --> </div> <!-- pre-round text --> - <span id="gameText" class="hidden fade"></span> + <span id="game-text" class="hidden fade"></span> - <!-- post round text --> - <span id="round-end" class="hidden fade"></span> + <!-- post game buttons --> + <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 --> <span id="ready" class="hidden title">READY?</span> @@ -132,6 +145,7 @@ </div> <script> + // SCREEN / SETUP VARIABLES let gameBorderWidth = 1; let targetWidth = 50; let targetHeight = 50; @@ -150,38 +164,77 @@ let xFrameAdjustment; let yFrameAdjustment; + // UX HELPERS + let drawingCircle = false; + let needsResizeAdjustment = false; + + // GAME STATE VARIABLES + let gamemode = ""; // can be: "normal", "countdown", "infinite" + 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 targetsClickedThisRound = 0; - let gamemode = ""; // can be: "normal", "countdown", "infinite" - let roundmode = ""; // if gamemode is "countdown" or "infinite", can be: "rounds" or "frenzy" + let gameIsOver = false; - let currentRound = 0; - let countingDown = false; - - let totalTime = 0.0; + // GAMEPLAY VARIABLES + let targetsClicked = 0; + + // "NORMAL" MODE ONLY VARIABLES + let currentRound = 0; + let totalTime = 0.0; + let roundSeconds = 0.0; let roundTimeStart = 0; - let roundTimeEnd = 0; - let roundSeconds = 0.0; + let roundTimeEnd = 0; + let bestNormalTime = 0; - let bestNormalTime = "none"; + // "COUNTDOWN" MODE ONLY VARIABLES + let countdownTime = 0; + let bestCountdownScore = 0; - // text elements - let ready = document.getElementById("ready"); - let intro1 = document.getElementById("intro1"); - let intro2 = document.getElementById("intro2"); - let gameText = document.getElementById("gameText"); + + // 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"); + + let ready = document.getElementById("ready"); let one = document.getElementById("countdown1"); let two = document.getElementById("countdown2"); let three = document.getElementById("countdown3"); - let draw = document.getElementById("draw"); + // setup initialization 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) { pageHeight = window.innerHeight; pageWidth = window.innerWidth; @@ -203,45 +256,25 @@ console.log('frame: ' + frameWidth + ' x ' + frameHeight); } - async function initGame() { - setParameters(); - - center = generateTarget(xFrameAdjustment, yFrameAdjustment); - center.id = "center"; - document.body.insertAdjacentElement("afterbegin", center); - - // await drawTargetsInCircle(2); - await drawTargetsInCircle(false); - - // insert intro elements into document flow - intro1.style.display = "inline"; - intro2.style.display = "inline"; - - // reveal intro elements - intro1.style.opacity = "100"; - await sleep(500); - intro2.style.opacity = "100"; - await sleep(1500); + async function handlePlayAgain(e) { + gameIsOver = false; - intro1.classList.add("fade"); - intro2.classList.add("fade"); + // hide game over buttons + gameOverButtons.style.display = "none"; + gameOverButtons.style.opacity = "0"; - 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"; + // remove game text from DOM flow + gameText.style.display = "none"; + gameText.style.opacity = "0"; - if (localStorage.getItem("bestNormalTime") != null) { - bestNormalTime = Number.parseFloat(localStorage.getItem("bestNormalTime")); + if (gamemode == "normal") { + handleNormalMode(e); + } + else if (gamemode == "countdown") { + handleCountdownMode(e); + } + else { + alert("cannot play again, gamemode does not exit?"); } } @@ -249,42 +282,58 @@ gamemode = "normal"; console.log("game mode: " + gamemode); + // setup mode parameters currentRound = 1; totalTime = 0; + await fadeMainMenuOut(); + + // 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.style.opacity = "0"; + gameText.style.display = "inline"; + await sleep(500); + gameText.style.opacity = "100"; + + // enable player to start first round center.addEventListener("mouseover", handleHoverStart); center.addEventListener("mouseleave", handleHoverEnd); + } - document.getElementById("game-modes").style.opacity = "0"; - await sleep(500); - document.getElementById("game-modes").style.display = "none"; + async function handleCountdownMode(e) { + gamemode = "countdown"; + console.log("game mode: " + gamemode); - intro1.style.display = "none"; - intro2.style.display = "none"; - - gameText.innerText = "hover on middle circle to start the countdown,\nthen click the targets as fast as possible!"; + 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) { + gameIsOver = false; + gamemode = ""; 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.opacity = "0"; - intro1.style.display = "inline"; - intro2.style.display = "inline"; - 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"; + // reintroduce main menu elements into DOM flow + await sleep(800); await drawTargetsInCircle(2); + await fadeMainMenuIn(); } async function handleHoverStart(e) { @@ -294,27 +343,91 @@ console.log('hover count: ' + hoverCount); console.log('current round: ' + currentRound); + // remove all targets frameElement.replaceChildren(); - - // intro1.style.display = "none"; - // intro2.style.display = "none"; gameText.style.display = "none"; if (gamemode == "normal") { await startNormalRound(currentRound); } + else if (gamemode == "countdown") { + await startCountdownMode(); + } // 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) { if (roundNumber > 3) { handleReset({}); return; } - if (!await startCountdown(hoverCount)) { - // if user canceled hover, don't continue round + // if user cancels hover countdown, don't start round + if (!await normalModeRoundCountdown(hoverCount)) { console.log("player canceled coundown"); return; } @@ -332,13 +445,13 @@ // effectively begin the round console.log("starting round " + roundNumber); - document.getElementById("center").remove(); - targetsClickedThisRound = 0; + center.remove(); + targetsClicked = 0; roundTimeStart = Date.now(); - placeTarget(numTargets); - while (targetsClickedThisRound < numTargets) { + randomTargetGenerator(numTargets); + while (targetsClicked < numTargets) { await sleep(10); - if (targetsClickedThisRound == numTargets) break; + if (targetsClicked == numTargets) break; } roundTimeEnd = Date.now(); roundSeconds = (roundTimeEnd - roundTimeStart) / 1000; @@ -351,22 +464,29 @@ center.id = "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.opacity = "0"; if (roundNumber == 3) { // GAME OVER STUFF - if (totalTime < bestNormalTime || bestNormalTime == "none") { - alert("NEW FASTEST TIME!!! previous best: " + bestNormalTime + "s. congratulations cowboy."); + gameIsOver = true; + 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; localStorage.setItem("bestNormalTime", bestNormalTime); } gameText.innerText = "ROUND: " + roundSeconds.toFixed(3) + "s\nTOTAL: " + totalTime.toFixed(3) + - "s\nBEST SCORE: " + bestNormalTime.toFixed(3) + - "s\nhover on circle to go back to main menu.."; - center.addEventListener("mouseover", handleReset); + "s\nBEST SCORE: " + bestNormalTime.toFixed(3); + + gameOverButtons.style.display = "flex"; + gameOverButtons.style.opacity = "100"; } else { console.log("ROUND: " + roundSeconds.toFixed(3) + "s\nTOTAL: " + totalTime.toFixed(3) + "\nhover on circle to start round " + currentRound); @@ -375,7 +495,6 @@ center.addEventListener("mouseover", handleHoverStart); center.addEventListener("mouseleave", handleHoverEnd); } - gameText.style.display = "inline"; gameText.style.opacity = "100"; @@ -383,9 +502,10 @@ return; } - - - async function startCountdown(hoverNumber) { + /** + * HELPER TO KICK-OFF "NORMAL MODE" ROUNDS + */ + async function normalModeRoundCountdown(hoverNumber) { gameText.style.display = "none"; countingDown = true; @@ -411,15 +531,15 @@ if (!hovering || hoverNumber != hoverCount) return false; three.style.opacity = "100"; - await sleep(1000); + await sleep(500); if (!hovering || hoverNumber != hoverCount) return false; two.style.opacity = "100"; - await sleep(1000); + await sleep(500); if (!hovering || hoverNumber != hoverCount) return false; one.style.opacity = "100"; - await sleep(1000); + await sleep(500); if (!hovering || hoverNumber != hoverCount) return false; one.style.display = "none"; @@ -434,10 +554,12 @@ draw.style.opacity = "100"; countingDown = false; + // keep "DRAW" on screen for first second of the round setTimeout(() => {draw.style.display = "none"; draw.style.opacity = "0"; }, 1000); return true; } + function handleHoverEnd(e) { hovering = false; @@ -454,47 +576,81 @@ three.style.opacity = "0"; draw.style.opacity = "0"; - - if (currentRound <= 1) { - drawTargetsInCircle(); - gameText.innerText = "hover on middle circle to start the countdown,\nthen click the targets as fast as possible!"; + + if (gamemode == "normal") { + if (currentRound <= 1) { + 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"; countingDown = false; } } function handleTargetClick(e) { - targetsClickedThisRound++; + targetsClicked++; e.target.remove(); } 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(); setParameters(); - document.getElementById("center").remove(); + center.remove(); center = generateTarget(xFrameAdjustment, yFrameAdjustment); center.id = "center"; document.body.insertAdjacentElement("afterbegin", center); - if (currentRound == 0 || (gamemode == "normal" && currentRound == 1)) { - drawTargetsInCircle(); + if (gamemode == "") { + drawTargetsInCircle(0); } - - if (currentRound >= 1) { + else if (gamemode != "") { center.addEventListener("mouseover", handleHoverStart); center.addEventListener("mouseleave", handleHoverEnd); } console.log('page: ' + pageWidth + ' x ' + pageHeight); console.log('frame: ' + frameWidth + ' x ' + frameHeight); + needsResizeAdjustment = false; } async function drawTargetsInCircle(drawDurationSeconds) { + drawingCircle = true; let r, x, y; let radians; let target; @@ -507,8 +663,55 @@ target = generateTarget(x, y); 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 placeTarget(numTargets) { + function randomTargetGenerator(numTargets) { let radians, radius let x, y; let target; |
