(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 = `
`;
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); })();
Time
15s
Score
0
Combo
0x
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); })();
