local world = require("world") local player = require("player") local block_scanner = require("block_iterator") local creator = require("creator") local Core = require("pathfinder") local SmoothRotation = require("rotations_v3") local TARGET_BLOCKS = { ["minecraft:purple_terracotta"] = { r = 43, g = 14, b = 0, a = 190 }, } local SCAN_RADIUS = 48 local SCAN_BATCH = 7000 local REACH = 4.0 local WAYPOINT_DIST = 0.9 local MINE_TICKS = 30 local STUCK_TICKS = 80 local renders = {} local iterator = nil local scanDone = false local STATE = { SCANNING = "SCANNING", PATHFINDING = "PATHFINDING", MOVING = "MOVING", MINING = "MINING", } local state = STATE.SCANNING local currentTarget = nil local currentPath = nil local waypointIdx = 1 local mineTimer = 0 local stuckTimer = 0 local lastPX, lastPZ = nil, nil local function posKey(x, y, z) return x .. "," .. y .. "," .. z end local function dist3(ax, ay, az, bx, by, bz) local dx, dy, dz = ax-bx, ay-by, az-bz return math.sqrt(dx*dx + dy*dy + dz*dz) end local function pickNearest() local px, py, pz = player.entity.x, player.entity.y, player.entity.z local best, bestDist = nil, math.huge for _, r in pairs(renders) do local d = dist3(px, py, pz, r.x, r.y, r.z) if d < bestDist then bestDist = d best = r end end return best end local function resetIterator() local px, py, pz = player.entity.x, player.entity.y, player.entity.z iterator = block_scanner.new_iterator( math.floor(px), math.floor(py), math.floor(pz), SCAN_RADIUS) scanDone = false end local function stopMovement() player.input.setPressedForward(false) player.input.setPressedBack(false) player.input.setPressedLeft(false) player.input.setPressedRight(false) player.input.setPressedSprinting(false) player.input.setPressedJump(false) player.input.setPressedAttack(false) end registerClientTick(function() if not scanDone then if not iterator then resetIterator() end local batch, hasMore = iterator.next_batch(SCAN_BATCH) for i = 1, #batch do local e = batch[i] local x, y, z = e[1], e[2], e[3] local blockName = e[4].identifier local color = TARGET_BLOCKS[blockName] if color then local k = posKey(x, y, z) if not renders[k] then renders[k] = { x=x, y=y, z=z, r=color.r, g=color.g, b=color.b, a=color.a } end end end if not hasMore then scanDone = true end end local px, py, pz = player.entity.x, player.entity.y, player.entity.z if state == STATE.SCANNING then stopMovement() if scanDone then currentTarget = pickNearest() if currentTarget then state = STATE.PATHFINDING else resetIterator() end end elseif state == STATE.PATHFINDING then stopMovement() Core.findPath(currentTarget, function(path, err) if path and #path > 0 then currentPath = path waypointIdx = 1 stuckTimer = 0 lastPX, lastPZ = px, pz state = STATE.MOVING else if currentTarget then renders[posKey(currentTarget.x, currentTarget.y, currentTarget.z)] = nil end currentTarget = nil currentPath = nil state = STATE.SCANNING resetIterator() end end) elseif state == STATE.MOVING then if not currentPath or not currentTarget then stopMovement() state = STATE.SCANNING resetIterator() return end if lastPX then local moved = math.sqrt((px-lastPX)^2 + (pz-lastPZ)^2) if moved < 0.04 then stuckTimer = stuckTimer + 1 else stuckTimer = 0 end lastPX, lastPZ = px, pz end if stuckTimer > STUCK_TICKS then stopMovement() renders[posKey(currentTarget.x, currentTarget.y, currentTarget.z)] = nil currentTarget = nil currentPath = nil stuckTimer = 0 state = STATE.SCANNING resetIterator() return end if dist3(px, py, pz, currentTarget.x, currentTarget.y, currentTarget.z) <= REACH then stopMovement() state = STATE.MINING mineTimer = 0 return end local wp = currentPath[waypointIdx] if not wp then stopMovement() currentPath = nil state = STATE.PATHFINDING return end if dist3(px, py, pz, wp.x, wp.y, wp.z) < WAYPOINT_DIST then waypointIdx = waypointIdx + 1 return end SmoothRotation.rotateToCoordinates(wp.x + 0.5, wp.y + 1.0, wp.z + 0.5) SmoothRotation.update() local needJump = (wp.y - math.floor(py)) >= 1 player.input.setPressedJump(needJump) player.input.setPressedForward(true) player.input.setPressedSprinting(true) player.input.setPressedBack(false) player.input.setPressedLeft(false) player.input.setPressedRight(false) player.input.setPressedAttack(false) elseif state == STATE.MINING then if not currentTarget then stopMovement() state = STATE.SCANNING resetIterator() return end local tx, ty, tz = currentTarget.x, currentTarget.y, currentTarget.z local block = world.getBlock(tx, ty, tz) if not block or not TARGET_BLOCKS[block.identifier] then renders[posKey(tx, ty, tz)] = nil currentTarget = nil stopMovement() state = STATE.SCANNING resetIterator() return end if dist3(px, py, pz, tx, ty, tz) > REACH then stopMovement() currentPath = nil state = STATE.PATHFINDING return end SmoothRotation.rotateToCoordinates(tx + 0.5, ty + 0.5, tz + 0.5) SmoothRotation.update() player.input.setPressedForward(false) player.input.setPressedSprinting(false) player.input.setPressedJump(false) player.input.setPressedAttack(true) player.input.mineBlock() mineTimer = mineTimer + 1 if mineTimer > MINE_TICKS then renders[posKey(tx, ty, tz)] = nil currentTarget = nil stopMovement() state = STATE.SCANNING resetIterator() end end end) registerWorldRenderer(function(ctx) for _, r in pairs(renders) do local box = creator.createBox(r.x, r.y, r.z, r.x+1, r.y+1, r.z+1) ctx.renderFilled(box, r.r, r.g, r.b, r.a, true) ctx.renderOutline(box, r.r, r.g, r.b, r.a, 2, true) end if currentPath then for i = (waypointIdx or 1), #currentPath do local wp = currentPath[i] local box = creator.createBox(wp.x, wp.y, wp.z, wp.x+1, wp.y+1, wp.z+1) ctx.renderOutline(box, 255, 230, 0, 160, 1, true) end end if currentTarget then local t = currentTarget local box = creator.createBox(t.x-0.05, t.y-0.05, t.z-0.05, t.x+1.05, t.y+1.05, t.z+1.05) ctx.renderOutline(box, 255, 60, 60, 255, 3, true) end end) registerBlockUpdate(function(info) local k = posKey(info.position.x, info.position.y, info.position.z) if renders[k] then renders[k] = nil if currentTarget and currentTarget.x == info.position.x and currentTarget.y == info.position.y and currentTarget.z == info.position.z then currentTarget = nil currentPath = nil stopMovement() state = STATE.SCANNING resetIterator() end elseif info.new then local name = info.new.name or info.new.identifier or "" local color = TARGET_BLOCKS[name] if color then renders[k] = { x=info.position.x, y=info.position.y, z=info.position.z, r=color.r, g=color.g, b=color.b, a=color.a, } end end end)