157 lines
4.6 KiB
TypeScript
157 lines
4.6 KiB
TypeScript
import { logger } from '../util/logger.js';
|
||
import type { PostAction } from '../types.js';
|
||
|
||
const ROWS = 5;
|
||
const COLS = 12;
|
||
|
||
export interface PlacedItem {
|
||
row: number;
|
||
col: number;
|
||
w: number;
|
||
h: number;
|
||
postAction: PostAction;
|
||
}
|
||
|
||
export class InventoryTracker {
|
||
private grid: boolean[][];
|
||
private items: PlacedItem[] = [];
|
||
|
||
constructor() {
|
||
this.grid = Array.from({ length: ROWS }, () => Array(COLS).fill(false));
|
||
}
|
||
|
||
/** Initialize from a grid scan result (occupied cells + detected items). */
|
||
initFromScan(
|
||
cells: boolean[][],
|
||
items: { row: number; col: number; w: number; h: number }[],
|
||
defaultAction: PostAction = 'stash',
|
||
): void {
|
||
// Reset
|
||
for (let r = 0; r < ROWS; r++) {
|
||
this.grid[r].fill(false);
|
||
}
|
||
this.items = [];
|
||
|
||
// Mark occupied cells from scan
|
||
for (let r = 0; r < Math.min(cells.length, ROWS); r++) {
|
||
for (let c = 0; c < Math.min(cells[r].length, COLS); c++) {
|
||
this.grid[r][c] = cells[r][c];
|
||
}
|
||
}
|
||
|
||
// Record detected items, filtering out impossibly large ones (max POE2 item = 2×4)
|
||
for (const item of items) {
|
||
if (item.w > 2 || item.h > 4) {
|
||
logger.warn({ row: item.row, col: item.col, w: item.w, h: item.h }, 'Ignoring oversized item (false positive)');
|
||
continue;
|
||
}
|
||
this.items.push({ row: item.row, col: item.col, w: item.w, h: item.h, postAction: defaultAction });
|
||
}
|
||
|
||
logger.info({ occupied: ROWS * COLS - this.freeCells, items: this.items.length, free: this.freeCells }, 'Inventory initialized from scan');
|
||
}
|
||
|
||
/** Try to place an item of size w×h. Column-first to match game's left-priority placement. */
|
||
tryPlace(w: number, h: number, postAction: PostAction = 'stash'): { row: number; col: number } | null {
|
||
for (let col = 0; col <= COLS - w; col++) {
|
||
for (let row = 0; row <= ROWS - h; row++) {
|
||
if (this.fits(row, col, w, h)) {
|
||
this.place(row, col, w, h, postAction);
|
||
logger.info({ row, col, w, h, postAction, free: this.freeCells }, 'Item placed in inventory');
|
||
return { row, col };
|
||
}
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/** Check if an item of size w×h can fit anywhere. */
|
||
canFit(w: number, h: number): boolean {
|
||
for (let col = 0; col <= COLS - w; col++) {
|
||
for (let row = 0; row <= ROWS - h; row++) {
|
||
if (this.fits(row, col, w, h)) return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/** Get all placed items. */
|
||
getItems(): PlacedItem[] {
|
||
return [...this.items];
|
||
}
|
||
|
||
/** Get items with a specific postAction. */
|
||
getItemsByAction(action: PostAction): PlacedItem[] {
|
||
return this.items.filter(i => i.postAction === action);
|
||
}
|
||
|
||
/** Check if any items have a specific postAction. */
|
||
hasItemsWithAction(action: PostAction): boolean {
|
||
return this.items.some(i => i.postAction === action);
|
||
}
|
||
|
||
/** Remove a specific item from tracking and unmark its grid cells. */
|
||
removeItem(item: PlacedItem): void {
|
||
const idx = this.items.indexOf(item);
|
||
if (idx === -1) return;
|
||
// Unmark grid cells
|
||
for (let r = item.row; r < item.row + item.h; r++) {
|
||
for (let c = item.col; c < item.col + item.w; c++) {
|
||
this.grid[r][c] = false;
|
||
}
|
||
}
|
||
this.items.splice(idx, 1);
|
||
}
|
||
|
||
/** Remove all items with a specific postAction. */
|
||
removeItemsByAction(action: PostAction): void {
|
||
const toRemove = this.items.filter(i => i.postAction === action);
|
||
for (const item of toRemove) {
|
||
this.removeItem(item);
|
||
}
|
||
logger.info({ action, removed: toRemove.length, remaining: this.items.length }, 'Removed items by action');
|
||
}
|
||
|
||
/** Get a copy of the occupancy grid. */
|
||
getGrid(): boolean[][] {
|
||
return this.grid.map(row => [...row]);
|
||
}
|
||
|
||
/** Clear entire grid. */
|
||
clear(): void {
|
||
for (let r = 0; r < ROWS; r++) {
|
||
this.grid[r].fill(false);
|
||
}
|
||
this.items = [];
|
||
logger.info('Inventory cleared');
|
||
}
|
||
|
||
/** Get remaining free cells count. */
|
||
get freeCells(): number {
|
||
let count = 0;
|
||
for (let r = 0; r < ROWS; r++) {
|
||
for (let c = 0; c < COLS; c++) {
|
||
if (!this.grid[r][c]) count++;
|
||
}
|
||
}
|
||
return count;
|
||
}
|
||
|
||
private fits(row: number, col: number, w: number, h: number): boolean {
|
||
for (let r = row; r < row + h; r++) {
|
||
for (let c = col; c < col + w; c++) {
|
||
if (this.grid[r][c]) return false;
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
private place(row: number, col: number, w: number, h: number, postAction: PostAction): void {
|
||
for (let r = row; r < row + h; r++) {
|
||
for (let c = col; c < col + w; c++) {
|
||
this.grid[r][c] = true;
|
||
}
|
||
}
|
||
this.items.push({ row, col, w, h, postAction });
|
||
}
|
||
}
|