diff --git a/runtime/skills/chop-logs.js b/runtime/skills/chop-logs.js index 4f2be7d..271c515 100644 --- a/runtime/skills/chop-logs.js +++ b/runtime/skills/chop-logs.js @@ -25,6 +25,14 @@ function withTimeout(promise, ms, label) { return Promise.race([promise, timeout]).finally(() => clearTimeout(timer)); } +const UNSAFE_HOSTILE_DISTANCE = 8; +function nearbyUnsafeHostile(snapshot) { + const hostile = snapshot?.closestHostile; + const distance = Number(hostile?.distance); + if (!Number.isFinite(distance) || distance > UNSAFE_HOSTILE_DISTANCE) return null; + return { name: hostile?.name ?? "hostile", distance }; +} + export const skill = Object.freeze({ id: "gather.logs", title: "Gather logs", @@ -34,6 +42,17 @@ export const skill = Object.freeze({ timeoutMs: 90_000, preconditions(ctx) { if (!ctx?.bot) return { ok: false, code: "no_bot", detail: "bot missing" }; + const unsafeHostile = nearbyUnsafeHostile(ctx.snapshot); + if (unsafeHostile) { + // When defend/flee is cooling down, don't let collectBlock spend its + // whole 60s timeout chopping beside a mob. no_target reuses the + // scheduler's existing short backoff for gather skills. + return { + ok: false, + code: "no_target", + detail: `unsafe to gather logs: ${unsafeHostile.name} ${unsafeHostile.distance.toFixed(1)} blocks away`, + }; + } const known = logBlocks(ctx.bot); if (known.size === 0) { return { ok: false, code: "unsupported_version", detail: "no log blocks in registry" }; diff --git a/runtime/skills/contract.test.js b/runtime/skills/contract.test.js index 0891b1e..566e010 100644 --- a/runtime/skills/contract.test.js +++ b/runtime/skills/contract.test.js @@ -37,6 +37,17 @@ test("preconditions gate execution", async () => { } }); +test("gather.logs precondition refuses nearby hostiles", async () => { + const bot = { registry: { blocksByName: { oak_log: { id: 1 } } } }; + const res = await runSkill("gather.logs", { + bot, + snapshot: { closestHostile: { name: "drowned", distance: 6.1 } }, + }); + assert.equal(res.ok, false); + assert.equal(res.code, "no_target"); + assert.match(res.detail, /unsafe to gather logs: drowned 6\.1 blocks away/); +}); + test("preconditions that throw produce precondition_failed", async () => { const teardown = _registerForTest({ id: "test.precondition-throw",