312 lines
9.2 KiB
JavaScript
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;
|
|
}
|
|
}
|