Untitled

Game
 avatar
unknown
plain_text
a year ago
19 kB
11
Indexable
import React, { useEffect, useMemo, useRef, useState } from "react";
// 3D Survival Builder β€” Single-file React + R3F (Enhanced)
// Assumes libs available: @react-three/fiber, @react-three/drei, three, tailwind

import { Canvas, useFrame } from "@react-three/fiber";
import * as THREE from "three";
import { Sky } from "@react-three/drei";

export default function SurvivalBuilder3D() {
  // --- Game State ---
  const [day, setDay] = useState(1);
  const [health, setHealth] = useState(100);
  const [food, setFood] = useState(3);
  const [wood, setWood] = useState(0);
  const [stone, setStone] = useState(0);
  const [shelter, setShelter] = useState(false);
  const [hasAxe, setHasAxe] = useState(false);
  const [hasPick, setHasPick] = useState(false);
  const [log, setLog] = useState<string[]>(["You wake up in a wild land. Survive!"]);

  // Resource map (simple scattered items)
  const [trees, setTrees] = useState<Vec3[]>(() => scatter(10, 30, 30));
  const [rocks, setRocks] = useState<Vec3[]>(() => scatter(8, 30, 30));
  const [berries, setBerries] = useState<Vec3[]>(() => scatter(6, 30, 30));

  // Player ref (position shared between scene + HUD)
  const playerPosRef = useRef<THREE.Vector3>(new THREE.Vector3(0, 1, 0));

  // --- Lightweight day/night cycle (0..1) ---
  const [time01, setTime01] = useState(0); // advances continuously; 0=noon-ish, 0.5=night

  // Hunger over time (every 12s lose 1 food)
  useEffect(() => {
    const id = setInterval(() => {
      setFood((f) => {
        const next = f - 1;
        if (next < 0) {
          setHealth((h) => Math.max(0, h - 5));
          setLog((l) => ["You feel weak from hunger.", ...l]);
        } else {
          setLog((l) => ["You feel hungry.", ...l]);
        }
        return next;
      });
    }, 12000);
    return () => clearInterval(id);
  }, []);

  // Nightly random event on End Day
  const endDay = () => {
    const nextDay = day + 1;
    setDay(nextDay);

    // Consume food safely (and apply starvation if needed)
    setFood((f) => {
      const next = f - 1;
      if (next < 0) setHealth((h) => Math.max(0, h - 10));
      return next;
    });

    const events = [
      "A monster attacked at night!",
      "A storm damaged your camp!",
      "You found wild berries overnight!",
      "It was a peaceful night.",
    ];
    const event = events[Math.floor(Math.random() * events.length)];

    // Apply event effects with shelter benefits
    if (event.includes("monster")) {
      const dmg = shelter ? 8 : 15; // shelter reduces attack damage
      setHealth((h) => Math.max(0, h - dmg));
    }
    if (event.includes("storm")) {
      if (!shelter) setHealth((h) => Math.max(0, h - 10));
    }
    if (event.includes("berries")) setFood((f) => f + 2);

    // Overnight small heal if sheltered
    if (shelter) setHealth((h) => Math.min(100, h + 6));

    setLog((prev) => [
      `Day ${nextDay} begins...`,
      event,
      ...prev,
    ]);
  };

  const eat = () => {
    setFood((f) => {
      if (f > 0) {
        setHealth((h) => Math.min(100, h + 12));
        setLog((l) => ["You ate some food.", ...l]);
        return f - 1;
      } else {
        setLog((l) => ["No food left!", ...l]);
        return f;
      }
    });
  };

  const tryBuildShelter = () => {
    if (!shelter && wood >= 5 && stone >= 3) {
      setWood((w) => w - 5);
      setStone((s) => s - 3);
      setShelter(true);
      setLog((l) => ["You built a small shelter.", ...l]);
    } else setLog((l) => ["You need 5 wood + 3 stone to build a shelter.", ...l]);
  };

  // Simple crafting: 1) Stone Axe (2 wood + 1 stone) -> +1 extra wood per tree
  //                  2) Pickaxe (1 wood + 2 stone) -> +1 extra stone per rock
  const craftAxe = () => {
    if (hasAxe) return setLog((l) => ["You already have an axe.", ...l]);
    if (wood >= 2 && stone >= 1) {
      setWood((w) => w - 2);
      setStone((s) => s - 1);
      setHasAxe(true);
      setLog((l) => ["You crafted a Stone Axe.", ...l]);
    } else setLog((l) => ["Need 2 wood + 1 stone for an Axe.", ...l]);
  };
  const craftPick = () => {
    if (hasPick) return setLog((l) => ["You already have a pickaxe.", ...l]);
    if (wood >= 1 && stone >= 2) {
      setWood((w) => w - 1);
      setStone((s) => s - 2);
      setHasPick(true);
      setLog((l) => ["You crafted a Pickaxe.", ...l]);
    } else setLog((l) => ["Need 1 wood + 2 stone for a Pickaxe.", ...l]);
  };

  return (
    <div className="w-full h-[85vh] bg-neutral-900 text-white relative">
      {/* HUD */}
      <div className="absolute top-3 left-3 z-10 space-y-2">
        <h1 className="text-2xl font-bold">πŸ—ΊοΈ Survival Builder 3D</h1>
        <div className="flex flex-wrap gap-2 text-sm opacity-90">
          <Badge>Day {day}</Badge>
          <Badge>❀️ {Math.round(health)}</Badge>
          <Badge>πŸ— {food}</Badge>
          <Badge>πŸͺ΅ {wood}</Badge>
          <Badge>πŸͺ¨ {stone}</Badge>
          <Badge>🏠 {shelter ? "Shelter" : "No Shelter"}</Badge>
          {hasAxe && <Badge>πŸͺ“ Axe</Badge>}
          {hasPick && <Badge>⛏️ Pick</Badge>}
        </div>
        <div className="text-xs opacity-80 max-w-xl leading-relaxed">
          WASD = Move β€’ Space = Jump β€’ Q/E = Rotate Camera β€’ <b>F = Interact</b> β€’ R = Eat β€’ B = Build Shelter β€’ 1 = Craft Axe β€’ 2 = Craft Pickaxe β€’ Enter = End Day β€’ Shift = Sprint
        </div>
      </div>

      <div className="absolute bottom-3 left-3 right-3 z-10 max-h-32 overflow-y-auto text-sm bg-black/30 rounded-lg p-2 border border-white/10">
        <p className="opacity-80 mb-1">πŸ“œ Log</p>
        {log.slice(0, 10).map((line, i) => (
          <div key={i} className="opacity-90 leading-tight">β€’ {line}</div>
        ))}
      </div>

      {/* 3D Scene */}
      <Canvas shadows camera={{ position: [8, 8, 8], fov: 55 }}>
        {/* Lighting & sky are driven by time01 */}
        <DayNight time01={time01} onTick={setTime01} />

        <Ground />

        {/* Resources */}
        {trees.map((p, i) => (
          <Tree key={`t-${i}`} position={p} />
        ))}
        {rocks.map((p, i) => (
          <Rock key={`r-${i}`} position={p} />
        ))}
        {berries.map((p, i) => (
          <BerryBush key={`b-${i}`} position={p} />
        ))}

        {/* Shelter model if built */}
        {shelter && <Shelter position={[0, 0, -2]} />}

        {/* Player + Camera follow + interactions */}
        <Player
          posRef={playerPosRef}
          onGather={(type, index) => {
            if (type === "tree") {
              setTrees((arr) => arr.filter((_, i) => i !== index));
              setWood((w) => w + 1 + (hasAxe ? 1 : 0));
              setLog((l) => [hasAxe ? "Chopped wood (axe bonus)." : "Chopped wood.", ...l]);
            } else if (type === "rock") {
              setRocks((arr) => arr.filter((_, i) => i !== index));
              setStone((s) => s + 1 + (hasPick ? 1 : 0));
              setLog((l) => [hasPick ? "Mined stone (pick bonus)." : "Mined stone.", ...l]);
            } else if (type === "berry") {
              setBerries((arr) => arr.filter((_, i) => i !== index));
              setFood((f) => f + 1);
              setLog((l) => ["Picked berries.", ...l]);
            }
          }}
          resources={{ trees, rocks, berries }}
          onBuild={tryBuildShelter}
          onEat={eat}
          onEndDay={endDay}
          onHurt={(amt) => setHealth((h) => Math.max(0, h - amt))}
          onCraftAxe={craftAxe}
          onCraftPick={craftPick}
        />
      </Canvas>
    </div>
  );
}

// ---------------- Components ----------------

function Badge({ children }: { children: React.ReactNode }) {
  return (
    <span className="px-2 py-1 rounded bg-white/10 border border-white/10">{children}</span>
  );
}

function Ground() {
  // Displaced terrain using per-vertex noise
  const meshRef = useRef<THREE.Mesh>(null!);
  const geomRef = useRef<THREE.PlaneGeometry>(null!);

  const subdivisions = 128;

  useEffect(() => {
    const geom = geomRef.current;
    const pos = geom.attributes.position as THREE.BufferAttribute;
    const v = new THREE.Vector3();
    const noise = (x: number, z: number) => {
      // Simple 2D value noise via sin/cos blend
      return (
        Math.sin(x * 0.15) * 0.2 +
        Math.cos(z * 0.12) * 0.2 +
        Math.sin((x + z) * 0.07) * 0.25
      ) * 0.6;
    };
    for (let i = 0; i < pos.count; i++) {
      v.fromBufferAttribute(pos, i);
      const h = noise(v.x, v.z);
      pos.setZ(i, h);
    }
    pos.needsUpdate = true;
    geom.computeVertexNormals();
  }, []);

  return (
    <mesh ref={meshRef} rotation={[-Math.PI / 2, 0, 0]} receiveShadow>
      <planeGeometry ref={geomRef} args={[80, 80, subdivisions, subdivisions]} />
      <meshStandardMaterial color="#507d2a" />
    </mesh>
  );
}

function Tree({ position }: { position: Vec3 }) {
  // Stable random scale from position
  const scale = useMemo(() => 0.9 + (hash3(position) % 0.4), [position]);
  return (
    <group position={position} scale={scale}>
      <mesh castShadow position={[0, 1.25, 0]}> {/* trunk */}
        <cylinderGeometry args={[0.15, 0.2, 2.5, 8]} />
        <meshStandardMaterial color="#7b4b27" />
      </mesh>
      <mesh castShadow position={[0, 2.5, 0]}> {/* leaves */}
        <coneGeometry args={[1.1, 2.2, 8]} />
        <meshStandardMaterial color="#2f6f31" />
      </mesh>
    </group>
  );
}

function Rock({ position }: { position: Vec3 }) {
  return (
    <mesh castShadow position={position}>
      <dodecahedronGeometry args={[0.6, 0]} />
      <meshStandardMaterial color="#8a8a8a" roughness={1} metalness={0} />
    </mesh>
  );
}

function BerryBush({ position }: { position: Vec3 }) {
  return (
    <group position={position}>
      <mesh castShadow>
        <sphereGeometry args={[0.6, 16, 16]} />
        <meshStandardMaterial color="#2c7a44" />
      </mesh>
      <mesh castShadow position={[0.3, 0.4, 0]}>
        <sphereGeometry args={[0.12, 12, 12]} />
        <meshStandardMaterial color="#b42334" />
      </mesh>
      <mesh castShadow position={[-0.25, 0.3, -0.15]}>
        <sphereGeometry args={[0.1, 12, 12]} />
        <meshStandardMaterial color="#b42334" />
      </mesh>
    </group>
  );
}

function Shelter({ position = [0, 0, 0] as Vec3 }) {
  return (
    <group position={position}>
      <mesh castShadow position={[0, 0.6, 0]}>
        <boxGeometry args={[2.2, 1.2, 2.2]} />
        <meshStandardMaterial color="#6e5a43" />
      </mesh>
      <mesh castShadow rotation={[0, 0, Math.PI / 4]} position={[0, 1.4, 0]}>
        <boxGeometry args={[2.6, 0.2, 2.6]} />
        <meshStandardMaterial color="#5a4736" />
      </mesh>
    </group>
  );
}

// Dynamic lighting + sky driven by time01
function DayNight({ time01, onTick }: { time01: number; onTick: (t: number) => void }) {
  const dirLight = useRef<THREE.DirectionalLight>(null!);
  const ambLight = useRef<THREE.AmbientLight>(null!);

  useFrame((_, dt) => {
    // Advance time; 1 cycle ~= 120 seconds
    const speed = 1 / 120; // cycles per second
    const next = (time01 + dt * speed) % 1;
    onTick(next);

    const angle = next * Math.PI * 2; // 0..2PI
    const sunY = Math.sin(angle);
    const sunX = Math.cos(angle);

    // Position sun and set intensities
    if (dirLight.current) {
      dirLight.current.position.set(sunX * 100, Math.max(5, sunY * 100), 100);
      dirLight.current.intensity = THREE.MathUtils.lerp(0.2, 1.2, clamp01(sunY * 0.5 + 0.5));
      dirLight.current.color.setHSL(0.12, 0.7, THREE.MathUtils.lerp(0.4, 0.9, clamp01(sunY * 0.5 + 0.5)));
    }
    if (ambLight.current) {
      ambLight.current.intensity = THREE.MathUtils.lerp(0.1, 0.6, clamp01(sunY * 0.5 + 0.5));
    }
  });

  const sunPos = useMemo<[number, number, number]>(() => [100, 20, 100], []);

  return (
    <>
      <ambientLight ref={ambLight} intensity={0.5} />
      <directionalLight
        ref={dirLight}
        castShadow
        position={[6, 10, 4]}
        intensity={1.1}
        shadow-mapSize-width={2048}
        shadow-mapSize-height={2048}
      />
      <Sky sunPosition={sunPos} />
    </>
  );
}

// --- Player with simple 3rd-person movement + idle/walk animation ---
function Player({
  posRef,
  onGather,
  resources,
  onBuild,
  onEat,
  onEndDay,
  onHurt,
  onCraftAxe,
  onCraftPick,
}: {
  posRef: React.MutableRefObject<THREE.Vector3>;
  onGather: (type: "tree" | "rock" | "berry", index: number) => void;
  resources: { trees: Vec3[]; rocks: Vec3[]; berries: Vec3[] };
  onBuild: () => void;
  onEat: () => void;
  onEndDay: () => void;
  onHurt: (amt: number) => void;
  onCraftAxe: () => void;
  onCraftPick: () => void;
}) {
  const ref = useRef<THREE.Group>(null!);
  const vel = useRef(new THREE.Vector3());
  const dir = useRef(new THREE.Vector3());
  const keys = useRef<Record<string, boolean>>({});
  const onGround = useRef(true);
  const yawRef = useRef(0);

  // Keyboard
  useEffect(() => {
    const down = (e: KeyboardEvent) => {
      keys.current[e.code] = true;
      if (e.code === "KeyF") interact(); // Interact/Gather
      if (e.code === "KeyR") onEat();
      if (e.code === "KeyB") onBuild();
      if (e.code === "Digit1") onCraftAxe();
      if (e.code === "Digit2") onCraftPick();
      if (e.code === "Enter") onEndDay();
    };
    const up = (e: KeyboardEvent) => (keys.current[e.code] = false);
    window.addEventListener("keydown", down);
    window.addEventListener("keyup", up);
    return () => {
      window.removeEventListener("keydown", down);
      window.removeEventListener("keyup", up);
    };
  }, [onEndDay, onEat, onBuild, onCraftAxe, onCraftPick]);

  // Movement & simple physics
  useFrame((state, dt) => {
    dt = Math.min(0.05, dt);
    const speed = keys.current["ShiftLeft"] ? 6 : 3.5; // sprint with Shift

    // Direction from WASD relative to camera yaw
    const forward = (keys.current["KeyW"] || keys.current["ArrowUp"]) ? 1 : (keys.current["KeyS"] || keys.current["ArrowDown"]) ? -1 : 0;
    const strafe = (keys.current["KeyD"] || keys.current["ArrowRight"]) ? 1 : (keys.current["KeyA"] || keys.current["ArrowLeft"]) ? -1 : 0;

    // Gravity
    vel.current.y -= 9.8 * dt;

    // Jump
    if (keys.current["Space"] && onGround.current) {
      vel.current.y = 4.2;
      onGround.current = false;
    }

    // Horizontal movement
    const yaw = yawRef.current;
    dir.current.set(strafe, 0, forward).normalize().applyAxisAngle(new THREE.Vector3(0, 1, 0), yaw);
    const targetVel = dir.current.clone().multiplyScalar(speed);
    // lerp velocity for smoother accel/decel
    vel.current.x = THREE.MathUtils.damp(vel.current.x, targetVel.x, 6, dt);
    vel.current.z = THREE.MathUtils.damp(vel.current.z, targetVel.z, 6, dt);

    ref.current.position.addScaledVector(vel.current, dt);

    // Simple ground collision: terrain yβ‰ˆ0..small heights, keep feet on/above terrain
    if (ref.current.position.y <= 1) {
      ref.current.position.y = 1;
      vel.current.y = 0;
      onGround.current = true;
    }

    // Face movement direction
    if (forward !== 0 || strafe !== 0) {
      const angle = Math.atan2(dir.current.x, dir.current.z);
      ref.current.rotation.y = THREE.MathUtils.damp(ref.current.rotation.y, angle, 8, dt);
    }

    // Bobbing animation when moving
    const moving = forward !== 0 || strafe !== 0;
    const t = state.clock.elapsedTime;
    ref.current.position.y = 1 + (moving ? Math.sin(t * 10) * 0.06 : 0);
    (ref.current.children[0] as THREE.Mesh).rotation.z = moving ? Math.sin(t * 12) * 0.15 : 0; // arm swing

    // Camera follow (third-person) β€” rotate with Q/E
    const cam = state.camera as THREE.PerspectiveCamera;
    const camTarget = ref.current.position.clone().add(new THREE.Vector3(0, 1.5, 0));
    const behind = new THREE.Vector3(0, 2.2, 4.5).applyAxisAngle(new THREE.Vector3(0, 1, 0), yaw);
    const desired = camTarget.clone().add(behind);
    cam.position.lerp(desired, 0.12);
    cam.lookAt(camTarget);
    yawRef.current += ((keys.current["KeyQ"] ? -1 : 0) + (keys.current["KeyE"] ? 1 : 0)) * dt * 1.5;

    // Save shared pos
    posRef.current.copy(ref.current.position);
  });

  // Interaction: gather nearest resource within 1.6m
  const interact = () => {
    const p = ref.current.position;
    const nearest = nearestResource(p, resources);
    if (!nearest) return;
    const { type, index, dist } = nearest;
    if (dist <= 1.6) onGather(type as any, index);
  };

  return (
    <group ref={ref} position={[0, 1, 0]}>
      {/* Simple character: a capsule body + head, with fake arm swing */}
      <mesh castShadow position={[0, 0.8, 0]}> {/* body */}
        <capsuleGeometry args={[0.35, 0.8, 8, 16]} />
        <meshStandardMaterial color="#3b82f6" />
      </mesh>
      <mesh castShadow position={[0, 1.6, 0]}> {/* head */}
        <sphereGeometry args={[0.28, 16, 16]} />
        <meshStandardMaterial color="#93c5fd" />
      </mesh>
      <mesh castShadow position={[0.42, 1.1, 0]}> {/* right arm */}
        <cylinderGeometry args={[0.07, 0.07, 0.55, 8]} />
        <meshStandardMaterial color="#60a5fa" />
      </mesh>
      <mesh castShadow position={[-0.42, 1.1, 0]}> {/* left arm */}
        <cylinderGeometry args={[0.07, 0.07, 0.55, 8]} />
        <meshStandardMaterial color="#60a5fa" />
      </mesh>
      <mesh castShadow position={[0.18, 0.3, 0]}> {/* right leg */}
        <cylinderGeometry args={[0.08, 0.08, 0.6, 8]} />
        <meshStandardMaterial color="#2563eb" />
      </mesh>
      <mesh castShadow position={[-0.18, 0.3, 0]}> {/* left leg */}
        <cylinderGeometry args={[0.08, 0.08, 0.6, 8]} />
        <meshStandardMaterial color="#2563eb" />
      </mesh>
    </group>
  );
}

// -------------- Helpers ----------------

type Vec3 = [number, number, number];

function scatter(n: number, rangeX = 20, rangeZ = 20): Vec3[] {
  const arr: Vec3[] = [];
  for (let i = 0; i < n; i++) {
    const x = (Math.random() - 0.5) * rangeX * 2;
    const z = (Math.random() - 0.5) * rangeZ * 2;
    arr.push([x, 0, z]);
  }
  return arr;
}

function nearestResource(p: THREE.Vector3, res: { trees: Vec3[]; rocks: Vec3[]; berries: Vec3[] }) {
  let best: { type: string; index: number; dist: number } | null = null;
  const check = (type: keyof typeof res, arr: Vec3[]) => {
    arr.forEach((v, i) => {
      const d = dist3(p, v);
      if (!best || d < best.dist) best = { type, index: i, dist: d };
    });
  };
  check("trees", res.trees);
  check("rocks", res.rocks);
  check("berries", res.berries);
  return best;
}

function dist3(p: THREE.Vector3, v: Vec3) {
  const dx = p.x - v[0];
  const dy = p.y - v[1];
  const dz = p.z - v[2];
  return Math.sqrt(dx * dx + dy * dy + dz * dz);
}

function hash3(v: Vec3) {
  // tiny deterministic hash from position to [0,1)
  const s = Math.sin(v[0] * 12.9898 + v[1] * 78.233 + v[2] * 37.719) * 43758.5453;
  return s - Math.floor(s);
}

function clamp01(x: number) {
  return Math.max(0, Math.min(1, x));
}
Editor is loading...
Leave a Comment