Compare commits
5 Commits
23259a5410
...
38b7846607
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38b7846607 | ||
|
|
a3fe5c7471 | ||
|
|
a68db21f9c | ||
|
|
a74802afee | ||
|
|
fb53056e85 |
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
287
server/database.js
Normal 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 };
|
||||
@@ -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"
|
||||
|
||||
221
server/server.js
221
server/server.js
@@ -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:`);
|
||||
|
||||
Reference in New Issue
Block a user