feat: Enhance turtle model and visualization with block surroundings and world block management

This commit is contained in:
MayaTheShy
2026-02-16 01:35:52 -05:00
parent 836d734b1f
commit 5ec385c1f2
4 changed files with 312 additions and 28 deletions

View File

@@ -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)}

View File

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

View File

@@ -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 {

View File

@@ -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