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