From 220f2a90d4d983d161e26d0c543a9b0550037c6b Mon Sep 17 00:00:00 2001 From: MayaTheShy Date: Fri, 20 Feb 2026 02:10:58 -0500 Subject: [PATCH] feat: Implement ExtractionState for smart ore extraction with area scanning and mining logic --- server/states/ExtractionState.js | 321 +++++++++++++++++++++++++++++++ 1 file changed, 321 insertions(+) create mode 100644 server/states/ExtractionState.js diff --git a/server/states/ExtractionState.js b/server/states/ExtractionState.js new file mode 100644 index 0000000..3eecf78 --- /dev/null +++ b/server/states/ExtractionState.js @@ -0,0 +1,321 @@ +/** + * 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' }); + } +}