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 ( {/* Shell (main body) */} {/* Head */} {/* Eyes */} {/* Legs */} {/* Tail */} {/* Selection indicator */} {isSelected && ( <> {/* 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 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 Loading textures...; } return ( {blockGroups.map(([blockName, blockList]) => { const appearance = getBlockAppearance(blockName); const texture = textures[blockName]; return ( {blockList.map((block) => { const blockKey = `${block.x},${block.y},${block.z}`; const isHovered = hoveredBlock === blockKey; return ( { e.stopPropagation(); setHoveredBlock(blockKey); }} onPointerOut={() => setHoveredBlock(null)} > {/* Block label on hover */} {isHovered && ( {block.name.replace('minecraft:', '').replace(/_/g, ' ').toUpperCase()} )} ); })} ); })} ); } // 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} ); } // 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 ( <> {/* 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 (
); }