import React, { useRef, useMemo, useEffect, useState, useCallback } from 'react'; import { Canvas, useFrame, useLoader, useThree } from '@react-three/fiber'; import { OrbitControls, Grid, Text, Line } from '@react-three/drei'; import * as THREE from 'three'; import { useTurtleStore } from '../store/turtleStore'; // World interaction modes const INTERACTION_MODE = { LOOK: 'look', // Default orbit camera MOVE: 'move', // Click block to send turtle there BUILD: 'build', // Click face to place block preview SELECT: 'select' // Click-drag area selection }; // 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', // Logs 'minecraft:spruce_log': 'spruce_log', 'minecraft:birch_log': 'birch_log', 'minecraft:jungle_log': 'jungle_log', 'minecraft:acacia_log': 'acacia_log', 'minecraft:dark_oak_log': 'dark_oak_log', 'minecraft:mangrove_log': 'mangrove_log', 'minecraft:cherry_log': 'cherry_log', // Planks 'minecraft:spruce_planks': 'spruce_planks', 'minecraft:birch_planks': 'birch_planks', 'minecraft:jungle_planks': 'jungle_planks', 'minecraft:acacia_planks': 'acacia_planks', 'minecraft:dark_oak_planks': 'dark_oak_planks', // Stone variants 'minecraft:granite': 'granite', 'minecraft:polished_granite': 'polished_granite', 'minecraft:diorite': 'diorite', 'minecraft:polished_diorite': 'polished_diorite', 'minecraft:andesite': 'andesite', 'minecraft:polished_andesite': 'polished_andesite', 'minecraft:deepslate': 'deepslate', 'minecraft:cobbled_deepslate': 'cobbled_deepslate', 'minecraft:tuff': 'tuff', 'minecraft:calcite': 'calcite', 'minecraft:dripstone_block': 'dripstone_block', 'minecraft:smooth_stone': 'smooth_stone', 'minecraft:sandstone': 'sandstone', 'minecraft:red_sandstone': 'red_sandstone', // 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', // Nether ores 'minecraft:nether_gold_ore': 'nether_gold_ore', 'minecraft:nether_quartz_ore': 'nether_quartz_ore', 'minecraft:ancient_debris': 'ancient_debris_side', // Special blocks 'minecraft:obsidian': 'obsidian', 'minecraft:crying_obsidian': 'crying_obsidian', 'minecraft:netherrack': 'netherrack', 'minecraft:soul_sand': 'soul_sand', 'minecraft:soul_soil': 'soul_soil', 'minecraft:glowstone': 'glowstone', 'minecraft:end_stone': 'end_stone', 'minecraft:water': 'water_still', 'minecraft:lava': 'lava_still', 'minecraft:magma_block': 'magma', 'minecraft:amethyst_block': 'amethyst_block', 'minecraft:budding_amethyst': 'budding_amethyst', 'minecraft:moss_block': 'moss_block', 'minecraft:clay': 'clay', 'minecraft:terracotta': 'terracotta', 'minecraft:packed_ice': 'packed_ice', 'minecraft:blue_ice': 'blue_ice', 'minecraft:ice': 'ice', 'minecraft:snow_block': 'snow', 'minecraft:mycelium': 'mycelium_side', 'minecraft:podzol': 'podzol_side', // Building blocks 'minecraft:bricks': 'bricks', 'minecraft:stone_bricks': 'stone_bricks', 'minecraft:cracked_stone_bricks': 'cracked_stone_bricks', 'minecraft:mossy_cobblestone': 'mossy_cobblestone', 'minecraft:mossy_stone_bricks': 'mossy_stone_bricks', 'minecraft:prismarine': 'prismarine', 'minecraft:dark_prismarine': 'dark_prismarine', 'minecraft:purpur_block': 'purpur_block', 'minecraft:nether_bricks': 'nether_bricks', 'minecraft:red_nether_bricks': 'red_nether_bricks', 'minecraft:quartz_block': 'quartz_block_side', 'minecraft:basalt': 'basalt_side', 'minecraft:polished_basalt': 'polished_basalt_side', 'minecraft:blackstone': 'blackstone', 'minecraft:polished_blackstone': 'polished_blackstone', 'minecraft:gilded_blackstone': 'gilded_blackstone', // Functional blocks 'minecraft:crafting_table': 'crafting_table_front', 'minecraft:furnace': 'furnace_front', 'minecraft:chest': 'barrel_side', 'minecraft:barrel': 'barrel_side', 'minecraft:bookshelf': 'bookshelf', 'minecraft:tnt': 'tnt_side', 'minecraft:spawner': 'spawner', }; // Blocks with different top/side/bottom textures const MULTI_FACE_BLOCKS = { 'minecraft:grass_block': { top: 'grass_block_top', side: 'grass_block_side', bottom: 'dirt' }, 'minecraft:mycelium': { top: 'mycelium_top', side: 'mycelium_side', bottom: 'dirt' }, 'minecraft:podzol': { top: 'podzol_top', side: 'podzol_side', bottom: 'dirt' }, 'minecraft:oak_log': { top: 'oak_log_top', side: 'oak_log', bottom: 'oak_log_top' }, 'minecraft:spruce_log': { top: 'spruce_log_top', side: 'spruce_log', bottom: 'spruce_log_top' }, 'minecraft:birch_log': { top: 'birch_log_top', side: 'birch_log', bottom: 'birch_log_top' }, 'minecraft:jungle_log': { top: 'jungle_log_top', side: 'jungle_log', bottom: 'jungle_log_top' }, 'minecraft:acacia_log': { top: 'acacia_log_top', side: 'acacia_log', bottom: 'acacia_log_top' }, 'minecraft:dark_oak_log': { top: 'dark_oak_log_top', side: 'dark_oak_log', bottom: 'dark_oak_log_top' }, 'minecraft:sandstone': { top: 'sandstone_top', side: 'sandstone', bottom: 'sandstone_bottom' }, 'minecraft:red_sandstone': { top: 'red_sandstone_top', side: 'red_sandstone', bottom: 'red_sandstone_bottom' }, 'minecraft:crafting_table': { top: 'crafting_table_top', side: 'crafting_table_front', bottom: 'oak_planks' }, 'minecraft:furnace': { top: 'furnace_top', side: 'furnace_front', bottom: 'furnace_top' }, 'minecraft:tnt': { top: 'tnt_top', side: 'tnt_side', bottom: 'tnt_bottom' }, 'minecraft:quartz_block': { top: 'quartz_block_top', side: 'quartz_block_side', bottom: 'quartz_block_bottom' }, 'minecraft:basalt': { top: 'basalt_top', side: 'basalt_side', bottom: 'basalt_top' }, 'minecraft:barrel': { top: 'barrel_top', side: 'barrel_side', bottom: 'barrel_bottom' }, }; // 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 (including multi-face) function useMinecraftTextures(blocks) { const [textures, setTextures] = useState({}); const [multiFaceTextures, setMultiFaceTextures] = useState({}); const [loading, setLoading] = useState(true); useEffect(() => { const textureLoader = new THREE.TextureLoader(); const uniqueBlocks = [...new Set(blocks.map(b => b.name))]; const loadedTextures = {}; const loadedMultiFace = {}; let totalToLoad = 0; let loadedCount = 0; if (uniqueBlocks.length === 0) { setLoading(false); return; } // Collect all unique texture names to load const textureNames = new Set(); uniqueBlocks.forEach(blockName => { const multiFace = MULTI_FACE_BLOCKS[blockName]; if (multiFace) { textureNames.add(multiFace.top); textureNames.add(multiFace.side); textureNames.add(multiFace.bottom); } else { const textureName = TEXTURE_MAP[blockName] || null; if (textureName) textureNames.add(textureName); } }); totalToLoad = textureNames.size; if (totalToLoad === 0) { setLoading(false); return; } const loadedTexturesByName = {}; const checkDone = () => { loadedCount++; if (loadedCount >= totalToLoad) { // Build per-block texture maps uniqueBlocks.forEach(blockName => { const multiFace = MULTI_FACE_BLOCKS[blockName]; if (multiFace) { const top = loadedTexturesByName[multiFace.top]; const side = loadedTexturesByName[multiFace.side]; const bottom = loadedTexturesByName[multiFace.bottom]; if (top && side && bottom) { loadedMultiFace[blockName] = [side, side, top, bottom, side, side]; } else { // Fallback to single texture loadedTextures[blockName] = side || top || bottom || null; } } else { const textureName = TEXTURE_MAP[blockName]; if (textureName && loadedTexturesByName[textureName]) { loadedTextures[blockName] = loadedTexturesByName[textureName]; } } }); setTextures(loadedTextures); setMultiFaceTextures(loadedMultiFace); setLoading(false); } }; textureNames.forEach(textureName => { const url = `https://raw.githubusercontent.com/InventivetalentDev/minecraft-assets/1.20.4/assets/minecraft/textures/block/${textureName}.png`; textureLoader.load( url, (texture) => { texture.magFilter = THREE.NearestFilter; texture.minFilter = THREE.NearestFilter; texture.wrapS = THREE.RepeatWrapping; texture.wrapT = THREE.RepeatWrapping; texture.colorSpace = THREE.SRGBColorSpace; loadedTexturesByName[textureName] = texture; checkDone(); }, undefined, () => { checkDone(); } ); }); }, [blocks]); return { textures, multiFaceTextures, loading }; } // Minecraft-style turtle model function TurtleModel({ turtle, isSelected, onClick }) { const groupRef = useRef(); const { position, facing, mode, state } = turtle; const activeMode = state || mode || 'idle'; 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)) const facingRotations = [Math.PI, -Math.PI/2, 0, Math.PI/2]; const rotation = facing !== undefined ? [0, facingRotations[facing], 0] : [0, 0, 0]; const color = activeMode === 'mining' ? '#4ade80' : activeMode === 'exploring' ? '#60a5fa' : activeMode === 'returning' || activeMode === 'goHome' ? '#f59e0b' : activeMode === 'refueling' ? '#ef4444' : activeMode === 'farming' ? '#22c55e' : activeMode === 'dumpInventory' || activeMode === 'dumping' ? '#a855f7' : activeMode === 'moving' ? '#06b6d4' : '#9ca3af'; return ( {/* Main shell body - rounded edges */} {/* Shell pattern - hexagonal plates */} {/* Head with visor */} {/* Visor/Face plate */} {/* Eyes/LED indicators */} {/* LED glow */} {/* Legs - more mechanical looking */} {/* Feet */} {/* Tool/Mining arm - only show when mining */} {activeMode === 'mining' && ( <> )} {/* Antenna */} {/* Ambient glow around turtle */} {/* Selection indicator */} {isSelected && ( <> {/* Animated selection ring */} )} {/* Turtle ID label */} 🐢 {turtle.turtleID} ); } // 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 ( ); } // 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 using chunked InstancedMesh rendering for performance function WorldBlocks({ blocks, interactionMode, selectedTurtleId, onBlockClick }) { const [hoveredBlock, setHoveredBlock] = useState(null); const { textures, multiFaceTextures, loading } = useMinecraftTextures(blocks); // Chunk size (16x16 like Minecraft) const CHUNK_SIZE = 16; // Group blocks into chunks and by block type within each chunk const chunks = useMemo(() => { const chunkMap = new Map(); blocks.forEach(block => { const chunkX = Math.floor(block.x / CHUNK_SIZE); const chunkZ = Math.floor(block.z / CHUNK_SIZE); const chunkKey = `${chunkX},${chunkZ}`; if (!chunkMap.has(chunkKey)) { chunkMap.set(chunkKey, { key: chunkKey, chunkX, chunkZ, blocksByType: new Map() }); } const chunk = chunkMap.get(chunkKey); if (!chunk.blocksByType.has(block.name)) { chunk.blocksByType.set(block.name, []); } chunk.blocksByType.get(block.name).push(block); }); return Array.from(chunkMap.values()); }, [blocks]); if (loading) { return Loading textures...; } return ( {chunks.map(chunk => ( ))} {/* Hover label overlay */} {hoveredBlock && ( {hoveredBlock.name.replace('minecraft:', '').replace(/_/g, ' ').toUpperCase()} {'\n'} {`(${hoveredBlock.x}, ${hoveredBlock.y}, ${hoveredBlock.z})`} )} ); } // Individual chunk rendered with InstancedMesh per block type function ChunkMesh({ chunk, textures, multiFaceTextures, hoveredBlock, setHoveredBlock, interactionMode, selectedTurtleId, onBlockClick }) { return ( {Array.from(chunk.blocksByType.entries()).map(([blockName, blockList]) => ( ))} ); } // InstancedMesh for all blocks of one type within a chunk function BlockTypeInstances({ blockName, blockList, textures, multiFaceTextures, hoveredBlock, setHoveredBlock, interactionMode, selectedTurtleId, onBlockClick }) { const meshRef = useRef(); const appearance = useMemo(() => getBlockAppearance(blockName), [blockName]); const texture = textures[blockName]; const multiFace = multiFaceTextures[blockName]; const tempMatrix = useMemo(() => new THREE.Matrix4(), []); const tempColor = useMemo(() => new THREE.Color(), []); // Set instance matrices useEffect(() => { if (!meshRef.current) return; blockList.forEach((block, i) => { tempMatrix.setPosition(block.x, block.y, block.z); meshRef.current.setMatrixAt(i, tempMatrix); // Default color (white for textured, appearance color for untextured) tempColor.set(texture || multiFace ? '#ffffff' : appearance.color); meshRef.current.setColorAt(i, tempColor); }); meshRef.current.instanceMatrix.needsUpdate = true; if (meshRef.current.instanceColor) meshRef.current.instanceColor.needsUpdate = true; }, [blockList, texture, multiFace, appearance, tempMatrix, tempColor]); // Update hover highlighting useEffect(() => { if (!meshRef.current || !meshRef.current.instanceColor) return; blockList.forEach((block, i) => { const isHovered = hoveredBlock && hoveredBlock.x === block.x && hoveredBlock.y === block.y && hoveredBlock.z === block.z; if (isHovered) { tempColor.set('#ffffff'); meshRef.current.setColorAt(i, tempColor); } else { tempColor.set(texture || multiFace ? '#ffffff' : appearance.color); meshRef.current.setColorAt(i, tempColor); } }); meshRef.current.instanceColor.needsUpdate = true; }, [hoveredBlock, blockList, texture, multiFace, appearance, tempColor]); const handlePointerMove = useCallback((e) => { e.stopPropagation(); const instanceId = e.instanceId; if (instanceId !== undefined && instanceId < blockList.length) { setHoveredBlock(blockList[instanceId]); } }, [blockList, setHoveredBlock]); const handlePointerOut = useCallback(() => { setHoveredBlock(null); }, [setHoveredBlock]); const handleClick = useCallback((e) => { e.stopPropagation(); const instanceId = e.instanceId; if (instanceId !== undefined && instanceId < blockList.length) { const block = blockList[instanceId]; if (interactionMode === INTERACTION_MODE.MOVE && selectedTurtleId && onBlockClick) { // In MOVE mode, click block surface to send turtle to adjacent position const normal = e.face?.normal; const target = { x: block.x + (normal?.x || 0), y: block.y + (normal?.y || 0), z: block.z + (normal?.z || 0) }; onBlockClick({ type: 'move', target, block }); } else if (interactionMode === INTERACTION_MODE.BUILD && onBlockClick) { const normal = e.face?.normal; const target = { x: block.x + (normal?.x || 0), y: block.y + (normal?.y || 0), z: block.z + (normal?.z || 0) }; onBlockClick({ type: 'build', target, block }); } else if (interactionMode === INTERACTION_MODE.SELECT && onBlockClick) { onBlockClick({ type: 'select', block }); } } }, [blockList, interactionMode, selectedTurtleId, onBlockClick]); // For multi-face blocks, we can't easily use InstancedMesh with different material per face, // so we fall back to individual meshes for those (they're fewer — grass, logs, etc.) if (multiFace) { return ( {blockList.map((block) => { const blockKey = `${block.x},${block.y},${block.z}`; const isHovered = hoveredBlock && hoveredBlock.x === block.x && hoveredBlock.y === block.y && hoveredBlock.z === block.z; return ( { e.stopPropagation(); setHoveredBlock(block); }} onPointerOut={() => setHoveredBlock(null)} onClick={(e) => { e.stopPropagation(); if (interactionMode === INTERACTION_MODE.MOVE && selectedTurtleId && onBlockClick) { const normal = e.face?.normal; onBlockClick({ type: 'move', target: { x: block.x + (normal?.x || 0), y: block.y + (normal?.y || 0), z: block.z + (normal?.z || 0) }, block }); } else if (interactionMode === INTERACTION_MODE.BUILD && onBlockClick) { const normal = e.face?.normal; onBlockClick({ type: 'build', target: { x: block.x + (normal?.x || 0), y: block.y + (normal?.y || 0), z: block.z + (normal?.z || 0) }, block }); } else if (interactionMode === INTERACTION_MODE.SELECT && onBlockClick) { onBlockClick({ type: 'select', block }); } }} > {multiFace.map((faceTexture, i) => ( ))} ); })} ); } return ( ); } // 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 ( ); } // Home marker component function HomeMarker({ position }) { if (!position) return null; return ( HOME ); } // 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 ( setHovered(true)} onPointerOut={() => setHovered(false)} ref={meshRef} > {/* Wireframe box */} {/* Semi-transparent fill */} {/* Label */} {area.areaName || `Area ${area.areaID}`} {/* Dimensions label */} {width}×{height}×{depth} {/* Status indicator */} {area.status === 'mining' && ( )} ); } // 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 ( {/* Player head (Steve-like) */} {/* Body */} {/* Glow effect */} {/* Label */} Player {player.playerID} ); } // Move target preview (ghost block showing where turtle will go) function MoveTargetPreview({ position }) { const meshRef = useRef(); useFrame((state) => { if (meshRef.current) { meshRef.current.position.y = position.y + Math.sin(state.clock.elapsedTime * 3) * 0.1; meshRef.current.material.opacity = 0.3 + Math.sin(state.clock.elapsedTime * 2) * 0.15; } }); return ( MOVE HERE ); } // Build preview (ghost block showing where block will be placed) function BuildPreview({ position }) { const meshRef = useRef(); useFrame((state) => { if (meshRef.current) { meshRef.current.material.opacity = 0.3 + Math.sin(state.clock.elapsedTime * 2) * 0.15; } }); return ( ); } // Area selection wireframe for SELECT mode function SelectionArea({ start, end }) { if (!start || !end) return null; const minX = Math.min(start.x, end.x); const minY = Math.min(start.y, end.y); const minZ = Math.min(start.z, end.z); const maxX = Math.max(start.x, end.x); const maxY = Math.max(start.y, end.y); const maxZ = Math.max(start.z, end.z); const width = maxX - minX + 1; const height = maxY - minY + 1; const depth = maxZ - minZ + 1; const centerX = (minX + maxX) / 2; const centerY = (minY + maxY) / 2; const centerZ = (minZ + maxZ) / 2; return ( {`${width}×${height}×${depth} Selection`} ); } // Main scene component function Scene({ interactionMode, onInteraction }) { 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 ( <> {/* Grid */} {/* Render discovered blocks */} {/* Mining areas */} {miningAreas.map((area) => { const turtle = turtles.find(t => t.turtleID === area.turtleID); return ( setSelectedAreaId(selectedAreaId === area.areaID ? null : area.areaID)} /> ); })} {/* Home marker */} {homePosition && } {/* Turtles and paths */} {turtles.map((turtle) => ( selectTurtle(turtle.turtleID)} /> ))} {/* Players */} {players.map((player) => ( ))} {/* Camera controls */} ); } // Main Map3D component export default function Map3D() { return (
); }