PROMO 3X2 • 3 CUOTAS SIN INTERES
10% OFF CON TRANSFERENCIA
ENVÍO GRATIS SUPERIOR A $180.000
PROMO 3X2 • 3 CUOTAS SIN INTERES
10% OFF CON TRANSFERENCIA
ENVÍO GRATIS SUPERIOR A $180.000
PROMO 3X2 • 3 CUOTAS SIN INTERES
10% OFF CON TRANSFERENCIA
ENVÍO GRATIS SUPERIOR A $180.000
PROMO 3X2 • 3 CUOTAS SIN INTERES
10% OFF CON TRANSFERENCIA
ENVÍO GRATIS SUPERIOR A $180.000
PROMO 3X2 • 3 CUOTAS SIN INTERES
10% OFF CON TRANSFERENCIA
ENVÍO GRATIS SUPERIOR A $180.000
PROMO 3X2 • 3 CUOTAS SIN INTERES
10% OFF CON TRANSFERENCIA
ENVÍO GRATIS SUPERIOR A $180.000
PROMO 3X2 • 3 CUOTAS SIN INTERES
10% OFF CON TRANSFERENCIA
ENVÍO GRATIS SUPERIOR A $180.000
PROMO 3X2 • 3 CUOTAS SIN INTERES
10% OFF CON TRANSFERENCIA
ENVÍO GRATIS SUPERIOR A $180.000

import React, { useEffect, useRef, useState } from "react";

 

// HarveyWillys Runner — prototipo tipo "dino game" con SPRITES reales

// AHORA: soporta prendas reales (PNG con fondo transparente)

// Cómo usar:

// 1) Pone tus PNGs en /public/sprites/{tops,bottoms,shoes}/...

// 2) Editá los arrays TOPS/BOTTOMS/SHOES para apuntar a los archivos.

// 3) Ideal: 160x160 px (o 128x128) en canvas simple, centrados, sin margen. Se escalan en runtime.

// 4) Si falta alguna imagen, el juego hace fallback a bloques de color.

 

const W = 800;

const H = 300;

const GROUND_Y = 250; // línea de suelo

const GRAVITY = 0.7;

const JUMP_VELOCITY = -12.5;

const PLAYER_X = 90;

 

// Catálogo de sprites (EDITABLE)

// Ruta relativa a /public. Podes cambiar nombres y sumar items.

const TOPS = [

  { name: "Remera blanca", color: "#f4f4f5", img: "/sprites/tops/remera_blanca.png" },

  { name: "Buzo negro", color: "#111827", img: "/sprites/tops/buzo_negro.png" },

  { name: "Camisa denim", color: "#3b82f6", img: "/sprites/tops/camisa_denim.png" },

  { name: "Campera bomber", color: "#22c55e", img: "/sprites/tops/campera_bomber.png" },

  { name: "Hoodie arena", color: "#d6d3d1", img: "/sprites/tops/hoodie_arena.png" },

];

const BOTTOMS = [

  { name: "Jean azul", color: "#1e3a8a", img: "/sprites/bottoms/jean_azul.png" },

  { name: "Cargo verde", color: "#065f46", img: "/sprites/bottoms/cargo_verde.png" },

  { name: "Chino negro", color: "#0f172a", img: "/sprites/bottoms/chino_negro.png" },

  { name: "Jogger gris", color: "#4b5563", img: "/sprites/bottoms/jogger_gris.png" },

];

const SHOES = [

  { name: "Zapatillas blancas", color: "#fafafa", img: "/sprites/shoes/zapas_blancas.png" },

  { name: "Zapatillas negras", color: "#111111", img: "/sprites/shoes/zapas_negras.png" },

  { name: "Zapatillas rojas", color: "#ef4444", img: "/sprites/shoes/zapas_rojas.png" },

];

 

function pickOutfit() {

  const rand = (arr) => arr[Math.floor(Math.random() * arr.length)];

  return {

    top: rand(TOPS),

    bottom: rand(BOTTOMS),

    shoes: rand(SHOES),

  };

}

 

function useRaf(callback, active) {

  const rafRef = useRef();

  const cbRef = useRef(callback);

  cbRef.current = callback;

  useEffect(() => {

    if (!active) return;

    let last = performance.now();

    const loop = (t) => {

      const dt = Math.min(32, t - last); // cap dt en 32ms

      last = t;

      cbRef.current(dt);

      rafRef.current = requestAnimationFrame(loop);

    };

    rafRef.current = requestAnimationFrame(loop);

    return () => cancelAnimationFrame(rafRef.current);

  }, [active]);

}

 

export default function HarveyWillysRunner() {

  const canvasRef = useRef(null);

  const imagesRef = useRef({}); // { src: HTMLImageElement }

  const [assetsReady, setAssetsReady] = useState(false);

 

  const [gameState, setGameState] = useState("idle"); // idle | playing | gameover

  const [score, setScore] = useState(0);

  const [highScore, setHighScore] = useState(

    () => Number(localStorage.getItem("hw_runner_hs") || 0)

  );

  const [outfit, setOutfit] = useState(pickOutfit);

 

  // entidades del juego en refs (para evitar re-renders)

  const playerRef = useRef({ y: GROUND_Y - 52, vy: 0, w: 36, h: 52 });

  const obstaclesRef = useRef([]); // {x,y,w,h}

  const speedRef = useRef(6);

  const timeRef = useRef(0);

  const spawnTimerRef = useRef(0);

 

  // HiDPI scaling

  useEffect(() => {

    const canvas = canvasRef.current;

    const dpr = window.devicePixelRatio || 1;

    canvas.width = W * dpr;

    canvas.height = H * dpr;

    canvas.style.width = W + "px";

    canvas.style.height = H + "px";

    const ctx = canvas.getContext("2d");

    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);

  }, []);

 

  // Preload de sprites

  useEffect(() => {

    const all = [...TOPS, ...BOTTOMS, ...SHOES]

      .map((i) => i.img)

      .filter(Boolean);

    if (all.length === 0) {

      setAssetsReady(true);

      return;

    }

    let loaded = 0;

    const unique = Array.from(new Set(all));

    unique.forEach((src) => {

      const img = new Image();

      img.decoding = "async";

      img.onload = () => {

        imagesRef.current[src] = img;

        loaded += 1;

        if (loaded === unique.length) setAssetsReady(true);

      };

      img.onerror = () => {

        // si falla, seguimos con fallback

        loaded += 1;

        if (loaded === unique.length) setAssetsReady(true);

      };

      img.src = src;

    });

  }, []);

 

  // input

  useEffect(() => {

    const onKey = (e) => {

      if (e.repeat) return;

      if (e.code === "Space" || e.code === "ArrowUp") {

        e.preventDefault();

        handleJump();

      }

    };

    const onTouch = () => handleJump();

    window.addEventListener("keydown", onKey);

    window.addEventListener("touchstart", onTouch, { passive: true });

    return () => {

      window.removeEventListener("keydown", onKey);

      window.removeEventListener("touchstart", onTouch);

    };

  }, [gameState]);

 

  function resetGame(newOutfit = false) {

    playerRef.current = { y: GROUND_Y - 52, vy: 0, w: 36, h: 52 };

    obstaclesRef.current = [];

    speedRef.current = 6;

    timeRef.current = 0;

    spawnTimerRef.current = 0;

    setScore(0);

    if (newOutfit) setOutfit(pickOutfit());

    setGameState("idle");

  }

 

  function startGame() {

    resetGame(false);

    setGameState("playing");

  }

 

  function handleJump() {

    if (gameState === "idle") {

      if (!assetsReady) return; // esperar sprites

      startGame();

      return;

    }

    if (gameState !== "playing") return;

    const p = playerRef.current;

    const onGround = p.y >= GROUND_Y - p.h - 0.5;

    if (onGround) p.vy = JUMP_VELOCITY;

  }

 

  function spawnObstacle() {

    const h = 20 + Math.floor(Math.random() * 50); // 20..70

    const w = 12 + Math.floor(Math.random() * 25); // 12..37

    obstaclesRef.current.push({ x: W + 20, y: GROUND_Y - h, w, h });

  }

 

  function aabb(a, b) {

    return (

      PLAYER_X < b.x + b.w &&

      PLAYER_X + playerRef.current.w > b.x &&

      playerRef.current.y < b.y + b.h &&

      playerRef.current.y + playerRef.current.h > b.y

    );

  }

 

  // Utilidad de dibujo con fallback

  function drawPiece(ctx, imgSrc, fallbackColor, x, y, w, h) {

    const img = imagesRef.current[imgSrc];

    if (img && img.complete) {

      ctx.drawImage(img, x, y, w, h);

    } else {

      ctx.fillStyle = fallbackColor;

      ctx.fillRect(x, y, w, h);

    }

  }

 

  // game loop

  useRaf((dt) => {

    if (gameState !== "playing") return;

    const canvas = canvasRef.current;

    const ctx = canvas.getContext("2d");

 

    // update

    const p = playerRef.current;

    p.vy += GRAVITY;

    p.y += p.vy;

    if (p.y > GROUND_Y - p.h) {

      p.y = GROUND_Y - p.h;

      p.vy = 0;

    }

 

    const s = speedRef.current;

    timeRef.current += dt;

    spawnTimerRef.current += dt;

    const spawnEvery = Math.max(700 - timeRef.current * 0.03, 380); // se hace más difícil

    if (spawnTimerRef.current > spawnEvery) {

      spawnObstacle();

      spawnTimerRef.current = 0;

    }

 

    // mover obstáculos y colisiones

    obstaclesRef.current.forEach((o) => (o.x -= s));

    obstaclesRef.current = obstaclesRef.current.filter((o) => o.x + o.w > -10);

 

    for (const o of obstaclesRef.current) {

      if (aabb(p, o)) {

        setGameState("gameover");

        if (score > highScore) {

          localStorage.setItem("hw_runner_hs", String(score));

          setHighScore(score);

        }

        break;

      }

    }

 

    // subir velocidad gradualmente

    speedRef.current = Math.min(12, 6 + timeRef.current * 0.002);

    setScore((s) => s + Math.floor(dt * 0.08));

 

    // render

    ctx.clearRect(0, 0, W, H);

 

    // cielo

    ctx.fillStyle = "#ffffff";

    ctx.fillRect(0, 0, W, H);

 

    // marca

    ctx.fillStyle = "#0f172a";

    ctx.font = "bold 16px Inter, ui-sans-serif, system-ui";

    ctx.fillText("HARVEYWILLYS", 12, 24);

 

    // suelo

    ctx.strokeStyle = "#e5e7eb";

    ctx.lineWidth = 2;

    ctx.beginPath();

    ctx.moveTo(0, GROUND_Y + 0.5);

    ctx.lineTo(W, GROUND_Y + 0.5);

    ctx.stroke();

 

    // jugador (caja de destino por piezas)

    const px = PLAYER_X;

    const py = p.y;

    const pw = p.w;

    const ph = p.h;

 

    // sombra

    ctx.fillStyle = "rgba(0,0,0,0.08)";

    const shadowScale = p.y < GROUND_Y - p.h ? 1 : 1.3;

    ctx.beginPath();

    ctx.ellipse(px + pw / 2, GROUND_Y + 6, 16 * shadowScale, 4 * shadowScale, 0, 0, Math.PI * 2);

    ctx.fill();

 

    // cabeza

    ctx.fillStyle = "#f5d0c5";

    ctx.fillRect(px + pw / 2 - 7, py - 8, 14, 14);

 

    // DIMENSIONES de destino para sprites (ajustadas a la silueta base)

    const TOP_BOX = { x: px - 2, y: py + 4, w: pw + 4, h: ph - 24 };

    const BOTTOM_BOX = { x: px + 2, y: py + ph - 24, w: pw - 4, h: 18 };

    const SHOES_BOX = { x: px + 2, y: py + ph - 7, w: pw - 4, h: 6 };

 

    // prendas

    drawPiece(ctx, outfit.top.img, outfit.top.color, TOP_BOX.x, TOP_BOX.y, TOP_BOX.w, TOP_BOX.h);

    drawPiece(ctx, outfit.bottom.img, outfit.bottom.color, BOTTOM_BOX.x, BOTTOM_BOX.y, BOTTOM_BOX.w, BOTTOM_BOX.h);

    drawPiece(ctx, outfit.shoes.img, outfit.shoes.color, SHOES_BOX.x, SHOES_BOX.y, SHOES_BOX.w, SHOES_BOX.h);

 

    // obstáculos

    for (const o of obstaclesRef.current) {

      ctx.fillStyle = "#16a34a";

      ctx.fillRect(o.x, o.y, o.w, o.h);

      ctx.fillStyle = "rgba(255,255,255,0.35)";

      ctx.fillRect(o.x + 2, o.y + 4, 2, Math.max(0, o.h - 6));

    }

 

    // UI: score

    ctx.fillStyle = "#0f172a";

    ctx.font = "bold 18px Inter, ui-sans-serif, system-ui";

    ctx.textAlign = "right";

    ctx.fillText(String(score).padStart(5, "0"), W - 16, 28);

    ctx.font = "12px Inter, ui-sans-serif, system-ui";

    ctx.fillText("REC " + String(highScore).padStart(5, "0"), W - 16, 44);

 

    // outfit label

    ctx.textAlign = "left";

    ctx.font = "12px Inter, ui-sans-serif, system-ui";

    ctx.fillStyle = "#334155";

    ctx.fillText(

      `${outfit.top.name} + ${outfit.bottom.name} + ${outfit.shoes.name}`,

      12,

      H - 12

    );

  }, gameState === "playing");

 

  // overlays

  const overlay = (

    <div className="absolute inset-0 flex flex-col items-center justify-center pointer-events-none">

      {gameState === "idle" && (

        <div className="bg-white/90 backdrop-blur shadow-xl rounded-2xl p-6 text-center w-[min(92%,480px)]">

          <h2 className="text-xl font-bold tracking-tight">HARVEYWILLYS RUNNER</h2>

          <p className="text-sm text-slate-600 mt-1">Prototipo con sprites reales</p>

          <ul className="text-sm text-slate-600 mt-4 list-disc list-inside text-left">

            <li>SPACE o ↑ para saltar</li>

            <li>Toque/click en móvil</li>

            <li>Outfit random en cada sesión</li>

          </ul>

          {!assetsReady && (

            <div className="mt-3 text-xs text-slate-500">Cargando sprites…</div>

          )}

          <div className="flex gap-2 mt-4 justify-center pointer-events-auto">

            <button

              onClick={() => assetsReady && setGameState("playing")}

              className={`px-4 py-2 rounded-xl text-white ${assetsReady ? "bg-black hover:opacity-90 active:opacity-80" : "bg-slate-400 cursor-not-allowed"}`}

            >

              Jugar

            </button>

            <button

              onClick={() => setOutfit(pic