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_ATTACK_SETTLE_MS = 650;
|
||||||
const DEFEND_CLEAR_RADIUS = 4.5;
|
const DEFEND_CLEAR_RADIUS = 4.5;
|
||||||
const DEFEND_STUCK_WINDOW_MS = 20_000;
|
const DEFEND_STUCK_WINDOW_MS = 20_000;
|
||||||
|
const DEFEND_REPEAT_OK_RETREAT_COUNT = 2;
|
||||||
|
|
||||||
// A reflex returns one of:
|
// A reflex returns one of:
|
||||||
// { action: "noop" } — nothing to do
|
// { action: "noop" } — nothing to do
|
||||||
@@ -118,13 +119,19 @@ async function attackNearestUntilClear(bot, hostileName, opts = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function rememberDefendAttack(ctx, hostileName, res) {
|
function rememberDefendAttack(ctx, hostileName, res) {
|
||||||
|
const now = Date.now();
|
||||||
if (res?.ok) {
|
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;
|
if (ctx.defendAttackStuck?.name === hostileName) ctx.defendAttackStuck = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
ctx.defendAttackCleared = null;
|
||||||
if (res?.code !== "hostile_still_near") return;
|
if (res?.code !== "hostile_still_near") return;
|
||||||
const prev = ctx.defendAttackStuck;
|
const prev = ctx.defendAttackStuck;
|
||||||
const now = Date.now();
|
|
||||||
const count = prev?.name === hostileName && now - prev.ts < DEFEND_STUCK_WINDOW_MS
|
const count = prev?.name === hostileName && now - prev.ts < DEFEND_STUCK_WINDOW_MS
|
||||||
? prev.count + 1
|
? prev.count + 1
|
||||||
: 1;
|
: 1;
|
||||||
@@ -137,6 +144,12 @@ function shouldRetreatFromStuckAttack(ctx, hostileName) {
|
|||||||
return stuck.count >= 1 && Date.now() - stuck.ts < DEFEND_STUCK_WINDOW_MS;
|
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) {
|
function matchingHostileEntity(ctx, hostileName, dist) {
|
||||||
return Object.values(ctx.bot?.entities ?? {}).find(
|
return Object.values(ctx.bot?.entities ?? {}).find(
|
||||||
(e) =>
|
(e) =>
|
||||||
@@ -179,6 +192,10 @@ function defendReflex(ctx) {
|
|||||||
// Anything beyond 8m with full HP is ignored regardless of how many
|
// Anything beyond 8m with full HP is ignored regardless of how many
|
||||||
// hostiles the perceive snapshot enumerates.
|
// hostiles the perceive snapshot enumerates.
|
||||||
if (dist <= 4) {
|
if (dist <= 4) {
|
||||||
|
if (shouldRetreatFromRepeatedClear(ctx, hostile.name)) {
|
||||||
|
ctx.defendAttackCleared = null;
|
||||||
|
return dispatchDefendFlee(ctx, hostile, dist, { ignoreCooldown: true });
|
||||||
|
}
|
||||||
if (shouldRetreatFromStuckAttack(ctx, hostile.name)) {
|
if (shouldRetreatFromStuckAttack(ctx, hostile.name)) {
|
||||||
ctx.defendAttackStuck = null;
|
ctx.defendAttackStuck = null;
|
||||||
return dispatchDefendFlee(ctx, hostile, dist, { ignoreCooldown: true });
|
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");
|
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", () => {
|
test("eat wins over curriculum when food low and bot has food in inventory", () => {
|
||||||
const { ctx, dispatches } = makeCtx({
|
const { ctx, dispatches } = makeCtx({
|
||||||
snapshot: {
|
snapshot: {
|
||||||
|
|||||||
Reference in New Issue
Block a user