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