161 lines
5.2 KiB
TypeScript
161 lines
5.2 KiB
TypeScript
import { logger } from '../util/logger.js';
|
||
import type { OcrDaemon, GridItem, GridMatch } from './OcrDaemon.js';
|
||
import type { Region } from '../types.js';
|
||
|
||
// ── Grid type definitions ───────────────────────────────────────────────────
|
||
|
||
export interface GridLayout {
|
||
region: Region;
|
||
cols: number;
|
||
rows: number;
|
||
}
|
||
|
||
export interface CellCoord {
|
||
row: number;
|
||
col: number;
|
||
x: number;
|
||
y: number;
|
||
}
|
||
|
||
export interface ScanResult {
|
||
layout: GridLayout;
|
||
occupied: CellCoord[];
|
||
items: GridItem[];
|
||
matches?: GridMatch[];
|
||
}
|
||
|
||
// ── Calibrated grid layouts (2560×1440) ─────────────────────────────────────
|
||
|
||
export const GRID_LAYOUTS: Record<string, GridLayout> = {
|
||
/** Player inventory — always 12×5, right side (below equipment slots) */
|
||
inventory: {
|
||
region: { x: 1696, y: 788, width: 840, height: 350 },
|
||
cols: 12,
|
||
rows: 5,
|
||
},
|
||
/** Personal stash 12×12 — left side, tab not in folder */
|
||
stash12: {
|
||
region: { x: 23, y: 169, width: 840, height: 840 },
|
||
cols: 12,
|
||
rows: 12,
|
||
},
|
||
/** Personal stash 12×12 — left side, tab in folder */
|
||
stash12_folder: {
|
||
region: { x: 23, y: 216, width: 840, height: 840 },
|
||
cols: 12,
|
||
rows: 12,
|
||
},
|
||
/** Personal stash 24×24 (quad tab) — left side, tab not in folder */
|
||
stash24: {
|
||
region: { x: 23, y: 169, width: 840, height: 840 },
|
||
cols: 24,
|
||
rows: 24,
|
||
},
|
||
/** Personal stash 24×24 (quad tab) — left side, tab in folder */
|
||
stash24_folder: {
|
||
region: { x: 23, y: 216, width: 840, height: 840 },
|
||
cols: 24,
|
||
rows: 24,
|
||
},
|
||
/** Seller's public stash — always 12×12 */
|
||
seller: {
|
||
region: { x: 416, y: 299, width: 840, height: 840 },
|
||
cols: 12,
|
||
rows: 12,
|
||
},
|
||
/** NPC shop — 12×12 */
|
||
shop: {
|
||
region: { x: 23, y: 216, width: 840, height: 840 },
|
||
cols: 12,
|
||
rows: 12,
|
||
},
|
||
/** NPC vendor inventory — 12×12 */
|
||
vendor: {
|
||
region: { x: 416, y: 369, width: 840, height: 840 },
|
||
cols: 12,
|
||
rows: 12,
|
||
},
|
||
};
|
||
|
||
// Backward-compat exports
|
||
export const INVENTORY = GRID_LAYOUTS.inventory;
|
||
export const STASH_12x12 = GRID_LAYOUTS.stash12;
|
||
export const STASH_24x24 = GRID_LAYOUTS.stash24;
|
||
export const SELLER_12x12 = GRID_LAYOUTS.seller;
|
||
|
||
// ── GridReader ──────────────────────────────────────────────────────────────
|
||
|
||
export class GridReader {
|
||
constructor(private daemon: OcrDaemon) {}
|
||
|
||
/**
|
||
* Scan a named grid layout for occupied cells.
|
||
*/
|
||
async scan(layoutName: string, threshold?: number, targetRow?: number, targetCol?: number): Promise<ScanResult> {
|
||
const layout = GRID_LAYOUTS[layoutName];
|
||
if (!layout) throw new Error(`Unknown grid layout: ${layoutName}`);
|
||
|
||
const t = performance.now();
|
||
const { occupied, items, matches } = await this.getOccupiedCells(layout, threshold, targetRow, targetCol);
|
||
|
||
const ms = (performance.now() - t).toFixed(0);
|
||
logger.info(
|
||
{ layoutName, cols: layout.cols, rows: layout.rows, occupied: occupied.length, items: items.length, matches: matches?.length, ms },
|
||
'Grid scan complete',
|
||
);
|
||
|
||
return { layout, occupied, items, matches };
|
||
}
|
||
|
||
/** Get the screen-space center of a grid cell */
|
||
getCellCenter(layout: GridLayout, row: number, col: number): { x: number; y: number } {
|
||
const cellW = layout.region.width / layout.cols;
|
||
const cellH = layout.region.height / layout.rows;
|
||
return {
|
||
x: Math.round(layout.region.x + col * cellW + cellW / 2),
|
||
y: Math.round(layout.region.y + row * cellH + cellH / 2),
|
||
};
|
||
}
|
||
|
||
/** Scan the grid and return which cells are occupied and detected items */
|
||
async getOccupiedCells(layout: GridLayout, threshold?: number, targetRow?: number, targetCol?: number): Promise<{ occupied: CellCoord[]; items: GridItem[]; matches?: GridMatch[] }> {
|
||
const t = performance.now();
|
||
const result = await this.daemon.gridScan(
|
||
layout.region,
|
||
layout.cols,
|
||
layout.rows,
|
||
threshold,
|
||
targetRow,
|
||
targetCol,
|
||
);
|
||
|
||
const occupied: CellCoord[] = [];
|
||
for (let row = 0; row < result.cells.length; row++) {
|
||
for (let col = 0; col < result.cells[row].length; col++) {
|
||
if (result.cells[row][col]) {
|
||
const center = this.getCellCenter(layout, row, col);
|
||
occupied.push({ row, col, x: center.x, y: center.y });
|
||
}
|
||
}
|
||
}
|
||
|
||
const ms = (performance.now() - t).toFixed(0);
|
||
logger.info(
|
||
{ layout: `${layout.cols}x${layout.rows}`, occupied: occupied.length, items: result.items.length, matches: result.matches?.length, ms },
|
||
'Grid scan complete',
|
||
);
|
||
return { occupied, items: result.items, matches: result.matches };
|
||
}
|
||
|
||
/** Get all cell centers in the grid */
|
||
getAllCells(layout: GridLayout): CellCoord[] {
|
||
const cells: CellCoord[] = [];
|
||
for (let row = 0; row < layout.rows; row++) {
|
||
for (let col = 0; col < layout.cols; col++) {
|
||
const center = this.getCellCenter(layout, row, col);
|
||
cells.push({ row, col, x: center.x, y: center.y });
|
||
}
|
||
}
|
||
return cells;
|
||
}
|
||
}
|