feat(store): GOOD interop, rescan-merge, data staleness, lock detection
Task #5 building blocks, each a pure + unit-tested module: - goodInterop.ts: GOOD (Genshin Optimizer / Inventory Kamera / Akasha) import and export for scanned StoredArtifactRecords - slot/stat/set key maps both ways, substat string <-> { key, value }, main-value reconstruction on import (ADR-003). Export is lossless; import is best-effort (GOOD lacks piece names). - artifactMerge.ts: rescan-merge (ADR-006 follow-up). Level-independent identity (set + slot + main + substat NAME set) collapses leveled re-scan duplicates, keeping the higher-level/stronger record and summing timesSeen. Conservative: differing substat lineups never merge. - dataPackageStatus.ts: warns when the genshin-db package is older than ~45 days (a patch cycle) so new sets/characters aren't silently missed; surfaced in the Scanner Diagnose data-package line. Adds dataGeneratedAt to genshinData. - lockDetection.ts: EXPERIMENTAL read-only lock-status heuristic (gold-pixel ratio in a top-right icon crop). Pure + tested but not wired into capture; crop position and threshold need calibration against a reference 16:9 screenshot. 120 tests + build green. Remaining wiring (needs UI / live calibration): GOOD import/export buttons and live lock detection. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { detectLockState, isLocked, lockIconCropRect, lockSignalRatio } from "./lockDetection";
|
||||
import type { Bitmap } from "./ocrPreprocess";
|
||||
import { profileDetailRect } from "./layoutProfile";
|
||||
|
||||
// Build a BGRA bitmap where `goldPixels` of the pixels are lock-gold and the rest dark.
|
||||
function bitmap(goldPixels: number, total: number): Bitmap {
|
||||
const data = Buffer.alloc(total * 4);
|
||||
for (let pixel = 0; pixel < total; pixel++) {
|
||||
const index = pixel * 4;
|
||||
if (pixel < goldPixels) {
|
||||
data[index] = 40; // B
|
||||
data[index + 1] = 170; // G
|
||||
data[index + 2] = 230; // R -> gold
|
||||
}
|
||||
data[index + 3] = 255;
|
||||
}
|
||||
return { data, width: total, height: 1 };
|
||||
}
|
||||
|
||||
describe("lockDetection", () => {
|
||||
it("places the lock crop in the top-right of the detail card", () => {
|
||||
const size = { width: 2560, height: 1440 };
|
||||
const detail = profileDetailRect(size);
|
||||
const rect = lockIconCropRect(detail, size);
|
||||
expect(rect.x).toBeGreaterThan(detail.x + detail.width * 0.5);
|
||||
expect(rect.x + rect.width).toBeLessThanOrEqual(size.width);
|
||||
expect(rect.y).toBeLessThan(detail.y + detail.height * 0.5);
|
||||
});
|
||||
|
||||
it("measures the gold-pixel ratio", () => {
|
||||
expect(lockSignalRatio(bitmap(0, 100))).toBe(0);
|
||||
expect(lockSignalRatio(bitmap(50, 100))).toBeCloseTo(0.5, 5);
|
||||
expect(lockSignalRatio(bitmap(100, 100))).toBe(1);
|
||||
});
|
||||
|
||||
it("thresholds the ratio into a locked flag", () => {
|
||||
expect(isLocked(0.02)).toBe(false);
|
||||
expect(isLocked(0.2)).toBe(true);
|
||||
expect(detectLockState(bitmap(20, 100))).toBe(true);
|
||||
expect(detectLockState(bitmap(1, 100))).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user