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)}
|
||||
|
||||
Reference in New Issue
Block a user