Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions src/components/hud/HUD.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,17 @@ export default function HUD() {
const [showTimeMenu, setShowTimeMenu] = useState(false);

useEffect(() => {
const update = () => setTimeConfig(getTimeConfig(timeMode));
update();
const interval = setInterval(update, 5000);
setTimeConfig(getTimeConfig(timeMode));
const interval = setInterval(() => {
setTimeConfig((prev) => {
const next = getTimeConfig(timeMode);
// Only trigger a React re-render if the time string or phase actually changed
if (prev.formattedTime !== next.formattedTime || prev.resolvedPhase !== next.resolvedPhase) {
return next;
}
return prev;
});
}, 5000);
return () => clearInterval(interval);
}, [timeMode]);

Expand Down
268 changes: 139 additions & 129 deletions src/components/scene/GuavaTree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { useFrame } from '@react-three/fiber';
import { useMemo, useRef } from 'react';
import * as THREE from 'three';
import { Instances, Instance } from '@react-three/drei';
import {
seededRandom,
getBarkTexture,
Expand All @@ -17,88 +18,86 @@ import {
// Guava Tree — multi-stemmed, smooth bark, dense oval canopy
// ─────────────────────────────────────────────────────

/** Leaf cluster that sits AT a branch tip */
function GuavaLeafCluster({ position, size = 1.0 }: { position: Vec3; size?: number }) {
/** Aggregated Leaf clusters */
function GuavaLeafClusters({ positions }: { positions: Vec3[] }) {
const leafTex = getLeafTexture('#1a6e28', 'rgba(100, 210, 100, 0.25)', '#2d8838', 'oval');
const leaves = useMemo(() => {
const rng = seededRandom(
Math.floor(position[0] * 1000 + position[1] * 100 + position[2] * 10),
);
const count = 6 + Math.floor(rng() * 5);
const arr: { pos: Vec3; rot: [number, number, number]; scale: number }[] = [];
for (let i = 0; i < count; i++) {
const angle = rng() * Math.PI * 2;
const tilt = rng() * 0.5 - 0.25;
// Leaves radiate outward from this tip in a tight cluster
const spread = 0.08 + rng() * 0.18 * size;
arr.push({
pos: [
Math.cos(angle) * spread,
(rng() - 0.4) * 0.15 * size,
Math.sin(angle) * spread,
],
rot: [tilt, angle + rng() * 0.6, rng() * 0.3],
scale: (0.22 + rng() * 0.18) * size,
});
}
return arr;
}, [position, size]);

const allLeaves = useMemo(() => {
const leaves: { pos: Vec3; rot: [number, number, number]; scale: number }[] = [];
positions.forEach((pos, index) => {
const rng = seededRandom(Math.floor(pos[0] * 1000 + pos[1] * 100 + pos[2] * 10));
const size = 0.7 + (index % 4) * 0.12;
const count = 6 + Math.floor(rng() * 5);

for (let i = 0; i < count; i++) {
const angle = rng() * Math.PI * 2;
const tilt = rng() * 0.5 - 0.25;
const spread = 0.08 + rng() * 0.18 * size;
leaves.push({
pos: [
pos[0] + Math.cos(angle) * spread,
pos[1] + (rng() - 0.4) * 0.15 * size,
pos[2] + Math.sin(angle) * spread,
],
rot: [tilt, angle + rng() * 0.6, rng() * 0.3],
scale: (0.22 + rng() * 0.18) * size,
});
}
});
return leaves;
}, [positions]);

if (allLeaves.length === 0) return null;

return (
<group position={position}>
{leaves.map((leaf, i) => (
<mesh key={i} position={leaf.pos} rotation={leaf.rot} scale={leaf.scale}>
<planeGeometry args={[0.5, 0.7]} />
<meshStandardMaterial
map={leafTex}
color="#2a8a3a"
roughness={0.5}
metalness={0.03}
side={THREE.DoubleSide}
transparent
alphaTest={0.3}
/>
</mesh>
<Instances range={allLeaves.length} limit={allLeaves.length}>
<planeGeometry args={[0.5, 0.7]} />
<meshStandardMaterial
map={leafTex}
color="#2a8a3a"
roughness={0.5}
metalness={0.03}
side={THREE.DoubleSide}
transparent
alphaTest={0.3}
/>
{allLeaves.map((leaf, i) => (
<Instance key={i} position={leaf.pos} rotation={leaf.rot} scale={leaf.scale} />
))}
</group>
</Instances>
);
}

/** Renders all connected branch segments */
/** Renders all connected branch segments using Instances */
function BranchRenderer({ branches, barkColor = '#5a5045' }: {
branches: BranchSegment[];
barkColor?: string;
}) {
const barkTex = getBarkTexture(barkColor);
const barkNorm = getBarkNormal();

// Pre-compute transforms for each segment
const transforms = useMemo(() => {
return branches.map((b) => computeCylinderTransform(b.start, b.end));
const instancedData = useMemo(() => {
return branches.map((b) => {
const t = computeCylinderTransform(b.start, b.end);
const avgRadius = (b.radiusStart + b.radiusEnd) / 2;
return {
pos: t.position,
rot: t.quaternion,
scale: [avgRadius, t.length, avgRadius] as [number, number, number],
Comment on lines +81 to +86
};
});
}, [branches]);

if (instancedData.length === 0) return null;

return (
<group>
{branches.map((b, i) => {
const t = transforms[i];
return (
<mesh
key={i}
position={t.position}
quaternion={t.quaternion}
>
<cylinderGeometry args={[b.radiusEnd, b.radiusStart, t.length, 6]} />
<meshStandardMaterial
map={barkTex}
normalMap={barkNorm}
color={barkColor}
roughness={0.92}
metalness={0.0}
/>
</mesh>
);
})}
</group>
<Instances range={instancedData.length} limit={instancedData.length}>
<cylinderGeometry args={[1, 1, 1, 6]} />
<meshStandardMaterial map={barkTex} normalMap={barkNorm} color={barkColor} roughness={0.92} metalness={0.0} />
{instancedData.map((d, i) => (
<Instance key={i} position={d.pos} quaternion={d.rot} scale={d.scale} />
))}
</Instances>
);
}

Expand All @@ -115,45 +114,61 @@ function GuavaFruits({ fruitPositions, seed = 0 }: {
}
});

const fruits = useMemo(() => {
const { fruits, calyxes, stems } = useMemo(() => {
const rng = seededRandom(seed + 400);
return fruitPositions.map((pos) => ({
pos: [pos[0], pos[1] - 0.1 - rng() * 0.12, pos[2]] as Vec3,
scale: 0.07 + rng() * 0.035,
ripeness: rng(),
}));
const fArr: any[] = [];
const cArr: any[] = [];
const stArr: any[] = [];

fruitPositions.forEach((pos) => {
const p: Vec3 = [pos[0], pos[1] - 0.1 - rng() * 0.12, pos[2]];
const scale = 0.07 + rng() * 0.035;
const r = rng();

const color = new THREE.Color();
if (r < 0.5) {
color.lerpColors(new THREE.Color('#3a8a2a'), new THREE.Color('#8ac44a'), r * 2);
} else {
color.lerpColors(new THREE.Color('#8ac44a'), new THREE.Color('#d4c82a'), (r - 0.5) * 2);
}

fArr.push({ pos: p, scale, color });
cArr.push({ pos: [p[0], p[1] + scale * 0.85, p[2]], scale });
stArr.push({ pos: [p[0], p[1] + scale * 1.0, p[2]] });
});

return { fruits: fArr, calyxes: cArr, stems: stArr };
}, [fruitPositions, seed]);

return (
<group ref={groupRef}>
{fruits.map((fruit, i) => {
const r = fruit.ripeness;
const color = new THREE.Color();
if (r < 0.5) {
color.lerpColors(new THREE.Color('#3a8a2a'), new THREE.Color('#8ac44a'), r * 2);
} else {
color.lerpColors(new THREE.Color('#8ac44a'), new THREE.Color('#d4c82a'), (r - 0.5) * 2);
}

return (
<group key={i} position={fruit.pos}>
<mesh scale={[fruit.scale, fruit.scale * 1.05, fruit.scale]}>
<sphereGeometry args={[1, 10, 8]} />
<meshStandardMaterial color={color} roughness={0.5} metalness={0.01} />
</mesh>
{/* Calyx crown */}
<mesh position={[0, fruit.scale * 0.85, 0]} scale={[0.02, 0.015, 0.02]}>
<sphereGeometry args={[1, 6, 4]} />
<meshStandardMaterial color="#3a5a28" roughness={0.9} />
</mesh>
{/* Short stem connecting to branch */}
<mesh position={[0, fruit.scale * 1.0, 0]}>
<cylinderGeometry args={[0.005, 0.008, 0.08, 4]} />
<meshStandardMaterial color="#5a4a30" roughness={0.9} />
</mesh>
</group>
);
})}
{fruits.length > 0 && (
<Instances range={fruits.length} limit={fruits.length}>
<sphereGeometry args={[1, 10, 8]} />
<meshStandardMaterial roughness={0.5} metalness={0.01} />
{fruits.map((f, i) => (
<Instance key={i} position={f.pos} scale={[f.scale, f.scale * 1.05, f.scale]} color={f.color} />
))}
</Instances>
)}
{calyxes.length > 0 && (
<Instances range={calyxes.length} limit={calyxes.length}>
<sphereGeometry args={[1, 6, 4]} />
<meshStandardMaterial color="#3a5a28" roughness={0.9} />
{calyxes.map((c, i) => (
<Instance key={i} position={c.pos} scale={[0.02, 0.015, 0.02]} />
))}
</Instances>
)}
{stems.length > 0 && (
<Instances range={stems.length} limit={stems.length}>
<cylinderGeometry args={[0.005, 0.008, 0.08, 4]} />
<meshStandardMaterial color="#5a4a30" roughness={0.9} />
{stems.map((st, i) => (
<Instance key={i} position={st.pos} />
))}
</Instances>
)}
</group>
);
}
Expand All @@ -162,45 +177,41 @@ function GuavaFruits({ fruitPositions, seed = 0 }: {
function GuavaRoots({ seed = 0 }: { seed?: number }) {
const barkTex = getBarkTexture('#5a5045');

const roots = useMemo(() => {
const rootTransforms = useMemo(() => {
const rng = seededRandom(seed + 600);
const arr: { angle: number; length: number; thickness: number }[] = [];
const arr = [];
const count = 3 + Math.floor(rng() * 3);
for (let i = 0; i < count; i++) {
const angle = (i / count) * Math.PI * 2 + rng() * 0.6;
const length = 0.3 + rng() * 0.25;
const thickness = 0.04 + rng() * 0.03;

const start: Vec3 = [0, 0.05, 0];
const end: Vec3 = [
Math.cos(angle) * length,
-0.05,
Math.sin(angle) * length,
];
const t = computeCylinderTransform(start, end);
arr.push({
angle: (i / count) * Math.PI * 2 + rng() * 0.6,
length: 0.3 + rng() * 0.25,
thickness: 0.04 + rng() * 0.03,
pos: t.position,
rot: t.quaternion,
scale: [thickness * 0.3, t.length, thickness] as [number, number, number]
});
}
return arr;
}, [seed]);

const rootTransforms = useMemo(() => {
return roots.map((root) => {
const start: Vec3 = [0, 0.05, 0];
const end: Vec3 = [
Math.cos(root.angle) * root.length,
-0.05,
Math.sin(root.angle) * root.length,
];
return { ...computeCylinderTransform(start, end), root };
});
}, [roots]);
if (rootTransforms.length === 0) return null;

return (
<group>
<Instances range={rootTransforms.length} limit={rootTransforms.length}>
<cylinderGeometry args={[1, 1, 1, 5]} />
<meshStandardMaterial map={barkTex} color="#4a4035" roughness={0.98} />
{rootTransforms.map((rt, i) => (
<mesh
key={i}
position={rt.position}
quaternion={rt.quaternion}
>
<cylinderGeometry args={[rt.root.thickness * 0.3, rt.root.thickness, rt.length, 5]} />
<meshStandardMaterial map={barkTex} color="#4a4035" roughness={0.98} />
</mesh>
<Instance key={i} position={rt.pos} quaternion={rt.rot} scale={rt.scale} />
))}
</group>
</Instances>
);
}

Expand Down Expand Up @@ -298,12 +309,11 @@ export default function GuavaTree({ x, z, groundY = 0, isSpecial = false }: Guav

{/* ── Connected branches ── */}
<group ref={canopyRef}>
{/* Branch Segments */}
<BranchRenderer branches={treeData.branches} barkColor="#5a5045" />

{/* ── Leaf clusters AT branch tips ── */}
{treeData.leafTipPositions.map((pos, i) => (
<GuavaLeafCluster key={i} position={pos} size={0.7 + (i % 3) * 0.15} />
))}
{/* Aggregated Leaf Clusters */}
<GuavaLeafClusters positions={treeData.leafTipPositions} />

{/* ── Fruits hanging from branch junctions ── */}
<GuavaFruits fruitPositions={treeData.fruitPositions} seed={seed} />
Expand Down
Loading