Files
remoteturtle/server/states/BuildingState.js

312 lines
9.2 KiB
JavaScript

/**
* 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;
}
}