322 lines
10 KiB
JavaScript
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' });
|
|
}
|
|
}
|