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) => (
{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 (
))}
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}
);
})}
{particles.map((p) => (
))}
);
}
{shield > 0 && (
)}
{flash > 0 && (
)}
)}
{screen !== "playing" && (
{screen === "menu" && (
)}
{screen === "how" && (
)}
{screen === "paused" && (
)}
{screen === "gameover" && (
)}
)}
{screen === "playing" && (
)}
{screen === "playing" && mobileControls}
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}
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.
Pozastaveno
Konec hry
Skóre: {score}
Nejlepší skóre: {best}
Dosažený level: {level}
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}

