Neon Tap

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.