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,52 @@
|
||||
import { clampRect, type LayoutRect } from "./layoutProfile";
|
||||
import type { Bitmap } from "./ocrPreprocess";
|
||||
|
||||
// EXPERIMENTAL, read-only lock-status detection (nice-to-have). Genshin shows a
|
||||
// padlock at the top-right of the artifact detail card: a bright gold fill when
|
||||
// locked, a dim outline when not. This estimates that icon region and measures
|
||||
// the fraction of bright "lock-gold" pixels; above a threshold the piece is
|
||||
// considered locked.
|
||||
//
|
||||
// The crop position and threshold need calibration against a reference 16:9
|
||||
// screenshot before this is wired into the capture pipeline, so it ships pure and
|
||||
// unit-tested but unused by main.ts. It never drives any in-game action - it only
|
||||
// reads state for triage.
|
||||
|
||||
export function lockIconCropRect(detailRect: LayoutRect, imageSize: { width: number; height: number }): LayoutRect {
|
||||
return clampRect(
|
||||
{
|
||||
x: Math.round(detailRect.x + detailRect.width * 0.8),
|
||||
y: Math.round(detailRect.y + detailRect.height * 0.03),
|
||||
width: Math.round(detailRect.width * 0.16),
|
||||
height: Math.round(detailRect.height * 0.09),
|
||||
},
|
||||
imageSize,
|
||||
);
|
||||
}
|
||||
|
||||
// A gold/highlighted lock pixel: red high, green mid-high, blue low.
|
||||
function isLockGold(b: number, g: number, r: number): boolean {
|
||||
return r >= 180 && g >= 140 && b <= 120 && r > b + 40 && g > b + 20;
|
||||
}
|
||||
|
||||
export function lockSignalRatio(bitmap: Bitmap): number {
|
||||
const { data, width, height } = bitmap;
|
||||
const pixels = width * height;
|
||||
if (pixels === 0) return 0;
|
||||
let gold = 0;
|
||||
for (let pixel = 0; pixel < pixels; pixel++) {
|
||||
const index = pixel * 4;
|
||||
if (isLockGold(data[index], data[index + 1], data[index + 2])) gold++;
|
||||
}
|
||||
return gold / pixels;
|
||||
}
|
||||
|
||||
export const DEFAULT_LOCK_THRESHOLD = 0.06;
|
||||
|
||||
export function isLocked(signalRatio: number, threshold: number = DEFAULT_LOCK_THRESHOLD): boolean {
|
||||
return signalRatio >= threshold;
|
||||
}
|
||||
|
||||
export function detectLockState(bitmap: Bitmap, threshold: number = DEFAULT_LOCK_THRESHOLD): boolean {
|
||||
return isLocked(lockSignalRatio(bitmap), threshold);
|
||||
}
|
||||
Reference in New Issue
Block a user