695 lines
21 KiB
JavaScript
695 lines
21 KiB
JavaScript
import React, { useRef, useMemo, useEffect, useState } from 'react';
|
||
import { Canvas, useFrame, useLoader } from '@react-three/fiber';
|
||
import { OrbitControls, Grid, Text, Line } from '@react-three/drei';
|
||
import * as THREE from 'three';
|
||
import { useTurtleStore } from '../store/turtleStore';
|
||
|
||
// Texture mapping for Minecraft blocks
|
||
const TEXTURE_MAP = {
|
||
// Basic blocks
|
||
'minecraft:stone': 'stone',
|
||
'minecraft:cobblestone': 'cobblestone',
|
||
'minecraft:dirt': 'dirt',
|
||
'minecraft:grass_block': 'grass_block_side',
|
||
'minecraft:sand': 'sand',
|
||
'minecraft:red_sand': 'red_sand',
|
||
'minecraft:gravel': 'gravel',
|
||
'minecraft:bedrock': 'bedrock',
|
||
'minecraft:oak_log': 'oak_log',
|
||
'minecraft:oak_planks': 'oak_planks',
|
||
'minecraft:glass': 'glass',
|
||
|
||
// Stone variants
|
||
'minecraft:granite': 'granite',
|
||
'minecraft:diorite': 'diorite',
|
||
'minecraft:andesite': 'andesite',
|
||
'minecraft:deepslate': 'deepslate',
|
||
'minecraft:tuff': 'tuff',
|
||
'minecraft:calcite': 'calcite',
|
||
'minecraft:dripstone_block': 'dripstone_block',
|
||
|
||
// Ores (overworld)
|
||
'minecraft:coal_ore': 'coal_ore',
|
||
'minecraft:iron_ore': 'iron_ore',
|
||
'minecraft:gold_ore': 'gold_ore',
|
||
'minecraft:diamond_ore': 'diamond_ore',
|
||
'minecraft:emerald_ore': 'emerald_ore',
|
||
'minecraft:redstone_ore': 'redstone_ore',
|
||
'minecraft:lapis_ore': 'lapis_ore',
|
||
'minecraft:copper_ore': 'copper_ore',
|
||
|
||
// Deepslate ores
|
||
'minecraft:deepslate_coal_ore': 'deepslate_coal_ore',
|
||
'minecraft:deepslate_iron_ore': 'deepslate_iron_ore',
|
||
'minecraft:deepslate_gold_ore': 'deepslate_gold_ore',
|
||
'minecraft:deepslate_diamond_ore': 'deepslate_diamond_ore',
|
||
'minecraft:deepslate_emerald_ore': 'deepslate_emerald_ore',
|
||
'minecraft:deepslate_redstone_ore': 'deepslate_redstone_ore',
|
||
'minecraft:deepslate_lapis_ore': 'deepslate_lapis_ore',
|
||
'minecraft:deepslate_copper_ore': 'deepslate_copper_ore',
|
||
|
||
// Special blocks
|
||
'minecraft:obsidian': 'obsidian',
|
||
'minecraft:netherrack': 'netherrack',
|
||
'minecraft:soul_sand': 'soul_sand',
|
||
'minecraft:glowstone': 'glowstone',
|
||
'minecraft:end_stone': 'end_stone',
|
||
'minecraft:water': 'water_still',
|
||
'minecraft:lava': 'lava_still',
|
||
|
||
// Building blocks
|
||
'minecraft:bricks': 'bricks',
|
||
'minecraft:stone_bricks': 'stone_bricks',
|
||
'minecraft:mossy_cobblestone': 'mossy_cobblestone',
|
||
'minecraft:mossy_stone_bricks': 'mossy_stone_bricks',
|
||
'minecraft:prismarine': 'prismarine',
|
||
'minecraft:dark_prismarine': 'dark_prismarine',
|
||
};
|
||
|
||
// Function to get texture URL from a Minecraft texture service
|
||
function getTextureURL(blockName) {
|
||
const textureName = TEXTURE_MAP[blockName] || 'stone';
|
||
// Using a public Minecraft texture API
|
||
return `https://raw.githubusercontent.com/InventivetalentDev/minecraft-assets/1.20.4/assets/minecraft/textures/block/${textureName}.png`;
|
||
}
|
||
|
||
// Custom hook to load multiple textures
|
||
function useMinecraftTextures(blocks) {
|
||
const [textures, setTextures] = useState({});
|
||
const [loading, setLoading] = useState(true);
|
||
|
||
useEffect(() => {
|
||
const textureLoader = new THREE.TextureLoader();
|
||
const uniqueBlocks = [...new Set(blocks.map(b => b.name))];
|
||
const loadedTextures = {};
|
||
let loadedCount = 0;
|
||
|
||
if (uniqueBlocks.length === 0) {
|
||
setLoading(false);
|
||
return;
|
||
}
|
||
|
||
uniqueBlocks.forEach(blockName => {
|
||
const url = getTextureURL(blockName);
|
||
|
||
textureLoader.load(
|
||
url,
|
||
(texture) => {
|
||
texture.magFilter = THREE.NearestFilter;
|
||
texture.minFilter = THREE.NearestFilter;
|
||
texture.wrapS = THREE.RepeatWrapping;
|
||
texture.wrapT = THREE.RepeatWrapping;
|
||
loadedTextures[blockName] = texture;
|
||
loadedCount++;
|
||
|
||
if (loadedCount === uniqueBlocks.length) {
|
||
setTextures(loadedTextures);
|
||
setLoading(false);
|
||
}
|
||
},
|
||
undefined,
|
||
(error) => {
|
||
console.warn(`Failed to load texture for ${blockName}, using color fallback`);
|
||
loadedCount++;
|
||
|
||
if (loadedCount === uniqueBlocks.length) {
|
||
setTextures(loadedTextures);
|
||
setLoading(false);
|
||
}
|
||
}
|
||
);
|
||
});
|
||
}, [blocks]);
|
||
|
||
return { textures, loading };
|
||
}
|
||
|
||
// Minecraft-style turtle model
|
||
function TurtleModel({ turtle, isSelected, onClick }) {
|
||
const groupRef = useRef();
|
||
const { position, facing, mode } = turtle;
|
||
|
||
useFrame((state) => {
|
||
if (groupRef.current && isSelected) {
|
||
groupRef.current.children[0].position.y = Math.sin(state.clock.elapsedTime * 2) * 0.05;
|
||
}
|
||
});
|
||
|
||
if (!position) return null;
|
||
|
||
// Calculate rotation based on facing (0=North(-Z), 1=East(+X), 2=South(+Z), 3=West(-X))
|
||
// Add 180 degrees (Math.PI) to face the head forward initially, then apply facing rotation
|
||
const rotation = facing !== undefined ? [0, (facing * Math.PI / 2) + Math.PI, 0] : [0, Math.PI, 0];
|
||
|
||
const color = mode === 'mining' ? '#4ade80' :
|
||
mode === 'exploring' ? '#60a5fa' :
|
||
mode === 'returning' ? '#f59e0b' :
|
||
'#9ca3af';
|
||
|
||
return (
|
||
<group ref={groupRef} position={[position.x, position.y, position.z]} rotation={rotation} onClick={onClick}>
|
||
<group>
|
||
{/* Shell (main body) */}
|
||
<mesh position={[0, 0.2, 0]}>
|
||
<boxGeometry args={[0.8, 0.5, 0.9]} />
|
||
<meshStandardMaterial
|
||
color={color}
|
||
emissive={color}
|
||
emissiveIntensity={isSelected ? 0.4 : 0.2}
|
||
roughness={0.4}
|
||
/>
|
||
</mesh>
|
||
|
||
{/* Head */}
|
||
<mesh position={[0, 0.15, 0.55]}>
|
||
<boxGeometry args={[0.5, 0.35, 0.3]} />
|
||
<meshStandardMaterial color={color} roughness={0.3} />
|
||
</mesh>
|
||
|
||
{/* Eyes */}
|
||
<mesh position={[-0.12, 0.2, 0.7]}>
|
||
<boxGeometry args={[0.1, 0.1, 0.05]} />
|
||
<meshStandardMaterial color="#ffffff" emissive="#ffff00" emissiveIntensity={0.5} />
|
||
</mesh>
|
||
<mesh position={[0.12, 0.2, 0.7]}>
|
||
<boxGeometry args={[0.1, 0.1, 0.05]} />
|
||
<meshStandardMaterial color="#ffffff" emissive="#ffff00" emissiveIntensity={0.5} />
|
||
</mesh>
|
||
|
||
{/* Legs */}
|
||
<mesh position={[-0.3, -0.15, 0.25]}>
|
||
<boxGeometry args={[0.2, 0.2, 0.2]} />
|
||
<meshStandardMaterial color="#666" />
|
||
</mesh>
|
||
<mesh position={[0.3, -0.15, 0.25]}>
|
||
<boxGeometry args={[0.2, 0.2, 0.2]} />
|
||
<meshStandardMaterial color="#666" />
|
||
</mesh>
|
||
<mesh position={[-0.3, -0.15, -0.25]}>
|
||
<boxGeometry args={[0.2, 0.2, 0.2]} />
|
||
<meshStandardMaterial color="#666" />
|
||
</mesh>
|
||
<mesh position={[0.3, -0.15, -0.25]}>
|
||
<boxGeometry args={[0.2, 0.2, 0.2]} />
|
||
<meshStandardMaterial color="#666" />
|
||
</mesh>
|
||
|
||
{/* Tail */}
|
||
<mesh position={[0, 0.1, -0.55]}>
|
||
<boxGeometry args={[0.15, 0.15, 0.2]} />
|
||
<meshStandardMaterial color={color} />
|
||
</mesh>
|
||
</group>
|
||
|
||
{/* Selection indicator */}
|
||
{isSelected && (
|
||
<>
|
||
<mesh position={[0, 1.2, 0]}>
|
||
<coneGeometry args={[0.3, 0.5, 4]} />
|
||
<meshStandardMaterial color="#ffffff" emissive="#ffffff" emissiveIntensity={0.8} />
|
||
</mesh>
|
||
|
||
{/* Selection ring */}
|
||
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, -0.5, 0]}>
|
||
<ringGeometry args={[1, 1.2, 32]} />
|
||
<meshBasicMaterial color="#ffffff" side={THREE.DoubleSide} transparent opacity={0.5} />
|
||
</mesh>
|
||
</>
|
||
)}
|
||
|
||
{/* Turtle ID label */}
|
||
<Text
|
||
position={[0, 1.5, 0]}
|
||
fontSize={0.4}
|
||
color="white"
|
||
anchorX="center"
|
||
anchorY="middle"
|
||
>
|
||
🐢 {turtle.turtleID}
|
||
</Text>
|
||
</group>
|
||
);
|
||
}
|
||
|
||
// Path trail component
|
||
function PathTrail({ turtle }) {
|
||
const { position, homePosition } = turtle;
|
||
|
||
if (!position || !homePosition) return null;
|
||
|
||
const points = [
|
||
new THREE.Vector3(position.x, position.y, position.z),
|
||
new THREE.Vector3(homePosition.x, homePosition.y, homePosition.z)
|
||
];
|
||
|
||
return (
|
||
<Line
|
||
points={points}
|
||
color="#60a5fa"
|
||
lineWidth={2}
|
||
transparent
|
||
opacity={0.3}
|
||
dashed
|
||
dashScale={2}
|
||
/>
|
||
);
|
||
}
|
||
|
||
// Get color and texture info for block type
|
||
function getBlockAppearance(blockName) {
|
||
if (!blockName) return { color: '#888', pattern: 'solid' };
|
||
|
||
const name = blockName.toLowerCase();
|
||
|
||
// Ores - bright colors with sparkle
|
||
if (name.includes('diamond')) return { color: '#00ffff', pattern: 'ore', emissive: '#00ffff', intensity: 0.3 };
|
||
if (name.includes('emerald')) return { color: '#00ff00', pattern: 'ore', emissive: '#00ff00', intensity: 0.3 };
|
||
if (name.includes('gold')) return { color: '#ffd700', pattern: 'ore', emissive: '#ffd700', intensity: 0.2 };
|
||
if (name.includes('iron')) return { color: '#d4d4d4', pattern: 'ore' };
|
||
if (name.includes('coal')) return { color: '#1a1a1a', pattern: 'ore' };
|
||
if (name.includes('redstone')) return { color: '#ff0000', pattern: 'ore', emissive: '#ff0000', intensity: 0.2 };
|
||
if (name.includes('lapis')) return { color: '#1e40af', pattern: 'ore' };
|
||
if (name.includes('copper')) return { color: '#ff8c00', pattern: 'ore' };
|
||
|
||
// Common blocks - textured
|
||
if (name.includes('stone') && !name.includes('cobble')) return { color: '#7f7f7f', pattern: 'stone' };
|
||
if (name.includes('cobblestone')) return { color: '#808080', pattern: 'cobble' };
|
||
if (name.includes('dirt')) return { color: '#8b4513', pattern: 'dirt' };
|
||
if (name.includes('grass')) return { color: '#228b22', pattern: 'grass' };
|
||
if (name.includes('sand')) return { color: '#f4a460', pattern: 'sand' };
|
||
if (name.includes('gravel')) return { color: '#808080', pattern: 'gravel' };
|
||
if (name.includes('bedrock')) return { color: '#222', pattern: 'solid' };
|
||
if (name.includes('obsidian')) return { color: '#1a0033', pattern: 'solid' };
|
||
if (name.includes('netherrack')) return { color: '#8b0000', pattern: 'netherrack' };
|
||
|
||
return { color: '#666666', pattern: 'solid' };
|
||
}
|
||
|
||
// WorldBlocks component to render discovered blocks with Minecraft textures
|
||
function WorldBlocks({ blocks }) {
|
||
const [hoveredBlock, setHoveredBlock] = useState(null);
|
||
const { textures, loading } = useMinecraftTextures(blocks);
|
||
|
||
// Group blocks by block name for better performance
|
||
const blockGroups = useMemo(() => {
|
||
const groups = new Map();
|
||
|
||
blocks.forEach(block => {
|
||
if (!groups.has(block.name)) {
|
||
groups.set(block.name, []);
|
||
}
|
||
groups.get(block.name).push(block);
|
||
});
|
||
|
||
return Array.from(groups.entries());
|
||
}, [blocks]);
|
||
|
||
if (loading) {
|
||
return <Text position={[0, 5, 0]} fontSize={0.5} color="white">Loading textures...</Text>;
|
||
}
|
||
|
||
return (
|
||
<group>
|
||
{blockGroups.map(([blockName, blockList]) => {
|
||
const appearance = getBlockAppearance(blockName);
|
||
const texture = textures[blockName];
|
||
|
||
return (
|
||
<group key={blockName}>
|
||
{blockList.map((block) => {
|
||
const blockKey = `${block.x},${block.y},${block.z}`;
|
||
const isHovered = hoveredBlock === blockKey;
|
||
|
||
return (
|
||
<group key={blockKey}>
|
||
<mesh
|
||
position={[block.x, block.y, block.z]}
|
||
onPointerOver={(e) => {
|
||
e.stopPropagation();
|
||
setHoveredBlock(blockKey);
|
||
}}
|
||
onPointerOut={() => setHoveredBlock(null)}
|
||
>
|
||
<boxGeometry args={[1.0, 1.0, 1.0]} />
|
||
<meshStandardMaterial
|
||
map={texture}
|
||
color={texture ? '#ffffff' : appearance.color}
|
||
emissive={appearance.emissive || appearance.color}
|
||
emissiveIntensity={isHovered ? (appearance.intensity || 0.05) + 0.2 : (appearance.intensity || 0.05)}
|
||
roughness={appearance.pattern === 'ore' ? 0.4 : 0.8}
|
||
metalness={appearance.pattern === 'ore' ? 0.2 : 0.0}
|
||
transparent
|
||
opacity={isHovered ? 0.98 : 0.9}
|
||
/>
|
||
</mesh>
|
||
|
||
{/* Block label on hover */}
|
||
{isHovered && (
|
||
<Text
|
||
position={[block.x, block.y + 0.8, block.z]}
|
||
fontSize={0.3}
|
||
color="white"
|
||
anchorX="center"
|
||
anchorY="middle"
|
||
outlineWidth={0.05}
|
||
outlineColor="#000000"
|
||
>
|
||
{block.name.replace('minecraft:', '').replace(/_/g, ' ').toUpperCase()}
|
||
</Text>
|
||
)}
|
||
</group>
|
||
);
|
||
})}
|
||
</group>
|
||
);
|
||
})}
|
||
</group>
|
||
);
|
||
}
|
||
|
||
// Path trail component (old version removed above)
|
||
function OldPathTrail({ turtle }) {
|
||
const { position, homePosition } = turtle;
|
||
|
||
if (!position || !homePosition) return null;
|
||
|
||
const points = [
|
||
new THREE.Vector3(position.x, position.y, position.z),
|
||
new THREE.Vector3(homePosition.x, homePosition.y, homePosition.z)
|
||
];
|
||
|
||
return (
|
||
<Line
|
||
points={points}
|
||
color="#60a5fa"
|
||
lineWidth={2}
|
||
dashed
|
||
dashScale={2}
|
||
transparent
|
||
opacity={0.5}
|
||
/>
|
||
);
|
||
}
|
||
|
||
// Home marker component
|
||
function HomeMarker({ position }) {
|
||
if (!position) return null;
|
||
|
||
return (
|
||
<group position={[position.x, position.y, position.z]}>
|
||
<mesh>
|
||
<cylinderGeometry args={[1, 0.5, 0.5, 6]} />
|
||
<meshStandardMaterial color="#10b981" emissive="#10b981" emissiveIntensity={0.3} />
|
||
</mesh>
|
||
<Text
|
||
position={[0, 1.2, 0]}
|
||
fontSize={0.5}
|
||
color="#10b981"
|
||
anchorX="center"
|
||
anchorY="middle"
|
||
>
|
||
HOME
|
||
</Text>
|
||
</group>
|
||
);
|
||
}
|
||
|
||
// Mining area wireframe component
|
||
function MiningArea({ area, turtle, isSelected, onClick }) {
|
||
const meshRef = useRef();
|
||
const [hovered, setHovered] = useState(false);
|
||
|
||
// Calculate dimensions
|
||
const width = Math.abs(area.endX - area.startX) + 1;
|
||
const height = Math.abs(area.endY - area.startY) + 1;
|
||
const depth = Math.abs(area.endZ - area.startZ) + 1;
|
||
|
||
// Calculate center position
|
||
const centerX = (area.startX + area.endX) / 2;
|
||
const centerY = (area.startY + area.endY) / 2;
|
||
const centerZ = (area.startZ + area.endZ) / 2;
|
||
|
||
// Color based on turtle or status
|
||
const getColor = () => {
|
||
if (area.status === 'completed') return '#10b981'; // green
|
||
if (area.status === 'mining') return '#f59e0b'; // orange
|
||
if (turtle) return turtle.color || '#3b82f6'; // turtle color or blue
|
||
return '#6366f1'; // default purple
|
||
};
|
||
|
||
const color = getColor();
|
||
const opacity = isSelected ? 0.3 : hovered ? 0.2 : 0.1;
|
||
const lineWidth = isSelected ? 2.5 : hovered ? 2 : 1.5;
|
||
|
||
// Animate rotation slightly
|
||
useFrame((state) => {
|
||
if (meshRef.current && isSelected) {
|
||
meshRef.current.rotation.y = Math.sin(state.clock.elapsedTime * 0.5) * 0.1;
|
||
}
|
||
});
|
||
|
||
return (
|
||
<group
|
||
position={[centerX, centerY, centerZ]}
|
||
onClick={onClick}
|
||
onPointerOver={() => setHovered(true)}
|
||
onPointerOut={() => setHovered(false)}
|
||
ref={meshRef}
|
||
>
|
||
{/* Wireframe box */}
|
||
<lineSegments>
|
||
<edgesGeometry args={[new THREE.BoxGeometry(width, height, depth)]} />
|
||
<lineBasicMaterial color={color} linewidth={lineWidth} />
|
||
</lineSegments>
|
||
|
||
{/* Semi-transparent fill */}
|
||
<mesh>
|
||
<boxGeometry args={[width, height, depth]} />
|
||
<meshStandardMaterial
|
||
color={color}
|
||
transparent
|
||
opacity={opacity}
|
||
side={THREE.DoubleSide}
|
||
/>
|
||
</mesh>
|
||
|
||
{/* Label */}
|
||
<Text
|
||
position={[0, height / 2 + 1, 0]}
|
||
fontSize={0.5}
|
||
color={color}
|
||
anchorX="center"
|
||
anchorY="middle"
|
||
>
|
||
{area.areaName || `Area ${area.areaID}`}
|
||
</Text>
|
||
|
||
{/* Dimensions label */}
|
||
<Text
|
||
position={[0, height / 2 + 0.3, 0]}
|
||
fontSize={0.3}
|
||
color="white"
|
||
anchorX="center"
|
||
anchorY="middle"
|
||
>
|
||
{width}×{height}×{depth}
|
||
</Text>
|
||
|
||
{/* Status indicator */}
|
||
{area.status === 'mining' && (
|
||
<mesh position={[0, height / 2 + 1.8, 0]}>
|
||
<sphereGeometry args={[0.2, 16, 16]} />
|
||
<meshStandardMaterial color="#f59e0b" emissive="#f59e0b" emissiveIntensity={0.8} />
|
||
</mesh>
|
||
)}
|
||
</group>
|
||
);
|
||
}
|
||
|
||
// Player marker component
|
||
function PlayerMarker({ player }) {
|
||
const meshRef = useRef();
|
||
|
||
// Bobbing animation
|
||
useFrame((state) => {
|
||
if (meshRef.current) {
|
||
meshRef.current.position.y = player.position.y + Math.sin(state.clock.elapsedTime * 2) * 0.1;
|
||
}
|
||
});
|
||
|
||
return (
|
||
<group position={[player.position.x, player.position.y, player.position.z]}>
|
||
{/* Player head (Steve-like) */}
|
||
<mesh ref={meshRef} position={[0, 1, 0]}>
|
||
<boxGeometry args={[0.6, 0.6, 0.6]} />
|
||
<meshStandardMaterial
|
||
color="#8b7355"
|
||
emissive="#4a90e2"
|
||
emissiveIntensity={0.3}
|
||
/>
|
||
</mesh>
|
||
|
||
{/* Body */}
|
||
<mesh position={[0, 0.2, 0]}>
|
||
<boxGeometry args={[0.6, 0.9, 0.3]} />
|
||
<meshStandardMaterial color="#4a90e2" />
|
||
</mesh>
|
||
|
||
{/* Glow effect */}
|
||
<pointLight color="#4a90e2" intensity={1} distance={3} />
|
||
|
||
{/* Label */}
|
||
<Text
|
||
position={[0, 2, 0]}
|
||
fontSize={0.4}
|
||
color="#4a90e2"
|
||
anchorX="center"
|
||
anchorY="middle"
|
||
outlineWidth={0.05}
|
||
outlineColor="#000000"
|
||
>
|
||
Player {player.playerID}
|
||
</Text>
|
||
</group>
|
||
);
|
||
}
|
||
|
||
// Main scene component
|
||
function Scene() {
|
||
const turtles = useTurtleStore((state) => state.getTurtleArray());
|
||
const players = useTurtleStore((state) => Object.values(state.players || {}));
|
||
const selectedTurtleId = useTurtleStore((state) => state.selectedTurtleId);
|
||
const selectTurtle = useTurtleStore((state) => state.selectTurtle);
|
||
const worldBlocks = useTurtleStore((state) => state.worldBlocks || []);
|
||
|
||
// Mining areas state
|
||
const [miningAreas, setMiningAreas] = useState([]);
|
||
const [selectedAreaId, setSelectedAreaId] = useState(null);
|
||
|
||
// Load mining areas
|
||
useEffect(() => {
|
||
const fetchMiningAreas = async () => {
|
||
try {
|
||
const response = await fetch('http://localhost:3001/api/mining-areas');
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
setMiningAreas(data);
|
||
}
|
||
} catch (error) {
|
||
console.error('Failed to fetch mining areas:', error);
|
||
}
|
||
};
|
||
|
||
fetchMiningAreas();
|
||
const interval = setInterval(fetchMiningAreas, 5000); // Refresh every 5 seconds
|
||
return () => clearInterval(interval);
|
||
}, []);
|
||
|
||
// Calculate center point for camera focus
|
||
const centerPoint = useMemo(() => {
|
||
if (turtles.length === 0) return [0, 0, 0];
|
||
|
||
let sumX = 0, sumY = 0, sumZ = 0;
|
||
let count = 0;
|
||
|
||
turtles.forEach(turtle => {
|
||
if (turtle.position) {
|
||
sumX += turtle.position.x;
|
||
sumY += turtle.position.y;
|
||
sumZ += turtle.position.z;
|
||
count++;
|
||
}
|
||
});
|
||
|
||
if (count === 0) return [0, 0, 0];
|
||
return [sumX / count, sumY / count, sumZ / count];
|
||
}, [turtles]);
|
||
|
||
// Get home position from first turtle
|
||
const homePosition = turtles.find(t => t.homePosition)?.homePosition;
|
||
|
||
return (
|
||
<>
|
||
<ambientLight intensity={0.6} />
|
||
<pointLight position={[10, 10, 10]} intensity={1.2} />
|
||
<pointLight position={[-10, -10, -10]} intensity={0.6} />
|
||
<pointLight position={[0, 20, 0]} intensity={0.8} color="#ffffff" />
|
||
|
||
{/* Grid */}
|
||
<Grid
|
||
args={[100, 100]}
|
||
cellSize={1}
|
||
cellThickness={0.5}
|
||
cellColor="#1e293b"
|
||
sectionSize={5}
|
||
sectionThickness={1}
|
||
sectionColor="#334155"
|
||
fadeDistance={50}
|
||
fadeStrength={1}
|
||
followCamera={false}
|
||
infiniteGrid
|
||
/>
|
||
|
||
{/* Render discovered blocks */}
|
||
<WorldBlocks blocks={worldBlocks} />
|
||
|
||
{/* Mining areas */}
|
||
{miningAreas.map((area) => {
|
||
const turtle = turtles.find(t => t.turtleID === area.turtleID);
|
||
return (
|
||
<MiningArea
|
||
key={area.areaID}
|
||
area={area}
|
||
turtle={turtle}
|
||
isSelected={selectedAreaId === area.areaID}
|
||
onClick={() => setSelectedAreaId(selectedAreaId === area.areaID ? null : area.areaID)}
|
||
/>
|
||
);
|
||
})}
|
||
|
||
{/* Home marker */}
|
||
{homePosition && <HomeMarker position={homePosition} />}
|
||
|
||
{/* Turtles and paths */}
|
||
{turtles.map((turtle) => (
|
||
<React.Fragment key={turtle.turtleID}>
|
||
<PathTrail turtle={turtle} />
|
||
<TurtleModel
|
||
turtle={turtle}
|
||
isSelected={selectedTurtleId === turtle.turtleID}
|
||
onClick={() => selectTurtle(turtle.turtleID)}
|
||
/>
|
||
</React.Fragment>
|
||
))}
|
||
|
||
{/* Players */}
|
||
{players.map((player) => (
|
||
<PlayerMarker key={player.playerID} player={player} />
|
||
))}
|
||
|
||
{/* Camera controls */}
|
||
<OrbitControls
|
||
target={centerPoint}
|
||
enableDamping
|
||
dampingFactor={0.05}
|
||
minDistance={5}
|
||
maxDistance={100}
|
||
/>
|
||
</>
|
||
);
|
||
}
|
||
|
||
// Main Map3D component
|
||
export default function Map3D() {
|
||
return (
|
||
<div style={{ width: '100%', height: '100%', background: '#0a0e1a' }}>
|
||
<Canvas
|
||
camera={{ position: [15, 15, 15], fov: 60 }}
|
||
style={{ background: '#0a0e1a' }}
|
||
>
|
||
<Scene />
|
||
</Canvas>
|
||
</div>
|
||
);
|
||
}
|