tooltip bounds

This commit is contained in:
Boki 2026-02-10 21:21:07 -05:00
parent 930e00c9cc
commit bb2b9cf507
7 changed files with 474 additions and 56 deletions

View file

@ -2,6 +2,7 @@ import express from 'express';
import http from 'http';
import { WebSocketServer, WebSocket } from 'ws';
import path from 'path';
import { mkdir } from 'fs/promises';
import { fileURLToPath } from 'url';
import { logger } from '../util/logger.js';
import { sleep } from '../util/sleep.js';
@ -275,21 +276,47 @@ export class DashboardServer {
...matches.map(m => ({ row: m.row, col: m.col, label: `MATCH ${(m.similarity * 100).toFixed(0)}%` })),
];
// Focus game and hover each cell
// Focus game, take one snapshot with mouse on empty space
await this.debug.gameController.focusGame();
await mkdir('items', { recursive: true });
const tooltips: Array<{ row: number; col: number; label: string; text: string }> = [];
const ts = Date.now();
const reg = result.layout.region;
const cellW = reg.width / result.layout.cols;
const cellH = reg.height / result.layout.rows;
// Move mouse to empty space and take a single reference snapshot
this.debug.gameController.moveMouseInstant(reg.x + reg.width + 50, reg.y + reg.height / 2);
await sleep(50);
await this.debug.screenReader.snapshot();
await this.debug.screenReader.saveScreenshot(`items/${ts}_snapshot.png`);
await sleep(200); // Let game settle before first hover
for (const cell of hoverCells) {
const center = result.layout.region;
const cellW = center.width / result.layout.cols;
const cellH = center.height / result.layout.rows;
const x = Math.round(center.x + cell.col * cellW + cellW / 2);
const y = Math.round(center.y + cell.row * cellH + cellH / 2);
this.broadcastLog('info', `Hovering ${cell.label} (${cell.row},${cell.col}) at (${x},${y})...`);
await this.debug.gameController.moveMouseTo(x, y);
await sleep(1000);
const cellStart = performance.now();
const x = Math.round(reg.x + cell.col * cellW + cellW / 2);
const y = Math.round(reg.y + cell.row * cellH + cellH / 2);
// Quick Bézier move to the cell — tooltip appears on hover
await this.debug.gameController.moveMouseFast(x, y);
await sleep(50);
const afterMove = performance.now();
// Diff-OCR: finds tooltip by row/column density of darkened pixels
const imgPath = `items/${ts}_${cell.row}-${cell.col}.png`;
const diff = await this.debug.screenReader.diffOcr(imgPath);
const afterOcr = performance.now();
const text = diff.text.trim();
const regionInfo = diff.region ? ` at (${diff.region.x},${diff.region.y}) ${diff.region.width}x${diff.region.height}` : '';
tooltips.push({ row: cell.row, col: cell.col, label: cell.label, text });
this.broadcastLog('info',
`${cell.label} (${cell.row},${cell.col}) [move: ${(afterMove - cellStart).toFixed(0)}ms, ocr: ${(afterOcr - afterMove).toFixed(0)}ms, total: ${(afterOcr - cellStart).toFixed(0)}ms]${regionInfo}: ${text.substring(0, 150)}${text.length > 150 ? '...' : ''}`);
}
this.broadcastLog('info', `Done — hovered ${hoverCells.length} cells`);
res.json({ ok: true, itemSize, matchCount: matches.length, hoveredCount: hoverCells.length });
this.broadcastLog('info', `Done — hovered ${hoverCells.length} cells, read ${tooltips.filter(t => t.text).length} tooltips`);
res.json({ ok: true, itemSize, matchCount: matches.length, hoveredCount: hoverCells.length, tooltips });
} catch (err) {
logger.error({ err }, 'Debug test-match-hover failed');
res.status(500).json({ error: 'Test match hover failed' });

View file

@ -91,6 +91,14 @@ export class GameController {
await this.inputSender.moveMouse(x, y);
}
moveMouseInstant(x: number, y: number): void {
this.inputSender.moveMouseInstant(x, y);
}
async moveMouseFast(x: number, y: number): Promise<void> {
await this.inputSender.moveMouseFast(x, y);
}
async leftClickAt(x: number, y: number): Promise<void> {
await this.inputSender.leftClick(x, y);
}
@ -99,6 +107,14 @@ export class GameController {
await this.inputSender.rightClick(x, y);
}
async holdAlt(): Promise<void> {
await this.inputSender.keyDown(VK.MENU);
}
async releaseAlt(): Promise<void> {
await this.inputSender.keyUp(VK.MENU);
}
async pressEscape(): Promise<void> {
await this.inputSender.pressKey(VK.ESCAPE);
}

View file

@ -222,6 +222,46 @@ export class InputSender {
await randomDelay(5, 15);
}
moveMouseInstant(x: number, y: number): void {
this.moveMouseRaw(x, y);
}
/** Quick Bézier move — ~10-15ms, 5 steps, no jitter. Fast but not a raw teleport. */
async moveMouseFast(x: number, y: number): Promise<void> {
const start = this.getCursorPos();
const end: Point = { x, y };
const dx = end.x - start.x;
const dy = end.y - start.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 10) {
this.moveMouseRaw(x, y);
return;
}
const perpX = -dy / distance;
const perpY = dx / distance;
const spread = distance * 0.15;
const cp1: Point = {
x: start.x + dx * 0.3 + perpX * (Math.random() - 0.5) * spread,
y: start.y + dy * 0.3 + perpY * (Math.random() - 0.5) * spread,
};
const cp2: Point = {
x: start.x + dx * 0.7 + perpX * (Math.random() - 0.5) * spread,
y: start.y + dy * 0.7 + perpY * (Math.random() - 0.5) * spread,
};
const steps = 5;
for (let i = 1; i <= steps; i++) {
const t = easeInOutQuad(i / steps);
const pt = cubicBezier(t, start, cp1, cp2, end);
this.moveMouseRaw(Math.round(pt.x), Math.round(pt.y));
await sleep(2);
}
this.moveMouseRaw(x, y);
}
async leftClick(x: number, y: number): Promise<void> {
await this.moveMouse(x, y);
await randomDelay(20, 50);

View file

@ -43,6 +43,12 @@ export interface GridScanResult {
matches?: GridMatch[];
}
export interface DiffOcrResponse {
text: string;
lines: OcrLine[];
region?: Region;
}
export interface DetectGridResult {
detected: boolean;
region?: Region;
@ -151,6 +157,22 @@ export class OcrDaemon {
};
}
async snapshot(): Promise<void> {
await this.sendWithRetry({ cmd: 'snapshot' }, REQUEST_TIMEOUT);
}
async diffOcr(savePath?: string, region?: Region): Promise<DiffOcrResponse> {
const req: DaemonRequest = { cmd: 'diff-ocr' };
if (savePath) req.path = savePath;
if (region) req.region = region;
const resp = await this.sendWithRetry(req, REQUEST_TIMEOUT);
return {
text: resp.text ?? '',
lines: resp.lines ?? [],
region: resp.region,
};
}
async saveScreenshot(path: string, region?: Region): Promise<void> {
const req: DaemonRequest = { cmd: 'screenshot', path };
if (region) req.region = region;

View file

@ -1,7 +1,7 @@
import { mkdir } from 'fs/promises';
import { join } from 'path';
import { logger } from '../util/logger.js';
import { OcrDaemon, type OcrResponse } from './OcrDaemon.js';
import { OcrDaemon, type OcrResponse, type DiffOcrResponse } from './OcrDaemon.js';
import { GridReader, type GridLayout, type CellCoord } from './GridReader.js';
import type { Region } from '../types.js';
@ -102,6 +102,16 @@ export class ScreenReader {
return pos !== null;
}
// ── Snapshot / Diff-OCR (for tooltip reading) ──────────────────────
async snapshot(): Promise<void> {
await this.daemon.snapshot();
}
async diffOcr(savePath?: string, region?: Region): Promise<DiffOcrResponse> {
return this.daemon.diffOcr(savePath, region);
}
// ── Save utilities ──────────────────────────────────────────────────
async saveScreenshot(path: string): Promise<void> {