feat: Enhance turtle model and visualization with block surroundings and world block management
This commit is contained in:
@@ -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 (
|
||||
<group position={[position.x, position.y, position.z]} onClick={onClick}>
|
||||
{/* Turtle body */}
|
||||
<mesh ref={meshRef}>
|
||||
<boxGeometry args={[0.8, 0.6, 0.8]} />
|
||||
<meshStandardMaterial
|
||||
color={color}
|
||||
emissive={color}
|
||||
emissiveIntensity={isSelected ? 0.5 : 0.2}
|
||||
/>
|
||||
</mesh>
|
||||
<group ref={groupRef} position={[position.x, position.y, position.z]} rotation={rotation} onClick={onClick}>
|
||||
<group>
|
||||
{/* Shell (main body) */}
|
||||
<mesh position={[0, 0.2, 0]}>
|
||||
<boxGeometry args={[0.8, 0.5, 0.9]} />
|
||||
<meshStandardMaterial
|
||||
color={color}
|
||||
emissive={color}
|
||||
emissiveIntensity={isSelected ? 0.4 : 0.2}
|
||||
roughness={0.4}
|
||||
/>
|
||||
</mesh>
|
||||
|
||||
{/* Head */}
|
||||
<mesh position={[0, 0.15, 0.55]}>
|
||||
<boxGeometry args={[0.5, 0.35, 0.3]} />
|
||||
<meshStandardMaterial color={color} roughness={0.3} />
|
||||
</mesh>
|
||||
|
||||
{/* Eyes */}
|
||||
<mesh position={[-0.12, 0.2, 0.7]}>
|
||||
<boxGeometry args={[0.1, 0.1, 0.05]} />
|
||||
<meshStandardMaterial color="#ffffff" emissive="#ffff00" emissiveIntensity={0.5} />
|
||||
</mesh>
|
||||
<mesh position={[0.12, 0.2, 0.7]}>
|
||||
<boxGeometry args={[0.1, 0.1, 0.05]} />
|
||||
<meshStandardMaterial color="#ffffff" emissive="#ffff00" emissiveIntensity={0.5} />
|
||||
</mesh>
|
||||
|
||||
{/* Legs */}
|
||||
<mesh position={[-0.3, -0.15, 0.25]}>
|
||||
<boxGeometry args={[0.2, 0.2, 0.2]} />
|
||||
<meshStandardMaterial color="#666" />
|
||||
</mesh>
|
||||
<mesh position={[0.3, -0.15, 0.25]}>
|
||||
<boxGeometry args={[0.2, 0.2, 0.2]} />
|
||||
<meshStandardMaterial color="#666" />
|
||||
</mesh>
|
||||
<mesh position={[-0.3, -0.15, -0.25]}>
|
||||
<boxGeometry args={[0.2, 0.2, 0.2]} />
|
||||
<meshStandardMaterial color="#666" />
|
||||
</mesh>
|
||||
<mesh position={[0.3, -0.15, -0.25]}>
|
||||
<boxGeometry args={[0.2, 0.2, 0.2]} />
|
||||
<meshStandardMaterial color="#666" />
|
||||
</mesh>
|
||||
|
||||
{/* Tail */}
|
||||
<mesh position={[0, 0.1, -0.55]}>
|
||||
<boxGeometry args={[0.15, 0.15, 0.2]} />
|
||||
<meshStandardMaterial color={color} />
|
||||
</mesh>
|
||||
</group>
|
||||
|
||||
{/* Selection indicator */}
|
||||
{isSelected && (
|
||||
<>
|
||||
<mesh position={[0, 1, 0]}>
|
||||
<mesh position={[0, 1.2, 0]}>
|
||||
<coneGeometry args={[0.3, 0.5, 4]} />
|
||||
<meshStandardMaterial color="#ffffff" emissive="#ffffff" emissiveIntensity={0.8} />
|
||||
</mesh>
|
||||
|
||||
{/* Selection ring */}
|
||||
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, -0.4, 0]}>
|
||||
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, -0.5, 0]}>
|
||||
<ringGeometry args={[1, 1.2, 32]} />
|
||||
<meshBasicMaterial color="#ffffff" side={THREE.DoubleSide} transparent opacity={0.5} />
|
||||
</mesh>
|
||||
@@ -58,7 +104,7 @@ function TurtleMarker({ turtle, isSelected, onClick }) {
|
||||
anchorX="center"
|
||||
anchorY="middle"
|
||||
>
|
||||
T-{turtle.turtleID}
|
||||
🐢 {turtle.turtleID}
|
||||
</Text>
|
||||
</group>
|
||||
);
|
||||
@@ -75,6 +121,99 @@ function PathTrail({ turtle }) {
|
||||
new THREE.Vector3(homePosition.x, homePosition.y, homePosition.z)
|
||||
];
|
||||
|
||||
return (
|
||||
<Line
|
||||
points={points}
|
||||
color="#60a5fa"
|
||||
lineWidth={2}
|
||||
transparent
|
||||
opacity={0.3}
|
||||
dashed
|
||||
dashScale={2}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// 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 (
|
||||
<group>
|
||||
{Array.from(meshes.entries()).map(([color, blockList]) => (
|
||||
<group key={color}>
|
||||
{blockList.map((block, idx) => (
|
||||
<mesh
|
||||
key={`${block.x},${block.y},${block.z}`}
|
||||
position={[block.x, block.y, block.z]}
|
||||
>
|
||||
<boxGeometry args={[0.9, 0.9, 0.9]} />
|
||||
<meshStandardMaterial
|
||||
color={color}
|
||||
transparent
|
||||
opacity={0.6}
|
||||
roughness={0.8}
|
||||
/>
|
||||
</mesh>
|
||||
))}
|
||||
</group>
|
||||
))}
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
// 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 (
|
||||
<Line
|
||||
points={points}
|
||||
@@ -116,6 +255,7 @@ function Scene() {
|
||||
const turtles = useTurtleStore((state) => 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 (
|
||||
<>
|
||||
<ambientLight intensity={0.5} />
|
||||
<pointLight position={[10, 10, 10]} intensity={1} />
|
||||
<pointLight position={[-10, -10, -10]} intensity={0.5} />
|
||||
<ambientLight intensity={0.6} />
|
||||
<pointLight position={[10, 10, 10]} intensity={1.2} />
|
||||
<pointLight position={[-10, -10, -10]} intensity={0.6} />
|
||||
<pointLight position={[0, 20, 0]} intensity={0.8} color="#ffffff" />
|
||||
|
||||
{/* Grid */}
|
||||
<Grid
|
||||
@@ -161,6 +302,9 @@ function Scene() {
|
||||
infiniteGrid
|
||||
/>
|
||||
|
||||
{/* Render discovered blocks */}
|
||||
<WorldBlocks blocks={worldBlocks} />
|
||||
|
||||
{/* Home marker */}
|
||||
{homePosition && <HomeMarker position={homePosition} />}
|
||||
|
||||
@@ -168,7 +312,7 @@ function Scene() {
|
||||
{turtles.map((turtle) => (
|
||||
<React.Fragment key={turtle.turtleID}>
|
||||
<PathTrail turtle={turtle} />
|
||||
<TurtleMarker
|
||||
<TurtleModel
|
||||
turtle={turtle}
|
||||
isSelected={selectedTurtleId === turtle.turtleID}
|
||||
onClick={() => selectTurtle(turtle.turtleID)}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
25
turtle.lua
25
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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user