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 (
);
}