From 11bc1063f9f6d69ce500409549b0ef4721a576b2 Mon Sep 17 00:00:00 2001 From: "sufiya.naikwadi" Date: Sat, 31 Jan 2026 23:32:03 +0530 Subject: [PATCH] Aim_Training: gameplay, accessibility & stability improvements (difficulty scaling, bubble TTL, accuracy/combo, pause/restart, localStorage high score, container-bound spawns) --- Games/Aim_Training/index.html | 54 ++++- Games/Aim_Training/script.js | 440 +++++++++++++++++++++++++--------- Games/Aim_Training/styles.css | 55 +++-- 3 files changed, 411 insertions(+), 138 deletions(-) diff --git a/Games/Aim_Training/index.html b/Games/Aim_Training/index.html index fe562d49dc..c0f0b34b45 100644 --- a/Games/Aim_Training/index.html +++ b/Games/Aim_Training/index.html @@ -1,28 +1,58 @@ - - - + + + Aim training - - + + + + -
+
+ + + +
+

Aim training

+
+
+ +
Score: 0
High Score: 0
Time Left: 30 seconds
+ + +
+ Accuracy: 100% | + Combo: 0 +
+ +
- - - + + + + + + + + + + +
+ + - + \ No newline at end of file diff --git a/Games/Aim_Training/script.js b/Games/Aim_Training/script.js index 9f22495f29..a6df336485 100644 --- a/Games/Aim_Training/script.js +++ b/Games/Aim_Training/script.js @@ -1,160 +1,376 @@ -// Variables +/* =========================== + Aim Training - Enhanced JS + Drop-in replacement script + =========================== */ + +// --- Game state --- let score = 0; let highScore = 0; let timeLeft = 30; -let timerId; let level = 1; -let bubbleInterval = 1000; -let gameInProgress = false; // Track if the game is in progress -let bubbleGenerationTimeout; // Store the timeout for bubble generation +let gameInProgress = false; +let paused = false; -// Function to create a bubble -function createBubble() { - const bubble = document.createElement("div"); - bubble.className = "bubble"; +// Timers/IDs +let timerId = null; // countdown timer (1s) +let spawnIntervalId = null; // bubble spawner +let speedupIntervalId = null; // difficulty scaler +let bubbleTTLms = 1200; // each bubble lifetime +let spawnIntervalMs = 1000; // initial spawn cadence +const MIN_SPAWN_MS = 250; - // Generate random position - const posX = Math.random() * 500 + 50; - const posY = Math.random() * 300 + 50; +// Stats +let totalSpawns = 0; +let hits = 0; +let combo = 0; +let bestCombo = 0; - bubble.style.top = `${posY}px`; - bubble.style.left = `${posX}px`; +// Audio / UX +let muted = false; + +// --- DOM helpers (safe if elements missing) --- +const $ = (id) => document.getElementById(id); +const text = (id, value) => { const n = $(id); if (n) n.textContent = String(value); }; +const show = (id, on) => { const n = $(id); if (n) n.style.display = on ? 'block' : 'none'; }; - // Generate random size and color +// Ensure container exists +function getContainer() { + let el = $('bubbles'); + if (!el) { + el = document.createElement('div'); + el.id = 'bubbles'; + el.style.position = 'relative'; + el.style.width = '600px'; + el.style.height = '400px'; + el.style.border = '1px solid #ccc'; + el.style.overflow = 'hidden'; + document.body.appendChild(el); + } else { + // Make sure it positions children correctly + const style = window.getComputedStyle(el); + if (style.position === 'static') el.style.position = 'relative'; + if (style.overflow !== 'hidden') el.style.overflow = 'hidden'; + } + return el; +} + +// Load / save high score +function loadHighScore() { + const saved = Number(localStorage.getItem('aim_highscore') || 0); + highScore = Number.isFinite(saved) ? saved : 0; + text('highScoreValue', highScore); +} +function saveHighScore() { + localStorage.setItem('aim_highscore', String(highScore)); +} + +// Audio helpers +function playPopSound() { + if (muted) return; + const snd = $('popSound'); + if (!snd) return; + try { snd.currentTime = 0; snd.play(); } catch {} +} +function playEndSound() { + if (muted) return; + const snd = $('endSound'); + if (!snd) return; + try { snd.currentTime = 0; snd.play(); } catch {} +} + +// HUD updater (safe if nodes missing) +function updateHUD() { + const acc = totalSpawns ? Math.round((hits / totalSpawns) * 100) : 100; + text('scoreValue', score); + text('timerValue', timeLeft); + text('highScoreValue', highScore); + text('accuracyValue', `${acc}%`); + text('comboValue', `${combo}${bestCombo ? ` (best ${bestCombo})` : ''}`); +} + +// Announce for screen readers (optional live region) +function announce(msg) { + const live = $('status-live'); + if (live) live.textContent = msg; +} + +// Random readable color (avoid ultra dark) +function randomColor() { + const comp = () => Math.floor(Math.random() * 200) + 30; + return `rgb(${comp()}, ${comp()}, ${comp()})`; +} + +// Create a bubble element (with TTL, keyboard & touch) +function createBubble() { + const container = getContainer(); + const rect = container.getBoundingClientRect(); const size = Math.floor(Math.random() * 30) + 20; - const color = `rgb(${Math.random() * 255}, ${Math.random() * 255}, ${Math.random() * 255})`; + const bubble = document.createElement('div'); + bubble.className = 'bubble'; + bubble.style.position = 'absolute'; bubble.style.width = `${size}px`; bubble.style.height = `${size}px`; - bubble.style.backgroundColor = color; - - // Event listener to pop the bubble - bubble.addEventListener("click", function () { - if (bubble.parentNode) { - score++; - document.getElementById("scoreValue").textContent = score; - bubble.parentNode.removeChild(bubble); - playPopSound(); + bubble.style.borderRadius = '50%'; + bubble.style.background = randomColor(); + bubble.style.cursor = 'pointer'; + bubble.style.userSelect = 'none'; + bubble.tabIndex = 0; // focusable for keyboard + + // position within container bounds + const maxX = Math.max(0, rect.width - size); + const maxY = Math.max(0, rect.height - size); + const posX = Math.random() * maxX; + const posY = Math.random() * maxY; + bubble.style.left = `${posX}px`; + bubble.style.top = `${posY}px`; + + // TTL (miss) + const ttl = setTimeout(() => { + if (!bubble.parentNode) return; + onMiss(); + bubble.remove(); + }, bubbleTTLms); + + // Hit handlers: click / keyboard / touch + const registerHit = () => { + if (!gameInProgress || paused || !bubble.parentNode) return; + clearTimeout(ttl); + hits += 1; + combo += 1; + bestCombo = Math.max(bestCombo, combo); + score += 1; + updateHUD(); + bubble.remove(); + playPopSound(); + + // level progression each 10 hits + if (hits % 10 === 0) { + level += 1; + // Increase difficulty: slightly faster spawns and/or shorter TTL + spawnIntervalMs = Math.max(MIN_SPAWN_MS, spawnIntervalMs - 50); + bubbleTTLms = Math.max(700, bubbleTTLms - 20); + restartSpawner(); // apply new spawn cadence + } + }; + + bubble.addEventListener('click', registerHit); + bubble.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + registerHit(); } }); + bubble.addEventListener('touchstart', () => registerHit(), { passive: true }); + totalSpawns += 1; return bubble; } -// Function to start the game -function startGame() { - if (gameInProgress) { - return; // Return if game is already in progress - } - - const bubblesContainer = document.getElementById("bubbles"); - const timerElement = document.getElementById("timerValue"); - const startButton = document.getElementById("startButton"); +function onMiss() { + combo = 0; + updateHUD(); +} - // Clear previous bubbles - while (bubblesContainer.firstChild) { - bubblesContainer.firstChild.remove(); +// Spawner controls +function startSpawner() { + stopSpawner(); + const container = getContainer(); + spawnIntervalId = setInterval(() => { + if (!gameInProgress || paused) return; + container.appendChild(createBubble()); + }, spawnIntervalMs); +} +function restartSpawner() { + if (!gameInProgress) return; + startSpawner(); +} +function stopSpawner() { + if (spawnIntervalId) { + clearInterval(spawnIntervalId); + spawnIntervalId = null; } +} - // Reset game state - score = 0; - timeLeft = 30; - level = 1; - bubbleInterval = 1000; - document.getElementById("scoreValue").textContent = score; - document.getElementById("timerValue").textContent = timeLeft; - gameInProgress = true; +// Difficulty scaler over time +function startSpeedup() { + stopSpeedup(); + speedupIntervalId = setInterval(() => { + if (!gameInProgress || paused) return; + // every 5s, make spawns faster down to min + spawnIntervalMs = Math.max(MIN_SPAWN_MS, spawnIntervalMs - 50); + restartSpawner(); + }, 5000); +} +function stopSpeedup() { + if (speedupIntervalId) { + clearInterval(speedupIntervalId); + speedupIntervalId = null; + } +} - // Start the timer - timerId = setInterval(function () { - timeLeft--; - timerElement.textContent = timeLeft; +// Countdown tick +function startTimer() { + stopTimer(); + timerId = setInterval(() => { + if (!gameInProgress || paused) return; + timeLeft -= 1; + text('timerValue', timeLeft); - if (timeLeft === 10) { - showAlert(timeLeft); + // Accessible countdown in last 10s + if (timeLeft <= 10 && timeLeft > 0) { + const msg = `${timeLeft} seconds left!`; + const alertEl = $('countdown-alert'); + if (alertEl) { alertEl.textContent = msg; alertEl.style.display = 'block'; } + announce(msg); + } else { + show('countdown-alert', false); } - if (timeLeft === 0) { - clearInterval(timerId); + if (timeLeft <= 0) { + stopTimer(); endGame(); } }, 1000); +} +function stopTimer() { + if (timerId) { + clearInterval(timerId); + timerId = null; + } +} - // Start generating bubbles - generateBubble(bubblesContainer); +// Game lifecycle +function startGame() { + if (gameInProgress) return; - // Disable start button during gameplay - startButton.disabled = true; -} + // Reset state + score = 0; + hits = 0; + combo = 0; + bestCombo = 0; + totalSpawns = 0; + level = 1; + timeLeft = 30; + spawnIntervalMs = 1000; + bubbleTTLms = 1200; + paused = false; + gameInProgress = true; -// Function to generate bubbles -function generateBubble(container) { - if (!gameInProgress) { - return; // Return if game is not in progress + // Clear container + const container = getContainer(); + while (container.firstChild) container.firstChild.remove(); + + // UI + loadHighScore(); + updateHUD(); + const startBtn = $('startButton'); + if (startBtn) { + startBtn.disabled = false; + startBtn.textContent = 'Pause'; // will act as Pause during game } - const bubble = createBubble(); - container.appendChild(bubble); + // Start systems + startTimer(); + startSpawner(); + startSpeedup(); - // Schedule next bubble generation - bubbleGenerationTimeout = setTimeout(function () { - generateBubble(container); - }, bubbleInterval); + // Announce + announce('Game started.'); } -// Function to end the game -function endGame() { - gameInProgress = false; // Set gameInProgress to false +function endGame(silent = false) { + // stop systems + gameInProgress = false; + paused = false; + stopSpawner(); + stopSpeedup(); + stopTimer(); - // Clear bubble generation timeout - clearTimeout(bubbleGenerationTimeout); + // hide countdown + show('countdown-alert', false); - const startButton = document.getElementById("startButton"); - startButton.disabled = false; - playEndSound(); - - // Update high score + // High score if (score > highScore) { highScore = score; - document.getElementById("highScoreValue").textContent = highScore; + saveHighScore(); } + updateHUD(); + playEndSound(); - // Display game over message - setTimeout(function () { - alert("Game Over! Your score: " + score); - }, 100); + const startBtn = $('startButton'); + if (startBtn) { + startBtn.disabled = false; + startBtn.textContent = 'Start'; + } + + if (!silent) { + // Slight delay so HUD updates render + setTimeout(() => alert(`Game Over! Your score: ${score}`), 100); + } + + announce(`Game over. Your score is ${score}.`); } -// Function to play bubble pop sound -function playPopSound() { - const popSound = document.getElementById("popSound"); - popSound.currentTime = 0; - popSound.play(); +function togglePause() { + if (!gameInProgress) return; + paused = !paused; + const startBtn = $('startButton'); + if (paused) { + stopTimer(); + stopSpawner(); + if (startBtn) startBtn.textContent = 'Resume'; + announce('Game paused'); + } else { + startTimer(); + startSpawner(); + if (startBtn) startBtn.textContent = 'Pause'; + announce('Game resumed'); + } } -// Function to play game end sound -function playEndSound() { - const endSound = document.getElementById("endSound"); - endSound.currentTime = 0; - endSound.play(); -} - -// Function to show alert message -function showAlert(secondsLeft) { - const alertMessage = document.getElementById("countdown-alert"); - alertMessage.textContent = `${secondsLeft} seconds left!`; - alertMessage.style.display = 'block'; - -// Update the countdown every second until the game ends -const updateInterval = setInterval(() => { - secondsLeft--; - alertMessage.textContent = `${secondsLeft} seconds left!`; - if (secondsLeft <= 0) { - clearInterval(updateInterval); - alertMessage.style.display = 'none'; - } -}, 1000); +function restartGame() { + if (gameInProgress) endGame(true); + startGame(); +} + +// --- Event wiring --- +function wireControls() { + // Start / Pause / Resume button + const startBtn = $('startButton'); + if (startBtn) { + startBtn.addEventListener('click', () => { + if (!gameInProgress) startGame(); + else togglePause(); + }); + } + + // Keyboard shortcuts: P = pause/resume, R = restart + document.addEventListener('keydown', (e) => { + const k = e.key.toLowerCase(); + if (k === 'p') togglePause(); + if (k === 'r') restartGame(); + }); + + // Mute toggle (if present) + const muteBtn = $('muteToggle'); + if (muteBtn) { + muteBtn.addEventListener('click', () => { + muted = !muted; + muteBtn.textContent = muted ? 'Unmute' : 'Mute'; + }); + // initialize button label + muteBtn.textContent = muted ? 'Unmute' : 'Mute'; + } + + // Initialize HUD on load + loadHighScore(); + updateHUD(); } -// Event listener for start button -document.getElementById("startButton").addEventListener("click", startGame); +// Initialize after DOM ready +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', wireControls); +} else { + wireControls(); +} \ No newline at end of file diff --git a/Games/Aim_Training/styles.css b/Games/Aim_Training/styles.css index 2f7a19172a..186d44e739 100644 --- a/Games/Aim_Training/styles.css +++ b/Games/Aim_Training/styles.css @@ -27,6 +27,13 @@ h1 { margin-bottom: 10px; } +/* Optional secondary HUD */ +#secondaryHud { + font-size: 18px; + color: #333; + margin: 8px 0 16px; +} + #bubbles { position: relative; width: 600px; @@ -49,12 +56,21 @@ h1 { box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); /* Add box shadow */ } +/* Hover & active feedback */ .bubble:hover { background-color: #f00; transform: scale(1.2); - transform: scale(1.2); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.5); /* Increase shadow on hover */ - transition: transform 0.3s ease-in-out, box-shadow 0.3s ease-in-out; /* Add transition */ + transition: transform 0.3s ease-in-out, box-shadow 0.3s ease-in-out; +} +.bubble:active { + transform: scale(0.9); +} + +/* Keyboard focus outline for accessibility */ +.bubble:focus { + outline: 2px solid #000; + outline-offset: 1px; } #scoreValue, @@ -62,9 +78,10 @@ h1 { font-weight: bold; } +/* FIX: Use the defined @keyframes name (was 'blink' earlier) */ #timerValue { font-weight: bold; - animation: blink 1s infinite; + animation: countdown 1s infinite; } .start-button { @@ -74,13 +91,15 @@ h1 { color: #fff; border: none; cursor: pointer; - margin-top: 20px; + margin: 12px 6px 20px; + border-radius: 6px; transition: transform 0.2s ease-in-out; /* Add transition */ } .start-button:hover { transform: scale(1.1); /* Scale up on hover */ } +/* Fixed top alert for last-10-seconds message */ #countdown-alert { position: fixed; top: 10px; @@ -95,16 +114,24 @@ h1 { z-index: 1000; /* Ensure it's on top */ } +/* The animation used by #timerValue */ @keyframes countdown { - 0% { - transform: scale(1); - } - 50% { - transform: scale(1.1); - } - 100% { - transform: scale(1); - } + 0% { transform: scale(1); } + 50% { transform: scale(1.1); } + 100% { transform: scale(1); } +} + +/* Visually hidden utility for screen-reader only elements */ +.sr-only { + position: absolute !important; + width: 1px !important; + height: 1px !important; + padding: 0 !important; + margin: -1px !important; + overflow: hidden !important; + clip: rect(0,0,0,0) !important; + white-space: nowrap !important; + border: 0 !important; } @media (max-width: 768px) { @@ -112,4 +139,4 @@ h1 { width: 80%; height: 300px; } -} +} \ No newline at end of file