uranium / throwtomatoes.html
hihihi934's picture
Upload throwtomatoes.html
446d0d5 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Throw Tomatoes at Lay</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Bangers&family=Nunito:wght@400;700;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
<style>
:root {
--bg: #0d1a0d;
--fg: #e8f0e8;
--muted: #6b7a6b;
--accent: #ff3d00;
--card: rgba(18, 38, 18, 0.88);
--border: rgba(255, 61, 0, 0.25);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Nunito', sans-serif;
background: var(--bg);
color: var(--fg);
overflow: hidden;
height: 100vh;
cursor: crosshair;
}
.bangers { font-family: 'Bangers', cursive; letter-spacing: 0.06em; }
.screen { position: fixed; inset: 0; display: flex; align-items: center; justify-content: center; z-index: 50; transition: opacity 0.5s, visibility 0.5s; }
.screen.hidden { opacity: 0; visibility: hidden; pointer-events: none; }
#loginScreen {
background:
radial-gradient(ellipse at 20% 80%, rgba(255,61,0,0.12) 0%, transparent 50%),
radial-gradient(ellipse at 80% 20%, rgba(45,106,30,0.15) 0%, transparent 50%),
linear-gradient(160deg, #0a140a 0%, #0d1a0d 40%, #12100a 100%);
overflow-y: auto;
}
#gameCanvas { position: fixed; inset: 0; z-index: 0; }
.glass { background: var(--card); border: 1px solid var(--border); backdrop-filter: blur(16px); border-radius: 16px; }
.input-field {
width: 100%; padding: 12px 16px; border-radius: 10px;
border: 1px solid rgba(255,255,255,0.08); background: rgba(255,255,255,0.04);
color: var(--fg); font-size: 16px; font-family: 'Nunito', sans-serif;
outline: none; transition: border-color 0.3s, box-shadow 0.3s;
}
.input-field:focus { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(255,61,0,0.15); }
.input-field::placeholder { color: var(--muted); }
.btn-primary {
padding: 12px 28px; border-radius: 10px; border: none;
background: linear-gradient(135deg, #ff3d00, #d32f2f); color: #fff;
font-weight: 900; font-size: 16px; font-family: 'Nunito', sans-serif;
cursor: pointer; transition: transform 0.15s, box-shadow 0.3s;
box-shadow: 0 4px 20px rgba(255,61,0,0.3);
}
.btn-primary:hover { transform: translateY(-2px); box-shadow: 0 6px 28px rgba(255,61,0,0.45); }
.btn-primary:active { transform: translateY(0); }
.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
.btn-secondary {
padding: 12px 28px; border-radius: 10px; border: 1px solid var(--border);
background: transparent; color: var(--fg);
font-weight: 900; font-size: 16px; font-family: 'Nunito', sans-serif;
cursor: pointer; transition: background 0.2s, border-color 0.2s;
}
.btn-secondary:hover { background: rgba(255,61,0,0.1); border-color: var(--accent); }
.btn-ghost {
padding: 8px 16px; border-radius: 8px; border: 1px solid var(--border);
background: transparent; color: var(--fg); font-weight: 700; font-size: 13px;
font-family: 'Nunito', sans-serif; cursor: pointer; transition: background 0.2s, border-color 0.2s;
}
.btn-ghost:hover { background: rgba(255,61,0,0.1); border-color: var(--accent); }
#hud {
position: fixed; top: 0; left: 0; right: 0; z-index: 30;
padding: 12px 20px; display: flex; align-items: center; gap: 16px;
background: linear-gradient(180deg, rgba(0,0,0,0.6) 0%, transparent 100%);
pointer-events: none;
}
#hud > * { pointer-events: auto; }
#leaderboard {
position: fixed; top: 0; right: 0; bottom: 0; width: 340px; max-width: 90vw; z-index: 40;
transform: translateX(100%); transition: transform 0.4s cubic-bezier(0.22, 1, 0.36, 1);
background: rgba(10,20,10,0.95); backdrop-filter: blur(20px);
border-left: 1px solid var(--border); overflow-y: auto; padding: 24px;
}
#leaderboard.open { transform: translateX(0); }
.lb-entry { display: flex; align-items: center; gap: 12px; padding: 10px 14px; border-radius: 10px; margin-bottom: 6px; transition: background 0.2s; }
.lb-entry:hover { background: rgba(255,255,255,0.04); }
.lb-rank { width: 28px; height: 28px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 900; font-size: 13px; flex-shrink: 0; }
.lb-rank.gold { background: linear-gradient(135deg, #ffd700, #f0a500); color: #1a1a00; }
.lb-rank.silver { background: linear-gradient(135deg, #c0c0c0, #8a8a8a); color: #1a1a1a; }
.lb-rank.bronze { background: linear-gradient(135deg, #cd7f32, #a0522d); color: #fff; }
.lb-rank.normal { background: rgba(255,255,255,0.06); color: var(--muted); }
#toastContainer { position: fixed; bottom: 24px; right: 24px; z-index: 100; display: flex; flex-direction: column; gap: 8px; }
.toast { padding: 12px 20px; border-radius: 10px; font-size: 14px; font-weight: 700; animation: toastIn 0.35s ease-out, toastOut 0.35s 2.5s ease-in forwards; box-shadow: 0 8px 30px rgba(0,0,0,0.4); }
.toast.success { background: linear-gradient(135deg, #1b5e20, #2e7d32); color: #a5d6a7; border: 1px solid rgba(76,175,80,0.3); }
.toast.error { background: linear-gradient(135deg, #b71c1c, #c62828); color: #ef9a9a; border: 1px solid rgba(244,67,54,0.3); }
.toast.info { background: linear-gradient(135deg, #e65100, #f57c00); color: #ffe0b2; border: 1px solid rgba(255,152,0,0.3); }
@keyframes toastIn { from { opacity: 0; transform: translateX(40px); } to { opacity: 1; transform: translateX(0); } }
@keyframes toastOut { from { opacity: 1; } to { opacity: 0; transform: translateY(10px); } }
.count-pop { animation: countPop 0.3s ease-out; }
@keyframes countPop { 0% { transform: scale(1.5); } 100% { transform: scale(1); } }
.float-tomato { position: absolute; font-size: 40px; opacity: 0.1; animation: floatUp linear infinite; pointer-events: none; }
@keyframes floatUp { 0% { transform: translateY(100vh) rotate(0deg); opacity: 0; } 10% { opacity: 0.1; } 90% { opacity: 0.1; } 100% { transform: translateY(-100px) rotate(360deg); opacity: 0; } }
#lbOverlay { position: fixed; inset: 0; background: rgba(0,0,0,0.4); z-index: 35; opacity: 0; visibility: hidden; transition: opacity 0.3s, visibility 0.3s; }
#lbOverlay.open { opacity: 1; visibility: visible; }
.cloud-status { display: flex; align-items: center; gap: 8px; font-size: 12px; font-weight: 700; padding: 8px 14px; border-radius: 8px; margin-top: 20px; }
.cloud-status.off { background: rgba(255,255,255,0.03); color: var(--muted); }
.cloud-status.on { background: rgba(76,175,80,0.1); color: #4caf50; }
.cloud-status.loading { background: rgba(255,215,0,0.1); color: #ffd700; }
.cloud-dot { width: 8px; height: 8px; border-radius: 50%; }
.cloud-dot.off { background: #666; }
.cloud-dot.on { background: #4caf50; box-shadow: 0 0 6px rgba(76,175,80,0.6); }
.cloud-dot.loading { background: #ffd700; animation: pulse 1s infinite; }
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.4; } }
.form-switch { display: flex; gap: 0; margin-bottom: 24px; border-radius: 10px; overflow: hidden; border: 1px solid var(--border); }
.form-switch button { flex: 1; padding: 10px; border: none; background: transparent; color: var(--muted); font-weight: 900; font-size: 14px; cursor: pointer; transition: all 0.2s; font-family: 'Nunito', sans-serif; }
.form-switch button.active { background: var(--accent); color: #fff; }
.high-score-badge { display: inline-flex; align-items: center; gap: 6px; padding: 6px 14px; border-radius: 8px; background: rgba(255,215,0,0.1); border: 1px solid rgba(255,215,0,0.2); color: #ffd700; font-size: 13px; font-weight: 700; }
::-webkit-scrollbar { width: 6px; } ::-webkit-scrollbar-track { background: transparent; } ::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 3px; }
@media (max-width: 640px) { #leaderboard { width: 100%; } .login-card { margin: 16px; padding: 24px !important; } }
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } }
</style>
</head>
<body>
<div id="loginScreen" class="screen">
<div id="floatingTomatoes" aria-hidden="true"></div>
<div class="login-card glass" style="padding: 40px; max-width: 420px; width: 100%; position: relative; z-index: 2;">
<div style="text-align: center; margin-bottom: 32px;">
<div class="bangers" style="font-size: 52px; line-height: 1; color: var(--accent); text-shadow: 0 4px 20px rgba(255,61,0,0.3);">THROW TOMATOES</div>
<div class="bangers" style="font-size: 32px; color: var(--fg); margin-top: 4px;">at Lay</div>
<div style="margin-top: 12px; color: var(--muted); font-size: 14px;">Create an account to save your scores</div>
</div>
<div class="form-switch">
<button id="loginTabBtn" class="active" onclick="switchForm('login')">LOGIN</button>
<button id="registerTabBtn" onclick="switchForm('register')">REGISTER</button>
</div>
<form id="loginForm" autocomplete="off">
<div style="margin-bottom: 16px;">
<label style="font-size: 13px; font-weight: 700; color: var(--muted); display: block; margin-bottom: 6px;"><i class="fas fa-user" style="margin-right: 4px;"></i> USERNAME</label>
<input type="text" id="usernameInput" class="input-field" placeholder="Enter your name..." maxlength="20" required aria-label="Username">
</div>
<div style="margin-bottom: 24px;">
<label style="font-size: 13px; font-weight: 700; color: var(--muted); display: block; margin-bottom: 6px;"><i class="fas fa-key" style="margin-right: 4px;"></i> PASSWORD</label>
<input type="password" id="passwordInput" class="input-field" placeholder="Enter your password..." maxlength="30" required aria-label="Password">
</div>
<div id="loginHighScore" style="display: none; margin-bottom: 16px; text-align: center;"></div>
<button type="submit" id="loginBtn" class="btn-primary" style="width: 100%; font-size: 18px; padding: 14px;"><i class="fas fa-right-to-bracket" style="margin-right: 8px;"></i> LOGIN</button>
</form>
<form id="registerForm" style="display: none;" autocomplete="off">
<div style="margin-bottom: 16px;">
<label style="font-size: 13px; font-weight: 700; color: var(--muted); display: block; margin-bottom: 6px;"><i class="fas fa-user" style="margin-right: 4px;"></i> USERNAME</label>
<input type="text" id="regUsernameInput" class="input-field" placeholder="Choose a name (2-20 chars)..." maxlength="20" required aria-label="Username">
</div>
<div style="margin-bottom: 16px;">
<label style="font-size: 13px; font-weight: 700; color: var(--muted); display: block; margin-bottom: 6px;"><i class="fas fa-key" style="margin-right: 4px;"></i> PASSWORD</label>
<input type="password" id="regPasswordInput" class="input-field" placeholder="Choose a password (min 4 chars)..." maxlength="30" required aria-label="Password">
</div>
<div style="margin-bottom: 24px;">
<label style="font-size: 13px; font-weight: 700; color: var(--muted); display: block; margin-bottom: 6px;"><i class="fas fa-key" style="margin-right: 4px;"></i> CONFIRM PASSWORD</label>
<input type="password" id="regConfirmInput" class="input-field" placeholder="Confirm your password..." maxlength="30" required aria-label="Confirm Password">
</div>
<button type="submit" id="registerBtn" class="btn-primary" style="width: 100%; font-size: 18px; padding: 14px;"><i class="fas fa-user-plus" style="margin-right: 8px;"></i> CREATE ACCOUNT</button>
</form>
<div id="cloudStatus" class="cloud-status loading" style="justify-content: center;">
<span id="cloudDot" class="cloud-dot loading"></span>
<span id="cloudText">Connecting to cloud...</span>
</div>
</div>
</div>
<canvas id="gameCanvas"></canvas>
<div id="hud" style="display: none;">
<div class="glass" style="padding: 8px 18px; display: flex; align-items: center; gap: 10px; border-radius: 12px;"><i class="fas fa-user" style="color: var(--accent); font-size: 13px;"></i><span id="hudUsername" style="font-weight: 900; font-size: 14px;"></span></div>
<div class="glass" style="padding: 8px 18px; display: flex; align-items: center; gap: 8px; border-radius: 12px;"><span style="font-size: 20px;">๐Ÿ…</span><span id="hudCount" class="bangers" style="font-size: 22px; color: var(--accent);">0</span></div>
<div id="hudHighScore" class="glass" style="padding: 8px 14px; display: none; align-items: center; gap: 6px; border-radius: 12px;"><span style="font-size: 14px;">๐Ÿ†</span><span id="hudHighScoreVal" class="bangers" style="font-size: 16px; color: #ffd700;">0</span></div>
<div style="flex: 1;"></div>
<button id="lbBtn" class="btn-ghost" aria-label="Open leaderboard"><i class="fas fa-trophy" style="margin-right: 6px; color: #ffd700;"></i> Leaderboard</button>
<button id="logoutBtn" class="btn-ghost" aria-label="Logout"><i class="fas fa-right-from-bracket"></i></button>
</div>
<div id="instructions" style="display: none; position: fixed; bottom: 40px; left: 50%; transform: translateX(-50%); z-index: 30; pointer-events: none;">
<div class="glass" style="padding: 12px 24px; text-align: center;"><span style="font-weight: 700; color: var(--muted); font-size: 14px;"><i class="fas fa-hand-pointer" style="color: var(--accent); margin-right: 6px;"></i>Click anywhere to throw tomatoes at Lay</span></div>
</div>
<div id="lbOverlay"></div>
<div id="leaderboard" role="dialog" aria-label="Leaderboard">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<div class="bangers" style="font-size: 24px; color: #ffd700;"><i class="fas fa-trophy" style="margin-right: 8px;"></i> TOP THROWERS</div>
<button id="closeLbBtn" style="background: none; border: none; color: var(--muted); cursor: pointer; font-size: 20px; padding: 4px;" aria-label="Close leaderboard"><i class="fas fa-xmark"></i></button>
</div>
<div id="lbContent"></div>
<div style="margin-top: 20px; padding-top: 16px; border-top: 1px solid rgba(255,255,255,0.06);"><button id="refreshLbBtn" class="btn-ghost" style="width: 100%;"><i class="fas fa-arrows-rotate" style="margin-right: 6px;"></i> Refresh</button></div>
</div>
<div id="toastContainer" aria-live="polite"></div>
<script>
const $ = id => document.getElementById(id);
const API_URL = 'https://tomato.m-calculator.workers.dev';
let sessionToken = null;
let currentUser = null;
let currentHighScore = 0;
let cloudConnected = false;
function showToast(msg, type = 'info') {
const t = document.createElement('div');
t.className = 'toast ' + type;
t.textContent = msg;
$('toastContainer').appendChild(t);
setTimeout(() => t.remove(), 3200);
}
async function apiRequest(endpoint, options = {}) {
try {
const url = API_URL.replace(/\/+$/, '') + endpoint;
const headers = { 'Content-Type': 'application/json' };
if (sessionToken) headers['Authorization'] = 'Bearer ' + sessionToken;
const res = await fetch(url, { ...options, headers });
const data = await res.json();
if (!res.ok) {
showToast(data.error || 'Request failed', 'error');
return null;
}
return data;
} catch (e) {
showToast('Network error', 'error');
return null;
}
}
function switchForm(mode) {
if (mode === 'login') {
$('loginForm').style.display = 'block';
$('registerForm').style.display = 'none';
$('loginTabBtn').classList.add('active');
$('registerTabBtn').classList.remove('active');
$('loginHighScore').style.display = 'none';
} else {
$('loginForm').style.display = 'none';
$('registerForm').style.display = 'block';
$('loginTabBtn').classList.remove('active');
$('registerTabBtn').classList.add('active');
}
}
async function checkCloud() {
try {
const res = await fetch(API_URL.replace(/\/+$/, '') + '/health');
const data = await res.json();
if (data.status === 'ok' && data.kv_connected === true) {
$('cloudStatus').className = 'cloud-status on';
$('cloudDot').className = 'cloud-dot on';
$('cloudText').textContent = 'Cloud connected - Accounts saved';
cloudConnected = true;
return true;
} else {
$('cloudStatus').className = 'cloud-status off';
$('cloudDot').className = 'cloud-dot off';
$('cloudText').textContent = 'Cloud error: KV database missing';
return false;
}
} catch (e) {
$('cloudStatus').className = 'cloud-status off';
$('cloudDot').className = 'cloud-dot off';
$('cloudText').textContent = 'Offline mode - Create account to save';
return false;
}
}
// Check if username exists and show high score on focus out
$('usernameInput').addEventListener('blur', async () => {
const username = $('usernameInput').value.trim();
if (username.length < 2) return;
const res = await apiRequest('/login', {
method: 'POST',
body: JSON.stringify({ username, password: 'check' })
});
if (res === null) return; // Network error already handled
// If error says "User not found", show register hint
// If error says "Incorrect password", user exists - we can't get high score without correct password
$('loginHighScore').style.display = 'none';
});
$('registerForm').addEventListener('submit', async (e) => {
e.preventDefault();
const username = $('regUsernameInput').value.trim();
const password = $('regPasswordInput').value;
const confirm = $('regConfirmInput').value;
if (!username || username.length < 2) {
showToast('Username needs at least 2 characters', 'error');
return;
}
if (!password || password.length < 4) {
showToast('Password needs at least 4 characters', 'error');
return;
}
if (password !== confirm) {
showToast('Passwords do not match', 'error');
return;
}
const btn = $('registerBtn');
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin" style="margin-right:8px;"></i> CREATING...';
const res = await apiRequest('/register', { method: 'POST', body: JSON.stringify({ username, password }) });
if (res && res.token) {
sessionToken = res.token;
currentUser = res.username;
currentHighScore = 0;
showToast('Account created! Welcome, ' + currentUser + '!', 'success');
startGame();
}
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-user-plus" style="margin-right:8px;"></i> CREATE ACCOUNT';
});
$('loginForm').addEventListener('submit', async (e) => {
e.preventDefault();
const username = $('usernameInput').value.trim();
const password = $('passwordInput').value;
if (!username || username.length < 2) {
showToast('Username needs at least 2 characters', 'error');
return;
}
if (!password) {
showToast('Enter your password', 'error');
return;
}
const btn = $('loginBtn');
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin" style="margin-right:8px;"></i> LOGGING IN...';
const res = await apiRequest('/login', { method: 'POST', body: JSON.stringify({ username, password }) });
if (res && res.token) {
sessionToken = res.token;
currentUser = res.username;
currentHighScore = res.highScore || 0;
showToast('Welcome back, ' + currentUser + '!', 'success');
startGame();
}
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-right-to-bracket" style="margin-right:8px;"></i> LOGIN';
});
function logout() {
if (gameRunning && currentUser) persistScore();
currentUser = null;
sessionToken = null;
currentHighScore = 0;
gameRunning = false;
$('hud').style.display = 'none';
$('instructions').style.display = 'none';
$('loginScreen').classList.remove('hidden');
$('usernameInput').value = '';
$('passwordInput').value = '';
$('regUsernameInput').value = '';
$('regPasswordInput').value = '';
$('regConfirmInput').value = '';
$('loginHighScore').style.display = 'none';
switchForm('login');
setTimeout(() => $('usernameInput').focus(), 100);
}
function toggleLeaderboard() {
const lb = $('leaderboard'), ov = $('lbOverlay');
if (lb.classList.contains('open')) { lb.classList.remove('open'); ov.classList.remove('open'); }
else { lb.classList.add('open'); ov.classList.add('open'); refreshLeaderboard(); }
}
async function refreshLeaderboard() {
const c = $('lbContent');
c.innerHTML = '<div style="text-align:center;padding:30px;color:var(--muted);"><i class="fas fa-spinner fa-spin"></i></div>';
let entries = null;
if (cloudConnected) {
entries = await apiRequest('/leaderboard');
}
if (!entries) {
c.innerHTML = '<div style="text-align:center;padding:30px;color:var(--muted);font-size:14px;">Unable to load leaderboard</div>';
return;
}
const list = entries.entries || [];
if (list.length === 0) {
c.innerHTML = '<div style="text-align:center;padding:30px;color:var(--muted);font-size:14px;">No scores yet. Start throwing!</div>';
return;
}
c.innerHTML = list.map((e, i) => {
const rc = i === 0 ? 'gold' : i === 1 ? 'silver' : i === 2 ? 'bronze' : 'normal';
const me = e.username === currentUser;
return `<div class="lb-entry" style="${me ? 'background:rgba(255,61,0,0.08);border:1px solid rgba(255,61,0,0.15);' : ''}"><div class="lb-rank ${rc}">${i+1}</div><div style="flex:1;min-width:0;"><div style="font-weight:900;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${e.username}${me ? ' <span style="color:var(--accent);font-size:11px;">(YOU)</span>' : ''}</div></div><div style="display:flex;align-items:center;gap:6px;"><span style="font-size:14px;">๐Ÿ…</span><span class="bangers" style="font-size:18px;color:${i<3?'#ffd700':'var(--fg)'};">${e.score}</span></div></div>`;
}).join('');
}
// GAME ENGINE
const canvas = $('gameCanvas');
const ctx = canvas.getContext('2d');
let W, H, gameRunning = false, tomatoCount = 0, lastSaveTime = 0;
const SAVE_INTERVAL = 3000;
let tomatoes = [], splats = [], particles = [], floatingTexts = [], bgParticles = [];
let lay = { x: 0, y: 0, width: 120, height: 220, hitScale: 1, shakeX: 0, shakeY: 0, expression: 'normal', expressionTimer: 0 };
let audioCtx = null;
function getAudio() { if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)(); return audioCtx; }
function playSplatSound() { try { const c=getAudio(),b=c.createBuffer(1,c.sampleRate*0.12,c.sampleRate),d=b.getChannelData(0); for(let i=0;i<d.length;i++)d[i]=(Math.random()*2-1)*Math.exp(-i/c.sampleRate*25)*0.4; const s=c.createBufferSource();s.buffer=b;const f=c.createBiquadFilter();f.type='lowpass';f.frequency.value=800;s.connect(f);f.connect(c.destination);s.start();}catch(e){} }
function playThrowSound() { try { const c=getAudio(),o=c.createOscillator(),g=c.createGain();o.type='sine';o.frequency.setValueAtTime(300,c.currentTime);o.frequency.exponentialRampToValueAtTime(150,c.currentTime+0.15);g.gain.setValueAtTime(0.08,c.currentTime);g.gain.exponentialRampToValueAtTime(0.001,c.currentTime+0.15);o.connect(g);g.connect(c.destination);o.start();o.stop(c.currentTime+0.15);}catch(e){} }
function resize() { W=canvas.width=window.innerWidth;H=canvas.height=window.innerHeight;lay.x=W/2;lay.y=H/2+20;lay.width=Math.min(120,W*0.15);lay.height=lay.width*1.83; }
window.addEventListener('resize', resize);
function initBgParticles() { bgParticles=[]; for(let i=0;i<40;i++) bgParticles.push({x:Math.random()*W,y:Math.random()*H,r:Math.random()*2+0.5,vx:(Math.random()-0.5)*0.3,vy:(Math.random()-0.5)*0.3,alpha:Math.random()*0.15+0.03}); }
function throwTomato(tx, ty) {
const sx=tx+(Math.random()-0.5)*100,sy=H+40,sx2=sx+(Math.random()-0.5)*60;
tomatoes.push({sx,sy,sx2,sy2:sy-(H*0.6+Math.random()*100),ex:tx+(Math.random()-0.5)*20,ey:ty+(Math.random()-0.5)*20,x:sx,y:sy,t:0,duration:0.5+Math.random()*0.2,rotation:0,rotSpeed:(Math.random()-0.5)*15,size:18+Math.random()*8,shadow:1});
playThrowSound();
}
function createSplat(x,y,size) {
const drops=[],num=8+Math.floor(Math.random()*6);
for(let i=0;i<num;i++){const a=(Math.PI*2/num)*i+(Math.random()-0.5)*0.5,d=size*(0.8+Math.random()*1.2);drops.push({dx:Math.cos(a)*d,dy:Math.sin(a)*d*0.6,r:size*(0.15+Math.random()*0.25)});}
splats.push({x,y,drops,alpha:1,size});
for(let i=0;i<12;i++){const a=Math.random()*Math.PI*2,s=2+Math.random()*5;particles.push({x,y,vx:Math.cos(a)*s,vy:Math.sin(a)*s-2,r:2+Math.random()*4,alpha:1,color:Math.random()>0.3?'#e53935':'#c62828',gravity:0.15});}
for(let i=0;i<4;i++){const a=Math.random()*Math.PI*2,s=1+Math.random()*3;particles.push({x,y,vx:Math.cos(a)*s,vy:Math.sin(a)*s-1,r:1.5,alpha:1,color:'#5d4037',gravity:0.12});}
}
function addFloatingText(x,y,text) { floatingTexts.push({x,y,text,vy:-2,alpha:1,scale:1.5}); }
function drawTomato(x,y,size,rot) {
ctx.save();ctx.translate(x,y);ctx.rotate(rot);
ctx.beginPath();ctx.ellipse(2,size*0.1,size*0.8,size*0.3,0,0,Math.PI*2);ctx.fillStyle='rgba(0,0,0,0.2)';ctx.fill();
ctx.beginPath();ctx.arc(0,0,size,0,Math.PI*2);
const g=ctx.createRadialGradient(-size*0.3,-size*0.3,size*0.1,0,0,size);g.addColorStop(0,'#ff6659');g.addColorStop(0.5,'#e53935');g.addColorStop(1,'#b71c1c');ctx.fillStyle=g;ctx.fill();
ctx.beginPath();ctx.ellipse(-size*0.25,-size*0.3,size*0.3,size*0.2,-0.4,0,Math.PI*2);ctx.fillStyle='rgba(255,255,255,0.35)';ctx.fill();
ctx.beginPath();ctx.moveTo(-3,-size+2);ctx.quadraticCurveTo(0,-size-8,3,-size+2);ctx.strokeStyle='#2e7d32';ctx.lineWidth=2.5;ctx.lineCap='round';ctx.stroke();
ctx.beginPath();ctx.ellipse(6,-size+1,7,3,0.5,0,Math.PI*2);ctx.fillStyle='#43a047';ctx.fill();
ctx.beginPath();ctx.ellipse(-5,-size+2,6,2.5,-0.5,0,Math.PI*2);ctx.fillStyle='#388e3c';ctx.fill();
ctx.restore();
}
function drawLay() {
const x=lay.x+lay.shakeX,y=lay.y+lay.shakeY,w=lay.width*lay.hitScale,h=lay.height*lay.hitScale;
ctx.save();ctx.translate(x,y);
ctx.beginPath();ctx.ellipse(0,h*0.42,w*0.5,12,0,0,Math.PI*2);ctx.fillStyle='rgba(0,0,0,0.2)';ctx.fill();
ctx.fillStyle='#2c3e50';ctx.beginPath();ctx.roundRect(-w*0.22,h*0.18,w*0.16,h*0.25,4);ctx.fill();ctx.beginPath();ctx.roundRect(w*0.06,h*0.18,w*0.16,h*0.25,4);ctx.fill();
ctx.fillStyle='#1a1a2e';ctx.beginPath();ctx.ellipse(-w*0.14,h*0.43,w*0.12,6,0,0,Math.PI*2);ctx.fill();ctx.beginPath();ctx.ellipse(w*0.14,h*0.43,w*0.12,6,0,0,Math.PI*2);ctx.fill();
const sg=ctx.createLinearGradient(-w*0.3,-h*0.15,w*0.3,h*0.2);sg.addColorStop(0,'#f5f5f5');sg.addColorStop(1,'#e0e0e0');ctx.fillStyle=sg;ctx.beginPath();ctx.roundRect(-w*0.32,-h*0.15,w*0.64,h*0.37,8);ctx.fill();
ctx.beginPath();ctx.moveTo(-w*0.08,-h*0.15);ctx.lineTo(0,-h*0.08);ctx.lineTo(w*0.08,-h*0.15);ctx.strokeStyle='#ccc';ctx.lineWidth=1.5;ctx.stroke();
ctx.fillStyle='#f5f5f5';
ctx.save();ctx.translate(-w*0.32,-h*0.05);ctx.rotate(lay.expression==='scared'?-0.5:-0.15);ctx.beginPath();ctx.roundRect(-w*0.06,-w*0.04,w*0.08,h*0.22,4);ctx.fill();ctx.beginPath();ctx.arc(-w*0.02,h*0.18,w*0.06,0,Math.PI*2);ctx.fillStyle='#ffcc80';ctx.fill();ctx.restore();
ctx.save();ctx.translate(w*0.32,-h*0.05);ctx.rotate(lay.expression==='scared'?0.5:0.15);ctx.fillStyle='#f5f5f5';ctx.beginPath();ctx.roundRect(-w*0.02,-w*0.04,w*0.08,h*0.22,4);ctx.fill();ctx.beginPath();ctx.arc(w*0.02,h*0.18,w*0.06,0,Math.PI*2);ctx.fillStyle='#ffcc80';ctx.fill();ctx.restore();
const hr=w*0.35;ctx.beginPath();ctx.arc(0,-h*0.32,hr,0,Math.PI*2);const sk=ctx.createRadialGradient(-hr*0.2,-h*0.32-hr*0.2,2,0,-h*0.32,hr);sk.addColorStop(0,'#ffe0b2');sk.addColorStop(1,'#ffcc80');ctx.fillStyle=sk;ctx.fill();
ctx.beginPath();ctx.ellipse(0,-h*0.32-hr*0.6,hr*1.05,hr*0.55,0,Math.PI,Math.PI*2);ctx.fillStyle='#2c2c2c';ctx.fill();
ctx.beginPath();ctx.ellipse(-hr*0.9,-h*0.32,hr*0.2,hr*0.5,0.1,0,Math.PI*2);ctx.fill();ctx.beginPath();ctx.ellipse(hr*0.9,-h*0.32,hr*0.2,hr*0.5,-0.1,0,Math.PI*2);ctx.fill();
const eY=-h*0.33,eS=hr*0.35;
if(lay.expression==='hit'||lay.expression==='scared'){const xs=hr*0.12;ctx.strokeStyle='#333';ctx.lineWidth=2.5;ctx.lineCap='round';[-1,1].forEach(s=>{const ex=s*eS;ctx.beginPath();ctx.moveTo(ex-xs,eY-xs);ctx.lineTo(ex+xs,eY+xs);ctx.stroke();ctx.beginPath();ctx.moveTo(ex+xs,eY-xs);ctx.lineTo(ex-xs,eY+xs);ctx.stroke();});}
else{[-1,1].forEach(s=>{const ex=s*eS;ctx.beginPath();ctx.ellipse(ex,eY,hr*0.15,hr*0.12,0,0,Math.PI*2);ctx.fillStyle='#fff';ctx.fill();ctx.beginPath();ctx.arc(ex+s*1,eY,hr*0.07,0,Math.PI*2);ctx.fillStyle='#333';ctx.fill();ctx.beginPath();ctx.arc(ex+s*1+2,eY-2,hr*0.025,0,Math.PI*2);ctx.fillStyle='#fff';ctx.fill();});if(lay.expression==='annoyed'){ctx.strokeStyle='#333';ctx.lineWidth=2.5;ctx.lineCap='round';ctx.beginPath();ctx.moveTo(-eS-hr*0.15,eY-hr*0.18);ctx.lineTo(-eS+hr*0.12,eY-hr*0.22);ctx.stroke();ctx.beginPath();ctx.moveTo(eS+hr*0.15,eY-hr*0.18);ctx.lineTo(eS-hr*0.12,eY-hr*0.22);ctx.stroke();}}
ctx.beginPath();ctx.moveTo(0,eY+hr*0.15);ctx.lineTo(-3,eY+hr*0.25);ctx.lineTo(3,eY+hr*0.25);ctx.strokeStyle='#e0a060';ctx.lineWidth=1.5;ctx.stroke();
const mY=eY+hr*0.4;
if(lay.expression==='hit'){ctx.beginPath();ctx.moveTo(-hr*0.2,mY);ctx.quadraticCurveTo(-hr*0.1,mY-4,0,mY);ctx.quadraticCurveTo(hr*0.1,mY+4,hr*0.2,mY);ctx.strokeStyle='#333';ctx.lineWidth=2;ctx.stroke();}
else if(lay.expression==='annoyed'){ctx.beginPath();ctx.arc(0,mY+hr*0.15,hr*0.18,Math.PI+0.3,-0.3);ctx.strokeStyle='#333';ctx.lineWidth=2;ctx.stroke();}
else if(lay.expression==='scared'){ctx.beginPath();ctx.ellipse(0,mY+3,hr*0.1,hr*0.14,0,0,Math.PI*2);ctx.fillStyle='#333';ctx.fill();ctx.beginPath();ctx.ellipse(0,mY+3,hr*0.07,hr*0.1,0,0,Math.PI*2);ctx.fillStyle='#c62828';ctx.fill();}
else{ctx.beginPath();ctx.arc(0,mY-2,hr*0.15,0.2,Math.PI-0.2);ctx.strokeStyle='#333';ctx.lineWidth=2;ctx.stroke();}
const st=[{x:-w*0.15,y:-h*0.02,r:w*0.08},{x:w*0.2,y:h*0.05,r:w*0.06},{x:-w*0.05,y:h*0.1,r:w*0.07},{x:w*0.1,y:-h*0.08,r:w*0.05},{x:-w*0.25,y:h*0.15,r:w*0.06}];
st.forEach((s,i)=>{if(i<Math.min(splats.length,5)){ctx.beginPath();ctx.arc(s.x,s.y,s.r,0,Math.PI*2);ctx.fillStyle=`rgba(183,28,28,${0.5+Math.min(splats.length*0.03,0.3)})`;ctx.fill();}});
ctx.fillStyle='rgba(0,0,0,0.5)';ctx.beginPath();ctx.roundRect(-w*0.4,h*0.48,w*0.8,22,6);ctx.fill();ctx.fillStyle='#fff';ctx.font=`900 ${Math.max(12,w*0.14)}px Nunito`;ctx.textAlign='center';ctx.textBaseline='middle';ctx.fillText('LAY',0,h*0.49+11);
ctx.restore();
}
function drawBackground() {
const bg=ctx.createLinearGradient(0,0,0,H);bg.addColorStop(0,'#0a140a');bg.addColorStop(0.5,'#0d1a0d');bg.addColorStop(1,'#10150a');ctx.fillStyle=bg;ctx.fillRect(0,0,W,H);
const sp=ctx.createRadialGradient(lay.x,lay.y-50,10,lay.x,lay.y-50,Math.max(W,H)*0.5);sp.addColorStop(0,'rgba(255,200,100,0.06)');sp.addColorStop(0.5,'rgba(255,150,50,0.02)');sp.addColorStop(1,'transparent');ctx.fillStyle=sp;ctx.fillRect(0,0,W,H);
const flY=lay.y+lay.height*0.45;ctx.fillStyle='rgba(255,255,255,0.02)';ctx.fillRect(0,flY,W,H-flY);ctx.strokeStyle='rgba(255,255,255,0.05)';ctx.lineWidth=1;ctx.beginPath();ctx.moveTo(0,flY);ctx.lineTo(W,flY);ctx.stroke();
bgParticles.forEach(p=>{ctx.beginPath();ctx.arc(p.x,p.y,p.r,0,Math.PI*2);ctx.fillStyle=`rgba(255,200,100,${p.alpha})`;ctx.fill();});
}
function drawSplats() { splats.forEach(s=>{ctx.globalAlpha=s.alpha;ctx.beginPath();ctx.arc(s.x,s.y,s.size*0.6,0,Math.PI*2);ctx.fillStyle='#c62828';ctx.fill();s.drops.forEach(d=>{ctx.beginPath();ctx.arc(s.x+d.dx,s.y+d.dy,Math.max(1,d.r),0,Math.PI*2);ctx.fillStyle='#d32f2f';ctx.fill();});ctx.beginPath();ctx.arc(s.x,s.y,s.size*0.3,0,Math.PI*2);ctx.fillStyle='#e53935';ctx.fill();ctx.globalAlpha=1;}); }
function drawParticles() { particles.forEach(p=>{ctx.globalAlpha=p.alpha;ctx.beginPath();ctx.arc(p.x,p.y,Math.max(0.5,p.r),0,Math.PI*2);ctx.fillStyle=p.color;ctx.fill();ctx.globalAlpha=1;}); }
function drawFloatingTexts() { floatingTexts.forEach(ft=>{ctx.globalAlpha=ft.alpha;ctx.save();ctx.translate(ft.x,ft.y);ctx.scale(ft.scale,ft.scale);ctx.font='900 24px Bangers';ctx.textAlign='center';ctx.strokeStyle='rgba(0,0,0,0.5)';ctx.lineWidth=3;ctx.strokeText(ft.text,0,0);ctx.fillStyle='#ff6659';ctx.fillText(ft.text,0,0);ctx.restore();ctx.globalAlpha=1;}); }
function update(dt) {
bgParticles.forEach(p=>{p.x+=p.vx;p.y+=p.vy;if(p.x<0)p.x=W;if(p.x>W)p.x=0;if(p.y<0)p.y=H;if(p.y>H)p.y=0;});
lay.shakeX*=0.85;lay.shakeY*=0.85;lay.hitScale+=(1-lay.hitScale)*0.15;
if(lay.expressionTimer>0){lay.expressionTimer-=dt;if(lay.expressionTimer<=0){lay.expression=splats.length>15?'scared':splats.length>5?'annoyed':'normal';if(lay.expression!=='normal')lay.expressionTimer=lay.expression==='scared'?2:3;}}
tomatoes.forEach(t=>{t.t+=dt/t.duration;if(t.t>1)t.t=1;const p=t.t,o=1-p;t.x=o*o*t.sx+2*o*p*t.sx2+p*p*t.ex;t.y=o*o*t.sy+2*o*p*t.sy2+p*p*t.ey;t.rotation+=t.rotSpeed*dt;t.shadow=Math.max(0.1,1-((lay.y+lay.height*0.45)-t.y)/H);});
const done=tomatoes.filter(t=>t.t>=1);
done.forEach(t=>{if(Math.abs(t.ex-lay.x)<lay.width*0.8&&Math.abs(t.ey-lay.y)<lay.height*0.7){createSplat(t.ex,t.ey,t.size);playSplatSound();tomatoCount++;updateHUD();lay.shakeX=(Math.random()-0.5)*12;lay.shakeY=(Math.random()-0.5)*8;lay.hitScale=0.92;lay.expression='hit';lay.expressionTimer=0.4;if(tomatoCount%10===0)addFloatingText(t.ex,t.ey-30,tomatoCount+' TOMATOES!');else if(tomatoCount%5===0)addFloatingText(t.ex,t.ey-20,'+5 COMBO!');const now=Date.now();if(now-lastSaveTime>SAVE_INTERVAL){lastSaveTime=now;persistScore();}}});
tomatoes=tomatoes.filter(t=>t.t<1);
splats.forEach(s=>{s.alpha-=dt*0.008;});splats=splats.filter(s=>s.alpha>0.05);if(splats.length>30)splats=splats.slice(-30);
particles.forEach(p=>{p.x+=p.vx;p.y+=p.vy;p.vy+=p.gravity;p.alpha-=dt*1.5;p.r*=0.99;});particles=particles.filter(p=>p.alpha>0.01);
floatingTexts.forEach(ft=>{ft.y+=ft.vy;ft.alpha-=dt*0.8;ft.scale+=(1-ft.scale)*0.1;});floatingTexts=floatingTexts.filter(ft=>ft.alpha>0.01);
}
function render() {
ctx.clearRect(0,0,W,H);drawBackground();drawSplats();
const flY=lay.y+lay.height*0.45;tomatoes.forEach(t=>{ctx.globalAlpha=t.shadow*0.3;ctx.beginPath();ctx.ellipse(t.x,flY,t.size*0.5,t.size*0.15,0,0,Math.PI*2);ctx.fillStyle='#000';ctx.fill();ctx.globalAlpha=1;});
drawLay();tomatoes.forEach(t=>{drawTomato(t.x,t.y,t.size,t.rotation);});drawParticles();drawFloatingTexts();
}
let lastTime=0;
function gameLoop(ts){if(!gameRunning)return;const dt=Math.min((ts-lastTime)/1000,0.05);lastTime=ts;update(dt);render();requestAnimationFrame(gameLoop);}
function updateHUD(){
const el=$('hudCount');
el.textContent=tomatoCount;
el.classList.remove('count-pop');
void el.offsetWidth;
el.classList.add('count-pop');
// Update high score display if current count exceeds it
if(tomatoCount > currentHighScore) {
$('hudHighScoreVal').textContent = tomatoCount;
}
}
async function persistScore(){
if(!cloudConnected || !sessionToken) return;
const res = await apiRequest('/score', {method:'POST', body:JSON.stringify({score:tomatoCount})});
if(res && res.isNewHighScore) {
currentHighScore = res.score;
$('hudHighScoreVal').textContent = currentHighScore;
showToast('New High Score: ' + currentHighScore + ' ๐Ÿ…', 'success');
}
}
function startGame() {
$('loginScreen').classList.add('hidden');
$('hud').style.display='flex';
$('hudUsername').textContent=currentUser;
// Show high score if exists
if(currentHighScore > 0) {
$('hudHighScore').style.display = 'flex';
$('hudHighScoreVal').textContent = currentHighScore;
} else {
$('hudHighScore').style.display = 'none';
}
tomatoCount=0;tomatoes=[];splats=[];particles=[];floatingTexts=[];lay.hitScale=1;lay.shakeX=0;lay.shakeY=0;lay.expression='normal';lay.expressionTimer=0;
resize();initBgParticles();updateHUD();gameRunning=true;lastTime=performance.now();requestAnimationFrame(gameLoop);
$('instructions').style.display='block';
setTimeout(()=>{$('instructions').style.display='none';},4000);
}
canvas.addEventListener('click',e=>{if(gameRunning)throwTomato(e.clientX,e.clientY);});
canvas.addEventListener('touchstart',e=>{if(!gameRunning)return;e.preventDefault();throwTomato(e.touches[0].clientX,e.touches[0].clientY);},{passive:false});
window.addEventListener('beforeunload',()=>{if(gameRunning&&currentUser)persistScore();});
$('lbBtn').addEventListener('click', toggleLeaderboard);
$('logoutBtn').addEventListener('click', logout);
$('closeLbBtn').addEventListener('click', toggleLeaderboard);
$('lbOverlay').addEventListener('click', toggleLeaderboard);
$('refreshLbBtn').addEventListener('click', refreshLeaderboard);
(function(){const c=$('floatingTomatoes');for(let i=0;i<12;i++){const el=document.createElement('div');el.className='float-tomato';el.textContent='๐Ÿ…';el.style.left=Math.random()*100+'%';el.style.fontSize=(24+Math.random()*30)+'px';el.style.animationDuration=(8+Math.random()*10)+'s';el.style.animationDelay=Math.random()*10+'s';c.appendChild(el);}})();
resize();
checkCloud();
setTimeout(()=>$('usernameInput').focus(),300);
</script>
</body>
</html>