Compare commits

...

5 Commits

5 changed files with 658 additions and 40 deletions

View File

@@ -564,49 +564,100 @@
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
}
/* Inventory List */
.inventory-list {
/* Inventory Grid */
.inventory-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0.5rem;
max-height: 400px;
overflow-y: auto;
padding: 0.5rem;
background: #0f172a;
border-radius: 0.5rem;
border: 1px solid #1e293b;
}
.inventory-slot {
aspect-ratio: 1;
display: flex;
flex-direction: column;
gap: 0.5rem;
max-height: 300px;
overflow-y: auto;
}
.inventory-item {
display: flex;
align-items: center;
gap: 0.625rem;
padding: 0.625rem;
background: #1f2937;
border: 1px solid #374151;
justify-content: center;
background: #1e293b;
border: 2px solid #334155;
border-radius: 0.375rem;
padding: 0.5rem;
transition: all 0.2s;
cursor: pointer;
position: relative;
overflow: hidden;
}
.inventory-item:hover {
.inventory-slot.empty {
background: #0f172a;
border-style: dashed;
border-color: #1e293b;
}
.inventory-slot.filled {
background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
border-color: #3b82f6;
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.1);
}
.inventory-slot.filled:hover {
border-color: #60a5fa;
background: #1e3a5f;
transform: translateX(4px);
background: linear-gradient(135deg, #1e3a5f 0%, #1e293b 100%);
box-shadow: 0 4px 16px rgba(59, 130, 246, 0.3);
transform: translateY(-2px);
}
.item-icon {
font-size: 1.25rem;
flex-shrink: 0;
.slot-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.25rem;
}
.item-name {
flex: 1;
color: #e5e7eb;
font-size: 0.8125rem;
text-transform: capitalize;
.slot-item .item-icon {
font-size: 1.5rem;
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.5));
}
.item-count {
color: #9ca3af;
font-weight: 600;
.slot-item .item-count {
position: absolute;
bottom: 0.25rem;
right: 0.25rem;
background: rgba(0, 0, 0, 0.8);
color: #fff;
font-size: 0.625rem;
font-weight: 700;
padding: 0.125rem 0.375rem;
border-radius: 0.25rem;
font-family: 'Courier New', monospace;
font-size: 0.8125rem;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5);
}
.slot-name {
position: absolute;
top: 0.25rem;
left: 0.25rem;
right: 0.25rem;
font-size: 0.5rem;
color: #94a3b8;
font-weight: 600;
text-transform: uppercase;
text-align: center;
line-height: 1.1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.slot-number {
font-size: 0.75rem;
color: #475569;
font-weight: 600;
}
/* Responsive Design */
@@ -618,10 +669,43 @@
.action-grid {
grid-template-columns: 1fr;
}
.inventory-grid {
grid-template-columns: repeat(4, 1fr);
}
}
@media (max-width: 768px) {
.turtle-grid {
grid-template-columns: 1fr;
}
.panel-content {
flex-direction: column;
}
.turtle-list {
border-bottom: 1px solid #374151;
border-right: none;
}
.inventory-grid {
grid-template-columns: repeat(4, 1fr);
}
}
@media (max-width: 480px) {
.inventory-grid {
grid-template-columns: repeat(4, 1fr);
gap: 0.375rem;
}
.inventory-slot {
padding: 0.375rem;
}
.slot-item .item-icon {
font-size: 1.25rem;
}
}

View File

@@ -235,16 +235,43 @@ function TurtleDetails({ turtle }) {
{turtle.inventory && turtle.inventory.length > 0 && (
<div className="detail-section">
<h3>Inventory ({turtle.inventoryCount || turtle.inventory.length}/16)</h3>
<div className="inventory-list">
{turtle.inventory.map((item, index) => (
<div key={index} className="inventory-item">
<span className="item-icon">📦</span>
<span className="item-name">
{item.name.replace('minecraft:', '').replace(/_/g, ' ')}
</span>
<span className="item-count">×{item.count}</span>
</div>
))}
<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>
)}

287
server/database.js Normal file
View File

@@ -0,0 +1,287 @@
import Database from 'better-sqlite3';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const db = new Database(path.join(__dirname, 'turtle_control.db'));
// Initialize database schema
export function initializeDatabase() {
// Turtle homes table
db.exec(`
CREATE TABLE IF NOT EXISTS turtle_homes (
turtle_id INTEGER PRIMARY KEY,
x INTEGER NOT NULL,
y INTEGER NOT NULL,
z INTEGER NOT NULL,
updated_at INTEGER NOT NULL
)
`);
// Turtle configuration table
db.exec(`
CREATE TABLE IF NOT EXISTS turtle_config (
turtle_id INTEGER PRIMARY KEY,
max_distance INTEGER DEFAULT 200,
facing INTEGER DEFAULT 0,
config_json TEXT,
updated_at INTEGER NOT NULL
)
`);
// World blocks table
db.exec(`
CREATE TABLE IF NOT EXISTS world_blocks (
x INTEGER NOT NULL,
y INTEGER NOT NULL,
z INTEGER NOT NULL,
block_name TEXT NOT NULL,
metadata INTEGER DEFAULT 0,
discovered_by INTEGER NOT NULL,
discovered_at INTEGER NOT NULL,
PRIMARY KEY (x, y, z)
)
`);
// Turtle paths table (for path recording)
db.exec(`
CREATE TABLE IF NOT EXISTS turtle_paths (
id INTEGER PRIMARY KEY AUTOINCREMENT,
turtle_id INTEGER NOT NULL,
path_name TEXT NOT NULL,
path_data TEXT NOT NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
)
`);
// Task queue table (for multi-turtle coordination)
db.exec(`
CREATE TABLE IF NOT EXISTS task_queue (
id INTEGER PRIMARY KEY AUTOINCREMENT,
task_type TEXT NOT NULL,
task_data TEXT NOT NULL,
assigned_turtle_id INTEGER,
priority INTEGER DEFAULT 0,
status TEXT DEFAULT 'pending',
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
)
`);
// Mining areas table (for area visualization)
db.exec(`
CREATE TABLE IF NOT EXISTS mining_areas (
id INTEGER PRIMARY KEY AUTOINCREMENT,
turtle_id INTEGER NOT NULL,
min_x INTEGER NOT NULL,
min_y INTEGER NOT NULL,
min_z INTEGER NOT NULL,
max_x INTEGER NOT NULL,
max_y INTEGER NOT NULL,
max_z INTEGER NOT NULL,
status TEXT DEFAULT 'active',
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
)
`);
// Create indexes for better performance
db.exec(`
CREATE INDEX IF NOT EXISTS idx_world_blocks_discovered
ON world_blocks(discovered_by);
`);
db.exec(`
CREATE INDEX IF NOT EXISTS idx_task_queue_status
ON task_queue(status, priority DESC);
`);
console.log('✅ Database initialized');
}
// Turtle Homes
export function saveTurtleHome(turtleId, position) {
const stmt = db.prepare(`
INSERT OR REPLACE INTO turtle_homes (turtle_id, x, y, z, updated_at)
VALUES (?, ?, ?, ?, ?)
`);
stmt.run(turtleId, position.x, position.y, position.z, Date.now());
}
export function getTurtleHome(turtleId) {
const stmt = db.prepare('SELECT x, y, z FROM turtle_homes WHERE turtle_id = ?');
return stmt.get(turtleId);
}
export function getAllTurtleHomes() {
const stmt = db.prepare('SELECT turtle_id, x, y, z FROM turtle_homes');
return stmt.all();
}
// Turtle Configuration
export function saveTurtleConfig(turtleId, config) {
const stmt = db.prepare(`
INSERT OR REPLACE INTO turtle_config (turtle_id, max_distance, facing, config_json, updated_at)
VALUES (?, ?, ?, ?, ?)
`);
stmt.run(
turtleId,
config.maxDistance || 200,
config.facing || 0,
JSON.stringify(config),
Date.now()
);
}
export function getTurtleConfig(turtleId) {
const stmt = db.prepare('SELECT * FROM turtle_config WHERE turtle_id = ?');
const row = stmt.get(turtleId);
if (row && row.config_json) {
return JSON.parse(row.config_json);
}
return null;
}
// World Blocks
export function saveWorldBlock(x, y, z, blockName, metadata, discoveredBy) {
const stmt = db.prepare(`
INSERT OR REPLACE INTO world_blocks (x, y, z, block_name, metadata, discovered_by, discovered_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
`);
stmt.run(x, y, z, blockName, metadata || 0, discoveredBy, Date.now());
}
export function getWorldBlocks(limit = 10000) {
const stmt = db.prepare('SELECT * FROM world_blocks LIMIT ?');
return stmt.all(limit);
}
export function getWorldBlocksInArea(minX, minY, minZ, maxX, maxY, maxZ) {
const stmt = db.prepare(`
SELECT * FROM world_blocks
WHERE x BETWEEN ? AND ?
AND y BETWEEN ? AND ?
AND z BETWEEN ? AND ?
`);
return stmt.all(minX, maxX, minY, maxY, minZ, maxZ);
}
export function clearOldBlocks(daysOld = 7) {
const cutoffTime = Date.now() - (daysOld * 24 * 60 * 60 * 1000);
const stmt = db.prepare('DELETE FROM world_blocks WHERE discovered_at < ?');
const result = stmt.run(cutoffTime);
return result.changes;
}
// Turtle Paths
export function savePath(turtleId, pathName, pathData) {
const stmt = db.prepare(`
INSERT INTO turtle_paths (turtle_id, path_name, path_data, created_at, updated_at)
VALUES (?, ?, ?, ?, ?)
`);
const now = Date.now();
stmt.run(turtleId, pathName, JSON.stringify(pathData), now, now);
}
export function getPaths(turtleId) {
const stmt = db.prepare('SELECT * FROM turtle_paths WHERE turtle_id = ? ORDER BY created_at DESC');
return stmt.all(turtleId).map(row => ({
...row,
path_data: JSON.parse(row.path_data)
}));
}
export function deletePath(pathId) {
const stmt = db.prepare('DELETE FROM turtle_paths WHERE id = ?');
return stmt.run(pathId);
}
// Task Queue
export function createTask(taskType, taskData, priority = 0) {
const stmt = db.prepare(`
INSERT INTO task_queue (task_type, task_data, priority, status, created_at, updated_at)
VALUES (?, ?, ?, 'pending', ?, ?)
`);
const now = Date.now();
const result = stmt.run(taskType, JSON.stringify(taskData), priority, now, now);
return result.lastInsertRowid;
}
export function getNextTask() {
const stmt = db.prepare(`
SELECT * FROM task_queue
WHERE status = 'pending'
ORDER BY priority DESC, created_at ASC
LIMIT 1
`);
const row = stmt.get();
if (row) {
return {
...row,
task_data: JSON.parse(row.task_data)
};
}
return null;
}
export function assignTask(taskId, turtleId) {
const stmt = db.prepare(`
UPDATE task_queue
SET assigned_turtle_id = ?, status = 'assigned', updated_at = ?
WHERE id = ?
`);
stmt.run(turtleId, Date.now(), taskId);
}
export function completeTask(taskId) {
const stmt = db.prepare(`
UPDATE task_queue
SET status = 'completed', updated_at = ?
WHERE id = ?
`);
stmt.run(Date.now(), taskId);
}
export function getAllTasks() {
const stmt = db.prepare('SELECT * FROM task_queue ORDER BY priority DESC, created_at DESC');
return stmt.all().map(row => ({
...row,
task_data: JSON.parse(row.task_data)
}));
}
// Mining Areas
export function saveMiningArea(turtleId, bounds) {
const stmt = db.prepare(`
INSERT INTO mining_areas (turtle_id, min_x, min_y, min_z, max_x, max_y, max_z, status, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, 'active', ?, ?)
`);
const now = Date.now();
stmt.run(
turtleId,
bounds.minX, bounds.minY, bounds.minZ,
bounds.maxX, bounds.maxY, bounds.maxZ,
now, now
);
}
export function getMiningAreas() {
const stmt = db.prepare('SELECT * FROM mining_areas WHERE status = \'active\'');
return stmt.all();
}
export function closeMiningArea(areaId) {
const stmt = db.prepare('UPDATE mining_areas SET status = \'closed\', updated_at = ? WHERE id = ?');
stmt.run(Date.now(), areaId);
}
// Cleanup function
export function closeDatabase() {
db.close();
}
// Export database instance for custom queries if needed
export { db };

View File

@@ -19,7 +19,8 @@
"dependencies": {
"express": "^4.18.2",
"ws": "^8.14.2",
"cors": "^2.8.5"
"cors": "^2.8.5",
"better-sqlite3": "^9.2.2"
},
"devDependencies": {
"nodemon": "^3.0.1"

View File

@@ -2,6 +2,7 @@ import express from 'express';
import { WebSocketServer } from 'ws';
import cors from 'cors';
import { createServer } from 'http';
import * as db from './database.js';
const app = express();
const PORT = 3001;
@@ -10,6 +11,16 @@ const WS_PORT = 3002;
app.use(cors());
app.use(express.json());
// Initialize database
db.initializeDatabase();
// Load persisted data from database
console.log('📂 Loading persisted data from database...');
const savedHomes = db.getAllTurtleHomes();
const savedBlocks = db.getWorldBlocks();
console.log(` Loaded ${savedHomes.length} turtle homes`);
console.log(` Loaded ${savedBlocks.length} world blocks`);
// Store connected web clients and turtle data
const webClients = new Set();
const turtleData = new Map(); // turtleID -> turtle state
@@ -17,6 +28,22 @@ const worldBlocks = new Map(); // "x,y,z" -> {name, metadata, discoveredBy, time
const turtleHomes = new Map(); // turtleID -> {x, y, z} home position
const turtleConfig = new Map(); // turtleID -> {maxDistance, facing, etc}
// Load saved homes into memory
for (const home of savedHomes) {
turtleHomes.set(home.turtle_id, { x: home.x, y: home.y, z: home.z });
}
// Load saved blocks into memory
for (const block of savedBlocks) {
const key = `${block.x},${block.y},${block.z}`;
worldBlocks.set(key, {
name: block.block_name,
metadata: block.metadata,
discoveredBy: block.discovered_by,
timestamp: block.discovered_at
});
}
// Timeout for considering turtles offline (30 seconds)
const TURTLE_TIMEOUT = 30000;
@@ -62,6 +89,9 @@ function storeBlock(x, y, z, blockData, turtleID) {
discoveredBy: turtleID,
timestamp: Date.now()
});
// Persist to database
db.saveWorldBlock(x, y, z, blockData.name, blockData.metadata, turtleID);
}
// Helper to calculate block position based on turtle position and facing
@@ -250,13 +280,16 @@ app.post('/api/turtle/:id/home', (req, res) => {
const turtleID = parseInt(req.params.id);
const { position } = req.body;
if (!position || !position.x || !position.y || !position.z) {
if (!position || position.x === undefined || position.y === undefined || position.z === undefined) {
return res.status(400).json({ error: 'Invalid position' });
}
turtleHomes.set(turtleID, position);
console.log(`📍 Set home for turtle ${turtleID}:`, position);
// Persist to database
db.saveTurtleHome(turtleID, position);
// Update turtle data
if (turtleData.has(turtleID)) {
const turtle = turtleData.get(turtleID);
@@ -350,6 +383,192 @@ setInterval(() => {
}
}, 10000); // Check every 10 seconds
// ========== PATH RECORDING ENDPOINTS ==========
// Save a recorded path
app.post('/api/paths', (req, res) => {
try {
const { turtleId, pathName, pathData } = req.body;
db.savePath(turtleId, pathName, pathData);
res.json({ success: true, message: 'Path saved' });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Get all paths for a turtle
app.get('/api/paths/:turtleId', (req, res) => {
try {
const turtleId = parseInt(req.params.turtleId);
const paths = db.getPaths(turtleId);
res.json({ paths });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Delete a path
app.delete('/api/paths/:pathId', (req, res) => {
try {
const pathId = parseInt(req.params.pathId);
db.deletePath(pathId);
res.json({ success: true });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// ========== TASK QUEUE ENDPOINTS ==========
// Create a new task
app.post('/api/tasks', (req, res) => {
try {
const { taskType, taskData, priority } = req.body;
const taskId = db.createTask(taskType, taskData, priority || 0);
broadcastToClients({
type: 'task_created',
taskId,
taskType,
priority
});
res.json({ success: true, taskId });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Get all tasks
app.get('/api/tasks', (req, res) => {
try {
const tasks = db.getAllTasks();
res.json({ tasks });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Get next available task
app.get('/api/tasks/next', (req, res) => {
try {
const task = db.getNextTask();
res.json({ task });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Assign task to turtle
app.post('/api/tasks/:taskId/assign', (req, res) => {
try {
const taskId = parseInt(req.params.taskId);
const { turtleId } = req.body;
db.assignTask(taskId, turtleId);
broadcastToClients({
type: 'task_assigned',
taskId,
turtleId
});
res.json({ success: true });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Complete a task
app.post('/api/tasks/:taskId/complete', (req, res) => {
try {
const taskId = parseInt(req.params.taskId);
db.completeTask(taskId);
broadcastToClients({
type: 'task_completed',
taskId
});
res.json({ success: true });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// ========== MINING AREA ENDPOINTS ==========
// Save a mining area claim
app.post('/api/mining-areas', (req, res) => {
try {
const { turtleId, bounds } = req.body;
db.saveMiningArea(turtleId, bounds);
const areas = db.getMiningAreas();
broadcastToClients({
type: 'mining_areas_updated',
areas
});
res.json({ success: true });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Get all active mining areas
app.get('/api/mining-areas', (req, res) => {
try {
const areas = db.getMiningAreas();
res.json({ areas });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Close a mining area
app.post('/api/mining-areas/:areaId/close', (req, res) => {
try {
const areaId = parseInt(req.params.areaId);
db.closeMiningArea(areaId);
const areas = db.getMiningAreas();
broadcastToClients({
type: 'mining_areas_updated',
areas
});
res.json({ success: true });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// ========== STATISTICS ENDPOINTS ==========
// Get server statistics
app.get('/api/stats', (req, res) => {
try {
const stats = {
activeTurtles: turtleData.size,
totalBlocks: worldBlocks.size,
savedHomes: turtleHomes.size,
connectedClients: webClients.size,
tasks: db.getAllTasks().length,
miningAreas: db.getMiningAreas().length
};
res.json(stats);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Graceful shutdown
process.on('SIGINT', () => {
console.log('\n🛑 Shutting down server...');
db.closeDatabase();
process.exit(0);
});
server.listen(PORT, () => {
console.log(`✅ Server ready!`);
console.log(`\nConfigured turtles to send updates to:`);