diff --git a/.gitignore b/.gitignore index 572406b..856f35a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /node_modules -package-lock.json \ No newline at end of file +package-lock.json +.env \ No newline at end of file diff --git a/index.js b/index.js index 0e10616..6d9866e 100644 --- a/index.js +++ b/index.js @@ -3,12 +3,22 @@ const fs = require("fs"); const path = require("path"); const mineflayer = require("mineflayer"); const gui = require("mineflayer-gui"); +const Vec3 = require("vec3"); + +require("dotenv").config(); const logs = []; const MAX_LOGS = 200; -const LOOP_DELAY_MS = 30_000; +const LOOP_DELAY_MS = 10_000; const MAX_ALLOWED_MOVE_BLOCKS = 10; const REJOIN_DELAY_MS = 60_000; +const BULB_VERIFY_RETRIES = 3; + +const ITEM_MODE = normalizeItemMode( + process.env.ITEM_MODE || process.env.SPAWNER_MODE || "bones", +); +const BUTTON_POSITION = parseEnvPosition("BUTTON"); +const BULB_POSITION = parseEnvPosition("BULB"); let spawnerLoopRunning = false; let joinPosition = null; @@ -16,6 +26,102 @@ let hasJoined = false; let spawnerPosition = null; let reconnectTimer = null; let bot = null; +let bothModeStep = 0; + +function normalizeItemMode(value) { + const input = String(value || "") + .trim() + .toLowerCase(); + if (input === "both") return "both"; + if (input === "arrow" || input === "arrows") return "arrows"; + return "bones"; +} + +function parseEnvPosition(prefix) { + const compact = process.env[`${prefix}_POS`]; + if (compact) { + const parsed = compact + .split(/[\s,]+/) + .filter(Boolean) + .map((v) => Number(v)); + if (parsed.length === 3 && parsed.every(Number.isFinite)) { + return new Vec3(parsed[0], parsed[1], parsed[2]); + } + } + + const x = Number(process.env[`${prefix}_X`]); + const y = Number(process.env[`${prefix}_Y`]); + const z = Number(process.env[`${prefix}_Z`]); + if ([x, y, z].every(Number.isFinite)) { + return new Vec3(x, y, z); + } + + return null; +} + +function isBulbOn(block) { + if (!block) return null; + + const props = + typeof block.getProperties === "function" ? block.getProperties() : null; + if (props && typeof props.lit === "boolean") return props.lit; + if (props && typeof props.powered === "boolean") return props.powered; + + const name = String(block.name || "").toLowerCase(); + if (name.includes("lit")) return true; + if (name.includes("unlit")) return false; + if (name.includes("redstone_lamp")) return false; + return null; +} + +async function ensureBulbModeBeforeOpen(mode) { + if (!BULB_POSITION || !BUTTON_POSITION) { + pushLog("missing BULB_POS/BUTTON_POS in .env; skipping mode check"); + return false; + } + + const desiredBulbOn = mode === "arrows"; + let bulbBlock = bot.blockAt(BULB_POSITION); + if (!bulbBlock || bulbBlock.name === "air") { + pushLog("bulb block not found at BULB_POS"); + return false; + } + + let currentBulbOn = isBulbOn(bulbBlock); + if (currentBulbOn === desiredBulbOn) { + return false; + } + + const buttonBlock = bot.blockAt(BUTTON_POSITION); + if (!buttonBlock || buttonBlock.name === "air") { + pushLog("button block not found at BUTTON_POS"); + return false; + } + + for (let attempt = 1; attempt <= BULB_VERIFY_RETRIES; attempt += 1) { + try { + await bot.lookAt(buttonBlock.position.offset(0.5, 0.5, 0.5), true); + await bot.activateBlock(buttonBlock); + pushLog( + `clicked button to set bulb ${desiredBulbOn ? "on" : "off"} for ${mode} (attempt ${attempt}/${BULB_VERIFY_RETRIES})`, + ); + await sleep(900); + + bulbBlock = bot.blockAt(BULB_POSITION); + currentBulbOn = isBulbOn(bulbBlock); + if (currentBulbOn === desiredBulbOn) { + return true; + } + } catch (err) { + pushLog(`failed to click button: ${err.message}`); + } + } + + pushLog( + `bulb state still incorrect after ${BULB_VERIFY_RETRIES} attempts (wanted ${desiredBulbOn ? "on" : "off"})`, + ); + return false; +} function createBotInstance() { hasJoined = false; @@ -237,23 +343,32 @@ function waitForWindowOpen(timeoutMs = 5000) { }); } -async function openSpawnerGui() { - const targetBlock = await rotateToSpawner(); - if (!targetBlock || targetBlock.name === "air") { - pushLog("unable to target spawner block"); - return; +async function openSpawnerGui(maxAttempts = 2) { + for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { + const targetBlock = await rotateToSpawner(); + if (!targetBlock || targetBlock.name === "air") { + pushLog("unable to target spawner block"); + return false; + } + + const openPromise = waitForWindowOpen(5000); + closeCurrentWindow(); + await bot.activateBlock(targetBlock); + const opened = await openPromise; + + if (opened) { + return true; + } + + pushLog( + `no gui opened from block '${targetBlock.name}' (attempt ${attempt}/${maxAttempts})`, + ); + if (attempt < maxAttempts) { + await sleep(700); + } } - const openPromise = waitForWindowOpen(5000); - await bot.activateBlock(targetBlock); - const opened = await openPromise; - - if (!opened) { - pushLog(`no gui opened from block '${targetBlock.name}'`); - return false; - } - - return true; + return false; } function getWindowItems() { @@ -302,6 +417,43 @@ async function dropAllBonesFromCurrentWindow() { pushLog(`dropped bones total: ${dropped}`); } +async function dropAllArrowsFromCurrentWindow() { + if (!bot.currentWindow) { + pushLog("no window open to drop arrows"); + return; + } + + let dropped = 0; + while (true) { + const arrowStacks = getWindowItems().filter( + (item) => item && String(item.name || "").toLowerCase() === "arrow", + ); + const arrowCount = arrowStacks.reduce( + (sum, item) => sum + (item.count || 0), + 0, + ); + + if (arrowCount === 0) break; + + const ok = await bot.gui + .Query() + .Window(comparator) + .Drop("Arrow", 64) + .end() + .run(); + + if (!ok) { + pushLog("failed to drop arrows from window"); + break; + } + + dropped += Math.min(64, arrowCount); + await sleep(150); + } + + pushLog(`dropped arrows total: ${dropped}`); +} + function closeCurrentWindow() { if (!bot.currentWindow) return; if (typeof bot.closeWindow === "function") { @@ -313,13 +465,32 @@ function closeCurrentWindow() { } } -async function runSpawnerCycle() { +async function runSpawnerPhase(mode) { + const toggled = await ensureBulbModeBeforeOpen(mode); + await sleep(toggled ? 1300 : 500); const opened = await openSpawnerGui(); if (!opened) return; - await dropAllBonesFromCurrentWindow(); + + if (mode === "arrows") { + await dropAllArrowsFromCurrentWindow(); + } else { + await dropAllBonesFromCurrentWindow(); + } + closeCurrentWindow(); } +async function runSpawnerCycle() { + if (ITEM_MODE === "both") { + const phase = bothModeStep < 2 ? "bones" : "arrows"; + await runSpawnerPhase(phase); + bothModeStep = (bothModeStep + 1) % 4; + return; + } + + await runSpawnerPhase(ITEM_MODE); +} + function startSpawnerLoop() { if (spawnerLoopRunning) { pushLog("spawner loop already running"); @@ -332,6 +503,7 @@ function startSpawnerLoop() { } spawnerLoopRunning = true; + bothModeStep = 0; pushLog("spawner loop started"); (async () => { @@ -343,7 +515,7 @@ function startSpawnerLoop() { } if (!spawnerLoopRunning) break; - pushLog("waiting 30s for next cycle"); + pushLog("waiting 10s for next cycle"); await sleep(LOOP_DELAY_MS); } })(); @@ -398,3 +570,11 @@ function itemToString(item) { } createBotInstance(); + +if (BULB_POSITION && BUTTON_POSITION) { + pushLog( + `mode=${ITEM_MODE}, bulb=${BULB_POSITION.x},${BULB_POSITION.y},${BULB_POSITION.z}, button=${BUTTON_POSITION.x},${BUTTON_POSITION.y},${BUTTON_POSITION.z}`, + ); +} else { + pushLog("set BULB_POS and BUTTON_POS in .env to enable mode checking"); +} diff --git a/package.json b/package.json index 58bff07..ec11cd8 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,9 @@ "test": "echo \"Error: no test specified\" && exit 1" }, "dependencies": { + "dotenv": "^17.3.1", "mineflayer": "^4.35.0", - "mineflayer-gui": "^4.0.2" + "mineflayer-gui": "^4.0.2", + "vec3": "^0.1.10" } } \ No newline at end of file