switched to new way :)
This commit is contained in:
parent
b03a2a25f1
commit
f22d182c8f
30 changed files with 0 additions and 0 deletions
316
src-old/inventory/InventoryManager.ts
Normal file
316
src-old/inventory/InventoryManager.ts
Normal file
|
|
@ -0,0 +1,316 @@
|
|||
import { join } from 'path';
|
||||
import { InventoryTracker } from './InventoryTracker.js';
|
||||
import type { PlacedItem } from './InventoryTracker.js';
|
||||
import { GRID_LAYOUTS } from '../game/GridReader.js';
|
||||
import { sleep } from '../util/sleep.js';
|
||||
import { logger } from '../util/logger.js';
|
||||
import type { Config, PostAction } from '../types.js';
|
||||
import type { GameController } from '../game/GameController.js';
|
||||
import type { ScreenReader } from '../game/ScreenReader.js';
|
||||
import type { ClientLogWatcher } from '../log/ClientLogWatcher.js';
|
||||
|
||||
const SALVAGE_TEMPLATE = join('assets', 'salvage.png');
|
||||
|
||||
export class InventoryManager {
|
||||
readonly tracker = new InventoryTracker();
|
||||
private atOwnHideout = true;
|
||||
private currentSellerAccount = '';
|
||||
private gameController: GameController;
|
||||
private screenReader: ScreenReader;
|
||||
private logWatcher: ClientLogWatcher;
|
||||
private config: Config;
|
||||
|
||||
constructor(
|
||||
gameController: GameController,
|
||||
screenReader: ScreenReader,
|
||||
logWatcher: ClientLogWatcher,
|
||||
config: Config,
|
||||
) {
|
||||
this.gameController = gameController;
|
||||
this.screenReader = screenReader;
|
||||
this.logWatcher = logWatcher;
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
/** Set location state (called by executors when they travel). */
|
||||
setLocation(atHome: boolean, seller?: string): void {
|
||||
this.atOwnHideout = atHome;
|
||||
this.currentSellerAccount = seller || '';
|
||||
}
|
||||
|
||||
get isAtOwnHideout(): boolean {
|
||||
return this.atOwnHideout;
|
||||
}
|
||||
|
||||
get sellerAccount(): string {
|
||||
return this.currentSellerAccount;
|
||||
}
|
||||
|
||||
/** Scan the real inventory via grid reader and initialize the tracker. */
|
||||
async scanInventory(defaultAction: PostAction = 'stash'): Promise<void> {
|
||||
logger.info('Scanning inventory...');
|
||||
await this.gameController.focusGame();
|
||||
await sleep(300);
|
||||
await this.gameController.openInventory();
|
||||
|
||||
const result = await this.screenReader.grid.scan('inventory');
|
||||
|
||||
// Build cells grid from occupied coords
|
||||
const cells: boolean[][] = Array.from({ length: 5 }, () => Array(12).fill(false));
|
||||
for (const cell of result.occupied) {
|
||||
if (cell.row < 5 && cell.col < 12) {
|
||||
cells[cell.row][cell.col] = true;
|
||||
}
|
||||
}
|
||||
this.tracker.initFromScan(cells, result.items, defaultAction);
|
||||
|
||||
// Close inventory
|
||||
await this.gameController.pressEscape();
|
||||
await sleep(300);
|
||||
}
|
||||
|
||||
/** Startup clear: scan inventory, deposit everything to stash. */
|
||||
async clearToStash(): Promise<void> {
|
||||
logger.info('Checking inventory for leftover items...');
|
||||
await this.scanInventory('stash');
|
||||
|
||||
if (this.tracker.getItems().length === 0) {
|
||||
logger.info('Inventory empty, nothing to clear');
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info({ items: this.tracker.getItems().length }, 'Found leftover items, depositing to stash');
|
||||
await this.depositItemsToStash(this.tracker.getItems());
|
||||
this.tracker.clear();
|
||||
logger.info('Inventory cleared to stash');
|
||||
}
|
||||
|
||||
/** Ensure we are at own hideout, travel if needed. */
|
||||
async ensureAtOwnHideout(): Promise<boolean> {
|
||||
if (this.atOwnHideout) {
|
||||
logger.info('Already at own hideout');
|
||||
return true;
|
||||
}
|
||||
|
||||
await this.gameController.focusGame();
|
||||
await sleep(300);
|
||||
|
||||
const arrived = await this.waitForAreaTransition(
|
||||
this.config.travelTimeoutMs,
|
||||
() => this.gameController.goToHideout(),
|
||||
);
|
||||
if (!arrived) {
|
||||
logger.error('Timed out going to own hideout');
|
||||
return false;
|
||||
}
|
||||
|
||||
await sleep(1500); // Wait for hideout to render
|
||||
this.atOwnHideout = true;
|
||||
this.currentSellerAccount = '';
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Open stash and Ctrl+click given items to deposit. */
|
||||
async depositItemsToStash(items: PlacedItem[]): Promise<void> {
|
||||
if (items.length === 0) return;
|
||||
|
||||
const stashPos = await this.findAndClickNameplate('Stash');
|
||||
if (!stashPos) {
|
||||
logger.error('Could not find Stash nameplate');
|
||||
return;
|
||||
}
|
||||
await sleep(1000); // Wait for stash to open
|
||||
|
||||
const inventoryLayout = GRID_LAYOUTS.inventory;
|
||||
logger.info({ count: items.length }, 'Depositing items to stash');
|
||||
|
||||
await this.gameController.holdCtrl();
|
||||
for (const item of items) {
|
||||
const center = this.screenReader.grid.getCellCenter(inventoryLayout, item.row, item.col);
|
||||
await this.gameController.leftClickAt(center.x, center.y);
|
||||
await sleep(150);
|
||||
}
|
||||
await this.gameController.releaseCtrl();
|
||||
await sleep(500);
|
||||
|
||||
// Close stash
|
||||
await this.gameController.pressEscape();
|
||||
await sleep(500);
|
||||
|
||||
logger.info({ deposited: items.length }, 'Items deposited to stash');
|
||||
}
|
||||
|
||||
/** Open salvage bench, template-match salvage button, Ctrl+click items. */
|
||||
async salvageItems(items: PlacedItem[]): Promise<boolean> {
|
||||
if (items.length === 0) return true;
|
||||
|
||||
const salvageNameplate = await this.findAndClickNameplate('SALVAGE BENCH');
|
||||
if (!salvageNameplate) {
|
||||
logger.error('Could not find Salvage nameplate');
|
||||
return false;
|
||||
}
|
||||
await sleep(1000); // Wait for salvage bench UI to open
|
||||
|
||||
// Template-match salvage.png to activate salvage mode
|
||||
const salvageBtn = await this.screenReader.templateMatch(SALVAGE_TEMPLATE);
|
||||
if (salvageBtn) {
|
||||
await this.gameController.leftClickAt(salvageBtn.x, salvageBtn.y);
|
||||
await sleep(500);
|
||||
} else {
|
||||
logger.warn('Could not find salvage button via template match, trying to proceed anyway');
|
||||
}
|
||||
|
||||
// CTRL+Click each inventory item to salvage
|
||||
const inventoryLayout = GRID_LAYOUTS.inventory;
|
||||
logger.info({ count: items.length }, 'Salvaging inventory items');
|
||||
|
||||
await this.gameController.holdCtrl();
|
||||
for (const item of items) {
|
||||
const center = this.screenReader.grid.getCellCenter(inventoryLayout, item.row, item.col);
|
||||
await this.gameController.leftClickAt(center.x, center.y);
|
||||
await sleep(150);
|
||||
}
|
||||
await this.gameController.releaseCtrl();
|
||||
await sleep(500);
|
||||
|
||||
// Close salvage bench
|
||||
await this.gameController.pressEscape();
|
||||
await sleep(500);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Full post-purchase processing cycle:
|
||||
* 1. Go home
|
||||
* 2. Salvage 'salvage' items if any
|
||||
* 3. Re-scan inventory (picks up salvage materials)
|
||||
* 4. Deposit everything to stash
|
||||
* 5. Clear tracker
|
||||
*/
|
||||
async processInventory(): Promise<void> {
|
||||
try {
|
||||
// Step 1: ensure at own hideout
|
||||
const home = await this.ensureAtOwnHideout();
|
||||
if (!home) {
|
||||
logger.error('Cannot process inventory: failed to reach hideout');
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 2: salvage items tagged 'salvage'
|
||||
if (this.tracker.hasItemsWithAction('salvage')) {
|
||||
const salvageItems = this.tracker.getItemsByAction('salvage');
|
||||
const salvaged = await this.salvageItems(salvageItems);
|
||||
if (salvaged) {
|
||||
this.tracker.removeItemsByAction('salvage');
|
||||
} else {
|
||||
logger.warn('Salvage failed, depositing all items to stash instead');
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: re-scan inventory (picks up salvage materials + any remaining items)
|
||||
await this.scanInventory('stash');
|
||||
|
||||
// Step 4: deposit all remaining items to stash
|
||||
const allItems = this.tracker.getItems();
|
||||
if (allItems.length > 0) {
|
||||
await this.depositItemsToStash(allItems);
|
||||
}
|
||||
|
||||
// Step 5: clear tracker
|
||||
this.tracker.clear();
|
||||
logger.info('Inventory processing complete');
|
||||
} catch (err) {
|
||||
logger.error({ err }, 'Inventory processing failed');
|
||||
|
||||
// Try to recover UI state
|
||||
try {
|
||||
await this.gameController.pressEscape();
|
||||
await sleep(300);
|
||||
} catch {
|
||||
// Best-effort
|
||||
}
|
||||
|
||||
this.tracker.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/** Find and click a nameplate by OCR text (fuzzy, with retries). */
|
||||
async findAndClickNameplate(
|
||||
name: string,
|
||||
maxRetries: number = 3,
|
||||
retryDelayMs: number = 1000,
|
||||
): Promise<{ x: number; y: number } | null> {
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
logger.info({ name, attempt, maxRetries }, 'Searching for nameplate...');
|
||||
const pos = await this.screenReader.findTextOnScreen(name, true);
|
||||
|
||||
if (pos) {
|
||||
logger.info({ name, x: pos.x, y: pos.y }, 'Clicking nameplate');
|
||||
await this.gameController.leftClickAt(pos.x, pos.y);
|
||||
return pos;
|
||||
}
|
||||
|
||||
if (attempt < maxRetries) {
|
||||
await sleep(retryDelayMs);
|
||||
}
|
||||
}
|
||||
|
||||
logger.warn({ name, maxRetries }, 'Nameplate not found after all retries');
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for area transition via Client.txt log.
|
||||
* If `triggerAction` is provided, the listener is registered BEFORE the action
|
||||
* executes, preventing the race where the event fires before we listen.
|
||||
*/
|
||||
waitForAreaTransition(
|
||||
timeoutMs: number,
|
||||
triggerAction?: () => Promise<void>,
|
||||
): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
let resolved = false;
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
this.logWatcher.removeListener('area-entered', handler);
|
||||
resolve(false);
|
||||
}
|
||||
}, timeoutMs);
|
||||
|
||||
const handler = () => {
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
clearTimeout(timer);
|
||||
resolve(true);
|
||||
}
|
||||
};
|
||||
|
||||
// Register listener FIRST
|
||||
this.logWatcher.once('area-entered', handler);
|
||||
|
||||
// THEN trigger the action that causes the transition
|
||||
if (triggerAction) {
|
||||
triggerAction().catch(() => {
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
clearTimeout(timer);
|
||||
this.logWatcher.removeListener('area-entered', handler);
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Get inventory state for dashboard display. */
|
||||
getInventoryState(): { grid: boolean[][]; items: { row: number; col: number; w: number; h: number }[]; free: number } {
|
||||
return {
|
||||
grid: this.tracker.getGrid(),
|
||||
items: this.tracker.getItems(),
|
||||
free: this.tracker.freeCells,
|
||||
};
|
||||
}
|
||||
}
|
||||
157
src-old/inventory/InventoryTracker.ts
Normal file
157
src-old/inventory/InventoryTracker.ts
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
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 });
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue