diff --git a/Domains/Frontend/MiniProjects/EmojiReactionSpeedTest/README.md b/Domains/Frontend/MiniProjects/EmojiReactionSpeedTest/README.md new file mode 100644 index 00000000..0ef2baee --- /dev/null +++ b/Domains/Frontend/MiniProjects/EmojiReactionSpeedTest/README.md @@ -0,0 +1,28 @@ +# Emoji Reaction Speed Test + +Develop an interactive reaction speed game where random emojis appear on screen and players must click the matching target emoji before time runs out. + +## How to play + +**Goal:** Quickly click the single matching emoji from a scattered group before the timer runs out. + +**Scoring:** Faster clicks and building a streak earn high points. A streak activates a 1.5x combo multiplier for bonus score. + +**Game Over:** Losing all three lives (by wrong clicks or timeouts) ends the game. + +## Features + +- Random emoji generation with multiple decoy emojis on screen +- Target emoji display with countdown timer for each round +- Score tracking system with combo multipliers for consecutive hits +- Multiple difficulty levels (easy, medium, hard) with varying speeds +- Smooth animations and visual feedback for correct/incorrect clicks + +## Tech Stack + +HTML5 +CSS3 +JavaScript (Vanilla) + +## Future +High scores, sound effects for hits/misses diff --git a/Domains/Frontend/MiniProjects/EmojiReactionSpeedTest/index.css b/Domains/Frontend/MiniProjects/EmojiReactionSpeedTest/index.css new file mode 100644 index 00000000..fca1a8cb --- /dev/null +++ b/Domains/Frontend/MiniProjects/EmojiReactionSpeedTest/index.css @@ -0,0 +1,202 @@ +body { + font-family: "Segoe UI Emoji", "Apple Color Emoji", "Poppins", sans-serif; + background: #253851; + color: #f3f4f6; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + min-height: 100vh; + overflow: hidden; +} +#game-info { + text-align: center; + margin-top: 0px; + font-family: "Poppins", sans-serif; +} + +#stats-header { + display: flex; + justify-content: space-around; + align-items: center; + padding: 5px 10px 10px 10px; + background: #374151; + border-bottom: 2px solid #4b5563; + flex-wrap: wrap; + gap: 15px; +} + +#target-area { + font-size: 1.2rem; + text-align: center; +} + +#target-display { + font-size: 3rem; + display: block; + margin-top: 5px; + border: 3px solid #60a5fa; + border-radius: 8px; + padding: 5px 10px; +} + +#lives-combo, +#stats { + text-align: center; +} + +#combo-badge { + margin-left: 10px; + padding: 3px 8px; + background: #f59e0b; + color: black; + border-radius: 4px; + font-weight: bold; + animation: pulse 0.5s; +} + +@keyframes pulse { + 0%, + 100% { + transform: scale(1); + } + 50% { + transform: scale(1.2); + } +} + +#timer-container { + width: 100%; + height: 10px; + background: #4b5563; +} + +#timer-bar { + height: 100%; + width: 100%; + background-color: #10b981; + transition: width 0.1s linear, background-color 0.3s; +} + +#timer-bar.warning { + background-color: #f59e0b; +} + +#timer-bar.danger { + background-color: #ef4444; +} + +#game-area { + flex-grow: 1; + position: relative; + width: 90%; + max-width: 1000px; + height: 450px; + margin: 10px auto; + border: 2px dashed #4b5563; +} + +.emoji-tile { + position: absolute; + font-size: 4rem; + cursor: pointer; + transition: transform 0.1s; +} + +.emoji-tile:hover { + transform: scale(1.1); +} + +.correct-flash { + transform: scale(1.5) !important; + filter: drop-shadow(0 0 10px #10b981); + transition: transform 0.1s; +} + +.shake-error { + animation: shake 0.5s; + filter: drop-shadow(0 0 10px #ef4444); +} + +@keyframes shake { + 0%, + 100% { + transform: translateX(0); + } + 20%, + 60% { + transform: translateX(-5px); + } + 40%, + 80% { + transform: translateX(5px); + } +} + +#game-over-screen { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.9); + color: white; + display: none; + flex-direction: column; + justify-content: center; + align-items: center; + z-index: 100; + text-align: center; +} + +#final-stats p { + margin: 8px 0; + font-size: 1.1rem; +} + +#restart-button, +#start-button { + padding: 10px 20px; + margin-top: 20px; + font-size: 1.2rem; + cursor: pointer; + background: #f59e0b; + border: none; + border-radius: 5px; + color: black; + font-weight: bold; +} + +#restart-button:hover, +#start-button:hover { + background: #d97706; +} + +footer { + padding: 15px; + text-align: center; + background: #374151; + margin-top: auto; + font-family: "Poppins", sans-serif; +} +p { + margin: 5px 0; + text-align: center; +} +#controls-bar { + margin-bottom: 10px; +} + +#controls-bar label { + margin-right: 10px; + font-weight: bold; + font-family: "Poppins", sans-serif; + color: #374151; +} + +#controls-bar select { + padding: 5px 10px; + margin-right: 10px; + border-radius: 4px; + border: none; +} diff --git a/Domains/Frontend/MiniProjects/EmojiReactionSpeedTest/index.html b/Domains/Frontend/MiniProjects/EmojiReactionSpeedTest/index.html new file mode 100644 index 00000000..347451e7 --- /dev/null +++ b/Domains/Frontend/MiniProjects/EmojiReactionSpeedTest/index.html @@ -0,0 +1,68 @@ + + + + + + Emoji Reaction Speed Test + + + + + + +
+
+

Emoji Reaction Speed Test

+

+ Test your reaction speed by clicking on the emoji as soon as it + appears! +

+
+ +
Target:
+
+ Score: 0 | Streak: + 0 (Best: 0) +
+
+ Lives: ❤️ ❤️ ❤️ + +
+
+
+
+
+
+
+
+ + + + + + diff --git a/Domains/Frontend/MiniProjects/EmojiReactionSpeedTest/index.js b/Domains/Frontend/MiniProjects/EmojiReactionSpeedTest/index.js new file mode 100644 index 00000000..e0e9dec9 --- /dev/null +++ b/Domains/Frontend/MiniProjects/EmojiReactionSpeedTest/index.js @@ -0,0 +1,321 @@ +let currentTarget = ""; +let score = 0; +let streak = 0; +let bestStreak = 0; +let lives = 3; +let comboMultiplier = 1; +let gameActive = false; +let roundStartTime = 0; +let reactionTimes = []; +let timerInterval = null; +let totalRounds = 0; +let emojis = [ + "😀", + "😂", + "😍", + "😎", + "🤔", + "😴", + "😡", + "😭", + "😱", + "🤯", + "🥳", + "😇", + "🤪", + "😷", + "🤢", + "👻", + "💩", + "🎃", + "🌟", + "🔥", +]; +let difficultySettings = { + Easy: { + timePerRound: 3000, + numberOfDecoys: 2, + comboThreshold: 3, + }, + Medium: { + timePerRound: 2000, + numberOfDecoys: 4, + comboThreshold: 5, + }, + Hard: { + timePerRound: 1000, + numberOfDecoys: 6, + comboThreshold: 7, + }, +}; +let currentDifficulty = difficultySettings.Medium; +let roundEmojis = []; +let correctClicks = 0; + +const clearGameArea = () => { + gameArea.innerHTML = ""; +}; + +const gameArea = document.getElementById("game-area"); +const targetDisplay = document.getElementById("target-display"); +const scoreDisplay = document.getElementById("score-display"); +const streakDisplay = document.getElementById("streak-display"); +const bestStreakDisplay = document.getElementById("best-streak-display"); +const livesDisplay = document.getElementById("lives-display"); +const comboBadge = document.getElementById("combo-badge"); +const timerBar = document.getElementById("timer-bar"); +const gameOverScreen = document.getElementById("game-over-screen"); +const difficultySelector = document.getElementById("difficulty-select"); +const startButton = document.getElementById("start-button"); +const restartButton = document.getElementById("restart-button"); + +const startGame = (difficulty) => { + // reset core game state + score = 0; + streak = 0; + bestStreak = 0; + lives = 3; + comboMultiplier = 1; + reactionTimes = []; + correctClicks = 0; + totalRounds = 0; + roundEmojis = []; + gameActive = true; + + if (timerInterval) clearInterval(timerInterval); + // defensive: if difficulty is invalid, fall back to Medium + currentDifficulty = + difficultySettings[difficulty] || difficultySettings.Medium; + + // If the game-over screen was visible, hide it so the game area is interactive again + if (gameOverScreen) gameOverScreen.style.display = "none"; + + updateUI(); + startRound(); +}; +startRound = () => { + if (!gameActive) return; + clearGameArea(); + const randomIndex = Math.floor(Math.random() * emojis.length); + currentTarget = emojis[randomIndex]; + targetDisplay.textContent = currentTarget; + totalRounds++; + roundEmojis = []; + const numDecoys = currentDifficulty.numberOfDecoys; + while (roundEmojis.length < numDecoys) { + const idx = Math.floor(Math.random() * emojis.length); + const cand = emojis[idx]; + if (cand === currentTarget) continue; + if (!roundEmojis.includes(cand)) roundEmojis.push(cand); + } + + const targetPosition = Math.floor(Math.random() * (roundEmojis.length + 1)); + roundEmojis.splice(targetPosition, 0, currentTarget); + for (let i = roundEmojis.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [roundEmojis[i], roundEmojis[j]] = [roundEmojis[j], roundEmojis[i]]; + } + + gameArea.innerHTML = ""; + const positions = []; + + roundEmojis.forEach((emoji) => { + const tile = document.createElement("div"); + tile.className = "emoji-tile"; + tile.textContent = emoji; + + let validPosition = false; + let attempts = 0; + let top, left; + + while (!validPosition && attempts < 20) { + top = Math.random() * 80 + 5; + left = Math.random() * 85 + 5; + + validPosition = true; + for (let pos of positions) { + const distance = Math.sqrt( + Math.pow(top - pos.top, 2) + Math.pow(left - pos.left, 2) + ); + if (distance < 15) { + validPosition = false; + break; + } + } + attempts++; + } + + positions.push({ top, left }); + tile.style.top = top + "%"; + tile.style.left = left + "%"; + gameArea.appendChild(tile); + }); + + startTimer(); + roundStartTime = Date.now(); + updateUI(); +}; + +const updateUI = () => { + scoreDisplay.textContent = score; + streakDisplay.textContent = streak; + bestStreakDisplay.textContent = bestStreak; + livesDisplay.textContent = lives; + + const hearts = "❤️ ".repeat(lives).trim(); + livesDisplay.textContent = hearts || "💔"; + + if (comboMultiplier > 1) { + comboBadge.textContent = comboMultiplier + "x"; + comboBadge.style.display = "inline"; + } else { + comboBadge.style.display = "none"; + } +}; + +const startTimer = () => { + if (timerInterval) clearInterval(timerInterval); + const timePerRound = currentDifficulty.timePerRound; + timerBar.style.width = "100%"; + timerBar.classList.remove("warning", "danger"); + + timerInterval = setInterval(() => { + updateTimer(); + }, 50); +}; +const updateTimer = () => { + const elapsed = Date.now() - roundStartTime; + const timePerRound = currentDifficulty.timePerRound; + const remainingTime = timePerRound - elapsed; + const percentage = (remainingTime / timePerRound) * 100; + + timerBar.style.width = Math.max(0, percentage) + "%"; + + if (percentage < 30 && percentage >= 15) { + timerBar.classList.add("warning"); + timerBar.classList.remove("danger"); + } else if (percentage < 15) { + timerBar.classList.add("danger"); + timerBar.classList.remove("warning"); + } + + if (remainingTime <= 0) { + clearInterval(timerInterval); + handleTimeout(); + } +}; +const handleTimeout = () => { + streak = 0; + comboMultiplier = 1; + lives--; + updateUI(); + + if (lives <= 0) { + gameOver(); + } else { + setTimeout(startRound, 500); + } +}; + +const handleEmojiClick = (clickedEmoji, element) => { + if (!gameActive) return; + + if (clickedEmoji === currentTarget) { + const reactionTime = Date.now() - roundStartTime; + reactionTimes.push(reactionTime); + correctClicks++; + + streak++; + if (streak > bestStreak) { + bestStreak = streak; + } + + calculateScore(reactionTime); + + element.classList.add("correct-flash"); + clearInterval(timerInterval); + + setTimeout(() => { + startRound(); + }, 500); + } else { + streak = 0; + comboMultiplier = 1; + lives--; + + element.classList.add("shake-error"); + updateUI(); + + if (lives <= 0) { + clearInterval(timerInterval); + gameOver(); + } else { + clearInterval(timerInterval); + setTimeout(() => { + startRound(); + }, 500); + } + } +}; + +// Scoring +const calculateScore = (reactionTime) => { + const basePoints = 10; + let speedBonus = 0; + + if (reactionTime < 500) { + speedBonus = 50; + } else if (reactionTime < 800) { + speedBonus = 30; + } else if (reactionTime < 1200) { + speedBonus = 15; + } else { + speedBonus = 5; + } + + if (streak >= currentDifficulty.comboThreshold && comboMultiplier === 1) { + comboMultiplier = 1.5; + comboBadge.style.display = "inline"; + comboBadge.style.animation = "pulse 0.5s"; + } + + const points = Math.round((basePoints + speedBonus) * comboMultiplier); + score += points; + + updateUI(); +}; + +const gameOver = () => { + gameActive = false; + if (timerInterval) clearInterval(timerInterval); + document.getElementById("final-score").textContent = score; + document.getElementById("final-best-streak").textContent = bestStreak; + const avgTime = + reactionTimes.length > 0 + ? Math.round( + reactionTimes.reduce((a, b) => a + b, 0) / reactionTimes.length + ) + : 0; + document.getElementById("final-avg-time").textContent = + avgTime > 0 ? avgTime + " ms" : "N/A"; + + gameOverScreen.style.display = "flex"; +}; + +startButton.addEventListener("click", () => { + const selectedDifficulty = difficultySelector.value; + startGame(selectedDifficulty); +}); + +restartButton.addEventListener("click", () => { + const selectedDifficulty = difficultySelector.value; + startGame(selectedDifficulty); +}); + +gameArea.addEventListener("click", (event) => { + if (event.target.classList.contains("emoji-tile")) { + handleEmojiClick(event.target.textContent, event.target); + } +}); + +updateUI();