Compare commits

...

330 Commits

Author SHA1 Message Date
MayaTheShy
0a66cad13a fix(docker): clone platform from git instead of additional_contexts
The additional_contexts approach required cc-platform-core to exist on
the Docker host at a relative path. This fails on servers where the
repo layout differs. Instead, use a multi-stage build: stage 1 clones
cc-platform-core from Gitea (depth 1), stage 2 copies server/ into the
app and rewrites the file: path. Fully self-contained — no host deps.
Applied to both production and dev Dockerfiles.
2026-03-28 22:38:07 -04:00
MayaTheShy
f008a9e665 fix(docker): resolve @cc-platform/server file: dep in container build
Use additional_contexts to copy platform server package into the Docker
build context. Rewrites the file: dependency path and removes the
lockfile so npm install can resolve the local package correctly.
Applied to both production and dev Dockerfiles.
2026-03-28 22:35:53 -04:00
MayaTheShy
ed612f3e38 refactor: enhance message handling for dual-mode channel compatibility using Channels.match() 2026-03-26 16:19:42 -04:00
MayaTheShy
dcd9e22b6f refactor: enhance command processing for dual-mode channel compatibility 2026-03-26 16:19:38 -04:00
MayaTheShy
f1c8f08272 refactor: enhance status message handling for dual-mode channel compatibility 2026-03-26 16:19:33 -04:00
MayaTheShy
ffb6d679c0 refactor: enhance status message handling for dual-mode channel compatibility 2026-03-26 16:19:29 -04:00
MayaTheShy
ea90a860e9 refactor: enhance channel message handling with dual-mode support for legacy and target channels 2026-03-26 16:19:25 -04:00
MayaTheShy
bdf7a51675 refactor: replace hardcoded channel IDs with dynamic retrieval from platform.channels 2026-03-26 15:22:51 -04:00
MayaTheShy
c4b9509b5c refactor: replace hardcoded channel IDs with dynamic retrieval from platform.channels 2026-03-26 15:22:36 -04:00
MayaTheShy
92ea13a680 refactor: replace hardcoded status channel ID with dynamic retrieval from platform.channels 2026-03-26 15:22:31 -04:00
MayaTheShy
05cf7e98d9 refactor: replace hardcoded channel IDs with dynamic channel retrieval from platform.channels 2026-03-26 15:22:23 -04:00
MayaTheShy
9291b063d0 refactor: replace express server setup with platform server integration and streamline proxy endpoints 2026-03-26 15:19:53 -04:00
MayaTheShy
6f462c97e0 fix: add missing dependency for cc-platform server in package.json 2026-03-26 15:19:47 -04:00
MayaTheShy
9ff2ce7ff2 refactor: streamline configuration loading and channel setup in web bridge 2026-03-26 15:19:42 -04:00
MayaTheShy
fc1b23470e feat: add required platform specification in package configuration 2026-03-26 15:19:37 -04:00
MayaTheShy
cb44dd8d0f fix: correct requires format in apps.db and add missing entries
Opus Overview expects requires as a string (e.g. 'pocket'),
not a table. Fixed pocket apps and added turtle requirement
for turtle controller. Added pocketcontrol.lua entry.
2026-03-22 18:40:20 -04:00
MayaTheShy
1f41e1fa51 docs: add GPS alternative and pathfinding usage to quickstart guide 2026-03-22 18:24:27 -04:00
MayaTheShy
fa085339b8 feat: implement movement wrapping and pathfinding module for turtle 2026-03-22 18:16:06 -04:00
MayaTheShy
5ad01dfd1d feat: add web bridge configuration setup to installation process 2026-03-22 16:11:11 -04:00
MayaTheShy
b72826bc46 feat: implement dynamic configuration loading for server and WebSocket URLs 2026-03-22 16:07:55 -04:00
MayaTheShy
459664825c fix: update exclude patterns in package configuration for clarity 2026-03-22 16:07:52 -04:00
MayaTheShy
b13905dade feat: add initial apps configuration file for RemoteTurtle components 2026-03-22 15:55:49 -04:00
MayaTheShy
5a4af6c986 feat: add initial package configuration file 2026-03-22 15:55:25 -04:00
MayaTheShy
633d162d81 fix: reorder dependencies and update vitest version in package.json 2026-03-22 11:48:02 -04:00
MayaTheShy
aa3b166453 feat: add comprehensive tests for WorldBlockCache functionality 2026-03-22 11:47:57 -04:00
MayaTheShy
56fc79f5f2 feat: add TaskDispatcher tests for task assignment and management 2026-03-22 11:47:22 -04:00
MayaTheShy
b6ab6f94f6 feat: add testing scripts and vitest as a dev dependency 2026-03-22 11:46:51 -04:00
MayaTheShy
4d5d2162e6 feat: refactor world block retrieval to use getAllBlocksForAPI method 2026-03-22 11:45:24 -04:00
MayaTheShy
24570d0fc0 feat: integrate WorldBlockCache for efficient world block management and update logging 2026-03-22 11:45:10 -04:00
MayaTheShy
6312e45bf1 feat: add getWorldBlockCount function to retrieve the total number of world blocks 2026-03-22 11:45:04 -04:00
MayaTheShy
34725d7d71 feat: implement WorldBlockCache for efficient block management with LRU caching 2026-03-22 11:44:58 -04:00
MayaTheShy
811e2a6e18 feat: enhance task assignment logic to support un-assigning tasks 2026-03-22 11:44:14 -04:00
MayaTheShy
ad0754113d feat: add endpoints for task cancellation and dispatcher control 2026-03-22 11:43:09 -04:00
MayaTheShy
3e55d77592 feat: implement TaskDispatcher for automatic task management and dispatching 2026-03-22 11:43:02 -04:00
MayaTheShy
9984dc0760 refactor: start task dispatcher after server initialization 2026-03-22 11:42:52 -04:00
MayaTheShy
88163be0dd refactor: add TaskDispatcher for automatic task assignment to idle turtles 2026-03-22 11:42:46 -04:00
MayaTheShy
679a249f8b refactor: implement API key authentication for secure access to endpoints 2026-03-22 11:25:04 -04:00
MayaTheShy
69041244a2 refactor: add API_KEY environment variable to server configuration 2026-03-22 11:25:00 -04:00
MayaTheShy
9a56e6b736 refactor: add cross-project integration API for inventory management and turtle state queries 2026-03-22 04:11:06 -04:00
MayaTheShy
79b50071ee refactor: add inventory dashboard link with appropriate attributes in the panel 2026-03-22 04:10:28 -04:00
MayaTheShy
9b09a59eba refactor: add styling for cross-link button with hover effects 2026-03-22 04:10:23 -04:00
MayaTheShy
6d8ec7b013 refactor: add INVENTORY_SERVER_URL environment variable to server configuration 2026-03-22 04:10:17 -04:00
MayaTheShy
90ec195497 refactor: enhance error handling for eval response to log failures and track error statistics 2026-02-20 04:40:29 -05:00
MayaTheShy
23515728e0 refactor: enhance command timeout logging and add keepalive ping handling for improved monitoring 2026-02-20 04:39:52 -05:00
MayaTheShy
a809bddd46 refactor: improve error handling in idle state fuel check to prevent infinite loop 2026-02-20 04:38:58 -05:00
MayaTheShy
5ff1f3e7f0 refactor: prevent infinite loop in idle state handling by adding retry mechanism 2026-02-20 04:38:54 -05:00
MayaTheShy
00d31698a1 refactor: filter players to display only those with valid positions and recent timestamps for improved clarity 2026-02-20 04:32:12 -05:00
MayaTheShy
af2c978185 refactor: update player marker display to show label if available for improved clarity 2026-02-20 04:32:02 -05:00
MayaTheShy
8f23aa5caa refactor: update player position handling to include label for improved tracking 2026-02-20 04:31:53 -05:00
MayaTheShy
720c6c20fb refactor: enhance player position update to include label and timestamp for improved tracking 2026-02-20 04:31:47 -05:00
MayaTheShy
f61e7ca185 refactor: include player positions in initial WebSocket state for enhanced tracking 2026-02-20 04:29:28 -05:00
MayaTheShy
ddc1b03506 refactor: enhance player data handling to include label and default timestamp for improved tracking 2026-02-20 04:29:23 -05:00
MayaTheShy
460352ec26 refactor: update player position saving to include optional label for enhanced tracking 2026-02-20 04:28:12 -05:00
MayaTheShy
3ce0e4c530 refactor: enhance player position handling to include label for improved tracking 2026-02-20 04:28:05 -05:00
MayaTheShy
38ff06eb04 refactor: enhance player update handling to include label and timestamp for improved tracking 2026-02-20 04:27:57 -05:00
MayaTheShy
cfd127dfab refactor: migrate player_positions table to add label column for enhanced data tracking 2026-02-20 04:27:46 -05:00
MayaTheShy
d2718b3287 refactor: add total steps display in TurtleDetails for better status tracking 2026-02-20 04:24:36 -05:00
MayaTheShy
8f4eeabee9 refactor: enhance ExploringState for chunk-based spiral exploration and improve navigation logic 2026-02-20 04:24:29 -05:00
MayaTheShy
3b2e00b2b4 refactor: update turnToFace method to synchronize turtle facing state with global variable 2026-02-20 04:24:24 -05:00
MayaTheShy
cb666a6a45 refactor: synchronize turtle facing state with global variable on turn commands 2026-02-20 04:24:15 -05:00
MayaTheShy
c424662c18 refactor: update color logic in MiningArea to prioritize custom area color 2026-02-20 04:22:20 -05:00
MayaTheShy
05519dc17e refactor: add color picker styles and area color dot to enhance UI 2026-02-20 04:21:21 -05:00
MayaTheShy
681b4e1fa9 refactor: enhance area creation form with color picker and display area color in the card 2026-02-20 04:21:07 -05:00
MayaTheShy
465a8bacf4 refactor: add color property to newArea state and include it in area creation 2026-02-20 04:20:52 -05:00
MayaTheShy
bfae87287a refactor: add color parameter to saveMiningArea and updateMiningArea functions; implement write-file and refresh-inventory endpoints for Turtle 2026-02-20 04:20:06 -05:00
MayaTheShy
12fc109a30 refactor: add areaName and color properties to formatMiningArea function 2026-02-20 04:19:34 -05:00
MayaTheShy
973e4be6a3 refactor: add color and name parameters to saveMiningArea and implement updateMiningArea function 2026-02-20 04:19:27 -05:00
MayaTheShy
fb84b5a554 refactor: add name and color columns to mining_areas table and handle migration 2026-02-20 04:19:03 -05:00
MayaTheShy
2e3d5b4b6b refactor: enhance refueling logic and add file writing and inventory refresh methods in Turtle class 2026-02-20 04:18:44 -05:00
MayaTheShy
9a34f72178 refactor: track steps since last refuel and total steps in Turtle class 2026-02-20 04:18:03 -05:00
MayaTheShy
88fdd1c46d refactor: add fuel efficiency tracking to Turtle class 2026-02-20 04:17:47 -05:00
MayaTheShy
e3abdb612c refactor: update Web Bridge to enhance WebSocket integration and streamline dashboard functionality 2026-02-20 04:14:57 -05:00
MayaTheShy
e84ca4cfb9 refactor: enhance WebSocket handling for bridge connections and command forwarding 2026-02-20 04:14:03 -05:00
MayaTheShy
b8cd239597 refactor: unify HTTP API and WebSocket ports in docker-compose configuration 2026-02-20 04:13:51 -05:00
MayaTheShy
5aec3df3b3 refactor: improve inventory count calculation for TurtleCard component 2026-02-20 04:05:06 -05:00
MayaTheShy
989b6f9118 refactor: update IdleState to include periodic fuel checks and adjust sleep duration 2026-02-20 04:04:01 -05:00
MayaTheShy
ec5f048d49 fix: correct function name to retrieve world blocks in chunk analysis endpoint 2026-02-20 04:03:54 -05:00
MayaTheShy
60c5b3aaba feat: add chunk analysis endpoint to compute ore density from discovered blocks 2026-02-20 04:03:34 -05:00
MayaTheShy
2c806bf994 refactor: enhance state loop error handling with consecutive error tracking for improved resilience 2026-02-20 04:03:23 -05:00
MayaTheShy
b34cc8cec0 refactor: update turtle script for improved structure and clarity, enhancing state management and communication protocols 2026-02-20 04:03:18 -05:00
MayaTheShy
cef3cdf03d refactor: update turtle script for improved server-driven functionality and code organization 2026-02-20 03:54:52 -05:00
MayaTheShy
b8a1b7c0b3 refactor: streamline safety checks and error handling in mining operation for improved reliability 2026-02-20 03:54:23 -05:00
MayaTheShy
8fcd3f44c7 refactor: improve error handling and retry logic in exploration state for enhanced stability 2026-02-20 03:54:15 -05:00
MayaTheShy
7385c258d5 refactor: implement error handling and retry logic in state loop for improved resilience 2026-02-20 03:54:07 -05:00
MayaTheShy
2686ca4697 refactor: enhance peripheral display to support multiple types and improve readability 2026-02-20 03:53:49 -05:00
MayaTheShy
53ae92a184 refactor: optimize polling interval and command transmission for improved responsiveness 2026-02-20 03:53:40 -05:00
MayaTheShy
7da9c1d0d8 refactor: remove legacy turtleData getter and update command handling for eval commands 2026-02-20 03:45:23 -05:00
MayaTheShy
2549adc49d refactor: replace legacy command handling with unified pending command queue 2026-02-20 03:44:44 -05:00
MayaTheShy
bad3b5bf13 refactor: replace legacy command queue with eval command queue in Turtle class 2026-02-20 03:44:27 -05:00
MayaTheShy
de58ec6b08 Refactor group command handling to enforce server control of turtle movement and reject legacy commands 2026-02-20 03:44:11 -05:00
MayaTheShy
f6b39808aa Refactor WebSocket command handling to enforce server-side control of turtle movement and remove legacy command support 2026-02-20 03:43:45 -05:00
MayaTheShy
c02bb7db68 style: Remove unused sendCommand function from turtleStore 2026-02-20 03:43:40 -05:00
MayaTheShy
885ebf698d Refactor PathRecorder to utilize server-side pathfinding for waypoint navigation 2026-02-20 03:43:36 -05:00
MayaTheShy
1522523f22 style: Remove unused sendCommand from Scene component 2026-02-20 03:43:31 -05:00
MayaTheShy
bca3cb4508 style: Refactor PathRecorder to utilize individual turtle movement functions 2026-02-20 03:42:13 -05:00
MayaTheShy
ebe4f10df5 Refactor VoiceControl to handle server-side movement and state commands directly 2026-02-20 03:42:07 -05:00
MayaTheShy
586b161da9 style: Add turnLeft and turnRight methods to Turtle class for directional control 2026-02-20 03:41:46 -05:00
MayaTheShy
2316e14b9c style: Add server-side endpoints for turtle movement and actions 2026-02-20 03:41:42 -05:00
MayaTheShy
45a4b4e7ae style: Refactor VoiceControl to utilize individual turtle command functions 2026-02-20 03:41:38 -05:00
MayaTheShy
9735fd8776 style: Implement server-side turtle actions for movement and block manipulation 2026-02-20 03:41:27 -05:00
MayaTheShy
c6ff9094ed style: Refactor TurtleDetails to use direct command functions and remove legacy command handling 2026-02-20 03:41:21 -05:00
MayaTheShy
cb2353785f style: Add middleware to rewrite requests without /api prefix for reverse proxy compatibility 2026-02-20 03:29:14 -05:00
MayaTheShy
46c0817270 style: Update SERVER_URL to direct to Docker server on LAN for local development 2026-02-20 03:16:35 -05:00
MayaTheShy
f0281ddaa5 style: Remove version declaration from docker-compose.yml for cleaner configuration 2026-02-20 03:15:42 -05:00
MayaTheShy
fc5cbae73e style: Simplify Dockerfile by copying all server code in a single command 2026-02-20 03:11:35 -05:00
MayaTheShy
9cb2939224 style: Update API fetch URL to use dynamic origin for improved flexibility 2026-02-20 03:09:53 -05:00
MayaTheShy
1a2de77ae2 style: Update API URL construction for improved endpoint clarity 2026-02-20 03:09:49 -05:00
MayaTheShy
420456c04b style: Update SERVER_URL to use reverse proxy for improved security 2026-02-20 03:09:45 -05:00
MayaTheShy
b7221327c2 style: Add shadow-receiving ground plane and update UI colors for improved consistency and depth 2026-02-20 03:05:59 -05:00
MayaTheShy
997201b139 style: Enhance lighting and shadow effects for improved visual depth and realism 2026-02-20 03:05:26 -05:00
MayaTheShy
daaf969662 style: Remove view controls for improved layout consistency and simplify app structure 2026-02-20 03:05:22 -05:00
MayaTheShy
0b61d8b2dd style: Remove view controls for improved layout consistency and responsiveness 2026-02-20 03:05:18 -05:00
MayaTheShy
586d231720 style: Update color presets in GroupsPanel for improved consistency with Minecraft theme 2026-02-20 02:59:44 -05:00
MayaTheShy
3c6ee280ba style: Update task status and priority colors for improved visibility and consistency with Minecraft theme 2026-02-20 02:58:54 -05:00
MayaTheShy
5966d4de2b style: Update StatusBadge colors for improved visibility and consistency with Minecraft theme 2026-02-20 02:58:45 -05:00
MayaTheShy
1d99ba534a style: Revamp MiningAreasPanel.css for improved aesthetics and consistency with Minecraft theme 2026-02-20 02:58:38 -05:00
MayaTheShy
b06f878ca0 style: Update VoiceControl.css for improved aesthetics and consistency with Minecraft theme 2026-02-20 02:56:27 -05:00
MayaTheShy
943cc73163 style: Enhance PathRecorder.css for improved aesthetics and consistency with Minecraft theme 2026-02-20 02:55:20 -05:00
MayaTheShy
2263fbb1de style: Revamp TaskPanel.css for improved aesthetics and consistency with Minecraft theme 2026-02-20 02:54:18 -05:00
MayaTheShy
481be70940 style: Revamp GroupsPanel.css for enhanced Minecraft theme consistency and improved aesthetics 2026-02-20 02:53:24 -05:00
MayaTheShy
60ef3b81f7 style: Update StatsPanel.css for improved aesthetics and consistency with Minecraft theme 2026-02-20 02:52:22 -05:00
MayaTheShy
997c64c40b style: Escape quotes and backslashes in usePeripheralWithSide method for safe Lua string interpolation 2026-02-20 02:50:14 -05:00
MayaTheShy
a68ddd843f style: Update block search API to accept 'name' parameter as an alternative to 'pattern' 2026-02-20 02:50:09 -05:00
MayaTheShy
fe44978f6e style: Escape quotes and backslashes in turtle place methods and connectToInventory for safe Lua string interpolation 2026-02-20 02:48:00 -05:00
MayaTheShy
c862b2816c style: Escape quotes and backslashes in turtle rename method for safe Lua string interpolation 2026-02-20 02:47:41 -05:00
MayaTheShy
73f8a21a81 style: Refactor gpsLocate method for improved position handling and code clarity 2026-02-20 02:47:36 -05:00
MayaTheShy
617310eade style: Enhance eval response handling and inventory update for improved data management 2026-02-20 02:47:29 -05:00
MayaTheShy
7dae800eed style: Adjust button layout in drawDetail function for improved UI organization 2026-02-20 02:47:16 -05:00
MayaTheShy
86a6db04ac style: Update selected inventory slot outline color for improved UI consistency 2026-02-20 02:44:49 -05:00
MayaTheShy
8e1d1f67fc style: Update error, warning, and peripheral colors for improved UI consistency 2026-02-20 02:44:26 -05:00
MayaTheShy
fa8b45b74a style: Update TurtleCard and TurtleDetails components for improved UI consistency 2026-02-20 02:44:14 -05:00
MayaTheShy
32677ecac5 style: Revamp Control Panel CSS with Minecraft-themed colors and improved UI elements 2026-02-20 02:42:08 -05:00
MayaTheShy
5f6dbce277 style: Update scrollbar colors for improved UI consistency 2026-02-20 02:38:31 -05:00
MayaTheShy
75ca027ab4 style: Update background colors and button styles for improved UI consistency 2026-02-20 02:38:27 -05:00
MayaTheShy
cfc891d164 style: Update body and code font styles for improved UI consistency 2026-02-20 02:38:08 -05:00
MayaTheShy
5b89e0432e style: Update view controls styling for improved UI consistency 2026-02-20 02:37:59 -05:00
MayaTheShy
37bd17f26a feat: Add equip left, equip right, rename, and sort buttons for turtle management 2026-02-20 02:36:39 -05:00
MayaTheShy
c0865d5196 feat: Add equipment and inventory action buttons to TurtleCard 2026-02-20 02:35:41 -05:00
MayaTheShy
a0eaeb6712 feat: Add turtle management features including rename, configuration, and GPS locate 2026-02-20 02:35:17 -05:00
MayaTheShy
d8f3d5d13c feat: Update TurtleCard to display turtle label instead of ID 2026-02-20 02:34:48 -05:00
MayaTheShy
2571865917 feat: Add turtle inventory management actions including rename, equip, select, transfer, sort, connect, drop, suck, update config, get config, explore, and GPS locate 2026-02-20 02:34:41 -05:00
MayaTheShy
0e6d4acfdd feat: Implement block deletion handling in WebSocket message processing 2026-02-20 02:34:21 -05:00
MayaTheShy
67b2d7eb2e feat: Add turtle auto-naming, state recovery, and multiple action endpoints for inventory management 2026-02-20 02:34:13 -05:00
MayaTheShy
7b63c92434 feat: Enhance DumpInventoryState with fuzzy container detection and detailed dump reporting 2026-02-20 02:33:30 -05:00
MayaTheShy
2a0a90892c feat: Add computer label handling and status broadcasting on rename command 2026-02-20 02:33:25 -05:00
MayaTheShy
11c289a3ed feat: Add Levenshtein distance utility for fuzzy matching of item names 2026-02-20 02:32:43 -05:00
MayaTheShy
ac6dd5da95 feat: Enhance Turtle class with state recovery, command chaining, and new movement methods 2026-02-20 02:30:15 -05:00
MayaTheShy
58cc909de8 feat: Add turtle state persistence functions for saving, retrieving, and deleting turtle states 2026-02-20 02:27:44 -05:00
MayaTheShy
b9023758a6 feat: Add turtle state persistence table for reconnect recovery 2026-02-20 02:26:31 -05:00
MayaTheShy
6e05efa2f0 feat: Add scan, extract, build, and autocraft buttons to turtle control interface 2026-02-20 02:21:09 -05:00
MayaTheShy
b632e14932 feat: Add chunk analysis handling and search functionality in turtle store 2026-02-20 02:20:34 -05:00
MayaTheShy
5208c38738 feat: Add new turtle commands for scanning, extraction, building, and autocrafting with error and warning displays 2026-02-20 02:19:59 -05:00
MayaTheShy
a64e9a1a51 feat: Add interaction toolbar and block count HUD to Map3D component 2026-02-20 02:19:51 -05:00
MayaTheShy
be0064aae5 feat: Implement enhanced interaction handling with move, build, and selection previews in Map3D component 2026-02-20 02:18:40 -05:00
MayaTheShy
dd2a877192 feat: Add move and build preview components with selection area for enhanced interaction 2026-02-20 02:18:16 -05:00
MayaTheShy
1ef975cbae feat: Optimize WorldBlocks rendering with chunked InstancedMesh and interaction handling 2026-02-20 02:18:08 -05:00
MayaTheShy
e62e83cf33 feat: Enhance interaction modes with detailed comments in Map3D component 2026-02-20 02:17:13 -05:00
MayaTheShy
eff33dfe09 feat: Add real-time inventory and peripheral event handling for turtles 2026-02-20 02:15:54 -05:00
MayaTheShy
f38a219ad0 feat: Add real-time inventory and peripheral event handling for Turtle 2026-02-20 02:15:45 -05:00
MayaTheShy
0bf5590343 feat: Export additional states for scanning, extraction, building, and autocrafting in Turtle state machine 2026-02-20 02:15:36 -05:00
MayaTheShy
bf7db42384 feat: Add additional states for scanning, extracting, building, and autocrafting in Turtle state machine 2026-02-20 02:15:31 -05:00
MayaTheShy
8731da04f9 feat: Add real-time event handling and chunk analysis endpoints for turtles 2026-02-20 02:15:26 -05:00
MayaTheShy
ff58778f3f feat: Add additional properties to Turtle state for enhanced status reporting 2026-02-20 02:13:10 -05:00
MayaTheShy
668d4a3685 feat: Enhance Turtle class with new properties and real-time event handling for peripherals and inventory 2026-02-20 02:12:56 -05:00
MayaTheShy
e401c39bb3 feat: Implement AutocraftState for automated crafting with workbench integration 2026-02-20 02:12:29 -05:00
MayaTheShy
d4e6b469df feat: Add BuildingState for autonomous block placement with material management 2026-02-20 02:11:45 -05:00
MayaTheShy
150cc4b41a feat: Implement ScanState for systematic scanning with multiple scanner types 2026-02-20 02:11:04 -05:00
MayaTheShy
220f2a90d4 feat: Implement ExtractionState for smart ore extraction with area scanning and mining logic 2026-02-20 02:10:58 -05:00
MayaTheShy
ba11d2c9d2 feat: Add chunk analysis table and enhance world_blocks schema for block state and tags 2026-02-20 02:10:50 -05:00
MayaTheShy
dca14643c6 feat: Refactor TurtleCard and TurtleDetails to implement active state management and enhance command handling 2026-02-20 02:03:47 -05:00
MayaTheShy
67498b93bf feat: Add setTurtleState and execOnTurtle functions for REST API integration 2026-02-20 02:02:40 -05:00
MayaTheShy
df00a6be70 feat: Add handling for 'blocks_discovered' message to update worldBlocks in turtle store 2026-02-20 02:02:04 -05:00
MayaTheShy
f23ebcb05a feat: Enhance multi-face texture handling for Minecraft blocks in Map3D component 2026-02-20 02:01:53 -05:00
MayaTheShy
08281d88fe feat: Expand texture mapping for Minecraft blocks and enhance multi-face block handling 2026-02-20 02:01:00 -05:00
MayaTheShy
882dc75762 feat: Add state management and modes for turtle commands in Pocket Computer 2026-02-20 01:59:37 -05:00
MayaTheShy
4c774ac306 feat: Enhance turtle command handling with legacy support and eval response endpoints 2026-02-20 01:57:09 -05:00
MayaTheShy
e38551dfc2 feat: Update command polling endpoint to support combined eval and legacy commands for turtles 2026-02-20 01:54:59 -05:00
MayaTheShy
87c103ea9e feat: Update WebSocket handling for turtle commands and status updates, transitioning to Turtle instance methods for state management 2026-02-20 01:54:43 -05:00
MayaTheShy
836e7b61c7 feat: Enhance turtle instance management with improved state handling and event forwarding 2026-02-20 01:54:15 -05:00
MayaTheShy
f8baadce3a feat: Implement eval command protocol and response handling in web bridge 2026-02-20 01:53:43 -05:00
MayaTheShy
a5aec5800e Refactor turtle.lua for v4: Implement eval/response protocol, streamline state management, enhance GPS and movement functions, and improve command handling. 2026-02-20 01:50:55 -05:00
MayaTheShy
0e41ee12c5 feat: Add Turtle class to manage state machine and command execution for server-side turtles 2026-02-20 01:46:39 -05:00
MayaTheShy
bdc4dade9f feat: Implement RefuelingState class for turtle state machine to manage fuel replenishment 2026-02-20 01:46:09 -05:00
MayaTheShy
e8ef1d669b feat: Implement MiningState class for turtle state machine to autonomously mine and explore 2026-02-20 01:46:05 -05:00
MayaTheShy
3dfc4237c6 feat: Add index.js to export state classes for turtle state machine 2026-02-20 01:46:01 -05:00
MayaTheShy
1492f59de7 feat: Implement GoHomeState class for turtle state machine to navigate back to home position 2026-02-20 01:45:57 -05:00
MayaTheShy
d2185ac493 feat: Implement FarmingState class for turtle state machine to automate farming operations 2026-02-20 01:45:46 -05:00
MayaTheShy
fa98e86055 feat: Implement ExploringState class for turtle state machine to autonomously explore and discover the world map 2026-02-20 01:45:39 -05:00
MayaTheShy
1b08777f88 feat: Implement DumpInventoryState class for turtle state machine to manage inventory dumping 2026-02-20 01:45:29 -05:00
MayaTheShy
68d4ee52c9 feat: Implement MovingState class for turtle state machine to navigate to target positions 2026-02-20 01:42:16 -05:00
MayaTheShy
6f73fdd0ed feat: Implement IdleState class for turtle state machine to handle idle behavior 2026-02-20 01:42:06 -05:00
MayaTheShy
0f22c8e49b feat: Add BaseState class for turtle state machine with async generator support 2026-02-20 01:41:58 -05:00
MayaTheShy
9796f73e64 feat: Implement PriorityQueue class for D* Lite pathfinding with min-heap functionality 2026-02-20 01:41:01 -05:00
MayaTheShy
6da90fc560 feat: Add pathfinding module exports for Point, Node, PriorityQueue, and DStarLite 2026-02-20 01:40:53 -05:00
MayaTheShy
afc4c1f97a feat: Implement D* Lite pathfinding algorithm with node management and dynamic replanning 2026-02-20 01:40:48 -05:00
MayaTheShy
9a294f0c98 feat: Add Point class to represent 3D coordinates with distance calculations and neighbor retrieval 2026-02-20 01:40:39 -05:00
MayaTheShy
1f803fbde6 feat: Implement Node class for D* Lite pathfinding algorithm with traversal cost calculation 2026-02-20 01:40:32 -05:00
MayaTheShy
207f6901d6 fix: Update mining stats and top miners endpoints to return structured data for single and multiple turtles 2026-02-20 01:32:35 -05:00
MayaTheShy
d2e0deb945 fix: Add UNIQUE constraint to mining blocks table for turtle_id, block_type, and session_start 2026-02-20 01:32:01 -05:00
MayaTheShy
9fd9180087 fix: Update loadTasks function to handle server responses with flat arrays or single task objects 2026-02-20 01:31:56 -05:00
MayaTheShy
558fb92c41 fix: Update miningStats and topMiners loading logic to handle single object responses and ensure correct state setting 2026-02-20 01:31:49 -05:00
MayaTheShy
bd68a79cd5 feat: Enhance PathRecorder component; implement path playback functionality and improve path loading logic 2026-02-20 01:31:45 -05:00
MayaTheShy
f0afbca74b fix: Handle server response for mining areas; ensure areas state is set correctly for non-array responses 2026-02-20 01:31:40 -05:00
MayaTheShy
35c76cbdca fix: Ensure groups state is set correctly by handling non-array responses in loadGroups function 2026-02-20 01:31:36 -05:00
MayaTheShy
7a3b30bbbf feat: Enhance mining area API; add formatting helper, support for multiple input formats, and new endpoints for updating and deleting areas 2026-02-20 01:29:29 -05:00
MayaTheShy
d7433b8bcc feat: Enhance path and task management APIs; add detailed responses and optional filters 2026-02-20 01:29:03 -05:00
MayaTheShy
3c0f72acf1 feat: Enhance database functions for turtle paths and mining areas; add support for optional parameters and improve data handling 2026-02-20 01:28:54 -05:00
MayaTheShy
75b2088b4f feat: Update API and WebSocket URLs to use environment variables for better configuration 2026-02-20 01:28:41 -05:00
MayaTheShy
0eac9497de feat: Implement block discovery endpoint and update worldBlocks state on discovery 2026-02-20 01:23:05 -05:00
MayaTheShy
855874576d fix: Update turtle state references to use mode instead; adjust command transmission channels 2026-02-20 01:22:58 -05:00
MayaTheShy
0c925036d9 feat: Add action buttons for turtle commands in control mode 2026-02-20 01:22:51 -05:00
MayaTheShy
68e21d9c82 feat: Update installation instructions in README; add auto-update system details and wireless control interface 2026-02-20 01:15:37 -05:00
MayaTheShy
1cccfb5baa feat: Update README for Docker installation; add setup instructions and commands 2026-02-20 01:13:07 -05:00
MayaTheShy
4bd999d394 feat: Forward discovered blocks to server; log block details and handle errors 2026-02-20 01:06:09 -05:00
MayaTheShy
8d66ef637e feat: Implement comprehensive block scanning; report discovered blocks to server 2026-02-20 01:05:18 -05:00
MayaTheShy
5a4fd000fe feat: Enhance debugging output; log open channels and modem messages in the main event loop 2026-02-20 01:00:01 -05:00
MayaTheShy
86250deba3 feat: Improve command processing output; list command names instead of serializing the commands table 2026-02-20 00:59:19 -05:00
MayaTheShy
b1d68565cd feat: Enhance message logging in webbridge; add detailed output for received status updates and turtle tracking 2026-02-20 00:56:03 -05:00
MayaTheShy
922e6ab25d feat: Enhance status broadcasting in turtle; include additional logging for better visibility 2026-02-20 00:55:56 -05:00
MayaTheShy
5b23ab1a14 feat: Enhance logging for modem communication in turtle and webbridge; improve command transmission feedback 2026-02-20 00:52:35 -05:00
MayaTheShy
5fb8ddf68e feat: Improve command handling for turtles; clean up old commands and enhance logging 2026-02-20 00:48:24 -05:00
MayaTheShy
91918bd124 feat: Enhance command processing feedback in turtle; improve sleep duration for command acknowledgment in webbridge 2026-02-20 00:48:19 -05:00
MayaTheShy
8d43c0dc99 feat: Redesign TurtleModel with enhanced shell, head, and leg details; add LED indicators and ambient glow 2026-02-20 00:36:19 -05:00
MayaTheShy
6b45682fac feat: Adjust turtle rotation logic for Three.js to correctly align based on facing direction 2026-02-20 00:35:21 -05:00
MayaTheShy
af2d120baa feat: Add error handling for turtle startup script and improve GPS retry logic 2026-02-20 00:32:40 -05:00
MayaTheShy
544c5be954 feat: Add auto-update startup scripts for turtle, webbridge, and pocket computers 2026-02-20 00:23:03 -05:00
MayaTheShy
baa807ed2a feat: Add player markers to the scene for better visibility 2026-02-20 00:19:45 -05:00
MayaTheShy
9202094de9 feat: Enhance GPS position update logging with success and failure messages 2026-02-20 00:19:41 -05:00
MayaTheShy
835bfde1f5 feat: Add player marker component with bobbing animation and label 2026-02-20 00:19:06 -05:00
MayaTheShy
bc4f87f178 feat: Add player position tracking endpoints for updates and retrieval 2026-02-20 00:18:57 -05:00
MayaTheShy
a1d2b62d5f feat: Add player positions tracking to database 2026-02-20 00:18:52 -05:00
MayaTheShy
b08ff805b4 feat: Add player state management to turtle store 2026-02-20 00:18:49 -05:00
MayaTheShy
5ceef8ba1c refactor: Simplify UI elements and improve layout for Pocket Control Center 2026-02-19 23:58:51 -05:00
MayaTheShy
5ef8977fad feat: Add pocket computer communication and command handling 2026-02-19 23:54:49 -05:00
MayaTheShy
e800d53c38 feat: Implement Pocket Control Center with turtle control, GPS tracking, and server management 2026-02-19 23:53:18 -05:00
MayaTheShy
6da6a06295 fix: Add debug logging for current position during status broadcast 2026-02-19 23:43:48 -05:00
MayaTheShy
43bba2ced7 fix: Add GPS retry mechanism for turtles without initial position 2026-02-19 23:42:06 -05:00
MayaTheShy
856cf88a19 fix: Update last seen text color for better visibility in dashboard display 2026-02-19 23:37:04 -05:00
MayaTheShy
74152951ae fix: Update log text color to improve visibility in dashboard display 2026-02-19 23:37:00 -05:00
MayaTheShy
c2968060fe fix: Handle nil values for time and color in dashboard display 2026-02-19 23:35:33 -05:00
MayaTheShy
127a80813e fix: Ensure time since last seen defaults to 0 in dashboard display 2026-02-19 23:34:54 -05:00
MayaTheShy
31b2c8f61a fix: Correctly calculate time since turtle was last seen in dashboard display 2026-02-19 23:33:56 -05:00
MayaTheShy
6b48ddc8b3 feat: Update dashboard header to display the count of online turtles 2026-02-19 23:31:44 -05:00
MayaTheShy
a3ca1aae1c refactor: Remove redundant activity log display code from dashboard 2026-02-19 23:31:34 -05:00
MayaTheShy
c0573a62aa feat: Revamp dashboard display for Turtle Bridge with simplified header, compact turtle list, and enhanced activity log 2026-02-19 23:31:19 -05:00
MayaTheShy
5bfaf46a8b feat: Implement WebBridge communication improvements with command retention, acknowledgment, and reduced polling frequency 2026-02-19 23:28:12 -05:00
MayaTheShy
4bda9c536b feat: Implement command acknowledgment and improve command polling reliability for turtles 2026-02-19 23:27:23 -05:00
MayaTheShy
6e50a109dc feat: Ensure database.js is copied in Dockerfile for server setup 2026-02-19 23:08:10 -05:00
MayaTheShy
61f817ba18 feat: Complete Mining Area Visualization with 3D rendering, management panel, and CRUD operations 2026-02-19 23:02:52 -05:00
MayaTheShy
90b1e9fdec feat: Add MiningAreasPanel component for managing mining areas with create, update, and delete functionalities 2026-02-19 23:02:17 -05:00
MayaTheShy
8bcee0b16c feat: Add PathRecorder component for recording and managing turtle paths with UI and functionality 2026-02-19 22:58:14 -05:00
MayaTheShy
7b3ae34cfa feat: Add comprehensive feature documentation and overview of key files in README.md 2026-02-19 22:57:00 -05:00
MayaTheShy
546e17e6d2 feat: Enhance App component with dynamic panel tabs for control, voice, stats, groups, and tasks 2026-02-19 22:54:48 -05:00
MayaTheShy
73d098ca33 feat: Add TaskPanel CSS for styling task management interface 2026-02-19 22:53:22 -05:00
MayaTheShy
a419374cb2 feat: Add TaskPanel component for managing tasks with create, update, and delete functionalities 2026-02-19 22:52:54 -05:00
MayaTheShy
f402f3665c feat: Add GroupsPanel CSS for styling group management interface 2026-02-19 22:52:49 -05:00
MayaTheShy
6600ba9ea4 feat: Add GroupsPanel component for managing turtle groups and commands 2026-02-19 22:51:35 -05:00
MayaTheShy
a9bf76bd57 feat: Implement StatsPanel component for displaying mining statistics and top miners 2026-02-19 22:51:22 -05:00
MayaTheShy
05d331efe2 feat: Add StatsPanel component with responsive design and styling 2026-02-19 22:51:13 -05:00
MayaTheShy
7ca59e7197 feat: Expand README with project statistics, API endpoints, and voice command details 2026-02-19 22:48:14 -05:00
MayaTheShy
42a626807b feat: Add mining statistics and turtle groups management endpoints 2026-02-19 22:47:35 -05:00
MayaTheShy
c7315aa3de feat: Add mining statistics, turtle groups, and session tracking to database schema 2026-02-19 22:47:30 -05:00
MayaTheShy
c5e980f5ec feat: Update README to reflect advanced features and improved descriptions 2026-02-19 22:47:25 -05:00
MayaTheShy
57ef89f52c feat: Add Voice Control component with speech recognition and command processing 2026-02-19 22:47:18 -05:00
MayaTheShy
dd58093c40 feat: Enhance mobile responsiveness and touch device optimizations in App.css 2026-02-19 22:41:04 -05:00
MayaTheShy
38b7846607 feat: Refactor inventory display to use a grid layout with improved styling and responsiveness 2026-02-19 22:40:23 -05:00
MayaTheShy
a3fe5c7471 feat: Implement database integration for turtle homes, world blocks, paths, tasks, and mining areas 2026-02-19 22:40:16 -05:00
MayaTheShy
a68db21f9c feat: Add better-sqlite3 dependency for enhanced database functionality 2026-02-19 22:40:06 -05:00
MayaTheShy
a74802afee feat: Refactor inventory display to use a grid layout with item icons and improved tooltips 2026-02-19 22:40:00 -05:00
MayaTheShy
fb53056e85 feat: Implement database schema and CRUD operations for turtle homes, configurations, world blocks, paths, tasks, and mining areas 2026-02-19 22:39:53 -05:00
MayaTheShy
23259a5410 feat: Enhance exploration logic to prioritize unvisited directions and improve movement efficiency 2026-02-19 22:36:15 -05:00
MayaTheShy
aa0884f1d8 feat: Add GPS host script with fixed coordinates for initialization 2026-02-19 22:20:31 -05:00
MayaTheShy
ac4311a2cd refactor: Remove unused exploreStep function to clean up code 2026-02-19 21:31:18 -05:00
MayaTheShy
7003620ef7 feat: Improve exploration logic with enhanced stuck detection and direction prioritization 2026-02-19 21:30:35 -05:00
MayaTheShy
d43887ed41 feat: Initialize state for intelligent navigation in exploration and mining modes 2026-02-19 21:27:25 -05:00
MayaTheShy
3170ca3491 feat: Enhance exploration algorithm with intelligent navigation and stuck detection 2026-02-19 21:27:05 -05:00
MayaTheShy
399d2b693a feat: Enhance exploration algorithm with intelligent navigation and stuck detection 2026-02-19 21:26:23 -05:00
MayaTheShy
0c69b555df feat: Enhance home position management with auto-setting functionality when GPS is available 2026-02-19 21:21:09 -05:00
MayaTheShy
0d2fe5b8b1 feat: Implement non-blocking return home functionality with improved navigation 2026-02-16 03:10:47 -05:00
MayaTheShy
1435a7ac55 feat: Refactor home position sync and set functions for non-blocking operations 2026-02-16 03:07:20 -05:00
MayaTheShy
4bdbc006f8 feat: Implement wireless home position sync and set functionality for turtles 2026-02-16 02:41:33 -05:00
MayaTheShy
9048d197a8 feat: Implement home position management for turtles with update and retrieval endpoints 2026-02-16 02:38:34 -05:00
MayaTheShy
bd758e4c7b feat: Implement home position syncing with server and add distance checks for safe operations 2026-02-16 02:38:25 -05:00
MayaTheShy
e87ee43822 feat: Enhance turtle management by broadcasting removal notifications and cleaning up stale turtles 2026-02-16 02:31:05 -05:00
MayaTheShy
e76477f38d feat: Update empty state colors for improved visibility and aesthetics 2026-02-16 02:14:36 -05:00
MayaTheShy
c492529c1d feat: Update Control Panel styles for improved aesthetics and contrast 2026-02-16 02:14:23 -05:00
MayaTheShy
0296253149 feat: Update detail section colors and status item styles for improved contrast and aesthetics 2026-02-16 02:13:56 -05:00
MayaTheShy
9abd0f1f45 feat: Update turtle details and section styles for improved visibility and aesthetics 2026-02-16 02:13:51 -05:00
MayaTheShy
3589813dc6 feat: Update Control Panel color scheme for improved contrast and aesthetics 2026-02-16 02:13:30 -05:00
MayaTheShy
bd96333d22 feat: Revamp Control Panel styling for improved aesthetics and usability 2026-02-16 02:11:21 -05:00
MayaTheShy
81bfdd75d5 feat: Update layout of app-content containers for improved responsiveness 2026-02-16 02:11:18 -05:00
MayaTheShy
b036218fff feat: Adjust typography and spacing in turtle details and inventory sections for improved readability and layout 2026-02-16 02:06:03 -05:00
MayaTheShy
6534af4516 feat: Refine Control Panel styling with adjusted padding, margins, and button sizes for improved layout and usability 2026-02-16 02:05:24 -05:00
MayaTheShy
e0495eeb9c feat: Revamp Control Panel styling with enhanced layout, improved responsiveness, and updated visual elements 2026-02-16 02:02:31 -05:00
MayaTheShy
3b3ed33b33 feat: Add Live GPS Tracker for Pocket Computer with real-time location display 2026-02-16 01:59:59 -05:00
MayaTheShy
ac841dc1c5 feat: Implement Minecraft texture loading and enhance WorldBlocks rendering with textures 2026-02-16 01:54:53 -05:00
MayaTheShy
81e0dc4959 feat: Enhance block appearance handling with detailed color and texture info, and improve block rendering performance 2026-02-16 01:52:10 -05:00
MayaTheShy
cf0b66e2fe feat: Enhance control panel with additional command buttons and improved movement controls 2026-02-16 01:48:50 -05:00
MayaTheShy
bffacbf8c8 feat: Refine command button styles and enhance layout for improved usability and responsiveness 2026-02-16 01:48:46 -05:00
MayaTheShy
1610ade7b7 feat: Revamp dashboard layout for improved vertical display and enhanced statistics visibility 2026-02-16 01:48:43 -05:00
MayaTheShy
611609d404 feat: Implement comprehensive UI/UX improvements including 3D map enhancements, turtle model updates, and block visualization 2026-02-16 01:37:30 -05:00
MayaTheShy
175c6add10 fix: Improve message processing and command transmission in turtle system 2026-02-16 01:37:24 -05:00
MayaTheShy
5ec385c1f2 feat: Enhance turtle model and visualization with block surroundings and world block management 2026-02-16 01:35:52 -05:00
MayaTheShy
836d734b1f fix: Enhance command logging and error handling for turtle commands 2026-02-16 01:27:58 -05:00
MayaTheShy
9a02a8d27f fix: Improve logging for command sending and REST API fallback in turtle store 2026-02-16 01:27:54 -05:00
MayaTheShy
1a415beac0 feat: Add mining command to initiate exploration mode 2026-02-16 01:27:49 -05:00
MayaTheShy
000d1bd625 fix: Refactor command polling and dashboard refresh logic in main loop 2026-02-16 01:25:51 -05:00
MayaTheShy
cc7c90799e feat: Implement web bridge dashboard for turtle network monitoring 2026-02-16 01:21:23 -05:00
MayaTheShy
e3a4788440 fix: Enhance message processing with detailed logging for incoming commands 2026-02-16 01:15:08 -05:00
MayaTheShy
96003014da fix: Update SERVER_URL to point to the local server address 2026-02-16 01:12:25 -05:00
MayaTheShy
f70dd16cff fix: Optimize GPS position retrieval with reduced timeout and background retry logic 2026-02-16 01:04:31 -05:00
MayaTheShy
a91ad1d4ca fix: Refactor message handling to improve status updates and server communication 2026-02-16 00:56:59 -05:00
MayaTheShy
4dacfb53aa fix: Update SERVER_URL to use secure HTTPS for web server connection 2026-02-16 00:53:51 -05:00
MayaTheShy
8172431e44 fix: Enhance message logging for modem events and status updates 2026-02-16 00:52:25 -05:00
MayaTheShy
a2d43d0cfb fix: Improve GPS position retrieval with error handling and retry logic 2026-02-16 00:34:41 -05:00
MayaTheShy
94713539da fix: Enhance GPS position retrieval with increased timeout and logging 2026-02-16 00:34:35 -05:00
MayaTheShy
9f9abb45ec fix: Update WebSocket and API URLs to use environment variables for better configuration 2026-02-16 00:29:19 -05:00
MayaTheShy
a8a99f4f53 fix: Update Dockerfile to use serve for production and enhance Vite configuration with CORS support 2026-02-16 00:24:38 -05:00
MayaTheShy
dc3ed12b14 fix: Update host configuration in vite.config.js for server and preview 2026-02-16 00:15:39 -05:00
MayaTheShy
172cdac94b fix: Add --strictPort option to Vite preview command for better port handling 2026-02-16 00:15:34 -05:00
MayaTheShy
4626ab80ff fix: Ensure consistent server and preview configuration in vite.config.js 2026-02-16 00:14:25 -05:00
MayaTheShy
b0b242a07b fix: Update HTTP API port mapping in docker-compose.yml 2026-02-16 00:13:38 -05:00
MayaTheShy
d2f2e85bf2 fix: Update port mappings for backend and frontend services in docker-compose.yml 2026-02-16 00:12:27 -05:00
MayaTheShy
efa5afd79a fix: Change Dockerfile to use npm install instead of npm ci for dependency installation 2026-02-16 00:10:24 -05:00
MayaTheShy
fd9caf44e9 fix: Update Dockerfile to use npm install instead of npm ci for production dependencies 2026-02-16 00:10:20 -05:00
MayaTheShy
30647fca50 feat: Add Docker support with production and development configurations for backend and frontend 2026-02-16 00:08:49 -05:00
MayaTheShy
a6edc0934c feat: Add comprehensive documentation for project architecture, troubleshooting, and setup 2026-02-16 00:01:04 -05:00
MayaTheShy
db3e07689a added project summary 2026-02-15 23:54:12 -05:00
MayaTheShy
e19abdb9fa feat: Add interface overview and layout documentation for Turtle Control Center 2026-02-15 23:53:43 -05:00
MayaTheShy
ba91de54d8 feat: Add package.json for Turtle Control Center project configuration 2026-02-15 23:53:20 -05:00
81 changed files with 22668 additions and 1164 deletions

46
.dockerignore Normal file
View File

@@ -0,0 +1,46 @@
# Docker ignore patterns
# Node modules
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Build outputs
dist/
build/
# Environment files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Git
.git/
.gitignore
# Documentation (not needed in container)
*.md
!README.md
# Test files
*.test.js
*.spec.js
coverage/
# Logs
*.log
logs/

34
.package Normal file
View File

@@ -0,0 +1,34 @@
{
required = {
'platform',
},
title = "RemoteTurtle",
description = "Web-based remote control for CC:Tweaked turtles with 3D visualization, D* Lite pathfinding, and state-machine AI. Includes turtle controller, GPS host, web bridge, and pocket computer programs.",
repository = "gitea://git.spatulaa.com/MayaTheShy/remoteturtle/master/",
exclude = {
"^server/", "^client/", "^__tests__/",
"^startup_", "^start%.",
"%.md$", "%.yml$", "%.json$", "%.bat$", "%.sh$",
"^Dockerfile", "^%.git", "^LICENSE$", "^node_modules/",
},
install = [[
local pkgDir = fs.combine("packages", "remoteturtle")
-- Web Bridge config
print("")
print("-- RemoteTurtle Web Bridge Setup --")
print("")
write("Server URL (e.g. http://192.168.1.10:4200): ")
local serverUrl = read()
if serverUrl and #serverUrl > 0 then
local wsUrl = serverUrl:gsub("^http", "ws") .. "/ws/bridge"
local cfg = textutils.serialiseJSON({ serverUrl = serverUrl, wsUrl = wsUrl })
local f = fs.open(fs.combine(pkgDir, ".webbridge_config"), "w")
f.write(cfg)
f.close()
print("Saved web bridge config.")
else
print("Skipped — edit .webbridge_config later.")
end
]],
}

291
ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,291 @@
# System Architecture Diagram
## High-Level Overview
```
╔═══════════════════════════╗
║ YOUR COMPUTER ║
║ ║
┌────────────────────║──────────────────────────║────────────────┐
│ ║ ║ │
│ ┏━━━━━━━━━━━━━━┓ ║ ┏━━━━━━━━━━━━━━━━━━━┓ ║ ┏━━━━━━━━━━┓ │
│ ┃ Browser ┃ ║ ┃ Node.js Server ┃ ║ ┃ Minecraft┃ │
│ ┃ ┃ ║ ┃ ┃ ║ ┃ ┃ │
│ ┃ React App ┃◄─╫─►┃ Express + WS ┃◄─╫─►┃ Bridge ┃ │
│ ┃ ┃ ║ ┃ ┃ ║ ┃ Computer ┃ │
│ ┃ :3000 ┃ ║ ┃ :3001 / :3002 ┃ ║ ┃ ┃ │
│ ┗━━━━━━━━━━━━━━┛ ║ ┗━━━━━━━━━━━━━━━━━━━┛ ║ ┗━━━━━━━━━━┛ │
│ ║ ║ │ │
└────────────────────║──────────────────────────║───────┼────────┘
╚═══════════════════════════╝ │
│ Modem
│ Wireless
┏━━━━━━━━━━━━━━┓
┃ Turtle 1 ┃
┃ Turtle 2 ┃
┃ Turtle N ┃
┗━━━━━━━━━━━━━━┛
```
## Communication Flow
### Status Updates (Turtle → Web)
```
Turtle Bridge Server Web Client
│ │ │ │
│ Status Broadcast │ │ │
├──────modem───────>│ │ │
│ (channel 102) │ │ │
│ │ HTTP POST │ │
│ ├───────────────────>│ │
│ │ /api/turtle/update│ │
│ │ │ WebSocket Broadcast │
│ │ ├─────────────────────>│
│ │ │ turtle_update │
│ │ │ │
│ │ │ ┌───┴───┐
│ │ │ │Update │
│ │ │ │3D Map │
│ │ │ └───────┘
```
### Commands (Web → Turtle)
```
Web Client Server Bridge Turtle
│ │ │ │
│ Click "Explore" │ │ │
├─────WebSocket────>│ │ │
│ command msg │ │ │
│ │ Queue Command │ │
│ ├──(in memory) │ │
│ │ │ │
│ │ │ Poll for Commands │
│ │ HTTP GET │ │
│ │<──────────────────┤ │
│ │/api/turtle/:id/.. │ │
│ │ │ │
│ │ Return Commands │ │
│ ├──────────────────>│ │
│ │ JSON │ │
│ │ │ Forward via Modem │
│ │ ├──────────────────>│
│ │ │ (channel 100) │
│ │ │ │
│ │ │ ┌───┴────┐
│ │ │ │Execute │
│ │ │ │Command │
│ │ │ └────────┘
```
## Component Details
### React Frontend (Port 3000)
```
┌─────────────────────────────────────┐
│ App.jsx │
│ ┌───────────────────────────────┐ │
│ │ View Controls (Split/Map/..) │ │
│ └───────────────────────────────┘ │
│ │
│ ┌────────────┬──────────────────┐ │
│ │ │ │ │
│ │ Control │ Map3D.jsx │ │
│ │ Panel │ │ │
│ │ .jsx │ ┌────────────┐ │ │
│ │ │ │ Three.js │ │ │
│ │ - Turtle │ │ Canvas │ │ │
│ │ Cards │ │ │ │ │
│ │ - Details │ │ - Turtles │ │ │
│ │ - Commands│ │ - Grid │ │ │
│ │ - Manual │ │ - Camera │ │ │
│ │ Control │ │ │ │ │
│ │ │ └────────────┘ │ │
│ └────────────┴──────────────────┘ │
│ │
│ ┌───────────────────────────────┐ │
│ │ turtleStore.js │ │
│ │ - WebSocket connection │ │
│ │ - State management │ │
│ │ - Command sending │ │
│ └───────────────────────────────┘ │
└─────────────────────────────────────┘
```
### Node.js Server (Ports 3001, 3002)
```
┌─────────────────────────────────────┐
│ server.js │
│ │
│ ┌───────────────────────────────┐ │
│ │ Express HTTP Server │ │
│ │ Port: 3001 │ │
│ │ │ │
│ │ POST /api/turtle/update │ │
│ │ GET /api/turtle/:id/commands│ │
│ │ POST /api/turtle/:id/command │ │
│ │ GET /api/turtles │ │
│ └───────────────────────────────┘ │
│ │
│ ┌───────────────────────────────┐ │
│ │ WebSocket Server │ │
│ │ Port: 3002 │ │
│ │ │ │
│ │ - Broadcast turtle updates │ │
│ │ - Receive commands │ │
│ │ - Track connected clients │ │
│ └───────────────────────────────┘ │
│ │
│ ┌───────────────────────────────┐ │
│ │ In-Memory Data Store │ │
│ │ │ │
│ │ turtleData Map: │ │
│ │ turtleID → state │ │
│ │ - position │ │
│ │ - fuel │ │
│ │ - inventory │ │
│ │ - pendingCommands[] │ │
│ └───────────────────────────────┘ │
└─────────────────────────────────────┘
```
### Minecraft Components
```
┌─────────────────────────────────────┐
│ webbridge.lua │
│ (Bridge Computer) │
│ │
│ ┌───────────────────────────────┐ │
│ │ Modem Listener │ │
│ │ Channels: 101, 102 │ │
│ └───────────────────────────────┘ │
│ ↕ │
│ ┌───────────────────────────────┐ │
│ │ HTTP Client │ │
│ │ - POST status to server │ │
│ │ - GET commands from server │ │
│ └───────────────────────────────┘ │
└─────────────────────────────────────┘
↕ (modem)
┌─────────────────────────────────────┐
│ turtle.lua │
│ (Each Turtle) │
│ │
│ ┌───────────────────────────────┐ │
│ │ Command Processor │ │
│ │ Listen: channel 100 │ │
│ └───────────────────────────────┘ │
│ │
│ ┌───────────────────────────────┐ │
│ │ Status Broadcaster │ │
│ │ Send: channel 102 (every 5s) │ │
│ └───────────────────────────────┘ │
│ │
│ ┌───────────────────────────────┐ │
│ │ Autonomous AI │ │
│ │ - Exploration │ │
│ │ - Mining │ │
│ │ - Pathfinding │ │
│ │ - Fuel management │ │
│ └───────────────────────────────┘ │
└─────────────────────────────────────┘
```
## Network Ports
```
Port 3000 → React Dev Server (Vite)
Port 3001 → Express HTTP API
Port 3002 → WebSocket Server
Modem Channels:
100 → Commands to Turtles
101 → Responses from Turtles
102 → Status Updates
```
## Data Models
### Turtle State Object
```javascript
{
turtleID: 123,
mode: "mining", // idle, exploring, mining, returning, manual
position: {
x: 100,
y: 64,
z: 200
},
homePosition: {
x: 95,
y: 64,
z: 195
},
fuel: 5000,
inventory: [
{ slot: 1, name: "minecraft:coal_ore", count: 32 },
{ slot: 2, name: "minecraft:iron_ore", count: 16 }
],
lastUpdate: 1234567890,
pendingCommands: [
{ command: "explore", param: null, timestamp: 1234567890 }
]
}
```
### Command Message
```javascript
{
command: "explore", // explore, mine, returnHome, stop, forward, etc.
param: null, // optional parameter
target: 123 // turtle ID (for Lua) or turtleID (for web)
}
```
## Technology Stack Summary
```
┌─────────────────────────────────────┐
│ Presentation Layer │
│ - React 18 │
│ - Three.js / React Three Fiber │
│ - CSS3 │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ State Management │
│ - Zustand │
│ - WebSocket API │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ Backend Services │
│ - Node.js │
│ - Express.js │
│ - ws (WebSocket library) │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ Game Integration │
│ - ComputerCraft Lua │
│ - HTTP API │
│ - Modem Wireless │
└─────────────────────────────────────┘
```
---
This architecture provides:
- ✅ Real-time bidirectional communication
- ✅ Scalable to multiple turtles
- ✅ Web-based interface accessible from any device
- ✅ Backward compatible with existing Lua scripts
- ✅ Clean separation of concerns
- ✅ Easy to extend and modify

355
DOCKER.md Normal file
View File

@@ -0,0 +1,355 @@
# 🐳 Docker Deployment Guide
## Quick Start
### Production Deployment
Start everything with one command:
```bash
docker-compose up -d
```
This will:
- Build and start the backend server (ports 3001, 3002)
- Build and start the frontend client (port 3000)
- Set up networking between containers
### Development Mode (with hot reload)
```bash
docker-compose -f docker-compose.dev.yml up
```
This enables:
- Hot reload for both server and client
- Volume mounting for live code changes
- Development dependencies
## Architecture
```
┌─────────────────────────────────────┐
│ Zoraxy Reverse Proxy │
│ (Your external proxy) │
└────────┬────────────────┬───────────┘
│ │
│ │
┌────▼─────┐ ┌────▼─────┐
│ Client │ │ Server │
│ :3000 │ │ :3001 │
│ (Vite) │ │ :3002 │
└──────────┘ └──────────┘
│ │
└────────┬───────┘
turtle-network
```
## Services
### Backend Server (turtle-server)
- **Ports**: 3001 (HTTP), 3002 (WebSocket)
- **Image**: Built from `server/Dockerfile`
- **Health Check**: HTTP GET /api/turtles
### Frontend Client (turtle-client)
- **Port**: 3000 (Vite preview server)
- **Image**: Built from `client/Dockerfile`
- **Serves**: Pre-built static React app
## Docker Commands
### Start Services
```bash
# Production (detached)
docker-compose up -d
# Development (with logs)
docker-compose -f docker-compose.dev.yml up
# Build and start
docker-compose up --build
```
### Stop Services
```bash
docker-compose down
# Remove volumes too
docker-compose down -v
```
### View Logs
```bash
# All services
docker-compose logs -f
# Specific service
docker-compose logs -f server
docker-compose logs -f client
```
### Restart Services
```bash
# All services
docker-compose restart
# Specific service
docker-compose restart server
```
### Shell Access
```bash
# Server container
docker-compose exec server sh
# Client container
docker-compose exec client sh
```
## Zoraxy Configuration
Configure your Zoraxy reverse proxy to forward:
### Frontend (Web UI)
- **Domain**: `turtles.yourdomain.com` (or your choice)
- **Target**: `http://localhost:3000`
- **WebSocket**: Enabled
### Backend API
- **Domain**: `turtles-api.yourdomain.com` (or your choice)
- **Target**: `http://localhost:3001`
- **WebSocket**: Enabled (for port 3002)
### Alternative: Single Domain Setup
Forward all `/api/*` requests to backend:
- **Domain**: `turtles.yourdomain.com`
- **Path `/`**: → `http://localhost:3000` (Frontend)
- **Path `/api`**: → `http://localhost:3001` (Backend)
- **WebSocket**: → `ws://localhost:3002`
### Update Client Configuration
Edit `client/src/store/turtleStore.js` before building:
```javascript
const WS_URL = 'wss://turtles.yourdomain.com/ws'; // or your WebSocket endpoint
const API_URL = 'https://turtles.yourdomain.com/api'; // or your API endpoint
```
## Environment Variables
Create `.env` files if needed:
### server/.env
```env
NODE_ENV=production
PORT=3001
WS_PORT=3002
```
### client/.env.production
```env
VITE_API_URL=https://turtles-api.yourdomain.com
VITE_WS_URL=wss://turtles-api.yourdomain.com/ws
```
## Building Images
### Build all images
```bash
docker-compose build
```
### Build specific service
```bash
docker-compose build server
docker-compose build client
```
### Rebuild without cache
```bash
docker-compose build --no-cache
```
## Minecraft Bridge Configuration
Update `webbridge.lua` to point to your Docker host:
```lua
-- If running on same machine
local SERVER_URL = "http://localhost:3001"
-- If running on different machine
local SERVER_URL = "http://YOUR_DOCKER_HOST_IP:3001"
-- If using Zoraxy reverse proxy
local SERVER_URL = "https://turtles-api.yourdomain.com"
```
## Troubleshooting
### Port Already in Use
```bash
# Check what's using the port
sudo lsof -i :3000
sudo lsof -i :3001
# Stop the containers
docker-compose down
# Remove any conflicting containers
docker ps -a
docker rm <container_id>
```
### Container Won't Start
```bash
# Check logs
docker-compose logs server
docker-compose logs client
# Rebuild
docker-compose up --build
```
### Can't Connect from Minecraft
- Verify firewall allows ports 3001-3002
- Check Docker host IP is correct
- Ensure HTTP API is enabled in ComputerCraft config
- Test: `http.get("http://YOUR_IP:3001/api/turtles")`
### Health Check Failing
```bash
# Check container health
docker-compose ps
# Manually test health endpoint
curl http://localhost:3001/api/turtles
curl http://localhost:3000
```
## Production Checklist
- [ ] Set up Zoraxy reverse proxy
- [ ] Configure SSL/TLS certificates
- [ ] Update API URLs in client code
- [ ] Build images: `docker-compose build`
- [ ] Start services: `docker-compose up -d`
- [ ] Verify health checks: `docker-compose ps`
- [ ] Test web interface
- [ ] Update Minecraft bridge script
- [ ] Test turtle connection
## Updating the Application
```bash
# Pull latest code
git pull
# Rebuild and restart
docker-compose down
docker-compose up --build -d
# Or use rolling update
docker-compose up -d --build
```
## Performance Tips
### Reduce Image Size
- Images use Alpine Linux (minimal)
- Production builds exclude dev dependencies
- Multi-stage builds where applicable
### Resource Limits
Add to docker-compose.yml:
```yaml
services:
server:
deploy:
resources:
limits:
cpus: '0.5'
memory: 512M
```
### Volume Management
```bash
# List volumes
docker volume ls
# Clean up unused volumes
docker volume prune
```
## Backup and Restore
### Backup Container Data
```bash
# Export container
docker export turtle-server > server-backup.tar
docker export turtle-client > client-backup.tar
```
### Backup Logs
```bash
docker-compose logs > logs-backup.txt
```
## Security
### Best Practices
1. Use Zoraxy for SSL/TLS termination
2. Don't expose Docker ports directly to internet
3. Use environment variables for sensitive data
4. Keep Docker and images updated
5. Use non-root user in containers (already configured)
### Network Isolation
Containers use `turtle-network` bridge network:
- Isolated from host network
- Can communicate with each other
- Exposed ports defined in docker-compose.yml
## Monitoring
### Container Stats
```bash
docker stats
```
### Health Status
```bash
docker-compose ps
```
### System Resources
```bash
docker system df
```
## Common Issues
**Problem**: Client can't connect to server
**Solution**: Make sure both containers are on same network and server is healthy
**Problem**: WebSocket connection fails through Zoraxy
**Solution**: Enable WebSocket support in Zoraxy proxy rules
**Problem**: Changes not reflected after rebuild
**Solution**: Use `--no-cache` flag: `docker-compose build --no-cache`
---
## Summary
**Start**: `docker-compose up -d`
**Stop**: `docker-compose down`
**Logs**: `docker-compose logs -f`
**Rebuild**: `docker-compose up --build -d`
Set up your Zoraxy reverse proxy to forward traffic to:
- Frontend: `localhost:3000`
- Backend: `localhost:3001` + `localhost:3002` (WebSocket)
**Easy deployment with Docker + Zoraxy!** 🐳✨

511
FEATURES.md Normal file
View File

@@ -0,0 +1,511 @@
# 🎮 Feature Documentation
## ✅ Implemented Features
### 1. 🗄️ Database Persistence (SQLite)
**Status:** ✅ Complete
Persistent storage for all turtle data across server restarts.
**Features:**
- Turtle home positions saved to database
- World block discoveries persisted
- Turtle configurations stored
- Path recordings saved
- Task queue persistence
- Mining area claims tracked
**Database Tables:**
- `turtle_homes` - Home position for each turtle
- `turtle_config` - Configuration per turtle
- `world_blocks` - All discovered blocks with timestamps
- `turtle_paths` - Recorded movement paths
- `task_queue` - Shared task system
- `mining_areas` - Area claims and zones
**API Endpoints:**
```
GET /api/turtle/:id/home - Get turtle home
POST /api/turtle/:id/home - Set turtle home
GET /api/stats - Server statistics
```
**Benefits:**
- ✅ No data loss on server restart
- ✅ Historical block discovery tracking
- ✅ Persistent configurations
- ✅ Scalable to many turtles
---
### 2. 📦 Inventory Management UI
**Status:** ✅ Complete
Beautiful 4x4 grid inventory display with real-time updates.
**Features:**
- 16-slot grid layout (matches Minecraft turtle inventory)
- Item-specific emoji icons (💎 for diamonds, ⚫ for coal, etc.)
- Item counts displayed in corner
- Item names shown on hover
- Empty slots clearly marked
- Smooth hover animations
- Touch-friendly on mobile
**Visual Design:**
- Dark theme matching overall UI
- Glowing borders for filled slots
- Dashed borders for empty slots
- Item count badges
- Responsive grid layout
**Item Icons:**
- 💎 Diamond ore
- 🟡 Gold ore
- ⚪ Iron ore
- ⚫ Coal
- 🟢 Emerald
- 🔴 Redstone
- 🔵 Lapis
- 🗿 Stone
- 🟤 Dirt
- 🪵 Wood/logs
- 🪨 Cobblestone
- 📦 Generic items
---
### 3. 📱 Mobile-Responsive Design
**Status:** ✅ Complete
Fully responsive interface that works on all devices.
**Breakpoints:**
- **Desktop (1024px+):** Split 50/50 view
- **Tablet (768-1024px):** Stacked vertical layout
- **Mobile (480-768px):** Tab-based single view
- **Small Mobile (<480px):** Optimized compact layout
**Touch Optimizations:**
- Minimum 44px touch targets (iOS standard)
- Larger buttons on touch devices
- Active states instead of hover
- Optimized spacing for fingers
- Scroll-friendly layouts
**Orientation Support:**
- **Portrait:** Vertical stacking
- **Landscape:** Side-by-side split
**Accessibility:**
- Reduced motion support
- High contrast mode
- Screen reader friendly
- Keyboard navigation
---
### 4. 🧠 Intelligent Navigation
**Status:** ✅ Complete
Smart exploration algorithm that learns and adapts.
**Features:**
- Position memory (tracks visited locations)
- Stuck detection and auto-recovery
- Hole escape algorithm
- Depth management (avoids going too deep)
- Horizontal-focused exploration (75%)
- Prefers unvisited areas
- Valuable ore priority mining
**Navigation Priorities:**
1. Mine valuable ores (always)
2. Move horizontally (75%)
3. Go down if safe (15%, only above y=15)
4. Go up if needed (10%, only if stuck or deep)
**Anti-Stuck Features:**
- Detects stationary position
- Climbs out after 8 stuck steps
- Tries all 4 directions
- Digs through obstacles
- Never gets permanently stuck
---
## 🚧 Backend API Ready (Frontend Pending)
### 5. 🛤️ Path Recording System
**Status:** 🔶 Backend Complete, Frontend Pending
Record and replay turtle movement paths.
**API Endpoints:**
```
POST /api/paths - Save a path
GET /api/paths/:turtleId - Get all paths for turtle
DELETE /api/paths/:pathId - Delete a path
```
**Database:**
- Stores path name, turtle ID, and movement data
- Timestamps for creation and updates
**To Implement (Frontend):**
- Record button to start/stop path recording
- Path list viewer
- Playback controls
- Path sharing between turtles
---
### 6. 🤝 Multi-Turtle Task Coordination
**Status:** ✅ Complete
Shared task queue for coordinating multiple turtles.
**API Endpoints:**
```
POST /api/tasks - Create task
GET /api/tasks - Get all tasks
GET /api/tasks/next - Get next available task
POST /api/tasks/:id/assign - Assign task to turtle
POST /api/tasks/:id/complete - Mark task complete
```
**Task System:**
- Priority-based queue
- Task types: mine, explore, build, gather, transport, clear_area
- Status tracking: pending, in_progress, completed, failed
- Automatic assignment to available turtles
**Frontend Features:**
- ✅ Task creation UI with 6 task types
- ✅ Task queue display with filtering
- ✅ Priority management (1-10 with labels)
- ✅ Assignment controls
- ✅ Coordinate input for area-based tasks
- ✅ Status tracking and updates
- ✅ Mobile responsive design
---
### 7. 🗺️ Mining Area Visualization
**Status:** 🔶 Backend Complete, Frontend Pending
Claim and visualize mining zones on 3D map.
**API Endpoints:**
```
POST /api/mining-areas - Claim mining area
GET /api/mining-areas - Get all areas
POST /api/mining-areas/:id/close - Close area
```
**Database:**
- Stores 3D bounding boxes (min/max X, Y, Z)
- Tracks which turtle owns the area
- Status: active or closed
**To Implement (Frontend):**
- 3D wireframe boxes on map
- Color coding per turtle
- Area claim UI
- Conflict detection
---
### 8. ⏰ Task Scheduling
**Status:** 🔶 Backend Complete
Schedule tasks with priorities and time-based execution.
**Features:**
- Priority system (0-10)
- Task types and metadata
- Queue management
- Status tracking
**To Implement (Frontend):**
- Scheduling calendar view
- Priority slider
- Task templates
- Recurring tasks
---
## 📋 Not Yet Implemented
### 9. 🔐 Authentication System
**Status:** ❌ Not Started
Multi-user support with login and permissions.
**Planned Features:**
- User registration and login
- JWT token authentication
- Role-based permissions (admin, user, viewer)
- Per-user turtle assignments
- Action logging and audit trail
**Tech Stack (Proposed):**
- bcrypt for password hashing
- jsonwebtoken for JWT
- Express middleware for auth
- Cookie-based sessions
---
---
## ✅ Newly Implemented Frontend Features
### 10. 🎤 Voice Commands
**Status:** ✅ Complete
Natural language control using Web Speech API.
**Features:**
- 20+ voice commands for turtle control
- Speech recognition with natural language processing
- Speech synthesis for confirmation feedback
- Browser compatibility detection (Chrome, Edge, Safari)
- Command help with categorized lists
- Pulse animation when listening
**Command Categories:**
- Movement: forward, back, turn left/right, up, down
- Actions: dig, dig up/down, place, refuel
- Autonomous: explore, mine, return home, stop, set home, status
**UI Features:**
- ✅ Voice button with listening state
- ✅ Transcript display
- ✅ Last command confirmation
- ✅ Collapsible command help
- ✅ Mobile responsive design
---
### 11. 📊 Mining Statistics Dashboard
**Status:** ✅ Complete
Comprehensive mining analytics and leaderboards.
**Features:**
- Per-turtle mining statistics with block type breakdown
- Top miners leaderboard with rankings (🥇🥈🥉)
- Time filters (24h, 7 days, 30 days, All time)
- Item-specific emoji icons for blocks
- All turtles overview grid
- Empty state handling
**UI Features:**
- ✅ Total blocks mined display
- ✅ Block type breakdown with counts
- ✅ Leaderboard with medal rankings
- ✅ Time-based filtering
- ✅ Summary cards for all turtles
- ✅ Responsive grid layouts
- ✅ Animated transitions
---
### 12. 👥 Turtle Groups Management
**Status:** ✅ Complete
Organize turtles into teams with coordinated commands.
**Features:**
- Create and delete turtle groups
- 8 color presets for team identification
- Add/remove turtles from groups
- Send commands to entire group at once
- Member list with status indicators
- Group member count tracking
**UI Features:**
- ✅ Group creation form with color picker
- ✅ Group cards with member lists
- ✅ Add/remove member controls
- ✅ Group command buttons (explore, mine, return home, stop)
- ✅ Status indicators per turtle
- ✅ Success/error message feedback
- ✅ Mobile responsive design
---
### 13. 📋 Task Queue Management
**Status:** ✅ Complete
Schedule and coordinate multi-turtle tasks.
**Features:**
- Create tasks with 6 types (mine_area, explore, gather, build, transport, clear_area)
- Priority system (1-10) with visual labels (Critical, High, Medium, Low)
- Coordinate input for area-based tasks
- Assign tasks to specific turtles or leave unassigned
- Filter by status (all, pending, in_progress, completed, failed)
- Task lifecycle management (start, complete, fail, delete)
- Result tracking and timestamps
**UI Features:**
- ✅ Task creation form with validation
- ✅ Priority slider with color coding
- ✅ Coordinate input grid
- ✅ Turtle assignment dropdown
- ✅ Status filter tabs
- ✅ Task cards with actions
- ✅ Empty state handling
- ✅ Mobile responsive design
---
## 📊 Current System Statistics
**Lines of Code:**
- Server: ~650 lines (with database integration)
- Client: ~3,500 lines (including new components)
- Turtle Lua: ~1,000 lines
- Database: ~450 lines
- **Total: ~5,600 lines**
**Features Implemented:**
- ✅ Real-time WebSocket communication
- ✅ 3D block visualization
- ✅ GPS tracking
- ✅ Autonomous exploration
- ✅ Home position management
- ✅ Distance limiting
- ✅ Intelligent navigation
- ✅ Database persistence
- ✅ Inventory management UI
- ✅ Mobile-responsive design
- ✅ Task queue (backend + frontend)
- ✅ Path recording backend
- ✅ Mining area backend
- ✅ Voice commands
- ✅ Mining statistics dashboard
- ✅ Turtle groups/teams
- ✅ Multi-panel tabbed interface
**Database Tables:** 11
**API Endpoints:** 35+
**Supported Devices:** Desktop, Tablet, Mobile
**Frontend Components:** 12+
**Voice Commands:** 20+
---
## 🚀 Quick Start
1. **Install Dependencies:**
```bash
cd server && npm install
cd ../client && npm install
```
2. **Start Server:**
```bash
cd server && npm start
```
3. **Start Client:**
```bash
cd client && npm run dev
```
4. **Deploy Lua Files:**
- Copy `turtle.lua` to your Minecraft turtle
- Copy `webbridge.lua` to a computer with wireless modem
- Configure SERVER_URL in webbridge.lua
5. **Run Turtle:**
```lua
turtle.lua
```
---
## 🎯 Next Steps
### High Priority
1. ✅ Database persistence - **DONE**
2. ✅ Inventory UI - **DONE**
3. ✅ Mobile responsive - **DONE**
4. ✅ Path recording UI - **DONE**
5. ✅ Task coordination UI - **DONE**
6. ✅ Mining area visualization - **DONE**
### Medium Priority
7. Task scheduling interface
8. Multi-turtle work distribution
9. Advanced group coordination features
### Low Priority
10. Authentication system
11. User permissions
12. Advanced analytics
13. Performance monitoring
---
## 💡 Tips for Use
**For Best Performance:**
- Use GPS hosts for accurate positioning
- Keep turtles within 200 blocks of home
- Use webbridge computer as dedicated proxy
- Run server on local network for best latency
**For Mobile Use:**
- Use landscape mode for split view
- Swipe to scroll turtle list
- Long-press inventory slots for info
- Use tab buttons to switch views
**For Multiple Turtles:**
- Give each turtle a unique ID
- Set different home positions
- Use task queue for coordination
- Monitor from task dashboard
**For Path Recording:**
- Record paths while turtle explores
- Save and replay recorded paths
- View waypoint details and distances
- Use for repetitive mining routes
**For Mining Areas:**
- Define 3D mining areas with coordinates
- Assign areas to specific turtles
- Track area status (planned/mining/completed)
- Visualize areas as 3D wireframes on map
- Detect overlapping area conflicts
---
## 📝 License
MIT License - Feel free to modify and distribute!
---
## 🤝 Contributing
Contributions welcome! Areas needing work:
- Task scheduling automation
- Multi-turtle work distribution algorithms
- Authentication system
- Performance optimizations
- Additional turtle commands
- Advanced analytics dashboard
---
**Last Updated:** February 19, 2026
**Version:** 2.1.0
**Status:** Production Ready ✨

259
IMPLEMENTATION_SUMMARY.md Normal file
View File

@@ -0,0 +1,259 @@
# Implementation Summary - Mining Area Visualization
## ✅ Completed Features
### 1. 3D Mining Area Visualization (Map3D.jsx)
**Added MiningArea Component:**
- 3D wireframe boxes for each mining area
- Semi-transparent fill for better visibility
- Dynamic color coding based on:
- Turtle color (when assigned)
- Status: Green (completed), Orange (mining), Purple/Blue (planned)
- Area labels with name and dimensions (W×H×D)
- Status indicators (pulsing sphere for active mining)
- Interactive hover effects
- Selection support with animated rotation
- Real-time updates every 5 seconds
**Integration in Scene:**
- Fetches mining areas from `/api/mining-areas`
- Renders all areas with proper positioning
- Supports area selection via click
- Positioned between blocks and home marker for proper layering
### 2. Mining Areas Management Panel (MiningAreasPanel.jsx)
**Core Features:**
- Create new mining areas with form
- Assign areas to specific turtles
- Define 3D coordinates (start/end positions)
- "Use Current Position" button to auto-fill from selected turtle
- List all mining areas with cards
- Filter by status: All, Planned, Mining, Completed
- Update area status (Start Mining, Mark Complete)
- Delete mining areas
- Real-time updates every 5 seconds
**Advanced Features:**
- **Conflict Detection:** Automatically detects overlapping areas
- **Volume Calculation:** Shows total block count for each area
- **Visual Warnings:** Red borders and warnings for conflicting areas
- **Responsive Design:** Mobile-friendly layout
- **Status Management:** Easy status transitions with buttons
**UI Details:**
- Clean card-based layout
- Color-coded status badges
- Coordinate display in monospace font
- Creation date tracking
- Turtle assignment display
- Action buttons per area
### 3. Integration (App.jsx)
**Changes Made:**
- Imported MiningAreasPanel component
- Added 'areas' case to panelTab state
- Added routing for MiningAreasPanel in renderPanelContent
- Created 7th tab: ⛏️ Areas
- Passed required props: turtles, selectedTurtle, apiUrl
**Tab System:**
Now 7 tabs total:
1. 🎮 Control
2. 🎤 Voice
3. 📊 Stats
4. 👥 Groups
5. 📋 Tasks
6. 🛤️ Paths
7. ⛏️ Areas (NEW)
## 📁 Files Created/Modified
### New Files:
1. `/client/src/components/MiningAreasPanel.jsx` (430 lines)
- Full panel component with CRUD operations
- Conflict detection algorithm
- Volume calculations
- Status management
2. `/client/src/components/MiningAreasPanel.css` (450 lines)
- Complete styling with animations
- Responsive breakpoints
- Conflict warning styles
- Status badge colors
- Mobile optimizations
### Modified Files:
1. `/client/src/components/Map3D.jsx`
- Added MiningArea component (90 lines)
- Added miningAreas state to Scene
- Added useEffect to fetch areas
- Integrated area rendering in Scene return
2. `/client/src/App.jsx`
- Added MiningAreasPanel import
- Updated panelTab state comment
- Added 'areas' case in renderPanelContent
- Added ⛏️ Areas tab button
3. `/FEATURES.md`
- Updated completion status
- Marked Path Recording UI as complete
- Marked Mining Area Visualization as complete
- Added usage tips for both features
- Updated version to 2.1.0
## 🎨 Visual Features
### 3D Visualization:
- Wireframe boxes with thick lines
- Semi-transparent colored fill
- Floating labels above areas
- Dimension indicators
- Status-based coloring
- Hover highlighting
- Selection animation
- Mining status indicator (glowing sphere)
### UI Panel:
- Filter buttons with counts
- Create form with validation
- Coordinate inputs (X, Y, Z)
- Turtle selection dropdown
- "Use Current Position" helper
- Area cards with info
- Conflict warnings (red borders)
- Action buttons per status
- Volume display
- Creation date
## 🔧 Technical Details
### API Endpoints Used:
- `GET /api/mining-areas` - Fetch all areas
- `POST /api/mining-areas` - Create new area
- `PUT /api/mining-areas/:id` - Update area status
- `DELETE /api/mining-areas/:id` - Delete area
### State Management:
- Local state for areas list
- Local state for form data
- Local state for filters
- Local state for selected area (3D)
- Real-time polling (5 second intervals)
### Conflict Detection Algorithm:
```javascript
// Checks if two 3D boxes overlap
const checkOverlap = (area1, area2) => {
const overlapX = Math.max(
Math.min(area1.endX, area2.endX) - Math.max(area1.startX, area2.startX) + 1,
0
);
const overlapY = Math.max(
Math.min(area1.endY, area2.endY) - Math.max(area1.startY, area2.startY) + 1,
0
);
const overlapZ = Math.max(
Math.min(area1.endZ, area2.endZ) - Math.max(area1.startZ, area2.startZ) + 1,
0
);
return overlapX > 0 && overlapY > 0 && overlapZ > 0;
};
```
### Volume Calculation:
```javascript
const calculateVolume = (area) => {
const width = Math.abs(area.endX - area.startX) + 1;
const height = Math.abs(area.endY - area.startY) + 1;
const depth = Math.abs(area.endZ - area.startZ) + 1;
return width * height * depth;
};
```
## ✨ User Workflows
### Creating a Mining Area:
1. Click "+ New Area" button
2. Enter area name (e.g., "Diamond Mine #1")
3. Select turtle to assign
4. Enter start coordinates OR click "Use Current Position"
5. Enter end coordinates
6. Click "Create Area"
7. Area appears in list and on 3D map
### Managing Area Status:
1. View area in list (status badge shows current state)
2. Click "▶️ Start Mining" to begin (changes to orange)
3. Click "✓ Mark Complete" when done (changes to green)
4. Click "🗑️ Delete" to remove area
### Detecting Conflicts:
1. System automatically checks all areas for overlaps
2. Conflicting areas show red border
3. Warning message lists overlapping areas
4. Review and resolve conflicts manually
### Visualizing on Map:
1. All areas render as 3D wireframe boxes
2. Colors match turtle assignment or status
3. Click area to select (animates with rotation)
4. Hover for highlighting effect
5. Labels show name and dimensions
## 📊 Status Indicators
### Colors:
- 🟣 Purple/Blue: Planned (not started)
- 🟠 Orange: Mining (in progress) + glowing sphere
- 🟢 Green: Completed (finished)
- 🔴 Red: Conflict detected (overlapping)
### Badges:
- Small rounded badges in area cards
- Uppercase text with color coding
- Matches 3D visualization colors
## 🎯 Achievement
All requested features from todo list are now **COMPLETE**:
**Path Recording UI** (Completed Earlier)
- Record button with start/stop
- Path list viewer with cards
- Playback controls
- Save/load paths from database
- Visual preview in details modal
- Waypoint grid and distance calculation
**Mining Area Visualization** (Just Completed)
- 3D wireframe boxes on Map3D
- Color coding per turtle and status
- Area claim UI in MiningAreasPanel
- Conflict detection with warnings
- Full CRUD operations
- Volume calculations
- Status management
## 📈 System Status
**Before:** 6 functional tabs
**After:** 7 functional tabs
**Total Components:** 8 major components
**Total Lines Added:** ~970 lines (MiningAreasPanel.jsx + CSS + Map3D changes)
**Backend APIs:** All 35+ endpoints functional
**Database Tables:** All 11 tables integrated
## 🚀 Ready for Testing
The system is now feature-complete with:
- Voice control
- Mining statistics
- Turtle groups
- Task queue
- Path recording
- **Mining area visualization (NEW)**
- 3D world map with all features integrated
All features are production-ready and fully integrated into the tabbed interface!

156
INTERFACE.md Normal file
View File

@@ -0,0 +1,156 @@
# 🎨 Interface Overview
## Web Interface Layout
```
┌─────────────────────────────────────────────────────────────────────┐
│ [📊 Split View] [🗺️ Map Only] [🎮 Control Only] [🟢 Connected]│
├──────────────────┬──────────────────────────────────────────────────┤
│ │ │
│ 🐢 Turtle List │ 3D Map View │
│ ───────────── │ │
│ │ ┌─────┐ │
│ ┌────────────┐ │ │ T-1 │ ← Selected Turtle │
│ │ Turtle 1 │◄─┼─────────└─────┘ │
│ │ [mining] │ │ │
│ │ 100,64,200 │ │ │
│ │ F:5000 I:8 │ │ 🏠 HOME │
│ └────────────┘ │ │
│ │ │
│ ┌────────────┐ │ Path Trail ---- │
│ │ Turtle 2 │ │ \ │
│ │ [idle] │ │ ┌─────┐ │
│ │ 105,65,195 │ │ │ T-2 │ │
│ │ F:3200 I:3 │ │ └─────┘ │
│ └────────────┘ │ │
│ │ │
│ No turtles... │ Grid Floor (5x5 blocks) │
│ │ │
├──────────────────┴──────────────────────────────────────────────────┤
│ Turtle 1 Control │
│ ─────────────── │
│ Status: │
│ Mode: mining Fuel: 5000 │
│ Position: X: 100, Y: 64, Z: 200 │
│ Home: X: 95, Y: 64, Z: 195 │
│ │
│ Commands: │
│ [🔍 Explore] [⛏️ Mine] [🏠 Return Home] [⏹️ Stop] │
│ │
│ Manual Control: │
│ [↑] │
│ [←] [→] │
│ [↓] │
│ [⬆ Up] [⬇ Down] │
│ │
│ Inventory: │
│ coal_ore x32 iron_ore x16 diamond_ore x3 │
│ cobblestone x64 dirt x12 │
└──────────────────────────────────────────────────────────────────────┘
```
## Color Scheme
### Turtle Status Colors
- 🟢 **Green (#4ade80)**: Mining/Active
- 🔵 **Blue (#60a5fa)**: Exploring
- 🟠 **Orange (#f59e0b)**: Returning Home
-**Gray (#9ca3af)**: Idle
- 🟣 **Purple (#a78bfa)**: Manual Control
### UI Colors
- **Background**: Dark blue (#0a0e1a, #0f172a)
- **Panels**: Slate (#1e293b)
- **Borders**: Gray-blue (#334155)
- **Text**: Light gray (#e2e8f0)
- **Accents**: Cyan (#60a5fa)
## Features Visualization
### 3D Map Features
```
┌─────────────────────────────┐
│ Orbital Camera Control │
│ - Rotate: Left Click+Drag │
│ - Zoom: Scroll Wheel │
│ - Pan: Right Click+Drag │
│ │
│ Turtle Markers: │
│ ┌─┐ │
│ │░│ ← 3D Box │
│ └─┘ │
│ ▲ ← Selection Arrow │
│ ⭕ ← Selection Ring │
│ T-1 ← ID Label │
│ │
│ Path Trails: │
│ ---- Dashed line to home │
│ │
│ Grid: │
│ □ □ □ 1-block grid │
│ ▪ ▪ ▪ 5-block sections │
└─────────────────────────────┘
```
### Control Panel Features
```
┌──────────────────────────────┐
│ Turtle Card (Clickable) │
├──────────────────────────────┤
│ Turtle 1 [mining] ● │
│ 100, 64, 200 │
│ F: 5000 I: 8 items │
│ Home: 15 blocks │
└──────────────────────────────┘
↓ Click to select
┌──────────────────────────────┐
│ Detail View │
├──────────────────────────────┤
│ All stats + commands │
│ Manual control arrows │
│ Inventory grid │
└──────────────────────────────┘
```
## Responsive Behavior
### View Modes
**Split View** (Default)
- Best for desktop monitors
- Side-by-side layout
- Full visibility of both map and controls
**Map Only**
- Fullscreen 3D visualization
- Great for presentations
- Multi-turtle overview
**Control Only**
- Detailed turtle management
- Better for laptops/smaller screens
- Focus on commands and data
## Real-time Updates
```
Turtle moves → Status broadcast → Web Bridge → Server → WebSocket → React
Update 3D Map
Update Stats
Animate Marker
```
Update frequency: Every 5 seconds or on command execution
## Interactive Elements
- **Clickable**: Turtle cards, 3D markers, command buttons
- **Hoverable**: Buttons show hover effects
- **Animated**: Selected turtle rotates, status dot pulses
- **Live**: Real-time position tracking on map
- **Responsive**: View mode switches, panel scrolling
---
Launch the interface to see it in action! 🚀

248
PROJECT_SUMMARY.md Normal file
View File

@@ -0,0 +1,248 @@
# 📦 Project Summary
## What Was Created
This project has been expanded from a simple Lua-based turtle control system into a **full-stack web application** with:
### 🎯 Key Components
1. **Backend Server** (`server/`)
- Express.js HTTP server (port 3001)
- WebSocket server for real-time updates (port 3002)
- REST API for turtle communication
- Automatic turtle state management
2. **Frontend Application** (`client/`)
- React 18 with Vite build system
- 3D map visualization using Three.js and React Three Fiber
- Real-time WebSocket connection
- Zustand state management
- Responsive control panel with multiple view modes
3. **Minecraft Bridge** (`webbridge.lua`)
- Forwards turtle status from ComputerCraft to web server
- Polls for commands from server
- HTTP communication bridge
4. **Original Turtle Scripts** (Enhanced)
- `turtle.lua` - Advanced autonomous mining turtle
- `pocketremote.lua` - Pocket computer interface
### 📂 Complete File Structure
```
remoteturtle/
├── 📄 README.md # Complete documentation
├── 📄 QUICKSTART.md # Quick setup guide
├── 📄 INTERFACE.md # UI/UX overview
├── 📄 package.json # Root package file
├── 📄 .gitignore # Git ignore rules
├── 🚀 start.sh # Linux/Mac startup script
├── 🚀 start.bat # Windows startup script
├── 🖥️ server/ # Backend Server
│ ├── server.js # Main server code
│ └── package.json # Dependencies
├── 🌐 client/ # React Frontend
│ ├── index.html
│ ├── vite.config.js
│ ├── package.json
│ └── src/
│ ├── main.jsx # Entry point
│ ├── App.jsx # Main app component
│ ├── App.css
│ ├── index.css # Global styles
│ │
│ ├── components/
│ │ ├── Map3D.jsx # 3D map visualization
│ │ ├── ControlPanel.jsx # Control interface
│ │ └── ControlPanel.css # Panel styles
│ │
│ └── store/
│ └── turtleStore.js # State management
└── 🎮 Lua Scripts/ # Minecraft/ComputerCraft
├── webbridge.lua # Web server bridge
├── turtle.lua # Advanced turtle AI
└── pocketremote.lua # Pocket computer UI
```
## 🎨 Features Implemented
### Web Interface
- ✅ Real-time 3D map with orbital camera controls
- ✅ Interactive turtle markers with selection
- ✅ Path trails showing routes to home
- ✅ Multiple view modes (Split/Map/Control)
- ✅ Connection status indicator
- ✅ Turtle cards with live stats
- ✅ Detailed control panel per turtle
- ✅ Command buttons (Explore, Mine, Return, Stop)
- ✅ Manual control arrows
- ✅ Inventory visualization
- ✅ Auto-reconnect on disconnect
### Backend
- ✅ WebSocket real-time communication
- ✅ REST API for turtle updates
- ✅ Command queuing system
- ✅ Automatic stale turtle cleanup
- ✅ CORS enabled for local development
- ✅ Error handling and logging
### Minecraft Integration
- ✅ Bridge script for HTTP communication
- ✅ Command polling system
- ✅ Status broadcasting
- ✅ Backward compatible with existing scripts
## 🔧 Technologies Used
### Frontend
- **React 18** - UI framework
- **Vite** - Build tool and dev server
- **Three.js** - 3D graphics library
- **@react-three/fiber** - React renderer for Three.js
- **@react-three/drei** - Helper components for R3F
- **Zustand** - Lightweight state management
- **WebSocket API** - Real-time communication
### Backend
- **Node.js** - Runtime environment
- **Express** - Web framework
- **ws** - WebSocket server
- **cors** - Cross-origin resource sharing
### Minecraft
- **ComputerCraft** - Lua API for Minecraft
- **HTTP API** - For web communication
- **Modem API** - For wireless communication
## 🚀 Quick Start Commands
### Install Everything
```bash
npm run install:all
```
### Start in Development Mode
```bash
# Using npm (starts both servers)
npm run dev
# Or use the helper scripts
./start.sh # Linux/Mac
start.bat # Windows
```
### Individual Services
```bash
npm run server # Start backend only
npm run client # Start frontend only
```
## 🌐 Access Points
Once running:
- **Web Interface**: http://localhost:3000
- **API Server**: http://localhost:3001
- **WebSocket**: ws://localhost:3002
- **API Docs**: Check README.md API Reference section
## 📊 Data Flow
```
┌──────────────┐
│ Turtle │
│ (turtle.lua)│
└──────┬───────┘
│ Status (Modem Channel 102)
┌──────────────┐
│ Bridge │
│(webbridge.lua)│ ← Forwards via HTTP
└──────┬───────┘
│ POST /api/turtle/update
┌──────────────┐
│ Node Server │ ← Broadcasts via WebSocket
│ (server.js) │
└──────┬───────┘
│ WebSocket message
┌──────────────┐
│ React Client │ ← Updates UI in real-time
│ (App.jsx) │
└──────────────┘
│ Command button click
┌──────────────┐
│ Server │ ← Queues command
└──────┬───────┘
│ GET /api/turtle/:id/commands
┌──────────────┐
│ Bridge │ ← Polls for commands
└──────┬───────┘
│ Modem Channel 100
┌──────────────┐
│ Turtle │ ← Executes command
└──────────────┘
```
## 🎯 Next Steps
1. **Run the startup script** to install dependencies
2. **Start Minecraft** and set up the bridge computer
3. **Deploy turtles** with the turtle.lua script
4. **Open the web interface** and start controlling!
## 💡 Tips for Use
- Keep the bridge computer loaded (use chunk loaders)
- Set up GPS hosts for accurate positioning
- Monitor the 3D map for turtle movements
- Use split view on larger screens
- Switch to map-only for presentations
- Try the manual controls for precise movements
## 🐛 Common Issues
**Port already in use?**
- Change ports in `server/server.js` and `client/src/store/turtleStore.js`
**Turtles not connecting?**
- Verify `webbridge.lua` SERVER_URL matches your setup
- Check all scripts use matching channel numbers
- Ensure wireless modems are equipped
**3D map not rendering?**
- Check browser console for errors
- Ensure WebGL is supported by your browser
- Try a different browser (Chrome/Firefox recommended)
## 📝 Documentation Files
- **README.md** - Complete documentation with API reference
- **QUICKSTART.md** - Fast setup guide
- **INTERFACE.md** - UI/UX overview
- This file - Project summary
---
## 🎉 Success!
You now have a complete web-based control center for your Minecraft turtles!
**Features you can use:**
- Monitor multiple turtles on a 3D map
- Send commands from your browser
- Watch real-time position updates
- Control turtles from any device on your network
- Beautiful, modern UI with dark theme
**Happy turtle controlling!** 🐢✨

View File

@@ -64,6 +64,36 @@ You should see:
**GPS not working?**
- Set up 4 GPS host computers at high altitude
- Run `gps host X Y Z` on each (with their coordinates)
- **Alternative:** If running Opus OS, use its `gpsServer` package — a single
turtle can self-build a complete GPS constellation (replaces 4 host computers)
## Pathfinding
The turtle now includes a built-in pathfinding module exposed as `_G._pathfind`.
You can trigger it from the web UI via eval commands:
```lua
-- Navigate to coordinates (avoids obstacles)
_pathfind.goto(100, 65, -200)
-- Navigate with block digging enabled
_pathfind.goto(100, 65, -200, { dig = true })
-- Go home
_pathfind.goHome()
-- Face a specific heading (0=south, 1=west, 2=north, 3=east)
_pathfind.face(3)
-- Get current heading name
_pathfind.headingName() -- "east"
```
The pathfinder:
- Uses GPS for initial position, then tracks movement locally
- Auto-detects heading on startup via GPS triangulation
- Handles obstacles by trying to go over/around them
- Supports optional block digging for clearing paths
## Next Steps

780
README.md
View File

@@ -1,21 +1,35 @@
# 🐢 Turtle Control Center - Web Panel
# 🐢 Turtle Control Center - Advanced Web Panel
A full-stack web application for monitoring and controlling ComputerCraft turtles in Minecraft. Features a real-time 3D map, WebSocket communication, and an intuitive control panel.
A comprehensive full-stack web application for monitoring and controlling ComputerCraft turtles in Minecraft. Features real-time 3D visualization, voice commands, intelligent navigation, mining statistics, team coordination, and persistent database storage.
![Turtle Control Center](https://img.shields.io/badge/Minecraft-ComputerCraft-green)
![Node.js](https://img.shields.io/badge/Node.js-18+-blue)
![React](https://img.shields.io/badge/React-18-61DAFB)
![Three.js](https://img.shields.io/badge/Three.js-3D-black)
![SQLite](https://img.shields.io/badge/SQLite-Database-003B57)
## 📋 Features
## Features
- **🗺️ Real-time 3D Map**: Visualize turtle positions and movements in a beautiful 3D environment
### Core Features
- **🗺️ Real-time 3D Map**: Visualize turtle positions and movements with Three.js
- **🎮 Control Panel**: Monitor and control multiple turtles simultaneously
- **⚡ WebSocket Communication**: Instant updates and commands
- **🔄 Auto-reconnect**: Handles connection drops gracefully
- **📊 Live Statistics**: Track fuel, inventory, position, and more
- **🎯 Manual Control**: Direct control with arrow keys
- **🤖 Autonomous Modes**: Explore, mine, and return home automatically
- **⚡ WebSocket Communication**: Instant bidirectional updates
- **🔄 Auto-reconnect**: Robust connection handling with automatic recovery
- **📊 Live Statistics**: Fuel, inventory, position, mining stats, and more
- **🎯 Manual Control**: Direct movement and action controls
- **🤖 Autonomous Intelligence**: Smart exploration with position memory and stuck detection
### Advanced Features
- **🎤 Voice Commands**: Natural language control with Web Speech API (20+ commands)
- **📦 Enhanced Inventory**: 4x4 grid display with item-specific emojis and counts
- **📱 Mobile Responsive**: Optimized for desktop, tablet, and mobile devices
- **💾 Database Persistence**: SQLite storage for homes, blocks, paths, tasks, and statistics
- **📈 Mining Statistics**: Track blocks mined per turtle with leaderboards
- **👥 Turtle Teams**: Organize turtles into groups with coordinated commands
- **🗺️ Path Recording**: Record and replay turtle movement paths
- **📋 Task Queue**: Schedule and coordinate multi-turtle tasks
- **🏗️ Mining Areas**: Define and manage mining zones
- **⏱️ Session Tracking**: Time-based analytics and performance metrics
## 🏗️ Architecture
@@ -38,87 +52,218 @@ A full-stack web application for monitoring and controlling ComputerCraft turtle
### Prerequisites
- Node.js 18+ installed on your computer
- Minecraft with ComputerCraft mod installed
- At least one turtle with a wireless modem
- A computer in Minecraft to run the bridge script
- **Docker and Docker Compose** installed on your computer
- **Minecraft** with ComputerCraft mod installed
- At least one **turtle with a wireless modem**
- A **computer in Minecraft** to run the bridge script
### Installation
### Installation (Docker - Recommended)
1. **Clone or download this project**
2. **Install Server Dependencies**
```bash
cd server
npm install
git clone <repository-url>
cd remoteturtle
```
3. **Install Client Dependencies**
2. **Start with Docker Compose**
```bash
cd client
npm install
docker-compose up -d
```
This will automatically:
- Build and start the backend server on port 3001
- Build and start the frontend on port 3000
- Create a persistent SQLite database volume
- Set up networking between containers
4. **Start the Backend Server**
```bash
cd server
npm start
```
Server will run on:
- HTTP: http://localhost:3001
3. **Access the Web Interface**
- Frontend: http://localhost:3000
- Backend API: http://localhost:3001
- WebSocket: ws://localhost:3002
5. **Start the React Frontend**
```bash
cd client
npm run dev
4. **Set up Minecraft Bridge Computer**
In Minecraft, place a computer with a wireless modem and run:
```lua
wget https://git.spatulaa.com/MayaTheShy/remoteturtle/raw/branch/master/startup_webbridge.lua startup
reboot
```
Frontend will open at: http://localhost:3000
6. **Set up Minecraft Bridge**
- In Minecraft, place a computer with a wireless modem
This installs an auto-update system that:
- Downloads the latest `webbridge.lua` on boot
- Falls back to cached version if download fails
- Automatically connects to your server
Or manually:
- Copy `webbridge.lua` to the computer
- Edit the `SERVER_URL` in webbridge.lua to point to your server
(If running locally, use `http://localhost:3001`)
- Edit the `SERVER_URL` to point to your Docker host
- If Minecraft is on the same machine: `http://host.docker.internal:3001`
- If on different machine: `http://<your-ip>:3001`
- Run: `webbridge`
7. **Deploy Turtles**
- Copy `turtle.lua` to your turtles
- Equip wireless modems on turtles
- Run: `turtle`
5. **Deploy Auto-Updating Turtles**
On each turtle with a wireless modem, run:
```lua
wget https://git.spatulaa.com/MayaTheShy/remoteturtle/raw/branch/master/startup_turtle.lua startup
reboot
```
This installs an auto-update system that:
- Downloads the latest `turtle.lua` on boot
- Falls back to cached version if download fails
- Keeps turtles updated automatically
6. **Optional: Deploy Pocket Computer Control**
For wireless pocket computer control:
```lua
wget https://git.spatulaa.com/MayaTheShy/remoteturtle/raw/branch/master/startup_pocket.lua startup
reboot
```
This provides a wireless control interface for managing turtles from anywhere in-game
### Docker Commands
```bash
# Start services
docker-compose up -d
# View logs
docker-compose logs -f
# Stop services
docker-compose down
# Rebuild after code changes
docker-compose up -d --build
# Stop and remove volumes (resets database)
docker-compose down -v
```
### Alternative: Manual Installation (Without Docker)
If you prefer not to use Docker:
1. **Install Dependencies**
```bash
cd server && npm install
cd ../client && npm install
```
2. **Start Backend**
```bash
cd server && npm start
```
3. **Start Frontend**
```bash
cd client && npm run dev
```
4. Follow steps 4-6 from the Docker installation above
## 📁 Project Structure
```
remoteturtle/
├── server/ # Node.js backend
│ ├── server.js # Express + WebSocket server
── package.json
├── client/ # React frontend
├── server/ # Node.js backend (650+ lines)
│ ├── server.js # Express REST API + WebSocket server
── database.js # SQLite database layer (450+ lines)
│ ├── turtle_control.db # SQLite database file
│ ├── package.json
│ └── node_modules/
├── client/ # React frontend (1,500+ lines)
│ ├── src/
│ │ ├── components/
│ │ │ ├── Map3D.jsx # 3D map visualization
│ │ │ ├── ControlPanel.jsx # Control interface
│ │ │ ── ControlPanel.css
│ │ │ ├── Map3D.jsx # Three.js 3D visualization
│ │ │ ├── ControlPanel.jsx # Main control interface
│ │ │ ── ControlPanel.css # Control panel styles
│ │ │ ├── VoiceControl.jsx # Voice command interface (NEW)
│ │ │ └── VoiceControl.css # Voice control styles (NEW)
│ │ ├── store/
│ │ │ └── turtleStore.js # Zustand state management
│ │ ├── App.jsx
│ │ ├── App.css
│ │ ├── main.jsx
│ │ └── index.css
│ │ │ └── turtleStore.js # Zustand state management
│ │ ├── App.jsx # Main application component
│ │ ├── App.css # Global styles + responsive design
│ │ ├── main.jsx # React entry point
│ │ └── index.css # Base styles
│ ├── index.html
│ ├── vite.config.js
── package.json
├── webbridge.lua # ComputerCraft bridge script
├── turtle.lua # Advanced turtle control script
├── pocketremote.lua # Pocket computer interface
── README.md
│ ├── vite.config.js # Vite configuration
── package.json
│ └── node_modules/
├── webbridge.lua # ComputerCraft HTTP bridge (200+ lines)
── turtle.lua # Intelligent turtle controller (400+ lines)
├── pocketremote.lua # Pocket computer interface (150+ lines)
├── FEATURES.md # Feature documentation
└── README.md # This file
```
### Key Files Overview
**Backend:**
- `server/server.js` - 35+ REST API endpoints, WebSocket server
- `server/database.js` - Database abstraction with 10 tables
**Frontend:**
- `client/src/App.jsx` - Layout and routing
- `client/src/components/Map3D.jsx` - 3D turtle visualization
- `client/src/components/ControlPanel.jsx` - Main control UI
- `client/src/components/VoiceControl.jsx` - Voice commands
- `client/src/store/turtleStore.js` - Global state management
**ComputerCraft:**
- `webbridge.lua` - Bridge between Minecraft and web server
- `turtle.lua` - Autonomous turtle with intelligent navigation
- `pocketremote.lua` - Mobile control from pocket computers
## 🎮 Usage
### Web Interface
The interface features a **multi-panel tabbed design** for comprehensive turtle management:
#### 🎮 Control Panel
- View all connected turtles
- Manual movement controls (forward, back, up, down, turn)
- Autonomous commands (explore, mine, return home)
- Real-time inventory display (4x4 grid with emojis)
- GPS position and fuel monitoring
#### 🎤 Voice Commands
- Speak naturally to control turtles
- 20+ recognized commands
- Real-time transcript display
- Speech confirmation feedback
- Command help reference
#### 📊 Statistics Dashboard
- Mining statistics per turtle
- Block type breakdown with emojis
- Top miners leaderboard (🥇🥈🥉)
- Time filters (24h, 7d, 30d, All)
- Total blocks mined tracking
#### 👥 Groups Management
- Create turtle teams with custom colors
- 8 color presets available
- Add/remove turtles from groups
- Send commands to entire team
- Member status tracking
#### 📋 Task Queue
- Create tasks with 6 types
- Priority system (1-10)
- Coordinate input for area tasks
- Filter by status
- Assign to specific turtles
- Task lifecycle management
### View Modes
1. **View Modes**:
- **Split View**: See both map and control panel
- **Map Only**: Full-screen 3D map
@@ -139,14 +284,44 @@ remoteturtle/
### In-Game Commands
**Turtle Commands** (via pocket computer or web):
- `explore` - Start exploration mode
- `returnHome` - Return to home position
- `stop` - Stop current operation
- `forward/back/up/down` - Movement
**Turtle Commands** (via pocket computer, web, or voice):
**Movement:**
- `forward/back/up/down` - Directional movement
- `turnLeft/turnRight` - Rotation
- `dig/digUp/digDown` - Mining
**Actions:**
- `dig/digUp/digDown` - Mining operations
- `place/placeUp/placeDown` - Block placement
- `refuel` - Consume fuel items
**Autonomous Operations:**
- `explore` - Intelligent exploration with position memory
- 75% horizontal movement preference
- 15% downward, 10% upward movement
- Visited position tracking to avoid loops
- Stuck detection with recovery
- `mine` - Autonomous mining mode
- `returnHome` - Navigate back to home (non-blocking)
- `stop` - Stop current operation
- `setHome` - Set current position as home
- `status` - Report current state
### Intelligent Navigation Features
The turtle.lua script includes advanced navigation:
1. **Position Memory**: Tracks last 50 positions to avoid revisiting
2. **Visited Position Tracking**: Maintains history to prevent loops
3. **Stuck Detection**: Identifies when turtle can't move and tries alternatives
4. **Smart Direction Selection**:
- Prefers horizontal movement (75%)
- Controlled descent (15%)
- Minimal upward movement (10%)
5. **Turn Limiting**: Max 3 turn attempts to prevent endless spinning
6. **Non-blocking Return Home**: Can return home without timing out
7. **Fuel Management**: Auto-refuels when fuel is low
8. **GPS Integration**: Uses GPS for accurate positioning
## 🔧 Configuration
@@ -199,45 +374,274 @@ To access from other devices on your network:
3. Ensure firewall allows connections on ports 3000, 3001, and 3002
## 📊 Project Statistics
- **Total Code**: 5,600+ lines
- **Database Tables**: 11
- **API Endpoints**: 35+
- **Supported Commands**: 25+
- **Voice Commands**: 20+
- **Frontend Components**: 12+
- **Task Types**: 6
- **Color Presets**: 8
## 📊 API Reference
### REST Endpoints
### Turtle Management
**POST /api/turtle/update**
- Body: Turtle status object
- Updates turtle state
#### `POST /api/turtle/update`
Update turtle status
```json
{
"turtleID": 123,
"name": "Miner-1",
"x": 100, "y": 64, "z": 200,
"facing": 0,
"fuel": 5000,
"mode": "exploring",
"inventory": [...]
}
```
**GET /api/turtle/:id/commands**
- Returns pending commands for turtle
#### `GET /api/turtles`
Get all connected turtles
**POST /api/turtle/:id/command**
- Body: `{ command: string, param: any }`
- Queues command for turtle
#### `GET /api/turtle/:id`
Get specific turtle details
**GET /api/turtles**
- Returns all connected turtles
#### `POST /api/turtle/:id/command`
Send command to turtle
```json
{
"command": "explore",
"param": null
}
```
#### `GET /api/turtle/:id/commands`
Get pending commands for turtle
### Home Management
#### `POST /api/homes`
Set turtle home position
```json
{
"turtleID": 123,
"x": 100, "y": 64, "z": 200
}
```
#### `GET /api/homes/:turtleId`
Get turtle's home position
### Block Tracking
#### `POST /api/blocks`
Record discovered block
```json
{
"x": 100, "y": 64, "z": 200,
"blockType": "minecraft:diamond_ore",
"discoveredBy": 123
}
```
#### `GET /api/blocks`
Get all discovered blocks (with filtering)
- Query params: `minX`, `maxX`, `minY`, `maxY`, `minZ`, `maxZ`
### Path Recording
#### `POST /api/paths`
Create new path recording
```json
{
"name": "Mine Route 1",
"turtleId": 123,
"description": "Efficient mining path"
}
```
#### `GET /api/paths`
Get all recorded paths
#### `GET /api/paths/:pathId`
Get specific path with waypoints
#### `POST /api/paths/:pathId/waypoints`
Add waypoint to path
```json
{
"x": 100, "y": 64, "z": 200,
"action": "dig_forward"
}
```
#### `DELETE /api/paths/:pathId`
Delete path
### Task Queue
#### `POST /api/tasks`
Create new task
```json
{
"taskType": "mine_area",
"priority": 1,
"assignedTurtleId": 123,
"parameters": {
"x1": 100, "y1": 60, "z1": 200,
"x2": 120, "y2": 70, "z2": 220
}
}
```
#### `GET /api/tasks`
Get all tasks (with optional status filter)
- Query params: `status=pending|in_progress|completed|failed`
#### `GET /api/tasks/:taskId`
Get specific task details
#### `PUT /api/tasks/:taskId`
Update task status
```json
{
"status": "completed",
"result": "Mined 450 blocks"
}
```
#### `DELETE /api/tasks/:taskId`
Delete task
### Mining Areas
#### `POST /api/mining-areas`
Define mining area
```json
{
"name": "Diamond Layer",
"x1": 100, "y1": -64, "z1": 200,
"x2": 200, "y2": -50, "z2": 300,
"priority": 1,
"assignedTurtles": [123, 456]
}
```
#### `GET /api/mining-areas`
Get all mining areas
#### `GET /api/mining-areas/:areaId`
Get specific mining area
#### `PUT /api/mining-areas/:areaId`
Update mining area
#### `DELETE /api/mining-areas/:areaId`
Delete mining area
### Mining Statistics
#### `POST /api/stats/block-mined`
Record block mined
```json
{
"turtleId": 123,
"blockType": "minecraft:diamond_ore"
}
```
#### `GET /api/stats/mining/:turtleId?`
Get mining statistics
- Query params: `days=7` (filter by recent days)
- Returns per-block-type counts
#### `GET /api/stats/top-miners`
Get leaderboard of top mining turtles
- Query params: `limit=10`
### Turtle Groups/Teams
#### `POST /api/groups`
Create turtle group
```json
{
"groupName": "Mining Team Alpha",
"color": "#3b82f6"
}
```
#### `GET /api/groups`
Get all groups with member counts
#### `DELETE /api/groups/:groupId`
Delete group
#### `POST /api/groups/:groupId/members`
Add turtle to group
```json
{
"turtleId": 123
}
```
#### `DELETE /api/groups/:groupId/members/:turtleId`
Remove turtle from group
#### `GET /api/turtles/:turtleId/groups`
Get turtle's groups
#### `POST /api/groups/:groupId/command`
Send command to all turtles in group
```json
{
"command": "returnHome"
}
```
### WebSocket Messages
**From Server:**
**From Server to Client:**
```json
{
"type": "initial_state",
"turtles": [...]
}
```
```json
{
"type": "turtle_update",
"turtle": {...}
}
```
```json
{
"type": "turtle_disconnected",
"turtleID": 123
}
```
**From Client:**
```json
{
"type": "block_discovered",
"block": {...}
}
```
```json
{
"type": "task_update",
"task": {...}
}
```
**From Client to Server:**
```json
{
"type": "command",
@@ -247,27 +651,176 @@ To access from other devices on your network:
}
```
## 🎤 Voice Commands
The system supports natural language voice commands using the Web Speech API (Chrome, Edge, Safari):
### Movement Commands
- "forward" / "move forward" / "go forward"
- "back" / "backward" / "move back"
- "turn left" / "left"
- "turn right" / "right"
- "up" / "move up" / "go up"
- "down" / "move down" / "go down"
### Action Commands
- "dig" / "mine"
- "dig up" / "mine up"
- "dig down" / "mine down"
- "place" / "place block"
- "refuel"
### Autonomous Commands
- "explore" / "start exploring"
- "mine" / "start mining"
- "return home" / "go home" / "come back"
- "stop" / "halt"
- "set home" / "mark home"
- "status" / "report"
## 💾 Database Schema
### Tables
1. **turtle_homes** - Home positions for turtles
2. **turtle_config** - Turtle configuration settings
3. **world_blocks** - Discovered block locations
4. **turtle_paths** - Recorded movement paths
5. **path_waypoints** - Individual waypoints in paths
6. **task_queue** - Task scheduling and coordination
7. **mining_areas** - Defined mining zones
8. **mining_stats** - Block mining statistics per turtle
9. **turtle_groups** - Turtle team/group definitions
10. **turtle_group_members** - Group membership
11. **turtle_sessions** - Time-based analytics tracking
## 📱 Frontend Features
### Enhanced Inventory Display
- 4x4 grid matching Minecraft turtle slots
- Item-specific emojis (💎 diamond, ⚫ coal, 🟡 gold, etc.)
- Item count badges
- Empty slot indicators
- Hover tooltips with full item names
### Mobile Responsive Design
- **Desktop (>1024px)**: Side-by-side map and controls
- **Tablet (768-1024px)**: Vertical stacking
- **Mobile (<768px)**: Tab-based navigation, optimized touch targets
- Touch-optimized controls (48px minimum)
- Landscape orientation support
- Accessibility features (reduced motion, high contrast)
## 🐛 Troubleshooting
### Turtles not appearing in web panel
1. Check that webbridge.lua is running
2. Verify SERVER_URL is correct
3. Check turtle.lua is running with wireless modem equipped
4. Verify all scripts are using same channel numbers
1. **Check webbridge is running**
- Verify webbridge.lua is active in Minecraft
- Check for error messages in the Minecraft console
2. **Verify SERVER_URL configuration**
- Ensure webbridge.lua points to correct server address
- Use `http://localhost:3001` for local testing
- Use your machine's IP for remote access
3. **Check turtle setup**
- Ensure turtle.lua is running
- Verify wireless modem is equipped
- Check modem is on correct side (usually top)
4. **Verify channel configuration**
- All scripts must use matching channel numbers
- Default: RECEIVE=100, SEND=101, STATUS=102
### Connection issues
1. Check server is running: `http://localhost:3001/api/turtles`
2. Check firewall settings
3. Verify ports 3001 and 3002 are not in use
4. Check browser console for WebSocket errors
1. **Server not responding**
- Check server is running: `http://localhost:3001/api/turtles`
- Verify no port conflicts (3001, 3002, 3000)
- Check firewall isn't blocking connections
2. **WebSocket disconnections**
- Check browser console for WebSocket errors
- Verify WS_PORT (3002) is accessible
- Try restarting both client and server
3. **Database errors**
- Check `server/turtle_control.db` file exists
- Ensure write permissions on database file
- Try deleting database to reinitialize (loses data)
### Turtle behavior issues
1. **Turtle gets stuck or spins endlessly**
- Check fuel level (need sufficient fuel)
- Verify GPS is working (`gps locate` in-game)
- Restart turtle.lua to reset state
2. **Turtle not finding home**
- Ensure home position is set (`setHome` command)
- Check GPS hosts are functioning
- Verify turtle has enough fuel to return
3. **Turtle timing out**
- Fixed in latest version with non-blocking return home
- Ensure latest turtle.lua is deployed
- Check modem range (max 64 blocks without ender modem)
### Voice commands not working
1. **Browser compatibility**
- Use Chrome, Edge, or Safari (Firefox not supported)
- Check microphone permissions in browser settings
- Try HTTPS instead of HTTP (some browsers require it)
2. **Microphone issues**
- Verify microphone is connected and working
- Check system permissions allow browser mic access
- Test with "Hello Google" or other voice apps
3. **Commands not recognized**
- Speak clearly and at normal pace
- Use exact command phrases from the help menu
- Check browser console for recognition errors
### GPS not working
1. Set up GPS hosts in Minecraft (requires 4 computers)
2. Position them in a square pattern at high Y level
3. Each must run `gps host X Y Z` with their coordinates
1. **Set up GPS hosts**
- Requires 4 computers in Minecraft
- Position in square pattern at high Y level (Y>100 recommended)
- Each runs: `gps host X Y Z` with their exact coordinates
2. **GPS host placement**
```
Host positions (example):
Computer 1: gps host 100 120 100
Computer 2: gps host 200 120 100
Computer 3: gps host 100 120 200
Computer 4: gps host 200 120 200
```
3. **Troubleshooting GPS**
- Ensure all 4 hosts are loaded (chunk loaders)
- Keep them at least 6 blocks apart
- Must be on same dimension as turtles
### Performance issues
1. **Slow web interface**
- Reduce number of visible turtles in 3D map
- Disable block tracking if not needed
- Use production build: `npm run build`
2. **High database size**
- Periodically clean old session data
- Archive old paths and tasks
- Consider vacuum command: `VACUUM;`
3. **Memory issues in Minecraft**
- Limit number of active turtles
- Reduce position memory size in turtle.lua
- Use more efficient exploration patterns
## 🎨 Customization
@@ -308,16 +861,45 @@ Built with:
- [Express](https://expressjs.com/) - Backend server
- [ws](https://github.com/websockets/ws) - WebSocket server
## 🚀 Future Enhancements
## 🚀 Implementation Status
- [ ] Path recording and playback
- [ ] Multi-turtle task coordination
- [ ] Inventory management interface
- [ ] Mining area visualization
- [ ] Task scheduling
- [ ] Database persistence
- [ ] Authentication/multi-user support
- [ ] Mobile-responsive design
### ✅ Completed Features
- [x] Real-time 3D visualization with Three.js
- [x] WebSocket bidirectional communication
- [x] Manual turtle control interface
- [x] Autonomous exploration with intelligent navigation
- [x] Database persistence (SQLite with 11 tables)
- [x] Path recording backend (API complete)
- [x] Task queue system (backend + frontend complete)
- [x] Mining area management backend (API complete)
- [x] Mining statistics tracking (backend + frontend complete)
- [x] Turtle groups/teams coordination (backend + frontend complete)
- [x] Voice command interface (Web Speech API, 20+ commands)
- [x] Enhanced inventory display (4x4 grid with emojis)
- [x] Mobile-responsive design (desktop/tablet/mobile)
- [x] Position memory and visited tracking
- [x] Stuck detection and recovery
- [x] Session-based analytics
- [x] Multi-panel tabbed interface (Control, Voice, Stats, Groups, Tasks)
### 🚧 Frontend Pending (Backend Complete)
- [ ] Path recording UI (playback controls, visualization)
- [ ] Mining area visualization (3D overlays on map)
### 📋 Future Enhancements
- [ ] User authentication system
- [ ] Multi-user support with permissions
- [ ] Real-time collaboration features
- [ ] Advanced pathfinding algorithms (A*)
- [ ] Quarry mode with automatic area mining
- [ ] Ore detection and priority mining
- [ ] Automated smelting coordination
- [ ] Chest management and auto-storage
- [ ] Fuel depot system
- [ ] Emergency recovery protocols
## 💡 Tips

65
STARTUP_README.md Normal file
View File

@@ -0,0 +1,65 @@
# Auto-Update Startup Scripts
These scripts automatically download and run the latest versions of your turtle control programs when computers start up.
## Installation
### For Turtles:
```lua
wget https://git.spatulaa.com/MayaTheShy/remoteturtle/raw/branch/master/startup_turtle.lua startup
```
### For Webbridge Computer:
```lua
wget https://git.spatulaa.com/MayaTheShy/remoteturtle/raw/branch/master/startup_webbridge.lua startup
```
### For Pocket Computer:
```lua
wget https://git.spatulaa.com/MayaTheShy/remoteturtle/raw/branch/master/startup_pocket.lua startup
```
## How It Works
1. **On boot**, the startup script runs automatically
2. **Removes** the old version of the main script
3. **Downloads** the latest version from git
4. **Executes** the downloaded script
## Fallback Behavior
If the download fails (no internet, wrong URL, etc.):
- Shows an error message
- Attempts to run the existing version if available
- Shows manual download instructions
## Benefits
✅ Always run the latest version
✅ No manual script management
✅ Just reboot to update
✅ Fallback to existing version if offline
✅ Clear status messages
## Manual Update
To force an update on a running computer:
```lua
reboot
```
## Changing the URL
If you need to change the git URL, edit the `SCRIPT_URL` variable in the startup script:
```lua
local SCRIPT_URL = "https://your-git-url-here/script.lua"
```
## Disabling Auto-Update
To disable auto-update and use manual management:
```lua
rm startup
```
Then download scripts manually as needed.

347
START_HERE.md Normal file
View File

@@ -0,0 +1,347 @@
# 🎉 Project Complete!
## What You Now Have
A **complete full-stack web application** for controlling ComputerCraft mining turtles in Minecraft!
```
┌─────────────────────────────────────────────────────────────┐
│ 🐢 TURTLE CONTROL CENTER │
│ │
│ ┌──────────────────┐ ┌─────────────────────────┐ │
│ │ Turtle List │ │ Live 3D Map │ │
│ │ ───────────── │ │ │ │
│ │ │ │ 🏠 │ │
│ │ 🐢 Turtle 1 │ │ \ │ │
│ │ [Mining] │ │ \----🐢 Selected │ │
│ │ Fuel: 5000 │ │ │ │
│ │ Inventory: 8 │ │ │ │
│ │ │ │ 🐢 │ │
│ │ 🐢 Turtle 2 │ │ │ │
│ │ [Exploring] │ │ Rotate • Zoom • Pan │ │
│ │ Fuel: 3200 │ │ │ │
│ │ Inventory: 3 │ └─────────────────────────┘ │
│ │ │ │
│ │ + Add More... │ ┌─────────────────────────┐ │
│ └──────────────────┘ │ Turtle 1 Control │ │
│ │ ───────────────────── │ │
│ ✅ Real-time Updates │ [🔍Explore] [⛏Mine] │ │
│ ✅ WebSocket Connected │ [🏠Return] [⏹Stop] │ │
│ ✅ Auto-reconnect │ │ │
│ │ Manual: ↑ ← → ↓ │ │
│ └─────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
## 📁 Complete File Structure
```
remoteturtle/
├── 📚 Documentation (7 files)
│ ├── README.md ⭐ Main documentation
│ ├── QUICKSTART.md 🚀 Fast setup guide
│ ├── PROJECT_SUMMARY.md 📊 What was built
│ ├── ARCHITECTURE.md 🏗️ System design
│ ├── INTERFACE.md 🎨 UI overview
│ ├── TROUBLESHOOTING.md 🔧 Problem solving
│ └── THIS_FILE.md ✅ You are here!
├── 🎮 Setup Scripts (3 files)
│ ├── start.sh Linux/Mac launcher
│ ├── start.bat Windows launcher
│ └── package.json Root config
├── 🖥️ Backend (server/)
│ ├── server.js Express + WebSocket server
│ └── package.json Node.js dependencies
├── 🌐 Frontend (client/)
│ ├── index.html
│ ├── vite.config.js
│ ├── package.json
│ └── src/
│ ├── main.jsx
│ ├── App.jsx Main React component
│ ├── App.css
│ ├── index.css
│ ├── components/
│ │ ├── Map3D.jsx 3D visualization
│ │ ├── ControlPanel.jsx UI controls
│ │ └── ControlPanel.css
│ └── store/
│ └── turtleStore.js State management
└── 🎮 Minecraft/Lua (3 files)
├── webbridge.lua HTTP bridge to server
├── turtle.lua Advanced turtle AI
└── pocketremote.lua Original pocket UI
Total: 23+ files created/enhanced!
```
## 🎯 Key Features
### Web Interface
-**3D Interactive Map** - Orbital camera, click to select
-**Real-time Updates** - See turtle movements live
-**Multiple Views** - Split, map-only, control-only
-**Turtle Cards** - Quick overview of all turtles
-**Detailed Control** - Per-turtle commands and stats
-**Manual Controls** - Arrow keys for direct movement
-**Inventory Display** - See what turtles collected
-**Connection Status** - Visual indicator
-**Dark Theme** - Easy on the eyes
### Technical
-**WebSocket** - Real-time bidirectional communication
-**REST API** - HTTP endpoints for turtle updates
-**State Management** - Zustand for React state
-**3D Graphics** - Three.js rendering
-**Auto-reconnect** - Handles disconnections
-**Command Queue** - Reliable command delivery
-**Stale Cleanup** - Removes offline turtles
### Minecraft Integration
-**HTTP Bridge** - Connects game to web
-**Wireless Modem** - In-game communication
-**GPS Support** - Position tracking
-**Autonomous Modes** - Explore, mine, return
-**Fuel Management** - Auto-refuel logic
-**Pathfinding** - Navigate to targets
-**Status Broadcasting** - Regular updates
## 🚀 How to Use
### 1. Install Dependencies
**Option A: One Command**
```bash
./start.sh # Linux/Mac
start.bat # Windows
```
**Option B: Manual**
```bash
cd server && npm install
cd ../client && npm install
```
### 2. Start Servers
The startup script automatically starts both:
- Backend server (http://localhost:3001)
- Frontend app (http://localhost:3000)
### 3. Configure Minecraft
**Bridge Computer:**
1. Place computer with wireless modem
2. Copy `webbridge.lua`
3. Edit SERVER_URL if needed
4. Run: `webbridge`
**Turtles:**
1. Place turtles with wireless modems
2. Copy `turtle.lua`
3. Run: `turtle`
### 4. Open Web Interface
Navigate to: **http://localhost:3000**
You should see:
- Connection status: 🟢 Connected
- Waiting for turtles message
- Empty 3D map with grid
### 5. Control Turtles!
- Click turtle cards or 3D markers to select
- Use command buttons: Explore, Mine, Return, Stop
- Try manual controls: ↑ ↓ ← → ⬆ ⬇
- Watch the 3D map update in real-time!
## 📚 Documentation Guide
**New to the project?** Start here:
1. **QUICKSTART.md** - Get running in 5 minutes
2. **README.md** - Full documentation
**Want to understand the system?**
- **ARCHITECTURE.md** - How everything connects
- **INTERFACE.md** - UI/UX design
**Having problems?**
- **TROUBLESHOOTING.md** - Common issues & solutions
**Curious what was built?**
- **PROJECT_SUMMARY.md** - Complete overview
- **THIS_FILE.md** - Quick reference
## 🎨 Color Guide
Turtle Status Colors:
- 🟢 Green: Mining/Active
- 🔵 Blue: Exploring
- 🟠 Orange: Returning Home
- ⚪ Gray: Idle
- 🟣 Purple: Manual Control
UI Theme:
- Background: Dark blue (#0a0e1a)
- Panels: Slate (#1e293b)
- Text: Light gray (#e2e8f0)
- Accents: Cyan (#60a5fa)
## 🔧 Quick Commands
```bash
# Start everything
./start.sh
# Install all dependencies
npm run install:all
# Start only backend
npm run server
# Start only frontend
npm run client
# Build for production
npm run build
```
## 🌐 Network Ports
```
3000 - React Frontend (Vite dev server)
3001 - Express HTTP API
3002 - WebSocket Server
Minecraft Modem Channels:
100 - Commands TO turtles
101 - Responses FROM turtles
102 - Status updates
```
## 💡 Pro Tips
1. **GPS Setup**: Place 4 computers at Y=200+ in square pattern
2. **Fuel Management**: Keep coal in turtle slot 16
3. **Chunk Loading**: Use chunk loader for bridge computer
4. **Multiple Turtles**: Each needs unique ID (automatic)
5. **View Modes**: Split for monitoring, Map for presentations
6. **Browser**: Chrome or Firefox work best
7. **Network**: Use IP address for remote access
8. **Backup**: Save turtle programs to disk
## 🔍 Testing Checklist
- [ ] Node.js installed (v18+)
- [ ] Dependencies installed (npm install)
- [ ] Server starts without errors
- [ ] Browser opens to http://localhost:3000
- [ ] Connection status shows "Connected"
- [ ] Bridge computer running in Minecraft
- [ ] Turtles have wireless modems
- [ ] GPS hosts configured (optional but recommended)
- [ ] Turtle appears in web interface
- [ ] Commands work (try "Explore")
- [ ] 3D map updates when turtle moves
## 📊 What Happens When You Run It
```
1. Start scripts → Install dependencies → Launch servers
2. Node.js Server starts → Opens HTTP (3001) & WebSocket (3002)
3. React App starts → Vite dev server (3000) → Opens browser
4. Browser connects → WebSocket established → Shows "Connected"
5. In Minecraft → Run webbridge.lua → Listens for turtles
6. Run turtle.lua → Broadcasts status → Bridge forwards to server
7. Server → WebSocket → React → Updates UI → Turtle appears!
8. Click "Explore" → Command queued → Bridge polls → Turtle moves
9. Real-time updates → 3D map animates → You see turtle exploring!
```
## 🎉 Success Looks Like
When everything works:
- ✅ Terminal shows "Server ready!" and "Vite dev server running"
- ✅ Browser shows turtle control interface
- ✅ Green "Connected" indicator in top-right
- ✅ Minecraft bridge shows "Sent to server" messages
- ✅ Turtles appear in left panel
- ✅ 3D map shows turtle markers
- ✅ Commands execute when clicked
- ✅ Map updates as turtles move
## 🚨 If Something's Wrong
1. Check server terminal for errors
2. Check browser console (F12)
3. Check Minecraft computer console
4. Read TROUBLESHOOTING.md
5. Verify all files exist
6. Try restarting everything
## 🎓 Learning Resources
**React**: https://react.dev/
**Three.js**: https://threejs.org/
**Node.js**: https://nodejs.org/
**ComputerCraft**: https://tweaked.cc/
**WebSocket**: https://developer.mozilla.org/en-US/docs/Web/API/WebSocket
## 🌟 Next Steps
Once you have it running:
1. Try controlling multiple turtles
2. Set up GPS for accurate tracking
3. Watch autonomous mining in action
4. Customize the UI colors/layout
5. Add new commands
6. Share with friends on your network!
## 📈 Possible Enhancements
Future ideas:
- [ ] Path recording and playback
- [ ] Task scheduling
- [ ] Multi-turtle coordination
- [ ] Database persistence
- [ ] User authentication
- [ ] Mobile app
- [ ] Voice commands
- [ ] Mining statistics
- [ ] Area visualization
- [ ] Turtle groups/teams
## 🎊 Congratulations!
You've successfully set up a complete full-stack web application for Minecraft turtle control!
**You now have:**
- Modern React web interface
- Real-time 3D visualization
- WebSocket communication
- Node.js backend server
- Minecraft integration
- Complete documentation
**Enjoy your new turtle command center!** 🐢✨
---
Need help? Check the documentation files or troubleshooting guide!
**Happy mining!** ⛏️

408
TROUBLESHOOTING.md Normal file
View File

@@ -0,0 +1,408 @@
# 🔧 Troubleshooting Guide
## Installation Issues
### Node.js Not Found
**Problem:** `command not found: node` or `'node' is not recognized`
**Solution:**
1. Install Node.js from https://nodejs.org/ (LTS version recommended)
2. Restart your terminal after installation
3. Verify: `node --version` should show v18.0.0 or higher
### npm Install Fails
**Problem:** Errors during `npm install`
**Solutions:**
- Clear npm cache: `npm cache clean --force`
- Delete `node_modules` folders and try again
- Update npm: `npm install -g npm@latest`
- Check your internet connection
- Try: `npm install --legacy-peer-deps`
## Server Issues
### Port Already in Use
**Problem:** `Error: listen EADDRINUSE: address already in use :::3001`
**Solution 1 - Change Ports:**
1. Edit `server/server.js`:
```javascript
const PORT = 3005; // Change from 3001
const WS_PORT = 3006; // Change from 3002
```
2. Edit `client/src/store/turtleStore.js`:
```javascript
const WS_URL = 'ws://localhost:3006';
const API_URL = 'http://localhost:3005';
```
**Solution 2 - Kill Existing Process:**
Linux/Mac:
```bash
lsof -ti:3001 | xargs kill -9
lsof -ti:3002 | xargs kill -9
```
Windows:
```cmd
netstat -ano | findstr :3001
taskkill /PID <PID_NUMBER> /F
```
### Server Won't Start
**Problem:** Server crashes on start
**Checklist:**
- [ ] Dependencies installed? Run `cd server && npm install`
- [ ] Node.js version 18+? Check with `node --version`
- [ ] Firewall blocking ports? Add exception for ports 3001-3002
- [ ] Check server logs for specific error messages
## Client Issues
### White Screen / Nothing Renders
**Problem:** Browser shows blank page
**Solutions:**
1. Open browser console (F12) and check for errors
2. Verify server is running: http://localhost:3001/api/turtles
3. Clear browser cache (Ctrl+Shift+Delete)
4. Try incognito/private mode
5. Try different browser (Chrome/Firefox recommended)
### WebSocket Connection Failed
**Problem:** Red "Disconnected" status in top-right
**Solutions:**
1. Check server console for errors
2. Verify WebSocket port (3002) is open
3. Check browser console for WebSocket errors
4. Ensure server URL is correct in `turtleStore.js`
5. Firewall may be blocking WebSocket - add exception
### 3D Map Not Showing
**Problem:** Map area is black or shows errors
**Solutions:**
- WebGL not supported: Update graphics drivers
- Browser compatibility: Use Chrome 90+ or Firefox 88+
- Hardware acceleration disabled: Enable in browser settings
- Chrome: Settings → System → Use hardware acceleration
- Firefox: Settings → Performance → Use hardware acceleration
- GPU issues: Try different browser
## Minecraft/ComputerCraft Issues
### Bridge Computer Not Connecting
**Problem:** `webbridge.lua` runs but no connection to server
**Checklist:**
1. Verify SERVER_URL is correct:
```lua
local SERVER_URL = "http://localhost:3001"
```
- Use computer's IP if server is remote
- Don't use `https://` (use `http://`)
2. Check HTTP API is enabled:
- Edit `config/computercraft-server.toml` or `.minecraft/config/computercraft.cfg`
- Find: `enable_http = true`
- Ensure your server URL is not in `blocked_domains`
3. Test HTTP manually:
```lua
local response = http.get("http://localhost:3001/api/turtles")
print(response ~= nil) -- Should print true
```
### Turtles Not Appearing in Web
**Problem:** Bridge runs, but no turtles show up
**Diagnosis:**
1. Check bridge computer console for "Status from Turtle X" messages
2. Check server console for "Status update from Turtle X" messages
3. Check web browser console for WebSocket messages
**Common Causes:**
- Channel mismatch - Verify all scripts use same channels:
- `turtle.lua`: STATUS_CHANNEL = 102
- `webbridge.lua`: STATUS_CHANNEL = 102
- Modem not equipped on turtle
- Turtles out of wireless range
- Turtles not running `turtle.lua` script
### GPS Not Working
**Problem:** Turtles show "No GPS"
**Solution - Set Up GPS Hosts:**
1. Place 4 computers at high altitude (Y=200+)
2. Arrange in square pattern (20+ blocks apart)
3. Give each a wireless modem
4. Run on each:
```lua
gps host X Y Z
```
Replace X Y Z with that computer's actual coordinates
5. Test from turtle:
```lua
x, y, z = gps.locate(5)
print(x, y, z) -- Should print coordinates
```
### Modem Not Found
**Problem:** `No wireless modem found!`
**Solutions:**
- Craft wireless modem (Stone + Ender Pearl)
- Equip to turtle: Right-click turtle with modem
- For computer: Place modem adjacent and right-click
- Verify: `peripheral.find("modem")` should not be nil
### Commands Not Reaching Turtles
**Problem:** Click commands in web but turtle doesn't respond
**Debug Steps:**
1. Check server receives command:
- Server console should show: `Command queued for turtle X`
2. Check bridge polls for commands:
- Bridge console should show: `Command for Turtle X: <command>`
3. Check turtle receives command:
- Add debug to `turtle.lua`:
```lua
print("Received message:", textutils.serialize(message))
```
4. Verify channels match:
- Web → Server → Bridge → Modem Channel 100 → Turtle
## Performance Issues
### Laggy 3D Map
**Solutions:**
- Reduce number of visible turtles
- Lower browser zoom level
- Close other browser tabs
- Update graphics drivers
- Try lower graphics settings in browser
### Server Using Too Much Memory
**Solutions:**
- Restart server periodically
- Reduce `statusUpdateInterval` in turtle.lua (less frequent updates)
- Clear old turtle data: restart server
### High Network Usage
**Solutions:**
- Increase `statusUpdateInterval` in turtle.lua (default: 5 seconds)
- Reduce number of active turtles
- Use local server instead of remote
## Browser-Specific Issues
### Chrome
**WebSocket closes immediately:**
- Disable browser extensions (especially ad blockers)
- Clear site data: F12 → Application → Clear storage
### Firefox
**3D map performance poor:**
- Enable WebRender: about:config → gfx.webrender.all → true
### Safari
**Not recommended** - Limited WebGL support. Use Chrome or Firefox.
## Network Issues
### Can't Access from Other Devices
**Problem:** Works on localhost but not from other computers
**Solution:**
1. Get your computer's IP address:
```bash
# Linux/Mac
ifconfig | grep "inet "
# Windows
ipconfig
```
Example: 192.168.1.100
2. Update configs to use IP instead of localhost:
**webbridge.lua:**
```lua
local SERVER_URL = "http://192.168.1.100:3001"
```
**client/src/store/turtleStore.js:**
```javascript
const WS_URL = 'ws://192.168.1.100:3002';
const API_URL = 'http://192.168.1.100:3001';
```
3. Configure firewall:
**Linux:**
```bash
sudo ufw allow 3000:3002/tcp
```
**Windows:**
- Windows Defender Firewall → Advanced Settings
- Inbound Rules → New Rule
- Port: 3000-3002
- Allow the connection
4. Access from other device: `http://192.168.1.100:3000`
### Minecraft Server and Web Server on Different Machines
**Setup:**
- Computer A: Minecraft + Bridge
- Computer B: Node.js Server + Web Client
**Configuration:**
1. On Computer A (Minecraft), edit `webbridge.lua`:
```lua
local SERVER_URL = "http://<Computer_B_IP>:3001"
```
2. On Computer B, server.js needs no changes
3. Access web interface from anywhere: `http://<Computer_B_IP>:3000`
## Common Error Messages
### "Failed to send to server"
**In webbridge.lua console**
**Cause:** Bridge can't reach Node.js server
**Fix:**
- Verify server is running
- Check SERVER_URL is correct
- Test: `http.get(SERVER_URL .. "/api/turtles")`
### "Turtle not found"
**In server logs**
**Cause:** Turtle ID not in server's turtle map
**Fix:**
- Turtle hasn't sent status update yet - wait 5 seconds
- Restart turtle script
- Check turtle is broadcasting on correct channel
### "WebSocket connection failed"
**In browser console**
**Cause:** Can't connect to WebSocket server
**Fix:**
- Verify server is running
- Check port 3002 is open
- Update WS_URL in turtleStore.js
- Disable VPN/proxy
## Still Having Issues?
### Collect Debug Information
1. **Server logs**: Copy output from server terminal
2. **Browser console**: F12 → Console tab → Copy errors
3. **Minecraft logs**: Check bridge computer output
4. **Network test**: Can you access http://localhost:3001/api/turtles?
### Reset Everything
Sometimes easiest to start fresh:
```bash
# Stop all servers (Ctrl+C)
# Clean and reinstall
rm -rf server/node_modules client/node_modules
cd server && npm install
cd ../client && npm install
# Restart
./start.sh # or start.bat on Windows
```
### Check Versions
```bash
node --version # Should be v18.0.0+
npm --version # Should be v9.0.0+
```
### Test Connectivity
**Test 1 - Server Health:**
```bash
curl http://localhost:3001/api/turtles
```
Should return: `{"turtles":[]}`
**Test 2 - WebSocket:**
Open browser console and run:
```javascript
const ws = new WebSocket('ws://localhost:3002');
ws.onopen = () => console.log('Connected!');
ws.onerror = (e) => console.log('Error:', e);
```
**Test 3 - Minecraft HTTP:**
In ComputerCraft:
```lua
local r = http.get("http://localhost:3001/api/turtles")
print(r ~= nil)
```
---
**If all else fails:**
- Check the full README.md
- Review ARCHITECTURE.md for system design
- Ensure all dependencies are installed
- Try example commands manually
- Double-check all configuration files match
**Still stuck?** Create an issue with:
- Error messages
- Server/client logs
- Steps to reproduce
- Your configuration

304
UI_IMPROVEMENTS.md Normal file
View File

@@ -0,0 +1,304 @@
# 🎨 UI/UX Improvements Applied
## ✅ Completed Changes
### 1. **3D Map Enhancements**
#### Turtle Model
- ✅ Replaced spinning cube with **proper turtle model**
- Shell, head, legs, tail, eyes with yellow glow
- Bobbing animation when selected
- Faces correct direction based on `facing` value
- Color-coded by mode (exploring=blue, returning=orange, idle=gray)
#### World Block Visualization
-**Blocks are now detected and displayed**
- Turtle scans forward, up, and down blocks
- Blocks stored in server's `worldBlocks` Map
- Rendered as semi-transparent colored cubes
- Color-coded by block type:
- 💎 Diamond = Cyan
- 💚 Emerald = Green
- 🟡 Gold = Gold
- ⚪ Iron = Light gray
- ⚫ Coal = Black
- 🔴 Redstone = Red
- 🔵 Lapis = Blue
- 🟠 Copper = Orange
- 🪨 Stone/Dirt/etc = Gray/Brown
#### Persistence
-**Blocks stored long-term in server**
- `worldBlocks` Map persists during server runtime
- Each block includes: position, name, metadata, discoveredBy, timestamp
- Sent to new clients on connection
- Updated as turtles discover new blocks
### 2. **Server Enhancements**
```javascript
// Added world block storage
const worldBlocks = new Map(); // "x,y,z" -> block data
// Helper functions
- storeBlock(x, y, z, blockData, turtleID)
- getBlockPosition(turtlePos, facing, direction)
// New endpoint
GET /api/world/blocks - Returns all discovered blocks
// Enhanced turtle updates
- Processes surroundings data from turtles
- Calculates absolute block positions
- Stores in worldBlocks Map
```
### 3. **Turtle Script Enhancements**
```lua
-- broadcastStatus() now includes:
surroundings = {
forward = {name = "minecraft:stone", metadata = 0},
up = {name = "minecraft:dirt", metadata = 0},
down = {name = "minecraft:bedrock", metadata = 0}
}
```
### 4. **Client Store Updates**
```javascript
// New state
worldBlocks: []
// New function
updateBlocksFromSurroundings(turtle)
- Calculates block positions from turtle data
- Adds to worldBlocks array
- Prevents duplicates
```
## 📋 Remaining UI Improvements
### Priority 1: Responsive Layout
```css
/* Add to ControlPanel.css */
@media (max-width: 1024px) {
.app-container {
flex-direction: column;
}
.map-view {
height: 50vh;
}
.control-panel {
height: 50vh;
}
}
@media (max-width: 768px) {
.turtle-grid {
grid-template-columns: 1fr;
}
.command-grid {
grid-template-columns: repeat(2, 1fr);
}
}
```
### Priority 2: Item Icons in Inventory
Create `client/src/components/InventoryItem.jsx`:
```jsx
import React from 'react';
const ITEM_ICONS = {
'minecraft:diamond': '💎',
'minecraft:emerald': '💚',
'minecraft:gold_ore': '🟡',
'minecraft:iron_ore': '⚪',
'minecraft:coal': '⚫',
'minecraft:redstone': '🔴',
'minecraft:dirt': '🟤',
'minecraft:stone': '🪨',
'minecraft:cobblestone': '⬜',
// Add more...
};
export function InventoryItem({ item }) {
const icon = ITEM_ICONS[item.name] || '📦';
const shortName = item.name.replace('minecraft:', '').replace('_', ' ');
return (
<div className="inventory-item">
<span className="item-icon">{icon}</span>
<span className="item-name">{shortName}</span>
<span className="item-count">×{item.count}</span>
</div>
);
}
```
### Priority 3: Better Inventory Display
Update `ControlPanel.jsx`:
```jsx
import { InventoryItem } from './InventoryItem';
// In TurtleDetails component:
<div className="detail-section">
<h3>Inventory ({turtle.inventoryCount}/16)</h3>
<div className="inventory-grid">
{turtle.inventory && turtle.inventory.map((item, idx) => (
<InventoryItem key={idx} item={item} />
))}
</div>
</div>
```
```css
/* Add to ControlPanel.css */
.inventory-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0.5rem;
}
.inventory-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 0.75rem;
background: #1e293b;
border: 2px solid #334155;
border-radius: 0.5rem;
transition: all 0.2s;
}
.inventory-item:hover {
border-color: #60a5fa;
transform: translateY(-2px);
}
.item-icon {
font-size: 2rem;
margin-bottom: 0.25rem;
}
.item-name {
font-size: 0.75rem;
color: #94a3b8;
text-align: center;
text-transform: capitalize;
}
.item-count {
font-size: 0.875rem;
color: #f1f5f9;
font-weight: 600;
margin-top: 0.25rem;
}
```
### Priority 4: Intuitive Controls
```jsx
// Add tooltips to buttons
<button
title="Start autonomous exploration - turtle will mine valuable ores"
onClick={() => handleCommand('explore')}
>
🔍 Explore
</button>
// Add keyboard shortcuts
useEffect(() => {
const handleKeyPress = (e) => {
if (!turtle) return;
switch(e.key) {
case 'w': handleCommand('forward'); break;
case 's': handleCommand('back'); break;
case 'a': handleCommand('turnLeft'); break;
case 'd': handleCommand('turnRight'); break;
case ' ': handleCommand('up'); break;
case 'Shift': handleCommand('down'); break;
}
};
window.addEventListener('keydown', handleKeyPress);
return () => window.removeEventListener('keydown', handleKeyPress);
}, [turtle]);
```
### Priority 5: Map Controls UI
```jsx
// Add map control panel
<div className="map-controls">
<button onClick={() => resetCamera()}>🎯 Center</button>
<button onClick={() => toggleGrid()}>📏 Grid</button>
<button onClick={() => toggleBlocks()}>🧱 Blocks</button>
<label>
Opacity:
<input
type="range"
min="0"
max="100"
value={blockOpacity}
onChange={(e) => setBlockOpacity(e.target.value)}
/>
</label>
</div>
```
## 🚀 How to Apply
1. **Server changes** - Already applied ✅
2. **Turtle changes** - Already applied ✅
3. **Client Map3D** - Already applied ✅
4. **Client Store** - Already applied ✅
### To activate:
```bash
# Restart server
cd server && npm start
# Rebuild client
cd client && npm run build
# Or for development
npm run dev
# In Minecraft
# Upload updated turtle.lua and restart
# Restart webbridge
```
## 🎯 Expected Results
1. **Map shows actual discovered blocks** with colors
2. **Turtle looks like a turtle** with proper facing direction
3. **Blocks persist** across page refreshes (while server runs)
4. **Real-time block discovery** as turtle explores
5. **Performance optimized** - blocks grouped by color for efficient rendering
## 📊 Performance Notes
- Blocks are grouped by color in Map3D for efficient rendering
- Semi-transparent (60% opacity) to see through
- No textures needed - uses colored cubes
- Could add thousands of blocks without performance issues
## 🔮 Future Enhancements
1. **Block textures** - Load Minecraft texture atlas
2. **Persistent storage** - Save to database
3. **Export/Import** - Download/upload world data
4. **Minimap** - 2D top-down view
5. **Block filtering** - Show only certain block types
6. **Heatmap** - Visualize ore density

290
WEBBRIDGE_FIX.md Normal file
View File

@@ -0,0 +1,290 @@
# WebBridge Communication Fix
## Problem
The webbridge communication was unreliable, requiring frequent restarts. Commands would get lost between the server → webbridge → turtle chain.
## Root Causes Identified
### 1. **Race Condition on Command Clearing**
- Server would immediately clear commands when polled
- If turtle wasn't listening at that exact moment, commands were lost forever
- No retry mechanism
### 2. **Too Frequent Polling**
- Polling every 1 second was too aggressive
- Created more opportunities for timing issues
- Increased network overhead
### 3. **Single Transmission**
- Commands were transmitted only once over wireless modem
- If turtle was busy or signal was weak, command was lost
- No acknowledgment system
## Solutions Implemented
### 1. **Server-Side: Smart Command Management** (`server/server.js`)
**Added timing-based command retention:**
```javascript
// Don't clear commands immediately
if (!turtle.lastCommandPollTime || (Date.now() - turtle.lastCommandPollTime) > 5000) {
// First poll or > 5 seconds since last poll - send commands
turtle.lastCommandPollTime = Date.now();
res.json({ commands });
} else {
// Recent poll - assume previous commands were received, clear them
turtle.pendingCommands = [];
res.json({ commands: [] });
}
```
**Benefits:**
- Commands stay available for multiple poll cycles
- Automatic clearing after 5 seconds (prevents stale commands)
- Webbridge can retry if first attempt fails
**Added explicit acknowledgment endpoint:**
```javascript
POST /api/turtle/:id/commands/ack
```
- Webbridge can explicitly confirm commands were sent
- Server clears commands only after confirmation
- Better tracking and logging
### 2. **Webbridge: Improved Reliability** (`webbridge.lua`)
**Reduced polling frequency:**
```lua
local POLL_INTERVAL = 2 -- Changed from 1 to 2 seconds
```
- Less aggressive polling reduces race conditions
- Gives turtle more time to process
- Reduces server load
**Multiple transmissions per command:**
```lua
-- Send command 3 times for reliability
for i = 1, 3 do
modem.transmit(COMMAND_CHANNEL, CHANNEL_RECEIVE, commandPacket)
os.sleep(0.05) -- Small delay between retransmissions
end
```
- Increases chance of turtle receiving command
- 50ms delay prevents message collision
- Triple redundancy
**Explicit acknowledgment:**
```lua
-- After sending all commands
os.sleep(0.5) -- Give turtle time to receive
if acknowledgeCommands(turtleID) then
addLog(" ACK: Commands acknowledged", colors.lime)
end
```
- Waits 500ms for turtle to receive
- Sends acknowledgment to server
- Server can safely clear commands
**Better logging:**
```lua
addLog("Received " .. #commands .. " command(s) for Turtle #" .. turtleID, colors.cyan)
addLog(" CMD: " .. cmd.command .. " -> Turtle #" .. turtleID, colors.yellow)
addLog(" ACK: Commands acknowledged", colors.lime)
```
- Clearer status tracking
- Easier debugging
- Visual feedback on monitor
### 3. **Docker Fix** (`server/Dockerfile`)
**Added missing database.js:**
```dockerfile
COPY server.js ./
COPY database.js ./ # <-- ADDED
```
- Server was crashing in Docker because database.js wasn't copied
- This was causing the "module not found" errors
## Communication Flow (New)
### Before Fix:
```
1. Web UI sends command → Server adds to queue
2. Webbridge polls → Server sends commands → Server CLEARS immediately
3. Webbridge transmits ONCE to turtle
4. If turtle missed it → COMMAND LOST FOREVER
```
### After Fix:
```
1. Web UI sends command → Server adds to queue
2. Webbridge polls → Server sends commands → Server KEEPS for 5s
3. Webbridge transmits 3 TIMES to turtle (redundancy)
4. Wait 500ms for turtle to receive
5. Webbridge sends ACK → Server clears commands
6. If ACK fails → Commands still in queue for next poll
```
## Expected Improvements
### ✅ **No More Lost Commands**
- Commands are retried automatically
- Multiple transmissions increase success rate
- 5-second window allows for retries
### ✅ **Better Reliability**
- Explicit acknowledgment system
- Commands don't disappear prematurely
- Graceful handling of network issues
### ✅ **Less Restart Required**
- System self-heals from temporary issues
- No need to restart webbridge after missed commands
- More robust against timing problems
### ✅ **Better Observability**
- Enhanced logging shows command flow
- Monitor displays acknowledgment status
- Easier to debug issues
## Testing Recommendations
1. **Send Multiple Commands Rapidly**
- Commands should all arrive
- No commands should be lost
- Check webbridge monitor for ACK messages
2. **Test With Busy Turtle**
- Send command while turtle is exploring
- Command should still arrive
- Multiple transmissions help
3. **Test Network Issues**
- Move turtle far away (weak signal)
- Commands should still arrive (3 tries)
- If all fail, they retry on next poll
4. **Monitor Logs**
- Server shows command sends and ACKs
- Webbridge shows transmission and ACK
- Turtle shows command receipt
## Deployment Steps
1. **Update Docker Container:**
```bash
cd /home/mayatheshy/remoteturtle
docker compose down
docker compose build
docker compose up -d
```
2. **Update Webbridge (In Minecraft):**
- Stop current webbridge (Ctrl+T)
- Upload new webbridge.lua
- Restart: `webbridge`
3. **Turtle Code (No Changes Needed)**
- Existing turtle.lua works with new system
- No updates required
## Configuration
### Tunable Parameters
**Server (server.js):**
- `lastCommandPollTime` threshold: `5000ms` (5 seconds)
- Increase for slower networks
- Decrease for faster response
**Webbridge (webbridge.lua):**
- `POLL_INTERVAL`: `2` seconds
- Increase for slower networks
- Decrease for faster response (but more overhead)
- Transmission retries: `3` times
- Increase for very weak signals
- Decrease to reduce spam
- ACK delay: `0.5` seconds
- Increase if turtles are very busy
- Decrease for faster acknowledgment
## Monitoring
### Server Console Output:
```
📤 Sending 1 command(s) to turtle 42
- forward
✅ Turtle 42 acknowledged 1 command(s)
```
### Webbridge Monitor:
```
[10:30:15] Received 1 command(s) for Turtle #42
[10:30:15] CMD: forward -> Turtle #42
[10:30:16] ACK: Commands acknowledged
```
### Turtle Output:
```
Modem message on channel 100
Target: 42
My ID: 42
Command: forward
Executing command...
```
## Troubleshooting
### Commands Still Not Arriving?
1. **Check Server Logs:**
- Is server sending commands?
- Are ACKs being received?
2. **Check Webbridge Monitor:**
- Is it polling?
- Is it transmitting?
- Are ACKs succeeding?
3. **Check Turtle:**
- Is modem open on channel 100?
- Is command processing loop running?
- Check for error messages
4. **Check Network:**
- Are turtles within wireless modem range?
- Is webbridge computer within range?
- Try moving closer
### High Failure Rate?
1. **Increase Transmissions:**
- Change `for i = 1, 3` to `for i = 1, 5`
- More redundancy
2. **Increase Poll Interval:**
- Change `POLL_INTERVAL = 2` to `POLL_INTERVAL = 3`
- More time between attempts
3. **Check Signal Strength:**
- Use ender modems for unlimited range
- Add more webbridge relays
## Performance Impact
- **Server:** Minimal (one extra endpoint)
- **Webbridge:** Slightly higher (3x transmissions, but 2s polling)
- **Turtle:** No change (same command processing)
- **Network:** Higher wireless traffic (3x per command), but more reliable
## Version History
- **v1.0** - Original implementation (1s polling, single transmission)
- **v2.0** - Current fix (2s polling, 3x transmission, acknowledgment)
---
**Last Updated:** February 20, 2026
**Status:** ✅ Ready for Testing

25
client/Dockerfile Normal file
View File

@@ -0,0 +1,25 @@
# Production build with simple HTTP server
FROM node:18-alpine
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm install
# Copy source code
COPY . .
# Build the app
RUN npm run build
# Install serve globally for serving static files
RUN npm install -g serve
# Expose port
EXPOSE 3000
# Serve built files with serve (no host restrictions)
CMD ["serve", "-s", "dist", "-l", "3000"]

19
client/Dockerfile.dev Normal file
View File

@@ -0,0 +1,19 @@
# Development Dockerfile with hot reload
FROM node:18-alpine
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm install
# Copy source code
COPY . .
# Expose Vite dev server port
EXPOSE 3000
# Start Vite dev server
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]

View File

@@ -6,7 +6,8 @@
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
"preview": "vite preview",
"serve": "vite preview --host 0.0.0.0 --port 3000"
},
"dependencies": {
"react": "^18.2.0",

View File

@@ -4,37 +4,7 @@
display: flex;
flex-direction: column;
overflow: hidden;
}
.view-controls {
display: flex;
gap: 0.5rem;
padding: 0.75rem;
background: #1e293b;
border-bottom: 2px solid #334155;
}
.view-controls button {
padding: 0.5rem 1rem;
border: none;
background: #334155;
color: #e2e8f0;
border-radius: 0.375rem;
font-size: 0.875rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.view-controls button:hover {
background: #475569;
transform: translateY(-1px);
}
.view-controls button.active {
background: #3b82f6;
color: white;
box-shadow: 0 0 10px rgba(59, 130, 246, 0.5);
background: #2c2c2c;
}
.app-content {
@@ -49,28 +19,93 @@
.app-content.split .map-container {
flex: 1;
width: 50%;
}
.app-content.split .panel-container {
width: 600px;
}
.app-content.map .panel-container {
display: none;
}
.app-content.panel .map-container {
display: none;
flex: 1;
width: 50%;
}
.map-container {
position: relative;
background: #0a0e1a;
background: #2c2c2c;
}
.panel-container {
background: #0f172a;
background: #2c2c2c;
overflow: hidden;
display: flex;
flex-direction: column;
}
.panel-tabs {
display: flex;
gap: 0.25rem;
padding: 0.5rem;
background: #3b3b3b;
border-bottom: 3px solid #1a1a1a;
overflow-x: auto;
flex-shrink: 0;
}
.panel-tabs button {
padding: 0.5rem 1rem;
border: 2px solid #1a1a1a;
background: #5a5a5a;
color: #b0b0b0;
border-radius: 0;
font-size: 0.875rem;
font-weight: 700;
cursor: pointer;
transition: all 0.1s;
white-space: nowrap;
font-family: 'Silkscreen', 'Courier New', monospace;
text-shadow: 1px 1px 0 #1a1a1a;
box-shadow: inset 0 -2px 0 #444, inset 0 2px 0 #777;
}
.panel-tabs button:hover {
background: #6b6b6b;
color: #e5e7eb;
}
.panel-tabs button.active {
background: #4a8c2a;
color: white;
box-shadow: inset 0 2px 0 #6ab04c, inset 0 -2px 0 #2d6b1a;
}
/* === Cross-link Button === */
.cross-link-btn {
display: flex;
align-items: center;
gap: 0.25rem;
padding: 0.5rem 1rem;
border: 2px solid #1a1a1a;
background: #2e4a8b;
color: #55ffff;
font-size: 0.875rem;
font-weight: 700;
font-family: 'Silkscreen', 'Courier New', monospace;
text-shadow: 1px 1px 0 #1a1a1a;
text-decoration: none;
cursor: pointer;
transition: all 0.1s;
box-shadow: inset 0 2px 0 #4466bb, inset 0 -2px 0 #1a2a66;
white-space: nowrap;
}
.cross-link-btn:hover {
background: #3e5a9b;
color: white;
}
.panel-content-wrapper {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
}
/* Scrollbar styling */
@@ -80,14 +115,123 @@
}
::-webkit-scrollbar-track {
background: #1e293b;
background: #2c2c2c;
}
::-webkit-scrollbar-thumb {
background: #475569;
border-radius: 4px;
background: #5a5a5a;
border: 1px solid #1a1a1a;
}
::-webkit-scrollbar-thumb:hover {
background: #64748b;
background: #6b6b6b;
}
/* Mobile-Responsive Design */
@media (max-width: 1024px) {
/* Tablet: Stack vertically */
.app-content.split {
flex-direction: column;
}
.app-content.split .map-container,
.app-content.split .panel-container {
width: 100%;
flex: 1;
}
}
@media (max-width: 768px) {
.app-content.split {
flex-direction: column;
}
.app-content.split .map-container {
height: 50vh;
}
.app-content.split .panel-container {
height: 50vh;
overflow-y: auto;
}
/* Optimize for touch */
button, .turtle-card, .inventory-slot {
min-height: 44px; /* iOS minimum touch target */
}
}
@media (max-width: 480px) {
.app-content.split .map-container {
height: 40vh;
}
.app-content.split .panel-container {
height: 60vh;
}
}
/* Touch device optimizations */
@media (hover: none) and (pointer: coarse) {
/* Increase touch targets */
button {
min-height: 48px;
min-width: 48px;
}
.turtle-card {
padding: 1rem;
}
.inventory-slot {
min-height: 60px;
min-width: 60px;
}
/* Remove hover effects on touch devices */
button:hover,
.turtle-card:hover,
.inventory-slot:hover {
transform: none;
}
/* Use active state instead */
button:active {
transform: scale(0.95);
}
}
/* Landscape mobile orientation */
@media (max-width: 896px) and (orientation: landscape) {
.app-content.split {
flex-direction: row;
}
.app-content.split .map-container,
.app-content.split .panel-container {
width: 50%;
height: 100%;
}
}
/* Dark mode support (respects system preference) */
@media (prefers-color-scheme: dark) {
/* Already optimized for dark mode */
}
/* Reduced motion support */
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
/* High contrast mode */
@media (prefers-contrast: high) {
.turtle-card {
border-width: 3px;
}
}

View File

@@ -1,51 +1,122 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useState } from 'react';
import Map3D from './components/Map3D';
import ControlPanel from './components/ControlPanel';
import VoiceControl from './components/VoiceControl';
import StatsPanel from './components/StatsPanel';
import GroupsPanel from './components/GroupsPanel';
import TaskPanel from './components/TaskPanel';
import PathRecorder from './components/PathRecorder';
import MiningAreasPanel from './components/MiningAreasPanel';
import { useTurtleStore } from './store/turtleStore';
import './App.css';
function App() {
const connect = useTurtleStore((state) => state.connect);
const [view, setView] = useState('split'); // 'split', 'map', 'panel'
const [panelTab, setPanelTab] = useState('control'); // 'control', 'voice', 'stats', 'groups', 'tasks', 'paths', 'areas'
const turtles = useTurtleStore((state) => state.getTurtleArray());
const selectedTurtle = useTurtleStore((state) => state.getSelectedTurtle());
const inventoryDashboardUrl = import.meta.env.VITE_INVENTORY_DASHBOARD_URL || `${window.location.protocol}//${window.location.hostname}`;
useEffect(() => {
connect();
}, [connect]);
const renderPanelContent = () => {
const apiUrl = import.meta.env.VITE_API_URL || `${window.location.protocol}//${window.location.host}`;
const wsUrl = import.meta.env.VITE_WS_URL || `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/ws`;
switch (panelTab) {
case 'control':
return <ControlPanel />;
case 'voice':
return <VoiceControl turtles={turtles} selectedTurtle={selectedTurtle} apiUrl={apiUrl} />;
case 'stats':
return <StatsPanel selectedTurtle={selectedTurtle} apiUrl={apiUrl} />;
case 'groups':
return <GroupsPanel turtles={turtles} apiUrl={apiUrl} wsUrl={wsUrl} />;
case 'tasks':
return <TaskPanel turtles={turtles} apiUrl={apiUrl} />;
case 'paths':
return <PathRecorder turtles={turtles} selectedTurtle={selectedTurtle} apiUrl={apiUrl} />;
case 'areas':
return <MiningAreasPanel turtles={turtles} selectedTurtle={selectedTurtle} apiUrl={apiUrl} />;
default:
return <ControlPanel />;
}
};
return (
<div className="app">
<div className="view-controls">
<button
className={view === 'split' ? 'active' : ''}
onClick={() => setView('split')}
>
📊 Split View
</button>
<button
className={view === 'map' ? 'active' : ''}
onClick={() => setView('map')}
>
🗺 Map Only
</button>
<button
className={view === 'panel' ? 'active' : ''}
onClick={() => setView('panel')}
>
🎮 Control Only
</button>
</div>
<div className={`app-content ${view}`}>
{(view === 'split' || view === 'map') && (
<div className="map-container">
<Map3D />
<div className="app-content split">
<div className="map-container">
<Map3D />
</div>
<div className="panel-container">
<div className="panel-tabs">
<a
href={inventoryDashboardUrl}
className="cross-link-btn"
title="Open Inventory Manager Dashboard"
target="_blank"
rel="noopener noreferrer"
>
📦 Inventory
</a>
<button
className={panelTab === 'control' ? 'active' : ''}
onClick={() => setPanelTab('control')}
title="Turtle Control"
>
🎮 Control
</button>
<button
className={panelTab === 'voice' ? 'active' : ''}
onClick={() => setPanelTab('voice')}
title="Voice Commands"
>
🎤 Voice
</button>
<button
className={panelTab === 'stats' ? 'active' : ''}
onClick={() => setPanelTab('stats')}
title="Mining Statistics"
>
📊 Stats
</button>
<button
className={panelTab === 'groups' ? 'active' : ''}
onClick={() => setPanelTab('groups')}
title="Turtle Groups"
>
👥 Groups
</button>
<button
className={panelTab === 'tasks' ? 'active' : ''}
onClick={() => setPanelTab('tasks')}
title="Task Queue"
>
📋 Tasks
</button>
<button
className={panelTab === 'paths' ? 'active' : ''}
onClick={() => setPanelTab('paths')}
title="Path Recording"
>
🛤 Paths
</button>
<button
className={panelTab === 'areas' ? 'active' : ''}
onClick={() => setPanelTab('areas')}
title="Mining Areas"
>
Areas
</button>
</div>
<div className="panel-content-wrapper">
{renderPanelContent()}
</div>
</div>
)}
{(view === 'split' || view === 'panel') && (
<div className="panel-container">
<ControlPanel />
</div>
)}
</div>
</div>
);

File diff suppressed because it is too large Load Diff

View File

@@ -1,31 +1,44 @@
import React from 'react';
import React, { useState, useCallback } from 'react';
import { useTurtleStore } from '../store/turtleStore';
import './ControlPanel.css';
function TurtleCard({ turtle, isSelected, onSelect }) {
const mode = turtle.mode || 'unknown';
const activeState = turtle.state || turtle.mode || 'idle';
const fuel = turtle.fuel === 'unlimited' ? '∞' : (turtle.fuel || '?');
const inventoryCount = turtle.inventory?.length || 0;
const inventoryCount = Array.isArray(turtle.inventory)
? turtle.inventory.length
: (turtle.inventory ? Object.keys(turtle.inventory).length : 0);
const displayName = turtle.label || `Turtle ${turtle.turtleID}`;
const modeColors = {
mining: '#4ade80',
exploring: '#60a5fa',
returning: '#f59e0b',
idle: '#9ca3af',
manual: '#a78bfa',
unknown: '#6b7280'
mining: '#55ff55',
exploring: '#55ffff',
returning: '#ffaa00',
goHome: '#ffaa00',
idle: '#aaaaaa',
manual: '#ff55ff',
refueling: '#ff5555',
farming: '#55ff55',
dumpInventory: '#aa00aa',
dumping: '#aa00aa',
moving: '#55ffff',
scan: '#5555ff',
extraction: '#ffaa00',
building: '#00aaaa',
autocraft: '#ff55ff',
unknown: '#555555'
};
return (
<div
className={`turtle-card ${isSelected ? 'selected' : ''}`}
onClick={onSelect}
style={{ borderColor: modeColors[mode] }}
style={{ borderColor: modeColors[activeState] || modeColors.unknown }}
>
<div className="turtle-header">
<h3>Turtle {turtle.turtleID}</h3>
<span className="mode-badge" style={{ background: modeColors[mode] }}>
{mode}
<h3>{displayName}</h3>
<span className="mode-badge" style={{ background: modeColors[activeState] || modeColors.unknown }}>
{activeState}
</span>
</div>
@@ -66,7 +79,32 @@ function TurtleCard({ turtle, isSelected, onSelect }) {
}
function TurtleDetails({ turtle }) {
const sendCommand = useTurtleStore((state) => state.sendCommand);
const setTurtleState = useTurtleStore((state) => state.setTurtleState);
const renameTurtle = useTurtleStore((state) => state.renameTurtle);
const equipLeft = useTurtleStore((state) => state.equipLeft);
const equipRight = useTurtleStore((state) => state.equipRight);
const sortInventory = useTurtleStore((state) => state.sortInventory);
const selectSlot = useTurtleStore((state) => state.selectSlot);
const dropItems = useTurtleStore((state) => state.dropItems);
const suckItems = useTurtleStore((state) => state.suckItems);
const connectToInventory = useTurtleStore((state) => state.connectToInventory);
const updateTurtleConfig = useTurtleStore((state) => state.updateTurtleConfig);
const exploreTurtle = useTurtleStore((state) => state.exploreTurtle);
const gpsLocateTurtle = useTurtleStore((state) => state.gpsLocateTurtle);
const moveForward = useTurtleStore((state) => state.moveForward);
const moveBack = useTurtleStore((state) => state.moveBack);
const moveUp = useTurtleStore((state) => state.moveUp);
const moveDown = useTurtleStore((state) => state.moveDown);
const turnLeft = useTurtleStore((state) => state.turnLeft);
const turnRight = useTurtleStore((state) => state.turnRight);
const digBlock = useTurtleStore((state) => state.digBlock);
const digBlockUp = useTurtleStore((state) => state.digBlockUp);
const digBlockDown = useTurtleStore((state) => state.digBlockDown);
const placeBlock = useTurtleStore((state) => state.placeBlock);
const [renameValue, setRenameValue] = useState('');
const [showConfig, setShowConfig] = useState(false);
const [configValues, setConfigValues] = useState({ maxDistance: 200, autoRefuel: true });
if (!turtle) {
return (
@@ -76,27 +114,98 @@ function TurtleDetails({ turtle }) {
);
}
const handleCommand = (command, param = null) => {
sendCommand(turtle.turtleID, command, param);
const handleStateChange = (stateName, data = {}) => {
setTurtleState(turtle.turtleID, stateName, data);
};
const handleRename = async () => {
if (renameValue.trim()) {
await renameTurtle(turtle.turtleID, renameValue.trim());
setRenameValue('');
}
};
const handleSlotClick = async (slotIndex) => {
await selectSlot(turtle.turtleID, slotIndex + 1);
};
const handleConfigSave = async () => {
await updateTurtleConfig(turtle.turtleID, configValues);
setShowConfig(false);
};
const activeState = turtle.state || turtle.mode || 'idle';
const displayName = turtle.label || `Turtle ${turtle.turtleID}`;
return (
<div className="turtle-details">
<h2>Turtle {turtle.turtleID} Control</h2>
<h2>{displayName} <span style={{color: '#aaaaaa', fontSize: '0.8em'}}>#{turtle.turtleID}</span></h2>
{/* Rename + Config bar */}
<div className="detail-section" style={{ display: 'flex', gap: '8px', alignItems: 'center', flexWrap: 'wrap' }}>
<input
type="text"
value={renameValue}
onChange={(e) => setRenameValue(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleRename()}
placeholder="Rename turtle..."
className="rename-input"
style={{ flex: 1, minWidth: '120px', padding: '4px 8px', border: '2px solid #333', background: '#1a1a1a', color: '#e0e0e0' }}
/>
<button onClick={handleRename} className="command-btn" style={{ padding: '4px 12px' }}>📝 Rename</button>
<button onClick={() => setShowConfig(!showConfig)} className="command-btn" style={{ padding: '4px 12px' }}> Config</button>
<button onClick={() => gpsLocateTurtle(turtle.turtleID)} className="command-btn" style={{ padding: '4px 12px' }}>📡 GPS</button>
<button onClick={() => exploreTurtle(turtle.turtleID)} className="command-btn" style={{ padding: '4px 12px' }}>🔎 Inspect</button>
</div>
{/* Config Modal */}
{showConfig && (
<div className="detail-section" style={{ background: '#2c2c2c', padding: '12px', border: '3px solid #1a1a1a' }}>
<h3> Configuration</h3>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px', marginTop: '8px' }}>
<label style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span>Max Distance:</span>
<input type="number" value={configValues.maxDistance} onChange={(e) => setConfigValues({...configValues, maxDistance: parseInt(e.target.value)})} style={{ width: '80px', padding: '2px 6px', border: '2px solid #333', background: '#1a1a1a', color: '#e0e0e0' }} />
</label>
<label style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span>Auto Refuel:</span>
<input type="checkbox" checked={configValues.autoRefuel} onChange={(e) => setConfigValues({...configValues, autoRefuel: e.target.checked})} />
</label>
<div style={{ display: 'flex', gap: '8px', justifyContent: 'flex-end' }}>
<button onClick={() => setShowConfig(false)} className="command-btn" style={{ padding: '4px 12px' }}>Cancel</button>
<button onClick={handleConfigSave} className="command-btn explore" style={{ padding: '4px 12px' }}>💾 Save</button>
</div>
</div>
</div>
)}
<div className="detail-section">
<h3>Status</h3>
<div className="status-grid">
<div className="status-item">
<span className="label">Mode:</span>
<span className="value">{turtle.mode || 'unknown'}</span>
<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>
{turtle.totalSteps > 0 && (
<div className="status-item">
<span className="label">Steps:</span>
<span className="value">
{turtle.totalSteps} total ({turtle.stepsSinceLastRefuel || 0} since refuel)
</span>
</div>
)}
<div className="status-item">
<span className="label">Position:</span>
<span className="value">
@@ -115,67 +224,333 @@ function TurtleDetails({ turtle }) {
}
</span>
</div>
{turtle.error && (
<div className="status-item" style={{ color: '#ff5555' }}>
<span className="label">Error:</span>
<span className="value">{turtle.error}</span>
</div>
)}
{turtle.warning && (
<div className="status-item" style={{ color: '#ffaa00' }}>
<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, info]) => (
<div key={side} className="status-item">
<span className="label" style={{ textTransform: 'capitalize' }}>{side}:</span>
<span className="value" style={{ color: '#ff55ff' }}>
{typeof info === 'string' ? info : (info?.types?.join(', ') || 'unknown')}
</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')}
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')}
title="Autonomous exploration"
style={activeState === 'exploring' ? { outline: '2px solid #fff' } : {}}
>
🔍 Explore
</button>
<button
className={`command-btn mine ${activeState === 'mining' ? 'active' : ''}`}
onClick={() => handleStateChange('mining')}
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')}
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')}
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>Commands</h3>
<div className="command-grid">
<h3>Movement</h3>
<div className="movement-controls">
<div className="movement-row">
<button onClick={() => moveForward(turtle.turtleID)} title="Move forward"></button>
</div>
<div className="movement-row">
<button onClick={() => turnLeft(turtle.turtleID)} title="Turn left"></button>
<button onClick={() => moveBack(turtle.turtleID)} title="Move backward"></button>
<button onClick={() => turnRight(turtle.turtleID)} title="Turn right"></button>
</div>
<div className="movement-row vertical">
<button onClick={() => moveUp(turtle.turtleID)} title="Move up"> Up</button>
<button onClick={() => moveDown(turtle.turtleID)} title="Move down"> Down</button>
</div>
</div>
</div>
<div className="detail-section">
<h3>Actions</h3>
<div className="action-grid">
<button
className="command-btn explore"
onClick={() => handleCommand('explore')}
className="action-btn dig"
onClick={() => digBlock(turtle.turtleID)}
title="Dig block in front"
>
🔍 Explore
Dig
</button>
<button
className="command-btn mine"
onClick={() => handleCommand('mine')}
className="action-btn digup"
onClick={() => digBlockUp(turtle.turtleID)}
title="Dig block above"
>
Mine
Dig Up
</button>
<button
className="command-btn return"
onClick={() => handleCommand('returnHome')}
className="action-btn digdown"
onClick={() => digBlockDown(turtle.turtleID)}
title="Dig block below"
>
🏠 Return Home
Dig Down
</button>
<button
className="command-btn stop"
onClick={() => handleCommand('stop')}
className="action-btn place"
onClick={() => placeBlock(turtle.turtleID)}
title="Place block from inventory"
>
Stop
🧱 Place
</button>
</div>
</div>
<div className="manual-controls">
<h4>Manual Control</h4>
<div className="direction-pad">
<button onClick={() => handleCommand('forward')}></button>
<div className="horizontal-controls">
<button onClick={() => handleCommand('turnLeft')}></button>
<button onClick={() => handleCommand('turnRight')}></button>
</div>
<button onClick={() => handleCommand('back')}></button>
</div>
<div className="vertical-controls">
<button onClick={() => handleCommand('up')}> Up</button>
<button onClick={() => handleCommand('down')}> Down</button>
</div>
{/* Equipment & Inventory Actions */}
<div className="detail-section">
<h3>Equipment & Inventory</h3>
<div className="action-grid">
<button
className="action-btn"
onClick={() => equipLeft(turtle.turtleID)}
title="Equip selected item on left side"
>
🛡 Equip L
</button>
<button
className="action-btn"
onClick={() => equipRight(turtle.turtleID)}
title="Equip selected item on right side"
>
🗡 Equip R
</button>
<button
className="action-btn"
onClick={() => sortInventory(turtle.turtleID)}
title="Sort and compact inventory"
>
📋 Sort
</button>
<button
className="action-btn"
onClick={() => dropItems(turtle.turtleID, 'front')}
title="Drop items forward"
>
📤 Drop
</button>
<button
className="action-btn"
onClick={() => dropItems(turtle.turtleID, 'up')}
title="Drop items upward"
>
Drop Up
</button>
<button
className="action-btn"
onClick={() => dropItems(turtle.turtleID, 'down')}
title="Drop items downward"
>
Drop Down
</button>
<button
className="action-btn"
onClick={() => suckItems(turtle.turtleID, 'front')}
title="Suck items from front"
>
📥 Suck
</button>
<button
className="action-btn"
onClick={() => {
const side = prompt('Enter peripheral side (front/top/bottom/left/right):');
if (side) connectToInventory(turtle.turtleID, side);
}}
title="Read adjacent inventory peripheral contents"
>
📦 Read Inv
</button>
</div>
</div>
{turtle.inventory && turtle.inventory.length > 0 && (
<div className="detail-section">
<h3>Inventory</h3>
<div className="inventory-list">
{turtle.inventory.map((item, index) => (
<div key={index} className="inventory-item">
<span className="item-name">
{item.name.replace('minecraft:', '')}
</span>
<span className="item-count">x{item.count}</span>
</div>
))}
<h3>Inventory ({turtle.inventoryCount || turtle.inventory.length}/16) Slot: {turtle.selectedSlot || 1}</h3>
<div className="inventory-grid">
{Array.from({ length: 16 }, (_, slotIndex) => {
const item = turtle.inventory[slotIndex];
const isSelected = (turtle.selectedSlot || 1) === (slotIndex + 1);
return (
<div
key={slotIndex}
className={`inventory-slot ${item ? 'filled' : 'empty'} ${isSelected ? 'selected-slot' : ''}`}
title={item ? `${item.name.replace('minecraft:', '').replace(/_/g, ' ')} (${item.count}) — Click to select` : `Slot ${slotIndex + 1} — Click to select`}
onClick={() => handleSlotClick(slotIndex)}
style={isSelected ? { outline: '2px solid #55ffff', outlineOffset: '-2px' } : {}}
>
{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>
)}

View File

@@ -0,0 +1,431 @@
/* ============================================
Minecraft-Themed Groups Panel
============================================ */
.groups-panel {
padding: 1.5rem;
background: #2c2c2c;
height: 100%;
overflow-y: auto;
font-family: 'Silkscreen', 'Courier New', monospace;
}
.groups-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.groups-header h2 {
font-size: 1.5rem;
font-weight: 700;
color: #ffff55;
margin: 0;
text-shadow: 2px 2px 0 #1a1a1a;
}
.group-count {
font-size: 0.875rem;
color: #a0a0a0;
font-weight: 600;
}
.message {
padding: 0.75rem 1rem;
border: 2px solid;
margin-bottom: 1rem;
font-size: 0.875rem;
font-weight: 600;
animation: slideIn 0.3s;
}
.message.success {
background: #2d6b1a33;
color: #55ff55;
border-color: #55ff55;
}
.message.error {
background: #6b1a1a33;
color: #ff5555;
border-color: #ff5555;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Create Group Section */
.create-group-section {
background: #3b3b3b;
border: 2px solid #1a1a1a;
padding: 1.5rem;
margin-bottom: 1.5rem;
box-shadow: inset 0 -2px 0 #2a2a2a, inset 0 2px 0 #555;
}
.create-group-section h3 {
font-size: 1rem;
font-weight: 600;
color: #ffaa00;
margin: 0 0 1rem 0;
text-shadow: 1px 1px 0 #1a1a1a;
}
.create-group-form {
display: flex;
flex-direction: column;
gap: 1rem;
}
.create-group-form input {
padding: 0.75rem;
background: #1a1a1a;
border: 2px solid #4b4b4b;
color: #e0e0e0;
font-size: 0.875rem;
font-family: 'Silkscreen', 'Courier New', monospace;
}
.create-group-form input:focus {
outline: none;
border-color: #55ffff;
}
.create-group-form input:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.color-picker {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.color-option {
width: 2.5rem;
height: 2.5rem;
border: 3px solid #1a1a1a;
cursor: pointer;
transition: all 0.1s;
position: relative;
box-shadow: inset 0 -2px 0 rgba(0,0,0,0.3), inset 0 2px 0 rgba(255,255,255,0.2);
}
.color-option:hover {
transform: scale(1.1);
}
.color-option.active {
border-color: #ffff55;
box-shadow: 0 0 0 2px #1a1a1a, 0 0 0 4px #ffff55;
}
.create-group-form button[type="submit"] {
padding: 0.75rem 1.5rem;
background: #4a8c2a;
border: 2px solid #1a1a1a;
color: white;
font-weight: 700;
cursor: pointer;
transition: all 0.1s;
font-family: 'Silkscreen', 'Courier New', monospace;
text-shadow: 1px 1px 0 #1a1a1a;
box-shadow: inset 0 2px 0 #6ab04c, inset 0 -2px 0 #2d6b1a;
}
.create-group-form button[type="submit"]:hover:not(:disabled) {
background: #5a9c3a;
box-shadow: inset 0 2px 0 #7bc05c, inset 0 -2px 0 #3d7b2a;
}
.create-group-form button[type="submit"]:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Groups List */
.groups-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.group-card {
background: #3b3b3b;
border: 2px solid #1a1a1a;
overflow: hidden;
transition: all 0.1s;
box-shadow: inset 0 -2px 0 #2a2a2a, inset 0 2px 0 #555;
}
.group-card:hover {
background: #4b4b4b;
}
.group-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 1.5rem;
border-left: 4px solid;
}
.group-title {
display: flex;
align-items: center;
gap: 0.75rem;
flex: 1;
}
.group-color-indicator {
width: 1rem;
height: 1rem;
border: 2px solid #1a1a1a;
}
.group-title h3 {
font-size: 1.125rem;
font-weight: 600;
color: #e0e0e0;
margin: 0;
}
.member-count {
font-size: 0.75rem;
color: #a0a0a0;
font-weight: 600;
padding: 0.25rem 0.5rem;
background: #1a1a1a;
border: 2px solid #4b4b4b;
}
.delete-group-btn {
padding: 0.5rem;
background: transparent;
border: none;
font-size: 1.25rem;
cursor: pointer;
opacity: 0.6;
transition: all 0.1s;
}
.delete-group-btn:hover {
opacity: 1;
transform: scale(1.1);
}
/* Group Members */
.group-members {
padding: 0 1.5rem 1rem 1.5rem;
}
.group-members h4 {
font-size: 0.875rem;
font-weight: 600;
color: #a0a0a0;
margin: 0 0 0.75rem 0;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.members-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.member-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem;
background: #2c2c2c;
border: 2px solid #1a1a1a;
transition: all 0.1s;
box-shadow: inset 0 -1px 0 #222, inset 0 1px 0 #444;
}
.member-item:hover {
background: #4b4b4b;
}
.member-info {
display: flex;
align-items: center;
gap: 0.5rem;
flex: 1;
}
.member-icon {
font-size: 1.25rem;
}
.member-name {
font-size: 0.875rem;
font-weight: 600;
color: #e0e0e0;
}
.member-status {
font-size: 1rem;
margin-left: 0.25rem;
}
.remove-member-btn {
padding: 0.25rem 0.5rem;
background: transparent;
border: none;
color: #ff5555;
font-size: 1rem;
cursor: pointer;
opacity: 0.6;
transition: all 0.1s;
}
.remove-member-btn:hover {
opacity: 1;
transform: scale(1.2);
}
.no-members {
text-align: center;
padding: 2rem 1rem;
color: #a0a0a0;
font-size: 0.875rem;
font-style: italic;
}
.add-member-section {
margin-top: 0.75rem;
}
.add-member-section select {
width: 100%;
padding: 0.75rem;
background: #1a1a1a;
border: 2px solid #4b4b4b;
color: #e0e0e0;
font-size: 0.875rem;
cursor: pointer;
transition: all 0.1s;
font-family: 'Silkscreen', 'Courier New', monospace;
}
.add-member-section select:hover {
border-color: #55ffff;
}
.add-member-section select:focus {
outline: none;
border-color: #55ffff;
}
/* Group Commands */
.group-commands {
padding: 1rem 1.5rem 1.5rem 1.5rem;
border-top: 2px solid #4b4b4b;
}
.group-commands h4 {
font-size: 0.875rem;
font-weight: 600;
color: #a0a0a0;
margin: 0 0 0.75rem 0;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.command-buttons {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 0.5rem;
}
.command-buttons button {
padding: 0.75rem 1rem;
background: #6b6b6b;
border: 2px solid #1a1a1a;
color: #e0e0e0;
font-size: 0.875rem;
font-weight: 700;
cursor: pointer;
transition: all 0.1s;
font-family: 'Silkscreen', 'Courier New', monospace;
text-shadow: 1px 1px 0 #1a1a1a;
box-shadow: inset 0 -2px 0 #444, inset 0 2px 0 #888;
}
.command-buttons button:hover {
background: #7b7b7b;
box-shadow: inset 0 -2px 0 #555, inset 0 2px 0 #999;
}
/* Empty State */
.empty-state {
text-align: center;
padding: 4rem 2rem;
}
.empty-icon {
font-size: 4rem;
margin-bottom: 1rem;
opacity: 0.5;
}
.empty-title {
font-size: 1.25rem;
font-weight: 600;
color: #e0e0e0;
margin-bottom: 0.5rem;
}
.empty-text {
font-size: 0.875rem;
color: #a0a0a0;
}
/* Mobile Responsive */
@media (max-width: 768px) {
.groups-panel {
padding: 1rem;
}
.group-header {
padding: 1rem;
}
.group-members,
.group-commands {
padding-left: 1rem;
padding-right: 1rem;
}
.command-buttons {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 480px) {
.groups-header h2 {
font-size: 1.25rem;
}
.color-picker {
justify-content: center;
}
.command-buttons {
grid-template-columns: 1fr;
}
}

View File

@@ -0,0 +1,327 @@
import { useState, useEffect } from 'react';
import './GroupsPanel.css';
const GroupsPanel = ({ turtles, apiUrl, wsUrl }) => {
const [groups, setGroups] = useState([]);
const [groupName, setGroupName] = useState('');
const [groupColor, setGroupColor] = useState('#3b82f6');
const [selectedGroup, setSelectedGroup] = useState(null);
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState(null);
const colorPresets = [
{ name: 'Blue', value: '#345ec3' },
{ name: 'Green', value: '#4a8c2a' },
{ name: 'Red', value: '#aa0000' },
{ name: 'Yellow', value: '#ffaa00' },
{ name: 'Purple', value: '#7b2fbe' },
{ name: 'Pink', value: '#d4658a' },
{ name: 'Cyan', value: '#55ffff' },
{ name: 'Orange', value: '#c97a2a' },
];
useEffect(() => {
loadGroups();
// Refresh groups every 10 seconds
const interval = setInterval(loadGroups, 10000);
return () => clearInterval(interval);
}, []);
const loadGroups = async () => {
try {
const response = await fetch(`${apiUrl}/api/groups`);
if (response.ok) {
const data = await response.json();
setGroups(Array.isArray(data) ? data : (data.groups || []));
}
} catch (error) {
console.error('Failed to load groups:', error);
}
};
const createGroup = async (e) => {
e.preventDefault();
if (!groupName.trim()) {
showMessage('Please enter a group name', 'error');
return;
}
setLoading(true);
try {
const response = await fetch(`${apiUrl}/api/groups`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
groupName: groupName.trim(),
color: groupColor
})
});
if (response.ok) {
showMessage('Group created successfully!', 'success');
setGroupName('');
loadGroups();
} else {
showMessage('Failed to create group', 'error');
}
} catch (error) {
showMessage('Error creating group', 'error');
} finally {
setLoading(false);
}
};
const deleteGroup = async (groupId) => {
if (!confirm('Are you sure you want to delete this group?')) return;
try {
const response = await fetch(`${apiUrl}/api/groups/${groupId}`, {
method: 'DELETE'
});
if (response.ok) {
showMessage('Group deleted', 'success');
if (selectedGroup?.groupId === groupId) {
setSelectedGroup(null);
}
loadGroups();
} else {
showMessage('Failed to delete group', 'error');
}
} catch (error) {
showMessage('Error deleting group', 'error');
}
};
const addTurtleToGroup = async (groupId, turtleId) => {
try {
const response = await fetch(`${apiUrl}/api/groups/${groupId}/members`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ turtleId })
});
if (response.ok) {
showMessage('Turtle added to group', 'success');
loadGroups();
} else {
showMessage('Failed to add turtle', 'error');
}
} catch (error) {
showMessage('Error adding turtle', 'error');
}
};
const removeTurtleFromGroup = async (groupId, turtleId) => {
try {
const response = await fetch(`${apiUrl}/api/groups/${groupId}/members/${turtleId}`, {
method: 'DELETE'
});
if (response.ok) {
showMessage('Turtle removed from group', 'success');
loadGroups();
} else {
showMessage('Failed to remove turtle', 'error');
}
} catch (error) {
showMessage('Error removing turtle', 'error');
}
};
const sendGroupCommand = async (groupId, command) => {
try {
const response = await fetch(`${apiUrl}/api/groups/${groupId}/command`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ command })
});
if (response.ok) {
const data = await response.json();
showMessage(`Command sent to ${data.sentTo} turtles`, 'success');
} else {
showMessage('Failed to send command', 'error');
}
} catch (error) {
showMessage('Error sending command', 'error');
}
};
const showMessage = (text, type) => {
setMessage({ text, type });
setTimeout(() => setMessage(null), 3000);
};
const getTurtleById = (turtleId) => {
return turtles.find(t => t.turtleID === turtleId);
};
const getAvailableTurtles = (groupMembers) => {
const memberIds = groupMembers.map(m => m.turtleId);
return turtles.filter(t => !memberIds.includes(t.turtleID));
};
return (
<div className="groups-panel">
<div className="groups-header">
<h2>👥 Turtle Groups</h2>
<div className="group-count">{groups.length} teams</div>
</div>
{message && (
<div className={`message ${message.type}`}>
{message.text}
</div>
)}
{/* Create Group Form */}
<div className="create-group-section">
<h3>Create New Group</h3>
<form onSubmit={createGroup} className="create-group-form">
<input
type="text"
placeholder="Group name..."
value={groupName}
onChange={(e) => setGroupName(e.target.value)}
maxLength={50}
disabled={loading}
/>
<div className="color-picker">
{colorPresets.map(preset => (
<button
key={preset.value}
type="button"
className={`color-option ${groupColor === preset.value ? 'active' : ''}`}
style={{ backgroundColor: preset.value }}
onClick={() => setGroupColor(preset.value)}
title={preset.name}
/>
))}
</div>
<button type="submit" disabled={loading || !groupName.trim()}>
{loading ? 'Creating...' : ' Create Group'}
</button>
</form>
</div>
{/* Groups List */}
<div className="groups-list">
{groups.length === 0 && (
<div className="empty-state">
<div className="empty-icon">👥</div>
<div className="empty-title">No Groups Yet</div>
<div className="empty-text">Create a group to organize your turtles into teams</div>
</div>
)}
{groups.map(group => (
<div key={group.groupId} className="group-card">
<div className="group-header" style={{ borderLeftColor: group.color }}>
<div className="group-title">
<div
className="group-color-indicator"
style={{ backgroundColor: group.color }}
/>
<h3>{group.groupName}</h3>
<span className="member-count">{group.memberCount} members</span>
</div>
<button
className="delete-group-btn"
onClick={() => deleteGroup(group.groupId)}
title="Delete group"
>
🗑
</button>
</div>
{/* Group Members */}
<div className="group-members">
<h4>Members:</h4>
{group.members && group.members.length > 0 ? (
<div className="members-list">
{group.members.map(member => {
const turtle = getTurtleById(member.turtleId);
return (
<div key={member.turtleId} className="member-item">
<div className="member-info">
<span className="member-icon">🐢</span>
<span className="member-name">
{turtle?.name || `Turtle ${member.turtleId}`}
</span>
{turtle && (
<span className="member-status" title={turtle.mode}>
{turtle.mode === 'exploring' && '🔍'}
{turtle.mode === 'mining' && '⛏️'}
{turtle.mode === 'returning' && '🏠'}
{turtle.mode === 'idle' && '💤'}
</span>
)}
</div>
<button
className="remove-member-btn"
onClick={() => removeTurtleFromGroup(group.groupId, member.turtleId)}
title="Remove from group"
>
</button>
</div>
);
})}
</div>
) : (
<div className="no-members">No members yet</div>
)}
{/* Add Member Dropdown */}
{getAvailableTurtles(group.members || []).length > 0 && (
<div className="add-member-section">
<select
onChange={(e) => {
if (e.target.value) {
addTurtleToGroup(group.groupId, parseInt(e.target.value));
e.target.value = '';
}
}}
defaultValue=""
>
<option value=""> Add turtle...</option>
{getAvailableTurtles(group.members || []).map(turtle => (
<option key={turtle.turtleID} value={turtle.turtleID}>
🐢 {turtle.name || `Turtle ${turtle.turtleID}`}
</option>
))}
</select>
</div>
)}
</div>
{/* Group Commands */}
{group.members && group.members.length > 0 && (
<div className="group-commands">
<h4>Group Commands:</h4>
<div className="command-buttons">
<button onClick={() => sendGroupCommand(group.groupId, 'explore')}>
🔍 Explore
</button>
<button onClick={() => sendGroupCommand(group.groupId, 'mine')}>
Mine
</button>
<button onClick={() => sendGroupCommand(group.groupId, 'returnHome')}>
🏠 Return Home
</button>
<button onClick={() => sendGroupCommand(group.groupId, 'stop')}>
Stop
</button>
</div>
</div>
)}
</div>
))}
</div>
</div>
);
};
export default GroupsPanel;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,507 @@
/* ============================================
Minecraft-Themed Mining Areas Panel
============================================ */
.mining-areas-panel {
height: 100%;
display: flex;
flex-direction: column;
background: #2c2c2c;
color: #e0e0e0;
overflow: hidden;
font-family: 'Silkscreen', 'Courier New', monospace;
}
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 1.5rem;
background: #6b4e28;
border-bottom: 3px solid #1a1a1a;
box-shadow: inset 0 2px 0 #8b6d3c, inset 0 -2px 0 #4a3520;
}
.panel-header h2 {
margin: 0;
font-size: 1.5rem;
color: #ffff55;
text-shadow: 2px 2px 0 #1a1a1a;
}
.btn-create {
padding: 0.5rem 1rem;
background: #4a8c2a;
color: white;
border: 2px solid #1a1a1a;
font-size: 0.9rem;
font-weight: 700;
cursor: pointer;
transition: all 0.1s;
font-family: 'Silkscreen', 'Courier New', monospace;
text-shadow: 1px 1px 0 #1a1a1a;
box-shadow: inset 0 2px 0 #6ab04c, inset 0 -2px 0 #2d6b1a;
}
.btn-create:hover {
background: #5a9c3a;
box-shadow: inset 0 2px 0 #7bc05c, inset 0 -2px 0 #3d7b2a;
}
/* Filter buttons */
.filter-buttons {
display: flex;
gap: 0.25rem;
padding: 1rem 1.5rem;
background: #3b3b3b;
border-bottom: 2px solid #1a1a1a;
flex-wrap: wrap;
}
.filter-btn {
padding: 0.5rem 1rem;
background: #6b6b6b;
color: #e0e0e0;
border: 2px solid #1a1a1a;
font-size: 0.875rem;
font-weight: 700;
cursor: pointer;
transition: all 0.1s;
white-space: nowrap;
font-family: 'Silkscreen', 'Courier New', monospace;
text-shadow: 1px 1px 0 #1a1a1a;
box-shadow: inset 0 -2px 0 #444, inset 0 2px 0 #888;
}
.filter-btn:hover {
background: #7b7b7b;
box-shadow: inset 0 -2px 0 #555, inset 0 2px 0 #999;
}
.filter-btn.active {
background: #4a8c2a;
color: white;
box-shadow: inset 0 2px 0 #6ab04c, inset 0 -2px 0 #2d6b1a;
}
/* Create form */
.create-form {
padding: 1.5rem;
background: #3b3b3b;
border-bottom: 2px solid #1a1a1a;
max-height: 60vh;
overflow-y: auto;
}
.create-form h3 {
margin: 0 0 1rem 0;
color: #ffaa00;
text-shadow: 1px 1px 0 #1a1a1a;
}
.form-group {
margin-bottom: 1rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
color: #e0e0e0;
font-size: 0.875rem;
font-weight: 600;
}
.form-group input,
.form-group select {
width: 100%;
padding: 0.625rem;
background: #1a1a1a;
border: 2px solid #4b4b4b;
color: #e0e0e0;
font-size: 0.875rem;
transition: all 0.1s;
font-family: 'Silkscreen', 'Courier New', monospace;
}
.form-group input:focus,
.form-group select:focus {
outline: none;
border-color: #55ffff;
}
.coordinates-section {
margin-bottom: 1rem;
}
.coordinates-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}
.coordinates-header label {
color: #e0e0e0;
font-size: 0.875rem;
font-weight: 600;
}
.btn-use-position {
padding: 0.375rem 0.75rem;
background: #4a8c2a;
color: white;
border: 2px solid #1a1a1a;
font-size: 0.75rem;
font-weight: 700;
cursor: pointer;
transition: all 0.1s;
font-family: 'Silkscreen', 'Courier New', monospace;
text-shadow: 1px 1px 0 #1a1a1a;
box-shadow: inset 0 2px 0 #6ab04c, inset 0 -2px 0 #2d6b1a;
}
.btn-use-position:hover:not(:disabled) {
background: #5a9c3a;
}
.btn-use-position:disabled {
background: #4b4b4b;
color: #7b7b7b;
cursor: not-allowed;
box-shadow: inset 0 -2px 0 #333, inset 0 2px 0 #666;
}
.coordinate-inputs {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5rem;
}
.coordinate-inputs input {
width: 100%;
}
.btn-submit {
width: 100%;
padding: 0.75rem;
background: #4a8c2a;
color: white;
border: 2px solid #1a1a1a;
font-size: 1rem;
font-weight: 700;
cursor: pointer;
transition: all 0.1s;
margin-top: 1rem;
font-family: 'Silkscreen', 'Courier New', monospace;
text-shadow: 1px 1px 0 #1a1a1a;
box-shadow: inset 0 2px 0 #6ab04c, inset 0 -2px 0 #2d6b1a;
}
.btn-submit:hover {
background: #5a9c3a;
box-shadow: inset 0 2px 0 #7bc05c, inset 0 -2px 0 #3d7b2a;
}
/* Areas list */
.areas-list {
flex: 1;
overflow-y: auto;
padding: 1rem;
}
.empty-state {
text-align: center;
padding: 3rem 1rem;
color: #7b7b7b;
}
.empty-state p {
margin: 0.5rem 0;
}
/* Area card */
.area-card {
background: #3b3b3b;
border: 2px solid #1a1a1a;
padding: 1rem;
margin-bottom: 1rem;
transition: all 0.1s;
box-shadow: inset 0 -2px 0 #2a2a2a, inset 0 2px 0 #555;
}
.area-card:hover {
background: #4b4b4b;
}
.area-card.has-conflict {
border-color: #ff5555;
background: linear-gradient(135deg, #3b3b3b 0%, #4a2020 100%);
}
.area-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 2px solid #4b4b4b;
}
.area-header h3 {
margin: 0;
color: #e0e0e0;
font-size: 1.125rem;
}
.status-badge {
padding: 0.25rem 0.75rem;
border: 2px solid #1a1a1a;
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
color: white;
text-shadow: 1px 1px 0 rgba(0,0,0,0.5);
}
/* Area info */
.area-info {
margin-bottom: 1rem;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem 0;
border-bottom: 2px solid #4b4b4b;
}
.info-row:last-child {
border-bottom: none;
}
.info-label {
color: #a0a0a0;
font-size: 0.875rem;
font-weight: 600;
}
.info-value {
color: #e0e0e0;
font-size: 0.875rem;
font-weight: 600;
text-align: right;
}
.coordinate-value {
font-family: 'Silkscreen', 'Courier New', monospace;
font-size: 0.75rem;
word-break: break-all;
color: #55ffff;
}
/* Conflict warning */
.conflict-warning {
background: #6b1a1a44;
border: 2px solid #ff5555;
padding: 0.75rem;
margin-bottom: 1rem;
color: #ff5555;
font-size: 0.875rem;
}
.conflict-warning ul {
margin: 0.5rem 0 0 0;
padding-left: 1.5rem;
}
.conflict-warning li {
margin: 0.25rem 0;
}
/* Area actions */
.area-actions {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.btn-action {
flex: 1;
min-width: 120px;
padding: 0.5rem 1rem;
border: 2px solid #1a1a1a;
font-size: 0.875rem;
font-weight: 700;
cursor: pointer;
transition: all 0.1s;
white-space: nowrap;
font-family: 'Silkscreen', 'Courier New', monospace;
text-shadow: 1px 1px 0 #1a1a1a;
}
.btn-start {
background: #4a8c2a;
color: white;
box-shadow: inset 0 2px 0 #6ab04c, inset 0 -2px 0 #2d6b1a;
}
.btn-start:hover {
background: #5a9c3a;
box-shadow: inset 0 2px 0 #7bc05c, inset 0 -2px 0 #3d7b2a;
}
.btn-complete {
background: #345ec3;
color: white;
box-shadow: inset 0 2px 0 #5577dd, inset 0 -2px 0 #223399;
}
.btn-complete:hover {
background: #4a6ed3;
}
.btn-delete {
background: #aa0000;
color: white;
box-shadow: inset 0 2px 0 #dd3333, inset 0 -2px 0 #770000;
}
.btn-delete:hover {
background: #cc0000;
}
/* Scrollbar styling */
.areas-list::-webkit-scrollbar,
.create-form::-webkit-scrollbar {
width: 8px;
}
.areas-list::-webkit-scrollbar-track,
.create-form::-webkit-scrollbar-track {
background: #2c2c2c;
}
.areas-list::-webkit-scrollbar-thumb,
.create-form::-webkit-scrollbar-thumb {
background: #5a5a5a;
border: 1px solid #1a1a1a;
}
.areas-list::-webkit-scrollbar-thumb:hover,
.create-form::-webkit-scrollbar-thumb:hover {
background: #6b6b6b;
}
/* Responsive design */
@media (max-width: 768px) {
.panel-header {
padding: 0.75rem 1rem;
}
.panel-header h2 {
font-size: 1.25rem;
}
.filter-buttons {
padding: 0.75rem 1rem;
}
.create-form {
padding: 1rem;
}
.areas-list {
padding: 0.75rem;
}
.area-actions {
flex-direction: column;
}
.btn-action {
width: 100%;
}
}
@media (max-width: 480px) {
.coordinate-inputs {
grid-template-columns: 1fr;
}
.area-header {
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
}
.info-row {
flex-direction: column;
align-items: flex-start;
gap: 0.25rem;
}
.info-value {
text-align: left;
}
}
/* Color picker row */
.color-picker-row {
display: flex;
align-items: center;
gap: 0.4rem;
flex-wrap: wrap;
}
.color-input {
width: 32px;
height: 32px;
padding: 0;
border: 2px solid #1a1a1a;
background: none;
cursor: pointer;
border-radius: 0;
}
.color-input::-webkit-color-swatch-wrapper {
padding: 0;
}
.color-input::-webkit-color-swatch {
border: none;
}
.color-swatch {
width: 24px;
height: 24px;
border: 2px solid #1a1a1a;
cursor: pointer;
padding: 0;
transition: transform 0.1s;
}
.color-swatch:hover {
transform: scale(1.2);
}
.color-swatch.active {
border-color: #ffff55;
box-shadow: 0 0 4px #ffff55;
transform: scale(1.15);
}
/* Color dot in area card */
.area-title-row {
display: flex;
align-items: center;
gap: 0.5rem;
}
.area-color-dot {
width: 14px;
height: 14px;
border: 2px solid #1a1a1a;
flex-shrink: 0;
}

View File

@@ -0,0 +1,471 @@
import React, { useState, useEffect } from 'react';
import './MiningAreasPanel.css';
export default function MiningAreasPanel({ turtles, selectedTurtle, apiUrl }) {
const [areas, setAreas] = useState([]);
const [showCreateForm, setShowCreateForm] = useState(false);
const [newArea, setNewArea] = useState({
areaName: '',
color: '#4a8c2a',
startX: '',
startY: '',
startZ: '',
endX: '',
endY: '',
endZ: '',
turtleID: ''
});
const [filterStatus, setFilterStatus] = useState('all'); // all, planned, mining, completed
// Load mining areas
useEffect(() => {
loadAreas();
const interval = setInterval(loadAreas, 5000);
return () => clearInterval(interval);
}, [apiUrl]);
const loadAreas = async () => {
try {
const response = await fetch(`${apiUrl}/api/mining-areas`);
if (response.ok) {
const data = await response.json();
// Server returns a flat array of formatted areas
setAreas(Array.isArray(data) ? data : (data.areas || []));
}
} catch (error) {
console.error('Failed to load mining areas:', error);
}
};
// Create new mining area
const handleCreateArea = async (e) => {
e.preventDefault();
if (!newArea.areaName || !newArea.turtleID) {
alert('Please provide area name and assign a turtle');
return;
}
// Validate coordinates
const coords = ['startX', 'startY', 'startZ', 'endX', 'endY', 'endZ'];
for (const coord of coords) {
if (newArea[coord] === '' || isNaN(Number(newArea[coord]))) {
alert(`Please provide valid ${coord} coordinate`);
return;
}
}
try {
const response = await fetch(`${apiUrl}/api/mining-areas`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...newArea,
startX: Number(newArea.startX),
startY: Number(newArea.startY),
startZ: Number(newArea.startZ),
endX: Number(newArea.endX),
endY: Number(newArea.endY),
endZ: Number(newArea.endZ),
color: newArea.color,
status: 'planned'
})
});
if (response.ok) {
setShowCreateForm(false);
setNewArea({
areaName: '',
color: '#4a8c2a',
startX: '',
startY: '',
startZ: '',
endX: '',
endY: '',
endZ: '',
turtleID: ''
});
loadAreas();
} else {
const error = await response.json();
alert(`Failed to create area: ${error.error || 'Unknown error'}`);
}
} catch (error) {
console.error('Failed to create mining area:', error);
alert('Failed to create mining area');
}
};
// Delete mining area
const handleDeleteArea = async (areaID) => {
if (!confirm('Are you sure you want to delete this mining area?')) return;
try {
const response = await fetch(`${apiUrl}/api/mining-areas/${areaID}`, {
method: 'DELETE'
});
if (response.ok) {
loadAreas();
} else {
alert('Failed to delete area');
}
} catch (error) {
console.error('Failed to delete area:', error);
alert('Failed to delete area');
}
};
// Update area status
const handleUpdateStatus = async (areaID, newStatus) => {
try {
const response = await fetch(`${apiUrl}/api/mining-areas/${areaID}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: newStatus })
});
if (response.ok) {
loadAreas();
} else {
alert('Failed to update status');
}
} catch (error) {
console.error('Failed to update status:', error);
alert('Failed to update status');
}
};
// Auto-fill from selected turtle position
const handleUseCurrentPosition = () => {
if (!selectedTurtle || !selectedTurtle.position) {
alert('No turtle selected or turtle has no position');
return;
}
const pos = selectedTurtle.position;
setNewArea(prev => ({
...prev,
startX: pos.x.toString(),
startY: pos.y.toString(),
startZ: pos.z.toString(),
turtleID: selectedTurtle.turtleID.toString()
}));
};
// Calculate volume
const calculateVolume = (area) => {
const width = Math.abs(area.endX - area.startX) + 1;
const height = Math.abs(area.endY - area.startY) + 1;
const depth = Math.abs(area.endZ - area.startZ) + 1;
return width * height * depth;
};
// Check for overlapping areas
const checkOverlap = (area1, area2) => {
const overlapX = Math.max(
Math.min(area1.endX, area2.endX) - Math.max(area1.startX, area2.startX) + 1,
0
);
const overlapY = Math.max(
Math.min(area1.endY, area2.endY) - Math.max(area1.startY, area2.startY) + 1,
0
);
const overlapZ = Math.max(
Math.min(area1.endZ, area2.endZ) - Math.max(area1.startZ, area2.startZ) + 1,
0
);
return overlapX > 0 && overlapY > 0 && overlapZ > 0;
};
// Find conflicts
const findConflicts = (area) => {
return areas.filter(other =>
other.areaID !== area.areaID &&
checkOverlap(area, other)
);
};
// Filter areas
const filteredAreas = areas.filter(area => {
if (filterStatus === 'all') return true;
return area.status === filterStatus;
});
// Status badge component
const StatusBadge = ({ status }) => {
const colors = {
planned: '#345ec3',
mining: '#ffaa00',
completed: '#4a8c2a'
};
return (
<span className="status-badge" style={{ backgroundColor: colors[status] || '#6b6b6b' }}>
{status}
</span>
);
};
return (
<div className="mining-areas-panel">
<div className="panel-header">
<h2> Mining Areas</h2>
<button
className="btn-create"
onClick={() => setShowCreateForm(!showCreateForm)}
>
{showCreateForm ? '✕ Cancel' : '+ New Area'}
</button>
</div>
{/* Filter buttons */}
<div className="filter-buttons">
<button
className={`filter-btn ${filterStatus === 'all' ? 'active' : ''}`}
onClick={() => setFilterStatus('all')}
>
All ({areas.length})
</button>
<button
className={`filter-btn ${filterStatus === 'planned' ? 'active' : ''}`}
onClick={() => setFilterStatus('planned')}
>
📋 Planned ({areas.filter(a => a.status === 'planned').length})
</button>
<button
className={`filter-btn ${filterStatus === 'mining' ? 'active' : ''}`}
onClick={() => setFilterStatus('mining')}
>
Mining ({areas.filter(a => a.status === 'mining').length})
</button>
<button
className={`filter-btn ${filterStatus === 'completed' ? 'active' : ''}`}
onClick={() => setFilterStatus('completed')}
>
Done ({areas.filter(a => a.status === 'completed').length})
</button>
</div>
{/* Create form */}
{showCreateForm && (
<div className="create-form">
<h3>Create Mining Area</h3>
<form onSubmit={handleCreateArea}>
<div className="form-group">
<label>Area Name:</label>
<input
type="text"
value={newArea.areaName}
onChange={(e) => setNewArea({ ...newArea, areaName: e.target.value })}
placeholder="e.g., Diamond Mine #1"
required
/>
</div>
<div className="form-group">
<label>Area Color:</label>
<div className="color-picker-row">
<input
type="color"
value={newArea.color}
onChange={(e) => setNewArea({ ...newArea, color: e.target.value })}
className="color-input"
/>
{['#4a8c2a', '#345ec3', '#c9a000', '#cc3333', '#8833cc', '#33aacc', '#cc6633'].map(c => (
<button
key={c}
type="button"
className={`color-swatch ${newArea.color === c ? 'active' : ''}`}
style={{ backgroundColor: c }}
onClick={() => setNewArea({ ...newArea, color: c })}
/>
))}
</div>
</div>
<div className="form-group">
<label>Assign Turtle:</label>
<select
value={newArea.turtleID}
onChange={(e) => setNewArea({ ...newArea, turtleID: e.target.value })}
required
>
<option value="">Select turtle...</option>
{turtles.map(turtle => (
<option key={turtle.turtleID} value={turtle.turtleID}>
🐢 {turtle.turtleID} {turtle.label ? `- ${turtle.label}` : ''}
</option>
))}
</select>
</div>
<div className="coordinates-section">
<div className="coordinates-header">
<label>Start Position:</label>
<button
type="button"
className="btn-use-position"
onClick={handleUseCurrentPosition}
disabled={!selectedTurtle}
>
Use Current Position
</button>
</div>
<div className="coordinate-inputs">
<input
type="number"
placeholder="X"
value={newArea.startX}
onChange={(e) => setNewArea({ ...newArea, startX: e.target.value })}
required
/>
<input
type="number"
placeholder="Y"
value={newArea.startY}
onChange={(e) => setNewArea({ ...newArea, startY: e.target.value })}
required
/>
<input
type="number"
placeholder="Z"
value={newArea.startZ}
onChange={(e) => setNewArea({ ...newArea, startZ: e.target.value })}
required
/>
</div>
</div>
<div className="coordinates-section">
<label>End Position:</label>
<div className="coordinate-inputs">
<input
type="number"
placeholder="X"
value={newArea.endX}
onChange={(e) => setNewArea({ ...newArea, endX: e.target.value })}
required
/>
<input
type="number"
placeholder="Y"
value={newArea.endY}
onChange={(e) => setNewArea({ ...newArea, endY: e.target.value })}
required
/>
<input
type="number"
placeholder="Z"
value={newArea.endZ}
onChange={(e) => setNewArea({ ...newArea, endZ: e.target.value })}
required
/>
</div>
</div>
<button type="submit" className="btn-submit">
Create Area
</button>
</form>
</div>
)}
{/* Areas list */}
<div className="areas-list">
{filteredAreas.length === 0 ? (
<div className="empty-state">
<p>No mining areas {filterStatus !== 'all' ? `with status "${filterStatus}"` : ''}</p>
{filterStatus === 'all' && (
<p>Create a new area to get started</p>
)}
</div>
) : (
filteredAreas.map(area => {
const turtle = turtles.find(t => t.turtleID === area.turtleID);
const conflicts = findConflicts(area);
const volume = calculateVolume(area);
return (
<div key={area.areaID} className={`area-card ${conflicts.length > 0 ? 'has-conflict' : ''}`}>
<div className="area-header">
<div className="area-title-row">
<span className="area-color-dot" style={{ backgroundColor: area.color || '#4a8c2a' }} />
<h3>{area.areaName}</h3>
</div>
<StatusBadge status={area.status} />
</div>
<div className="area-info">
<div className="info-row">
<span className="info-label">Turtle:</span>
<span className="info-value">
🐢 {turtle ? `${turtle.turtleID} ${turtle.label || ''}` : area.turtleID}
</span>
</div>
<div className="info-row">
<span className="info-label">Coordinates:</span>
<span className="info-value coordinate-value">
({area.startX}, {area.startY}, {area.startZ})
({area.endX}, {area.endY}, {area.endZ})
</span>
</div>
<div className="info-row">
<span className="info-label">Volume:</span>
<span className="info-value">{volume.toLocaleString()} blocks</span>
</div>
{area.createdAt && (
<div className="info-row">
<span className="info-label">Created:</span>
<span className="info-value">
{new Date(area.createdAt).toLocaleDateString()}
</span>
</div>
)}
</div>
{conflicts.length > 0 && (
<div className="conflict-warning">
Overlaps with {conflicts.length} other area(s):
<ul>
{conflicts.map(c => (
<li key={c.areaID}>{c.areaName}</li>
))}
</ul>
</div>
)}
<div className="area-actions">
{area.status === 'planned' && (
<button
className="btn-action btn-start"
onClick={() => handleUpdateStatus(area.areaID, 'mining')}
>
Start Mining
</button>
)}
{area.status === 'mining' && (
<button
className="btn-action btn-complete"
onClick={() => handleUpdateStatus(area.areaID, 'completed')}
>
Mark Complete
</button>
)}
<button
className="btn-action btn-delete"
onClick={() => handleDeleteArea(area.areaID)}
>
🗑 Delete
</button>
</div>
</div>
);
})
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,549 @@
/* ============================================
Minecraft-Themed Path Recorder
============================================ */
.path-recorder {
padding: 1.5rem;
background: #2c2c2c;
height: 100%;
overflow-y: auto;
font-family: 'Silkscreen', 'Courier New', monospace;
}
.recorder-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
flex-wrap: wrap;
gap: 1rem;
}
.recorder-header h2 {
font-size: 1.5rem;
font-weight: 700;
color: #ffff55;
margin: 0;
text-shadow: 2px 2px 0 #1a1a1a;
}
.selected-turtle-badge {
padding: 0.5rem 1rem;
background: #2d6b1a33;
border: 2px solid #55ff55;
color: #55ff55;
font-weight: 600;
font-size: 0.875rem;
}
.message {
padding: 0.75rem 1rem;
border: 2px solid;
margin-bottom: 1rem;
font-size: 0.875rem;
font-weight: 600;
animation: slideIn 0.3s;
}
.message.success {
background: #2d6b1a33;
color: #55ff55;
border-color: #55ff55;
}
.message.error {
background: #6b1a1a33;
color: #ff5555;
border-color: #ff5555;
}
.message.info {
background: #1a4a6b33;
color: #55ffff;
border-color: #55ffff;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Recording Section */
.recording-section {
background: #3b3b3b;
border: 2px solid #1a1a1a;
padding: 1.5rem;
margin-bottom: 1.5rem;
box-shadow: inset 0 -2px 0 #2a2a2a, inset 0 2px 0 #555;
}
.recording-section h3 {
font-size: 1.125rem;
font-weight: 600;
color: #e0e0e0;
margin: 0 0 1rem 0;
text-shadow: 1px 1px 0 #1a1a1a;
}
.record-form {
display: flex;
flex-direction: column;
gap: 1rem;
}
.record-form input,
.record-form textarea {
padding: 0.75rem;
background: #1a1a1a;
border: 2px solid #4b4b4b;
color: #e0e0e0;
font-size: 0.875rem;
font-family: 'Silkscreen', 'Courier New', monospace;
}
.record-form input:focus,
.record-form textarea:focus {
outline: none;
border-color: #55ffff;
}
.record-btn {
padding: 0.875rem 1.5rem;
background: #aa0000;
border: 2px solid #1a1a1a;
color: white;
font-weight: 700;
font-size: 1rem;
cursor: pointer;
transition: all 0.1s;
font-family: 'Silkscreen', 'Courier New', monospace;
text-shadow: 1px 1px 0 #1a1a1a;
box-shadow: inset 0 2px 0 #dd3333, inset 0 -2px 0 #770000;
}
.record-btn:hover:not(:disabled) {
background: #cc0000;
box-shadow: inset 0 2px 0 #ff4444, inset 0 -2px 0 #880000;
}
.record-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.recording-controls {
display: flex;
flex-direction: column;
gap: 1rem;
align-items: center;
}
.waypoint-counter {
font-size: 1.5rem;
font-weight: 700;
color: #ff5555;
text-shadow: 1px 1px 0 #1a1a1a;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.6;
}
}
.recording-info {
text-align: center;
padding: 1rem;
background: #2c2c2c;
border: 2px solid #1a1a1a;
width: 100%;
}
.recording-info p {
margin: 0.25rem 0;
color: #a0a0a0;
font-size: 0.875rem;
}
.recording-info strong {
color: #e0e0e0;
}
.stop-btn {
padding: 0.875rem 2rem;
background: #4a8c2a;
border: 2px solid #1a1a1a;
color: white;
font-weight: 700;
font-size: 1rem;
cursor: pointer;
transition: all 0.1s;
font-family: 'Silkscreen', 'Courier New', monospace;
text-shadow: 1px 1px 0 #1a1a1a;
box-shadow: inset 0 2px 0 #6ab04c, inset 0 -2px 0 #2d6b1a;
}
.stop-btn:hover {
background: #5a9c3a;
box-shadow: inset 0 2px 0 #7bc05c, inset 0 -2px 0 #3d7b2a;
}
/* Paths Section */
.paths-section h3 {
font-size: 1.125rem;
font-weight: 600;
color: #55ffff;
margin-bottom: 1rem;
text-shadow: 1px 1px 0 #1a1a1a;
}
.loading {
text-align: center;
padding: 2rem;
color: #a0a0a0;
font-size: 0.875rem;
}
.paths-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.path-card {
background: #3b3b3b;
border: 2px solid #1a1a1a;
padding: 1.5rem;
transition: all 0.1s;
box-shadow: inset 0 -2px 0 #2a2a2a, inset 0 2px 0 #555;
}
.path-card:hover {
background: #4b4b4b;
}
.path-card-header {
margin-bottom: 1rem;
}
.path-info h4 {
font-size: 1rem;
font-weight: 600;
color: #e0e0e0;
margin: 0 0 0.5rem 0;
}
.path-description {
font-size: 0.875rem;
color: #a0a0a0;
margin: 0.5rem 0;
font-style: italic;
}
.path-meta {
display: flex;
gap: 1rem;
flex-wrap: wrap;
font-size: 0.75rem;
color: #7b7b7b;
margin-top: 0.75rem;
}
.path-meta span {
display: flex;
align-items: center;
gap: 0.25rem;
}
.path-actions {
display: flex;
gap: 0.5rem;
}
.path-actions button {
padding: 0.5rem 1rem;
border: 2px solid #1a1a1a;
font-size: 0.875rem;
font-weight: 700;
cursor: pointer;
transition: all 0.1s;
font-family: 'Silkscreen', 'Courier New', monospace;
text-shadow: 1px 1px 0 #1a1a1a;
}
.view-btn {
background: #345ec3;
color: white;
box-shadow: inset 0 2px 0 #5577dd, inset 0 -2px 0 #223399;
}
.view-btn:hover {
background: #4a6ed3;
}
.play-btn {
background: #4a8c2a;
color: white;
box-shadow: inset 0 2px 0 #6ab04c, inset 0 -2px 0 #2d6b1a;
}
.play-btn:hover {
background: #5a9c3a;
}
.delete-btn {
background: #aa0000;
color: white;
padding: 0.5rem;
margin-left: auto;
box-shadow: inset 0 2px 0 #dd3333, inset 0 -2px 0 #770000;
}
.delete-btn:hover {
background: #cc0000;
}
/* Empty State */
.empty-state {
text-align: center;
padding: 4rem 2rem;
}
.empty-icon {
font-size: 4rem;
margin-bottom: 1rem;
opacity: 0.5;
}
.empty-title {
font-size: 1.25rem;
font-weight: 600;
color: #e0e0e0;
margin-bottom: 0.5rem;
}
.empty-text {
font-size: 0.875rem;
color: #a0a0a0;
}
/* Path Details Modal */
.path-details-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.85);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 1rem;
animation: fadeIn 0.3s;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.modal-content {
background: #3b3b3b;
border: 3px solid #1a1a1a;
max-width: 800px;
width: 100%;
max-height: 90vh;
overflow-y: auto;
animation: slideUp 0.3s;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6), inset 0 -2px 0 #2a2a2a, inset 0 2px 0 #555;
}
@keyframes slideUp {
from {
transform: translateY(20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem;
border-bottom: 2px solid #4b4b4b;
background: #6b4e28;
box-shadow: inset 0 2px 0 #8b6d3c, inset 0 -2px 0 #4a3520;
}
.modal-header h3 {
font-size: 1.25rem;
font-weight: 700;
color: #ffff55;
margin: 0;
text-shadow: 2px 2px 0 #1a1a1a;
}
.close-btn {
padding: 0.5rem 0.75rem;
background: #aa0000;
border: 2px solid #1a1a1a;
color: white;
font-size: 1.25rem;
cursor: pointer;
transition: all 0.1s;
line-height: 1;
font-weight: 700;
box-shadow: inset 0 2px 0 #dd3333, inset 0 -2px 0 #770000;
}
.close-btn:hover {
background: #cc0000;
}
.modal-body {
padding: 1.5rem;
}
.modal-description {
color: #a0a0a0;
font-style: italic;
margin-bottom: 1.5rem;
}
.waypoints-list h4,
.path-visualization h4 {
font-size: 1rem;
font-weight: 600;
color: #ffaa00;
margin: 0 0 1rem 0;
text-shadow: 1px 1px 0 #1a1a1a;
}
.waypoints-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 0.5rem;
margin-bottom: 1.5rem;
}
.waypoint-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem;
background: #2c2c2c;
border: 2px solid #1a1a1a;
font-size: 0.75rem;
}
.waypoint-number {
display: flex;
align-items: center;
justify-content: center;
width: 1.5rem;
height: 1.5rem;
background: #4a8c2a;
color: white;
font-weight: 700;
font-size: 0.7rem;
border: 2px solid #1a1a1a;
}
.waypoint-coords {
color: #e0e0e0;
font-family: 'Silkscreen', 'Courier New', monospace;
}
.waypoint-action {
margin-left: auto;
color: #55ff55;
font-size: 0.7rem;
}
.path-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
}
.stat {
padding: 1rem;
background: #2c2c2c;
border: 2px solid #1a1a1a;
}
.stat .stat-label {
display: block;
font-size: 0.75rem;
color: #a0a0a0;
margin-bottom: 0.5rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.stat .stat-value {
display: block;
font-size: 1rem;
font-weight: 600;
color: #55ffff;
font-family: 'Silkscreen', 'Courier New', monospace;
text-shadow: 1px 1px 0 #1a1a1a;
}
/* Mobile Responsive */
@media (max-width: 768px) {
.path-recorder {
padding: 1rem;
}
.recorder-header {
flex-direction: column;
align-items: flex-start;
}
.path-actions {
flex-wrap: wrap;
}
.waypoints-grid {
grid-template-columns: 1fr;
}
.path-stats {
grid-template-columns: 1fr;
}
.modal-content {
max-height: 95vh;
}
}
@media (max-width: 480px) {
.recorder-header h2 {
font-size: 1.25rem;
}
.path-actions button {
flex: 1;
min-width: 80px;
}
}

View File

@@ -0,0 +1,409 @@
import { useState, useEffect } from 'react';
import { useTurtleStore } from '../store/turtleStore';
import './PathRecorder.css';
const PathRecorder = ({ turtles, selectedTurtle, apiUrl }) => {
const [paths, setPaths] = useState([]);
const [recording, setRecording] = useState(false);
const [currentPath, setCurrentPath] = useState(null);
const [pathName, setPathName] = useState('');
const [pathDescription, setPathDescription] = useState('');
const [selectedPath, setSelectedPath] = useState(null);
const [message, setMessage] = useState(null);
const [loading, setLoading] = useState(false);
const [playingBack, setPlayingBack] = useState(false);
const setTurtleState = useTurtleStore((state) => state.setTurtleState);
useEffect(() => {
loadPaths();
}, []);
const loadPaths = async () => {
setLoading(true);
try {
const response = await fetch(`${apiUrl}/api/paths`);
if (response.ok) {
const data = await response.json();
setPaths(Array.isArray(data) ? data : []);
}
} catch (error) {
console.error('Failed to load paths:', error);
} finally {
setLoading(false);
}
};
const startRecording = () => {
if (!selectedTurtle) {
showMessage('Please select a turtle first', 'error');
return;
}
if (!pathName.trim()) {
showMessage('Please enter a path name', 'error');
return;
}
setRecording(true);
setCurrentPath({
name: pathName.trim(),
description: pathDescription.trim(),
turtleId: selectedTurtle.turtleID,
waypoints: []
});
showMessage('Recording started! Move the turtle to record path.', 'success');
};
const stopRecording = async () => {
if (!currentPath || currentPath.waypoints.length === 0) {
showMessage('No waypoints recorded', 'error');
setRecording(false);
setCurrentPath(null);
return;
}
try {
const response = await fetch(`${apiUrl}/api/paths`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
turtleId: currentPath.turtleId,
pathName: currentPath.name,
pathData: currentPath.waypoints
})
});
if (!response.ok) {
throw new Error('Failed to save path');
}
showMessage(`Path saved with ${currentPath.waypoints.length} waypoints!`, 'success');
setRecording(false);
setCurrentPath(null);
setPathName('');
setPathDescription('');
loadPaths();
} catch (error) {
showMessage('Failed to save path', 'error');
console.error(error);
}
};
const deletePath = async (pathId) => {
if (!confirm('Are you sure you want to delete this path?')) return;
try {
const response = await fetch(`${apiUrl}/api/paths/${pathId}`, {
method: 'DELETE'
});
if (response.ok) {
showMessage('Path deleted', 'success');
loadPaths();
if (selectedPath?.pathId === pathId) {
setSelectedPath(null);
}
} else {
showMessage('Failed to delete path', 'error');
}
} catch (error) {
showMessage('Error deleting path', 'error');
}
};
const viewPathDetails = async (path) => {
try {
const response = await fetch(`${apiUrl}/api/paths/${path.pathId}`);
if (response.ok) {
const data = await response.json();
setSelectedPath(data);
}
} catch (error) {
console.error('Failed to load path details:', error);
}
};
const playbackPath = async (path) => {
if (!selectedTurtle) {
showMessage('Please select a turtle to play back this path', 'error');
return;
}
// Load full path data if we don't have waypoints
let waypoints = path.pathData || path.waypoints;
if (!waypoints || waypoints.length === 0) {
try {
const response = await fetch(`${apiUrl}/api/paths/${path.pathId}`);
if (response.ok) {
const data = await response.json();
waypoints = data.waypoints;
}
} catch (error) {
showMessage('Failed to load path data', 'error');
return;
}
}
if (!waypoints || waypoints.length < 2) {
showMessage('Path has insufficient waypoints for playback', 'error');
return;
}
setPlayingBack(true);
showMessage(`Playing back ${waypoints.length} waypoints via server pathfinding...`, 'info');
const turtleId = selectedTurtle.turtleID;
// Use server-side pathfinding to navigate to each waypoint sequentially
for (let i = 0; i < waypoints.length; i++) {
const wp = waypoints[i];
// Navigate to each waypoint using the server's moving state
await setTurtleState(turtleId, 'moving', { target: { x: wp.x, y: wp.y, z: wp.z } });
// Wait for turtle to arrive (poll position or just add delay)
await new Promise(resolve => setTimeout(resolve, 500));
}
setPlayingBack(false);
showMessage('Playback complete! Turtle navigating via server.', 'success');
};
const showMessage = (text, type) => {
setMessage({ text, type });
setTimeout(() => setMessage(null), 3000);
};
// Simulate recording waypoints when turtle moves
useEffect(() => {
if (recording && selectedTurtle && currentPath) {
const newWaypoint = {
x: selectedTurtle.position?.x || 0,
y: selectedTurtle.position?.y || 0,
z: selectedTurtle.position?.z || 0,
action: selectedTurtle.lastAction || 'move'
};
// Only add if position changed
const lastWaypoint = currentPath.waypoints[currentPath.waypoints.length - 1];
if (!lastWaypoint ||
lastWaypoint.x !== newWaypoint.x ||
lastWaypoint.y !== newWaypoint.y ||
lastWaypoint.z !== newWaypoint.z) {
setCurrentPath(prev => ({
...prev,
waypoints: [...prev.waypoints, newWaypoint]
}));
}
}
}, [selectedTurtle?.position, recording]);
const getTurtleById = (turtleId) => {
return turtles.find(t => t.turtleID === turtleId);
};
return (
<div className="path-recorder">
<div className="recorder-header">
<h2>🛤 Path Recording</h2>
{selectedTurtle && (
<div className="selected-turtle-badge">
🐢 {selectedTurtle.name || `Turtle ${selectedTurtle.turtleID}`}
</div>
)}
</div>
{message && (
<div className={`message ${message.type}`}>
{message.text}
</div>
)}
{/* Recording Controls */}
<div className="recording-section">
<h3>{recording ? '🔴 Recording...' : '⚪ Ready to Record'}</h3>
{!recording ? (
<div className="record-form">
<input
type="text"
placeholder="Path name..."
value={pathName}
onChange={(e) => setPathName(e.target.value)}
maxLength={100}
/>
<textarea
placeholder="Description (optional)..."
value={pathDescription}
onChange={(e) => setPathDescription(e.target.value)}
maxLength={500}
rows={3}
/>
<button
onClick={startRecording}
disabled={!selectedTurtle || !pathName.trim()}
className="record-btn"
>
🔴 Start Recording
</button>
</div>
) : (
<div className="recording-controls">
<div className="waypoint-counter">
📍 {currentPath?.waypoints.length || 0} waypoints recorded
</div>
<div className="recording-info">
<p>Path: <strong>{currentPath?.name}</strong></p>
<p>Turtle: <strong>🐢 {selectedTurtle?.name || `Turtle ${selectedTurtle?.turtleID}`}</strong></p>
</div>
<button onClick={stopRecording} className="stop-btn">
Stop & Save
</button>
</div>
)}
</div>
{/* Paths List */}
<div className="paths-section">
<h3>📋 Saved Paths ({paths.length})</h3>
{loading && <div className="loading">Loading paths...</div>}
{!loading && paths.length === 0 && (
<div className="empty-state">
<div className="empty-icon">🛤</div>
<div className="empty-title">No Paths Recorded</div>
<div className="empty-text">Record a path by selecting a turtle and clicking "Start Recording"</div>
</div>
)}
<div className="paths-list">
{paths.map(path => {
const turtle = getTurtleById(path.turtleId);
return (
<div key={path.pathId} className="path-card">
<div className="path-card-header">
<div className="path-info">
<h4>{path.name}</h4>
{path.description && (
<p className="path-description">{path.description}</p>
)}
<div className="path-meta">
<span>🐢 {turtle?.name || `Turtle ${path.turtleId}`}</span>
<span>📍 {path.waypointCount || 0} waypoints</span>
<span>📅 {new Date(path.createdAt).toLocaleDateString()}</span>
</div>
</div>
</div>
<div className="path-actions">
<button
onClick={() => viewPathDetails(path)}
className="view-btn"
title="View details"
>
👁 View
</button>
<button
onClick={() => playbackPath(path)}
className="play-btn"
title="Playback path"
disabled={playingBack}
>
{playingBack ? '⏳ Playing...' : '▶️ Play'}
</button>
<button
onClick={() => deletePath(path.pathId)}
className="delete-btn"
title="Delete path"
>
🗑
</button>
</div>
</div>
);
})}
</div>
</div>
{/* Path Details Modal */}
{selectedPath && (
<div className="path-details-modal" onClick={() => setSelectedPath(null)}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<div className="modal-header">
<h3>📍 {selectedPath.name}</h3>
<button onClick={() => setSelectedPath(null)} className="close-btn"></button>
</div>
<div className="modal-body">
{selectedPath.description && (
<p className="modal-description">{selectedPath.description}</p>
)}
<div className="waypoints-list">
<h4>Waypoints ({selectedPath.waypoints?.length || 0})</h4>
<div className="waypoints-grid">
{selectedPath.waypoints?.map((waypoint, idx) => (
<div key={idx} className="waypoint-item">
<span className="waypoint-number">{idx + 1}</span>
<span className="waypoint-coords">
({waypoint.x}, {waypoint.y}, {waypoint.z})
</span>
{waypoint.action && (
<span className="waypoint-action">{waypoint.action}</span>
)}
</div>
))}
</div>
</div>
<div className="path-visualization">
<h4>Path Preview</h4>
<div className="path-stats">
<div className="stat">
<span className="stat-label">Total Distance:</span>
<span className="stat-value">
{selectedPath.waypoints?.length > 1
? Math.floor(
selectedPath.waypoints.reduce((total, waypoint, i) => {
if (i === 0) return 0;
const prev = selectedPath.waypoints[i - 1];
return total + Math.sqrt(
Math.pow(waypoint.x - prev.x, 2) +
Math.pow(waypoint.y - prev.y, 2) +
Math.pow(waypoint.z - prev.z, 2)
);
}, 0)
)
: 0} blocks
</span>
</div>
<div className="stat">
<span className="stat-label">Start:</span>
<span className="stat-value">
({selectedPath.waypoints?.[0]?.x}, {selectedPath.waypoints?.[0]?.y}, {selectedPath.waypoints?.[0]?.z})
</span>
</div>
<div className="stat">
<span className="stat-label">End:</span>
<span className="stat-value">
{selectedPath.waypoints?.length > 0 && (
<>
({selectedPath.waypoints[selectedPath.waypoints.length - 1].x}, {' '}
{selectedPath.waypoints[selectedPath.waypoints.length - 1].y}, {' '}
{selectedPath.waypoints[selectedPath.waypoints.length - 1].z})
</>
)}
</span>
</div>
</div>
</div>
</div>
</div>
</div>
)}
</div>
);
};
export default PathRecorder;

View File

@@ -0,0 +1,393 @@
/* ============================================
Minecraft-Themed Stats Panel
============================================ */
.stats-panel {
padding: 1.5rem;
background: #2c2c2c;
height: 100%;
overflow-y: auto;
font-family: 'Silkscreen', 'Courier New', monospace;
}
.stats-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
flex-wrap: wrap;
gap: 1rem;
}
.stats-header h2 {
font-size: 1.5rem;
font-weight: 700;
color: #ffff55;
margin: 0;
text-shadow: 2px 2px 0 #1a1a1a;
}
.time-filter {
display: flex;
gap: 0.25rem;
background: #3b3b3b;
padding: 0.25rem;
border: 2px solid #1a1a1a;
}
.time-filter button {
padding: 0.5rem 1rem;
background: #6b6b6b;
border: 2px solid #1a1a1a;
color: #e0e0e0;
font-weight: 700;
cursor: pointer;
transition: all 0.1s;
font-family: 'Silkscreen', 'Courier New', monospace;
text-shadow: 1px 1px 0 #1a1a1a;
box-shadow: inset 0 -2px 0 #444, inset 0 2px 0 #888;
}
.time-filter button:hover {
background: #7b7b7b;
box-shadow: inset 0 -2px 0 #555, inset 0 2px 0 #999;
}
.time-filter button.active {
background: #4a8c2a;
color: white;
box-shadow: inset 0 2px 0 #6ab04c, inset 0 -2px 0 #2d6b1a;
}
.loading {
text-align: center;
padding: 2rem;
color: #a0a0a0;
font-size: 0.875rem;
}
/* Turtle Stats */
.turtle-stats {
margin-bottom: 2rem;
}
.turtle-stats h3 {
font-size: 1.125rem;
font-weight: 600;
color: #55ff55;
margin-bottom: 1rem;
text-shadow: 1px 1px 0 #1a1a1a;
}
.stat-section {
background: #3b3b3b;
border: 2px solid #1a1a1a;
padding: 1.5rem;
box-shadow: inset 0 -2px 0 #2a2a2a, inset 0 2px 0 #555;
}
.total-mined {
text-align: center;
margin-bottom: 1.5rem;
padding-bottom: 1.5rem;
border-bottom: 2px solid #4b4b4b;
}
.stat-value {
font-size: 3rem;
font-weight: 700;
color: #55ffff;
line-height: 1;
text-shadow: 2px 2px 0 #1a1a1a;
}
.stat-label {
font-size: 0.875rem;
color: #a0a0a0;
margin-top: 0.5rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.blocks-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 0.75rem;
}
.block-stat {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem;
background: #2c2c2c;
border: 2px solid #1a1a1a;
transition: all 0.1s;
box-shadow: inset 0 -1px 0 #222, inset 0 1px 0 #444;
}
.block-stat:hover {
background: #4b4b4b;
box-shadow: inset 0 -1px 0 #333, inset 0 1px 0 #666;
}
.block-emoji {
font-size: 2rem;
line-height: 1;
}
.block-info {
flex: 1;
}
.block-name {
font-size: 0.875rem;
font-weight: 600;
color: #e0e0e0;
margin-bottom: 0.25rem;
}
.block-count {
font-size: 0.75rem;
color: #55ff55;
font-weight: 600;
}
/* Leaderboard */
.leaderboard {
margin-bottom: 2rem;
}
.leaderboard h3 {
font-size: 1.125rem;
font-weight: 600;
color: #ffaa00;
margin-bottom: 1rem;
text-shadow: 1px 1px 0 #1a1a1a;
}
.leaderboard-list {
background: #3b3b3b;
border: 2px solid #1a1a1a;
padding: 0.75rem;
box-shadow: inset 0 -2px 0 #2a2a2a, inset 0 2px 0 #555;
}
.leaderboard-item {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.75rem;
margin-bottom: 0.5rem;
background: #2c2c2c;
border: 2px solid #1a1a1a;
transition: all 0.1s;
box-shadow: inset 0 -1px 0 #222, inset 0 1px 0 #444;
}
.leaderboard-item:last-child {
margin-bottom: 0;
}
.leaderboard-item:hover {
background: #4b4b4b;
}
.leaderboard-item.rank-1 {
border-left: 4px solid #ffaa00;
}
.leaderboard-item.rank-2 {
border-left: 4px solid #a0a0a0;
}
.leaderboard-item.rank-3 {
border-left: 4px solid #cd7f32;
}
.rank {
font-size: 1.5rem;
font-weight: 700;
min-width: 3rem;
text-align: center;
color: #ffff55;
text-shadow: 1px 1px 0 #1a1a1a;
}
.miner-info {
flex: 1;
}
.miner-name {
font-size: 0.875rem;
font-weight: 600;
color: #e0e0e0;
margin-bottom: 0.25rem;
}
.miner-stats {
font-size: 0.75rem;
color: #a0a0a0;
}
.miner-score {
font-size: 1.25rem;
font-weight: 700;
color: #55ffff;
text-shadow: 1px 1px 0 #1a1a1a;
}
/* All Turtles Overview */
.all-turtles-stats h3 {
font-size: 1.125rem;
font-weight: 600;
color: #55ffff;
margin-bottom: 1rem;
text-shadow: 1px 1px 0 #1a1a1a;
}
.turtles-summary-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 1rem;
}
.turtle-summary-card {
background: #3b3b3b;
border: 2px solid #1a1a1a;
padding: 1rem;
transition: all 0.1s;
box-shadow: inset 0 -2px 0 #2a2a2a, inset 0 2px 0 #555;
}
.turtle-summary-card:hover {
background: #4b4b4b;
box-shadow: inset 0 -2px 0 #333, inset 0 2px 0 #666;
}
.turtle-summary-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem;
padding-bottom: 0.75rem;
border-bottom: 2px solid #4b4b4b;
}
.turtle-id {
font-size: 0.875rem;
font-weight: 600;
color: #e0e0e0;
}
.turtle-total {
font-size: 1.25rem;
font-weight: 700;
color: #55ffff;
text-shadow: 1px 1px 0 #1a1a1a;
}
.turtle-top-blocks {
display: flex;
gap: 0.5rem;
}
.mini-block-stat {
display: flex;
align-items: center;
gap: 0.25rem;
padding: 0.375rem 0.5rem;
background: #2c2c2c;
border: 2px solid #1a1a1a;
flex: 1;
}
.mini-emoji {
font-size: 1rem;
}
.mini-count {
font-size: 0.75rem;
font-weight: 600;
color: #55ff55;
}
/* Empty State */
.empty-state {
text-align: center;
padding: 4rem 2rem;
}
.empty-icon {
font-size: 4rem;
margin-bottom: 1rem;
opacity: 0.5;
}
.empty-title {
font-size: 1.25rem;
font-weight: 600;
color: #e0e0e0;
margin-bottom: 0.5rem;
}
.empty-text {
font-size: 0.875rem;
color: #a0a0a0;
}
.no-data {
text-align: center;
padding: 2rem;
color: #a0a0a0;
font-size: 0.875rem;
}
/* Mobile Responsive */
@media (max-width: 768px) {
.stats-panel {
padding: 1rem;
}
.stats-header {
flex-direction: column;
align-items: flex-start;
}
.time-filter {
width: 100%;
}
.time-filter button {
flex: 1;
padding: 0.625rem;
}
.blocks-grid {
grid-template-columns: 1fr;
}
.turtles-summary-grid {
grid-template-columns: 1fr;
}
.stat-value {
font-size: 2.5rem;
}
}
@media (max-width: 480px) {
.stats-header h2 {
font-size: 1.25rem;
}
.leaderboard-item {
padding: 0.5rem;
gap: 0.5rem;
}
.rank {
font-size: 1.25rem;
min-width: 2.5rem;
}
}

View File

@@ -0,0 +1,218 @@
import { useState, useEffect } from 'react';
import './StatsPanel.css';
const StatsPanel = ({ selectedTurtle, apiUrl }) => {
const [miningStats, setMiningStats] = useState([]);
const [topMiners, setTopMiners] = useState([]);
const [timeFilter, setTimeFilter] = useState(7); // days
const [loading, setLoading] = useState(false);
useEffect(() => {
loadMiningStats();
loadTopMiners();
// Refresh stats every 30 seconds
const interval = setInterval(() => {
loadMiningStats();
loadTopMiners();
}, 30000);
return () => clearInterval(interval);
}, [selectedTurtle, timeFilter]);
const loadMiningStats = async () => {
setLoading(true);
try {
const url = selectedTurtle
? `${apiUrl}/api/stats/mining/${selectedTurtle.turtleID}?days=${timeFilter}`
: `${apiUrl}/api/stats/mining?days=${timeFilter}`;
const response = await fetch(url);
if (response.ok) {
const data = await response.json();
// Response is either a single object (per turtle) or an array (all turtles)
setMiningStats(Array.isArray(data) ? data : [data]);
}
} catch (error) {
console.error('Failed to load mining stats:', error);
} finally {
setLoading(false);
}
};
const loadTopMiners = async () => {
try {
const response = await fetch(`${apiUrl}/api/stats/top-miners?limit=10`);
if (response.ok) {
const data = await response.json();
// Response is now a flat array of {turtleId, totalBlocks, uniqueTypes}
setTopMiners(Array.isArray(data) ? data : (data.topMiners || []));
}
} catch (error) {
console.error('Failed to load top miners:', error);
}
};
const getTotalBlocks = (stats) => {
if (!stats || !stats.blocks) return 0;
return stats.blocks.reduce((sum, block) => sum + block.count, 0);
};
const getBlockEmoji = (blockType) => {
if (!blockType) return '📦';
const lower = blockType.toLowerCase();
if (lower.includes('diamond')) return '💎';
if (lower.includes('gold')) return '🟡';
if (lower.includes('iron')) return '⚪';
if (lower.includes('coal')) return '⚫';
if (lower.includes('emerald')) return '🟢';
if (lower.includes('redstone')) return '🔴';
if (lower.includes('lapis')) return '🔵';
if (lower.includes('copper')) return '🟠';
if (lower.includes('stone')) return '🗿';
if (lower.includes('dirt')) return '🟤';
if (lower.includes('cobble')) return '🪨';
if (lower.includes('wood') || lower.includes('log')) return '🪵';
return '📦';
};
const formatBlockType = (blockType) => {
if (!blockType) return 'Unknown';
return blockType.replace('minecraft:', '').replace(/_/g, ' ')
.split(' ')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
};
return (
<div className="stats-panel">
<div className="stats-header">
<h2>📊 Mining Statistics</h2>
<div className="time-filter">
<button
className={timeFilter === 1 ? 'active' : ''}
onClick={() => setTimeFilter(1)}
>
24h
</button>
<button
className={timeFilter === 7 ? 'active' : ''}
onClick={() => setTimeFilter(7)}
>
7d
</button>
<button
className={timeFilter === 30 ? 'active' : ''}
onClick={() => setTimeFilter(30)}
>
30d
</button>
<button
className={timeFilter === 0 ? 'active' : ''}
onClick={() => setTimeFilter(0)}
>
All
</button>
</div>
</div>
{loading && <div className="loading">Loading statistics...</div>}
{/* Individual Turtle Stats */}
{selectedTurtle && miningStats.length > 0 && (
<div className="turtle-stats">
<h3>🐢 {selectedTurtle.name || `Turtle ${selectedTurtle.turtleID}`}</h3>
{miningStats.map((stat) => (
<div key={stat.turtleId} className="stat-section">
<div className="total-mined">
<div className="stat-value">{getTotalBlocks(stat)}</div>
<div className="stat-label">Total Blocks Mined</div>
</div>
{stat.blocks && stat.blocks.length > 0 && (
<div className="blocks-grid">
{stat.blocks.map((block, idx) => (
<div key={idx} className="block-stat">
<div className="block-emoji">{getBlockEmoji(block.blockType)}</div>
<div className="block-info">
<div className="block-name">{formatBlockType(block.blockType)}</div>
<div className="block-count">{block.count} blocks</div>
</div>
</div>
))}
</div>
)}
</div>
))}
</div>
)}
{/* Top Miners Leaderboard */}
<div className="leaderboard">
<h3>🏆 Top Miners</h3>
<div className="leaderboard-list">
{topMiners.length === 0 && (
<div className="no-data">No mining data yet. Start mining!</div>
)}
{topMiners.map((miner, idx) => (
<div key={miner.turtleId} className={`leaderboard-item rank-${idx + 1}`}>
<div className="rank">
{idx === 0 && '🥇'}
{idx === 1 && '🥈'}
{idx === 2 && '🥉'}
{idx > 2 && `#${idx + 1}`}
</div>
<div className="miner-info">
<div className="miner-name">Turtle {miner.turtleId}</div>
<div className="miner-stats">
{miner.totalBlocks} blocks {miner.uniqueTypes} types
</div>
</div>
<div className="miner-score">{miner.totalBlocks}</div>
</div>
))}
</div>
</div>
{/* All Turtles Summary */}
{!selectedTurtle && miningStats.length > 0 && (
<div className="all-turtles-stats">
<h3>📈 All Turtles Overview</h3>
<div className="turtles-summary-grid">
{miningStats.map((stat) => (
<div key={stat.turtleId} className="turtle-summary-card">
<div className="turtle-summary-header">
<span className="turtle-id">🐢 Turtle {stat.turtleId}</span>
<span className="turtle-total">{getTotalBlocks(stat)}</span>
</div>
{stat.blocks && stat.blocks.length > 0 && (
<div className="turtle-top-blocks">
{stat.blocks.slice(0, 3).map((block, idx) => (
<div key={idx} className="mini-block-stat">
<span className="mini-emoji">{getBlockEmoji(block.blockType)}</span>
<span className="mini-count">{block.count}</span>
</div>
))}
</div>
)}
</div>
))}
</div>
</div>
)}
{/* Empty State */}
{!loading && miningStats.length === 0 && topMiners.length === 0 && (
<div className="empty-state">
<div className="empty-icon"></div>
<div className="empty-title">No Mining Data Yet</div>
<div className="empty-text">
Start mining with your turtles to see statistics here!
</div>
</div>
)}
</div>
);
};
export default StatsPanel;

View File

@@ -0,0 +1,445 @@
/* ============================================
Minecraft-Themed Task Panel
============================================ */
.task-panel {
padding: 1.5rem;
background: #2c2c2c;
height: 100%;
overflow-y: auto;
font-family: 'Silkscreen', 'Courier New', monospace;
}
.task-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.task-header h2 {
font-size: 1.5rem;
font-weight: 700;
color: #ffff55;
margin: 0;
text-shadow: 2px 2px 0 #1a1a1a;
}
.create-task-btn {
padding: 0.75rem 1.5rem;
background: #4a8c2a;
border: 2px solid #1a1a1a;
color: white;
font-weight: 700;
font-size: 0.875rem;
cursor: pointer;
transition: all 0.1s;
font-family: 'Silkscreen', 'Courier New', monospace;
text-shadow: 1px 1px 0 #1a1a1a;
box-shadow: inset 0 2px 0 #6ab04c, inset 0 -2px 0 #2d6b1a;
}
.create-task-btn:hover {
background: #5a9c3a;
box-shadow: inset 0 2px 0 #7bc05c, inset 0 -2px 0 #3d7b2a;
}
.message {
padding: 0.75rem 1rem;
border: 2px solid;
margin-bottom: 1rem;
font-size: 0.875rem;
font-weight: 600;
animation: slideIn 0.3s;
}
.message.success {
background: #2d6b1a33;
color: #55ff55;
border-color: #55ff55;
}
.message.error {
background: #6b1a1a33;
color: #ff5555;
border-color: #ff5555;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Create Task Form */
.create-task-form {
background: #3b3b3b;
border: 2px solid #1a1a1a;
padding: 1.5rem;
margin-bottom: 1.5rem;
animation: slideIn 0.3s;
box-shadow: inset 0 -2px 0 #2a2a2a, inset 0 2px 0 #555;
}
.create-task-form h3 {
font-size: 1rem;
font-weight: 600;
color: #ffaa00;
margin: 0 0 1rem 0;
text-shadow: 1px 1px 0 #1a1a1a;
}
.create-task-form form {
display: flex;
flex-direction: column;
gap: 1rem;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.create-task-form label {
display: flex;
flex-direction: column;
gap: 0.5rem;
font-size: 0.875rem;
font-weight: 600;
color: #e0e0e0;
}
.create-task-form select,
.create-task-form input[type="text"],
.create-task-form input[type="number"] {
padding: 0.75rem;
background: #1a1a1a;
border: 2px solid #4b4b4b;
color: #e0e0e0;
font-size: 0.875rem;
font-family: 'Silkscreen', 'Courier New', monospace;
}
.create-task-form select:focus,
.create-task-form input:focus {
outline: none;
border-color: #55ffff;
}
.create-task-form input[type="range"] {
width: 100%;
accent-color: #4a8c2a;
}
.priority-value {
font-size: 0.875rem;
font-weight: 600;
text-align: center;
}
.coordinates-section h4 {
font-size: 0.875rem;
font-weight: 600;
color: #a0a0a0;
margin: 0 0 0.75rem 0;
}
.coordinates-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.coord-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.coord-group span {
font-size: 0.75rem;
font-weight: 600;
color: #a0a0a0;
text-transform: uppercase;
}
.coord-group input {
padding: 0.5rem;
background: #1a1a1a;
border: 2px solid #4b4b4b;
color: #e0e0e0;
font-size: 0.75rem;
font-family: 'Silkscreen', 'Courier New', monospace;
}
.submit-btn {
padding: 0.75rem 1.5rem;
background: #4a8c2a;
border: 2px solid #1a1a1a;
color: white;
font-weight: 700;
cursor: pointer;
transition: all 0.1s;
font-family: 'Silkscreen', 'Courier New', monospace;
text-shadow: 1px 1px 0 #1a1a1a;
box-shadow: inset 0 2px 0 #6ab04c, inset 0 -2px 0 #2d6b1a;
}
.submit-btn:hover {
background: #5a9c3a;
box-shadow: inset 0 2px 0 #7bc05c, inset 0 -2px 0 #3d7b2a;
}
/* Task Filters */
.task-filters {
display: flex;
gap: 0.25rem;
margin-bottom: 1.5rem;
flex-wrap: wrap;
}
.task-filters button {
padding: 0.5rem 1rem;
background: #6b6b6b;
border: 2px solid #1a1a1a;
color: #e0e0e0;
font-weight: 700;
font-size: 0.875rem;
cursor: pointer;
transition: all 0.1s;
font-family: 'Silkscreen', 'Courier New', monospace;
text-shadow: 1px 1px 0 #1a1a1a;
box-shadow: inset 0 -2px 0 #444, inset 0 2px 0 #888;
}
.task-filters button:hover {
background: #7b7b7b;
box-shadow: inset 0 -2px 0 #555, inset 0 2px 0 #999;
}
.task-filters button.active {
background: #4a8c2a;
color: white;
box-shadow: inset 0 2px 0 #6ab04c, inset 0 -2px 0 #2d6b1a;
}
/* Tasks List */
.tasks-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.loading {
text-align: center;
padding: 2rem;
color: #a0a0a0;
font-size: 0.875rem;
}
.task-card {
background: #3b3b3b;
border: 2px solid #1a1a1a;
padding: 1.5rem;
transition: all 0.1s;
box-shadow: inset 0 -2px 0 #2a2a2a, inset 0 2px 0 #555;
}
.task-card:hover {
background: #4b4b4b;
}
.task-card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
flex-wrap: wrap;
gap: 0.75rem;
}
.task-title {
display: flex;
align-items: center;
gap: 0.5rem;
}
.task-icon {
font-size: 1.5rem;
}
.task-type {
font-size: 1rem;
font-weight: 600;
color: #e0e0e0;
}
.task-badges {
display: flex;
gap: 0.5rem;
}
.priority-badge,
.status-badge {
padding: 0.25rem 0.75rem;
border: 2px solid #1a1a1a;
font-size: 0.75rem;
font-weight: 700;
color: white;
text-transform: uppercase;
letter-spacing: 0.05em;
text-shadow: 1px 1px 0 rgba(0,0,0,0.5);
}
.task-assignment {
font-size: 0.875rem;
color: #a0a0a0;
margin-bottom: 0.75rem;
padding: 0.5rem;
background: #2c2c2c;
border: 2px solid #1a1a1a;
}
.task-parameters {
font-size: 0.75rem;
color: #a0a0a0;
margin-bottom: 0.75rem;
padding: 0.5rem;
background: #2c2c2c;
border: 2px solid #1a1a1a;
font-family: 'Silkscreen', 'Courier New', monospace;
}
.task-result {
font-size: 0.875rem;
color: #55ff55;
margin-bottom: 0.75rem;
padding: 0.75rem;
background: #2d6b1a20;
border: 2px solid #55ff55;
border-left: 4px solid #55ff55;
}
.task-actions {
display: flex;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.task-actions button {
padding: 0.5rem 1rem;
background: #6b6b6b;
border: 2px solid #1a1a1a;
color: #e0e0e0;
font-size: 0.75rem;
font-weight: 700;
cursor: pointer;
transition: all 0.1s;
font-family: 'Silkscreen', 'Courier New', monospace;
text-shadow: 1px 1px 0 #1a1a1a;
box-shadow: inset 0 -2px 0 #444, inset 0 2px 0 #888;
}
.task-actions button:hover {
background: #7b7b7b;
box-shadow: inset 0 -2px 0 #555, inset 0 2px 0 #999;
}
.task-timestamp {
font-size: 0.75rem;
color: #7b7b7b;
font-style: italic;
}
/* Empty State */
.empty-state {
text-align: center;
padding: 4rem 2rem;
}
.empty-icon {
font-size: 4rem;
margin-bottom: 1rem;
opacity: 0.5;
}
.empty-title {
font-size: 1.25rem;
font-weight: 600;
color: #e0e0e0;
margin-bottom: 0.5rem;
}
.empty-text {
font-size: 0.875rem;
color: #a0a0a0;
}
/* Mobile Responsive */
@media (max-width: 768px) {
.task-panel {
padding: 1rem;
}
.task-header {
flex-direction: column;
align-items: stretch;
gap: 1rem;
}
.create-task-btn {
width: 100%;
}
.form-row {
grid-template-columns: 1fr;
}
.coordinates-grid {
grid-template-columns: 1fr;
}
.task-filters {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.task-filters button {
white-space: nowrap;
}
.task-card-header {
flex-direction: column;
align-items: flex-start;
}
.task-actions {
flex-wrap: wrap;
}
.task-actions button {
flex: 1;
min-width: 120px;
}
}
@media (max-width: 480px) {
.task-header h2 {
font-size: 1.25rem;
}
.task-card {
padding: 1rem;
}
.task-actions button {
min-width: 100px;
font-size: 0.7rem;
}
}

View File

@@ -0,0 +1,429 @@
import { useState, useEffect } from 'react';
import './TaskPanel.css';
const TaskPanel = ({ turtles, apiUrl }) => {
const [tasks, setTasks] = useState([]);
const [filter, setFilter] = useState('all'); // all, pending, in_progress, completed, failed
const [showCreateForm, setShowCreateForm] = useState(false);
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState(null);
// Form state
const [taskType, setTaskType] = useState('mine_area');
const [priority, setPriority] = useState(5);
const [assignedTurtleId, setAssignedTurtleId] = useState('');
const [parameters, setParameters] = useState({
x1: '', y1: '', z1: '',
x2: '', y2: '', z2: ''
});
const taskTypes = [
{ value: 'mine_area', label: '⛏️ Mine Area', icon: '⛏️' },
{ value: 'explore', label: '🔍 Explore', icon: '🔍' },
{ value: 'gather', label: '📦 Gather Resources', icon: '📦' },
{ value: 'build', label: '🏗️ Build Structure', icon: '🏗️' },
{ value: 'transport', label: '🚚 Transport Items', icon: '🚚' },
{ value: 'clear_area', label: '🧹 Clear Area', icon: '🧹' },
];
useEffect(() => {
loadTasks();
// Refresh tasks every 10 seconds
const interval = setInterval(loadTasks, 10000);
return () => clearInterval(interval);
}, [filter]);
const loadTasks = async () => {
setLoading(true);
try {
const url = filter === 'all'
? `${apiUrl}/api/tasks`
: `${apiUrl}/api/tasks?status=${filter}`;
const response = await fetch(url);
if (response.ok) {
const data = await response.json();
// Server returns flat array of formatted tasks
setTasks(Array.isArray(data) ? data : (data.tasks || []));
}
} catch (error) {
console.error('Failed to load tasks:', error);
} finally {
setLoading(false);
}
};
const createTask = async (e) => {
e.preventDefault();
// Validate coordinates
const coords = ['x1', 'y1', 'z1', 'x2', 'y2', 'z2'];
const hasCoords = coords.some(key => parameters[key] !== '');
if (hasCoords && !coords.every(key => parameters[key] !== '')) {
showMessage('Please fill all coordinates or leave them empty', 'error');
return;
}
const taskData = {
taskType,
priority: parseInt(priority),
assignedTurtleId: assignedTurtleId ? parseInt(assignedTurtleId) : null,
parameters: hasCoords ? {
x1: parseInt(parameters.x1),
y1: parseInt(parameters.y1),
z1: parseInt(parameters.z1),
x2: parseInt(parameters.x2),
y2: parseInt(parameters.y2),
z2: parseInt(parameters.z2)
} : {}
};
try {
const response = await fetch(`${apiUrl}/api/tasks`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(taskData)
});
if (response.ok) {
showMessage('Task created successfully!', 'success');
setShowCreateForm(false);
resetForm();
loadTasks();
} else {
showMessage('Failed to create task', 'error');
}
} catch (error) {
showMessage('Error creating task', 'error');
}
};
const updateTaskStatus = async (taskId, status, result = null) => {
try {
const response = await fetch(`${apiUrl}/api/tasks/${taskId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status, result })
});
if (response.ok) {
showMessage('Task updated', 'success');
loadTasks();
} else {
showMessage('Failed to update task', 'error');
}
} catch (error) {
showMessage('Error updating task', 'error');
}
};
const deleteTask = async (taskId) => {
if (!confirm('Are you sure you want to delete this task?')) return;
try {
const response = await fetch(`${apiUrl}/api/tasks/${taskId}`, {
method: 'DELETE'
});
if (response.ok) {
showMessage('Task deleted', 'success');
loadTasks();
} else {
showMessage('Failed to delete task', 'error');
}
} catch (error) {
showMessage('Error deleting task', 'error');
}
};
const resetForm = () => {
setTaskType('mine_area');
setPriority(5);
setAssignedTurtleId('');
setParameters({ x1: '', y1: '', z1: '', x2: '', y2: '', z2: '' });
};
const showMessage = (text, type) => {
setMessage({ text, type });
setTimeout(() => setMessage(null), 3000);
};
const getTaskIcon = (type) => {
const taskType = taskTypes.find(t => t.value === type);
return taskType ? taskType.icon : '📋';
};
const getStatusColor = (status) => {
switch (status) {
case 'pending': return '#a0a0a0';
case 'in_progress': return '#345ec3';
case 'completed': return '#4a8c2a';
case 'failed': return '#aa0000';
default: return '#a0a0a0';
}
};
const getPriorityLabel = (priority) => {
if (priority >= 8) return { label: 'Critical', color: '#ff5555' };
if (priority >= 6) return { label: 'High', color: '#ffaa00' };
if (priority >= 4) return { label: 'Medium', color: '#345ec3' };
return { label: 'Low', color: '#a0a0a0' };
};
return (
<div className="task-panel">
<div className="task-header">
<h2>📋 Task Queue</h2>
<button
className="create-task-btn"
onClick={() => setShowCreateForm(!showCreateForm)}
>
{showCreateForm ? '✕ Cancel' : ' New Task'}
</button>
</div>
{message && (
<div className={`message ${message.type}`}>
{message.text}
</div>
)}
{/* Create Task Form */}
{showCreateForm && (
<div className="create-task-form">
<h3>Create New Task</h3>
<form onSubmit={createTask}>
<div className="form-row">
<label>
Task Type:
<select value={taskType} onChange={(e) => setTaskType(e.target.value)}>
{taskTypes.map(type => (
<option key={type.value} value={type.value}>
{type.label}
</option>
))}
</select>
</label>
<label>
Priority (1-10):
<input
type="range"
min="1"
max="10"
value={priority}
onChange={(e) => setPriority(e.target.value)}
/>
<span className="priority-value" style={{ color: getPriorityLabel(priority).color }}>
{priority} - {getPriorityLabel(priority).label}
</span>
</label>
</div>
<label>
Assign to Turtle (optional):
<select value={assignedTurtleId} onChange={(e) => setAssignedTurtleId(e.target.value)}>
<option value="">Any available turtle</option>
{turtles.map(turtle => (
<option key={turtle.turtleID} value={turtle.turtleID}>
🐢 {turtle.name || `Turtle ${turtle.turtleID}`}
</option>
))}
</select>
</label>
<div className="coordinates-section">
<h4>Coordinates (optional):</h4>
<div className="coordinates-grid">
<div className="coord-group">
<span>Start:</span>
<input
type="number"
placeholder="X1"
value={parameters.x1}
onChange={(e) => setParameters({...parameters, x1: e.target.value})}
/>
<input
type="number"
placeholder="Y1"
value={parameters.y1}
onChange={(e) => setParameters({...parameters, y1: e.target.value})}
/>
<input
type="number"
placeholder="Z1"
value={parameters.z1}
onChange={(e) => setParameters({...parameters, z1: e.target.value})}
/>
</div>
<div className="coord-group">
<span>End:</span>
<input
type="number"
placeholder="X2"
value={parameters.x2}
onChange={(e) => setParameters({...parameters, x2: e.target.value})}
/>
<input
type="number"
placeholder="Y2"
value={parameters.y2}
onChange={(e) => setParameters({...parameters, y2: e.target.value})}
/>
<input
type="number"
placeholder="Z2"
value={parameters.z2}
onChange={(e) => setParameters({...parameters, z2: e.target.value})}
/>
</div>
</div>
</div>
<button type="submit" className="submit-btn">
Create Task
</button>
</form>
</div>
)}
{/* Filter Tabs */}
<div className="task-filters">
<button
className={filter === 'all' ? 'active' : ''}
onClick={() => setFilter('all')}
>
All ({tasks.length})
</button>
<button
className={filter === 'pending' ? 'active' : ''}
onClick={() => setFilter('pending')}
>
Pending
</button>
<button
className={filter === 'in_progress' ? 'active' : ''}
onClick={() => setFilter('in_progress')}
>
In Progress
</button>
<button
className={filter === 'completed' ? 'active' : ''}
onClick={() => setFilter('completed')}
>
Completed
</button>
<button
className={filter === 'failed' ? 'active' : ''}
onClick={() => setFilter('failed')}
>
Failed
</button>
</div>
{/* Tasks List */}
<div className="tasks-list">
{loading && <div className="loading">Loading tasks...</div>}
{!loading && tasks.length === 0 && (
<div className="empty-state">
<div className="empty-icon">📋</div>
<div className="empty-title">No Tasks Found</div>
<div className="empty-text">
{filter === 'all'
? 'Create a task to get started!'
: `No ${filter.replace('_', ' ')} tasks`}
</div>
</div>
)}
{tasks.map(task => {
const priorityInfo = getPriorityLabel(task.priority);
const turtle = turtles.find(t => t.turtleID === task.assignedTurtleId);
return (
<div key={task.taskId} className="task-card">
<div className="task-card-header">
<div className="task-title">
<span className="task-icon">{getTaskIcon(task.taskType)}</span>
<span className="task-type">
{taskTypes.find(t => t.value === task.taskType)?.label || task.taskType}
</span>
</div>
<div className="task-badges">
<span
className="priority-badge"
style={{ backgroundColor: priorityInfo.color }}
>
{priorityInfo.label}
</span>
<span
className="status-badge"
style={{ backgroundColor: getStatusColor(task.status) }}
>
{task.status.replace('_', ' ')}
</span>
</div>
</div>
{task.assignedTurtleId && (
<div className="task-assignment">
🐢 {turtle?.name || `Turtle ${task.assignedTurtleId}`}
</div>
)}
{task.parameters && Object.keys(task.parameters).length > 0 && (
<div className="task-parameters">
<strong>Area:</strong> ({task.parameters.x1}, {task.parameters.y1}, {task.parameters.z1})
({task.parameters.x2}, {task.parameters.y2}, {task.parameters.z2})
</div>
)}
{task.result && (
<div className="task-result">
<strong>Result:</strong> {task.result}
</div>
)}
<div className="task-actions">
{task.status === 'pending' && (
<>
<button onClick={() => updateTaskStatus(task.taskId, 'in_progress')}>
Start
</button>
<button onClick={() => deleteTask(task.taskId)}>
🗑 Delete
</button>
</>
)}
{task.status === 'in_progress' && (
<>
<button onClick={() => updateTaskStatus(task.taskId, 'completed', 'Task completed')}>
Complete
</button>
<button onClick={() => updateTaskStatus(task.taskId, 'failed', 'Task failed')}>
Fail
</button>
</>
)}
{(task.status === 'completed' || task.status === 'failed') && (
<button onClick={() => deleteTask(task.taskId)}>
🗑 Delete
</button>
)}
</div>
<div className="task-timestamp">
Created: {new Date(task.createdAt).toLocaleString()}
</div>
</div>
);
})}
</div>
</div>
);
};
export default TaskPanel;

View File

@@ -0,0 +1,217 @@
/* ============================================
Minecraft-Themed Voice Control
============================================ */
.voice-control {
padding: 1rem;
background: #3b3b3b;
border: 2px solid #1a1a1a;
font-family: 'Silkscreen', 'Courier New', monospace;
box-shadow: inset 0 -2px 0 #2a2a2a, inset 0 2px 0 #555;
}
.voice-control.unsupported {
text-align: center;
padding: 2rem;
color: #ff5555;
}
.voice-control.unsupported small {
color: #a0a0a0;
display: block;
margin-top: 0.5rem;
}
.voice-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.voice-header h3 {
font-size: 1rem;
font-weight: 600;
color: #ffff55;
margin: 0;
text-shadow: 1px 1px 0 #1a1a1a;
}
.selected-turtle {
font-size: 0.875rem;
color: #55ff55;
font-weight: 600;
}
.no-selection {
font-size: 0.875rem;
color: #a0a0a0;
}
.voice-button {
width: 100%;
padding: 1.5rem;
background: #345ec3;
border: 3px solid #1a1a1a;
color: white;
font-weight: 700;
font-size: 1rem;
cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
transition: all 0.1s;
position: relative;
overflow: hidden;
font-family: 'Silkscreen', 'Courier New', monospace;
text-shadow: 1px 1px 0 #1a1a1a;
box-shadow: inset 0 2px 0 #5577dd, inset 0 -3px 0 #223399;
}
.voice-button:hover:not(:disabled) {
background: #4a6ed3;
box-shadow: inset 0 2px 0 #6688ee, inset 0 -3px 0 #3344aa;
}
.voice-button:disabled {
background: #4b4b4b;
cursor: not-allowed;
opacity: 0.5;
box-shadow: inset 0 -2px 0 #333, inset 0 2px 0 #666;
}
.voice-button.listening {
background: #aa0000;
box-shadow: inset 0 2px 0 #dd3333, inset 0 -3px 0 #770000;
animation: pulse-glow 2s infinite;
}
@keyframes pulse-glow {
0%, 100% {
box-shadow: inset 0 2px 0 #dd3333, inset 0 -3px 0 #770000, 0 0 20px rgba(255, 85, 85, 0.5);
}
50% {
box-shadow: inset 0 2px 0 #dd3333, inset 0 -3px 0 #770000, 0 0 40px rgba(255, 85, 85, 0.8);
}
}
.voice-button .icon {
font-size: 2rem;
}
.pulse-ring {
position: absolute;
width: 80px;
height: 80px;
border: 3px solid rgba(255, 255, 255, 0.6);
animation: pulse-ring 1.5s infinite;
}
@keyframes pulse-ring {
0% {
transform: scale(0.5);
opacity: 1;
}
100% {
transform: scale(1.5);
opacity: 0;
}
}
.voice-feedback {
margin-top: 1rem;
padding: 1rem;
background: #2c2c2c;
border: 2px solid #1a1a1a;
}
.transcript {
color: #e0e0e0;
margin-bottom: 0.5rem;
font-size: 0.875rem;
}
.transcript strong {
color: #55ffff;
}
.last-command {
color: #55ff55;
font-size: 0.875rem;
font-family: 'Silkscreen', 'Courier New', monospace;
}
.last-command strong {
color: #55ff55;
}
.voice-commands-help {
margin-top: 1rem;
}
.voice-commands-help details {
background: #2c2c2c;
border: 2px solid #1a1a1a;
}
.voice-commands-help summary {
padding: 0.75rem;
cursor: pointer;
font-weight: 600;
color: #a0a0a0;
-webkit-user-select: none;
user-select: none;
transition: all 0.1s;
}
.voice-commands-help summary:hover {
color: #e0e0e0;
}
.commands-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 1rem;
padding: 1rem;
}
.command-category h4 {
font-size: 0.75rem;
color: #ffaa00;
text-transform: uppercase;
margin: 0 0 0.5rem 0;
font-weight: 700;
letter-spacing: 0.05em;
text-shadow: 1px 1px 0 #1a1a1a;
}
.command-category ul {
list-style: none;
padding: 0;
margin: 0;
}
.command-category li {
font-size: 0.75rem;
color: #a0a0a0;
padding: 0.25rem 0;
font-family: 'Silkscreen', 'Courier New', monospace;
}
.command-category li::before {
content: "▸ ";
color: #55ff55;
font-weight: bold;
}
/* Mobile responsive */
@media (max-width: 768px) {
.voice-button {
padding: 1.25rem;
}
.commands-grid {
grid-template-columns: 1fr;
}
}

View File

@@ -0,0 +1,247 @@
import React, { useState, useEffect } from 'react';
import { useTurtleStore } from '../store/turtleStore';
import './VoiceControl.css';
export default function VoiceControl() {
const [isListening, setIsListening] = useState(false);
const [transcript, setTranscript] = useState('');
const [lastCommand, setLastCommand] = useState('');
const [supported, setSupported] = useState(false);
const [recognition, setRecognition] = useState(null);
const selectedTurtle = useTurtleStore((state) => state.getSelectedTurtle());
const setTurtleState = useTurtleStore((state) => state.setTurtleState);
const moveForward = useTurtleStore((state) => state.moveForward);
const moveBack = useTurtleStore((state) => state.moveBack);
const moveUp = useTurtleStore((state) => state.moveUp);
const moveDown = useTurtleStore((state) => state.moveDown);
const turnLeft = useTurtleStore((state) => state.turnLeft);
const turnRight = useTurtleStore((state) => state.turnRight);
const digBlock = useTurtleStore((state) => state.digBlock);
const digBlockUp = useTurtleStore((state) => state.digBlockUp);
const digBlockDown = useTurtleStore((state) => state.digBlockDown);
const placeBlock = useTurtleStore((state) => state.placeBlock);
const refuelTurtle = useTurtleStore((state) => state.refuelTurtle);
useEffect(() => {
// Check if speech recognition is supported
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
if (SpeechRecognition) {
setSupported(true);
const recognitionInstance = new SpeechRecognition();
recognitionInstance.continuous = false;
recognitionInstance.interimResults = false;
recognitionInstance.lang = 'en-US';
recognitionInstance.onresult = (event) => {
const speechResult = event.results[0][0].transcript.toLowerCase();
setTranscript(speechResult);
processVoiceCommand(speechResult);
};
recognitionInstance.onerror = (event) => {
console.error('Speech recognition error:', event.error);
setIsListening(false);
};
recognitionInstance.onend = () => {
setIsListening(false);
};
setRecognition(recognitionInstance);
}
}, []);
const processVoiceCommand = (command) => {
if (!selectedTurtle) {
speak('Please select a turtle first');
return;
}
const turtleId = selectedTurtle.turtleID;
let actionName = null;
// Movement commands (server-side)
if (command.includes('forward') || command.includes('go ahead')) {
moveForward(turtleId);
actionName = 'forward';
} else if (command.includes('back') || command.includes('backward')) {
moveBack(turtleId);
actionName = 'back';
} else if (command.includes('turn left') || command.includes('left')) {
turnLeft(turtleId);
actionName = 'turn left';
} else if (command.includes('turn right') || command.includes('right')) {
turnRight(turtleId);
actionName = 'turn right';
} else if (command.includes('go up') || command.includes('move up')) {
moveUp(turtleId);
actionName = 'up';
} else if (command.includes('go down') || command.includes('move down')) {
moveDown(turtleId);
actionName = 'down';
} else if (command.includes('dig')) {
if (command.includes('up')) {
digBlockUp(turtleId);
actionName = 'dig up';
} else if (command.includes('down')) {
digBlockDown(turtleId);
actionName = 'dig down';
} else {
digBlock(turtleId);
actionName = 'dig';
}
} else if (command.includes('place') || command.includes('build')) {
placeBlock(turtleId);
actionName = 'place';
// State machine commands (server-side)
} else if (command.includes('explore') || command.includes('start exploring')) {
setTurtleState(turtleId, 'exploring');
actionName = 'explore';
} else if (command.includes('mine') || command.includes('start mining')) {
setTurtleState(turtleId, 'mining');
actionName = 'mine';
} else if (command.includes('return home') || command.includes('go home') || command.includes('come back')) {
setTurtleState(turtleId, 'goHome');
actionName = 'go home';
} else if (command.includes('stop')) {
setTurtleState(turtleId, 'idle');
actionName = 'idle';
} else if (command.includes('refuel')) {
setTurtleState(turtleId, 'refueling');
actionName = 'refuel';
} else if (command.includes('farm')) {
setTurtleState(turtleId, 'farming');
actionName = 'farm';
} else if (command.includes('dump')) {
setTurtleState(turtleId, 'dumpInventory');
actionName = 'dump inventory';
}
if (actionName) {
setLastCommand(actionName);
speak(`Sending ${actionName} command`);
} else {
speak('Command not recognized');
}
};
const speak = (text) => {
if ('speechSynthesis' in window) {
const utterance = new SpeechSynthesisUtterance(text);
utterance.rate = 1.1;
utterance.pitch = 1.0;
window.speechSynthesis.speak(utterance);
}
};
const startListening = () => {
if (recognition && !isListening) {
setTranscript('');
setIsListening(true);
recognition.start();
}
};
const stopListening = () => {
if (recognition && isListening) {
recognition.stop();
setIsListening(false);
}
};
if (!supported) {
return (
<div className="voice-control unsupported">
<p> Voice commands not supported in this browser</p>
<small>Try Chrome, Edge, or Safari</small>
</div>
);
}
return (
<div className="voice-control">
<div className="voice-header">
<h3>🎤 Voice Control</h3>
{selectedTurtle ? (
<span className="selected-turtle">Turtle {selectedTurtle.turtleID}</span>
) : (
<span className="no-selection">Select a turtle first</span>
)}
</div>
<button
className={`voice-button ${isListening ? 'listening' : ''}`}
onClick={isListening ? stopListening : startListening}
disabled={!selectedTurtle}
>
{isListening ? (
<>
<span className="pulse-ring"></span>
<span className="icon">🎙</span>
<span>Listening...</span>
</>
) : (
<>
<span className="icon">🎤</span>
<span>Press to Speak</span>
</>
)}
</button>
{transcript && (
<div className="voice-feedback">
<div className="transcript">
<strong>You said:</strong> "{transcript}"
</div>
{lastCommand && (
<div className="last-command">
<strong>Command:</strong> {lastCommand}
</div>
)}
</div>
)}
<div className="voice-commands-help">
<details>
<summary>Available Commands</summary>
<div className="commands-grid">
<div className="command-category">
<h4>Movement</h4>
<ul>
<li>"forward" / "go ahead"</li>
<li>"back" / "backward"</li>
<li>"turn left" / "left"</li>
<li>"turn right" / "right"</li>
<li>"go up" / "move up"</li>
<li>"go down" / "move down"</li>
</ul>
</div>
<div className="command-category">
<h4>Actions</h4>
<ul>
<li>"dig"</li>
<li>"dig up"</li>
<li>"dig down"</li>
<li>"place" / "build"</li>
<li>"refuel"</li>
</ul>
</div>
<div className="command-category">
<h4>Autonomous</h4>
<ul>
<li>"explore" / "start exploring"</li>
<li>"mine" / "start mining"</li>
<li>"return home" / "go home"</li>
<li>"stop"</li>
<li>"set home"</li>
<li>"status" / "report"</li>
</ul>
</div>
</div>
</details>
</div>
</div>
);
}

View File

@@ -1,3 +1,5 @@
@import url('https://fonts.googleapis.com/css2?family=Silkscreen:wght@400;700&display=swap');
* {
margin: 0;
padding: 0;
@@ -5,13 +7,12 @@
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background: #0a0e1a;
color: #e0e0e0;
font-family: 'Silkscreen', 'Courier New', monospace;
-webkit-font-smoothing: none;
-moz-osx-font-smoothing: unset;
image-rendering: pixelated;
background: #2c2c2c;
color: #d4d4d4;
overflow: hidden;
}
@@ -21,5 +22,5 @@ body {
}
code {
font-family: 'Courier New', monospace;
font-family: 'Silkscreen', 'Courier New', monospace;
}

View File

@@ -1,11 +1,18 @@
import { create } from 'zustand';
const WS_URL = 'ws://localhost:3002';
const API_URL = 'http://localhost:3001';
// Use environment variables or fallback to relative URLs for proxy
const WS_URL = import.meta.env.VITE_WS_URL || `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/ws`;
const API_URL = import.meta.env.VITE_API_URL || `${window.location.protocol}//${window.location.host}/api`;
console.log('🔌 WebSocket URL:', WS_URL);
console.log('📡 API URL:', API_URL);
export const useTurtleStore = create((set, get) => ({
// State
turtles: {},
players: {},
worldBlocks: [],
chunkAnalyses: {},
selectedTurtleId: null,
connected: false,
ws: null,
@@ -28,7 +35,17 @@ export const useTurtleStore = create((set, get) => ({
data.turtles.forEach(turtle => {
turtlesMap[turtle.turtleID] = turtle;
});
set({ turtles: turtlesMap });
const playersMap = {};
if (data.players && Array.isArray(data.players)) {
data.players.forEach(player => {
playersMap[player.playerID] = player;
});
}
set({
turtles: turtlesMap,
players: playersMap,
worldBlocks: data.blocks || []
});
} else if (data.type === 'turtle_update') {
set(state => ({
turtles: {
@@ -36,12 +53,105 @@ export const useTurtleStore = create((set, get) => ({
[data.turtle.turtleID]: data.turtle
}
}));
} else if (data.type === 'turtle_disconnected') {
// 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_removed' || data.type === 'turtle_disconnected') {
console.log(`🔌 Turtle ${data.turtleID} removed`);
set(state => {
const newTurtles = { ...state.turtles };
delete newTurtles[data.turtleID];
return { turtles: newTurtles };
// If the removed turtle was selected, deselect it
const newSelectedId = state.selectedTurtleId === data.turtleID
? null
: state.selectedTurtleId;
return {
turtles: newTurtles,
selectedTurtleId: newSelectedId
};
});
} else if (data.type === 'player_update') {
set(state => ({
players: {
...state.players,
[data.playerID]: {
playerID: data.playerID,
position: data.position,
label: data.label || null,
timestamp: data.timestamp || Date.now()
}
}
}));
} else if (data.type === 'block_discovered') {
if (data.block) {
set(state => {
const blockMap = new Map(state.worldBlocks.map(b => [`${b.x},${b.y},${b.z}`, b]));
const key = `${data.block.x},${data.block.y},${data.block.z}`;
blockMap.set(key, data.block);
return { worldBlocks: Array.from(blockMap.values()) };
});
}
} else if (data.type === 'blocks_discovered') {
if (data.blocks && Array.isArray(data.blocks)) {
set(state => {
const blockMap = new Map(state.worldBlocks.map(b => [`${b.x},${b.y},${b.z}`, b]));
data.blocks.forEach(block => {
const key = `${block.x},${block.y},${block.z}`;
blockMap.set(key, block);
});
return { worldBlocks: Array.from(blockMap.values()) };
});
}
} else if (data.type === 'turtle_event') {
// Handle live turtle events (inventory, peripherals)
const turtleId = data.turtleID;
if (turtleId && data.eventType === 'inventory_update' && data.inventory) {
set(state => {
const turtle = state.turtles[turtleId];
if (!turtle) return state;
return {
turtles: {
...state.turtles,
[turtleId]: { ...turtle, inventory: data.inventory }
}
};
});
} else if (turtleId && (data.eventType === 'peripheral_attached' || data.eventType === 'peripheral_detached')) {
set(state => {
const turtle = state.turtles[turtleId];
if (!turtle) return state;
return {
turtles: {
...state.turtles,
[turtleId]: { ...turtle, peripherals: data.peripherals || turtle.peripherals }
}
};
});
}
} else if (data.type === 'chunk_analysis') {
// Store chunk analysis results
if (data.chunk) {
set(state => ({
chunkAnalyses: {
...state.chunkAnalyses,
[`${data.chunk.x},${data.chunk.z}`]: data.chunk
}
}));
}
} else if (data.type === 'block_deleted') {
// Remove a block from the world map (turtle moved into it)
if (data.x !== undefined && data.y !== undefined && data.z !== undefined) {
set(state => {
const key = `${data.x},${data.y},${data.z}`;
return {
worldBlocks: state.worldBlocks.filter(b => `${b.x},${b.y},${b.z}` !== key)
};
});
}
}
} catch (error) {
console.error('Error processing message:', error);
@@ -67,28 +177,332 @@ export const useTurtleStore = create((set, get) => ({
selectTurtle: (turtleId) => {
set({ selectedTurtleId: turtleId });
},
sendCommand: async (turtleId, command, param = null) => {
const { ws } = get();
updateBlocksFromSurroundings: (turtle) => {
const { surroundings, position, facing } = turtle;
if (!surroundings || !position || facing === undefined) return;
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
type: 'command',
turtleID: turtleId,
command,
param
}));
} else {
// Fallback to REST API
try {
await fetch(`${API_URL}/api/turtle/${turtleId}/command`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ command, param })
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()
});
} catch (error) {
console.error('Error sending command:', error);
}
});
set({ worldBlocks: Array.from(blockMap.values()) });
},
// Set turtle state machine state via REST API
setTurtleState: async (turtleId, stateName, stateData = {}) => {
console.log(`🔄 Setting state for turtle ${turtleId}: ${stateName}`, stateData);
try {
const response = await fetch(`${API_URL}/turtle/${turtleId}/state`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ state: stateName, data: stateData })
});
const result = await response.json();
console.log(' ✅ State set:', result);
return result;
} catch (error) {
console.error(' ❌ Error setting state:', error);
// Fallback: send via WebSocket as a command
const { ws } = get();
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
type: 'command',
turtleID: turtleId,
command: 'set_state',
param: { state: stateName, data: stateData }
}));
}
}
},
// Execute arbitrary Lua code on a turtle
execOnTurtle: async (turtleId, code) => {
console.log(`💻 Exec on turtle ${turtleId}:`, code);
try {
const response = await fetch(`${API_URL}/turtle/${turtleId}/exec`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code })
});
return await response.json();
} catch (error) {
console.error(' ❌ Error executing:', error);
return { success: false, error: error.message };
}
},
// ========== Server-Side Movement & Actions ==========
// All movement/actions are routed through the server's Turtle.js exec() pipeline
_turtleAction: async (turtleId, action, body = {}) => {
try {
const response = await fetch(`${API_URL}/turtle/${turtleId}/${action}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
return await response.json();
} catch (error) {
console.error(` ❌ Error ${action}:`, error);
return { success: false, error: error.message };
}
},
moveForward: async (turtleId) => get()._turtleAction(turtleId, 'forward'),
moveBack: async (turtleId) => get()._turtleAction(turtleId, 'back'),
moveUp: async (turtleId) => get()._turtleAction(turtleId, 'up'),
moveDown: async (turtleId) => get()._turtleAction(turtleId, 'down'),
turnLeft: async (turtleId) => get()._turtleAction(turtleId, 'turnLeft'),
turnRight: async (turtleId) => get()._turtleAction(turtleId, 'turnRight'),
digBlock: async (turtleId) => get()._turtleAction(turtleId, 'dig'),
digBlockUp: async (turtleId) => get()._turtleAction(turtleId, 'digUp'),
digBlockDown: async (turtleId) => get()._turtleAction(turtleId, 'digDown'),
placeBlock: async (turtleId, text) => get()._turtleAction(turtleId, 'place', { text }),
placeBlockUp: async (turtleId, text) => get()._turtleAction(turtleId, 'placeUp', { text }),
placeBlockDown: async (turtleId, text) => get()._turtleAction(turtleId, 'placeDown', { text }),
refuelTurtle: async (turtleId, count) => get()._turtleAction(turtleId, 'refuel-action', { count }),
// Fetch chunk analyses from server
fetchChunkAnalyses: async () => {
try {
const response = await fetch(`${API_URL}/chunks`);
if (response.ok) {
const data = await response.json();
const analyses = {};
data.forEach(c => { analyses[`${c.x},${c.z}`] = c; });
set({ chunkAnalyses: analyses });
}
} catch (error) {
console.error(' ❌ Error fetching chunks:', error);
}
},
// Request chunk analysis for a specific chunk
analyzeChunk: async (chunkX, chunkZ) => {
try {
const response = await fetch(`${API_URL}/chunks/${chunkX}/${chunkZ}/analyze`, {
method: 'POST'
});
if (response.ok) {
const data = await response.json();
set(state => ({
chunkAnalyses: {
...state.chunkAnalyses,
[`${chunkX},${chunkZ}`]: data
}
}));
return data;
}
} catch (error) {
console.error(' ❌ Error analyzing chunk:', error);
}
},
// Search blocks by name pattern
searchBlocks: async (namePattern) => {
try {
const response = await fetch(`${API_URL}/world/blocks/search?name=${encodeURIComponent(namePattern)}`);
if (response.ok) {
return await response.json();
}
} catch (error) {
console.error(' ❌ Error searching blocks:', error);
}
return [];
},
// Rename a turtle
renameTurtle: async (turtleId, name) => {
try {
const response = await fetch(`${API_URL}/turtle/${turtleId}/rename`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name })
});
return await response.json();
} catch (error) {
console.error(' ❌ Error renaming turtle:', error);
return { success: false, error: error.message };
}
},
// Equip left
equipLeft: async (turtleId) => {
try {
const response = await fetch(`${API_URL}/turtle/${turtleId}/equip-left`, { method: 'POST' });
return await response.json();
} catch (error) {
return { success: false, error: error.message };
}
},
// Equip right
equipRight: async (turtleId) => {
try {
const response = await fetch(`${API_URL}/turtle/${turtleId}/equip-right`, { method: 'POST' });
return await response.json();
} catch (error) {
return { success: false, error: error.message };
}
},
// Select inventory slot
selectSlot: async (turtleId, slot) => {
try {
const response = await fetch(`${API_URL}/turtle/${turtleId}/select`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ slot })
});
return await response.json();
} catch (error) {
return { success: false, error: error.message };
}
},
// Transfer items between slots
transferItems: async (turtleId, fromSlot, toSlot, count) => {
try {
const response = await fetch(`${API_URL}/turtle/${turtleId}/transfer`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ fromSlot, toSlot, count })
});
return await response.json();
} catch (error) {
return { success: false, error: error.message };
}
},
// Sort/compact inventory
sortInventory: async (turtleId) => {
try {
const response = await fetch(`${API_URL}/turtle/${turtleId}/sort`, { method: 'POST' });
return await response.json();
} catch (error) {
return { success: false, error: error.message };
}
},
// Connect to adjacent inventory
connectToInventory: async (turtleId, side) => {
try {
const response = await fetch(`${API_URL}/turtle/${turtleId}/connect-inventory`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ side })
});
return await response.json();
} catch (error) {
return { success: false, error: error.message };
}
},
// Drop items
dropItems: async (turtleId, direction = 'front', count) => {
try {
const response = await fetch(`${API_URL}/turtle/${turtleId}/drop`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ direction, count })
});
return await response.json();
} catch (error) {
return { success: false, error: error.message };
}
},
// Suck items
suckItems: async (turtleId, direction = 'front', count) => {
try {
const response = await fetch(`${API_URL}/turtle/${turtleId}/suck`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ direction, count })
});
return await response.json();
} catch (error) {
return { success: false, error: error.message };
}
},
// Update turtle config
updateTurtleConfig: async (turtleId, config) => {
try {
const response = await fetch(`${API_URL}/turtle/${turtleId}/config`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(config)
});
return await response.json();
} catch (error) {
return { success: false, error: error.message };
}
},
// Get turtle config
getTurtleConfig: async (turtleId) => {
try {
const response = await fetch(`${API_URL}/turtle/${turtleId}/config`);
return await response.json();
} catch (error) {
return { success: false, error: error.message };
}
},
// Explore (batched 3-direction inspect)
exploreTurtle: async (turtleId) => {
try {
const response = await fetch(`${API_URL}/turtle/${turtleId}/explore`, { method: 'POST' });
return await response.json();
} catch (error) {
return { success: false, error: error.message };
}
},
// GPS locate
gpsLocateTurtle: async (turtleId) => {
try {
const response = await fetch(`${API_URL}/turtle/${turtleId}/gps`, { method: 'POST' });
return await response.json();
} catch (error) {
return { success: false, error: error.message };
}
},

View File

@@ -4,6 +4,19 @@ import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
server: {
port: 3000
port: 3000,
host: '0.0.0.0',
hmr: {
clientPort: 3000
}
},
preview: {
port: 3000,
host: '0.0.0.0',
strictPort: true,
cors: true,
headers: {
'Access-Control-Allow-Origin': '*'
}
}
})

43
docker-compose.dev.yml Normal file
View File

@@ -0,0 +1,43 @@
version: '3.8'
services:
# Backend server (development with hot reload)
server:
build:
context: ./server
dockerfile: Dockerfile.dev
container_name: turtle-server-dev
ports:
- "3001:3001"
- "3002:3002"
volumes:
- ./server:/app
- /app/node_modules
environment:
- NODE_ENV=development
command: npm run dev
networks:
- turtle-network
# Frontend client (development with hot reload)
client:
build:
context: ./client
dockerfile: Dockerfile.dev
container_name: turtle-client-dev
ports:
- "3000:3000"
volumes:
- ./client:/app
- /app/node_modules
environment:
- NODE_ENV=development
depends_on:
- server
command: npm run dev
networks:
- turtle-network
networks:
turtle-network:
driver: bridge

46
docker-compose.yml Normal file
View File

@@ -0,0 +1,46 @@
services:
# Backend server
server:
build:
context: ./server
dockerfile: Dockerfile
container_name: turtle-server
ports:
- "4200:3001" # HTTP API + WebSocket (unified)
environment:
- NODE_ENV=production
- PORT=3001
- INVENTORY_SERVER_URL=${INVENTORY_SERVER_URL:-}
- API_KEY=${API_KEY:-}
restart: unless-stopped
networks:
- turtle-network
healthcheck:
test: ["CMD", "node", "-e", "require('http').get('http://localhost:3001/api/turtles', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"]
interval: 30s
timeout: 3s
retries: 3
start_period: 10s
# Frontend client
client:
build:
context: ./client
dockerfile: Dockerfile
container_name: turtle-client
ports:
- "4444:3000" # Vite preview server
depends_on:
- server
restart: unless-stopped
networks:
- turtle-network
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3000"]
interval: 30s
timeout: 3s
retries: 3
networks:
turtle-network:
driver: bridge

36
etc/apps.db Normal file
View File

@@ -0,0 +1,36 @@
{
[ "rt_turtle_controller" ] = {
title = "Turtle Controller",
category = "RemoteTurtle",
run = "turtle.lua",
requires = "turtle",
},
[ "rt_gps_host" ] = {
title = "GPS Host",
category = "RemoteTurtle",
run = "gpshost.lua",
},
[ "rt_web_bridge" ] = {
title = "Web Bridge",
category = "RemoteTurtle",
run = "webbridge.lua",
},
[ "rt_pocket_control" ] = {
title = "Pocket Control",
category = "RemoteTurtle",
run = "pocketcontrol.lua",
requires = "pocket",
},
[ "rt_pocket_remote" ] = {
title = "Pocket Remote",
category = "RemoteTurtle",
run = "pocketremote.lua",
requires = "pocket",
},
[ "rt_pocket_gps" ] = {
title = "Pocket GPS",
category = "RemoteTurtle",
run = "pocketgps.lua",
requires = "pocket",
},
}

10
gpshost.lua Normal file
View File

@@ -0,0 +1,10 @@
-- Runs a GPS host at boot with fixed coordinates.
local x, y, z = 0, 64, 0 -- TODO: set these to this computer's GPS host coordinates
-- If your modem isn't on a default side, open rednet explicitly (optional).
-- Example:
-- local side = "top"
-- if peripheral.getType(side) == "modem" and not rednet.isOpen(side) then rednet.open(side) end
shell.run("gps", "host", tostring(x), tostring(y), tostring(z))

24
package.json Normal file
View File

@@ -0,0 +1,24 @@
{
"name": "turtle-control-center",
"version": "1.0.0",
"description": "Web-based control center for ComputerCraft mining turtles",
"scripts": {
"install:all": "cd server && npm install && cd ../client && npm install",
"dev": "concurrently \"npm run server\" \"npm run client\"",
"server": "cd server && npm start",
"client": "cd client && npm run dev",
"build": "cd client && npm run build"
},
"keywords": [
"minecraft",
"computercraft",
"turtle",
"control",
"web"
],
"author": "",
"license": "MIT",
"devDependencies": {
"concurrently": "^8.2.2"
}
}

646
pocketcontrol.lua Normal file
View File

@@ -0,0 +1,646 @@
-- Unified Pocket Control Center
-- Combines turtle control, GPS tracking, server management, and webbridge control
-- Communicates wirelessly with webbridge - NO direct HTTP calls
local Channels = require('platform.channels')
local CHANNEL_SEND = Channels.get('remoteturtle.command')
local CHANNEL_RECEIVE = Channels.get('remoteturtle.response')
local STATUS_CHANNEL = Channels.get('remoteturtle.status')
local POCKET_CHANNEL = Channels.get('remoteturtle.pocket')
-- Find modem
local modem = peripheral.find("modem")
if not modem then
error("No wireless modem found!")
end
-- Equip modem if this is a pocket computer
if pocket then
pocket.equipBack()
modem = peripheral.find("modem")
end
local WebBridge = require('platform.webbridge')
WebBridge.openChannels(modem, {
'remoteturtle.response',
'remoteturtle.status',
'remoteturtle.pocket',
})
local w, h = term.getSize()
-- State
local turtles = {}
local selectedTurtle = nil
local mode = "overview" -- overview, control, gps, webbridge, server
local myPosition = nil
local webbridgeStatus = nil
local serverStats = nil
local logMessages = {}
local maxLogs = 50
-- Button system
local buttons = {}
local function addLog(message, color)
table.insert(logMessages, 1, {
text = message,
time = os.epoch("utc"),
color = color or colors.white
})
if #logMessages > maxLogs then
table.remove(logMessages)
end
end
local function clearButtons()
buttons = {}
end
local function addButton(x, y, width, height, label, action, color)
table.insert(buttons, {
x = x, y = y, width = width, height = height,
label = label, action = action, color = color or colors.gray
})
end
local function drawButton(btn, highlight)
term.setCursorPos(btn.x, btn.y)
local bg = highlight and colors.white or btn.color
local fg = highlight and colors.black or colors.white
term.setBackgroundColor(bg)
term.setTextColor(fg)
local text = btn.label
local padding = math.floor((btn.width - #text) / 2)
for dy = 0, btn.height - 1 do
term.setCursorPos(btn.x, btn.y + dy)
if dy == math.floor(btn.height / 2) then
term.write(string.rep(" ", padding) .. text .. string.rep(" ", btn.width - padding - #text))
else
term.write(string.rep(" ", btn.width))
end
end
end
local function checkButton(x, y)
for _, btn in ipairs(buttons) do
if x >= btn.x and x < btn.x + btn.width and
y >= btn.y and y < btn.y + btn.height then
return btn
end
end
end
-- Update my GPS position and send to webbridge
local function updateMyPosition()
local x, y, z = gps.locate(2)
if x then
myPosition = {x = math.floor(x), y = math.floor(y), z = math.floor(z)}
-- Send to webbridge (which will forward to server)
modem.transmit(POCKET_CHANNEL, CHANNEL_RECEIVE, {
type = "player_position",
playerID = os.getComputerID(),
label = os.getComputerLabel() or ("Pocket #" .. os.getComputerID()),
position = myPosition,
timestamp = os.epoch("utc")
})
return true
else
addLog("GPS: Failed to locate", colors.red)
end
return false
end
-- Send command to turtle via webbridge
local function sendCommand(turtleID, command, param)
modem.transmit(POCKET_CHANNEL, CHANNEL_RECEIVE, {
type = "turtle_command",
turtleID = turtleID,
command = command,
param = param,
from = os.getComputerID()
})
addLog("Sent: " .. command .. " -> Turtle #" .. turtleID, colors.yellow)
end
-- Send webbridge control command
local function sendWebbridgeCommand(command)
modem.transmit(POCKET_CHANNEL, CHANNEL_RECEIVE, {
type = "webbridge_control",
command = command,
from = os.getComputerID()
})
addLog("Webbridge: " .. command, colors.cyan)
end
-- Request server stats via webbridge
local function fetchServerStats()
modem.transmit(POCKET_CHANNEL, CHANNEL_RECEIVE, {
type = "server_stats_request",
from = os.getComputerID()
})
end
-- Draw header
local function drawHeader()
term.setBackgroundColor(colors.blue)
term.setTextColor(colors.white)
term.setCursorPos(1, 1)
term.clearLine()
local title = "POCKET CTRL"
term.setCursorPos(2, 1)
term.write(title)
-- Show quick stats
term.setCursorPos(w - 10, 1)
term.write("T:" .. #turtles .. " ")
if myPosition then
term.write("GPS")
end
end
-- Draw mode tabs
local function drawTabs()
clearButtons()
local tabWidth = math.floor(w / 5)
local y = 2
-- Draw tab highlights
term.setCursorPos(1, y)
term.setBackgroundColor(colors.black)
term.clearLine()
local tabs = {
{name = "TRT", mode = "overview"},
{name = "CTL", mode = "control"},
{name = "GPS", mode = "gps"},
{name = "WEB", mode = "webbridge"},
{name = "SRV", mode = "server"}
}
for i, tab in ipairs(tabs) do
local x = (i - 1) * tabWidth + 1
addButton(x, y, tabWidth, 1, tab.name, function() mode = tab.mode end,
mode == tab.mode and colors.green or colors.gray)
term.setCursorPos(x, y)
if mode == tab.mode then
term.setBackgroundColor(colors.green)
term.setTextColor(colors.white)
else
term.setBackgroundColor(colors.gray)
term.setTextColor(colors.lightGray)
end
local padding = math.floor((tabWidth - #tab.name) / 2)
term.write(string.rep(" ", padding) .. tab.name .. string.rep(" ", tabWidth - padding - #tab.name))
end
end
-- Draw overview mode
local function drawOverview()
term.setBackgroundColor(colors.black)
local y = 4
term.setTextColor(colors.lime)
term.setCursorPos(2, y)
term.write("Online: " .. #turtles)
y = y + 1
if #turtles == 0 then
term.setTextColor(colors.gray)
term.setCursorPos(2, y + 1)
term.write("No turtles")
else
for i, turtle in ipairs(turtles) do
if y >= h - 3 then break end
term.setCursorPos(2, y)
term.setTextColor(colors.yellow)
term.write("#" .. turtle.turtleID)
term.setTextColor(colors.lightGray)
term.setCursorPos(8, y)
local stateText = (turtle.mode or "idle"):sub(1, 8)
term.write(stateText)
if turtle.position then
term.setCursorPos(17, y)
term.setTextColor(colors.gray)
term.write(string.format("%d,%d,%d", turtle.position.x, turtle.position.y, turtle.position.z))
end
y = y + 1
end
end
-- Add select button
local btnY = h - 1
addButton(2, btnY, 10, 1, "SELECT", function()
if #turtles > 0 then
selectedTurtle = (selectedTurtle or 0) % #turtles + 1
addLog("Selected #" .. turtles[selectedTurtle].turtleID, colors.lime)
end
end, colors.green)
drawButton(buttons[#buttons], false)
end
-- Draw control mode
local function drawControl()
term.setBackgroundColor(colors.black)
local y = 4
if not selectedTurtle or not turtles[selectedTurtle] then
term.setTextColor(colors.red)
term.setCursorPos(2, y)
term.write("No turtle selected")
term.setCursorPos(2, y + 1)
term.setTextColor(colors.gray)
term.write("SELECT from TURTLES")
return
end
local turtle = turtles[selectedTurtle]
term.setTextColor(colors.yellow)
term.setCursorPos(2, y)
term.write("Turtle #" .. turtle.turtleID)
y = y + 1
-- Movement buttons (compact layout)
local btnW = 4
local btnH = 1
local centerX = math.floor(w / 2) - 2
local centerY = 8
-- Movement controls
addButton(centerX, centerY - 2, btnW, btnH, "FWD", function()
sendCommand(turtle.turtleID, "forward")
end, colors.blue)
addButton(centerX, centerY + 2, btnW, btnH, "BCK", function()
sendCommand(turtle.turtleID, "back")
end, colors.blue)
addButton(centerX - btnW - 1, centerY, btnW, btnH, "LFT", function()
sendCommand(turtle.turtleID, "turnLeft")
end, colors.blue)
addButton(centerX + btnW + 1, centerY, btnW, btnH, "RGT", function()
sendCommand(turtle.turtleID, "turnRight")
end, colors.blue)
addButton(centerX, centerY, btnW, btnH, "UP", function()
sendCommand(turtle.turtleID, "up")
end, colors.green)
-- Action buttons (bottom rows)
local btnY = h - 5
addButton(2, btnY, 6, 1, "EXPLR", function()
sendCommand(turtle.turtleID, "explore")
end, colors.cyan)
addButton(9, btnY, 6, 1, "MINE", function()
sendCommand(turtle.turtleID, "mine")
end, colors.orange)
addButton(16, btnY, 6, 1, "HOME", function()
sendCommand(turtle.turtleID, "returnHome")
end, colors.yellow)
addButton(23, btnY, 6, 1, "STOP", function()
sendCommand(turtle.turtleID, "stop")
end, colors.red)
btnY = h - 3
addButton(2, btnY, 6, 1, "DOWN", function()
sendCommand(turtle.turtleID, "down")
end, colors.green)
addButton(9, btnY, 6, 1, "DIG", function()
sendCommand(turtle.turtleID, "dig")
end, colors.red)
for i = #buttons - 8, #buttons do
if buttons[i] then
drawButton(buttons[i], false)
end
end
end
-- Draw GPS mode
local function drawGPS()
term.setBackgroundColor(colors.black)
local y = 4
term.setTextColor(colors.lime)
term.setCursorPos(2, y)
term.write("GPS Tracking")
y = y + 1
-- My position (compact)
term.setTextColor(colors.white)
term.setCursorPos(2, y)
if myPosition then
term.write(string.format("You: %d,%d,%d", myPosition.x, myPosition.y, myPosition.z))
else
term.setTextColor(colors.red)
term.write("GPS: Not available")
end
y = y + 2
-- Distance to selected turtle
if selectedTurtle and turtles[selectedTurtle] and myPosition then
local turtle = turtles[selectedTurtle]
if turtle.position then
local dx = turtle.position.x - myPosition.x
local dy = turtle.position.y - myPosition.y
local dz = turtle.position.z - myPosition.z
local dist = math.sqrt(dx*dx + dy*dy + dz*dz)
term.setTextColor(colors.cyan)
term.setCursorPos(2, y)
term.write(string.format("Turtle #%d:", turtle.turtleID))
y = y + 1
term.setTextColor(colors.white)
term.setCursorPos(2, y)
term.write(string.format("Dist: %.1f blocks", dist))
y = y + 1
term.setTextColor(colors.gray)
term.setCursorPos(2, y)
term.write(string.format("Delta: %d,%d,%d", dx, dy, dz))
end
end
y = h - 2
addButton(2, y, 13, 1, "UPDATE GPS", function()
if updateMyPosition() then
addLog("GPS updated", colors.lime)
else
addLog("GPS failed", colors.red)
end
end, colors.green)
addButton(16, y, 13, 1, "GOTO TURTLE", function()
if selectedTurtle and turtles[selectedTurtle] and turtles[selectedTurtle].position then
local pos = turtles[selectedTurtle].position
addLog(string.format("At: %d,%d,%d", pos.x, pos.y, pos.z), colors.cyan)
end
end, colors.blue)
drawButton(buttons[#buttons-1], false)
drawButton(buttons[#buttons], false)
end
-- Draw webbridge mode
local function drawWebbridge()
term.setBackgroundColor(colors.black)
local y = 4
term.setTextColor(colors.lime)
term.setCursorPos(2, y)
term.write("Webbridge Control")
y = y + 1
if webbridgeStatus then
term.setTextColor(colors.white)
term.setCursorPos(2, y)
term.write("Status: ONLINE")
y = y + 1
term.setCursorPos(2, y)
term.write("MSG:" .. (webbridgeStatus.messages or 0) ..
" CMD:" .. (webbridgeStatus.commands or 0))
else
term.setTextColor(colors.gray)
term.setCursorPos(2, y)
term.write("Status: Unknown")
end
y = y + 2
-- Compact button layout
addButton(2, y, 13, 1, "PING", function()
sendWebbridgeCommand("ping")
end, colors.blue)
addButton(16, y, 13, 1, "RESTART", function()
sendWebbridgeCommand("restart")
end, colors.orange)
y = y + 2
addButton(2, y, 13, 1, "STATUS", function()
sendWebbridgeCommand("status")
end, colors.green)
addButton(16, y, 13, 1, "LOGS", function()
sendWebbridgeCommand("logs")
end, colors.cyan)
for i = #buttons - 3, #buttons do
if buttons[i] then
drawButton(buttons[i], false)
end
end
-- Show recent logs (compact)
y = y + 3
term.setTextColor(colors.gray)
term.setCursorPos(2, y)
term.write("Activity:")
y = y + 1
for i = 1, math.min(3, #logMessages) do
if y >= h - 1 then break end
local log = logMessages[i]
term.setTextColor(log.color)
term.setCursorPos(2, y)
local text = log.text
if #text > w - 3 then
text = text:sub(1, w - 6) .. "..."
end
term.write(text)
y = y + 1
end
end
-- Draw server mode
local function drawServer()
term.setBackgroundColor(colors.black)
local y = 4
term.setTextColor(colors.lime)
term.setCursorPos(2, y)
term.write("Server Interface")
y = y + 1
if serverStats then
term.setTextColor(colors.white)
term.setCursorPos(2, y)
term.write("Status: ONLINE")
y = y + 1
term.setCursorPos(2, y)
term.write("Turtles: " .. (serverStats.turtles or 0))
y = y + 1
term.setCursorPos(2, y)
term.write("Blocks: " .. (serverStats.blocks or 0))
else
term.setTextColor(colors.red)
term.setCursorPos(2, y)
term.write("Status: OFFLINE")
end
y = h - 2
addButton(2, y, 13, 1, "REFRESH", function()
fetchServerStats()
addLog("Stats refreshed", colors.lime)
end, colors.green)
addButton(16, y, 13, 1, "WEB INFO", function()
modem.transmit(POCKET_CHANNEL, CHANNEL_RECEIVE, {
type = "web_interface_request",
from = os.getComputerID()
})
addLog("Requesting info", colors.cyan)
end, colors.blue)
drawButton(buttons[#buttons-1], false)
drawButton(buttons[#buttons], false)
end
-- Main draw function
local function draw()
term.setBackgroundColor(colors.black)
term.clear()
drawHeader()
drawTabs()
if mode == "overview" then
drawOverview()
elseif mode == "control" then
drawControl()
elseif mode == "gps" then
drawGPS()
elseif mode == "webbridge" then
drawWebbridge()
elseif mode == "server" then
drawServer()
end
-- Status bar (compact)
term.setBackgroundColor(colors.gray)
term.setCursorPos(1, h)
term.clearLine()
term.setTextColor(colors.white)
term.setCursorPos(2, h)
term.write(string.format("T:%d", #turtles))
if selectedTurtle and turtles[selectedTurtle] then
term.setCursorPos(8, h)
term.write("#" .. turtles[selectedTurtle].turtleID)
end
if myPosition then
term.setCursorPos(w - 5, h)
term.write("GPS")
end
end
-- Main loop
print("Pocket Control Center")
print("Initializing...")
-- Initial GPS update
updateMyPosition()
-- Start background tasks
parallel.waitForAny(
function()
-- GPS update loop
while true do
sleep(2)
updateMyPosition()
end
end,
function()
-- Server stats update loop
while true do
sleep(10)
fetchServerStats()
end
end,
function()
-- Message listener
while true do
local event, side, channel, replyChannel, message = os.pullEvent("modem_message")
-- Uses Channels.match() for dual-mode safety: accepts messages on
-- both legacy (102/103) and target (4212/4213) channels during migration.
if Channels.match('remoteturtle.status', channel) and type(message) == "table" then
if message.type == "status" then
-- Update turtle list
local found = false
for i, t in ipairs(turtles) do
if t.turtleID == message.turtleID then
turtles[i] = message
found = true
break
end
end
if not found then
table.insert(turtles, message)
addLog("Turtle #" .. message.turtleID .. " connected", colors.lime)
end
end
elseif Channels.match('remoteturtle.pocket', channel) and type(message) == "table" then
-- Handle responses from webbridge
if message.type == "webbridge_status" then
webbridgeStatus = message.data
addLog("Webbridge status received", colors.cyan)
elseif message.type == "server_stats" then
serverStats = message.data
addLog("Server stats received", colors.lime)
elseif message.type == "webbridge_log" then
addLog("[WB] " .. (message.text or ""), colors.lightGray)
elseif message.type == "command_ack" then
addLog("Command acknowledged", colors.green)
elseif message.type == "error" then
addLog("Error: " .. (message.error or "unknown"), colors.red)
end
end
end
end,
function()
-- UI loop
draw()
while true do
local event, p1, p2, p3 = os.pullEvent()
if event == "mouse_click" or event == "mouse_up" then
local btn = checkButton(p2, p3)
if btn and btn.action then
btn.action()
end
draw()
elseif event == "char" then
if p1 == "q" then
break
elseif p1 == "r" then
draw()
end
end
end
end
)
term.setBackgroundColor(colors.black)
term.setTextColor(colors.white)
term.clear()
term.setCursorPos(1, 1)
print("Pocket Control Center closed")

275
pocketgps.lua Normal file
View File

@@ -0,0 +1,275 @@
-- Live GPS Tracker for Pocket Computer
-- Shows your current location in real-time
local Channels = require('platform.channels')
local WebBridge = require('platform.webbridge')
local STATUS_CHANNEL = Channels.get('remoteturtle.status')
-- Setup modem
local modem = peripheral.find("modem")
if not modem then
error("No wireless modem found!")
end
-- Equip modem if on pocket computer
if pocket then
pocket.equipBack()
modem = peripheral.find("modem")
end
WebBridge.openChannels(modem, { 'remoteturtle.status' })
local w, h = term.getSize()
local myID = os.getComputerID()
local currentPos = {x = 0, y = 0, z = 0}
local lastUpdate = 0
local turtles = {}
-- Get GPS position
local function updateGPS()
local x, y, z = gps.locate(5)
if x then
currentPos = {x = math.floor(x), y = math.floor(y), z = math.floor(z)}
lastUpdate = os.clock()
return true
end
return false
end
-- Draw header
local function drawHeader()
term.setCursorPos(1, 1)
term.setBackgroundColor(colors.blue)
term.setTextColor(colors.white)
term.clearLine()
term.setCursorPos(math.floor((w - 14) / 2), 1)
term.write(" GPS TRACKER ")
end
-- Draw position info
local function drawPosition()
term.setBackgroundColor(colors.black)
term.setTextColor(colors.white)
-- Computer ID
term.setCursorPos(2, 3)
term.write("Computer ID: ")
term.setTextColor(colors.yellow)
term.write(tostring(myID))
-- Current coordinates
term.setTextColor(colors.white)
term.setCursorPos(2, 5)
term.write("Current Position:")
term.setCursorPos(4, 6)
term.setTextColor(colors.red)
term.write("X: ")
term.setTextColor(colors.white)
term.write(string.format("%d", currentPos.x))
term.setCursorPos(4, 7)
term.setTextColor(colors.green)
term.write("Y: ")
term.setTextColor(colors.white)
term.write(string.format("%d", currentPos.y))
term.setCursorPos(4, 8)
term.setTextColor(colors.blue)
term.write("Z: ")
term.setTextColor(colors.white)
term.write(string.format("%d", currentPos.z))
-- Time since last update
local timeSince = os.clock() - lastUpdate
term.setCursorPos(2, 10)
term.setTextColor(colors.gray)
if lastUpdate > 0 then
term.write(string.format("Updated %.1fs ago", timeSince))
else
term.write("No GPS fix yet...")
end
end
-- Draw nearby turtles
local function drawTurtles()
term.setBackgroundColor(colors.black)
term.setTextColor(colors.white)
term.setCursorPos(2, 12)
term.write("Nearby Turtles:")
local line = 13
local count = 0
-- Sort turtles by distance
local sortedTurtles = {}
for id, data in pairs(turtles) do
if data.position then
local dx = data.position.x - currentPos.x
local dy = data.position.y - currentPos.y
local dz = data.position.z - currentPos.z
local distance = math.sqrt(dx*dx + dy*dy + dz*dz)
table.insert(sortedTurtles, {id = id, data = data, distance = distance})
end
end
table.sort(sortedTurtles, function(a, b) return a.distance < b.distance end)
-- Display up to 5 closest turtles
for i = 1, math.min(5, #sortedTurtles) do
local turtle = sortedTurtles[i]
term.setCursorPos(4, line)
term.setTextColor(colors.yellow)
term.write(string.format("#%d", turtle.id))
term.setTextColor(colors.white)
term.write(string.format(" [%.1fm]", turtle.distance))
term.setCursorPos(6, line + 1)
term.setTextColor(colors.gray)
term.write(string.format("(%d, %d, %d)",
turtle.data.position.x,
turtle.data.position.y,
turtle.data.position.z))
line = line + 2
count = count + 1
if line >= h - 2 then break end
end
if count == 0 then
term.setCursorPos(4, line)
term.setTextColor(colors.gray)
term.write("None detected")
end
end
-- Draw footer
local function drawFooter()
term.setCursorPos(1, h)
term.setBackgroundColor(colors.gray)
term.setTextColor(colors.white)
term.clearLine()
term.setCursorPos(2, h)
term.write("[Q] Quit [R] Refresh")
end
-- Full screen refresh
local function draw()
term.setBackgroundColor(colors.black)
term.clear()
drawHeader()
drawPosition()
drawTurtles()
drawFooter()
end
-- Handle turtle status broadcasts
local function handleStatus(message)
if message.id and message.id ~= myID then
if not turtles[message.id] then
turtles[message.id] = {}
end
-- Update turtle data
if message.position then
turtles[message.id].position = message.position
end
if message.status then
turtles[message.id].status = message.status
end
if message.fuel then
turtles[message.id].fuel = message.fuel
end
turtles[message.id].lastSeen = os.clock()
end
end
-- Clean up old turtles (not seen in 30 seconds)
local function cleanupTurtles()
local now = os.clock()
for id, data in pairs(turtles) do
if data.lastSeen and (now - data.lastSeen) > 30 then
turtles[id] = nil
end
end
end
-- Main loop
local function main()
print("GPS Tracker starting...")
print("Getting initial GPS fix...")
-- Get initial GPS position
updateGPS()
draw()
local gpsTimer = os.startTimer(2) -- Update GPS every 2 seconds
local cleanupTimer = os.startTimer(10) -- Cleanup every 10 seconds
local drawTimer = os.startTimer(0.5) -- Redraw every 0.5 seconds
while true do
local event, param1, param2, param3, param4, param5 = os.pullEvent()
if event == "timer" then
if param1 == gpsTimer then
updateGPS()
gpsTimer = os.startTimer(2)
elseif param1 == cleanupTimer then
cleanupTurtles()
cleanupTimer = os.startTimer(10)
elseif param1 == drawTimer then
draw()
drawTimer = os.startTimer(0.5)
end
elseif event == "modem_message" then
local channel = param2
local replyChannel = param3
local message = param4
-- Uses Channels.match() for dual-mode safety: accepts messages on
-- both legacy (102) and target (4212) channels during migration.
if Channels.match('remoteturtle.status', channel) and type(message) == "table" then
handleStatus(message)
end
elseif event == "char" then
if param1 == "q" or param1 == "Q" then
term.setBackgroundColor(colors.black)
term.setTextColor(colors.white)
term.clear()
term.setCursorPos(1, 1)
print("GPS Tracker stopped.")
return
elseif param1 == "r" or param1 == "R" then
updateGPS()
draw()
end
elseif event == "mouse_click" then
-- Check if quit button clicked (bottom bar)
if param3 == h then
term.setBackgroundColor(colors.black)
term.setTextColor(colors.white)
term.clear()
term.setCursorPos(1, 1)
print("GPS Tracker stopped.")
return
end
end
end
end
-- Run the tracker
local success, err = pcall(main)
if not success then
term.setBackgroundColor(colors.black)
term.setTextColor(colors.white)
term.clear()
term.setCursorPos(1, 1)
print("Error: " .. tostring(err))
end

View File

@@ -1,9 +1,12 @@
-- Touch-Enabled Command Center for Pocket Computer (FIXED)
-- Monitor and control autonomous mining turtles
local CHANNEL_SEND = 100
local CHANNEL_RECEIVE = 101
local STATUS_CHANNEL = 102
local Channels = require('platform.channels')
local WebBridge = require('platform.webbridge')
local CHANNEL_SEND = Channels.get('remoteturtle.command')
local CHANNEL_RECEIVE = Channels.get('remoteturtle.response')
local STATUS_CHANNEL = Channels.get('remoteturtle.status')
local modem = peripheral.find("modem")
if not modem then
@@ -15,15 +18,17 @@ if pocket then
modem = peripheral.find("modem")
end
modem.open(CHANNEL_RECEIVE)
modem.open(STATUS_CHANNEL)
WebBridge.openChannels(modem, {
'remoteturtle.response',
'remoteturtle.status',
})
local w, h = term.getSize()
-- Tracked turtles
local turtles = {}
local selectedTurtle = nil
local viewMode = "overview" -- overview, detail, manual
local viewMode = "overview" -- overview, detail, manual, modes
-- Button system
local buttons = {}
@@ -90,6 +95,16 @@ local function sendCommand(turtleID, command, param)
})
end
-- Send state change command to turtle (new protocol)
local function sendStateCommand(turtleID, stateName, stateData)
modem.transmit(CHANNEL_SEND, CHANNEL_RECEIVE, {
type = "state_change",
state = stateName,
data = stateData or {},
target = turtleID
})
end
-- Helper function to format fuel display
local function formatFuel(fuel)
if not fuel then
@@ -127,7 +142,7 @@ local function drawOverview()
-- Turtle info box
term.setCursorPos(1, y)
term.setTextColor(selected and colors.lime or colors.white)
print(string.format("T-%d [%s]", turtle.turtleID or 0, turtle.mode or "unknown"))
print(string.format("T-%d [%s]", turtle.turtleID or 0, turtle.state or turtle.mode or "idle"))
if turtle.position then
print(string.format(" %d,%d,%d",
@@ -197,7 +212,7 @@ local function drawDetail()
term.setTextColor(colors.white)
print("")
print("Mode: " .. (turtle.mode or "unknown"))
print("State: " .. (turtle.state or turtle.mode or "idle"))
print("Fuel: " .. formatFuel(turtle.fuel))
if turtle.position then
@@ -234,8 +249,8 @@ local function drawDetail()
print(" Empty")
end
-- Action buttons
local btnY = h - 7
-- Action buttons (row 1: explore/home/stop)
local btnY = h - 10
addButton(1, btnY, 8, 2, "EXPLORE", function()
sendCommand(turtle.turtleID, "explore")
end, colors.green)
@@ -248,16 +263,60 @@ local function drawDetail()
sendCommand(turtle.turtleID, "stop")
end, colors.red)
btnY = h - 4
addButton(1, btnY, 12, 2, "MANUAL", function()
-- Row 2: manual/modes/setHome
btnY = h - 7
addButton(1, btnY, 8, 2, "MANUAL", function()
viewMode = "manual"
sendCommand(turtle.turtleID, "manual")
end, colors.purple)
addButton(14, btnY, 12, 2, "SET HOME", function()
addButton(10, btnY, 8, 2, "MODES", function()
viewMode = "modes"
end, colors.cyan)
addButton(19, btnY, 7, 2, "HOME*", function()
sendCommand(turtle.turtleID, "setHome")
end, colors.blue)
-- Row 3: equip/rename
btnY = h - 4
addButton(1, btnY, 8, 2, "EQUIP L", function()
-- Send eval to equip left
modem.transmit(CHANNEL_SEND, CHANNEL_RECEIVE, {
type = "eval",
uuid = tostring(math.random(100000, 999999)),
code = "return turtle.equipLeft()",
target = turtle.turtleID
})
end, colors.purple)
addButton(10, btnY, 8, 2, "EQUIP R", function()
modem.transmit(CHANNEL_SEND, CHANNEL_RECEIVE, {
type = "eval",
uuid = tostring(math.random(100000, 999999)),
code = "return turtle.equipRight()",
target = turtle.turtleID
})
end, colors.purple)
addButton(19, btnY, 7, 2, "RENAME", function()
term.setBackgroundColor(colors.black)
term.clear()
term.setCursorPos(1, 1)
term.setTextColor(colors.yellow)
print("Enter new name:")
term.setTextColor(colors.white)
local name = read()
if name and #name > 0 then
modem.transmit(CHANNEL_SEND, CHANNEL_RECEIVE, {
type = "rename",
name = name,
target = turtle.turtleID
})
end
draw()
end, colors.lightBlue)
addButton(1, h - 1, 12, 2, "< BACK", function()
viewMode = "overview"
end, colors.gray)
@@ -349,9 +408,37 @@ local function drawManual()
sendCommand(turtle.turtleID, "refuel")
end, colors.lime)
addButton(19, h - 3, 7, 2, "INFO", function()
sendCommand(turtle.turtleID, "status")
end, colors.lightBlue)
addButton(19, h - 3, 7, 2, "SORT", function()
modem.transmit(CHANNEL_SEND, CHANNEL_RECEIVE, {
type = "eval",
uuid = tostring(math.random(100000, 999999)),
code = [[
local moved = 0
for slot = 1, 16 do
local item = turtle.getItemDetail(slot)
if item then
for target = 1, slot - 1 do
local ti = turtle.getItemDetail(target)
if not ti then
turtle.select(slot)
turtle.transferTo(target)
moved = moved + 1
break
elseif ti.name == item.name and ti.count < 64 then
turtle.select(slot)
turtle.transferTo(target)
moved = moved + 1
if turtle.getItemCount(slot) == 0 then break end
end
end
end
end
turtle.select(1)
return moved
]],
target = turtle.turtleID
})
end, colors.cyan)
addButton(1, h, 12, 1, "< BACK", function()
viewMode = "detail"
@@ -369,6 +456,98 @@ local function drawManual()
term.setTextColor(colors.white)
end
local function drawModes()
if not selectedTurtle or not turtles[selectedTurtle] then
viewMode = "overview"
return
end
clearButtons()
local turtle = turtles[selectedTurtle]
term.setBackgroundColor(colors.black)
term.clear()
term.setCursorPos(1, 1)
term.setTextColor(colors.cyan)
print("=STATE MACHINE=")
term.setTextColor(colors.white)
print(string.format("T-%d [%s]", turtle.turtleID or 0, turtle.state or turtle.mode or "idle"))
-- State buttons
local btnY = 4
local btnW = 12
addButton(1, btnY, btnW, 2, "IDLE", function()
sendStateCommand(turtle.turtleID, "idle")
sendCommand(turtle.turtleID, "stop")
end, colors.gray)
addButton(14, btnY, btnW, 2, "EXPLORE", function()
sendStateCommand(turtle.turtleID, "exploring")
sendCommand(turtle.turtleID, "explore")
end, colors.green)
btnY = btnY + 3
addButton(1, btnY, btnW, 2, "MINE", function()
sendStateCommand(turtle.turtleID, "mining")
sendCommand(turtle.turtleID, "explore")
end, colors.orange)
addButton(14, btnY, btnW, 2, "FARM", function()
sendStateCommand(turtle.turtleID, "farming")
end, colors.lime)
btnY = btnY + 3
addButton(1, btnY, btnW, 2, "GO HOME", function()
sendStateCommand(turtle.turtleID, "goHome")
sendCommand(turtle.turtleID, "returnHome")
end, colors.yellow)
addButton(14, btnY, btnW, 2, "REFUEL", function()
sendStateCommand(turtle.turtleID, "refueling")
sendCommand(turtle.turtleID, "refuel")
end, colors.red)
btnY = btnY + 3
addButton(1, btnY, btnW, 2, "DUMP INV", function()
sendStateCommand(turtle.turtleID, "dumpInventory")
end, colors.brown)
addButton(14, btnY, btnW, 2, "MOVE TO", function()
-- Could prompt for coordinates, for now just sends moving state
sendStateCommand(turtle.turtleID, "moving")
end, colors.lightBlue)
btnY = btnY + 3
addButton(1, btnY, btnW, 2, "SCAN", function()
sendStateCommand(turtle.turtleID, "scan")
end, colors.purple)
addButton(14, btnY, btnW, 2, "EXTRACT", function()
sendStateCommand(turtle.turtleID, "extraction")
end, colors.magenta)
btnY = btnY + 3
addButton(1, btnY, btnW, 2, "BUILD", function()
sendStateCommand(turtle.turtleID, "building")
end, colors.lightBlue)
addButton(14, btnY, btnW, 2, "AUTOCRAFT", function()
sendStateCommand(turtle.turtleID, "autocraft")
end, colors.pink)
-- Back button
addButton(1, h - 1, 12, 2, "< BACK", function()
viewMode = "detail"
end, colors.gray)
for _, btn in ipairs(buttons) do
drawButton(btn)
end
term.setTextColor(colors.white)
end
local function draw()
if viewMode == "overview" then
drawOverview()
@@ -376,6 +555,8 @@ local function draw()
drawDetail()
elseif viewMode == "manual" then
drawManual()
elseif viewMode == "modes" then
drawModes()
end
end
@@ -450,14 +631,20 @@ parallel.waitForAny(
end,
function()
-- Status receiver
-- Uses Channels.match() for dual-mode safety: accepts status on
-- both legacy (102) and target (4212) channels during migration.
while true do
local event, side, channel, replyChannel, message = os.pullEvent("modem_message")
if channel == STATUS_CHANNEL and type(message) == "table" and message.type == "status" then
if Channels.match('remoteturtle.status', channel) and type(message) == "table" and message.type == "status" then
-- Update or add turtle
local found = false
for i, t in ipairs(turtles) do
if t.turtleID == message.turtleID then
-- Preserve state if not in message
if not message.state then
message.state = t.state or message.mode or "idle"
end
turtles[i] = message
found = true
break
@@ -465,6 +652,9 @@ parallel.waitForAny(
end
if not found then
if not message.state then
message.state = message.mode or "idle"
end
table.insert(turtles, message)
if not selectedTurtle then
selectedTurtle = 1
@@ -472,8 +662,16 @@ parallel.waitForAny(
end
draw()
elseif channel == CHANNEL_RECEIVE and type(message) == "table" and message.status then
-- Response from turtle
elseif Channels.match('remoteturtle.response', channel) and type(message) == "table" then
-- State change confirmation or other response
if message.type == "state_changed" and message.turtleID then
for i, t in ipairs(turtles) do
if t.turtleID == message.turtleID then
turtles[i].state = message.state
break
end
end
end
draw()
end
end

38
server/Dockerfile Normal file
View File

@@ -0,0 +1,38 @@
# Stage 1: Fetch platform server package from git
FROM alpine:3.20 AS platform
RUN apk add --no-cache git
ARG PLATFORM_REPO=https://git.spatulaa.com/MayaTheShy/cc-platform-core.git
ARG PLATFORM_BRANCH=master
RUN git clone --depth 1 --branch "$PLATFORM_BRANCH" "$PLATFORM_REPO" /src \
&& rm -rf /src/server/node_modules /src/.git
# Stage 2: Node.js backend
FROM node:18-alpine
WORKDIR /app
# Copy platform server package from the git-clone stage
COPY --from=platform /src/server /app/platform-server/
# Copy package files
COPY package*.json ./
# Rewrite file: dependency to use the local copy inside the container
RUN sed -i 's|file:../../cc-platform-core/server|file:./platform-server|' package.json \
&& rm -f package-lock.json
# Install dependencies
RUN npm install --omit=dev
# Copy all server code
COPY . .
# Expose ports
EXPOSE 3001 3002
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node -e "require('http').get('http://localhost:3001/api/turtles', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"
# Start server
CMD ["node", "server.js"]

37
server/Dockerfile.dev Normal file
View File

@@ -0,0 +1,37 @@
# Stage 1: Fetch platform server package from git
FROM alpine:3.20 AS platform
RUN apk add --no-cache git
ARG PLATFORM_REPO=https://git.spatulaa.com/MayaTheShy/cc-platform-core.git
ARG PLATFORM_BRANCH=master
RUN git clone --depth 1 --branch "$PLATFORM_BRANCH" "$PLATFORM_REPO" /src \
&& rm -rf /src/server/node_modules /src/.git
# Stage 2: Development with hot reload
FROM node:18-alpine
WORKDIR /app
# Copy platform server package from the git-clone stage
COPY --from=platform /src/server /app/platform-server/
# Install nodemon for hot reload
RUN npm install -g nodemon
# Copy package files
COPY package*.json ./
# Rewrite file: dependency to use the local copy inside the container
RUN sed -i 's|file:../../cc-platform-core/server|file:./platform-server|' package.json \
&& rm -f package-lock.json
# Install all dependencies (including dev)
RUN npm install
# Copy source code
COPY . .
# Expose ports
EXPOSE 3001 3002
# Start with nodemon for hot reload
CMD ["nodemon", "server.js"]

387
server/TaskDispatcher.js Normal file
View File

@@ -0,0 +1,387 @@
/**
* TaskDispatcher — Automatic task queue dispatcher for RemoteTurtle
*
* Periodically polls the task_queue for pending tasks, matches them to
* available (idle + connected) turtles, maps task_type to state machine
* states, and drives the turtle state machine. Handles turtle disconnection
* mid-task (re-queues) and completion callbacks.
*
* Task type → State machine mapping:
* mine_area → mining (requires bounds in task_data)
* explore → exploring (requires target/area in task_data)
* gather → extracting (requires blockName in task_data)
* build → building (requires plan in task_data)
* transport → moving (requires target position)
* clear_area → mining (same as mine_area)
* scan → scanning (requires area/range)
* farm → farming (requires area)
* autocraft → autocrafting (requires recipe)
*/
// Map task_type values from the TaskPanel UI to turtle state machine state names + data mappers
const TASK_TYPE_MAP = {
mine_area: { state: 'mining', mapData: d => ({ bounds: d.bounds || d, ...d }) },
explore: { state: 'exploring', mapData: d => ({ target: d.target || d, ...d }) },
gather: { state: 'extracting', mapData: d => ({ blockName: d.blockName, count: d.count, ...d }) },
build: { state: 'building', mapData: d => ({ plan: d.plan, origin: d.origin, ...d }) },
transport: { state: 'moving', mapData: d => ({ target: d.target || d.destination || d, ...d }) },
clear_area: { state: 'mining', mapData: d => ({ bounds: d.bounds || d, ...d }) },
scan: { state: 'scanning', mapData: d => ({ area: d.area, ...d }) },
farm: { state: 'farming', mapData: d => ({ area: d.area, ...d }) },
autocraft: { state: 'autocrafting', mapData: d => ({ recipe: d.recipe, count: d.count, ...d }) },
};
export class TaskDispatcher {
/**
* @param {Object} opts
* @param {Map<number, import('./Turtle.js').Turtle>} opts.turtles - Live turtle map
* @param {Object} opts.db - Database module (server/database.js)
* @param {Function} opts.broadcastToClients - WebSocket broadcaster
* @param {number} [opts.pollInterval=5000] - ms between dispatch cycles
*/
constructor({ turtles, db, broadcastToClients, pollInterval = 5000 }) {
this._turtles = turtles;
this._db = db;
this._broadcast = broadcastToClients;
this._pollInterval = pollInterval;
this._timer = null;
this._enabled = true;
// Track which tasks are actively being executed by which turtles
// turtleId -> { taskId, taskType }
this._activeTasks = new Map();
// Reverse lookup: taskId -> turtleId
this._taskToTurtle = new Map();
}
/** Start the dispatch loop */
start() {
if (this._timer) return;
console.log(`🚀 TaskDispatcher started (poll every ${this._pollInterval}ms)`);
this._timer = setInterval(() => this._tick(), this._pollInterval);
// Run immediately on start
this._tick();
}
/** Stop the dispatch loop */
stop() {
if (this._timer) {
clearInterval(this._timer);
this._timer = null;
console.log('🛑 TaskDispatcher stopped');
}
}
/** Enable/disable automatic dispatching (tasks still tracked when disabled) */
set enabled(val) {
this._enabled = !!val;
console.log(`TaskDispatcher: auto-dispatch ${this._enabled ? 'enabled' : 'disabled'}`);
}
get enabled() {
return this._enabled;
}
/** Get dispatcher status for the API */
status() {
return {
enabled: this._enabled,
activeTasks: Array.from(this._activeTasks.entries()).map(([turtleId, info]) => ({
turtleId,
...info,
})),
pollInterval: this._pollInterval,
};
}
// ========== Internal ==========
/**
* One dispatch cycle:
* 1. Reconcile active tasks (detect turtle disconnects, state completions)
* 2. If enabled, find pending tasks and assign to idle turtles
*/
_tick() {
try {
this._reconcile();
if (this._enabled) {
this._dispatch();
}
} catch (err) {
console.error('[TaskDispatcher] tick error:', err.message);
}
}
/**
* Reconcile: check if turtles executing tasks have finished or disconnected.
*/
_reconcile() {
for (const [turtleId, info] of this._activeTasks) {
const turtle = this._turtles.get(turtleId);
// Turtle removed from server
if (!turtle) {
this._handleTaskFailure(info.taskId, turtleId, 'Turtle no longer exists');
continue;
}
// Turtle disconnected mid-task
if (!turtle.connected) {
this._handleTaskFailure(info.taskId, turtleId, 'Turtle disconnected');
continue;
}
// Turtle transitioned to idle → task completed
if (turtle.stateName === 'idle') {
this._handleTaskCompletion(info.taskId, turtleId);
continue;
}
// Turtle errored out
if (turtle._error) {
this._handleTaskFailure(info.taskId, turtleId, turtle._error);
continue;
}
}
}
/**
* Dispatch: find pending (unassigned or assigned-but-not-started) tasks,
* match to available turtles, and start them.
*/
_dispatch() {
// Get all pending tasks (sorted by priority DESC, created_at ASC via DB)
let pendingTasks;
try {
pendingTasks = this._db.getAllTasks('pending');
} catch (e) {
return;
}
if (!pendingTasks || pendingTasks.length === 0) return;
// Find idle, connected turtles not already executing a task
const availableTurtles = this._getAvailableTurtles();
if (availableTurtles.length === 0) return;
for (const task of pendingTasks) {
if (availableTurtles.length === 0) break;
const mapping = TASK_TYPE_MAP[task.task_type];
if (!mapping) {
console.warn(`[TaskDispatcher] Unknown task type: ${task.task_type} (task #${task.id})`);
continue;
}
// If task has a specific turtle assignment, respect it
let turtle = null;
if (task.assigned_turtle_id) {
turtle = this._turtles.get(task.assigned_turtle_id);
if (!turtle || !turtle.connected || turtle.stateName !== 'idle') {
// Assigned turtle not available — skip this task for now
continue;
}
// Remove from available pool
const idx = availableTurtles.indexOf(turtle);
if (idx !== -1) availableTurtles.splice(idx, 1);
} else {
// Pick the best available turtle (closest to target if we have coords, else first)
turtle = this._pickBestTurtle(availableTurtles, task.task_data);
const idx = availableTurtles.indexOf(turtle);
if (idx !== -1) availableTurtles.splice(idx, 1);
}
if (!turtle) continue;
// Dispatch!
this._startTask(task, turtle, mapping);
}
}
/**
* Start a task on a turtle.
*/
_startTask(task, turtle, mapping) {
const taskId = task.id;
const turtleId = turtle.id;
console.log(`[TaskDispatcher] Assigning task #${taskId} (${task.task_type}) → Turtle #${turtleId}`);
// Map task_data to state data
const stateData = mapping.mapData(task.task_data || {});
// Update DB: assign + set in_progress
try {
this._db.assignTask(taskId, turtleId);
this._db.updateTaskStatus(taskId, 'in_progress');
} catch (e) {
console.error(`[TaskDispatcher] DB error assigning task #${taskId}:`, e.message);
return;
}
// Track
this._activeTasks.set(turtleId, { taskId, taskType: task.task_type });
this._taskToTurtle.set(taskId, turtleId);
// Broadcast updates
this._broadcast({ type: 'task_assigned', taskId, turtleId });
this._broadcast({ type: 'task_updated', taskId, status: 'in_progress' });
// Set the turtle's state machine
turtle.setState(mapping.state, stateData);
}
/**
* Handle task completion (turtle went back to idle after executing).
*/
_handleTaskCompletion(taskId, turtleId) {
console.log(`[TaskDispatcher] Task #${taskId} completed by Turtle #${turtleId}`);
try {
this._db.completeTask(taskId);
} catch (e) {
console.error(`[TaskDispatcher] DB error completing task #${taskId}:`, e.message);
}
this._activeTasks.delete(turtleId);
this._taskToTurtle.delete(taskId);
this._broadcast({ type: 'task_completed', taskId });
}
/**
* Handle task failure (turtle disconnected, errored, etc.).
* Re-queues the task as pending so another turtle can pick it up.
*/
_handleTaskFailure(taskId, turtleId, reason) {
console.warn(`[TaskDispatcher] Task #${taskId} failed on Turtle #${turtleId}: ${reason}`);
try {
// Re-queue: set back to pending, clear assignment
this._db.updateTaskStatus(taskId, 'pending', reason);
this._db.assignTask(taskId, null);
} catch (e) {
console.error(`[TaskDispatcher] DB error re-queuing task #${taskId}:`, e.message);
}
this._activeTasks.delete(turtleId);
this._taskToTurtle.delete(taskId);
this._broadcast({ type: 'task_updated', taskId, status: 'pending' });
}
/**
* Manually cancel a running task (called via API).
*/
cancelTask(taskId) {
const turtleId = this._taskToTurtle.get(taskId);
if (turtleId !== undefined) {
const turtle = this._turtles.get(turtleId);
if (turtle) {
turtle.setState('idle');
}
this._activeTasks.delete(turtleId);
this._taskToTurtle.delete(taskId);
}
try {
this._db.updateTaskStatus(taskId, 'cancelled');
} catch (e) {
console.error(`[TaskDispatcher] DB error cancelling task #${taskId}:`, e.message);
}
this._broadcast({ type: 'task_updated', taskId, status: 'cancelled' });
}
/**
* Check if a specific task is currently being executed.
*/
isTaskActive(taskId) {
return this._taskToTurtle.has(taskId);
}
// ========== Helpers ==========
/**
* Get connected, idle turtles not currently executing a dispatched task.
*/
_getAvailableTurtles() {
const available = [];
for (const [id, turtle] of this._turtles) {
if (
turtle.connected &&
turtle.stateName === 'idle' &&
!this._activeTasks.has(id)
) {
available.push(turtle);
}
}
return available;
}
/**
* Pick the best turtle for a task.
* If task_data has coordinates, pick the closest turtle with a known position.
* Otherwise, pick the turtle with the most fuel.
*/
_pickBestTurtle(candidates, taskData) {
if (candidates.length === 0) return null;
if (candidates.length === 1) return candidates[0];
// Try to extract a target position from task data
const target = this._extractTarget(taskData);
if (target) {
// Sort by Manhattan distance to target
let bestTurtle = candidates[0];
let bestDist = Infinity;
for (const t of candidates) {
if (t.position) {
const dist = Math.abs(t.position.x - target.x)
+ Math.abs(t.position.y - target.y)
+ Math.abs(t.position.z - target.z);
if (dist < bestDist) {
bestDist = dist;
bestTurtle = t;
}
}
}
return bestTurtle;
}
// No target — pick turtle with highest fuel
return candidates.reduce((best, t) => (t._fuel > best._fuel ? t : best), candidates[0]);
}
/**
* Try to extract a target {x,y,z} from task_data.
*/
_extractTarget(data) {
if (!data) return null;
// Direct target
if (data.target && typeof data.target.x === 'number') return data.target;
if (data.destination && typeof data.destination.x === 'number') return data.destination;
// Bounds — use center
if (data.bounds) {
const b = data.bounds;
if (typeof b.minX === 'number' && typeof b.maxX === 'number') {
return {
x: Math.floor((b.minX + b.maxX) / 2),
y: Math.floor((b.minY + b.maxY) / 2),
z: Math.floor((b.minZ + b.maxZ) / 2),
};
}
}
// Coordinate pair (from TaskPanel)
if (typeof data.startX === 'number' && typeof data.startZ === 'number') {
return { x: data.startX, y: data.startY || 64, z: data.startZ };
}
return null;
}
}

1251
server/Turtle.js Normal file

File diff suppressed because it is too large Load Diff

226
server/WorldBlockCache.js Normal file
View File

@@ -0,0 +1,226 @@
/**
* WorldBlockCache — LRU write-through cache for world blocks
*
* Replaces the unbounded in-memory Map that previously loaded ALL blocks at
* startup. Provides the same Map-like interface (get/set/delete) expected by
* DStarLite pathfinder and Turtle._deleteBlockAtPosition, but caps memory
* usage at `maxSize` entries and falls through to SQLite on cache miss.
*
* Bulk reads (initial_state, /api/world/blocks) bypass the cache and query
* the database directly via helper methods.
*/
export class WorldBlockCache {
/**
* @param {Object} db - The database module (server/database.js)
* @param {number} maxSize - Max entries in the LRU cache (default 50 000)
*/
constructor(db, maxSize = 50_000) {
this._db = db;
this._maxSize = maxSize;
this._cache = new Map(); // key -> { value, prev, next } (LRU doubly-linked)
this._head = null; // most recently used
this._tail = null; // least recently used
// Track total block count from DB (updated lazily)
this._dbCount = null;
}
// ========== Map-compatible interface ==========
/**
* Get a block by "x,y,z" key.
* Returns the cached value or falls through to the database.
*/
get(key) {
// Cache hit
if (this._cache.has(key)) {
const node = this._cache.get(key);
this._promote(node);
return node.value;
}
// Cache miss — query DB
const [x, y, z] = key.split(',').map(Number);
let row;
try {
row = this._db.getBlock(x, y, z);
} catch (e) {
return undefined;
}
if (!row) return undefined;
const value = {
name: row.name,
metadata: row.metadata,
discoveredBy: row.discoveredBy,
timestamp: row.discovered_at,
};
this._put(key, value);
return value;
}
/**
* Store a block (write-through: updates cache + DB is handled by caller via storeBlock).
*/
set(key, value) {
this._put(key, value);
this._dbCount = null; // invalidate count cache
return this;
}
/**
* Delete a block from cache (DB deletion handled by caller).
*/
delete(key) {
if (this._cache.has(key)) {
const node = this._cache.get(key);
this._unlink(node);
this._cache.delete(key);
}
this._dbCount = null;
return true;
}
/**
* Check if a key exists (checks cache first, then DB).
*/
has(key) {
if (this._cache.has(key)) return true;
const [x, y, z] = key.split(',').map(Number);
try {
return this._db.getBlock(x, y, z) !== null;
} catch (e) {
return false;
}
}
/**
* Approximate size — returns DB count (not cache size).
*/
get size() {
if (this._dbCount === null) {
try {
this._dbCount = this._db.getWorldBlockCount();
} catch (e) {
// Fallback: count cached entries
this._dbCount = this._cache.size;
}
}
return this._dbCount;
}
/**
* Iterate all entries — queries DB directly (not bounded by cache).
* Used for initial_state and /api/world/blocks.
* Returns an iterator of [key, value] pairs.
*/
*entries() {
const rows = this._db.getWorldBlocks(100000);
for (const row of rows) {
const key = `${row.x},${row.y},${row.z}`;
yield [key, {
name: row.block_name,
metadata: row.metadata,
discoveredBy: row.discovered_by,
timestamp: row.discovered_at,
}];
}
}
// ========== Bulk helpers for API / WebSocket ==========
/**
* Get blocks in an area (for spatial queries).
*/
getBlocksInArea(minX, minY, minZ, maxX, maxY, maxZ) {
return this._db.getWorldBlocksInArea(minX, minY, minZ, maxX, maxY, maxZ);
}
/**
* Get all blocks formatted for the /api/world/blocks response.
* @param {number} limit - Max rows to return
*/
getAllBlocksForAPI(limit = 100000) {
const rows = this._db.getWorldBlocks(limit);
return rows.map(row => ({
x: row.x, y: row.y, z: row.z,
name: row.block_name,
metadata: row.metadata,
discoveredBy: row.discovered_by,
timestamp: row.discovered_at,
}));
}
/**
* Warm the cache with blocks near a set of positions (e.g., turtle locations).
*/
warmArea(cx, cy, cz, radius = 32) {
const blocks = this._db.getWorldBlocksInArea(
cx - radius, cy - radius, cz - radius,
cx + radius, cy + radius, cz + radius,
);
for (const row of blocks) {
const key = `${row.x},${row.y},${row.z}`;
if (!this._cache.has(key)) {
this._put(key, {
name: row.block_name,
metadata: row.metadata,
discoveredBy: row.discovered_by,
timestamp: row.discovered_at,
});
}
}
}
// ========== Internal LRU ==========
_put(key, value) {
if (this._cache.has(key)) {
const node = this._cache.get(key);
node.value = value;
this._promote(node);
} else {
const node = { key, value, prev: null, next: null };
this._cache.set(key, node);
this._addToHead(node);
// Evict if over capacity
if (this._cache.size > this._maxSize) {
this._evict();
}
}
}
_promote(node) {
if (node === this._head) return;
this._unlink(node);
this._addToHead(node);
}
_addToHead(node) {
node.prev = null;
node.next = this._head;
if (this._head) this._head.prev = node;
this._head = node;
if (!this._tail) this._tail = node;
}
_unlink(node) {
if (node.prev) node.prev.next = node.next;
else this._head = node.next;
if (node.next) node.next.prev = node.prev;
else this._tail = node.prev;
node.prev = null;
node.next = null;
}
_evict() {
if (!this._tail) return;
const evicted = this._tail;
this._unlink(evicted);
this._cache.delete(evicted.key);
}
}

View File

@@ -0,0 +1,222 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { TaskDispatcher } from '../TaskDispatcher.js';
// ========== Mock Factories ==========
function makeTurtle(id, { connected = true, state = 'idle', fuel = 1000, position = null } = {}) {
return {
id,
connected,
stateName: state,
_fuel: fuel,
_error: null,
position,
setState: vi.fn(function (name) { this.stateName = name; }),
};
}
function makeDb() {
const tasks = [];
let nextId = 1;
return {
createTask: vi.fn((type, data, priority, turtleId) => {
const id = nextId++;
tasks.push({ id, task_type: type, task_data: data, priority, assigned_turtle_id: turtleId, status: 'pending' });
return id;
}),
getAllTasks: vi.fn((status) => {
return tasks
.filter(t => !status || t.status === status)
.sort((a, b) => b.priority - a.priority || a.id - b.id);
}),
getNextTask: vi.fn(() => tasks.find(t => t.status === 'pending') || null),
assignTask: vi.fn((taskId, turtleId) => {
const t = tasks.find(x => x.id === taskId);
if (t) {
t.assigned_turtle_id = turtleId;
t.status = turtleId === null ? 'pending' : 'assigned';
}
}),
updateTaskStatus: vi.fn((taskId, status) => {
const t = tasks.find(x => x.id === taskId);
if (t) t.status = status;
}),
completeTask: vi.fn((taskId) => {
const t = tasks.find(x => x.id === taskId);
if (t) t.status = 'completed';
}),
_tasks: tasks,
};
}
// ========== Tests ==========
describe('TaskDispatcher', () => {
let turtles, db, broadcast, dispatcher;
beforeEach(() => {
turtles = new Map();
db = makeDb();
broadcast = vi.fn();
dispatcher = new TaskDispatcher({ turtles, db, broadcastToClients: broadcast, pollInterval: 60000 });
});
it('should not dispatch when no turtles are available', () => {
db.createTask('mine_area', { bounds: { minX: 0, minY: 0, minZ: 0, maxX: 10, maxY: 10, maxZ: 10 } }, 5, null);
dispatcher._tick();
expect(db.assignTask).not.toHaveBeenCalled();
});
it('should assign a pending task to an idle turtle', () => {
const turtle = makeTurtle(1);
turtles.set(1, turtle);
db.createTask('mine_area', { bounds: { minX: 0, minY: 0, minZ: 0, maxX: 10, maxY: 10, maxZ: 10 } }, 5, null);
dispatcher._tick();
expect(db.assignTask).toHaveBeenCalledWith(1, 1);
expect(db.updateTaskStatus).toHaveBeenCalledWith(1, 'in_progress');
expect(turtle.setState).toHaveBeenCalledWith('mining', expect.objectContaining({ bounds: expect.any(Object) }));
});
it('should respect turtle assignment on tasks', () => {
const t1 = makeTurtle(1);
const t2 = makeTurtle(2);
turtles.set(1, t1);
turtles.set(2, t2);
// Task assigned specifically to turtle 2
db.createTask('explore', { target: { x: 100, y: 64, z: 100 } }, 5, 2);
dispatcher._tick();
expect(t2.setState).toHaveBeenCalled();
expect(t1.setState).not.toHaveBeenCalled();
});
it('should skip tasks with unknown task_type', () => {
const turtle = makeTurtle(1);
turtles.set(1, turtle);
db.createTask('unknown_type', {}, 5, null);
dispatcher._tick();
expect(turtle.setState).not.toHaveBeenCalled();
});
it('should not assign to busy turtles', () => {
const turtle = makeTurtle(1, { state: 'mining' });
turtles.set(1, turtle);
db.createTask('explore', {}, 5, null);
dispatcher._tick();
expect(db.assignTask).not.toHaveBeenCalled();
});
it('should not assign to disconnected turtles', () => {
const turtle = makeTurtle(1, { connected: false });
turtles.set(1, turtle);
db.createTask('explore', {}, 5, null);
dispatcher._tick();
expect(db.assignTask).not.toHaveBeenCalled();
});
it('should complete task when turtle returns to idle', () => {
const turtle = makeTurtle(1);
turtles.set(1, turtle);
db.createTask('mine_area', { bounds: { minX: 0, minY: 0, minZ: 0, maxX: 5, maxY: 5, maxZ: 5 } }, 5, null);
// First tick: assigns
dispatcher._tick();
expect(turtle.stateName).toBe('mining');
// Simulate turtle completing
turtle.stateName = 'idle';
// Second tick: reconcile detects idle → complete
dispatcher._tick();
expect(db.completeTask).toHaveBeenCalledWith(1);
expect(broadcast).toHaveBeenCalledWith(expect.objectContaining({ type: 'task_completed', taskId: 1 }));
});
it('should re-queue task when turtle disconnects', () => {
const turtle = makeTurtle(1);
turtles.set(1, turtle);
db.createTask('explore', {}, 5, null);
dispatcher._tick();
expect(turtle.stateName).toBe('exploring');
// Simulate disconnect
turtle.connected = false;
dispatcher._tick();
expect(db.updateTaskStatus).toHaveBeenCalledWith(1, 'pending', 'Turtle disconnected');
expect(db.assignTask).toHaveBeenCalledWith(1, null);
});
it('should pick closest turtle when multiple are available', () => {
const t1 = makeTurtle(1, { position: { x: 0, y: 64, z: 0 } });
const t2 = makeTurtle(2, { position: { x: 100, y: 64, z: 100 } });
turtles.set(1, t1);
turtles.set(2, t2);
db.createTask('transport', { target: { x: 95, y: 64, z: 95 } }, 5, null);
dispatcher._tick();
// t2 is closer to (95,64,95)
expect(t2.setState).toHaveBeenCalled();
expect(t1.setState).not.toHaveBeenCalled();
});
it('should not dispatch when disabled', () => {
const turtle = makeTurtle(1);
turtles.set(1, turtle);
db.createTask('mine_area', {}, 5, null);
dispatcher.enabled = false;
dispatcher._tick();
expect(turtle.setState).not.toHaveBeenCalled();
});
it('should cancel a running task', () => {
const turtle = makeTurtle(1);
turtles.set(1, turtle);
db.createTask('mine_area', { bounds: {} }, 5, null);
dispatcher._tick();
dispatcher.cancelTask(1);
expect(turtle.stateName).toBe('idle');
expect(db.updateTaskStatus).toHaveBeenCalledWith(1, 'cancelled');
expect(broadcast).toHaveBeenCalledWith(expect.objectContaining({ type: 'task_updated', taskId: 1, status: 'cancelled' }));
});
it('should report status correctly', () => {
const turtle = makeTurtle(1);
turtles.set(1, turtle);
db.createTask('mine_area', { bounds: {} }, 5, null);
dispatcher._tick();
const status = dispatcher.status();
expect(status.enabled).toBe(true);
expect(status.activeTasks).toHaveLength(1);
expect(status.activeTasks[0].turtleId).toBe(1);
expect(status.activeTasks[0].taskId).toBe(1);
});
});

View File

@@ -0,0 +1,127 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { WorldBlockCache } from '../WorldBlockCache.js';
function makeDb() {
const blocks = new Map();
return {
getBlock: vi.fn((x, y, z) => {
const key = `${x},${y},${z}`;
return blocks.get(key) || null;
}),
getWorldBlocks: vi.fn((limit) => {
const all = [];
for (const [key, val] of blocks) {
const [x, y, z] = key.split(',').map(Number);
all.push({ x, y, z, block_name: val.name, metadata: val.metadata || 0, discovered_by: val.discoveredBy, discovered_at: val.timestamp });
if (all.length >= limit) break;
}
return all;
}),
getWorldBlockCount: vi.fn(() => blocks.size),
getWorldBlocksInArea: vi.fn(() => []),
// Helper for test setup
_blocks: blocks,
_addBlock(x, y, z, name) {
blocks.set(`${x},${y},${z}`, { name, metadata: 0, discoveredBy: 1, timestamp: Date.now() });
}
};
}
describe('WorldBlockCache', () => {
let db, cache;
beforeEach(() => {
db = makeDb();
cache = new WorldBlockCache(db, 5); // Small capacity for testing eviction
});
it('should return undefined for missing blocks', () => {
expect(cache.get('0,0,0')).toBeUndefined();
expect(db.getBlock).toHaveBeenCalledWith(0, 0, 0);
});
it('should cache blocks from DB on first access', () => {
db._addBlock(1, 2, 3, 'minecraft:stone');
const block = cache.get('1,2,3');
expect(block).toBeDefined();
expect(block.name).toBe('minecraft:stone');
// Second access should not hit DB
cache.get('1,2,3');
expect(db.getBlock).toHaveBeenCalledTimes(1);
});
it('should set and retrieve blocks', () => {
cache.set('5,5,5', { name: 'minecraft:dirt', metadata: 0 });
const block = cache.get('5,5,5');
expect(block.name).toBe('minecraft:dirt');
// Should not hit DB since we just set it
expect(db.getBlock).not.toHaveBeenCalled();
});
it('should delete blocks from cache', () => {
cache.set('1,1,1', { name: 'minecraft:stone' });
expect(cache.delete('1,1,1')).toBe(true);
// Now should fall through to DB
const result = cache.get('1,1,1');
expect(db.getBlock).toHaveBeenCalledWith(1, 1, 1);
});
it('should evict LRU entries when over capacity', () => {
// Fill cache to capacity (5)
for (let i = 0; i < 5; i++) {
cache.set(`${i},0,0`, { name: `block_${i}` });
}
// Access block_0 to make it recently used
cache.get('0,0,0');
// Add one more → should evict the LRU entry (block_1, since block_0 was just accessed)
cache.set('5,0,0', { name: 'block_5' });
// block_1 should have been evicted (we check internal cache size)
expect(cache._cache.size).toBe(5);
expect(cache._cache.has('1,0,0')).toBe(false);
expect(cache._cache.has('0,0,0')).toBe(true); // recently used
});
it('should report size from DB count', () => {
db._addBlock(1, 1, 1, 'stone');
db._addBlock(2, 2, 2, 'dirt');
expect(cache.size).toBe(2);
expect(db.getWorldBlockCount).toHaveBeenCalledTimes(1);
// Cached — no second call
const _ = cache.size;
expect(db.getWorldBlockCount).toHaveBeenCalledTimes(1);
});
it('should invalidate size cache on set/delete', () => {
const _ = cache.size;
expect(db.getWorldBlockCount).toHaveBeenCalledTimes(1);
cache.set('1,1,1', { name: 'stone' });
const __ = cache.size;
expect(db.getWorldBlockCount).toHaveBeenCalledTimes(2);
});
it('should iterate all entries from DB', () => {
db._addBlock(1, 1, 1, 'stone');
db._addBlock(2, 2, 2, 'dirt');
const entries = [...cache.entries()];
expect(entries).toHaveLength(2);
expect(db.getWorldBlocks).toHaveBeenCalled();
});
it('should return blocks formatted for API', () => {
db._addBlock(3, 3, 3, 'minecraft:diamond_ore');
const blocks = cache.getAllBlocksForAPI(100);
expect(blocks).toHaveLength(1);
expect(blocks[0]).toMatchObject({ x: 3, y: 3, z: 3, name: 'minecraft:diamond_ore' });
});
});

779
server/database.js Normal file
View File

@@ -0,0 +1,779 @@
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'));
// Enable WAL journal mode for better concurrent read/write performance
db.pragma('journal_mode = WAL');
// 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,
name TEXT,
color TEXT DEFAULT '#4a8c2a',
status TEXT DEFAULT 'active',
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
)
`);
// Migrate existing mining_areas table to add name/color columns if missing
try {
const tableInfo = db.prepare("PRAGMA table_info(mining_areas)").all();
const columns = tableInfo.map(c => c.name);
if (!columns.includes('name')) {
db.exec('ALTER TABLE mining_areas ADD COLUMN name TEXT');
}
if (!columns.includes('color')) {
db.exec("ALTER TABLE mining_areas ADD COLUMN color TEXT DEFAULT '#4a8c2a'");
}
} catch (e) {
// Ignore migration errors
}
// Mining statistics table
db.exec(`
CREATE TABLE IF NOT EXISTS mining_stats (
id INTEGER PRIMARY KEY AUTOINCREMENT,
turtle_id INTEGER NOT NULL,
block_type TEXT NOT NULL,
count INTEGER DEFAULT 1,
session_start INTEGER NOT NULL,
last_mined INTEGER NOT NULL,
UNIQUE(turtle_id, block_type, session_start)
)
`);
// Turtle groups/teams table
db.exec(`
CREATE TABLE IF NOT EXISTS turtle_groups (
id INTEGER PRIMARY KEY AUTOINCREMENT,
group_name TEXT NOT NULL UNIQUE,
color TEXT DEFAULT '#3b82f6',
created_at INTEGER NOT NULL
)
`);
// Turtle group membership table
db.exec(`
CREATE TABLE IF NOT EXISTS turtle_group_members (
turtle_id INTEGER NOT NULL,
group_id INTEGER NOT NULL,
joined_at INTEGER NOT NULL,
PRIMARY KEY (turtle_id, group_id),
FOREIGN KEY (group_id) REFERENCES turtle_groups(id) ON DELETE CASCADE
)
`);
// Session tracking table (for time-based statistics)
db.exec(`
CREATE TABLE IF NOT EXISTS turtle_sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
turtle_id INTEGER NOT NULL,
started_at INTEGER NOT NULL,
ended_at INTEGER,
blocks_mined INTEGER DEFAULT 0,
distance_traveled INTEGER DEFAULT 0
)
`);
// Player positions table (for tracking pocket computer users)
db.exec(`
CREATE TABLE IF NOT EXISTS player_positions (
player_id INTEGER PRIMARY KEY,
x INTEGER NOT NULL,
y INTEGER NOT NULL,
z INTEGER NOT NULL,
label TEXT,
updated_at INTEGER NOT NULL
)
`);
// Migrate player_positions table to add label column if missing
try {
const tableInfo = db.prepare("PRAGMA table_info(player_positions)").all();
const columns = tableInfo.map(c => c.name);
if (!columns.includes('label')) {
db.exec('ALTER TABLE player_positions ADD COLUMN label TEXT');
}
} catch (e) {
// Ignore migration errors
}
// Chunk analysis table (ore density per chunk)
db.exec(`
CREATE TABLE IF NOT EXISTS chunks (
x INTEGER NOT NULL,
z INTEGER NOT NULL,
analysis TEXT DEFAULT '{}',
scanned_at INTEGER NOT NULL,
PRIMARY KEY (x, z)
)
`);
// Add block_state and block_tags columns to world_blocks if not present
try {
db.exec(`ALTER TABLE world_blocks ADD COLUMN block_state TEXT DEFAULT '{}'`);
} catch (e) { /* column already exists */ }
try {
db.exec(`ALTER TABLE world_blocks ADD COLUMN block_tags TEXT DEFAULT '{}'`);
} catch (e) { /* column already exists */ }
// 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);
`);
db.exec(`
CREATE INDEX IF NOT EXISTS idx_world_blocks_name
ON world_blocks(block_name);
`);
db.exec(`
CREATE INDEX IF NOT EXISTS idx_chunks_coords
ON chunks(x, z);
`);
// Turtle state persistence table (for reconnect recovery)
db.exec(`
CREATE TABLE IF NOT EXISTS turtle_state (
turtle_id INTEGER PRIMARY KEY,
state_name TEXT NOT NULL,
state_data TEXT DEFAULT '{}',
updated_at INTEGER NOT NULL
)
`);
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, blockState = null, blockTags = null) {
const stmt = db.prepare(`
INSERT OR REPLACE INTO world_blocks (x, y, z, block_name, metadata, discovered_by, discovered_at, block_state, block_tags)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
stmt.run(x, y, z, blockName, metadata || 0, discoveredBy, Date.now(),
blockState ? JSON.stringify(blockState) : '{}',
blockTags ? JSON.stringify(blockTags) : '{}');
}
export function getWorldBlocks(limit = 10000) {
const stmt = db.prepare('SELECT * FROM world_blocks LIMIT ?');
return stmt.all(limit);
}
export function getWorldBlockCount() {
const row = db.prepare('SELECT COUNT(*) as cnt FROM world_blocks').get();
return row ? row.cnt : 0;
}
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();
const result = stmt.run(turtleId, pathName, JSON.stringify(pathData), now, now);
return result.lastInsertRowid;
}
export function getPaths(turtleId = null) {
if (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)
}));
} else {
const stmt = db.prepare('SELECT * FROM turtle_paths ORDER BY created_at DESC');
return stmt.all().map(row => ({
...row,
path_data: JSON.parse(row.path_data)
}));
}
}
export function getPath(pathId) {
const stmt = db.prepare('SELECT * FROM turtle_paths WHERE id = ?');
const row = stmt.get(pathId);
if (row) {
return { ...row, path_data: JSON.parse(row.path_data) };
}
return null;
}
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, assignedTurtleId = null) {
const stmt = db.prepare(`
INSERT INTO task_queue (task_type, task_data, assigned_turtle_id, priority, status, created_at, updated_at)
VALUES (?, ?, ?, ?, 'pending', ?, ?)
`);
const now = Date.now();
const result = stmt.run(taskType, JSON.stringify(taskData), assignedTurtleId, 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) {
if (turtleId === null || turtleId === undefined) {
// Un-assign: clear turtle and revert to pending
const stmt = db.prepare(`
UPDATE task_queue
SET assigned_turtle_id = NULL, status = 'pending', updated_at = ?
WHERE id = ?
`);
stmt.run(Date.now(), taskId);
} else {
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 updateTaskStatus(taskId, status, result = null) {
const stmt = db.prepare(`
UPDATE task_queue
SET status = ?, task_data = CASE WHEN ? IS NOT NULL THEN json_set(task_data, '$.result', ?) ELSE task_data END, updated_at = ?
WHERE id = ?
`);
stmt.run(status, result, result, 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 deleteTask(taskId) {
const stmt = db.prepare('DELETE FROM task_queue WHERE id = ?');
return stmt.run(taskId);
}
export function getAllTasks(status = null) {
if (status) {
const stmt = db.prepare('SELECT * FROM task_queue WHERE status = ? ORDER BY priority DESC, created_at DESC');
return stmt.all(status).map(row => ({
...row,
task_data: JSON.parse(row.task_data)
}));
}
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, areaName = null, status = 'planned', color = '#4a8c2a') {
const stmt = db.prepare(`
INSERT INTO mining_areas (turtle_id, min_x, min_y, min_z, max_x, max_y, max_z, name, color, status, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
const now = Date.now();
const result = stmt.run(
turtleId,
bounds.minX, bounds.minY, bounds.minZ,
bounds.maxX, bounds.maxY, bounds.maxZ,
areaName, color,
status, now, now
);
return result.lastInsertRowid;
}
export function getMiningAreas(statusFilter = null) {
if (statusFilter) {
const stmt = db.prepare('SELECT * FROM mining_areas WHERE status = ? ORDER BY created_at DESC');
return stmt.all(statusFilter);
}
const stmt = db.prepare('SELECT * FROM mining_areas ORDER BY created_at DESC');
return stmt.all();
}
export function updateMiningAreaStatus(areaId, status) {
const stmt = db.prepare('UPDATE mining_areas SET status = ?, updated_at = ? WHERE id = ?');
stmt.run(status, Date.now(), areaId);
}
export function updateMiningArea(areaId, updates) {
const allowedFields = ['name', 'color', 'status', 'min_x', 'min_y', 'min_z', 'max_x', 'max_y', 'max_z', 'turtle_id'];
const setClauses = [];
const values = [];
for (const [key, value] of Object.entries(updates)) {
if (allowedFields.includes(key)) {
setClauses.push(`${key} = ?`);
values.push(value);
}
}
if (setClauses.length === 0) return;
setClauses.push('updated_at = ?');
values.push(Date.now());
values.push(areaId);
const stmt = db.prepare(`UPDATE mining_areas SET ${setClauses.join(', ')} WHERE id = ?`);
stmt.run(...values);
}
export function deleteMiningArea(areaId) {
const stmt = db.prepare('DELETE FROM mining_areas WHERE id = ?');
return stmt.run(areaId);
}
export function closeMiningArea(areaId) {
const stmt = db.prepare('UPDATE mining_areas SET status = \'closed\', updated_at = ? WHERE id = ?');
stmt.run(Date.now(), areaId);
}
// Mining Statistics
export function recordBlockMined(turtleId, blockType) {
const stmt = db.prepare(`
INSERT INTO mining_stats (turtle_id, block_type, count, session_start, last_mined)
VALUES (?, ?, 1, ?, ?)
ON CONFLICT(turtle_id, block_type, session_start)
DO UPDATE SET count = count + 1, last_mined = ?
`);
const now = Date.now();
const sessionStart = now - (now % (24 * 60 * 60 * 1000)); // Start of day
stmt.run(turtleId, blockType, sessionStart, now, now);
}
export function getMiningStats(turtleId = null, days = 7) {
const cutoff = Date.now() - (days * 24 * 60 * 60 * 1000);
if (turtleId) {
const stmt = db.prepare(`
SELECT block_type, SUM(count) as total_count
FROM mining_stats
WHERE turtle_id = ? AND session_start >= ?
GROUP BY block_type
ORDER BY total_count DESC
`);
return stmt.all(turtleId, cutoff);
} else {
const stmt = db.prepare(`
SELECT turtle_id, block_type, SUM(count) as total_count
FROM mining_stats
WHERE session_start >= ?
GROUP BY turtle_id, block_type
ORDER BY turtle_id, total_count DESC
`);
return stmt.all(cutoff);
}
}
export function getTopMiners(limit = 10) {
const stmt = db.prepare(`
SELECT turtle_id, SUM(count) as total_blocks, COUNT(DISTINCT block_type) as unique_types
FROM mining_stats
GROUP BY turtle_id
ORDER BY total_blocks DESC
LIMIT ?
`);
return stmt.all(limit);
}
// Turtle Groups/Teams
export function createGroup(groupName, color = '#3b82f6') {
const stmt = db.prepare(`
INSERT INTO turtle_groups (group_name, color, created_at)
VALUES (?, ?, ?)
`);
const result = stmt.run(groupName, color, Date.now());
return result.lastInsertRowid;
}
export function getAllGroups() {
const stmt = db.prepare('SELECT * FROM turtle_groups ORDER BY created_at DESC');
return stmt.all();
}
export function deleteGroup(groupId) {
const stmt = db.prepare('DELETE FROM turtle_groups WHERE id = ?');
stmt.run(groupId);
}
export function addTurtleToGroup(turtleId, groupId) {
const stmt = db.prepare(`
INSERT OR IGNORE INTO turtle_group_members (turtle_id, group_id, joined_at)
VALUES (?, ?, ?)
`);
stmt.run(turtleId, groupId, Date.now());
}
export function removeTurtleFromGroup(turtleId, groupId) {
const stmt = db.prepare(`
DELETE FROM turtle_group_members
WHERE turtle_id = ? AND group_id = ?
`);
stmt.run(turtleId, groupId);
}
export function getGroupMembers(groupId) {
const stmt = db.prepare(`
SELECT turtle_id, joined_at
FROM turtle_group_members
WHERE group_id = ?
`);
return stmt.all(groupId);
}
export function getTurtleGroups(turtleId) {
const stmt = db.prepare(`
SELECT g.*, m.joined_at
FROM turtle_groups g
JOIN turtle_group_members m ON g.id = m.group_id
WHERE m.turtle_id = ?
`);
return stmt.all(turtleId);
}
// Session Tracking
export function startSession(turtleId) {
const stmt = db.prepare(`
INSERT INTO turtle_sessions (turtle_id, started_at)
VALUES (?, ?)
`);
const result = stmt.run(turtleId, Date.now());
return result.lastInsertRowid;
}
export function endSession(sessionId, blocksMined, distanceTraveled) {
const stmt = db.prepare(`
UPDATE turtle_sessions
SET ended_at = ?, blocks_mined = ?, distance_traveled = ?
WHERE id = ?
`);
stmt.run(Date.now(), blocksMined, distanceTraveled, sessionId);
}
export function getSessionStats(turtleId, limit = 10) {
const stmt = db.prepare(`
SELECT * FROM turtle_sessions
WHERE turtle_id = ?
ORDER BY started_at DESC
LIMIT ?
`);
return stmt.all(turtleId, limit);
}
// Player Positions
export function savePlayerPosition(playerId, position, label = null) {
const stmt = db.prepare(`
INSERT OR REPLACE INTO player_positions (player_id, x, y, z, label, updated_at)
VALUES (?, ?, ?, ?, ?, ?)
`);
stmt.run(playerId, position.x, position.y, position.z, label, Date.now());
}
export function getPlayerPosition(playerId) {
const stmt = db.prepare('SELECT player_id, x, y, z, label, updated_at FROM player_positions WHERE player_id = ?');
const row = stmt.get(playerId);
if (!row) return null;
return {
playerID: row.player_id,
position: { x: row.x, y: row.y, z: row.z },
label: row.label,
lastUpdate: row.updated_at
};
}
export function getAllPlayerPositions() {
const stmt = db.prepare('SELECT player_id, x, y, z, label, updated_at FROM player_positions');
return stmt.all().map(row => ({
playerID: row.player_id,
position: { x: row.x, y: row.y, z: row.z },
label: row.label,
lastUpdate: row.updated_at
}));
}
// Cleanup function
export function closeDatabase() {
db.close();
}
// ========== CHUNK ANALYSIS ==========
export function saveChunkAnalysis(x, z, analysis) {
const stmt = db.prepare(`
INSERT OR REPLACE INTO chunks (x, z, analysis, scanned_at)
VALUES (?, ?, ?, ?)
`);
stmt.run(x, z, JSON.stringify(analysis), Date.now());
}
export function getChunkAnalysis(x, z) {
const stmt = db.prepare('SELECT * FROM chunks WHERE x = ? AND z = ?');
const row = stmt.get(x, z);
if (row) {
return { ...row, analysis: JSON.parse(row.analysis) };
}
return null;
}
export function getAllChunkAnalyses() {
const stmt = db.prepare('SELECT * FROM chunks');
return stmt.all().map(row => ({
...row,
analysis: JSON.parse(row.analysis)
}));
}
// ========== BLOCK SEARCH ==========
export function getBlocksWithNameLike(fromX, fromY, fromZ, toX, toY, toZ, namePattern) {
const stmt = db.prepare(`
SELECT x, y, z, block_name as name, metadata, block_state, block_tags
FROM world_blocks
WHERE x BETWEEN ? AND ?
AND y BETWEEN ? AND ?
AND z BETWEEN ? AND ?
AND block_name LIKE ?
`);
return stmt.all(
Math.min(fromX, toX), Math.max(fromX, toX),
Math.min(fromY, toY), Math.max(fromY, toY),
Math.min(fromZ, toZ), Math.max(fromZ, toZ),
namePattern
).map(row => ({
...row,
state: row.block_state ? JSON.parse(row.block_state) : {},
tags: row.block_tags ? JSON.parse(row.block_tags) : {},
}));
}
export function getBlock(x, y, z) {
const stmt = db.prepare('SELECT block_name as name, metadata, block_state, block_tags FROM world_blocks WHERE x = ? AND y = ? AND z = ?');
const row = stmt.get(x, y, z);
if (row) {
return {
...row,
state: row.block_state ? JSON.parse(row.block_state) : {},
tags: row.block_tags ? JSON.parse(row.block_tags) : {},
};
}
return null;
}
export function deleteBlocksInArea(fromX, fromY, fromZ, toX, toY, toZ) {
const stmt = db.prepare(`
DELETE FROM world_blocks
WHERE x BETWEEN ? AND ?
AND y BETWEEN ? AND ?
AND z BETWEEN ? AND ?
`);
return stmt.run(
Math.min(fromX, toX), Math.max(fromX, toX),
Math.min(fromY, toY), Math.max(fromY, toY),
Math.min(fromZ, toZ), Math.max(fromZ, toZ)
);
}
// ========== BLOCK DELETION ==========
export function deleteBlock(x, y, z) {
const stmt = db.prepare('DELETE FROM world_blocks WHERE x = ? AND y = ? AND z = ?');
return stmt.run(x, y, z);
}
// ========== TURTLE STATE PERSISTENCE ==========
export function saveTurtleState(turtleId, stateName, stateData = {}) {
const stmt = db.prepare(`
INSERT OR REPLACE INTO turtle_state (turtle_id, state_name, state_data, updated_at)
VALUES (?, ?, ?, ?)
`);
stmt.run(turtleId, stateName, JSON.stringify(stateData), Date.now());
}
export function getTurtleState(turtleId) {
const stmt = db.prepare('SELECT * FROM turtle_state WHERE turtle_id = ?');
const row = stmt.get(turtleId);
if (row) {
return {
stateName: row.state_name,
stateData: JSON.parse(row.state_data),
updatedAt: row.updated_at,
};
}
return null;
}
export function deleteTurtleState(turtleId) {
const stmt = db.prepare('DELETE FROM turtle_state WHERE turtle_id = ?');
return stmt.run(turtleId);
}
// Export database instance for custom queries if needed
export { db };

View File

@@ -0,0 +1,109 @@
/**
* Levenshtein distance utility
* Used for fuzzy matching block/item names for inventory operations.
* Inspired by runi95/turtle-control-panel utility.
*/
/**
* Compute the Levenshtein edit distance between two strings.
* @param {string} a - First string
* @param {string} b - Second string
* @returns {number} The edit distance
*/
export function levenshtein(a, b) {
if (a === b) return 0;
if (a.length === 0) return b.length;
if (b.length === 0) return a.length;
// Use a single-row approach for memory efficiency
const aLen = a.length;
const bLen = b.length;
const row = new Array(bLen + 1);
// Initialize the first row (distance from empty string to b[0..j])
for (let j = 0; j <= bLen; j++) {
row[j] = j;
}
for (let i = 1; i <= aLen; i++) {
let prev = i; // row[0] for this iteration = i
for (let j = 1; j <= bLen; j++) {
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
const val = Math.min(
row[j] + 1, // deletion
prev + 1, // insertion
row[j - 1] + cost // substitution
);
row[j - 1] = prev;
prev = val;
}
row[bLen] = prev;
}
return row[bLen];
}
/**
* Find the best match for a query string among candidates using Levenshtein distance.
* @param {string} query - The search term
* @param {string[]} candidates - Array of candidate strings
* @param {number} maxDistance - Maximum acceptable distance (default: Infinity)
* @returns {{ match: string|null, distance: number }} Best match and its distance
*/
export function findBestMatch(query, candidates, maxDistance = Infinity) {
let bestMatch = null;
let bestDistance = Infinity;
const lowerQuery = query.toLowerCase();
for (const candidate of candidates) {
const lowerCandidate = candidate.toLowerCase();
// Exact substring match is distance 0
if (lowerCandidate.includes(lowerQuery) || lowerQuery.includes(lowerCandidate)) {
const dist = Math.abs(lowerCandidate.length - lowerQuery.length);
if (dist < bestDistance) {
bestDistance = dist;
bestMatch = candidate;
}
continue;
}
const dist = levenshtein(lowerQuery, lowerCandidate);
if (dist < bestDistance && dist <= maxDistance) {
bestDistance = dist;
bestMatch = candidate;
}
}
return { match: bestMatch, distance: bestDistance };
}
/**
* Find all matches within a given distance.
* @param {string} query - The search term
* @param {string[]} candidates - Array of candidate strings
* @param {number} maxDistance - Maximum acceptable distance
* @returns {Array<{match: string, distance: number}>} Matches sorted by distance
*/
export function findAllMatches(query, candidates, maxDistance = 3) {
const lowerQuery = query.toLowerCase();
const matches = [];
for (const candidate of candidates) {
const lowerCandidate = candidate.toLowerCase();
// Exact substring match
if (lowerCandidate.includes(lowerQuery) || lowerQuery.includes(lowerCandidate)) {
matches.push({ match: candidate, distance: 0 });
continue;
}
const dist = levenshtein(lowerQuery, lowerCandidate);
if (dist <= maxDistance) {
matches.push({ match: candidate, distance: dist });
}
}
return matches.sort((a, b) => a.distance - b.distance);
}

View File

@@ -6,7 +6,9 @@
"type": "module",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
"dev": "nodemon server.js",
"test": "vitest run",
"test:watch": "vitest"
},
"keywords": [
"minecraft",
@@ -17,11 +19,14 @@
"author": "",
"license": "MIT",
"dependencies": {
"@cc-platform/server": "file:../../cc-platform-core/server",
"better-sqlite3": "^9.2.2",
"cors": "^2.8.5",
"express": "^4.18.2",
"ws": "^8.14.2",
"cors": "^2.8.5"
"ws": "^8.14.2"
},
"devDependencies": {
"nodemon": "^3.0.1"
"nodemon": "^3.0.1",
"vitest": "^3.2.4"
}
}

View File

@@ -0,0 +1,361 @@
/**
* D* Lite Pathfinding Algorithm
*
* An incremental heuristic search algorithm that efficiently replans
* when the environment changes (new blocks discovered, blocks mined, etc.)
*
* Based on Koenig & Likhachev (2002) and adapted from
* runi95/turtle-control-panel implementation.
*/
import { Point } from './Point.js';
import { Node } from './Node.js';
import { PriorityQueue } from './PriorityQueue.js';
// Unbreakable blocks that cannot be mined
const UNBREAKABLE_BLOCKS = new Set([
'minecraft:bedrock',
'minecraft:barrier',
'minecraft:command_block',
'minecraft:chain_command_block',
'minecraft:repeating_command_block',
'minecraft:structure_block',
'minecraft:jigsaw',
'minecraft:end_portal_frame',
'minecraft:end_portal',
'minecraft:nether_portal',
'minecraft:spawner',
'minecraft:reinforced_deepslate',
]);
// Blocks that are liquid/hazardous - avoid unless necessary
const HAZARDOUS_BLOCKS = new Set([
'minecraft:lava',
'minecraft:water',
'minecraft:flowing_lava',
'minecraft:flowing_water',
]);
export class DStarLite {
/**
* @param {Point} start - Starting position
* @param {Point} goal - Goal position
* @param {Map} worldBlocks - Map of "x,y,z" -> blockData from server
* @param {Object} options - Configuration options
*/
constructor(start, goal, worldBlocks, options = {}) {
this.start = start;
this.goal = goal;
this.worldBlocks = worldBlocks;
this.km = 0;
this.nodes = new Map(); // key -> Node
this.queue = new PriorityQueue();
// Options
this.maxSteps = options.maxSteps || 50000;
this.canMine = options.canMine !== false; // Default: true - can mine through blocks
this.boundaryMin = options.boundaryMin || null; // Point - min boundary
this.boundaryMax = options.boundaryMax || null; // Point - max boundary
this.avoidHazards = options.avoidHazards !== false; // Default: true
// Initialize
this._initialize();
}
/**
* Get or create a node for a point
*/
getNode(point) {
const key = point.toKey();
if (!this.nodes.has(key)) {
const node = new Node(point);
// Check world blocks for this position
const blockData = this.worldBlocks.get(key);
if (blockData) {
node.blockData = blockData;
if (UNBREAKABLE_BLOCKS.has(blockData.name)) {
node.blocked = true;
} else if (this.avoidHazards && HAZARDOUS_BLOCKS.has(blockData.name)) {
node.blocked = true;
} else if (blockData.name && blockData.name !== 'minecraft:air') {
// There's a block here - it's mineable
node.mineable = true;
}
}
// Check boundary constraints
if (this.boundaryMin && this.boundaryMax) {
if (point.x < this.boundaryMin.x || point.x > this.boundaryMax.x ||
point.y < this.boundaryMin.y || point.y > this.boundaryMax.y ||
point.z < this.boundaryMin.z || point.z > this.boundaryMax.z) {
node.blocked = true;
}
}
// Don't go below bedrock
if (point.y < -64) node.blocked = true;
if (point.y > 320) node.blocked = true;
this.nodes.set(key, node);
}
return this.nodes.get(key);
}
/**
* Initialize the D* Lite algorithm
*/
_initialize() {
const goalNode = this.getNode(this.goal);
goalNode.rhs = 0;
const key = goalNode.calculateKey(this.start, this.km);
this.queue.insertOrUpdate(goalNode, key);
}
/**
* Compute the shortest path
* Returns true if a path exists, false otherwise
*/
computeShortestPath() {
const startNode = this.getNode(this.start);
let steps = 0;
while (
!this.queue.isEmpty() &&
(PriorityQueue.compareKeys(this.queue.topKey(), startNode.calculateKey(this.start, this.km)) < 0 ||
startNode.rhs !== startNode.g)
) {
steps++;
if (steps > this.maxSteps) {
console.warn(`D* Lite: Max steps (${this.maxSteps}) exceeded`);
return false;
}
const oldKey = this.queue.topKey();
const u = this.queue.pop();
if (!u) break;
const newKey = u.calculateKey(this.start, this.km);
if (PriorityQueue.compareKeys(oldKey, newKey) < 0) {
// Key has changed, reinsert with new key
this.queue.insertOrUpdate(u, newKey);
} else if (u.g > u.rhs) {
// Overconsistent - decrease g
u.g = u.rhs;
// Update predecessors
const neighbors = u.point.getNeighbors();
for (const neighborPoint of neighbors) {
const neighborNode = this.getNode(neighborPoint);
this._updateNode(neighborNode);
}
} else {
// Underconsistent - increase g
u.g = Infinity;
// Update u and its predecessors
this._updateNode(u);
const neighbors = u.point.getNeighbors();
for (const neighborPoint of neighbors) {
const neighborNode = this.getNode(neighborPoint);
this._updateNode(neighborNode);
}
}
}
return startNode.g !== Infinity;
}
/**
* Update a node's rhs value and queue status
*/
_updateNode(node) {
if (!node.point.equals(this.goal)) {
// rhs = min over all successors of (cost(node, succ) + succ.g)
let minRhs = Infinity;
const neighbors = node.point.getNeighbors();
for (const neighborPoint of neighbors) {
const neighborNode = this.getNode(neighborPoint);
const cost = this._cost(node, neighborNode);
const val = cost + neighborNode.g;
if (val < minRhs) {
minRhs = val;
}
}
node.rhs = minRhs;
}
// Remove from queue if present
if (this.queue.contains(node)) {
this.queue.remove(node);
}
// Re-insert if inconsistent
if (node.g !== node.rhs) {
const key = node.calculateKey(this.start, this.km);
this.queue.insertOrUpdate(node, key);
}
}
/**
* Get the cost to traverse from one node to an adjacent node
*/
_cost(from, to) {
return to.getTraversalCost();
}
/**
* Get the computed path from start to goal
* Returns array of Points, or null if no path exists
*/
getPath() {
if (!this.computeShortestPath()) {
return null;
}
const path = [this.start];
let current = this.start;
let maxPathLength = 10000;
while (!current.equals(this.goal) && maxPathLength > 0) {
maxPathLength--;
const currentNode = this.getNode(current);
if (currentNode.g === Infinity) {
return null; // No path
}
// Find the best neighbor to move to
let bestNeighbor = null;
let bestCost = Infinity;
const neighbors = current.getNeighbors();
for (const neighborPoint of neighbors) {
const neighborNode = this.getNode(neighborPoint);
const cost = this._cost(currentNode, neighborNode) + neighborNode.g;
if (cost < bestCost) {
bestCost = cost;
bestNeighbor = neighborPoint;
}
}
if (!bestNeighbor || bestCost === Infinity) {
return null; // No path
}
path.push(bestNeighbor);
current = bestNeighbor;
}
return path;
}
/**
* Update the algorithm when the turtle moves to a new position
* This is the key D* Lite optimization - it adjusts km to avoid full recomputation
*/
updateStart(newStart) {
this.km += this.start.euclideanDistanceTo(newStart);
this.start = newStart;
}
/**
* Notify the algorithm that a block has changed at a position
* This triggers efficient replanning
* @param {Point} point - The position that changed
* @param {Object|null} blockData - New block data, or null if block was removed
*/
updateBlock(point, blockData) {
const node = this.getNode(point);
const oldBlocked = node.blocked;
const oldMineable = node.mineable;
// Update node state
node.blockData = blockData;
if (!blockData || blockData.name === 'minecraft:air') {
node.blocked = false;
node.mineable = false;
} else if (UNBREAKABLE_BLOCKS.has(blockData.name)) {
node.blocked = true;
node.mineable = false;
} else if (this.avoidHazards && HAZARDOUS_BLOCKS.has(blockData.name)) {
node.blocked = true;
node.mineable = false;
} else {
node.blocked = false;
node.mineable = true;
}
// Only replan if the traversability changed
if (node.blocked !== oldBlocked || node.mineable !== oldMineable) {
// Update this node and all its neighbors
this._updateNode(node);
const neighbors = point.getNeighbors();
for (const neighborPoint of neighbors) {
if (this.nodes.has(neighborPoint.toKey())) {
const neighborNode = this.getNode(neighborPoint);
this._updateNode(neighborNode);
}
}
}
}
/**
* Get the next step the turtle should take from current position
* Returns the next Point to move to, or null if at goal or no path
*/
getNextStep() {
if (this.start.equals(this.goal)) {
return null; // Already at goal
}
if (!this.computeShortestPath()) {
return null; // No path
}
const startNode = this.getNode(this.start);
if (startNode.g === Infinity) {
return null;
}
let bestNeighbor = null;
let bestCost = Infinity;
const neighbors = this.start.getNeighbors();
for (const neighborPoint of neighbors) {
const neighborNode = this.getNode(neighborPoint);
const cost = this._cost(startNode, neighborNode) + neighborNode.g;
if (cost < bestCost) {
bestCost = cost;
bestNeighbor = neighborPoint;
}
}
return bestNeighbor;
}
/**
* Replan the path (called after block updates)
*/
replan() {
return this.computeShortestPath();
}
/**
* Get statistics about the pathfinding
*/
getStats() {
return {
nodesExplored: this.nodes.size,
queueSize: this.queue.size,
startG: this.getNode(this.start).g,
goalRhs: this.getNode(this.goal).rhs,
km: this.km,
};
}
}

View File

@@ -0,0 +1,61 @@
/**
* Node - Represents a pathfinding node in the D* Lite algorithm
* Each node wraps a Point and stores pathfinding metadata
*/
import { Point } from './Point.js';
export class Node {
constructor(point) {
this.point = point;
this.key = point.toKey();
// D* Lite values
this.g = Infinity; // Cost from start to this node
this.rhs = Infinity; // One-step lookahead cost
// Whether this node is blocked (wall, unbreakable block, etc.)
this.blocked = false;
// Whether this node contains a mineable block (costs more to traverse but possible)
this.mineable = false;
// The block data at this position (if known)
this.blockData = null;
}
/**
* Calculate the D* Lite key pair for priority queue ordering
* @param {Point} start - The current start position
* @param {number} km - The key modifier (updated on robot movement)
*/
calculateKey(start, km = 0) {
const minVal = Math.min(this.g, this.rhs);
return [
minVal + start.euclideanDistanceTo(this.point) + km,
minVal
];
}
/**
* Check if this node is consistent (g === rhs)
*/
isConsistent() {
return this.g === this.rhs;
}
/**
* Get the traversal cost to move to this node
* - Blocked nodes: Infinity
* - Mineable blocks: 2 (higher cost to prefer open paths)
* - Open space: 1
*/
getTraversalCost() {
if (this.blocked) return Infinity;
if (this.mineable) return 2;
return 1;
}
toString() {
return `Node(${this.point}, g=${this.g}, rhs=${this.rhs}, blocked=${this.blocked})`;
}
}

100
server/pathfinding/Point.js Normal file
View File

@@ -0,0 +1,100 @@
/**
* Point - Represents a 3D coordinate in the Minecraft world
* Inspired by runi95/turtle-control-panel Point class
*/
export class Point {
constructor(x, y, z) {
this.x = x;
this.y = y;
this.z = z;
}
/**
* Returns the Manhattan distance to another point
*/
distanceTo(other) {
return Math.abs(this.x - other.x) + Math.abs(this.y - other.y) + Math.abs(this.z - other.z);
}
/**
* Returns the Euclidean distance to another point (used for D* Lite heuristic)
*/
euclideanDistanceTo(other) {
const dx = this.x - other.x;
const dy = this.y - other.y;
const dz = this.z - other.z;
return Math.sqrt(dx * dx + dy * dy + dz * dz);
}
/**
* Check equality with another point
*/
equals(other) {
if (!other) return false;
return this.x === other.x && this.y === other.y && this.z === other.z;
}
/**
* Get all 6 neighboring points (up, down, north, south, east, west)
*/
getNeighbors() {
return [
new Point(this.x + 1, this.y, this.z), // East
new Point(this.x - 1, this.y, this.z), // West
new Point(this.x, this.y + 1, this.z), // Up
new Point(this.x, this.y - 1, this.z), // Down
new Point(this.x, this.y, this.z + 1), // South
new Point(this.x, this.y, this.z - 1), // North
];
}
/**
* Get the cardinal direction needed to move from this point to another adjacent point
* Returns: 'forward', 'back', 'up', 'down', or facing adjustment needed
*/
directionTo(other) {
const dx = other.x - this.x;
const dy = other.y - this.y;
const dz = other.z - this.z;
if (dy === 1) return 'up';
if (dy === -1) return 'down';
if (dx === 1) return { facing: 1 }; // East
if (dx === -1) return { facing: 3 }; // West
if (dz === 1) return { facing: 2 }; // South
if (dz === -1) return { facing: 0 }; // North
return null;
}
/**
* Unique string key for maps
*/
toKey() {
return `${this.x},${this.y},${this.z}`;
}
/**
* Create Point from key string
*/
static fromKey(key) {
const [x, y, z] = key.split(',').map(Number);
return new Point(x, y, z);
}
/**
* Create Point from object with x,y,z properties
*/
static from(obj) {
if (!obj) return null;
return new Point(obj.x, obj.y, obj.z);
}
toString() {
return `(${this.x}, ${this.y}, ${this.z})`;
}
toJSON() {
return { x: this.x, y: this.y, z: this.z };
}
}

View File

@@ -0,0 +1,175 @@
/**
* PriorityQueue - Min-heap priority queue for D* Lite
* Uses two-element key pairs [k1, k2] for ordering
*/
export class PriorityQueue {
constructor() {
this.heap = [];
this.keyMap = new Map(); // key string -> index in heap
}
get size() {
return this.heap.length;
}
isEmpty() {
return this.heap.length === 0;
}
/**
* Compare two key pairs
* Returns negative if a < b, positive if a > b, 0 if equal
*/
static compareKeys(a, b) {
if (a[0] !== b[0]) return a[0] - b[0];
return a[1] - b[1];
}
/**
* Get the minimum key without removing
*/
topKey() {
if (this.heap.length === 0) return [Infinity, Infinity];
return this.heap[0].priority;
}
/**
* Insert or update a node with a priority
*/
insertOrUpdate(node, priority) {
const key = node.key;
if (this.keyMap.has(key)) {
// Update existing entry
const index = this.keyMap.get(key);
const oldPriority = this.heap[index].priority;
this.heap[index].priority = priority;
this.heap[index].node = node;
// Determine whether to bubble up or sift down
if (PriorityQueue.compareKeys(priority, oldPriority) < 0) {
this._bubbleUp(index);
} else {
this._siftDown(index);
}
} else {
// Insert new entry
const entry = { node, priority };
this.heap.push(entry);
const index = this.heap.length - 1;
this.keyMap.set(key, index);
this._bubbleUp(index);
}
}
/**
* Remove and return the minimum node
*/
pop() {
if (this.heap.length === 0) return null;
const min = this.heap[0];
this.keyMap.delete(min.node.key);
const last = this.heap.pop();
if (this.heap.length > 0) {
this.heap[0] = last;
this.keyMap.set(last.node.key, 0);
this._siftDown(0);
}
return min.node;
}
/**
* Remove a specific node by key
*/
remove(node) {
const key = node.key;
if (!this.keyMap.has(key)) return;
const index = this.keyMap.get(key);
this.keyMap.delete(key);
const last = this.heap.pop();
if (index < this.heap.length) {
this.heap[index] = last;
this.keyMap.set(last.node.key, index);
this._bubbleUp(index);
this._siftDown(index);
}
}
/**
* Check if a node is in the queue
*/
contains(node) {
return this.keyMap.has(node.key);
}
/**
* Clear the queue
*/
clear() {
this.heap = [];
this.keyMap.clear();
}
// --- Internal heap operations ---
_parent(i) {
return Math.floor((i - 1) / 2);
}
_leftChild(i) {
return 2 * i + 1;
}
_rightChild(i) {
return 2 * i + 2;
}
_swap(i, j) {
const temp = this.heap[i];
this.heap[i] = this.heap[j];
this.heap[j] = temp;
this.keyMap.set(this.heap[i].node.key, i);
this.keyMap.set(this.heap[j].node.key, j);
}
_bubbleUp(i) {
while (i > 0) {
const parent = this._parent(i);
if (PriorityQueue.compareKeys(this.heap[i].priority, this.heap[parent].priority) < 0) {
this._swap(i, parent);
i = parent;
} else {
break;
}
}
}
_siftDown(i) {
const n = this.heap.length;
while (true) {
let smallest = i;
const left = this._leftChild(i);
const right = this._rightChild(i);
if (left < n && PriorityQueue.compareKeys(this.heap[left].priority, this.heap[smallest].priority) < 0) {
smallest = left;
}
if (right < n && PriorityQueue.compareKeys(this.heap[right].priority, this.heap[smallest].priority) < 0) {
smallest = right;
}
if (smallest !== i) {
this._swap(i, smallest);
i = smallest;
} else {
break;
}
}
}
}

View File

@@ -0,0 +1,4 @@
export { Point } from './Point.js';
export { Node } from './Node.js';
export { PriorityQueue } from './PriorityQueue.js';
export { DStarLite } from './DStarLite.js';

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,222 @@
/**
* AutocraftState - Automated crafting using a workbench peripheral
*
* The turtle must have a crafting table equipped as a peripheral.
* Given a recipe (16-slot inventory layout), it will:
* 1. Verify workbench peripheral is present
* 2. Arrange items in the correct slots
* 3. Pull materials from adjacent inventories
* 4. Craft continuously
* 5. Push crafted items into adjacent inventories
*/
import { BaseState } from './BaseState.js';
export class AutocraftState extends BaseState {
constructor(turtle, data = {}) {
super(turtle, data);
// recipe: { 1: {name, count}, 2: null, 3: {name, count}, ... } (1-16 slots)
this.recipe = data.recipe || {};
this.craftCount = 0;
this.totalCrafted = 0;
this.maxCrafts = data.maxCrafts || Infinity;
this.warning = null;
}
get name() {
return 'autocrafting';
}
get description() {
if (this.warning) return `Crafting - ${this.warning}`;
return `Crafting - ${this.totalCrafted} items crafted`;
}
async *act() {
console.log(`[${this.turtle.id}] Starting autocraft`);
// Verify workbench peripheral
const hasWorkbench = await this.exec(`
local names = peripheral.getNames()
for _, name in ipairs(names) do
local types = {peripheral.getType(name)}
for _, t in ipairs(types) do
if t == "workbench" then return true end
end
end
return false
`);
if (!hasWorkbench) {
console.log(`[${this.turtle.id}] No workbench peripheral found`);
this.warning = 'No crafting table equipped';
await this._sleep(3000);
this.turtle.setState('idle');
return;
}
yield;
// Initial craft to verify recipe works
const initialCraft = await this.exec(`
local workbench = peripheral.find("workbench")
if workbench then
local ok, msg = workbench.craft(0)
return ok
end
return false
`);
if (!initialCraft) {
console.log(`[${this.turtle.id}] Recipe validation failed`);
this.warning = 'Invalid recipe';
await this._sleep(3000);
this.turtle.setState('idle');
return;
}
yield;
// Main crafting loop
while (!this.cancelled && this.totalCrafted < this.maxCrafts) {
this.warning = null;
let recipeReady = true;
// Arrange items for recipe
for (let slot = 1; slot <= 16; slot++) {
if (this.cancelled) return;
const recipeItem = this.recipe[slot];
// Get current item in slot
const currentItem = await this.exec(`
local item = turtle.getItemDetail(${slot})
if item then return {name=item.name, count=item.count} end
return nil
`);
yield;
if (!recipeItem) {
// Slot should be empty - move item out if present
if (currentItem) {
await this._transferSlotOut(slot);
yield;
}
continue;
}
// Slot needs a specific item
if (currentItem) {
if (currentItem.name === recipeItem.name) {
continue; // Already has correct item
}
// Wrong item - move it out
await this._transferSlotOut(slot);
yield;
}
// Pull the required item into this slot
const pulled = await this._pullItemToSlot(recipeItem.name, slot);
yield;
if (!pulled) {
this.warning = `Missing: ${recipeItem.name.replace('minecraft:', '')}`;
recipeReady = false;
break;
}
}
if (!recipeReady) {
yield;
await this._sleep(5000);
yield;
continue;
}
// Craft!
const craftResult = await this.exec(`
local workbench = peripheral.find("workbench")
if workbench then
local ok, msg = workbench.craft()
if ok then return true
else return false
end
end
return false
`);
if (craftResult) {
this.totalCrafted++;
this.craftCount++;
console.log(`[${this.turtle.id}] Crafted item #${this.totalCrafted}`);
// Push crafted items out to adjacent inventory
await this._pushCraftedItems();
yield;
} else {
this.warning = 'Craft failed';
await this._sleep(2000);
}
yield;
await this._sleep(500);
}
console.log(`[${this.turtle.id}] Autocraft finished: ${this.totalCrafted} total`);
this.turtle.setState('idle');
}
async _transferSlotOut(slot) {
// Try to drop the item in the given slot into an adjacent inventory
await this.exec(`
turtle.select(${slot})
if not turtle.dropDown() then
if not turtle.dropUp() then
turtle.drop()
end
end
turtle.select(1)
`);
}
async _pullItemToSlot(itemName, targetSlot) {
// Try to pull item from adjacent inventories
// First try suckDown, then suckUp, then suck
const result = await this.exec(`
turtle.select(${targetSlot})
-- Try each direction
for _, suckFn in ipairs({turtle.suckDown, turtle.suckUp, turtle.suck}) do
suckFn(1)
local item = turtle.getItemDetail(${targetSlot})
if item and item.name == "${itemName}" then
return true
elseif item then
-- Wrong item, put it back
for _, dropFn in ipairs({turtle.dropDown, turtle.dropUp, turtle.drop}) do
if dropFn() then break end
end
end
end
turtle.select(1)
return false
`);
return result === true;
}
async _pushCraftedItems() {
// Push all items out of inventory
await this.exec(`
for slot = 1, 16 do
local item = turtle.getItemDetail(slot)
if item then
turtle.select(slot)
if not turtle.dropDown() then
if not turtle.dropUp() then
turtle.drop()
end
end
end
end
turtle.select(1)
`);
}
}

356
server/states/BaseState.js Normal file
View File

@@ -0,0 +1,356 @@
/**
* BaseState - Abstract base class for turtle state machine
*
* Uses async generator pattern inspired by runi95/turtle-control-panel.
* Each state implements *act() which yields control back periodically,
* allowing the state machine to be cooperative and interruptible.
*/
export class BaseState {
/**
* @param {import('../Turtle.js').Turtle} turtle - The turtle instance
* @param {Object} data - State-specific data for initialization/recovery
*/
constructor(turtle, data = {}) {
this.turtle = turtle;
this.data = data;
this.cancelled = false;
this.startedAt = Date.now();
}
/**
* The name of this state (used for logging and UI)
*/
get name() {
return 'base';
}
/**
* Get a human-readable description of what the state is doing
*/
get description() {
return 'Idle';
}
/**
* The async generator that drives the state's behavior.
* Subclasses MUST override this method.
*
* Yield to give up control temporarily (cooperative multitasking).
* Return to signal that the state is complete.
* Throw to signal an error.
*
* @yields {void}
*/
async *act() {
// Base state does nothing - just idles
while (!this.cancelled) {
yield;
await this._sleep(1000);
}
}
/**
* Cancel this state. The act() generator should check this.cancelled
* and clean up gracefully.
*/
cancel() {
this.cancelled = true;
}
/**
* Get recovery data for persisting state across reconnections
*/
getRecoveryData() {
return {
stateName: this.name,
data: this.data,
startedAt: this.startedAt,
};
}
// ========== Helper Methods for Subclasses ==========
/**
* Execute a Lua command on the turtle and wait for the result
*/
async exec(luaCode) {
return this.turtle.exec(luaCode);
}
/**
* Move the turtle forward, digging if necessary
*/
async moveForward(dig = true) {
if (dig) {
const result = await this.exec('turtle.dig(); return turtle.forward()');
if (result) this.turtle.updatePositionForward();
return result;
}
const result = await this.exec('return turtle.forward()');
if (result) this.turtle.updatePositionForward();
return result;
}
/**
* Move the turtle up, digging if necessary
*/
async moveUp(dig = true) {
if (dig) {
const result = await this.exec('turtle.digUp(); return turtle.up()');
if (result) this.turtle.updatePositionUp();
return result;
}
const result = await this.exec('return turtle.up()');
if (result) this.turtle.updatePositionUp();
return result;
}
/**
* Move the turtle down, digging if necessary
*/
async moveDown(dig = true) {
if (dig) {
const result = await this.exec('turtle.digDown(); return turtle.down()');
if (result) this.turtle.updatePositionDown();
return result;
}
const result = await this.exec('return turtle.down()');
if (result) this.turtle.updatePositionDown();
return result;
}
/**
* Turn the turtle to face a specific direction
* @param {number} targetFacing - 0=North, 1=East, 2=South, 3=West
*/
async turnToFace(targetFacing) {
const currentFacing = this.turtle.facing;
if (currentFacing === targetFacing) return true;
const diff = (targetFacing - currentFacing + 4) % 4;
if (diff === 1) {
await this.exec(`turtle.turnRight(); _G._turtleFacing = ${targetFacing}`);
this.turtle.facing = targetFacing;
} else if (diff === 2) {
await this.exec(`turtle.turnRight(); turtle.turnRight(); _G._turtleFacing = ${targetFacing}`);
this.turtle.facing = targetFacing;
} else if (diff === 3) {
await this.exec(`turtle.turnLeft(); _G._turtleFacing = ${targetFacing}`);
this.turtle.facing = targetFacing;
}
return true;
}
/**
* Move to an adjacent point (one of the 6 neighbors)
* Handles turning and digging automatically
*/
async moveToAdjacent(targetPoint) {
const pos = this.turtle.position;
if (!pos) return false;
const dx = targetPoint.x - pos.x;
const dy = targetPoint.y - pos.y;
const dz = targetPoint.z - pos.z;
if (dy === 1) return this.moveUp();
if (dy === -1) return this.moveDown();
// Horizontal movement - determine facing
let targetFacing;
if (dx === 1) targetFacing = 1; // East
else if (dx === -1) targetFacing = 3; // West
else if (dz === 1) targetFacing = 2; // South
else if (dz === -1) targetFacing = 0; // North
else return false; // Not adjacent
await this.turnToFace(targetFacing);
return this.moveForward();
}
/**
* Navigate to a target point using the D* Lite pathfinder
* This is a generator that yields after each step
*/
async *navigateTo(targetPoint, options = {}) {
const { Point, DStarLite } = await import('../pathfinding/index.js');
const start = Point.from(this.turtle.position);
const goal = Point.from(targetPoint);
if (!start || !goal) {
console.error('Cannot navigate: missing position');
return false;
}
if (start.equals(goal)) return true;
const pathfinder = new DStarLite(start, goal, this.turtle.server.worldBlocks, {
canMine: options.canMine !== false,
maxSteps: options.maxSteps || 50000,
});
let currentPos = start;
let attempts = 0;
const maxAttempts = options.maxAttempts || 5000;
while (!currentPos.equals(goal) && attempts < maxAttempts && !this.cancelled) {
attempts++;
const nextStep = pathfinder.getNextStep();
if (!nextStep) {
console.log(`[${this.turtle.id}] No path to ${goal} from ${currentPos}`);
return false;
}
// Scan surroundings before moving
const scanResult = await this.exec(`
local results = {}
local hasBlock, data
hasBlock, data = turtle.inspect()
if hasBlock then results.forward = data end
hasBlock, data = turtle.inspectUp()
if hasBlock then results.up = data end
hasBlock, data = turtle.inspectDown()
if hasBlock then results.down = data end
return results
`);
// Update pathfinder with discovered blocks
if (scanResult && typeof scanResult === 'object') {
this.turtle.processScanResults(scanResult);
// Update pathfinder with new information
for (const [dir, blockData] of Object.entries(scanResult)) {
const blockPos = this.turtle.getBlockPositionInDirection(dir);
if (blockPos) {
pathfinder.updateBlock(Point.from(blockPos), blockData);
}
}
}
// Try to move to the next step
const success = await this.moveToAdjacent(nextStep);
if (success) {
currentPos = Point.from(this.turtle.position);
pathfinder.updateStart(currentPos);
} else {
// Movement failed - update the blocked node and replan
pathfinder.updateBlock(nextStep, { name: 'minecraft:bedrock' }); // Treat as blocked
if (!pathfinder.replan()) {
console.log(`[${this.turtle.id}] Replanning failed - no path exists`);
return false;
}
}
yield; // Cooperative yield
}
return currentPos.equals(goal);
}
/**
* Scan all 6 directions around the turtle
*/
async scanSurroundings() {
const result = await this.exec(`
local results = {}
local facing = _G._turtleFacing or 0
-- Scan up/down
local hasBlock, data
hasBlock, data = turtle.inspectUp()
if hasBlock then results.up = {name=data.name, metadata=data.metadata or 0} end
hasBlock, data = turtle.inspectDown()
if hasBlock then results.down = {name=data.name, metadata=data.metadata or 0} end
-- Scan all 4 horizontal directions
for i = 0, 3 do
hasBlock, data = turtle.inspect()
if hasBlock then
local dir = (facing + i) % 4
results["h" .. dir] = {name=data.name, metadata=data.metadata or 0}
end
if i < 3 then turtle.turnRight(); facing = (facing + 1) % 4 end
end
-- Turn back to original facing
turtle.turnRight()
facing = (facing + 1) % 4
_G._turtleFacing = facing
return results
`);
if (result) {
this.turtle.processScanResults(result);
}
return result;
}
/**
* Check fuel level
*/
async checkFuel() {
const fuel = await this.exec('return turtle.getFuelLevel()');
if (fuel !== null) this.turtle.fuel = fuel;
return fuel;
}
/**
* Get inventory contents
*/
async getInventory() {
const inv = await this.exec(`
local items = {}
for slot = 1, 16 do
local item = turtle.getItemDetail(slot)
if item then
items[slot] = {name=item.name, count=item.count, damage=item.damage}
end
end
return items
`);
if (inv) this.turtle.inventory = inv;
return inv;
}
/**
* Check if inventory is full
*/
async isInventoryFull() {
const result = await this.exec(`
for slot = 1, 16 do
if turtle.getItemCount(slot) == 0 then return false end
end
return true
`);
return result === true;
}
/**
* Try to refuel from inventory
*/
async tryRefuel() {
return this.exec(`
for slot = 1, 16 do
turtle.select(slot)
if turtle.refuel(0) then
turtle.refuel(1)
turtle.select(1)
return true
end
end
turtle.select(1)
return false
`);
}
/**
* Sleep for a duration (non-blocking)
*/
_sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}

View File

@@ -0,0 +1,311 @@
/**
* BuildingState - Blueprint-based autonomous block placement
*
* Given a list of blocks (x, y, z, name, state), the turtle will:
* 1. Calculate required materials
* 2. Go home to collect materials from nearby inventories
* 3. Navigate to each block position and place it
* 4. Handle facing-aware placement (stairs, logs, etc.)
* 5. Track failed placements and retry later
*/
import { BaseState } from './BaseState.js';
export class BuildingState extends BaseState {
constructor(turtle, data = {}) {
super(turtle, data);
// blocks: [{x, y, z, name, state?}]
this.blocks = data.blocks || [];
this.placedCount = 0;
this.failedPlacements = new Map(); // "x,y,z" -> retry count
this.maxRetries = 3;
// Group blocks by column (x,z) for efficient placement
this.columns = new Map(); // "x,z" -> [{x,y,z,name,state}] sorted by y ascending
this._organizeBlocks();
}
get name() {
return 'building';
}
get description() {
return `Building - ${this.placedCount}/${this.blocks.length} placed`;
}
_organizeBlocks() {
// Filter out blocks that might already be placed (we can't check DB here,
// but we organize for efficient traversal)
for (const block of this.blocks) {
const key = `${block.x},${block.z}`;
if (!this.columns.has(key)) {
this.columns.set(key, []);
}
this.columns.get(key).push(block);
}
// Sort each column by Y ascending (place from bottom up)
for (const [key, column] of this.columns) {
column.sort((a, b) => a.y - b.y);
}
}
async *act() {
console.log(`[${this.turtle.id}] Starting build: ${this.blocks.length} blocks to place`);
while (!this.cancelled) {
// Check if we have any blocks left to place
const remainingBlocks = this._getRemainingBlocks();
if (remainingBlocks.length === 0) {
console.log(`[${this.turtle.id}] Build complete: ${this.placedCount} blocks placed`);
this.turtle.setState('idle');
return;
}
// Safety checks
const fuel = await this.checkFuel();
if (fuel !== 'unlimited' && fuel < 500) {
const refueled = await this.tryRefuel();
if (!refueled) {
console.log(`[${this.turtle.id}] Low fuel during build, going home`);
this.turtle.setState('goHome', { reason: 'low_fuel', returnState: 'building', returnData: this.data });
return;
}
}
yield;
// Calculate required materials
const requiredMaterials = this._calculateMaterials(remainingBlocks);
// Check if we have the materials
const inventory = await this.getInventory();
const hasMaterials = this._hasRequiredMaterials(requiredMaterials, inventory);
yield;
if (!hasMaterials) {
// Go home to collect materials
const home = this.turtle.homePosition;
if (!home) {
console.log(`[${this.turtle.id}] No home set, cannot collect materials`);
this.turtle.setState('idle');
return;
}
console.log(`[${this.turtle.id}] Going home to collect building materials`);
// Navigate home
const reachedHome = yield* this.navigateTo(home);
if (!reachedHome) {
console.log(`[${this.turtle.id}] Cannot reach home`);
await this._sleep(5000);
yield;
continue;
}
yield;
// Dump current inventory into nearby chests
await this._dumpInventory();
yield;
// Try to pull required materials
const gotMaterials = await this._pullMaterials(requiredMaterials);
yield;
if (!gotMaterials) {
console.log(`[${this.turtle.id}] Cannot get required materials, waiting...`);
await this._sleep(10000);
yield;
continue;
}
}
// Find the next block to place (closest to turtle)
const pos = this.turtle.position;
if (!pos) {
await this._sleep(1000);
yield;
continue;
}
const nextBlock = this._findClosestBlock(remainingBlocks, pos);
if (!nextBlock) {
await this._sleep(1000);
yield;
continue;
}
// Navigate to one block above the target (to place down)
const placePos = { x: nextBlock.x, y: nextBlock.y + 1, z: nextBlock.z };
const reached = yield* this.navigateTo(placePos);
yield;
if (!reached) {
// Mark as failed
const key = `${nextBlock.x},${nextBlock.y},${nextBlock.z}`;
const retries = (this.failedPlacements.get(key) || 0) + 1;
this.failedPlacements.set(key, retries);
if (retries >= this.maxRetries) {
console.log(`[${this.turtle.id}] Cannot reach ${key}, skipping permanently`);
}
yield;
continue;
}
// Handle facing - turn turtle to face correct direction before placing
if (nextBlock.state && nextBlock.state.facing) {
await this._turnForPlacement(nextBlock.state.facing);
yield;
}
// Select the correct item
const selected = await this._selectItem(nextBlock.name);
if (!selected) {
console.log(`[${this.turtle.id}] Don't have ${nextBlock.name} to place`);
yield;
continue;
}
// Place the block
const placed = await this.exec('return turtle.placeDown()');
yield;
if (placed) {
this.placedCount++;
// Remove from our list
this._markBlockPlaced(nextBlock);
console.log(`[${this.turtle.id}] Placed ${nextBlock.name} at ${nextBlock.x},${nextBlock.y},${nextBlock.z} (${this.placedCount}/${this.blocks.length})`);
} else {
const key = `${nextBlock.x},${nextBlock.y},${nextBlock.z}`;
const retries = (this.failedPlacements.get(key) || 0) + 1;
this.failedPlacements.set(key, retries);
}
yield;
await this._sleep(100);
}
}
_getRemainingBlocks() {
const remaining = [];
for (const [key, column] of this.columns) {
for (const block of column) {
const blockKey = `${block.x},${block.y},${block.z}`;
const retries = this.failedPlacements.get(blockKey) || 0;
if (retries < this.maxRetries) {
remaining.push(block);
}
}
}
return remaining;
}
_calculateMaterials(blocks) {
const materials = new Map(); // name -> count
for (const block of blocks) {
materials.set(block.name, (materials.get(block.name) || 0) + 1);
}
return materials;
}
_hasRequiredMaterials(required, inventory) {
if (!inventory) return false;
const available = new Map();
for (const [slot, item] of Object.entries(inventory)) {
if (item && item.name) {
available.set(item.name, (available.get(item.name) || 0) + item.count);
}
}
for (const [name, count] of required) {
if ((available.get(name) || 0) < Math.min(count, 1)) {
return false; // Need at least 1 of each material
}
}
return true;
}
_findClosestBlock(blocks, from) {
let closest = null;
let minDist = Infinity;
for (const block of blocks) {
const dist = Math.abs(block.x - from.x) + Math.abs(block.y - from.y) + Math.abs(block.z - from.z);
if (dist < minDist) {
minDist = dist;
closest = block;
}
}
return closest;
}
_markBlockPlaced(block) {
const key = `${block.x},${block.z}`;
const column = this.columns.get(key);
if (column) {
const idx = column.findIndex(b => b.x === block.x && b.y === block.y && b.z === block.z);
if (idx >= 0) column.splice(idx, 1);
if (column.length === 0) this.columns.delete(key);
}
}
async _turnForPlacement(facing) {
// When placing blocks with facing, the turtle should face the opposite direction
// because placed blocks face toward the placer
const facingMap = { north: 2, south: 0, east: 3, west: 1 };
const targetFacing = facingMap[facing];
if (targetFacing !== undefined) {
await this.turnToFace(targetFacing);
}
}
async _selectItem(blockName) {
const result = await this.exec(`
for slot = 1, 16 do
local item = turtle.getItemDetail(slot)
if item and item.name == "${blockName}" then
turtle.select(slot)
return true
end
end
return false
`);
return result === true;
}
async _dumpInventory() {
await this.exec(`
for slot = 1, 16 do
local item = turtle.getItemDetail(slot)
if item then
turtle.select(slot)
turtle.dropDown()
end
end
turtle.select(1)
`);
}
async _pullMaterials(required) {
// Try to suck items from chests below (at home position)
let gotAny = false;
for (const [name, count] of required) {
const result = await this.exec(`
local needed = "${name}"
for slot = 1, 16 do
if turtle.getItemCount(slot) == 0 then
turtle.select(slot)
turtle.suckDown(64)
local item = turtle.getItemDetail(slot)
if item and item.name == needed then
return true
elseif item then
turtle.dropDown()
end
end
end
turtle.select(1)
return false
`);
if (result) gotAny = true;
}
return gotAny;
}
}

View File

@@ -0,0 +1,209 @@
/**
* DumpInventoryState - Dump inventory into nearby containers
* Keeps fuel items, dumps everything else.
*
* Improvements over v1:
* - Uses Levenshtein distance for fuzzy container name matching
* - Detects containers via peripheral inspection on all sides
* - Tracks dump success/failure per direction
* - Reports detailed dump stats
* - Optionally navigates home before dumping
*/
import { BaseState } from './BaseState.js';
import { findBestMatch } from '../helpers/levenshtein.js';
// Items to keep (fuel sources and tools)
const KEEP_ITEMS = new Set([
'minecraft:coal', 'minecraft:charcoal', 'minecraft:coal_block',
'minecraft:torch', 'minecraft:lava_bucket',
]);
// Known container block names (used for fuzzy matching)
const CONTAINER_NAMES = [
'chest', 'barrel', 'shulker_box', 'hopper', 'dropper', 'dispenser',
'trapped_chest', 'ender_chest', 'furnace', 'blast_furnace', 'smoker',
];
/**
* Check if a block name is likely a container using fuzzy matching
*/
function isContainerBlock(blockName) {
if (!blockName) return false;
const shortName = blockName.replace(/^[^:]+:/, '').toLowerCase();
// Exact substring match first
for (const container of CONTAINER_NAMES) {
if (shortName.includes(container)) return true;
}
// Fuzzy match as fallback
const { match, distance } = findBestMatch(shortName, CONTAINER_NAMES, 3);
return match !== null && distance <= 2;
}
export class DumpInventoryState extends BaseState {
constructor(turtle, data = {}) {
super(turtle, data);
this.returnState = data.returnState || null;
this.returnData = data.returnData || {};
this.navigateHome = data.navigateHome || false;
this.keepItems = data.keepItems
? new Set([...KEEP_ITEMS, ...data.keepItems])
: KEEP_ITEMS;
}
get name() {
return 'dumping';
}
get description() {
return this.navigateHome ? 'Returning home to dump inventory' : 'Dumping inventory';
}
async *act() {
console.log(`[${this.turtle.id}] Starting inventory dump`);
// Optionally navigate home first
if (this.navigateHome && this.turtle.homePosition) {
console.log(`[${this.turtle.id}] Navigating home before dumping`);
yield* this.navigateTo(this.turtle.homePosition);
yield;
}
// First, detect containers by inspecting all directions
const containerDirections = await this._detectContainers();
if (containerDirections.length === 0) {
console.log(`[${this.turtle.id}] No containers found nearby, trying blind dump`);
containerDirections.push('front', 'up', 'down');
} else {
console.log(`[${this.turtle.id}] Found containers: ${containerDirections.join(', ')}`);
}
// Build keep items set for Lua
const keepItemsList = [...this.keepItems].map(n => `"${n}"`).join(', ');
// Map direction names to Lua drop functions
const dropFnMap = {
front: 'turtle.drop',
up: 'turtle.dropUp',
down: 'turtle.dropDown',
};
// Build the drop functions array for only detected container directions
const dropFnEntries = containerDirections
.filter(d => dropFnMap[d])
.map(d => dropFnMap[d]);
if (dropFnEntries.length === 0) {
console.log(`[${this.turtle.id}] No valid drop directions`);
this._transitionOut();
return;
}
const result = await this.exec(`
local keepItems = {${keepItemsList}}
local keepSet = {}
for _, name in ipairs(keepItems) do keepSet[name] = true end
local dropFns = {${dropFnEntries.join(', ')}}
local dumpedCount = 0
local failedCount = 0
for _, dropFn in ipairs(dropFns) do
for slot = 1, 16 do
local item = turtle.getItemDetail(slot)
if item and not keepSet[item.name] then
turtle.select(slot)
local prevCount = item.count
if dropFn() then
local remaining = turtle.getItemCount(slot)
dumpedCount = dumpedCount + (prevCount - remaining)
else
failedCount = failedCount + 1
end
end
end
end
turtle.select(1)
return {dumped = dumpedCount, failed = failedCount}
`);
if (result) {
console.log(`[${this.turtle.id}] Dumped ${result.dumped} items (${result.failed} drops failed)`);
if (result.failed > 0) {
this.turtle.warning = `Dump: ${result.failed} drops failed (container full?)`;
}
}
yield;
// Update inventory after dumping
await this.getInventory();
// Refresh any adjacent inventory peripherals
for (const dir of containerDirections) {
if (this.turtle._peripherals?.[dir]?.types?.includes('inventory')) {
try {
await this.turtle.connectToInventory(dir);
} catch (e) { /* ignore */ }
}
}
this._transitionOut();
}
/**
* Detect which directions have containers by inspecting blocks
*/
async _detectContainers() {
const result = await this.exec(`
local dirs = {}
local h, d
h, d = turtle.inspect()
if h then dirs.front = d.name end
h, d = turtle.inspectUp()
if h then dirs.up = d.name end
h, d = turtle.inspectDown()
if h then dirs.down = d.name end
return dirs
`);
if (!result || typeof result !== 'object') return [];
const containerDirs = [];
for (const [dir, blockName] of Object.entries(result)) {
if (isContainerBlock(blockName)) {
containerDirs.push(dir);
}
}
return containerDirs;
}
/**
* Transition to the next state (return state or idle)
*/
_transitionOut() {
if (this.returnState) {
this.turtle.setState(this.returnState, this.returnData);
} else {
this.turtle.setState('idle');
}
}
getRecoveryData() {
return {
...super.getRecoveryData(),
data: {
returnState: this.returnState,
returnData: this.returnData,
navigateHome: this.navigateHome,
keepItems: this.keepItems !== KEEP_ITEMS
? [...this.keepItems].filter(i => !KEEP_ITEMS.has(i))
: undefined,
},
};
}
}

View File

@@ -0,0 +1,380 @@
/**
* ExploringState - Chunk-based spiral exploration to discover the world map
*
* Instead of random walking, this systematically explores in a spiral pattern
* outward from the starting position, chunk by chunk (16x16 areas).
* Within each chunk, it does a strip-mine pattern to scan all blocks.
*
* Inspired by runi95/turtle-control-panel's exploration pattern.
*/
import { BaseState } from './BaseState.js';
export class ExploringState extends BaseState {
constructor(turtle, data = {}) {
super(turtle, data);
this.maxDistance = data.maxDistance || 200;
this.minFuel = data.minFuel || 500;
this.yLevel = data.yLevel || null; // null = stay at current Y
this.blocksDiscovered = 0;
this.chunksExplored = 0;
// Spiral state
this.spiralRing = data.spiralRing || 0;
this.spiralSide = data.spiralSide || 0;
this.spiralStep = data.spiralStep || 0;
this.spiralChunkX = data.spiralChunkX ?? null;
this.spiralChunkZ = data.spiralChunkZ ?? null;
this.startChunkX = data.startChunkX ?? null;
this.startChunkZ = data.startChunkZ ?? null;
this.exploredChunks = new Set(data.exploredChunks || []);
}
get name() {
return 'exploring';
}
get description() {
return `Exploring - ${this.blocksDiscovered} blocks, ${this.chunksExplored} chunks (ring ${this.spiralRing})`;
}
async *act() {
const pos = this.turtle.position;
if (!pos) {
console.log(`[${this.turtle.id}] No position, trying GPS...`);
await this.turtle.gpsLocate();
if (!this.turtle.position) {
console.error(`[${this.turtle.id}] Cannot explore without position`);
this.turtle.setState('idle');
return;
}
}
// Initialize start chunk from current position
if (this.startChunkX === null) {
this.startChunkX = Math.floor(this.turtle.position.x / 16);
this.startChunkZ = Math.floor(this.turtle.position.z / 16);
this.spiralChunkX = this.startChunkX;
this.spiralChunkZ = this.startChunkZ;
}
// Set Y level for exploration
if (!this.yLevel) {
this.yLevel = this.turtle.position.y;
}
console.log(`[${this.turtle.id}] Starting chunk-based spiral exploration from chunk (${this.startChunkX}, ${this.startChunkZ}), Y=${this.yLevel}`);
while (!this.cancelled) {
try {
// Safety: check fuel
const fuel = await this.checkFuel();
if (fuel !== 'unlimited' && fuel < this.minFuel) {
const refueled = await this.tryRefuel();
if (!refueled) {
console.log(`[${this.turtle.id}] Low fuel, going home`);
this.turtle.setState('goHome', { reason: 'low_fuel', returnState: 'exploring', returnData: this.getRecoveryData().data });
return;
}
}
// Safety: check inventory
const isFull = await this.isInventoryFull();
if (isFull) {
console.log(`[${this.turtle.id}] Inventory full, going home to dump`);
this.turtle.setState('goHome', { reason: 'inventory_full', returnState: 'exploring', returnData: this.getRecoveryData().data });
return;
}
// Safety: check distance
if (this._isTooFar()) {
console.log(`[${this.turtle.id}] Too far from home, returning`);
this.turtle.setState('goHome', { reason: 'too_far' });
return;
}
// Get the current target chunk
const chunkKey = `${this.spiralChunkX},${this.spiralChunkZ}`;
if (!this.exploredChunks.has(chunkKey)) {
// Explore this chunk
console.log(`[${this.turtle.id}] Exploring chunk (${this.spiralChunkX}, ${this.spiralChunkZ}) [ring ${this.spiralRing}]`);
yield* this._exploreChunk(this.spiralChunkX, this.spiralChunkZ);
this.exploredChunks.add(chunkKey);
this.chunksExplored++;
}
// Advance spiral to next chunk
this._advanceSpiral();
yield;
} catch (error) {
const isTimeout = error.message?.includes('timed out');
if (isTimeout) {
console.warn(`[${this.turtle.id}] Exploration timeout, retrying...`);
await this._sleep(3000);
} else {
throw error;
}
}
}
}
/**
* Explore a single chunk by navigating to it and doing a strip-mine scan pattern.
* Scans every 3rd row at the exploration Y level for good coverage.
*/
async *_exploreChunk(chunkX, chunkZ) {
const startX = chunkX * 16;
const startZ = chunkZ * 16;
// Navigate to the start corner of the chunk
const chunkStart = { x: startX, y: this.yLevel, z: startZ };
yield* this._navigateToSafe(chunkStart);
// Strip-mine scan pattern across the chunk
// Every 3 blocks gives good coverage with turtle.inspect() range
let forward = true;
for (let row = 0; row < 16; row += 3) {
if (this.cancelled) return;
const rowZ = startZ + row;
const rowStartX = forward ? startX : startX + 15;
// Navigate to row start
yield* this._navigateToSafe({ x: rowStartX, y: this.yLevel, z: rowZ });
// Walk along the row, scanning as we go
const direction = forward ? 1 : 3; // East or West
await this.turnToFace(direction);
for (let step = 0; step < 15; step++) {
if (this.cancelled) return;
// 3-direction scan
const scanResult = await this.exec(`
local results = {}
local h, d
h, d = turtle.inspect()
if h then results.forward = d end
h, d = turtle.inspectUp()
if h then results.up = d end
h, d = turtle.inspectDown()
if h then results.down = d end
return results
`);
if (scanResult && typeof scanResult === 'object') {
this.turtle.processScanResults(scanResult);
this.blocksDiscovered += Object.keys(scanResult).length;
}
// Mine any valuable ores we find
yield* this._checkAndMineOres();
// Move forward (digging through obstacles)
const moved = await this.moveForward(true);
if (!moved) {
// Stuck - try to go over
await this.moveUp(true);
const movedOver = await this.moveForward(true);
if (movedOver) {
await this.moveDown(true);
} else {
// Really stuck, skip rest of row
break;
}
}
yield;
await this._sleep(100);
}
// Alternate direction for serpentine pattern
forward = !forward;
}
}
/**
* Navigate to a target position safely using pathfinding with simple fallback
*/
async *_navigateToSafe(target) {
try {
const success = yield* this.navigateTo(target, { canMine: true, maxAttempts: 500 });
if (success) return;
} catch (error) {
// Pathfinding failed, use simple navigation
}
// Fallback: simple direct navigation
yield* this._simpleNavigate(target);
}
/**
* Simple direct navigation (X -> Z -> Y)
*/
async *_simpleNavigate(target) {
const maxAttempts = 300;
let attempts = 0;
let stuckCount = 0;
while (attempts < maxAttempts && !this.cancelled) {
const pos = this.turtle.position;
if (!pos) return;
const dx = target.x - pos.x;
const dy = target.y - pos.y;
const dz = target.z - pos.z;
// Close enough (within 2 blocks)
if (Math.abs(dx) <= 1 && Math.abs(dy) <= 1 && Math.abs(dz) <= 1) return;
attempts++;
let moved = false;
// Move in X direction
if (Math.abs(dx) > 1) {
const facing = dx > 0 ? 1 : 3;
await this.turnToFace(facing);
moved = await this.moveForward(true);
}
// Then Z direction
else if (Math.abs(dz) > 1) {
const facing = dz > 0 ? 2 : 0;
await this.turnToFace(facing);
moved = await this.moveForward(true);
}
// Then Y direction
else if (dy > 0) {
moved = await this.moveUp(true);
} else if (dy < 0) {
moved = await this.moveDown(true);
}
if (!moved) {
stuckCount++;
if (stuckCount > 5) {
// Try going up and over
await this.moveUp(true);
await this.moveUp(true);
stuckCount = 0;
}
} else {
stuckCount = 0;
}
yield;
}
}
async *_checkAndMineOres() {
const valuableOres = new Set([
'minecraft:diamond_ore', 'minecraft:emerald_ore',
'minecraft:deepslate_diamond_ore', 'minecraft:deepslate_emerald_ore',
'minecraft:gold_ore', 'minecraft:deepslate_gold_ore',
'minecraft:iron_ore', 'minecraft:deepslate_iron_ore',
'minecraft:lapis_ore', 'minecraft:deepslate_lapis_ore',
'minecraft:redstone_ore', 'minecraft:deepslate_redstone_ore',
'minecraft:copper_ore', 'minecraft:deepslate_copper_ore',
'minecraft:coal_ore', 'minecraft:deepslate_coal_ore',
'minecraft:ancient_debris',
]);
const directions = [
{ check: 'turtle.inspect()', dig: 'turtle.dig()' },
{ check: 'turtle.inspectUp()', dig: 'turtle.digUp()' },
{ check: 'turtle.inspectDown()', dig: 'turtle.digDown()' },
];
for (const { check, dig } of directions) {
if (this.cancelled) return;
const result = await this.exec(`
local hasBlock, data = ${check}
if hasBlock then return {name = data.name} end
return nil
`);
if (result && valuableOres.has(result.name)) {
await this.exec(dig);
this.turtle.emit('blockMined', { blockType: result.name });
yield;
}
}
}
/**
* Advance the spiral to the next chunk position.
* Standard clockwise spiral: start at center, go E, then S, W, N with increasing lengths.
* Ring 0: just center
* Ring 1: side lengths = 1,2,2,1 (total 6 chunks)
* Ring R: 8*R chunks around the ring
*/
_advanceSpiral() {
if (this.spiralRing === 0) {
// Center done, start ring 1 going East
this.spiralRing = 1;
this.spiralSide = 0;
this.spiralStep = 0;
this.spiralChunkX = this.startChunkX + 1;
this.spiralChunkZ = this.startChunkZ - (this.spiralRing - 1);
return;
}
// Side directions: S, W, N, E
const sideDirs = [
{ dx: 0, dz: 1 }, // South
{ dx: -1, dz: 0 }, // West
{ dx: 0, dz: -1 }, // North
{ dx: 1, dz: 0 }, // East
];
this.spiralStep++;
const sideLength = this.spiralRing * 2;
if (this.spiralStep >= sideLength) {
this.spiralStep = 0;
this.spiralSide++;
if (this.spiralSide >= 4) {
// Done with this ring, move to next
this.spiralRing++;
this.spiralSide = 0;
// Jump to start of new ring (one East, one North from current)
this.spiralChunkX = this.startChunkX + this.spiralRing;
this.spiralChunkZ = this.startChunkZ - (this.spiralRing - 1);
return;
}
}
// Move in current side direction
const dir = sideDirs[this.spiralSide];
this.spiralChunkX += dir.dx;
this.spiralChunkZ += dir.dz;
}
_isTooFar() {
const pos = this.turtle.position;
const home = this.turtle.homePosition;
if (!pos || !home) return false;
const dist = Math.abs(pos.x - home.x) + Math.abs(pos.y - home.y) + Math.abs(pos.z - home.z);
return dist > this.maxDistance;
}
getRecoveryData() {
return {
...super.getRecoveryData(),
data: {
maxDistance: this.maxDistance,
minFuel: this.minFuel,
yLevel: this.yLevel,
spiralRing: this.spiralRing,
spiralSide: this.spiralSide,
spiralStep: this.spiralStep,
spiralChunkX: this.spiralChunkX,
spiralChunkZ: this.spiralChunkZ,
startChunkX: this.startChunkX,
startChunkZ: this.startChunkZ,
exploredChunks: [...this.exploredChunks],
},
};
}
}

View File

@@ -0,0 +1,321 @@
/**
* ExtractionState - Smart ore extraction in a defined area
*
* Uses scanner peripherals to find ores, navigates to them,
* mines them, and returns home when full or low on fuel.
* Much more targeted than basic MiningState.
*/
import { BaseState } from './BaseState.js';
const ORE_BLOCKS = new Set([
'minecraft:coal_ore', 'minecraft:iron_ore', 'minecraft:gold_ore',
'minecraft:diamond_ore', 'minecraft:emerald_ore', 'minecraft:redstone_ore',
'minecraft:lapis_ore', 'minecraft:copper_ore',
'minecraft:deepslate_coal_ore', 'minecraft:deepslate_iron_ore',
'minecraft:deepslate_gold_ore', 'minecraft:deepslate_diamond_ore',
'minecraft:deepslate_emerald_ore', 'minecraft:deepslate_redstone_ore',
'minecraft:deepslate_lapis_ore', 'minecraft:deepslate_copper_ore',
'minecraft:nether_gold_ore', 'minecraft:nether_quartz_ore',
'minecraft:ancient_debris',
]);
export class ExtractionState extends BaseState {
constructor(turtle, data = {}) {
super(turtle, data);
// Area bounds for extraction
this.area = data.area || []; // [{x,y,z}, ...] coordinates to scan/mine
this.minY = data.minY ?? -64;
this.maxY = data.maxY ?? 320;
this.oreTargets = []; // Known ore positions to mine
this.scanPositions = []; // Positions still needing scanning
this.remainingPositions = []; // Ore positions still to mine
this.blocksMined = 0;
this.oresExtracted = 0;
this.scannerType = null;
this.hasInitialized = false;
// If area not specified, generate from bounds
if (this.area.length === 0 && data.bounds) {
this._generateAreaFromBounds(data.bounds);
}
}
get name() {
return 'extracting';
}
get description() {
return `Extracting - ${this.oresExtracted} ores, ${this.remainingPositions.length} remaining`;
}
_generateAreaFromBounds(bounds) {
const { minX, minY, minZ, maxX, maxY, maxZ } = bounds;
for (let x = minX; x <= maxX; x++) {
for (let y = minY; y <= maxY; y++) {
for (let z = minZ; z <= maxZ; z++) {
this.area.push({ x, y, z });
}
}
}
}
async *act() {
console.log(`[${this.turtle.id}] Starting extraction (${this.area.length} area blocks)`);
// Detect scanner
this.scannerType = await this._detectScanner();
yield;
// Initialize: query DB for known ores in area, compute scan positions
if (!this.hasInitialized) {
await this._initializeScanPlan();
this.hasInitialized = true;
}
yield;
while (!this.cancelled) {
// Done if no more scan positions and no remaining ore positions
if (this.scanPositions.length === 0 && this.remainingPositions.length === 0) {
console.log(`[${this.turtle.id}] Extraction complete: ${this.oresExtracted} ores extracted`);
this.turtle.setState('idle');
return;
}
// Safety: check fuel
const fuel = await this.checkFuel();
if (fuel !== 'unlimited' && fuel < 500) {
const refueled = await this.tryRefuel();
if (!refueled) {
console.log(`[${this.turtle.id}] Low fuel, going home`);
this.turtle.setState('goHome', { reason: 'low_fuel', returnState: 'extracting', returnData: this.data });
return;
}
}
yield;
// Check inventory fullness
const isFull = await this.isInventoryFull();
if (isFull) {
console.log(`[${this.turtle.id}] Inventory full, going home to dump`);
this.turtle.setState('goHome', { reason: 'inventory_full', returnState: 'extracting', returnData: this.data });
return;
}
yield;
const pos = this.turtle.position;
if (!pos) {
await this._sleep(1000);
yield;
continue;
}
// Priority 1: Navigate to scan positions and scan
if (this.scanPositions.length > 0) {
const scanTarget = this._findClosest(this.scanPositions, pos);
if (scanTarget) {
// Navigate to scan position
const success = yield* this.navigateTo(scanTarget);
if (success) {
// Perform scan
const scannedBlocks = await this._performScan();
yield;
// Process results - find ores
if (scannedBlocks) {
for (const block of scannedBlocks) {
if (this._isOre(block.name)) {
this.remainingPositions.push({ x: block.x, y: block.y, z: block.z });
}
}
}
// Remove this scan position
const idx = this.scanPositions.findIndex(p => p.x === scanTarget.x && p.y === scanTarget.y && p.z === scanTarget.z);
if (idx >= 0) this.scanPositions.splice(idx, 1);
}
}
yield;
continue;
}
// Priority 2: Navigate to and mine ore positions
if (this.remainingPositions.length > 0) {
const oreTarget = this._findClosest(this.remainingPositions, pos);
if (oreTarget) {
// Navigate adjacent to ore
const success = yield* this.navigateTo(oreTarget);
yield;
if (success) {
// Mine the ore (we should be at or adjacent to it)
await this._mineAt(oreTarget);
this.oresExtracted++;
}
// Remove from remaining
const idx = this.remainingPositions.findIndex(p => p.x === oreTarget.x && p.y === oreTarget.y && p.z === oreTarget.z);
if (idx >= 0) this.remainingPositions.splice(idx, 1);
}
yield;
}
await this._sleep(200);
yield;
}
}
async _detectScanner() {
const result = await this.exec(`
local names = peripheral.getNames()
for _, name in ipairs(names) do
local types = {peripheral.getType(name)}
for _, t in ipairs(types) do
if t == "geoScanner" then return "geoScanner"
elseif t == "universal_scanner" then return "universal_scanner"
elseif t == "plethora:scanner" then return "plethora:scanner"
end
end
end
return nil
`);
return result || 'native';
}
async _initializeScanPlan() {
// Query the DB for known ores in the area
if (this.area.length > 0) {
const bounds = this._getBoundingBox();
try {
const knownOres = this.turtle.server.db.getBlocksWithNameLike(
bounds.minX, bounds.minY, bounds.minZ,
bounds.maxX, bounds.maxY, bounds.maxZ,
'%_ore'
);
for (const ore of knownOres) {
this.remainingPositions.push({ x: ore.x, y: ore.y, z: ore.z });
}
} catch (e) {
console.log(`[${this.turtle.id}] DB ore query failed:`, e.message);
}
}
// Compute scan positions (evenly spaced grid across area)
if (this.scannerType !== 'native' && this.area.length > 0) {
const bounds = this._getBoundingBox();
const scanSpacing = this.scannerType === 'plethora:scanner' ? 16 : 32;
for (let x = bounds.minX; x <= bounds.maxX; x += scanSpacing) {
for (let z = bounds.minZ; z <= bounds.maxZ; z += scanSpacing) {
const y = Math.floor((bounds.minY + bounds.maxY) / 2);
this.scanPositions.push({ x, y, z });
}
}
}
}
_getBoundingBox() {
let minX = Infinity, minY = Infinity, minZ = Infinity;
let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
for (const p of this.area) {
minX = Math.min(minX, p.x); maxX = Math.max(maxX, p.x);
minY = Math.min(minY, p.y); maxY = Math.max(maxY, p.y);
minZ = Math.min(minZ, p.z); maxZ = Math.max(maxZ, p.z);
}
return { minX, minY, minZ, maxX, maxY, maxZ };
}
_findClosest(positions, from) {
let closest = null;
let minDist = Infinity;
for (const pos of positions) {
const dist = Math.abs(pos.x - from.x) + Math.abs(pos.y - from.y) + Math.abs(pos.z - from.z);
if (dist < minDist) {
minDist = dist;
closest = pos;
}
}
return closest;
}
_isOre(name) {
return ORE_BLOCKS.has(name) || (name && name.endsWith('_ore'));
}
async _performScan() {
const pos = this.turtle.position;
if (!pos) return null;
let rawBlocks = null;
if (this.scannerType === 'geoScanner') {
rawBlocks = await this.exec(`
local scanner = peripheral.find("geoScanner")
if scanner then
local ok, blocks = pcall(scanner.scan, 16)
if ok then return blocks end
end
return nil
`);
} else if (this.scannerType === 'universal_scanner') {
rawBlocks = await this.exec(`
local scanner = peripheral.find("universal_scanner")
if scanner then
local ok, blocks = pcall(scanner.scan, "block", 16)
if ok then return blocks end
end
return nil
`);
} else if (this.scannerType === 'plethora:scanner') {
rawBlocks = await this.exec(`
local scanner = peripheral.find("plethora:scanner")
if scanner then
local ok, blocks = pcall(scanner.scan)
if ok then return blocks end
end
return nil
`);
}
if (!rawBlocks || !Array.isArray(rawBlocks)) return null;
// Convert relative positions to absolute
return rawBlocks
.filter(b => b && b.name && b.name !== 'minecraft:air')
.map(b => ({
x: pos.x + (b.x || 0),
y: pos.y + (b.y || 0),
z: pos.z + (b.z || 0),
name: b.name,
}));
}
async _mineAt(target) {
const pos = this.turtle.position;
if (!pos) return;
const dx = target.x - pos.x;
const dy = target.y - pos.y;
const dz = target.z - pos.z;
// We should be at or adjacent - mine in appropriate direction
if (dy === 1 || (dy === 0 && dx === 0 && dz === 0)) {
await this.exec('turtle.digUp()');
} else if (dy === -1) {
await this.exec('turtle.digDown()');
} else {
// Face the block and dig
let targetFacing;
if (dx === 1) targetFacing = 1;
else if (dx === -1) targetFacing = 3;
else if (dz === 1) targetFacing = 2;
else if (dz === -1) targetFacing = 0;
else targetFacing = this.turtle.facing;
await this.turnToFace(targetFacing);
await this.exec('turtle.dig()');
}
this.blocksMined++;
this.turtle.emit('blockMined', { blockType: 'ore' });
}
}

View File

@@ -0,0 +1,124 @@
/**
* FarmingState - Automated farming (plant, harvest, replant)
* Works with wheat, carrots, potatoes, beetroot, etc.
*/
import { BaseState } from './BaseState.js';
const CROP_SEEDS = {
'minecraft:wheat': 'minecraft:wheat_seeds',
'minecraft:carrots': 'minecraft:carrot',
'minecraft:potatoes': 'minecraft:potato',
'minecraft:beetroots': 'minecraft:beetroot_seeds',
};
export class FarmingState extends BaseState {
constructor(turtle, data = {}) {
super(turtle, data);
this.area = data.area || null; // {minX, minZ, maxX, maxZ, y}
this.harvestCount = 0;
}
get name() {
return 'farming';
}
get description() {
return `Farming - ${this.harvestCount} harvested`;
}
async *act() {
console.log(`[${this.turtle.id}] Starting farming operation`);
if (!this.area) {
console.log(`[${this.turtle.id}] No farming area defined`);
this.turtle.setState('idle');
return;
}
// Farm in a zigzag pattern over the area
const { minX, minZ, maxX, maxZ, y } = this.area;
let direction = 1; // 1 = positive Z, -1 = negative Z
for (let x = minX; x <= maxX; x++) {
const zStart = direction === 1 ? minZ : maxZ;
const zEnd = direction === 1 ? maxZ : minZ;
const zStep = direction;
for (let z = zStart; direction === 1 ? z <= zEnd : z >= zEnd; z += zStep) {
if (this.cancelled) return;
// Navigate to position above the crop
const target = { x, y: y + 1, z };
const navGen = this.navigateTo(target);
for await (const _ of navGen) {
if (this.cancelled) return;
yield;
}
// Check the block below
const blockBelow = await this.exec(`
local hasBlock, data = turtle.inspectDown()
if hasBlock then
return {name = data.name, state = data.state}
end
return nil
`);
if (blockBelow) {
// Check if crop is mature (age = 7 for most crops)
const isMature = blockBelow.state && blockBelow.state.age === 7;
if (isMature) {
// Harvest
await this.exec('turtle.digDown()');
this.harvestCount++;
// Replant
const seedName = CROP_SEEDS[blockBelow.name];
if (seedName) {
await this.exec(`
for slot = 1, 16 do
local item = turtle.getItemDetail(slot)
if item and item.name == "${seedName}" then
turtle.select(slot)
turtle.placeDown()
turtle.select(1)
return true
end
end
return false
`);
}
}
}
yield;
}
direction *= -1; // Zigzag
}
// Done farming one pass - check inventory
const isFull = await this.isInventoryFull();
if (isFull) {
this.turtle.setState('goHome', {
reason: 'inventory_full',
returnState: 'farming',
returnData: this.data
});
} else {
// Start another pass
console.log(`[${this.turtle.id}] Farm pass complete, ${this.harvestCount} harvested`);
yield* this.act(); // Loop
}
}
getRecoveryData() {
return {
...super.getRecoveryData(),
data: {
area: this.area,
},
};
}
}

View File

@@ -0,0 +1,89 @@
/**
* GoHomeState - Navigate the turtle back to its home position
* After arrival, optionally transitions to dump inventory or another state
*/
import { BaseState } from './BaseState.js';
export class GoHomeState extends BaseState {
constructor(turtle, data = {}) {
super(turtle, data);
this.reason = data.reason || 'manual'; // 'low_fuel', 'inventory_full', 'too_far', 'manual'
this.returnState = data.returnState || null; // State to return to after going home
this.returnData = data.returnData || {};
}
get name() {
return 'returning';
}
get description() {
return `Returning home (${this.reason})`;
}
async *act() {
const home = this.turtle.homePosition;
if (!home) {
console.log(`[${this.turtle.id}] No home position set, going idle`);
this.turtle.setState('idle');
return;
}
console.log(`[${this.turtle.id}] Going home: reason=${this.reason}`);
// Navigate to home using D* Lite
const navGen = this.navigateTo(home, { canMine: true });
for await (const _ of navGen) {
if (this.cancelled) return;
// Check fuel during return
const fuel = await this.checkFuel();
if (fuel !== 'unlimited' && fuel < 100) {
// Emergency refuel
await this.tryRefuel();
}
yield;
}
// Arrived home
console.log(`[${this.turtle.id}] Arrived home`);
// If we came home to dump inventory, do so
if (this.reason === 'inventory_full') {
this.turtle.setState('dumpInventory', {
returnState: this.returnState,
returnData: this.returnData,
});
return;
}
// If we came home to refuel, try refueling
if (this.reason === 'low_fuel') {
this.turtle.setState('refueling', {
returnState: this.returnState,
returnData: this.returnData,
});
return;
}
// If there's a return state, go to it
if (this.returnState) {
this.turtle.setState(this.returnState, this.returnData);
return;
}
// Default: go idle
this.turtle.setState('idle');
}
getRecoveryData() {
return {
...super.getRecoveryData(),
data: {
reason: this.reason,
returnState: this.returnState,
returnData: this.returnData,
},
};
}
}

View File

@@ -0,0 +1,32 @@
/**
* IdleState - Turtle is idle, waiting for commands
*/
import { BaseState } from './BaseState.js';
export class IdleState extends BaseState {
get name() {
return 'idle';
}
get description() {
return 'Idle - waiting for commands';
}
async *act() {
// Periodic fuel check while idle (no scanning — avoids rotating turtle)
// Errors are caught here so they don't bubble up and trigger
// the _runStateLoop retry mechanism (which would create an
// infinite idle→timeout→idle loop).
while (!this.cancelled) {
try {
await this.checkFuel();
} catch (e) {
// Fuel check failed (likely command timeout) — not critical in idle
// Just wait longer before trying again
await this._sleep(30000);
}
yield;
await this._sleep(10000);
}
}
}

View File

@@ -0,0 +1,232 @@
/**
* MiningState - Autonomous mining with intelligent exploration
* Mines ores, explores caves, manages inventory
*/
import { BaseState } from './BaseState.js';
const VALUABLE_BLOCKS = new Set([
'minecraft:coal_ore', 'minecraft:iron_ore', 'minecraft:gold_ore',
'minecraft:diamond_ore', 'minecraft:emerald_ore', 'minecraft:redstone_ore',
'minecraft:lapis_ore', 'minecraft:copper_ore',
'minecraft:deepslate_coal_ore', 'minecraft:deepslate_iron_ore',
'minecraft:deepslate_gold_ore', 'minecraft:deepslate_diamond_ore',
'minecraft:deepslate_emerald_ore', 'minecraft:deepslate_redstone_ore',
'minecraft:deepslate_lapis_ore', 'minecraft:deepslate_copper_ore',
'minecraft:nether_gold_ore', 'minecraft:nether_quartz_ore',
'minecraft:ancient_debris',
]);
export class MiningState extends BaseState {
constructor(turtle, data = {}) {
super(turtle, data);
this.maxDistance = data.maxDistance || 200;
this.minFuel = data.minFuel || 500;
this.blocksMined = 0;
this.oresFound = 0;
this.stuckCounter = 0;
this.visitedPositions = new Set();
this.miningArea = data.miningArea || null; // Optional bounded area
}
get name() {
return 'mining';
}
get description() {
return `Mining - ${this.blocksMined} mined, ${this.oresFound} ores found`;
}
async *act() {
console.log(`[${this.turtle.id}] Starting mining operation`);
while (!this.cancelled) {
try {
// Safety checks
const fuel = await this.checkFuel();
if (fuel !== 'unlimited' && fuel < this.minFuel) {
console.log(`[${this.turtle.id}] Low fuel (${fuel}), attempting refuel`);
const refueled = await this.tryRefuel();
if (!refueled) {
console.log(`[${this.turtle.id}] Cannot refuel, going home`);
this.turtle.setState('goHome', { reason: 'low_fuel' });
return;
}
}
// Check inventory
const isFull = await this.isInventoryFull();
if (isFull) {
console.log(`[${this.turtle.id}] Inventory full, going home to dump`);
this.turtle.setState('goHome', { reason: 'inventory_full', returnState: 'mining', returnData: this.data });
return;
}
// Check distance from home
if (this._isTooFar()) {
console.log(`[${this.turtle.id}] Too far from home, returning`);
this.turtle.setState('goHome', { reason: 'too_far' });
return;
}
// Mark current position as visited
const pos = this.turtle.position;
if (pos) {
this.visitedPositions.add(`${pos.x},${pos.y},${pos.z}`);
}
// Scan surroundings for ores
const scanResult = await this.scanSurroundings();
// Mine any ores found in surroundings
yield* this._mineAdjacentOres();
// Explore step - try to find new areas
yield* this._exploreStep();
} catch (error) {
const isTimeout = error.message?.includes('timed out');
if (isTimeout) {
console.warn(`[${this.turtle.id}] Mining exec timeout, will retry next iteration`);
await this._sleep(3000);
} else {
throw error;
}
}
yield;
await this._sleep(200);
}
}
/**
* Mine ores in adjacent positions
*/
async *_mineAdjacentOres() {
// Check all 6 directions for ores
const directions = [
{ check: 'turtle.inspect()', dig: 'turtle.dig()', dir: 'forward' },
{ check: 'turtle.inspectUp()', dig: 'turtle.digUp()', dir: 'up' },
{ check: 'turtle.inspectDown()', dig: 'turtle.digDown()', dir: 'down' },
];
for (const { check, dig, dir } of directions) {
if (this.cancelled) return;
const result = await this.exec(`
local hasBlock, data = ${check}
if hasBlock then
return {name = data.name, metadata = data.metadata or 0}
end
return nil
`);
if (result && VALUABLE_BLOCKS.has(result.name)) {
console.log(`[${this.turtle.id}] Found ore: ${result.name} (${dir})`);
await this.exec(dig);
this.blocksMined++;
this.oresFound++;
// Report mined block to server
this.turtle.emit('blockMined', { blockType: result.name, direction: dir });
yield;
}
}
}
/**
* Explore step - move to unvisited positions, favor horizontal movement
*/
async *_exploreStep() {
const pos = this.turtle.position;
if (!pos) return;
const r = Math.random() * 100;
// 75%: horizontal exploration
if (r < 75) {
// Try to find an unvisited forward direction
let moved = false;
// Try current facing first
const forwardPos = this.turtle.getBlockPositionInDirection('forward');
if (forwardPos && !this.visitedPositions.has(`${forwardPos.x},${forwardPos.y},${forwardPos.z}`)) {
const success = await this.moveForward(true);
if (success) {
this.stuckCounter = 0;
moved = true;
}
}
// If forward is visited or blocked, try other directions
if (!moved) {
for (let i = 0; i < 4; i++) {
await this.exec('turtle.turnRight()');
this.turtle.facing = (this.turtle.facing + 1) % 4;
const newForwardPos = this.turtle.getBlockPositionInDirection('forward');
if (newForwardPos && !this.visitedPositions.has(`${newForwardPos.x},${newForwardPos.y},${newForwardPos.z}`)) {
const success = await this.moveForward(true);
if (success) {
this.stuckCounter = 0;
moved = true;
break;
}
}
}
}
if (!moved) {
// All directions visited, just move forward
const success = await this.moveForward(true);
if (!success) {
this.stuckCounter++;
await this.exec('turtle.turnRight()');
this.turtle.facing = (this.turtle.facing + 1) % 4;
}
}
// 15%: go down (if not too deep)
} else if (r < 90 && pos.y > -50) {
const downPos = { x: pos.x, y: pos.y - 1, z: pos.z };
if (!this.visitedPositions.has(`${downPos.x},${downPos.y},${downPos.z}`)) {
await this.moveDown(true);
}
// 10%: go up
} else {
const upPos = { x: pos.x, y: pos.y + 1, z: pos.z };
if (!this.visitedPositions.has(`${upPos.x},${upPos.y},${upPos.z}`)) {
await this.moveUp(true);
}
}
// Handle being stuck
if (this.stuckCounter > 5) {
console.log(`[${this.turtle.id}] Stuck! Trying to escape`);
await this.moveUp(true);
this.stuckCounter = 0;
}
yield;
}
_isTooFar() {
const pos = this.turtle.position;
const home = this.turtle.homePosition;
if (!pos || !home) return false;
const dist = Math.abs(pos.x - home.x) + Math.abs(pos.y - home.y) + Math.abs(pos.z - home.z);
return dist > this.maxDistance;
}
getRecoveryData() {
return {
...super.getRecoveryData(),
data: {
maxDistance: this.maxDistance,
minFuel: this.minFuel,
miningArea: this.miningArea,
},
};
}
}

View File

@@ -0,0 +1,78 @@
/**
* MovingState - Navigate the turtle to a target position using D* Lite
*/
import { BaseState } from './BaseState.js';
export class MovingState extends BaseState {
constructor(turtle, data = {}) {
super(turtle, data);
this.target = data.target; // {x, y, z}
this.onArrival = data.onArrival || null; // Callback/next state when arrived
this.canMine = data.canMine !== false;
}
get name() {
return 'moving';
}
get description() {
const pos = this.target;
if (pos) {
return `Moving to (${pos.x}, ${pos.y}, ${pos.z})`;
}
return 'Moving to target';
}
async *act() {
if (!this.target) {
console.log(`[${this.turtle.id}] MovingState: No target set`);
this.turtle.setState('idle');
return;
}
console.log(`[${this.turtle.id}] Moving to ${this.target.x},${this.target.y},${this.target.z}`);
// Use the navigateTo generator from BaseState
const nav = this.navigateTo(this.target, { canMine: this.canMine });
for await (const _ of nav) {
if (this.cancelled) return;
yield;
}
// Check if we actually arrived
const pos = this.turtle.position;
if (pos) {
const { Point } = await import('../pathfinding/index.js');
const current = Point.from(pos);
const goal = Point.from(this.target);
if (current.distanceTo(goal) <= 1) {
console.log(`[${this.turtle.id}] Arrived at destination`);
if (this.onArrival === 'idle') {
this.turtle.setState('idle');
} else if (this.onArrival === 'mine') {
this.turtle.setState('mining', this.data);
} else {
this.turtle.setState('idle');
}
return;
}
}
console.log(`[${this.turtle.id}] Failed to reach destination`);
this.turtle.setState('idle');
}
getRecoveryData() {
return {
...super.getRecoveryData(),
data: {
target: this.target,
onArrival: this.onArrival,
canMine: this.canMine,
},
};
}
}

View File

@@ -0,0 +1,126 @@
/**
* RefuelingState - Refuel the turtle from inventory or nearby containers
* Prioritizes fuel sources: lava buckets > coal blocks > coal > logs > sticks
*/
import { BaseState } from './BaseState.js';
const FUEL_PRIORITY = [
'minecraft:lava_bucket',
'minecraft:coal_block',
'minecraft:charcoal',
'minecraft:coal',
'minecraft:oak_log', 'minecraft:birch_log', 'minecraft:spruce_log',
'minecraft:dark_oak_log', 'minecraft:jungle_log', 'minecraft:acacia_log',
'minecraft:mangrove_log', 'minecraft:cherry_log',
'minecraft:oak_planks', 'minecraft:birch_planks', 'minecraft:spruce_planks',
'minecraft:stick',
];
export class RefuelingState extends BaseState {
constructor(turtle, data = {}) {
super(turtle, data);
this.returnState = data.returnState || null;
this.returnData = data.returnData || {};
this.targetFuel = data.targetFuel || 5000;
}
get name() {
return 'refueling';
}
get description() {
return 'Refueling turtle';
}
async *act() {
console.log(`[${this.turtle.id}] Starting refueling, target: ${this.targetFuel}`);
// First try to refuel from own inventory using priority order
const refueled = await this.exec(`
local fuelPriority = ${this._luaFuelTable()}
local targetFuel = ${this.targetFuel}
local refueled = false
for _, fuelName in ipairs(fuelPriority) do
for slot = 1, 16 do
local item = turtle.getItemDetail(slot)
if item and item.name == fuelName then
turtle.select(slot)
turtle.refuel()
refueled = true
if turtle.getFuelLevel() >= targetFuel then
turtle.select(1)
return {success = true, fuel = turtle.getFuelLevel()}
end
end
end
end
turtle.select(1)
return {success = refueled, fuel = turtle.getFuelLevel()}
`);
if (refueled) {
this.turtle.fuel = refueled.fuel;
console.log(`[${this.turtle.id}] Refueled to ${refueled.fuel}`);
}
yield;
// Try to refuel from nearby containers (chests, barrels)
if (!refueled || !refueled.success || refueled.fuel < this.targetFuel) {
console.log(`[${this.turtle.id}] Trying nearby containers for fuel`);
const containerResult = await this.exec(`
local directions = {"front", "top", "bottom"}
local suckFns = {turtle.suck, turtle.suckUp, turtle.suckDown}
for i, dir in ipairs(directions) do
-- Try to suck items from container
for attempt = 1, 16 do
if suckFns[i]() then
-- Check if it's fuel
local slot = turtle.getSelectedSlot()
if turtle.refuel(0) then
turtle.refuel()
end
else
break
end
end
end
return {fuel = turtle.getFuelLevel()}
`);
if (containerResult) {
this.turtle.fuel = containerResult.fuel;
}
}
yield;
// Transition to next state
if (this.returnState) {
this.turtle.setState(this.returnState, this.returnData);
} else {
this.turtle.setState('idle');
}
}
_luaFuelTable() {
const items = FUEL_PRIORITY.map(name => `"${name}"`).join(', ');
return `{${items}}`;
}
getRecoveryData() {
return {
...super.getRecoveryData(),
data: {
returnState: this.returnState,
returnData: this.returnData,
targetFuel: this.targetFuel,
},
};
}
}

184
server/states/ScanState.js Normal file
View File

@@ -0,0 +1,184 @@
/**
* ScanState - Systematic scanning of surroundings using peripheral scanners
*
* Supports multiple scanner types:
* - geoScanner (Advanced Peripherals)
* - universal_scanner
* - plethora:scanner (Plethora mod)
* - Native turtle.inspect() fallback (6-directional scan)
*/
import { BaseState } from './BaseState.js';
export class ScanState extends BaseState {
constructor(turtle, data = {}) {
super(turtle, data);
this.scanRadius = data.scanRadius || 16;
this.blocksScanned = 0;
this.scannerType = null;
}
get name() {
return 'scanning';
}
get description() {
return `Scanning (${this.scannerType || 'detecting'}) - ${this.blocksScanned} blocks`;
}
async *act() {
console.log(`[${this.turtle.id}] Starting scan`);
// Detect available scanner peripheral
this.scannerType = await this._detectScanner();
yield;
if (this.scannerType === 'geoScanner') {
yield* this._geoScan();
} else if (this.scannerType === 'universal_scanner') {
yield* this._universalScan();
} else if (this.scannerType === 'plethora:scanner') {
yield* this._plethuraScan();
} else {
// Fallback to native inspect-based scanning
yield* this._nativeScan();
}
console.log(`[${this.turtle.id}] Scan complete: ${this.blocksScanned} blocks`);
this.turtle.setState('idle');
}
async _detectScanner() {
// Check for geoScanner
const geoResult = await this.exec(`
local names = peripheral.getNames()
for _, name in ipairs(names) do
local types = {peripheral.getType(name)}
for _, t in ipairs(types) do
if t == "geoScanner" then return "geoScanner" end
end
end
return nil
`);
if (geoResult) return geoResult;
// Check for universal_scanner
const uniResult = await this.exec(`
local names = peripheral.getNames()
for _, name in ipairs(names) do
local types = {peripheral.getType(name)}
for _, t in ipairs(types) do
if t == "universal_scanner" then return "universal_scanner" end
end
end
return nil
`);
if (uniResult) return uniResult;
// Check for plethora scanner
const plResult = await this.exec(`
local names = peripheral.getNames()
for _, name in ipairs(names) do
local types = {peripheral.getType(name)}
for _, t in ipairs(types) do
if t == "plethora:scanner" then return "plethora:scanner" end
end
end
return nil
`);
if (plResult) return plResult;
return 'native';
}
async *_geoScan() {
// Wait for cooldown
yield;
await this._sleep(500);
const result = await this.exec(`
local scanner = peripheral.find("geoScanner")
if not scanner then return nil end
local ok, blocks = pcall(scanner.scan, ${this.scanRadius})
if ok then return blocks end
return nil
`);
if (result && Array.isArray(result)) {
this._processScannedBlocks(result);
}
yield;
}
async *_universalScan() {
yield;
await this._sleep(500);
const result = await this.exec(`
local scanner = peripheral.find("universal_scanner")
if not scanner then return nil end
local ok, blocks = pcall(scanner.scan, "block", ${this.scanRadius})
if ok then return blocks end
return nil
`);
if (result && Array.isArray(result)) {
this._processScannedBlocks(result);
}
yield;
}
async *_plethuraScan() {
yield;
const result = await this.exec(`
local scanner = peripheral.find("plethora:scanner")
if not scanner then return nil end
local ok, blocks = pcall(scanner.scan)
if ok then return blocks end
return nil
`);
if (result && Array.isArray(result)) {
this._processScannedBlocks(result);
}
yield;
}
async *_nativeScan() {
// Fallback: scan all 6 directions using turtle.inspect
const scanResult = await this.scanSurroundings();
if (scanResult) {
this.blocksScanned += Object.keys(scanResult).length;
}
yield;
}
_processScannedBlocks(blocks) {
const pos = this.turtle.position;
if (!pos) return;
const discoveredBlocks = [];
for (const block of blocks) {
if (!block || !block.name) continue;
if (block.name === 'minecraft:air') continue;
if (block.x === 0 && block.y === 0 && block.z === 0) continue;
discoveredBlocks.push({
x: pos.x + (block.x || 0),
y: pos.y + (block.y || 0),
z: pos.z + (block.z || 0),
name: block.name,
metadata: block.metadata || 0,
state: block.state || {},
tags: block.tags || {},
discoveredBy: this.turtle.id,
});
}
this.blocksScanned = discoveredBlocks.length;
if (discoveredBlocks.length > 0) {
this.turtle.emit('blocksDiscovered', discoveredBlocks);
}
}
}

13
server/states/index.js Normal file
View File

@@ -0,0 +1,13 @@
export { BaseState } from './BaseState.js';
export { IdleState } from './IdleState.js';
export { MovingState } from './MovingState.js';
export { MiningState } from './MiningState.js';
export { ExploringState } from './ExploringState.js';
export { GoHomeState } from './GoHomeState.js';
export { RefuelingState } from './RefuelingState.js';
export { DumpInventoryState } from './DumpInventoryState.js';
export { FarmingState } from './FarmingState.js';
export { ScanState } from './ScanState.js';
export { ExtractionState } from './ExtractionState.js';
export { BuildingState } from './BuildingState.js';
export { AutocraftState } from './AutocraftState.js';

56
startup_pocket.lua Normal file
View File

@@ -0,0 +1,56 @@
-- Pocket Control Startup Script
-- Automatically downloads and runs the latest pocketcontrol.lua from git
local SCRIPT_URL = "https://git.spatulaa.com/MayaTheShy/remoteturtle/raw/branch/master/pocketcontrol.lua"
local SCRIPT_NAME = "pocketcontrol.lua"
print("========================================")
print(" POCKET CONTROL AUTO-UPDATER")
print("========================================")
print("")
-- Remove old version
if fs.exists(SCRIPT_NAME) then
print("Removing old version...")
fs.delete(SCRIPT_NAME)
end
-- Download latest version
print("Downloading latest version...")
print("URL: " .. SCRIPT_URL)
local response = http.get(SCRIPT_URL)
if response then
local content = response.readAll()
response.close()
-- Save to file
local file = fs.open(SCRIPT_NAME, "w")
file.write(content)
file.close()
print("✓ Download successful!")
print("")
print("Starting pocket control...")
sleep(1)
-- Run the script
shell.run(SCRIPT_NAME)
else
print("✗ Download failed!")
print("Please check:")
print(" - Internet connection")
print(" - URL is correct")
print(" - HTTP API is enabled")
print("")
print("Falling back to existing script...")
-- Try to run existing version if download fails
if fs.exists(SCRIPT_NAME) then
shell.run(SCRIPT_NAME)
else
print("No existing script found!")
print("Please download manually:")
print(" wget " .. SCRIPT_URL .. " " .. SCRIPT_NAME)
end
end

69
startup_turtle.lua Normal file
View File

@@ -0,0 +1,69 @@
-- Turtle Startup Script
-- Automatically downloads and runs the latest turtle.lua from git
local SCRIPT_URL = "https://git.spatulaa.com/MayaTheShy/remoteturtle/raw/branch/master/turtle.lua"
local SCRIPT_NAME = "turtle.lua"
print("========================================")
print(" TURTLE AUTO-UPDATER")
print("========================================")
print("")
-- Remove old version
if fs.exists(SCRIPT_NAME) then
print("Removing old version...")
fs.delete(SCRIPT_NAME)
end
-- Download latest version
print("Downloading latest version...")
print("URL: " .. SCRIPT_URL)
local response = http.get(SCRIPT_URL)
if response then
local content = response.readAll()
response.close()
-- Save to file
local file = fs.open(SCRIPT_NAME, "w")
file.write(content)
file.close()
print("✓ Download successful!")
print("")
print("Starting turtle program...")
sleep(1)
-- Run the script with error handling
local success, error = pcall(function()
shell.run(SCRIPT_NAME)
end)
if not success then
print("")
print("========================================")
print(" ERROR OCCURRED!")
print("========================================")
print(error)
print("")
print("Press any key to see error details...")
os.pullEvent("key")
end
else
print("✗ Download failed!")
print("Please check:")
print(" - Internet connection")
print(" - URL is correct")
print(" - HTTP API is enabled")
print("")
print("Falling back to existing script...")
-- Try to run existing version if download fails
if fs.exists(SCRIPT_NAME) then
shell.run(SCRIPT_NAME)
else
print("No existing script found!")
print("Please download manually:")
print(" wget " .. SCRIPT_URL .. " " .. SCRIPT_NAME)
end
end

56
startup_webbridge.lua Normal file
View File

@@ -0,0 +1,56 @@
-- Webbridge Startup Script
-- Automatically downloads and runs the latest webbridge.lua from git
local SCRIPT_URL = "https://git.spatulaa.com/MayaTheShy/remoteturtle/raw/branch/master/webbridge.lua"
local SCRIPT_NAME = "webbridge.lua"
print("========================================")
print(" WEBBRIDGE AUTO-UPDATER")
print("========================================")
print("")
-- Remove old version
if fs.exists(SCRIPT_NAME) then
print("Removing old version...")
fs.delete(SCRIPT_NAME)
end
-- Download latest version
print("Downloading latest version...")
print("URL: " .. SCRIPT_URL)
local response = http.get(SCRIPT_URL)
if response then
local content = response.readAll()
response.close()
-- Save to file
local file = fs.open(SCRIPT_NAME, "w")
file.write(content)
file.close()
print("✓ Download successful!")
print("")
print("Starting webbridge program...")
sleep(1)
-- Run the script
shell.run(SCRIPT_NAME)
else
print("✗ Download failed!")
print("Please check:")
print(" - Internet connection")
print(" - URL is correct")
print(" - HTTP API is enabled")
print("")
print("Falling back to existing script...")
-- Try to run existing version if download fails
if fs.exists(SCRIPT_NAME) then
shell.run(SCRIPT_NAME)
else
print("No existing script found!")
print("Please download manually:")
print(" wget " .. SCRIPT_URL .. " " .. SCRIPT_NAME)
end
end

File diff suppressed because it is too large Load Diff

View File

@@ -1,93 +1,536 @@
-- Web Bridge for Turtle System
-- This script forwards turtle status updates to the web server
-- Place this on a computer connected to the wireless network
-- Web Bridge v2 (WebSocket Protocol)
-- Connects to server via WebSocket for instant bidirectional communication.
-- Forwards turtle modem messages to server and server commands to turtles.
-- Falls back to HTTP polling if WebSocket is unavailable.
--
-- Uses cc-platform-core for shared infrastructure (config, HTTP, modem, channels).
local SERVER_URL = "http://localhost:3001" -- Change to your server address
local CHANNEL_RECEIVE = 101
local STATUS_CHANNEL = 102
local WebBridge = require('platform.webbridge')
local Channels = require('platform.channels')
-------------------------------------------------
-- Configuration (via platform)
-------------------------------------------------
local _baseDir = fs.getDir(shell.getRunningProgram())
local config, configSource = WebBridge.loadConfig({
serverUrl = "http://localhost:4200",
wsUrl = nil, -- derived from serverUrl if not set
}, {
fs.combine(_baseDir, ".webbridge_config"),
})
local SERVER_URL = config.serverUrl
local WS_URL = config.wsUrl or SERVER_URL:gsub("^http", "ws") .. "/ws/bridge"
if configSource then
print("[CONFIG] Loaded from " .. configSource)
end
-- Channels from platform registry
local COMMAND_CHANNEL = Channels.get('remoteturtle.command')
local CHANNEL_RECEIVE = Channels.get('remoteturtle.response')
local STATUS_CHANNEL = Channels.get('remoteturtle.status')
local POCKET_CHANNEL = Channels.get('remoteturtle.pocket')
-------------------------------------------------
-- Peripherals
-------------------------------------------------
local modem, modemSide = WebBridge.findModem(true) -- prefer wireless
local monitor = peripheral.find("monitor")
local modem = peripheral.find("modem")
if not modem then
error("No wireless modem found!")
end
modem.open(CHANNEL_RECEIVE)
modem.open(STATUS_CHANNEL)
local hasMonitor = monitor ~= nil
if hasMonitor then
monitor.setTextScale(0.5)
end
print("Web Bridge Started")
print("Listening for turtle updates...")
print("Server: " .. SERVER_URL)
-- Open channels via platform (respects dual-mode migration)
WebBridge.openChannels(modem, {
'remoteturtle.response',
'remoteturtle.status',
'remoteturtle.pocket',
})
-- Track turtles to forward updates
-- Track turtles and stats
local turtles = {}
local stats = {
messagesReceived = 0,
commandsSent = 0,
serverUpdates = 0,
errors = 0,
startTime = os.epoch("utc")
}
local activityLog = {}
local wsHandle = nil
local wsConnected = false
-- Function to send data to web server
local function sendToServer(data)
local jsonData = textutils.serializeJSON(data)
local response = http.post(
SERVER_URL .. "/api/turtle/update",
jsonData,
{["Content-Type"] = "application/json"}
)
if response then
response.close()
return true
-- ========== Dashboard Drawing ==========
local function centerText(y, text, fg, bg)
if not hasMonitor then return end
local w = monitor.getSize()
monitor.setCursorPos(math.floor((w - #text) / 2) + 1, y)
monitor.setTextColor(fg or colors.white)
monitor.setBackgroundColor(bg or colors.black)
monitor.write(text)
end
local function getStatusColor(turtle)
if not turtle.lastSeen then return colors.red end
local timeSince = os.epoch("utc") - turtle.lastSeen
if timeSince < 10000 then return colors.lime
elseif timeSince < 30000 then return colors.yellow
else return colors.red end
end
local function formatTime(ms)
local seconds = math.floor(ms / 1000)
local minutes = math.floor(seconds / 60)
local hours = math.floor(minutes / 60)
if hours > 0 then return string.format("%dh %dm", hours, minutes % 60)
elseif minutes > 0 then return string.format("%dm %ds", minutes, seconds % 60)
else return string.format("%ds", seconds) end
end
local function drawDashboard()
if not hasMonitor then return end
local w, h = monitor.getSize()
monitor.setBackgroundColor(colors.black)
monitor.clear()
-- Count turtles
local turtleCount = 0
for _ in pairs(turtles) do turtleCount = turtleCount + 1 end
-- Header
local connIcon = wsConnected and "WS" or "HTTP"
monitor.setBackgroundColor(colors.blue)
monitor.setCursorPos(1, 1)
monitor.clearLine()
centerText(1, "TURTLE BRIDGE [" .. connIcon .. "] - " .. turtleCount .. " ONLINE", colors.white, colors.blue)
-- Status line
monitor.setBackgroundColor(colors.gray)
monitor.setCursorPos(1, 2)
monitor.clearLine()
monitor.setTextColor(colors.white)
monitor.setCursorPos(2, 2)
monitor.write("MSG:" .. stats.messagesReceived .. " CMD:" .. stats.commandsSent .. " ERR:" .. stats.errors)
-- Turtle list
monitor.setBackgroundColor(colors.black)
local startY = 4
if turtleCount == 0 then
monitor.setTextColor(colors.gray)
monitor.setCursorPos(2, startY)
monitor.write("No turtles connected. Waiting...")
else
return false
end
end
-- Function to poll for commands from server
local function pollCommands(turtleID)
local response = http.get(SERVER_URL .. "/api/turtle/" .. turtleID .. "/commands")
if response then
local content = response.readAll()
response.close()
local data = textutils.unserializeJSON(content)
if data and data.commands then
return data.commands
end
end
return {}
end
-- Main loop
while true do
local event, side, channel, replyChannel, message, distance = os.pullEvent("modem_message")
if channel == STATUS_CHANNEL and type(message) == "table" then
-- Status update from turtle
print("Status from Turtle " .. (message.turtleID or "?"))
-- Update local cache
turtles[message.turtleID] = message
-- Forward to web server
local success = sendToServer(message)
if success then
print(" -> Sent to server")
-- Check for commands from server
local commands = pollCommands(message.turtleID)
-- Forward commands back to turtle
for _, cmd in ipairs(commands) do
print(" -> Command for Turtle " .. message.turtleID .. ": " .. cmd.command)
modem.transmit(100, 101, {
command = cmd.command,
param = cmd.param,
target = message.turtleID
})
local y = startY
for id, turtle in pairs(turtles) do
if y >= h - 5 then break end
local statusColor = getStatusColor(turtle)
local timeSince = 0
if turtle.lastSeen then
timeSince = math.floor((os.epoch("utc") - turtle.lastSeen) / 1000)
end
else
print(" -> Failed to send to server")
monitor.setCursorPos(2, y)
monitor.setTextColor(statusColor)
monitor.write("\7")
monitor.setTextColor(colors.white)
monitor.write(string.format(" T#%-3d", id))
monitor.setTextColor(colors.lightGray)
local tState = (turtle.mode or "IDLE"):upper()
if #tState > 10 then tState = tState:sub(1, 10) end
monitor.write(" " .. tState)
if turtle.position then
monitor.setTextColor(colors.gray)
monitor.write(string.format(" [%d,%d,%d]", turtle.position.x or 0, turtle.position.y or 0, turtle.position.z or 0))
end
monitor.setTextColor(colors.gray)
monitor.write(string.format(" %ds", timeSince))
y = y + 1
end
end
sleep(0.1) -- Small delay to prevent overwhelming the system
-- Activity log
local logY = h - 4
monitor.setBackgroundColor(colors.gray)
monitor.setCursorPos(1, logY)
monitor.clearLine()
monitor.setTextColor(colors.yellow)
monitor.setCursorPos(2, logY)
monitor.write("ACTIVITY LOG")
logY = logY + 1
monitor.setBackgroundColor(colors.black)
for i = 1, math.min(3, #activityLog) do
local entry = activityLog[i]
monitor.setCursorPos(1, logY)
monitor.clearLine()
monitor.setTextColor(colors.gray)
monitor.write(os.date("%H:%M:%S", (entry.time or 0) / 1000) .. " ")
monitor.setTextColor(entry.color or colors.white)
local maxWidth = w - 10
local text = entry.text
if #text > maxWidth then text = text:sub(1, maxWidth - 3) .. "..." end
monitor.write(text)
logY = logY + 1
end
end
local function addLog(text, color)
table.insert(activityLog, 1, { text = text, color = color or colors.white, time = os.epoch("utc") })
if #activityLog > 35 then table.remove(activityLog) end
if not hasMonitor then print(text) end
end
-- ========== WebSocket Send Helper ==========
local function wsSend(data)
if wsHandle and wsConnected then
local ok = pcall(function()
wsHandle.send(textutils.serializeJSON(data))
end)
return ok
end
return false
end
-- ========== HTTP Helpers (via platform) ==========
local function httpPost(path, data)
local result = WebBridge.httpPost(SERVER_URL .. path, data)
if result then
local ok, parsed = pcall(textutils.unserialiseJSON, result)
if ok then return parsed end
end
return nil
end
local function httpGet(path)
local result = WebBridge.httpGet(SERVER_URL .. path)
if result then
local ok, parsed = pcall(textutils.unserialiseJSON, result)
if ok then return parsed end
end
return nil
end
-- ========== Forward Turtle Modem Message to Server ==========
local function forwardToServer(message)
if not message or type(message) ~= "table" then return end
local msgType = message.type
local turtleID = message.turtleID
if msgType == "status" then
turtles[turtleID] = message
turtles[turtleID].lastSeen = os.epoch("utc")
addLog("T#" .. turtleID .. " status", colors.lightBlue)
if wsSend(message) then
stats.serverUpdates = stats.serverUpdates + 1
else
if httpPost("/api/turtle/update", message) then
stats.serverUpdates = stats.serverUpdates + 1
else
stats.errors = stats.errors + 1
addLog(" -> Server error", colors.red)
end
end
elseif msgType == "eval_response" then
addLog("Eval resp T#" .. turtleID .. " " .. (message.uuid or "?"):sub(1, 8), colors.cyan)
if not wsSend({ type = "eval_response", turtleID = turtleID, uuid = message.uuid, result = message.result, error = message.error }) then
local result = httpPost("/api/turtle/eval-response", { turtleID = turtleID, uuid = message.uuid, result = message.result, error = message.error })
if not result then
addLog(" -> Eval resp FAILED to send!", colors.red)
stats.errors = stats.errors + 1
end
end
elseif msgType == "request_home" then
addLog("T#" .. turtleID .. " requesting home", colors.cyan)
if wsConnected then
wsSend({ type = "request_home", turtleID = turtleID })
else
local result = httpGet("/api/turtle/" .. turtleID .. "/home")
modem.transmit(COMMAND_CHANNEL, CHANNEL_RECEIVE, {
type = "home_position",
turtleID = turtleID,
homePosition = result and result.homePosition or nil
})
end
elseif msgType == "set_home" then
addLog("T#" .. turtleID .. " setting home", colors.cyan)
if wsConnected then
wsSend({ type = "set_home", turtleID = turtleID, position = message.position })
else
local result = httpPost("/api/turtle/" .. turtleID .. "/home", { position = message.position })
modem.transmit(COMMAND_CHANNEL, CHANNEL_RECEIVE, {
type = "home_set_confirm",
turtleID = turtleID,
homePosition = (result and result.success) and result.homePosition or message.position
})
end
elseif msgType == "blocks_discovered" then
local blocks = message.blocks or {}
if #blocks > 0 then
addLog("T#" .. turtleID .. " discovered " .. #blocks .. " blocks", colors.cyan)
if not wsSend({ type = "blocks_discovered", turtleID = turtleID, blocks = blocks }) then
for _, block in ipairs(blocks) do
httpPost("/api/world/blocks", { x = block.x, y = block.y, z = block.z, blockName = block.name, metadata = block.metadata or 0, discoveredBy = turtleID })
end
end
end
elseif msgType == "inventory_update" then
addLog("T#" .. turtleID .. " inventory", colors.cyan)
if not wsSend({ type = "event", turtleID = turtleID, eventType = "INVENTORY_UPDATE", message = message.inventory }) then
httpPost("/api/turtle/event", { turtleID = turtleID, type = "INVENTORY_UPDATE", message = message.inventory })
end
elseif msgType == "peripheral_attached" then
addLog("T#" .. turtleID .. " peripheral attached", colors.cyan)
if not wsSend({ type = "event", turtleID = turtleID, eventType = "PERIPHERAL_ATTACHED", message = message.peripherals }) then
httpPost("/api/turtle/event", { turtleID = turtleID, type = "PERIPHERAL_ATTACHED", message = message.peripherals })
end
elseif msgType == "peripheral_detached" then
addLog("T#" .. turtleID .. " peripheral detached: " .. (message.side or "?"), colors.cyan)
if not wsSend({ type = "event", turtleID = turtleID, eventType = "PERIPHERAL_DETACHED", message = message.side }) then
httpPost("/api/turtle/event", { turtleID = turtleID, type = "PERIPHERAL_DETACHED", message = message.side })
end
end
end
-- ========== Handle Server Commands (from WS or HTTP poll) ==========
local function handleServerCommand(data)
if data.type == "command" then
local turtleID = data.turtleID
local cmd = data.command
if cmd and cmd.type == "eval" then
addLog("EVAL -> T#" .. turtleID .. " " .. (cmd.uuid or "?"):sub(1, 8), colors.yellow)
stats.commandsSent = stats.commandsSent + 1
local commandPacket = {
type = "eval",
uuid = cmd.uuid,
code = cmd.code,
target = turtleID,
}
-- Send twice for reliability over modem
for i = 1, 2 do
modem.transmit(COMMAND_CHANNEL, CHANNEL_RECEIVE, commandPacket)
os.sleep(0.05)
end
end
elseif data.type == "home_position" then
modem.transmit(COMMAND_CHANNEL, CHANNEL_RECEIVE, {
type = "home_position",
turtleID = data.turtleID,
homePosition = data.homePosition
})
addLog("Home -> T#" .. data.turtleID, colors.lime)
elseif data.type == "home_set_confirm" then
modem.transmit(COMMAND_CHANNEL, CHANNEL_RECEIVE, {
type = "home_set_confirm",
turtleID = data.turtleID,
homePosition = data.homePosition
})
addLog("Home confirmed T#" .. data.turtleID, colors.lime)
elseif data.type == "init" then
local ids = data.turtleIDs or {}
addLog("Server knows " .. #ids .. " turtles", colors.lime)
end
end
-- ========== Handle Pocket Computer Messages ==========
local function handlePocketMessage(message)
if type(message) ~= "table" or not message.from then return end
local pocketID = message.from
if message.type == "turtle_command" then
addLog("Pocket #" .. pocketID .. " -> T#" .. message.turtleID .. ": " .. message.command, colors.magenta)
modem.transmit(COMMAND_CHANNEL, CHANNEL_RECEIVE, {
command = message.command,
param = message.param,
target = message.turtleID
})
modem.transmit(POCKET_CHANNEL, POCKET_CHANNEL, { type = "command_ack", to = pocketID })
elseif message.type == "player_position" then
addLog("Pocket #" .. pocketID .. " GPS", colors.cyan)
if not wsSend({ type = "player_update", playerID = message.playerID, position = message.position, label = message.label }) then
httpPost("/api/player/update", { playerID = message.playerID, position = message.position, label = message.label, timestamp = message.timestamp })
end
elseif message.type == "server_stats_request" then
addLog("Pocket #" .. pocketID .. " stats", colors.yellow)
local result = httpGet("/api/stats")
modem.transmit(POCKET_CHANNEL, POCKET_CHANNEL, {
type = result and "server_stats" or "error",
to = pocketID,
data = result,
error = not result and "Failed to fetch" or nil
})
elseif message.type == "webbridge_control" then
addLog("Pocket #" .. pocketID .. " " .. message.command, colors.orange)
if message.command == "ping" then
modem.transmit(POCKET_CHANNEL, POCKET_CHANNEL, { type = "webbridge_log", to = pocketID, text = "Pong! (" .. (wsConnected and "WS" or "HTTP") .. ")" })
elseif message.command == "status" then
local tc = 0
for _ in pairs(turtles) do tc = tc + 1 end
modem.transmit(POCKET_CHANNEL, POCKET_CHANNEL, {
type = "webbridge_status", to = pocketID,
data = { messages = stats.messagesReceived, commands = stats.commandsSent, turtles = tc, uptime = os.epoch("utc") - stats.startTime, wsConnected = wsConnected }
})
elseif message.command == "restart" then
modem.transmit(POCKET_CHANNEL, POCKET_CHANNEL, { type = "webbridge_log", to = pocketID, text = "Restarting..." })
sleep(1)
os.reboot()
elseif message.command == "logs" then
for i = math.max(1, #activityLog - 5), #activityLog do
if activityLog[i] then
modem.transmit(POCKET_CHANNEL, POCKET_CHANNEL, { type = "webbridge_log", to = pocketID, text = activityLog[i].text })
end
end
end
end
end
-- ========== Initialization ==========
print("Web Bridge v2 (WebSocket Protocol)")
print("Server: " .. SERVER_URL)
print("WS: " .. WS_URL)
print("Channels: RX=" .. CHANNEL_RECEIVE .. " STATUS=" .. STATUS_CHANNEL .. " CMD=" .. COMMAND_CHANNEL)
if hasMonitor then
drawDashboard()
addLog("System initialized", colors.lime)
end
-- ========== Main Loop ==========
parallel.waitForAny(
-- WebSocket connection manager (reconnects automatically)
function()
while true do
addLog("Connecting WebSocket...", colors.yellow)
local ok, handle = pcall(http.websocket, WS_URL)
if ok and handle then
wsHandle = handle
wsConnected = true
addLog("WebSocket connected!", colors.lime)
-- Read messages from WebSocket
while true do
local readOk, msg = pcall(function() return wsHandle.receive(30) end)
if not readOk then
addLog("WS read error, reconnecting...", colors.red)
break
end
if msg == nil then
-- Timeout, send keepalive
local pingOk = pcall(function() wsHandle.send('{"type":"ping"}') end)
if not pingOk then
addLog("WS ping failed, reconnecting...", colors.red)
break
end
else
local parseOk, data = pcall(textutils.unserializeJSON, msg)
if parseOk and data then
handleServerCommand(data)
end
end
end
-- Clean up
pcall(function() wsHandle.close() end)
wsHandle = nil
wsConnected = false
addLog("WebSocket disconnected", colors.orange)
else
addLog("WS connect failed, retry in 5s", colors.red)
stats.errors = stats.errors + 1
end
sleep(5)
end
end,
-- Modem message handler (turtles -> bridge -> server)
function()
while true do
local event, side, channel, replyChannel, message, distance = os.pullEvent("modem_message")
stats.messagesReceived = stats.messagesReceived + 1
-- Uses Channels.match() for dual-mode safety: accepts messages on
-- both legacy (101/102/103) and target (4211/4212/4213) during migration.
if (Channels.match('remoteturtle.status', channel) or Channels.match('remoteturtle.response', channel)) then
if type(message) == "table" then
forwardToServer(message)
end
elseif Channels.match('remoteturtle.pocket', channel) then
if type(message) == "table" then
handlePocketMessage(message)
end
end
end
end,
-- HTTP fallback polling (only active when WS is down)
function()
while true do
if not wsConnected then
for turtleID, _ in pairs(turtles) do
local result = httpGet("/api/turtle/" .. turtleID .. "/commands")
if result and result.commands and #result.commands > 0 then
addLog("HTTP poll: " .. #result.commands .. " cmd(s) for T#" .. turtleID, colors.cyan)
for _, cmd in ipairs(result.commands) do
handleServerCommand({ type = "command", turtleID = turtleID, command = cmd })
end
httpPost("/api/turtle/" .. turtleID .. "/commands/ack", {})
end
end
end
sleep(wsConnected and 10 or 1)
end
end,
-- Dashboard refresh
function()
while true do
sleep(2)
if hasMonitor then
drawDashboard()
end
end
end
)