Files
remoteturtle/server/states/ExtractionState.js

322 lines
10 KiB
JavaScript

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