diff --git a/client/src/components/Map3D.jsx b/client/src/components/Map3D.jsx index d674234..44640b2 100644 --- a/client/src/components/Map3D.jsx +++ b/client/src/components/Map3D.jsx @@ -1,49 +1,95 @@ -import React, { useRef, useMemo } from 'react'; +import React, { useRef, useMemo, useEffect } from 'react'; import { Canvas, useFrame } from '@react-three/fiber'; import { OrbitControls, Grid, Text, Line } from '@react-three/drei'; import * as THREE from 'three'; import { useTurtleStore } from '../store/turtleStore'; -// Turtle marker component -function TurtleMarker({ turtle, isSelected, onClick }) { - const meshRef = useRef(); - const { position, mode } = turtle; +// Minecraft-style turtle model +function TurtleModel({ turtle, isSelected, onClick }) { + const groupRef = useRef(); + const { position, facing, mode } = turtle; useFrame((state) => { - if (meshRef.current && isSelected) { - meshRef.current.rotation.y += 0.02; + 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 rotation = facing !== undefined ? [0, (facing * Math.PI / 2), 0] : [0, 0, 0]; + const color = mode === 'mining' ? '#4ade80' : mode === 'exploring' ? '#60a5fa' : mode === 'returning' ? '#f59e0b' : '#9ca3af'; return ( - - {/* Turtle body */} - - - - + + + {/* Shell (main body) */} + + + + + + {/* Head */} + + + + + + {/* Eyes */} + + + + + + + + + + {/* Legs */} + + + + + + + + + + + + + + + + + + {/* Tail */} + + + + + {/* Selection indicator */} {isSelected && ( <> - + {/* Selection ring */} - + @@ -58,7 +104,7 @@ function TurtleMarker({ turtle, isSelected, onClick }) { anchorX="center" anchorY="middle" > - T-{turtle.turtleID} + 🐢 {turtle.turtleID} ); @@ -75,6 +121,99 @@ function PathTrail({ turtle }) { new THREE.Vector3(homePosition.x, homePosition.y, homePosition.z) ]; + return ( + + ); +} + +// Get color for block type +function getBlockColor(blockName) { + if (!blockName) return '#888'; + + const name = blockName.toLowerCase(); + + // Ores + if (name.includes('diamond')) return '#00ffff'; + if (name.includes('emerald')) return '#00ff00'; + if (name.includes('gold')) return '#ffd700'; + if (name.includes('iron')) return '#d4d4d4'; + if (name.includes('coal')) return '#1a1a1a'; + if (name.includes('redstone')) return '#ff0000'; + if (name.includes('lapis')) return '#0000ff'; + if (name.includes('copper')) return '#ff8c00'; + + // Common blocks + if (name.includes('stone')) return '#7f7f7f'; + if (name.includes('dirt')) return '#8b4513'; + if (name.includes('grass')) return '#228b22'; + if (name.includes('sand')) return '#f4a460'; + if (name.includes('gravel')) return '#808080'; + if (name.includes('bedrock')) return '#222'; + if (name.includes('obsidian')) return '#1a0033'; + if (name.includes('netherrack')) return '#8b0000'; + + return '#666666'; // Default +} + +// WorldBlocks component to render discovered blocks +function WorldBlocks({ blocks }) { + const meshes = useMemo(() => { + const instances = new Map(); + + blocks.forEach(block => { + const color = getBlockColor(block.name); + if (!instances.has(color)) { + instances.set(color, []); + } + instances.get(color).push(block); + }); + + return instances; + }, [blocks]); + + return ( + + {Array.from(meshes.entries()).map(([color, blockList]) => ( + + {blockList.map((block, idx) => ( + + + + + ))} + + ))} + + ); +} + +// 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 ( state.getTurtleArray()); const selectedTurtleId = useTurtleStore((state) => state.selectedTurtleId); const selectTurtle = useTurtleStore((state) => state.selectTurtle); + const worldBlocks = useTurtleStore((state) => state.worldBlocks || []); // Calculate center point for camera focus const centerPoint = useMemo(() => { @@ -142,9 +282,10 @@ function Scene() { return ( <> - - - + + + + {/* Grid */} + {/* Render discovered blocks */} + + {/* Home marker */} {homePosition && } @@ -168,7 +312,7 @@ function Scene() { {turtles.map((turtle) => ( - selectTurtle(turtle.turtleID)} diff --git a/client/src/store/turtleStore.js b/client/src/store/turtleStore.js index b3332c8..88b6fc4 100644 --- a/client/src/store/turtleStore.js +++ b/client/src/store/turtleStore.js @@ -10,6 +10,7 @@ console.log('📡 API URL:', API_URL); export const useTurtleStore = create((set, get) => ({ // State turtles: {}, + worldBlocks: [], selectedTurtleId: null, connected: false, ws: null, @@ -32,7 +33,10 @@ export const useTurtleStore = create((set, get) => ({ data.turtles.forEach(turtle => { turtlesMap[turtle.turtleID] = turtle; }); - set({ turtles: turtlesMap }); + set({ + turtles: turtlesMap, + worldBlocks: data.blocks || [] + }); } else if (data.type === 'turtle_update') { set(state => ({ turtles: { @@ -40,6 +44,11 @@ export const useTurtleStore = create((set, get) => ({ [data.turtle.turtleID]: data.turtle } })); + + // Update world blocks from turtle surroundings + if (data.turtle.surroundings && data.turtle.position && data.turtle.facing !== undefined) { + get().updateBlocksFromSurroundings(data.turtle); + } } else if (data.type === 'turtle_disconnected') { set(state => { const newTurtles = { ...state.turtles }; @@ -71,6 +80,52 @@ export const useTurtleStore = create((set, get) => ({ selectTurtle: (turtleId) => { set({ selectedTurtleId: turtleId }); }, + + updateBlocksFromSurroundings: (turtle) => { + const { surroundings, position, facing } = turtle; + if (!surroundings || !position || facing === undefined) return; + + const newBlocks = []; + const blockMap = new Map(get().worldBlocks.map(b => [`${b.x},${b.y},${b.z}`, b])); + + // Calculate block positions based on turtle position and facing + const directions = { + forward: { x: 0, y: 0, z: 0 }, + up: { x: 0, y: 1, z: 0 }, + down: { x: 0, y: -1, z: 0 } + }; + + // Facing: 0=North(-Z), 1=East(+X), 2=South(+Z), 3=West(-X) + if (surroundings.forward) { + if (facing === 0) directions.forward = { x: 0, y: 0, z: -1 }; + else if (facing === 1) directions.forward = { x: 1, y: 0, z: 0 }; + else if (facing === 2) directions.forward = { x: 0, y: 0, z: 1 }; + else if (facing === 3) directions.forward = { x: -1, y: 0, z: 0 }; + } + + Object.entries(surroundings).forEach(([direction, blockData]) => { + const offset = directions[direction]; + if (!offset) return; + + const blockPos = { + x: position.x + offset.x, + y: position.y + offset.y, + z: position.z + offset.z + }; + + const key = `${blockPos.x},${blockPos.y},${blockPos.z}`; + if (!blockMap.has(key)) { + blockMap.set(key, { + ...blockPos, + ...blockData, + discoveredBy: turtle.turtleID, + timestamp: Date.now() + }); + } + }); + + set({ worldBlocks: Array.from(blockMap.values()) }); + }, sendCommand: async (turtleId, command, param = null) => { const { ws } = get(); diff --git a/server/server.js b/server/server.js index 17e41d4..d6ea768 100644 --- a/server/server.js +++ b/server/server.js @@ -13,6 +13,38 @@ app.use(express.json()); // Store connected web clients and turtle data const webClients = new Set(); const turtleData = new Map(); // turtleID -> turtle state +const worldBlocks = new Map(); // "x,y,z" -> {name, metadata, discoveredBy, timestamp} + +// Helper to store discovered blocks +function storeBlock(x, y, z, blockData, turtleID) { + const key = `${x},${y},${z}`; + worldBlocks.set(key, { + ...blockData, + discoveredBy: turtleID, + timestamp: Date.now() + }); +} + +// Helper to calculate block position based on turtle position and facing +function getBlockPosition(turtlePos, facing, direction) { + if (!turtlePos) return null; + + const pos = { ...turtlePos }; + + if (direction === 'up') { + pos.y += 1; + } else if (direction === 'down') { + pos.y -= 1; + } else if (direction === 'forward') { + // Calculate based on facing direction + if (facing === 0) pos.z -= 1; // North + else if (facing === 1) pos.x += 1; // East + else if (facing === 2) pos.z += 1; // South + else if (facing === 3) pos.x -= 1; // West + } + + return pos; +} // Create HTTP server const server = createServer(app); @@ -29,10 +61,17 @@ wss.on('connection', (ws) => { console.log('🌐 New web client connected'); webClients.add(ws); - // Send current turtle data to new client + // Send current turtle data and world blocks to new client + const blocks = []; + for (const [key, blockData] of worldBlocks.entries()) { + const [x, y, z] = key.split(',').map(Number); + blocks.push({ x, y, z, ...blockData }); + } + ws.send(JSON.stringify({ type: 'initial_state', - turtles: Array.from(turtleData.values()) + turtles: Array.from(turtleData.values()), + blocks: blocks })); ws.on('message', (message) => { @@ -101,6 +140,16 @@ app.post('/api/turtle/update', (req, res) => { ...turtleUpdate, lastUpdate: Date.now() }); + + // Store discovered blocks in world map + if (turtleUpdate.surroundings && turtleUpdate.position && turtleUpdate.facing !== undefined) { + for (const [direction, blockData] of Object.entries(turtleUpdate.surroundings)) { + const blockPos = getBlockPosition(turtleUpdate.position, turtleUpdate.facing, direction); + if (blockPos) { + storeBlock(blockPos.x, blockPos.y, blockPos.z, blockData, turtleID); + } + } + } // Broadcast to web clients broadcastToClients({ @@ -149,6 +198,19 @@ app.get('/api/turtles', (req, res) => { }); }); +// Get world blocks for map visualization +app.get('/api/world/blocks', (req, res) => { + const blocks = []; + for (const [key, blockData] of worldBlocks.entries()) { + const [x, y, z] = key.split(',').map(Number); + blocks.push({ + x, y, z, + ...blockData + }); + } + res.json({ blocks }); +}); + // Send command to turtle app.post('/api/turtle/:id/command', (req, res) => { try { diff --git a/turtle.lua b/turtle.lua index 57d1ec5..68f7b4e 100644 --- a/turtle.lua +++ b/turtle.lua @@ -358,6 +358,28 @@ function broadcastStatus() -- Don't update position on every broadcast to avoid GPS delays -- Position will be updated by movement functions + -- Scan surrounding blocks for map visualization + local surroundings = {} + local hasBlock, data + + -- Check forward + hasBlock, data = turtle.inspect() + if hasBlock and data then + surroundings.forward = {name = data.name, metadata = data.metadata or 0} + end + + -- Check up + hasBlock, data = turtle.inspectUp() + if hasBlock and data then + surroundings.up = {name = data.name, metadata = data.metadata or 0} + end + + -- Check down + hasBlock, data = turtle.inspectDown() + if hasBlock and data then + surroundings.down = {name = data.name, metadata = data.metadata or 0} + end + modem.transmit(STATUS_CHANNEL, CHANNEL_RECEIVE, { type = "status", turtleID = os.getComputerID(), @@ -367,7 +389,8 @@ function broadcastStatus() fuel = state.fuel, inventoryCount = #state.inventory, inventory = state.inventory, - facing = facing + facing = facing, + surroundings = surroundings }) end