fix(runtime/reflex): retreat after repeated melee clears

This commit is contained in:
2026-05-26 16:13:11 +03:00
parent b23aad2128
commit f13799de28
2 changed files with 44 additions and 1 deletions
+18 -1
View File
@@ -41,6 +41,7 @@ const DEFEND_ATTACK_MAX_SWINGS = 5;
const DEFEND_ATTACK_SETTLE_MS = 650;
const DEFEND_CLEAR_RADIUS = 4.5;
const DEFEND_STUCK_WINDOW_MS = 20_000;
const DEFEND_REPEAT_OK_RETREAT_COUNT = 2;
// A reflex returns one of:
// { action: "noop" } — nothing to do
@@ -118,13 +119,19 @@ async function attackNearestUntilClear(bot, hostileName, opts = {}) {
}
function rememberDefendAttack(ctx, hostileName, res) {
const now = Date.now();
if (res?.ok) {
const prev = ctx.defendAttackCleared;
const count = prev?.name === hostileName && now - prev.ts < DEFEND_STUCK_WINDOW_MS
? prev.count + 1
: 1;
ctx.defendAttackCleared = { name: hostileName, count, ts: now };
if (ctx.defendAttackStuck?.name === hostileName) ctx.defendAttackStuck = null;
return;
}
ctx.defendAttackCleared = null;
if (res?.code !== "hostile_still_near") return;
const prev = ctx.defendAttackStuck;
const now = Date.now();
const count = prev?.name === hostileName && now - prev.ts < DEFEND_STUCK_WINDOW_MS
? prev.count + 1
: 1;
@@ -137,6 +144,12 @@ function shouldRetreatFromStuckAttack(ctx, hostileName) {
return stuck.count >= 1 && Date.now() - stuck.ts < DEFEND_STUCK_WINDOW_MS;
}
function shouldRetreatFromRepeatedClear(ctx, hostileName) {
const cleared = ctx.defendAttackCleared;
if (!cleared || cleared.name !== hostileName) return false;
return cleared.count >= DEFEND_REPEAT_OK_RETREAT_COUNT && Date.now() - cleared.ts < DEFEND_STUCK_WINDOW_MS;
}
function matchingHostileEntity(ctx, hostileName, dist) {
return Object.values(ctx.bot?.entities ?? {}).find(
(e) =>
@@ -179,6 +192,10 @@ function defendReflex(ctx) {
// Anything beyond 8m with full HP is ignored regardless of how many
// hostiles the perceive snapshot enumerates.
if (dist <= 4) {
if (shouldRetreatFromRepeatedClear(ctx, hostile.name)) {
ctx.defendAttackCleared = null;
return dispatchDefendFlee(ctx, hostile, dist, { ignoreCooldown: true });
}
if (shouldRetreatFromStuckAttack(ctx, hostile.name)) {
ctx.defendAttackStuck = null;
return dispatchDefendFlee(ctx, hostile, dist, { ignoreCooldown: true });
+26
View File
@@ -139,6 +139,32 @@ test("defend flees after a verified attack leaves hostile in reach", () => {
assert.equal(dispatches[1].label, "flee from zombie");
});
test("defend flees after repeated attack successes still leave hostile in reach", () => {
const bot = makeCombatBot();
const { ctx, dispatches } = makeCtx({
bot,
snapshot: {
connected: true,
health: 20,
closestHostile: { name: "zombie", distance: 3 },
curriculum: { plan: { skillId: "gather.logs" } },
},
});
const first = runTick(ctx);
assert.equal(first.kind, "defend-attack");
dispatches[0].opts.onComplete({ ok: true, code: "done" });
const second = runTick(ctx);
assert.equal(second.kind, "defend-attack");
dispatches[1].opts.onComplete({ ok: true, code: "done" });
const third = runTick(ctx);
assert.equal(third.reflex, "defend");
assert.equal(third.kind, "defend-flee");
assert.equal(dispatches[2].label, "flee from zombie");
});
test("eat wins over curriculum when food low and bot has food in inventory", () => {
const { ctx, dispatches } = makeCtx({
snapshot: {