fix(webclient): don't report talk/trade NPC targets as combat opponents

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
max bittkerandClaude Opus 5.5 committed 2026-10-12 14:30:12 -04:00
1 parent f4b0e4a068
commit 07d53516ab
2 files changed
+19

No files matched your search

@@ -176,6 +176,20 @@ describe('BotStateCollector combat state', () => {
expect(fighting.combat).toMatchObject({ inCombat: true, targetIndex: 7 });
});
test('does not identify talking or trading with a shopkeeper as a fight', () => {
const client = createClient();
client.npc[7].type = { name: 'Shop keeper', vislevel: 0, op: ['Talk-to', 'Trade'] };
client.localPlayer.combatCycle = 90;
const collector = new BotStateCollector(client);
expect((collector as any).collectPlayerState(1, 100).combat)
.toMatchObject({ inCombat: false, targetIndex: -1, targetType: 'none' });
// Recent damage from another entity must not make the shopkeeper our opponent.
client.localPlayer.combatCycle = 120;
expect((collector as any).collectPlayerState(2, 100).combat)
.toMatchObject({ inCombat: true, targetIndex: -1, targetType: 'none' });
});
test('decodes player targets, which the client packs above the npc index space', () => {
const client = createClient();
client.localPlayer.faceEntity = 32768 + 3; // player slot 3, not npc 32771
@@ -561,6 +561,11 @@ export class BotStateCollector implements ScanProvider {
this.faceEntitySetCycle = clientCycle;
}
let target = BotStateCollector.decodeFaceEntity(rawFaceEntity);
// Trading/talking also sets faceEntity. An NPC without Attack is an
// interaction target, even during the walk-up grace or a recent fight.
if (target.type === 'npc' && !c.npc?.[target.index]?.type?.op?.some((op: string | null) => op !== null && /^attack$/i.test(op))) {
target = { type: 'none', index: -1 };
}
// combatCycle is set to loopCycle + 400 when damage is taken/dealt
// So if combatCycle > loopCycle, we're in combat (within 400 ticks of last hit)
const combatCycle = player.combatCycle ?? -1000;