import React, { useEffect, useMemo, useRef, useState } from "react"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { Play, Info, Volume2, VolumeX, House, Pause, RotateCcw, Trophy, Sparkles } from "lucide-react"; const GAME_W = 360; const GAME_H = 640; const PLAYER_SIZE = 34; const FALL_SPEED_BASE = 170; const SPAWN_BASE = 680; function clamp(n: number, min: number, max: number) { return Math.max(min, Math.min(max, n)); } type Screen = "menu" | "how" | "playing" | "paused" | "gameover"; type ItemKind = "gem" | "bomb" | "slow" | "shield" | "star"; type Item = { id: number; x: number; y: number; size: number; vy: number; kind: ItemKind; rotation: number; }; type Particle = { id: number; x: number; y: number; vx: number; vy: number; life: number; hue: number; }; function useAudio(enabled: boolean) { const ctxRef = useRef(null); const ensure = () => { if (typeof window === "undefined") return null; if (!ctxRef.current) { const Ctx = window.AudioContext || (window as any).webkitAudioContext; if (!Ctx) return null; ctxRef.current = new Ctx(); } if (ctxRef.current.state === "suspended") { ctxRef.current.resume(); } return ctxRef.current; }; const beep = (freq: number, duration = 0.08, type: OscillatorType = "sine", gainValue = 0.03) => { if (!enabled) return; const ctx = ensure(); if (!ctx) return; const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.type = type; osc.frequency.value = freq; gain.gain.value = gainValue; osc.connect(gain); gain.connect(ctx.destination); osc.start(); gain.gain.exponentialRampToValueAtTime(0.0001, ctx.currentTime + duration); osc.stop(ctx.currentTime + duration); }; return { tap: () => beep(420, 0.04, "triangle", 0.025), collect: () => { beep(740, 0.05, "triangle", 0.025); setTimeout(() => beep(980, 0.05, "sine", 0.02), 35); }, hit: () => { beep(180, 0.09, "sawtooth", 0.035); setTimeout(() => beep(120, 0.08, "square", 0.022), 45); }, shield: () => { beep(520, 0.05, "sine", 0.02); setTimeout(() => beep(640, 0.06, "triangle", 0.018), 40); }, level: () => { beep(500, 0.08, "triangle", 0.024); setTimeout(() => beep(680, 0.08, "triangle", 0.018), 50); setTimeout(() => beep(880, 0.1, "sine", 0.018), 110); }, }; } export default function MobileBrowserGameLanding() { const [screen, setScreen] = useState("menu"); const [muted, setMuted] = useState(false); const [score, setScore] = useState(0); const [best, setBest] = useState(0); const [energy, setEnergy] = useState(100); const [combo, setCombo] = useState(0); const [level, setLevel] = useState(1); const [shield, setShield] = useState(0); const [slow, setSlow] = useState(0); const [playerX, setPlayerX] = useState(GAME_W / 2 - PLAYER_SIZE / 2); const [items, setItems] = useState([]); const [particles, setParticles] = useState([]); const [flash, setFlash] = useState(0); const [announcement, setAnnouncement] = useState("Chytej body. Vyhýbej se bombám."); const audio = useAudio(!muted); const rafRef = useRef(null); const lastRef = useRef(0); const spawnTimerRef = useRef(0); const idRef = useRef(1); const leftHeld = useRef(false); const rightHeld = useRef(false); const comboDecayRef = useRef(0); const runningRef = useRef(false); const swipeRef = useRef(null); useEffect(() => { const raw = window.localStorage.getItem("neon-drop-best"); const parsed = raw ? Number(raw) : 0; if (!Number.isNaN(parsed)) setBest(parsed); }, []); useEffect(() => { if (score > best) { setBest(score); window.localStorage.setItem("neon-drop-best", String(score)); } }, [score, best]); const backgroundStars = useMemo( () => Array.from({ length: 28 }, (_, i) => ({ id: i, x: (i * 37) % GAME_W, y: (i * 61) % GAME_H, size: 1 + ((i * 7) % 3), drift: 10 + (i % 5) * 8, })), [] ); const resetGame = () => { setScore(0); setEnergy(100); setCombo(0); setLevel(1); setShield(0); setSlow(0); setPlayerX(GAME_W / 2 - PLAYER_SIZE / 2); setItems([]); setParticles([]); setFlash(0); setAnnouncement("Začni sbírat drahokamy."); spawnTimerRef.current = 0; comboDecayRef.current = 0; }; const spawnItem = () => { const roll = Math.random(); let kind: ItemKind = "gem"; if (roll > 0.82 && roll <= 0.91) kind = "bomb"; else if (roll > 0.91 && roll <= 0.95) kind = "slow"; else if (roll > 0.95 && roll <= 0.98) kind = "shield"; else if (roll > 0.98) kind = "star"; const size = kind === "bomb" ? 30 : kind === "star" ? 28 : 24; const margin = 16; const x = margin + Math.random() * (GAME_W - margin * 2 - size); const levelSpeed = FALL_SPEED_BASE + (level - 1) * 12; const slowFactor = slow > 0 ? 0.72 : 1; setItems((prev) => [ ...prev, { id: idRef.current++, x, y: -40, size, kind, vy: (levelSpeed + Math.random() * 60) * slowFactor, rotation: Math.random() * 360, }, ]); }; const burst = (x: number, y: number, hue: number, count = 10) => { setParticles((prev) => [ ...prev, ...Array.from({ length: count }, () => ({ id: idRef.current++, x, y, vx: -90 + Math.random() * 180, vy: -90 + Math.random() * 180, life: 0.6 + Math.random() * 0.4, hue, })), ]); }; const onCollect = (kind: ItemKind, x: number, y: number) => { if (kind === "gem") { audio.collect(); const nextCombo = combo + 1; setCombo(nextCombo); comboDecayRef.current = 2.1; const add = 10 + Math.min(40, Math.floor(nextCombo / 3) * 4); setScore((s) => s + add); burst(x, y, 180, 10); if (nextCombo > 0 && nextCombo % 12 === 0) { setLevel((l) => l + 1); setAnnouncement("Level up. Tempo roste."); audio.level(); } return; } if (kind === "star") { audio.level(); setScore((s) => s + 40); setEnergy((e) => Math.min(100, e + 10)); setAnnouncement("Bonusová hvězda."); burst(x, y, 45, 14); return; } if (kind === "slow") { audio.shield(); setSlow(6); setAnnouncement("Čas se zpomalil."); burst(x, y, 220, 12); return; } if (kind === "shield") { audio.shield(); setShield(9); setAnnouncement("Štít aktivní."); burst(x, y, 120, 12); return; } if (kind === "bomb") { if (shield > 0) { audio.hit(); setShield(0); setAnnouncement("Štít absorboval zásah."); burst(x, y, 0, 16); setFlash(0.35); } else { audio.hit(); setEnergy((e) => Math.max(0, e - 28)); setCombo(0); setAnnouncement("Zásah bombou."); burst(x, y, 0, 18); setFlash(0.65); } } }; const startGame = () => { audio.tap(); resetGame(); setScreen("playing"); }; useEffect(() => { runningRef.current = screen === "playing"; }, [screen]); useEffect(() => { if (screen !== "playing") { if (rafRef.current) cancelAnimationFrame(rafRef.current); rafRef.current = null; return; } lastRef.current = performance.now(); const frame = (now: number) => { const dt = Math.min(0.033, (now - lastRef.current) / 1000); lastRef.current = now; if (!runningRef.current) return; const moveSpeed = 250 + level * 6; let dir = 0; if (leftHeld.current) dir -= 1; if (rightHeld.current) dir += 1; if (dir !== 0) { setPlayerX((x) => clamp(x + dir * moveSpeed * dt, 10, GAME_W - PLAYER_SIZE - 10)); } if (flash > 0) setFlash((f) => Math.max(0, f - dt * 1.8)); if (shield > 0) setShield((v) => Math.max(0, v - dt)); if (slow > 0) setSlow((v) => Math.max(0, v - dt)); comboDecayRef.current = Math.max(0, comboDecayRef.current - dt); if (comboDecayRef.current === 0 && combo !== 0) setCombo(0); const spawnGap = Math.max(260, SPAWN_BASE - (level - 1) * 28); spawnTimerRef.current += dt * 1000; if (spawnTimerRef.current >= spawnGap) { spawnTimerRef.current = 0; spawnItem(); } const playerCenterX = playerX + PLAYER_SIZE / 2; const playerCenterY = GAME_H - 72; const playerRadius = PLAYER_SIZE * 0.55; setItems((prev) => { const next: Item[] = []; for (const item of prev) { const updated = { ...item, y: item.y + item.vy * dt * (slow > 0 ? 0.72 : 1), rotation: item.rotation + 90 * dt, }; const cx = updated.x + updated.size / 2; const cy = updated.y + updated.size / 2; const hit = Math.hypot(cx - playerCenterX, cy - playerCenterY) < playerRadius + updated.size * 0.38; if (hit) { onCollect(updated.kind, cx, cy); continue; } if (updated.y > GAME_H + 60) { if (updated.kind === "gem" || updated.kind === "star") { setCombo(0); } continue; } next.push(updated); } return next; }); setParticles((prev) => prev .map((p) => ({ ...p, x: p.x + p.vx * dt, y: p.y + p.vy * dt, vy: p.vy + 140 * dt, life: p.life - dt, })) .filter((p) => p.life > 0) ); rafRef.current = requestAnimationFrame(frame); }; rafRef.current = requestAnimationFrame(frame); return () => { if (rafRef.current) cancelAnimationFrame(rafRef.current); }; }, [screen, playerX, combo, level, shield, slow, flash]); useEffect(() => { if (energy <= 0 && screen === "playing") { setScreen("gameover"); setAnnouncement("Konec hry."); } }, [energy, screen]); const pointerMove = (delta: number) => { setPlayerX((x) => clamp(x + delta, 10, GAME_W - PLAYER_SIZE - 10)); }; const onTouchStart = (clientX: number) => { swipeRef.current = clientX; }; const onTouchMove = (clientX: number) => { if (swipeRef.current == null) return; const delta = clientX - swipeRef.current; swipeRef.current = clientX; pointerMove(delta * 1.2); }; const onTouchEnd = () => { swipeRef.current = null; }; const gameAreaStyle: React.CSSProperties = { width: "min(92vw, 420px)", aspectRatio: "9 / 16", position: "relative", borderRadius: 28, overflow: "hidden", border: "1px solid rgba(255,255,255,.08)", background: "radial-gradient(circle at 50% -10%, rgba(119,201,212,.18), transparent 35%), linear-gradient(180deg, #0c1319 0%, #111c22 55%, #14252b 100%)", boxShadow: "0 28px 70px rgba(0,0,0,.35)", }; const mobileControls = (
); return (

Neon Drop Arena

Mobilní browser hra pro návštěvníky tvého webu.

onTouchStart(e.touches[0].clientX)} onTouchMove={(e) => onTouchMove(e.touches[0].clientX)} onTouchEnd={onTouchEnd} onMouseMove={(e) => { if (window.innerWidth < 900) return; const rect = (e.currentTarget as HTMLDivElement).getBoundingClientRect(); const relative = (e.clientX - rect.left) / rect.width; setPlayerX(clamp(relative * GAME_W - PLAYER_SIZE / 2, 10, GAME_W - PLAYER_SIZE - 10)); }} >
{backgroundStars.map((star) => (
))}
Skóre {score}
Combo x{Math.max(1, combo)}
Level {level}
Energie
55 ? "bg-emerald-400" : energy > 25 ? "bg-amber-400" : "bg-rose-500"}`} style={{ width: `${energy}%` }} />
{energy}
{screen === "playing" && ( <> {items.map((item) => { const kindStyle: Record = { gem: "from-cyan-300 to-blue-500", bomb: "from-rose-400 to-red-700", slow: "from-violet-300 to-indigo-500", shield: "from-emerald-300 to-green-500", star: "from-yellow-300 to-amber-500", }; const borderRadius = item.kind === "bomb" ? "999px" : item.kind === "star" ? "10px" : "14px"; return (
); })} {particles.map((p) => (
))}
{shield > 0 && (
)}
{flash > 0 && (
)} )} {screen !== "playing" && (
{screen === "menu" && (

Neon Drop Arena

Rychlá mobilní hra. Chytej drahokamy, vyhýbej se bombám, sbírej power-upy a honěj highscore.

Nejlepší skóre: {best}
)} {screen === "how" && (

Jak hrát

• Tahej prstem doleva a doprava. Na desktopu pohni myší.

• Modré drahokamy dávají body. Čím delší série, tím vyšší combo.

• Červené bomby berou energii.

• Zelený štít pohltí jednu bombu.

• Fialová zpomalí hru. Zlatá hvězda přidá velký bonus.

)} {screen === "paused" && (

Pozastaveno

)} {screen === "gameover" && (

Konec hry

Skóre: {score}
Nejlepší skóre: {best}
Dosažený level: {level}
)}
)} {screen === "playing" && ( )} {screen === "playing" && mobileControls}

Proč to funguje na web

• okamžitě pochopitelné

• silně mobilní ovládání

• krátké session a highscore smyčka

• menu, pauza a restart hotové

• bez potřeby backendu

Stav hry

Skóre
{score}
Nejlepší
{best}
Combo
x{Math.max(1, combo)}
Level
{level}

Aktuální hlášení

{announcement}

); }