fix(runtime/reflex): retreat after repeated melee clears
This commit is contained in:
+18
-1
@@ -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 });
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user