Untitled
Gameunknown
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