Rain Lad: นักจับท้องฟ้า
Loading...
(function () { // 1. Initialize Telegram WebApp SDK const tg = window.Telegram?.WebApp; if (tg) { tg.ready(); tg.expand(); } // 2. Dynamic Style Injection const style = document.createElement('style'); style.textContent = ` :root { --bg-color: var(--tg-theme-bg-color, #121212); --text-color: var(--tg-theme-text-color, #ffffff); --btn-color: var(--tg-theme-button-color, #0088cc); --btn-text: var(--tg-theme-button-text-color, #ffffff); --card-bg: var(--tg-theme-secondary-bg-color, #1e1e1e); --accent-color: #ff4757; --success-color: #2ed573; } * { box-sizing: border-box; user-select: none; -webkit-user-select: none; margin: 0; padding: 0; touch-action: manipulation; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background-color: var(--bg-color); color: var(--text-color); min-height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; overflow: hidden; } .game-container { width: 100%; max-width: 420px; height: 100vh; display: flex; flex-direction: column; padding: 16px; position: relative; } .header-stats { display: flex; justify-content: space-between; align-items: center; background: var(--card-bg); padding: 12px 20px; border-radius: 16px; box-shadow: 0 4px 12px rgba(0,0,0,0.2); margin-bottom: 16px; } .stat-box { display: flex; flex-direction: column; align-items: center; } .stat-label { font-size: 11px; color: #888; text-transform: uppercase; font-weight: 700; } .stat-value { font-size: 20px; font-weight: 800; } .arena { flex: 1; background: var(--card-bg); border-radius: 20px; position: relative; overflow: hidden; border: 2px dashed rgba(255, 255, 255, 0.1); } .target { position: absolute; width: 64px; height: 64px; border-radius: 50%; background: radial-gradient(circle, var(--accent-color) 0%, #ff6b81 100%); border: 3px solid #ffffff; box-shadow: 0 0 15px var(--accent-color); cursor: pointer; display: flex; align-items: center; justify-content: center; font-weight: bold; color: white; transform: scale(0); animation: popIn 0.15s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards; } @keyframes popIn { to { transform: scale(1); } } .overlay { position: absolute; inset: 0; background: rgba(0, 0, 0, 0.85); backdrop-filter: blur(8px); display: flex; flex-direction: column; align-items: center; justify-content: center; z-index: 10; border-radius: 20px; padding: 24px; text-align: center; } .overlay h1 { font-size: 32px; margin-bottom: 8px; } .overlay p { font-size: 14px; color: #aaa; margin-bottom: 24px; } .btn { background: var(--btn-color); color: var(--btn-text); border: none; padding: 14px 32px; font-size: 18px; font-weight: 700; border-radius: 12px; cursor: pointer; box-shadow: 0 4px 12px rgba(0,0,0,0.3); transition: transform 0.1s; } .btn:active { transform: scale(0.95); } .combo-badge { position: absolute; font-weight: 900; font-size: 18px; color: var(--success-color); pointer-events: none; animation: floatUp 0.6s ease-out forwards; } @keyframes floatUp { 0% { opacity: 1; transform: translateY(0); } 100% { opacity: 0; transform: translateY(-30px); } } `; document.head.appendChild(style); // 3. Game State Logic const GAME_DURATION = 15; // 15 seconds let score = 0; let combo = 0; let hits = 0; let misses = 0; let timeLeft = GAME_DURATION; let gameTimer = null; let targetTimer = null; let activeTarget = null; let highScore = localStorage.getItem('speed_game_highscore') || 0; // 4. Helper Methods function triggerHaptic(type = 'light') { if (tg?.HapticFeedback) { if (type === 'success') tg.HapticFeedback.notificationOccurred('success'); else if (type === 'error') tg.HapticFeedback.notificationOccurred('error'); else tg.HapticFeedback.impactOccurred(type); } } // 5. Build DOM Structure Programmatically const container = document.createElement('div'); container.className = 'game-container'; container.innerHTML = `
Time 15s
Score 0
Combo 0x

⚡ Speed Tap

Tap as many targets as you can in 15 seconds!

`; document.body.appendChild(container); const arenaEl = document.getElementById('arena'); const overlayEl = document.getElementById('overlay'); const startBtn = document.getElementById('start-btn'); const timerVal = document.getElementById('timer-val'); const scoreVal = document.getElementById('score-val'); const comboVal = document.getElementById('combo-val'); const titleEl = document.getElementById('title'); const descEl = document.getElementById('description'); // 6. Game Core Engine function startGame() { triggerHaptic('medium'); score = 0; combo = 0; hits = 0; misses = 0; timeLeft = GAME_DURATION; scoreVal.textContent = '0'; comboVal.textContent = '0x'; timerVal.textContent = `${timeLeft}s`; overlayEl.style.display = 'none'; // Start 1-second countdown loop gameTimer = setInterval(() => { timeLeft--; timerVal.textContent = `${timeLeft}s`; if (timeLeft <= 0) { endGame(); } }, 1000); spawnTarget(); } function spawnTarget() { if (activeTarget) { activeTarget.remove(); activeTarget = null; } const target = document.createElement('div'); target.className = 'target'; // Random coordinates within the arena boundaries const targetSize = 64; const maxX = arenaEl.clientWidth - targetSize - 20; const maxY = arenaEl.clientHeight - targetSize - 20; const randomX = Math.max(10, Math.floor(Math.random() * maxX)); const randomY = Math.max(10, Math.floor(Math.random() * maxY)); target.style.left = `${randomX}px`; target.style.top = `${randomY}px`; // Handle Tap / Click on Target target.addEventListener('pointerdown', (e) => { e.stopPropagation(); hitTarget(target, randomX, randomY); }); arenaEl.appendChild(target); activeTarget = target; // Despawn target after dynamic delay based on remaining time (speeds up as time passes) const despawnTime = Math.max(450, 900 - (GAME_DURATION - timeLeft) * 25); clearTimeout(targetTimer); targetTimer = setTimeout(() => { if (activeTarget === target) { missTarget(); } }, despawnTime); } function hitTarget(target, x, y) { triggerHaptic('light'); hits++; combo++; score += 10 + combo * 2; // Show floating combo text showComboText(x, y, `+${10 + combo * 2}`); scoreVal.textContent = score; comboVal.textContent = `${combo}x`; clearTimeout(targetTimer); spawnTarget(); } function missTarget() { triggerHaptic('error'); misses++; combo = 0; comboVal.textContent = '0x'; if (activeTarget) { activeTarget.remove(); activeTarget = null; } spawnTarget(); } function showComboText(x, y, text) { const badge = document.createElement('div'); badge.className = 'combo-badge'; badge.textContent = text; badge.style.left = `${x + 15}px`; badge.style.top = `${y}px`; arenaEl.appendChild(badge); setTimeout(() => badge.remove(), 600); } function endGame() { clearInterval(gameTimer); clearTimeout(targetTimer); if (activeTarget) { activeTarget.remove(); activeTarget = null; } triggerHaptic('success'); if (score > highScore) { highScore = score; localStorage.setItem('speed_game_highscore', highScore); } const totalTaps = hits + misses; const accuracy = totalTaps > 0 ? Math.round((hits / totalTaps) * 100) : 0; const cps = (hits / GAME_DURATION).toFixed(1); // Clicks Per Second titleEl.textContent = '⏱️ Game Over!'; descEl.innerHTML = ` Score: ${score} pts
High Score: ${highScore} pts
Accuracy: ${accuracy}%
Speed: ${cps} taps/sec `; startBtn.textContent = 'PLAY AGAIN'; overlayEl.style.display = 'flex'; } // Handle Misses when clicking blank arena space arenaEl.addEventListener('pointerdown', (e) => { if (e.target === arenaEl && timeLeft > 0 && overlayEl.style.display === 'none') { missTarget(); } }); startBtn.addEventListener('click', startGame); })();