511 lines
19 KiB
JavaScript
511 lines
19 KiB
JavaScript
import React, { useState, useCallback } from 'react';
|
||
import { useTurtleStore } from '../store/turtleStore';
|
||
import './ControlPanel.css';
|
||
|
||
function TurtleCard({ turtle, isSelected, onSelect }) {
|
||
const activeState = turtle.state || turtle.mode || 'idle';
|
||
const fuel = turtle.fuel === 'unlimited' ? '∞' : (turtle.fuel || '?');
|
||
const inventoryCount = turtle.inventory?.length || 0;
|
||
const displayName = turtle.label || `Turtle ${turtle.turtleID}`;
|
||
|
||
const modeColors = {
|
||
mining: '#4ade80',
|
||
exploring: '#60a5fa',
|
||
returning: '#f59e0b',
|
||
goHome: '#f59e0b',
|
||
idle: '#9ca3af',
|
||
manual: '#a78bfa',
|
||
refueling: '#ef4444',
|
||
farming: '#22c55e',
|
||
dumpInventory: '#a855f7',
|
||
dumping: '#a855f7',
|
||
moving: '#06b6d4',
|
||
scan: '#8b5cf6',
|
||
extraction: '#f97316',
|
||
building: '#14b8a6',
|
||
autocraft: '#ec4899',
|
||
unknown: '#6b7280'
|
||
};
|
||
|
||
return (
|
||
<div
|
||
className={`turtle-card ${isSelected ? 'selected' : ''}`}
|
||
onClick={onSelect}
|
||
style={{ borderColor: modeColors[activeState] || modeColors.unknown }}
|
||
>
|
||
<div className="turtle-header">
|
||
<h3>{displayName}</h3>
|
||
<span className="mode-badge" style={{ background: modeColors[activeState] || modeColors.unknown }}>
|
||
{activeState}
|
||
</span>
|
||
</div>
|
||
|
||
<div className="turtle-stats">
|
||
<div className="stat">
|
||
<span className="label">Position:</span>
|
||
<span className="value">
|
||
{turtle.position
|
||
? `${turtle.position.x}, ${turtle.position.y}, ${turtle.position.z}`
|
||
: 'No GPS'
|
||
}
|
||
</span>
|
||
</div>
|
||
|
||
<div className="stat">
|
||
<span className="label">Fuel:</span>
|
||
<span className="value">{fuel}</span>
|
||
</div>
|
||
|
||
<div className="stat">
|
||
<span className="label">Inventory:</span>
|
||
<span className="value">{inventoryCount} items</span>
|
||
</div>
|
||
|
||
{turtle.position && turtle.homePosition && (
|
||
<div className="stat">
|
||
<span className="label">Home Distance:</span>
|
||
<span className="value">
|
||
{Math.abs(turtle.position.x - turtle.homePosition.x) +
|
||
Math.abs(turtle.position.y - turtle.homePosition.y) +
|
||
Math.abs(turtle.position.z - turtle.homePosition.z)} blocks
|
||
</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function TurtleDetails({ turtle }) {
|
||
const sendCommand = useTurtleStore((state) => state.sendCommand);
|
||
const setTurtleState = useTurtleStore((state) => state.setTurtleState);
|
||
|
||
if (!turtle) {
|
||
return (
|
||
<div className="turtle-details empty">
|
||
<p>Select a turtle to view details and control it</p>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
const handleCommand = (command, param = null) => {
|
||
sendCommand(turtle.turtleID, command, param);
|
||
};
|
||
|
||
const handleStateChange = (stateName, data = {}) => {
|
||
setTurtleState(turtle.turtleID, stateName, data);
|
||
};
|
||
|
||
const activeState = turtle.state || turtle.mode || 'idle';
|
||
|
||
return (
|
||
<div className="turtle-details">
|
||
<h2>Turtle {turtle.turtleID} Control</h2>
|
||
|
||
<div className="detail-section">
|
||
<h3>Status</h3>
|
||
<div className="status-grid">
|
||
<div className="status-item">
|
||
<span className="label">State:</span>
|
||
<span className="value">{activeState}</span>
|
||
</div>
|
||
{turtle.stateDescription && (
|
||
<div className="status-item">
|
||
<span className="label">Activity:</span>
|
||
<span className="value">{turtle.stateDescription}</span>
|
||
</div>
|
||
)}
|
||
<div className="status-item">
|
||
<span className="label">Fuel:</span>
|
||
<span className="value">
|
||
{turtle.fuel === 'unlimited' ? 'Unlimited' : turtle.fuel}
|
||
</span>
|
||
</div>
|
||
<div className="status-item">
|
||
<span className="label">Position:</span>
|
||
<span className="value">
|
||
{turtle.position
|
||
? `X: ${turtle.position.x}, Y: ${turtle.position.y}, Z: ${turtle.position.z}`
|
||
: 'No GPS'
|
||
}
|
||
</span>
|
||
</div>
|
||
<div className="status-item">
|
||
<span className="label">Home:</span>
|
||
<span className="value">
|
||
{turtle.homePosition
|
||
? `X: ${turtle.homePosition.x}, Y: ${turtle.homePosition.y}, Z: ${turtle.homePosition.z}`
|
||
: 'Not set'
|
||
}
|
||
</span>
|
||
</div>
|
||
{turtle.error && (
|
||
<div className="status-item" style={{ color: '#ef4444' }}>
|
||
<span className="label">Error:</span>
|
||
<span className="value">{turtle.error}</span>
|
||
</div>
|
||
)}
|
||
{turtle.warning && (
|
||
<div className="status-item" style={{ color: '#f59e0b' }}>
|
||
<span className="label">Warning:</span>
|
||
<span className="value">{turtle.warning}</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Peripherals section */}
|
||
{turtle.peripherals && Object.keys(turtle.peripherals).length > 0 && (
|
||
<div className="detail-section">
|
||
<h3>Peripherals</h3>
|
||
<div className="status-grid">
|
||
{Object.entries(turtle.peripherals).map(([side, type]) => (
|
||
<div key={side} className="status-item">
|
||
<span className="label" style={{ textTransform: 'capitalize' }}>{side}:</span>
|
||
<span className="value" style={{ color: '#a78bfa' }}>{type}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<div className="detail-section">
|
||
<h3>State Machine</h3>
|
||
<div className="command-grid">
|
||
<button
|
||
className={`command-btn ${activeState === 'idle' ? 'active' : ''}`}
|
||
onClick={() => { handleStateChange('idle'); handleCommand('stop'); }}
|
||
title="Stop and go idle"
|
||
style={activeState === 'idle' ? { outline: '2px solid #fff' } : {}}
|
||
>
|
||
⏸️ Idle
|
||
</button>
|
||
<button
|
||
className={`command-btn explore ${activeState === 'exploring' ? 'active' : ''}`}
|
||
onClick={() => { handleStateChange('exploring'); handleCommand('explore'); }}
|
||
title="Autonomous exploration"
|
||
style={activeState === 'exploring' ? { outline: '2px solid #fff' } : {}}
|
||
>
|
||
🔍 Explore
|
||
</button>
|
||
<button
|
||
className={`command-btn mine ${activeState === 'mining' ? 'active' : ''}`}
|
||
onClick={() => { handleStateChange('mining'); handleCommand('explore'); }}
|
||
title="Mining with ore priority"
|
||
style={activeState === 'mining' ? { outline: '2px solid #fff' } : {}}
|
||
>
|
||
⛏️ Mine
|
||
</button>
|
||
<button
|
||
className={`command-btn ${activeState === 'farming' ? 'active' : ''}`}
|
||
onClick={() => handleStateChange('farming')}
|
||
title="Automated farming"
|
||
style={activeState === 'farming' ? { outline: '2px solid #fff' } : {}}
|
||
>
|
||
🌾 Farm
|
||
</button>
|
||
<button
|
||
className={`command-btn return ${activeState === 'goHome' || activeState === 'returning' ? 'active' : ''}`}
|
||
onClick={() => { handleStateChange('goHome'); handleCommand('returnHome'); }}
|
||
title="Navigate home"
|
||
style={activeState === 'goHome' || activeState === 'returning' ? { outline: '2px solid #fff' } : {}}
|
||
>
|
||
🏠 Go Home
|
||
</button>
|
||
<button
|
||
className={`command-btn refuel ${activeState === 'refueling' ? 'active' : ''}`}
|
||
onClick={() => { handleStateChange('refueling'); handleCommand('refuel'); }}
|
||
title="Auto-refuel from inventory"
|
||
style={activeState === 'refueling' ? { outline: '2px solid #fff' } : {}}
|
||
>
|
||
⛽ Refuel
|
||
</button>
|
||
<button
|
||
className={`command-btn ${activeState === 'dumpInventory' || activeState === 'dumping' ? 'active' : ''}`}
|
||
onClick={() => handleStateChange('dumpInventory')}
|
||
title="Dump inventory into nearby container"
|
||
style={activeState === 'dumpInventory' || activeState === 'dumping' ? { outline: '2px solid #fff' } : {}}
|
||
>
|
||
📦 Dump
|
||
</button>
|
||
<button
|
||
className={`command-btn ${activeState === 'moving' ? 'active' : ''}`}
|
||
onClick={() => handleStateChange('moving')}
|
||
title="Navigate to target"
|
||
style={activeState === 'moving' ? { outline: '2px solid #fff' } : {}}
|
||
>
|
||
🧭 Move To
|
||
</button>
|
||
<button
|
||
className={`command-btn ${activeState === 'scan' ? 'active' : ''}`}
|
||
onClick={() => handleStateChange('scan')}
|
||
title="Scan surroundings with peripheral scanner"
|
||
style={activeState === 'scan' ? { outline: '2px solid #fff' } : {}}
|
||
>
|
||
📡 Scan
|
||
</button>
|
||
<button
|
||
className={`command-btn ${activeState === 'extraction' ? 'active' : ''}`}
|
||
onClick={() => {
|
||
const area = prompt('Enter extraction area (startX,startY,startZ,endX,endY,endZ):');
|
||
if (area) {
|
||
const [sx,sy,sz,ex,ey,ez] = area.split(',').map(Number);
|
||
if (!isNaN(sx) && !isNaN(sy) && !isNaN(sz) && !isNaN(ex) && !isNaN(ey) && !isNaN(ez)) {
|
||
const points = [];
|
||
for (let x = sx; x <= ex; x++) for (let y = sy; y <= ey; y++) for (let z = sz; z <= ez; z++) {
|
||
points.push({x, y, z});
|
||
}
|
||
handleStateChange('extraction', { area: points });
|
||
}
|
||
}
|
||
}}
|
||
title="Smart ore extraction in an area"
|
||
style={activeState === 'extraction' ? { outline: '2px solid #fff' } : {}}
|
||
>
|
||
💎 Extract
|
||
</button>
|
||
<button
|
||
className={`command-btn ${activeState === 'building' ? 'active' : ''}`}
|
||
onClick={() => {
|
||
const input = prompt('Enter blueprint JSON (array of {x,y,z,name} blocks):');
|
||
if (input) {
|
||
try {
|
||
const blocks = JSON.parse(input);
|
||
handleStateChange('building', { blocks });
|
||
} catch (e) {
|
||
alert('Invalid JSON blueprint');
|
||
}
|
||
}
|
||
}}
|
||
title="Build from blueprint"
|
||
style={activeState === 'building' ? { outline: '2px solid #fff' } : {}}
|
||
>
|
||
🏗️ Build
|
||
</button>
|
||
<button
|
||
className={`command-btn ${activeState === 'autocraft' ? 'active' : ''}`}
|
||
onClick={() => {
|
||
const input = prompt('Enter recipe JSON (array of 16 slot items, null for empty):');
|
||
if (input) {
|
||
try {
|
||
const recipe = JSON.parse(input);
|
||
handleStateChange('autocraft', { recipe });
|
||
} catch (e) {
|
||
alert('Invalid JSON recipe');
|
||
}
|
||
}
|
||
}}
|
||
title="Automated crafting with workbench"
|
||
style={activeState === 'autocraft' ? { outline: '2px solid #fff' } : {}}
|
||
>
|
||
🔨 Autocraft
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="detail-section">
|
||
<h3>Legacy Commands</h3>
|
||
<div className="command-grid">
|
||
<button
|
||
className="command-btn explore"
|
||
onClick={() => handleCommand('explore')}
|
||
title="Start autonomous exploration and mining"
|
||
>
|
||
🔍 Explore
|
||
</button>
|
||
<button
|
||
className="command-btn mine"
|
||
onClick={() => handleCommand('mine')}
|
||
title="Start mining mode"
|
||
>
|
||
⛏️ Mine
|
||
</button>
|
||
<button
|
||
className="command-btn return"
|
||
onClick={() => handleCommand('returnHome')}
|
||
title="Navigate back to home position"
|
||
>
|
||
🏠 Return Home
|
||
</button>
|
||
<button
|
||
className="command-btn stop"
|
||
onClick={() => handleCommand('stop')}
|
||
title="Stop all autonomous actions"
|
||
>
|
||
⏹️ Stop
|
||
</button>
|
||
<button
|
||
className="command-btn manual"
|
||
onClick={() => handleCommand('manual')}
|
||
title="Switch to manual control mode"
|
||
>
|
||
🎮 Manual Mode
|
||
</button>
|
||
<button
|
||
className="command-btn setHome"
|
||
onClick={() => handleCommand('setHome')}
|
||
title="Set current position as home"
|
||
>
|
||
📍 Set Home
|
||
</button>
|
||
<button
|
||
className="command-btn refuel"
|
||
onClick={() => handleCommand('refuel')}
|
||
title="Consume fuel from inventory"
|
||
>
|
||
⛽ Refuel
|
||
</button>
|
||
<button
|
||
className="command-btn status"
|
||
onClick={() => handleCommand('status')}
|
||
title="Request status update"
|
||
>
|
||
📊 Status
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="detail-section">
|
||
<h3>Movement</h3>
|
||
<div className="movement-controls">
|
||
<div className="movement-row">
|
||
<button onClick={() => handleCommand('forward')} title="Move forward">↑</button>
|
||
</div>
|
||
<div className="movement-row">
|
||
<button onClick={() => handleCommand('turnLeft')} title="Turn left">←</button>
|
||
<button onClick={() => handleCommand('back')} title="Move backward">↓</button>
|
||
<button onClick={() => handleCommand('turnRight')} title="Turn right">→</button>
|
||
</div>
|
||
<div className="movement-row vertical">
|
||
<button onClick={() => handleCommand('up')} title="Move up">⬆ Up</button>
|
||
<button onClick={() => handleCommand('down')} title="Move down">⬇ Down</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="detail-section">
|
||
<h3>Actions</h3>
|
||
<div className="action-grid">
|
||
<button
|
||
className="action-btn dig"
|
||
onClick={() => handleCommand('dig')}
|
||
title="Dig block in front"
|
||
>
|
||
⛏️ Dig
|
||
</button>
|
||
<button
|
||
className="action-btn digup"
|
||
onClick={() => handleCommand('digUp')}
|
||
title="Dig block above"
|
||
>
|
||
⬆️ Dig Up
|
||
</button>
|
||
<button
|
||
className="action-btn digdown"
|
||
onClick={() => handleCommand('digDown')}
|
||
title="Dig block below"
|
||
>
|
||
⬇️ Dig Down
|
||
</button>
|
||
<button
|
||
className="action-btn place"
|
||
onClick={() => handleCommand('place')}
|
||
title="Place block from inventory"
|
||
>
|
||
🧱 Place
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{turtle.inventory && turtle.inventory.length > 0 && (
|
||
<div className="detail-section">
|
||
<h3>Inventory ({turtle.inventoryCount || turtle.inventory.length}/16)</h3>
|
||
<div className="inventory-grid">
|
||
{Array.from({ length: 16 }, (_, slotIndex) => {
|
||
const item = turtle.inventory[slotIndex];
|
||
return (
|
||
<div
|
||
key={slotIndex}
|
||
className={`inventory-slot ${item ? 'filled' : 'empty'}`}
|
||
title={item ? `${item.name.replace('minecraft:', '').replace(/_/g, ' ')} (${item.count})` : 'Empty'}
|
||
>
|
||
{item ? (
|
||
<>
|
||
<div className="slot-item">
|
||
<span className="item-icon">
|
||
{item.name.includes('diamond') ? '💎' :
|
||
item.name.includes('gold') ? '<27>' :
|
||
item.name.includes('iron') ? '⚪' :
|
||
item.name.includes('coal') ? '⚫' :
|
||
item.name.includes('emerald') ? '🟢' :
|
||
item.name.includes('redstone') ? '🔴' :
|
||
item.name.includes('lapis') ? '🔵' :
|
||
item.name.includes('stone') ? '🗿' :
|
||
item.name.includes('dirt') ? '🟤' :
|
||
item.name.includes('wood') || item.name.includes('log') ? '🪵' :
|
||
item.name.includes('cobble') ? '🪨' : '<27>📦'}
|
||
</span>
|
||
<span className="item-count">{item.count}</span>
|
||
</div>
|
||
<div className="slot-name">
|
||
{item.name.replace('minecraft:', '').replace(/_/g, ' ').split(' ').slice(0, 2).join(' ')}
|
||
</div>
|
||
</>
|
||
) : (
|
||
<span className="slot-number">{slotIndex + 1}</span>
|
||
)}
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export default function ControlPanel() {
|
||
const turtles = useTurtleStore((state) => state.getTurtleArray());
|
||
const selectedTurtleId = useTurtleStore((state) => state.selectedTurtleId);
|
||
const selectTurtle = useTurtleStore((state) => state.selectTurtle);
|
||
const selectedTurtle = useTurtleStore((state) => state.getSelectedTurtle());
|
||
const connected = useTurtleStore((state) => state.connected);
|
||
|
||
return (
|
||
<div className="control-panel">
|
||
<div className="panel-header">
|
||
<h1>🐢 Turtle Control Center</h1>
|
||
<div className={`connection-status ${connected ? 'connected' : 'disconnected'}`}>
|
||
<span className="status-dot"></span>
|
||
{connected ? 'Connected' : 'Disconnected'}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="panel-content">
|
||
<div className="turtle-list">
|
||
<h2>Turtles ({turtles.length})</h2>
|
||
{turtles.length === 0 ? (
|
||
<div className="empty-state">
|
||
<p>No turtles connected</p>
|
||
<p className="hint">Waiting for turtles to come online...</p>
|
||
</div>
|
||
) : (
|
||
<div className="turtle-cards">
|
||
{turtles.map((turtle) => (
|
||
<TurtleCard
|
||
key={turtle.turtleID}
|
||
turtle={turtle}
|
||
isSelected={selectedTurtleId === turtle.turtleID}
|
||
onSelect={() => selectTurtle(turtle.turtleID)}
|
||
/>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<TurtleDetails turtle={selectedTurtle} />
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|