Document merchant HUD and location capture plan

This commit is contained in:
2026-08-12 14:04:28 +03:00
parent af9cd1e92d
commit 7c696b710c
11 changed files with 545 additions and 310 deletions
+73 -1
View File
@@ -6,6 +6,7 @@ import {
displayCaptureIssue,
isCalibrationReady,
isRectInside,
merchantGroups,
parseFieldValue,
parseStoreHeaderText,
parseTooltipText,
@@ -19,10 +20,16 @@ import {
findTextRunEnd,
fitOcrTextRect,
occupiedSlotScore,
isColoredItemTextPixel,
isWhiteTextPixel,
isYellowTextPixel,
} from "../src/vision.js";
import { activateShop, recordSaleMiss, resetShopProgress } from "../src/frame-analyzer.js";
import {
activateShop,
recordSaleMiss,
resetShopProgress,
shouldReadStoreHeader,
} from "../src/frame-analyzer.js";
test("keeps white text pixels and removes colored or dark pixels", () => {
assert.equal(isWhiteTextPixel(220, 215, 195), true);
@@ -37,6 +44,13 @@ test("keeps yellow tooltip prices without accepting brown UI pixels", () => {
assert.equal(isYellowTextPixel(190, 120, 40), false);
});
test("keeps blue and purple item names without accepting brown UI pixels", () => {
assert.equal(isColoredItemTextPixel(80, 145, 230), true);
assert.equal(isColoredItemTextPixel(190, 90, 220), true);
assert.equal(isColoredItemTextPixel(220, 215, 195), false);
assert.equal(isColoredItemTextPixel(190, 120, 40), false);
});
test("stops a tooltip text run before distant UI noise", () => {
const columns = Array(48).fill(false);
for (let index = 2; index <= 7; index += 1) columns[index] = true;
@@ -130,6 +144,33 @@ test("fits a short row at the frame edge even when the saved width is wider", ()
);
});
test("fits colored item-name rows only when item colors are enabled", () => {
const data = new Uint8ClampedArray(20 * 3 * 4);
for (let column = 0; column < 4; column += 1) {
const index = (20 + column) * 4;
data[index] = 190;
data[index + 1] = 90;
data[index + 2] = 220;
data[index + 3] = 255;
}
const canvas = {
width: 40,
height: 20,
getContext() {
return { getImageData: () => ({ data }) };
},
};
const rect = { x: 20, y: 5, width: 50, height: 3 };
assert.equal(fitOcrTextRect(canvas, rect), null);
assert.deepEqual(fitOcrTextRect(canvas, rect, { includeItemColors: true }), {
x: 20,
y: 5,
width: 10,
height: 3,
});
});
test("keeps only letters, digits and spaces in OCR item names", () => {
assert.equal(cleanItemName(" ~~ Soulshot: S-grade ` "), "Soulshot S grade");
assert.equal(cleanItemName("Кристалл № 12"), "Кристалл 12");
@@ -218,6 +259,32 @@ test("keeps transient grid images and slot state out of copied JSON", () => {
assert.doesNotMatch(JSON.stringify(exported), /iconSlots|gridCaptured|thumbnail|internal/);
});
test("builds one merchant board and keeps the latest price per item", () => {
const groups = merchantGroups([
{
source: "L2 window 1",
saleFound: true,
side: "sell",
merchant: "Dwa",
items: [
{ itemId: 1864, name: "Stem", side: "sell", merchant: "Dwa", priceAdena: 100, seenAt: "2026-08-12T10:00:00.000Z" },
{ itemId: 1864, name: "Stem", side: "sell", merchant: "Dwa", priceAdena: 120, seenAt: "2026-08-12T10:01:00.000Z" },
],
},
{
source: "L2 window 2",
saleFound: false,
items: [{ itemId: 57, name: "Adena", side: "buy", merchant: "Gnumli", priceAdena: 1, seenAt: "2026-08-12T10:02:00.000Z" }],
},
]);
assert.equal(groups.length, 2);
assert.equal(groups[0].merchant, "Dwa");
assert.equal(groups[0].active, true);
assert.deepEqual(groups[0].items.map(({ priceAdena }) => priceAdena), [120]);
assert.equal(groups[1].merchant, "Gnumli");
});
test("resets only transient shop progress when a shop closes or changes", () => {
const result = {
activeShopKey: "sell:gnumli",
@@ -264,6 +331,11 @@ test("keeps shop progress through two misses and clears it on the third", () =>
assert.equal(result.gridCaptured, false);
});
test("does not read a store header while a Price tooltip is visible", () => {
assert.equal(shouldReadStoreHeader(true), false);
assert.equal(shouldReadStoreHeader(false), true);
});
test("parses a hovered item tooltip", () => {
assert.deepEqual(
parseTooltipText("Tears of Eva\nPrice : 3,000,000 Adena\n(3 Million Adena)"),