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 = { /** 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 { 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; } }