Código del juego:
import React, { useEffect, useMemo, useRef, useState } from "react";
import {
View,
Text,
Pressable,
SafeAreaView,
Dimensions,
Animated,
Easing,
StyleSheet,
} from "react-native";
import { LinearGradient } from "expo-linear-gradient";
import * as Haptics from "expo-haptics";
const { width, height } = Dimensions.get("window");
const GAME_SECONDS = 30;
const TARGET_SIZE = 86;
const PADDING = 24;
function rand(min: number, max: number) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
export default function Index() {
const [phase, setPhase] = useState<"menu" | "playing" | "over">("menu");
const [timeLeft, setTimeLeft] = useState<number>(GAME_SECONDS);
const [score, setScore] = useState<number>(0);
const [combo, setCombo] = useState<number>(0);
const [target, setTarget] = useState<{ x: number; y: number; id: number }>(() => ({
x: rand(PADDING, width - TARGET_SIZE - PADDING),
y: rand(PADDING + 120, height - TARGET_SIZE - PADDING - 140),
id: 0,
}));
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
const pulse = useRef(new Animated.Value(0)).current;
const pop = useRef(new Animated.Value(0)).current;
const shake = useRef(new Animated.Value(0)).current;
const glow = useRef(new Animated.Value(0)).current;
const title = useMemo(() => {
if (phase === "menu") return "Neon Tap!";
if (phase === "playing") return "¡Dale!";
return "Fin de partida";
}, [phase]);
useEffect(() => {
Animated.loop(
Animated.sequence([
Animated.timing(pulse, {
toValue: 1,
duration: 900,
easing: Easing.inOut(Easing.quad),
useNativeDriver: true,
}),
Animated.timing(pulse, {
toValue: 0,
duration: 900,
easing: Easing.inOut(Easing.quad),
useNativeDriver: true,
}),
])
).start();
Animated.loop(
Animated.sequence([
Animated.timing(glow, {
toValue: 1,
duration: 1400,
easing: Easing.inOut(Easing.quad),
useNativeDriver: true,
}),
Animated.timing(glow, {
toValue: 0,
duration: 1400,
easing: Easing.inOut(Easing.quad),
useNativeDriver: true,
}),
])
).start();
}, [pulse, glow]);
function resetGame() {
setScore(0);
setCombo(0);
setTimeLeft(GAME_SECONDS);
setTarget({
x: rand(PADDING, width - TARGET_SIZE - PADDING),
y: rand(PADDING + 120, height - TARGET_SIZE - PADDING - 140),
id: Date.now(),
});
}
function startGame() {
resetGame();
setPhase("playing");
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
if (timerRef.current) clearInterval(timerRef.current);
timerRef.current = setInterval(() => {
setTimeLeft((t) => {
if (t <= 1) {
if (timerRef.current) clearInterval(timerRef.current);
timerRef.current = null;
setPhase("over");
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning);
return 0;
}
return t - 1;
});
}, 1000);
}
function moveTarget() {
setTarget((prev) => ({
x: rand(PADDING, width - TARGET_SIZE - PADDING),
y: rand(PADDING + 120, height - TARGET_SIZE - PADDING - 140),
id: prev.id + 1,
}));
}
function hit() {
const add = 1 + Math.min(combo, 9);
setScore((s) => s + add);
setCombo((c) => c + 1);
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
pop.setValue(0);
Animated.timing(pop, {
toValue: 1,
duration: 180,
easing: Easing.out(Easing.quad),
useNativeDriver: true,
}).start();
moveTarget();
}
function miss() {
setCombo(0);
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
shake.setValue(0);
Animated.sequence([
Animated.timing(shake, { toValue: 1, duration: 60, useNativeDriver: true }),
Animated.timing(shake, { toValue: 0, duration: 60, useNativeDriver: true }),
Animated.timing(shake, { toValue: 1, duration: 60, useNativeDriver: true }),
Animated.timing(shake, { toValue: 0, duration: 60, useNativeDriver: true }),
]).start();
}
useEffect(() => {
return () => {
if (timerRef.current) clearInterval(timerRef.current);
};
}, []);
const pulseScale = pulse.interpolate({
inputRange: [0, 1],
outputRange: [1, 1.06],
});
const glowOpacity = glow.interpolate({
inputRange: [0, 1],
outputRange: [0.35, 0.7],
});
const popScale = pop.interpolate({
inputRange: [0, 1],
outputRange: [1, 1.18],
});
const shakeX = shake.interpolate({
inputRange: [0, 1],
outputRange: [0, 10],
});
return (
<SafeAreaView style={{ flex: 1, backgroundColor: "#05060A" }}>
<LinearGradient colors={["#05060A", "#070A14", "#05060A"]} style={{ flex: 1 }}>
<View style={styles.hud}>
<View style={styles.hudBlock}>
<Text style={styles.hudLabel}>TIEMPO</Text>
<Text style={styles.hudValue}>{timeLeft}s</Text>
</View>
<View style={styles.hudCenter}>
<Text style={styles.title}>{title}</Text>
{phase === "playing" ? (
<Text style={styles.subtitle}>Toca el objetivo. No falles.</Text>
) : (
<Text style={styles.subtitle}>Un juego mínimo, visual y adictivo.</Text>
)}
</View>
<View style={styles.hudBlock}>
<Text style={styles.hudLabel}>PUNTOS</Text>
<Text style={styles.hudValue}>{score}</Text>
</View>
</View>
<Animated.View style={{ flex: 1, transform: [{ translateX: shakeX }] }}>
<Pressable style={{ flex: 1 }} onPress={() => phase === "playing" && miss()}>
{phase === "playing" && (
<View
style={{
position: "absolute",
left: target.x,
top: target.y,
width: TARGET_SIZE,
height: TARGET_SIZE,
}}
>
<Animated.View
style={[
styles.glowRing,
{ opacity: glowOpacity, transform: [{ scale: pulseScale }] },
]}
/>
<Animated.View
style={[
styles.target,
{ transform: [{ scale: Animated.multiply(pulseScale, popScale) }] },
]}
>
<Pressable
onPress={hit}
style={styles.targetInner}
android_ripple={{ color: "rgba(255,255,255,0.15)" }}
>
<Text style={styles.targetText}>TAP</Text>
</Pressable>
</Animated.View>
</View>
)}
</Pressable>
</Animated.View>
<View style={styles.bottom}>
<View style={styles.comboPill}>
<Text style={styles.comboLabel}>COMBO</Text>
<Text style={styles.comboValue}>{combo}</Text>
<Text style={styles.comboHint}>+{Math.min(combo, 9)} bonus</Text>
</View>
{phase === "menu" && (
<Pressable onPress={startGame} style={styles.primaryBtn}>
<Text style={styles.primaryBtnText}>Empezar</Text>
</Pressable>
)}
{phase === "over" && (
<View style={{ gap: 10, width: "100%" }}>
<View style={styles.resultCard}>
<Text style={styles.resultTitle}>Tu puntuación</Text>
<Text style={styles.resultScore}>{score}</Text>
<Text style={styles.resultSub}>
Consejo: prioriza precisión. El combo multiplica.
</Text>
</View>
<Pressable onPress={startGame} style={styles.primaryBtn}>
<Text style={styles.primaryBtnText}>Jugar otra vez</Text>
</Pressable>
<Pressable onPress={() => setPhase("menu")} style={styles.secondaryBtn}>
<Text style={styles.secondaryBtnText}>Volver al menú</Text>
</Pressable>
</View>
)}
</View>
</LinearGradient>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
hud: {
paddingHorizontal: 18,
paddingTop: 8,
paddingBottom: 10,
flexDirection: "row",
alignItems: "flex-end",
justifyContent: "space-between",
},
hudBlock: {
width: 86,
alignItems: "center",
paddingVertical: 10,
borderRadius: 16,
backgroundColor: "rgba(255,255,255,0.04)",
borderWidth: 1,
borderColor: "rgba(255,255,255,0.07)",
},
hudLabel: {
color: "rgba(255,255,255,0.7)",
fontSize: 10,
letterSpacing: 1.2,
fontWeight: "700",
},
hudValue: {
marginTop: 4,
color: "white",
fontSize: 20,
fontWeight: "800",
},
hudCenter: {
flex: 1,
alignItems: "center",
gap: 2,
paddingHorizontal: 10,
},
title: { color: "white", fontSize: 26, fontWeight: "900", letterSpacing: 0.6 },
subtitle: { color: "rgba(255,255,255,0.75)", fontSize: 12 },
glowRing: {
position: "absolute",
inset: -18,
borderRadius: 999,
backgroundColor: "rgba(0,255,255,0.10)",
borderWidth: 2,
borderColor: "rgba(0,255,255,0.35)",
},
target: {
width: TARGET_SIZE,
height: TARGET_SIZE,
borderRadius: 22,
overflow: "hidden",
backgroundColor: "rgba(255,255,255,0.06)",
borderWidth: 1,
borderColor: "rgba(255,255,255,0.14)",
},
targetInner: { flex: 1, alignItems: "center", justifyContent: "center", backgroundColor: "rgba(0,255,255,0.14)" },
targetText: { color: "white", fontSize: 18, fontWeight: "900", letterSpacing: 2 },
bottom: { padding: 18, paddingBottom: 22, gap: 12 },
comboPill: {
alignSelf: "center",
paddingVertical: 10,
paddingHorizontal: 16,
borderRadius: 999,
flexDirection: "row",
alignItems: "baseline",
gap: 10,
backgroundColor: "rgba(255,255,255,0.04)",
borderWidth: 1,
borderColor: "rgba(255,255,255,0.08)",
},
comboLabel: { color: "rgba(255,255,255,0.65)", fontSize: 10, fontWeight: "800", letterSpacing: 1.2 },
comboValue: { color: "white", fontSize: 18, fontWeight: "900" },
comboHint: { color: "rgba(255,255,255,0.7)", fontSize: 12 },
primaryBtn: { width: "100%", paddingVertical: 16, borderRadius: 18, backgroundColor: "rgba(255,255,255,0.92)", alignItems: "center" },
primaryBtnText: { color: "#05060A", fontSize: 16, fontWeight: "900", letterSpacing: 0.4 },
secondaryBtn: {
width: "100%",
paddingVertical: 14,
borderRadius: 18,
backgroundColor: "rgba(255,255,255,0.06)",
borderWidth: 1,
borderColor: "rgba(255,255,255,0.12)",
alignItems: "center",
},
secondaryBtnText: { color: "white", fontSize: 14, fontWeight: "800", opacity: 0.9 },
resultCard: {
padding: 16,
borderRadius: 18,
backgroundColor: "rgba(255,255,255,0.04)",
borderWidth: 1,
borderColor: "rgba(255,255,255,0.08)",
alignItems: "center",
gap: 6,
},
resultTitle: { color: "rgba(255,255,255,0.75)", fontSize: 12, fontWeight: "800", letterSpacing: 1.2 },
resultScore: { color: "white", fontSize: 44, fontWeight: "900" },
resultSub: { color: "rgba(255,255,255,0.75)", fontSize: 12, textAlign: "center", marginTop: 2 },
});
Desarrollado en ChatGPT.
