998 lines
36 KiB
HTML
998 lines
36 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
|
|
<style>
|
|
* {
|
|
box-sizing: border-box;
|
|
margin: 0;
|
|
padding: 0;
|
|
font-family: sans-serif;
|
|
}
|
|
:root {
|
|
--bg: #161616;
|
|
}
|
|
html {
|
|
height: 100vh;
|
|
width: 100vw;
|
|
background-color: var(--bg);
|
|
font-size: 24px;
|
|
overflow-x: hidden;
|
|
}
|
|
body {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
height: 100vh;
|
|
width: 100vw;
|
|
padding: 1rem;
|
|
}
|
|
#target-frame {
|
|
width: 60%;
|
|
height: 75%;
|
|
/** border: 1px solid white; */
|
|
}
|
|
#center {
|
|
height: 50px;
|
|
width: 50px;
|
|
background-color: red;
|
|
/** border: 5px solid red; */
|
|
border-radius: 50%;
|
|
position: absolute;
|
|
border-radius: 50%;
|
|
}
|
|
|
|
.target {
|
|
height: 50px;
|
|
width: 50px;
|
|
background-color: var(--bg);
|
|
border: 10px solid red;
|
|
position: absolute;
|
|
border-radius: 50%;
|
|
}
|
|
.target:active {
|
|
background-color: orange;
|
|
}
|
|
.target::after {
|
|
position: absolute;
|
|
background-color: red;
|
|
border-radius: 2rem;
|
|
height: 10px;
|
|
width: 10px;
|
|
top: 10px;
|
|
left: 10px;
|
|
content: "";
|
|
}
|
|
|
|
.red-dot {
|
|
height: 50px;
|
|
width: 50px;
|
|
background-color: red;
|
|
position: absolute;
|
|
border-radius: 50%;
|
|
}
|
|
|
|
.hidden {
|
|
display: none;
|
|
opacity: 0;
|
|
}
|
|
.fade {
|
|
transition: opacity 1s;
|
|
}
|
|
|
|
.title {
|
|
font-weight: bold;
|
|
font-size: 2rem;
|
|
}
|
|
|
|
#title-container {
|
|
justify-self: center;
|
|
justify-content: center;
|
|
flex-direction: row;
|
|
width: 100%;
|
|
gap: 1rem;
|
|
}
|
|
|
|
#text-frame {
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: flex-end;
|
|
gap: 1rem;
|
|
|
|
min-height: 15%;
|
|
width: 100%;
|
|
|
|
color: red;
|
|
text-align: center;
|
|
line-height: 1rem;
|
|
|
|
margin-top: 2rem;
|
|
}
|
|
|
|
#game-modes {
|
|
}
|
|
|
|
.button-container {
|
|
/** display: flex; */
|
|
align-content: center;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 100%;
|
|
gap: 1rem;
|
|
}
|
|
|
|
#game-over-buttons {
|
|
gap: 2rem;
|
|
}
|
|
|
|
button {
|
|
background-color: var(--bg);
|
|
color: red;
|
|
padding: 12px 12px;
|
|
border: 2px solid red;
|
|
border-radius: 1rem;
|
|
font-size: .75rem;
|
|
font-weight: bold;
|
|
}
|
|
button:hover {
|
|
cursor: pointer;
|
|
text-decoration: underline;
|
|
}
|
|
button:active {
|
|
background-color: red;
|
|
color: var(--bg);
|
|
font-style: italic;
|
|
}
|
|
|
|
@media screen and (max-width: 1024px) {
|
|
html {
|
|
font-size: 32px;
|
|
overflow: hidden;
|
|
}
|
|
body {
|
|
padding: 1rem .5rem;
|
|
max-height: 80vh;
|
|
}
|
|
#target-frame {
|
|
width: 90%;
|
|
}
|
|
#text-frame {
|
|
margin-top: 3rem;
|
|
}
|
|
#game-modes {
|
|
gap: .5rem;
|
|
}
|
|
#normal-sub-modes, #countdown-sub-modes, #game-over-buttons {
|
|
flex-direction: column;
|
|
gap: .5rem;
|
|
}
|
|
#game-text {
|
|
font-size: .8rem;
|
|
}
|
|
.title { font-size: 1.5rem; }
|
|
button { font-size: .75rem; }
|
|
}
|
|
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<!-- where the targets + bullseye are drawn -->
|
|
<div id="target-frame">
|
|
</div>
|
|
|
|
<!-- where we place text + buttons -->
|
|
<div id="text-frame">
|
|
|
|
<!-- intro related -->
|
|
<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="button-container hidden fade">
|
|
<button id="normal-mode">normal</button>
|
|
<button id="countdown-mode">countdown</button>
|
|
<!-- <button id="infinite">infinite</button> -->
|
|
</div>
|
|
|
|
<!-- normal mode sub-modes -->
|
|
<div id="normal-sub-modes" class="button-container hidden fade">
|
|
<button id="normal-3-rounds">3 rounds</button>
|
|
<button id="normal-6-rounds">6 rounds</button>
|
|
<button id="normal-10-rounds">10 rounds</button>
|
|
</div>
|
|
|
|
<!-- countdown mode sub-modes -->
|
|
<div id="countdown-sub-modes" class="button-container hidden fade">
|
|
<button id="countdown-15s">15 seconds</button>
|
|
<button id="countdown-30s">30 seconds</button>
|
|
<button id="countdown-60s">60 seconds</button>
|
|
</div>
|
|
|
|
<!-- pre-round text -->
|
|
<span id="game-text" class="hidden fade"></span>
|
|
|
|
<!-- post game buttons -->
|
|
<div id="game-over-buttons" class="button-container hidden fade">
|
|
<button id="play-again">play again</button>
|
|
<button id="reset-game">back to main menu</button>
|
|
</div>
|
|
|
|
<!-- countdown text -->
|
|
<span id="ready" class="hidden title">READY?</span>
|
|
<span id="countdown3" class="hidden title">3</span>
|
|
<span id="countdown2" class="hidden title">2</span>
|
|
<span id="countdown1" class="hidden title">1</span>
|
|
<span id="draw" class="hidden title">DRAW!</span>
|
|
|
|
</div>
|
|
|
|
<script>
|
|
// SCREEN / SETUP VARIABLES
|
|
let gameBorderWidth = 1;
|
|
let targetWidth = 50;
|
|
let targetHeight = 50;
|
|
|
|
let pageHeight;
|
|
let pageWidth;
|
|
|
|
let targetFrame;
|
|
let frameRect;
|
|
let frameHeight;
|
|
let frameWidth;
|
|
|
|
let center;
|
|
let ratio;
|
|
|
|
let xFrameAdjustment;
|
|
let yFrameAdjustment;
|
|
|
|
// UX HELPERS
|
|
let drawingCircle = false;
|
|
let needsResizeAdjustment = false;
|
|
|
|
// GAME STATE VARIABLES
|
|
let gamemode = ""; // can be: "normal" or "countdown"
|
|
let submode = "";
|
|
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
|
|
|
|
// GAMEPLAY VARIABLES
|
|
let highScore = 0;
|
|
let targetsClicked = 0;
|
|
let numTargets = 0;
|
|
let preRoundText = "hold still on the circle, then click targets as fast as possible!";
|
|
|
|
// "NORMAL" MODE ONLY VARIABLES
|
|
let currentRound = 0;
|
|
let maxNormalRounds = 0;
|
|
let totalTime = 0.0;
|
|
let roundSeconds = 0.0;
|
|
let roundTimeStart = 0;
|
|
let roundTimeEnd = 0;
|
|
let normalBestTimes = {};
|
|
let normalBullseyeNeeded = false;
|
|
|
|
// "COUNTDOWN" MODE ONLY VARIABLES
|
|
let countdownBullseyeNeeded = false;
|
|
let countdownTime = 0;
|
|
let countdownHighScores = {};
|
|
|
|
|
|
// 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 normalSubModes = document.getElementById("normal-sub-modes");
|
|
let countdownSubModes = document.getElementById("countdown-sub-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() {
|
|
// begin listening for changes
|
|
addEventListeners();
|
|
|
|
// determine screen parameters
|
|
setParameters();
|
|
|
|
// draw into animation
|
|
center = generateTarget(xFrameAdjustment, yFrameAdjustment);
|
|
center.id = "center";
|
|
document.body.insertAdjacentElement("afterbegin", center);
|
|
await drawCircle(2);
|
|
if (needsResizeAdjustment) {
|
|
// auto-reset screen if page was resized during animation
|
|
handlePageResize();
|
|
}
|
|
|
|
await fadeMainMenuIn();
|
|
|
|
// load saved high scores
|
|
if (localStorage.getItem("normalBestTimes") != null) {
|
|
normalBestTimes = JSON.parse(localStorage.getItem("normalBestTimes"));
|
|
}
|
|
if (localStorage.getItem("countdownHighScores") != null) {
|
|
countdownHighScores = JSON.parse(localStorage.getItem("countdownHighScores"));
|
|
}
|
|
}
|
|
|
|
function addEventListeners() {
|
|
window.addEventListener("resize", handlePageResize);
|
|
|
|
// prevent mobile browsers from tweaking when pressing+holding on mobile
|
|
document.addEventListener('touchstart', (e) => {e.preventDefault()}, {passive:false});
|
|
document.addEventListener('touchmove', (e) => {e.preventDefault()}, {passive:false});
|
|
document.addEventListener('touchend', (e) => {e.preventDefault()}, {passive:false});
|
|
document.addEventListener('touchcancel', (e) => {e.preventDefault()}, {passive:false});
|
|
|
|
// set each element's funtion.
|
|
// normal mode buttons
|
|
document.getElementById("normal-mode").addEventListener('click', (e) => {
|
|
e.preventDefault(); handleNormalMode();
|
|
}, { passive: false });
|
|
document.getElementById("normal-mode").addEventListener('touchend', (e) => {
|
|
e.preventDefault(); handleNormalMode();
|
|
}, { passive: false });
|
|
document.getElementById("normal-3-rounds").addEventListener('click', (e) => {
|
|
e.preventDefault(); setupNormalMode(3);
|
|
}, { passive: false });
|
|
document.getElementById("normal-3-rounds").addEventListener('touchend', (e) => {
|
|
e.preventDefault(); setupNormalMode(3);
|
|
}, { passive: false });
|
|
document.getElementById("normal-6-rounds").addEventListener('click', (e) => {
|
|
e.preventDefault(); setupNormalMode(6);
|
|
}, { passive: false });
|
|
document.getElementById("normal-6-rounds").addEventListener('touchend', (e) => {
|
|
e.preventDefault(); setupNormalMode(6);
|
|
}, { passive: false });
|
|
document.getElementById("normal-10-rounds").addEventListener('click', (e) => {
|
|
e.preventDefault(); setupNormalMode(10);
|
|
}, { passive: false });
|
|
document.getElementById("normal-10-rounds").addEventListener('touchend', (e) => {
|
|
e.preventDefault(); setupNormalMode(10);
|
|
}, { passive: false });
|
|
|
|
// countdown mode buttons
|
|
document.getElementById("countdown-mode").addEventListener('click', (e) => {
|
|
e.preventDefault(); handleCountdownMode();
|
|
}, { passive: false });
|
|
document.getElementById("countdown-mode").addEventListener('touchend', (e) => {
|
|
e.preventDefault(); handleCountdownMode();
|
|
}, { passive: false });
|
|
document.getElementById("countdown-15s").addEventListener('click', (e) => {
|
|
e.preventDefault(); setupCountdownMode(15);
|
|
}, { passive: false });
|
|
document.getElementById("countdown-15s").addEventListener('touchend', (e) => {
|
|
e.preventDefault(); setupCountdownMode(15);
|
|
}, { passive: false });
|
|
document.getElementById("countdown-30s").addEventListener('click', (e) => {
|
|
e.preventDefault(); setupCountdownMode(30);
|
|
}, { passive: false });
|
|
document.getElementById("countdown-30s").addEventListener('touchend', (e) => {
|
|
e.preventDefault(); setupCountdownMode(30);
|
|
}, { passive: false });
|
|
document.getElementById("countdown-60s").addEventListener('click', (e) => {
|
|
e.preventDefault(); setupCountdownMode(60);
|
|
}, { passive: false });
|
|
document.getElementById("countdown-60s").addEventListener('touchend', (e) => {
|
|
e.preventDefault(); setupCountdownMode(60);
|
|
}, { passive: false });
|
|
|
|
// gameover menu buttons
|
|
document.getElementById("play-again").addEventListener('click', (e) => {
|
|
e.preventDefault(); handlePlayAgain();
|
|
}, { passive: false });
|
|
document.getElementById("play-again").addEventListener('touchend', (e) => {
|
|
e.preventDefault(); handlePlayAgain();
|
|
}, { passive: false });
|
|
document.getElementById("reset-game").addEventListener('click', (e) => {
|
|
e.preventDefault(); handleReset();
|
|
}, { passive: false });
|
|
document.getElementById("reset-game").addEventListener('touchend', (e) => {
|
|
e.preventDefault(); handleReset();
|
|
}, { passive: false });
|
|
}
|
|
|
|
// helper function to determine screen-size + ratios for rendering targets
|
|
function setParameters(event) {
|
|
pageHeight = window.innerHeight;
|
|
pageWidth = window.innerWidth;
|
|
|
|
targetFrame = document.getElementById("target-frame");
|
|
frameRect = targetFrame.getBoundingClientRect();
|
|
frameHeight = targetFrame.offsetHeight;
|
|
frameWidth = targetFrame.offsetWidth;
|
|
|
|
// ratio = (frameHeight - (gameBorderWidth*2)) / (frameWidth - (gameBorderWidth*2));
|
|
ratio = frameHeight / frameWidth;
|
|
|
|
// adjustments to find center of frame in absolute coords (for origin of target cirlce)
|
|
xFrameAdjustment = ((frameWidth / 2) + frameRect.left) - (targetWidth / 2);
|
|
yFrameAdjustment = ((frameHeight / 2) + frameRect.top) - (targetWidth / 2);
|
|
|
|
// init logs:
|
|
console.log('page: ' + pageWidth + ' x ' + pageHeight);
|
|
console.log('frame: ' + frameWidth + ' x ' + frameHeight);
|
|
}
|
|
|
|
async function handleNormalMode(e) {
|
|
gamemode = "normal";
|
|
normalBullseyeNeeded = true;
|
|
|
|
await fadeMainMenuOut();
|
|
|
|
normalSubModes.style.display = "flex";
|
|
normalSubModes.style.opacity = "100";
|
|
}
|
|
|
|
async function setupNormalMode(numRounds) {
|
|
// fade buttons out
|
|
normalSubModes.style.opacity = "0";
|
|
sleep(500);
|
|
normalSubModes.style.display = "none";
|
|
|
|
// ensure mode parameters are [re]set
|
|
currentRound = 1;
|
|
maxNormalRounds = numRounds;
|
|
totalTime = 0;
|
|
|
|
submode = numRounds;
|
|
if (Object.keys(normalBestTimes).indexOf(submode.toString()) == -1) {
|
|
highScore = 0;
|
|
}
|
|
else {
|
|
highScore = normalBestTimes[submode];
|
|
}
|
|
|
|
// set game instruction text + fade it in
|
|
gameText.innerText = preRoundText;
|
|
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, {passive:false});
|
|
center.addEventListener("touchstart", handleHoverStart, {passive:false});
|
|
center.addEventListener("mouseleave", handleHoverEnd, {passive:false});
|
|
center.addEventListener("touchend", handleHoverEnd, {passive:false});
|
|
center.addEventListener("touchmove", handleHoverEnd, {passive:false});
|
|
center.addEventListener("touchcancel", handleHoverEnd, {passive:false});
|
|
}
|
|
|
|
async function startNormalRound(roundNumber) {
|
|
if (roundNumber > maxNormalRounds) {
|
|
handleReset({});
|
|
return;
|
|
}
|
|
|
|
// if user cancels hover countdown, don't start round
|
|
if (!await drawCountdown(hoverCount)) {
|
|
console.log("player canceled coundown");
|
|
return;
|
|
}
|
|
|
|
targetsClicked = 0;
|
|
if (roundNumber == maxNormalRounds) {
|
|
numTargets = roundNumber * 2;
|
|
}
|
|
else {
|
|
numTargets = roundNumber;
|
|
}
|
|
|
|
// effectively begin the round
|
|
console.log("starting round " + roundNumber);
|
|
center.remove();
|
|
normalBullseyeNeeded = false;
|
|
|
|
roundTimeStart = Date.now();
|
|
|
|
randomTargetGenerator(numTargets);
|
|
while (targetsClicked < numTargets) {
|
|
await sleep(10);
|
|
if (targetsClicked == numTargets) break;
|
|
}
|
|
|
|
roundTimeEnd = Date.now();
|
|
roundSeconds = (roundTimeEnd - roundTimeStart) / 1000;
|
|
totalTime += roundSeconds;
|
|
|
|
// auto-reset bullseye if page gets resized at all
|
|
if (needsResizeAdjustment) {
|
|
setParameters();
|
|
}
|
|
|
|
// ensure "DRAW" isn't still lingering on screen after round
|
|
draw.style.display = "none";
|
|
draw.style.opacity = "0";
|
|
|
|
// GAME OVER STUFF
|
|
if (roundNumber == maxNormalRounds) {
|
|
if (totalTime < highScore || highScore == 0) {
|
|
if (highScore == 0) {
|
|
alert("high score set!");
|
|
}
|
|
else {
|
|
alert("NEW FASTEST TIME!!! previous best: " + highScore.toFixed(3) + "s. congratulations cowboy.");
|
|
}
|
|
highScore = totalTime;
|
|
normalBestTimes[submode] = highScore;
|
|
localStorage.setItem("normalBestTimes", JSON.stringify(normalBestTimes));
|
|
}
|
|
|
|
gameText.innerText = "ROUND: " + roundSeconds.toFixed(3) +
|
|
"s\nTOTAL: " + totalTime.toFixed(3) +
|
|
"s\nBEST " + submode.toString() + "-ROUND TIME: " + highScore.toFixed(3) + "s";
|
|
|
|
numTargets = 0;
|
|
targetsClicked =0;
|
|
|
|
gameOverButtons.style.display = "flex";
|
|
gameOverButtons.style.opacity = "100";
|
|
}
|
|
// NORMAL END OF ROUND STUFF
|
|
else {
|
|
center = generateTarget(xFrameAdjustment, yFrameAdjustment);
|
|
center.id = "center";
|
|
document.body.insertAdjacentElement("afterbegin", center);
|
|
|
|
console.log("ROUND: " + roundSeconds.toFixed(3) + "s\nTOTAL: " + totalTime.toFixed(3) + "\nhover on circle to start round " + currentRound);
|
|
gameText.innerText = "ROUND: " + roundSeconds.toFixed(3) + "s\nTOTAL: " + totalTime.toFixed(3) + "s\nhover on circle to start round " + currentRound;
|
|
|
|
center.addEventListener("mouseover", handleHoverStart, {passive:false});
|
|
center.addEventListener("touchstart", handleHoverStart, {passive:false});
|
|
center.addEventListener("mouseleave", handleHoverEnd, {passive:false});
|
|
center.addEventListener("touchend", handleHoverEnd, {passive:false});
|
|
center.addEventListener("touchmove", handleHoverEnd, {passive:false});
|
|
center.addEventListener("touchcancel", handleHoverEnd, {passive:false});
|
|
|
|
normalBullseyeNeeded = true;
|
|
currentRound++;
|
|
}
|
|
|
|
gameText.style.display = "inline";
|
|
gameText.style.opacity = "100";
|
|
|
|
return;
|
|
}
|
|
|
|
async function setupCountdownMode(gameDuration) {
|
|
// fade buttons out
|
|
countdownSubModes.style.opacity = "0";
|
|
sleep(500);
|
|
countdownSubModes.style.display = "none";
|
|
|
|
// [re]set game parameters
|
|
targetsClicked = 0;
|
|
numTargets = 0;
|
|
countdownTime = gameDuration;
|
|
|
|
submode = gameDuration;
|
|
if (Object.keys(countdownHighScores).indexOf(submode.toString()) == -1) {
|
|
highScore = 0;
|
|
}
|
|
else {
|
|
highScore = countdownHighScores[submode];
|
|
}
|
|
|
|
// set game instruction text + fade it in
|
|
gameText.innerText = preRoundText;
|
|
gameText.style.opacity = "0";
|
|
gameText.style.display = "inline";
|
|
await sleep(500);
|
|
gameText.style.opacity = "100";
|
|
|
|
// enable player to start game
|
|
center.addEventListener("mouseover", handleHoverStart, {passive:false});
|
|
center.addEventListener("touchstart", handleHoverStart, {passive:false});
|
|
center.addEventListener("mouseleave", handleHoverEnd, {passive:false});
|
|
center.addEventListener("touchend", handleHoverEnd, {passive:false});
|
|
center.addEventListener("touchmove", handleHoverEnd, {passive:false});
|
|
center.addEventListener("touchcancel", handleHoverEnd, {passive:false});
|
|
}
|
|
|
|
async function handleCountdownMode(e) {
|
|
gamemode = "countdown";
|
|
countdownBullseyeNeeded = true;
|
|
|
|
await fadeMainMenuOut();
|
|
|
|
countdownSubModes.style.display = "flex";
|
|
countdownSubModes.style.opacity = "100";
|
|
}
|
|
|
|
async function handleHoverStart(e) {
|
|
e.preventDefault();
|
|
hovering = true;
|
|
hoverCount++;
|
|
|
|
console.log('hover count: ' + hoverCount);
|
|
console.log('current round: ' + currentRound);
|
|
|
|
// remove all targets
|
|
targetFrame.replaceChildren();
|
|
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 drawCountdown(hoverCount)) {
|
|
console.log("player canceled coundown");
|
|
return;
|
|
}
|
|
|
|
// clear screen + render countdown / score
|
|
center.remove();
|
|
gameText.innerText = countdownTime.toFixed(3) + "s\ntargets clicked: " + targetsClicked;
|
|
|
|
// delay reveal of score until after "DRAW" fades
|
|
setTimeout(() => {
|
|
gameText.style.display = "inline";
|
|
gameText.style.opacity = "100";}, 1000);
|
|
|
|
// begin actual game loop!
|
|
randomTargetGenerator(1);
|
|
numTargets++;
|
|
countdownBullseyeNeeded = false;
|
|
while (countdownTime > 0.0) {
|
|
gameText.innerText = countdownTime.toFixed(2) + "s\ntargets clicked: " + targetsClicked;
|
|
// auto-reset target spawn paremeters if page was resized
|
|
if (needsResizeAdjustment) {
|
|
handlePageResize();
|
|
}
|
|
// generate new target if last one was clicked
|
|
if (targetsClicked == numTargets) {
|
|
randomTargetGenerator(1);
|
|
numTargets++;
|
|
}
|
|
countdownTime = countdownTime - .01;
|
|
await sleep(10);
|
|
}
|
|
|
|
// ensure no targets are still on screen after timer ends
|
|
targetFrame.replaceChildren();
|
|
|
|
// show end of round text. if high score, alert player and save
|
|
if (targetsClicked > highScore || highScore == 0) {
|
|
if (targetsClicked == 0) {
|
|
alert("NEW HIGHEST SCORE!!! congratulations cowboy.");
|
|
}
|
|
else {
|
|
alert("NEW HIGHEST SCORE!!! previous best: " + highScore + " targets. congratulations cowboy.");
|
|
}
|
|
highScore = targetsClicked;
|
|
countdownHighScores[submode] = highScore;
|
|
localStorage.setItem("countdownHighScores", JSON.stringify(countdownHighScores));
|
|
}
|
|
gameText.innerText = "TARGETS CLICKED: " + targetsClicked +
|
|
"\n" + submode.toString() + "-SECOND HIGH SCORE: " + highScore;
|
|
|
|
numTargets = 0;
|
|
targetsClicked =0;
|
|
|
|
gameOverButtons.style.display = "flex";
|
|
gameOverButtons.style.opacity = "100";
|
|
}
|
|
|
|
|
|
|
|
/** HELPER TO KICK-OFF COUNTDOWNS FROM HOVER */
|
|
// this prevents countdown from continuing if user removes / reapplies hover while we're sleeping
|
|
function cancelCountdown(hoverNumber) {
|
|
return !hovering || (hoverNumber != hoverCount);
|
|
}
|
|
|
|
async function drawCountdown(hoverNumber) {
|
|
gameText.style.display = "none";
|
|
countingDown = true;
|
|
|
|
if (cancelCountdown(hoverNumber)) return false;
|
|
ready.style.display = "inline";
|
|
ready.style.opacity = "100";
|
|
await sleep(1000);
|
|
|
|
if (cancelCountdown(hoverNumber)) return false;
|
|
ready.style.display = "none";
|
|
ready.style.opacity = "0";
|
|
|
|
if (cancelCountdown(hoverNumber)) return false;
|
|
one.style.opacity = "0";
|
|
two.style.opacity = "0";
|
|
three.style.opacity = "0";
|
|
|
|
if (cancelCountdown(hoverNumber)) return false;
|
|
one.style.display = "inline";
|
|
two.style.display = "inline";
|
|
three.style.display = "inline";
|
|
|
|
if (cancelCountdown(hoverNumber)) return false;
|
|
three.style.opacity = "100";
|
|
await sleep(500);
|
|
|
|
if (cancelCountdown(hoverNumber)) return false;
|
|
two.style.opacity = "100";
|
|
await sleep(500);
|
|
|
|
if (cancelCountdown(hoverNumber)) return false;
|
|
one.style.opacity = "100";
|
|
await sleep(500);
|
|
|
|
if (cancelCountdown(hoverNumber)) return false;
|
|
one.style.display = "none";
|
|
two.style.display = "none";
|
|
three.style.display = "none";
|
|
|
|
one.style.opacity = "0";
|
|
two.style.opacity = "0";
|
|
three.style.opacity = "0";
|
|
|
|
draw.style.display = "inline";
|
|
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;
|
|
|
|
if (countingDown) {
|
|
ready.style.display = "none";
|
|
one.style.display = "none";
|
|
two.style.display = "none";
|
|
three.style.display = "none";
|
|
draw.style.display = "none";
|
|
|
|
ready.style.opacity = "0";
|
|
one.style.opacity = "0";
|
|
two.style.opacity = "0";
|
|
three.style.opacity = "0";
|
|
|
|
draw.style.opacity = "0";
|
|
|
|
if (gamemode == "normal") {
|
|
if (currentRound == 1) {
|
|
gameText.innerText = preRoundText;
|
|
}
|
|
else {
|
|
gameText.innerText = "ROUND: " + roundSeconds.toFixed(3) + "s\nTOTAL: " + totalTime.toFixed(3) + "s\nhover on circle to start round " + currentRound;
|
|
}
|
|
}
|
|
|
|
else if (gamemode == "countdown") {
|
|
gameText.innerText = preRoundText;
|
|
}
|
|
|
|
gameText.style.display = "inline";
|
|
countingDown = false;
|
|
}
|
|
}
|
|
|
|
async function handlePlayAgain(e) {
|
|
// hide game over buttons
|
|
gameOverButtons.style.display = "none";
|
|
gameOverButtons.style.opacity = "0";
|
|
|
|
// remove game text from DOM flow
|
|
gameText.style.display = "none";
|
|
gameText.style.opacity = "0";
|
|
|
|
setParameters();
|
|
center = generateTarget(xFrameAdjustment, yFrameAdjustment);
|
|
center.id = "center";
|
|
document.body.insertAdjacentElement("afterbegin", center);
|
|
|
|
if (gamemode == "normal") {
|
|
handleNormalMode(e);
|
|
}
|
|
else if (gamemode == "countdown") {
|
|
handleCountdownMode(e);
|
|
}
|
|
else {
|
|
alert("cannot play again, gamemode does not exit?");
|
|
}
|
|
}
|
|
|
|
|
|
async function handleReset(e) {
|
|
// bullseye won't be on screen after a game
|
|
setParameters();
|
|
center = generateTarget(xFrameAdjustment, yFrameAdjustment);
|
|
center.id = "center";
|
|
document.body.insertAdjacentElement("afterbegin", center);
|
|
|
|
// refresh game state variables
|
|
gamemode = "";
|
|
currentRound = 0;
|
|
|
|
// hide buttons
|
|
gameOverButtons.style.display = "none";
|
|
gameOverButtons.style.opacity = "0";
|
|
|
|
// remove game text from DOM flow
|
|
gameText.style.display = "none";
|
|
gameText.style.opacity = "0";
|
|
|
|
// reintroduce main menu elements into DOM flow
|
|
await sleep(800);
|
|
await drawCircle(2);
|
|
await fadeMainMenuIn();
|
|
|
|
// auto-reset bullseye if page was resized during animation
|
|
if (needsResizeAdjustment) {
|
|
handlePageResize();
|
|
}
|
|
}
|
|
|
|
function handleTargetClick(e) {
|
|
targetsClicked++;
|
|
e.target.remove();
|
|
}
|
|
|
|
function handlePageResize(event) {
|
|
// take note if page was resized during animation or mid gameplay to auto-reset at earliest convenience
|
|
if (drawingCircle || (targetsClicked < numTargets)) {
|
|
needsResizeAdjustment = true;
|
|
return;
|
|
}
|
|
|
|
targetFrame.replaceChildren();
|
|
setParameters();
|
|
|
|
// if at main menu, move bullseye
|
|
if (gamemode == "") {
|
|
center.style.top = yFrameAdjustment + "px";
|
|
center.style.left = xFrameAdjustment + "px";
|
|
drawCircle(0);
|
|
}
|
|
// if inbetween gameplay rounds, move bullseye w/ game event listener
|
|
else if ((gamemode == "normal" && normalBullseyeNeeded) ||
|
|
(gamemode == "countdown" && countdownBullseyeNeeded)
|
|
) {
|
|
center.style.top = yFrameAdjustment + "px";
|
|
center.style.left = xFrameAdjustment + "px";
|
|
}
|
|
|
|
console.log('page: ' + pageWidth + ' x ' + pageHeight);
|
|
console.log('frame: ' + frameWidth + ' x ' + frameHeight);
|
|
needsResizeAdjustment = false;
|
|
}
|
|
|
|
async function drawCircle(drawDurationSeconds) {
|
|
drawingCircle = true;
|
|
let r, x, y;
|
|
let radians;
|
|
let target;
|
|
for (let i = 0; i < 360; i = i + 2) {
|
|
r = (frameWidth / 2) - (targetWidth / 2) - (gameBorderWidth * 2);
|
|
radians = degreesToRadians(i);
|
|
x = (r * 1 * Math.cos(radians)) + xFrameAdjustment;
|
|
y = (r * ratio * Math.sin(radians)) + yFrameAdjustment;
|
|
|
|
target = generateTarget(x, y);
|
|
target.classList.add("red-dot");
|
|
targetFrame.insertAdjacentElement("afterbegin", target);
|
|
|
|
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 = "flex";
|
|
|
|
// make title elements actually visible
|
|
title1.style.opacity = "100";
|
|
await sleep(300);
|
|
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("red-dot");
|
|
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);
|
|
targetFrame.replaceChildren();
|
|
titleContainer.style.display = "none";
|
|
gameModes.style.display = "none";
|
|
|
|
title1.style.display = "none";
|
|
title2.style.display = "none";
|
|
}
|
|
|
|
|
|
function degreesToRadians(degrees) {
|
|
return degrees * (Math.PI / 180)
|
|
}
|
|
|
|
// function to place a target somewhere random on the circle
|
|
function randomTargetGenerator(numTargets) {
|
|
let radians, radius
|
|
let x, y;
|
|
let target;
|
|
for (let i = 0; i < numTargets; i++) {
|
|
radians = degreesToRadians(getRandomInt(0, 360));
|
|
radius = (frameWidth / 2) - (targetWidth / 2) - (gameBorderWidth * 2);
|
|
x = (radius * 1 * Math.cos(radians)) + xFrameAdjustment;
|
|
y = (radius * ratio * Math.sin(radians)) + yFrameAdjustment;
|
|
|
|
target = generateTarget(x, y);
|
|
target.classList.add("target");
|
|
target.addEventListener("click", handleTargetClick);
|
|
target.addEventListener("touchend", handleTargetClick);
|
|
targetFrame.insertAdjacentElement("afterbegin", target);
|
|
}
|
|
}
|
|
|
|
function generateTarget(x, y) {
|
|
const target = document.createElement("div");
|
|
target.style.top = y +'px';
|
|
target.style.left = x + 'px';
|
|
return target;
|
|
}
|
|
|
|
function getRandomInt(min, max) {
|
|
const minCeiled = Math.ceil(min);
|
|
const maxFloored = Math.floor(max);
|
|
return Math.floor(Math.random() * (maxFloored - minCeiled) + minCeiled); // The maximum is exclusive and the minimum is inclusive
|
|
}
|
|
|
|
function sleep(ms) {
|
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|