Files

168 lines
26 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Store Icon Progress Overlay Implementation Plan
**Intent:** Показывать прогресс обхода открытого магазина прямо поверх захваченного игрового кадра: занятые иконки ожидают tooltip в жёлтой рамке и становятся зелёными после успешного распознавания и привязки к слоту.
**Current Behavior:** Overlay показывает только служебные области текущего кадра и общий статус одного tooltip; приложение не знает список занятых слотов и не хранит прогресс по каждой иконке.
**Expected Outcome:** После распознавания магазина приложение находит занятые слоты его Buy/Sell-сетки, показывает их жёлтыми, сохраняет чистый снимок сетки без tooltip и переводит точный слот в зелёный после OCR предмета и совпадения его каталоговой иконки.
**Target-Perspective Output:** В развёрнутом превью выбранного окна (не уже 640 CSS px на desktop, когда позволяет viewport) виден реальный магазин с компактным чек-листом поверх его иконок: жёлтый означает «наведи курсор», зелёный — «этот слот уже прочитан»; рядом показан счётчик `2/5 иконок`.
**Truth Owner:** Активный `result` источника владеет текущим магазином и его `iconSlots`; `src/vision.js` владеет определением занятых ячеек и сравнением визуальной иконки, а `drawSourceOverlay` только рисует состояние.
**Contract Boundary:** Калибровка хранит необязательные `sellItemGridRegion` и `buyItemGridRegion` относительно найденного маркера магазина. `detectOccupiedSlots(frame, absoluteGridRect)` возвращает `{slotId,row,column,frameRect,score}[]`. После `resolveCatalogItem` `matchCatalogIconToSlot(slots, icon)` возвращает ровно один `slotId` только при прохождении абсолютного порога и отрыва от второго результата; иначе возвращает `null`.
**Cutover:** Существующие diagnostics boxes продолжают показывать pipeline, но прогресс иконок рисуется из отдельного устойчивого `result.iconSlots`; общий статус «Предмет ожидает» заменяется счётчиком слотов, когда сетка доступна.
**Displaced Path:** Не использовать ближайший к tooltip слот, порядок наведения или «первый жёлтый» как источник истины — эти эвристики могут покрасить не ту иконку.
**Value Density:** Две необязательные области калибровки, один лёгкий пиксельный проход по 18 ячейкам при открытии магазина и одно сравнение с pending slots после нового предмета; без OCR всех иконок и без нового backend-контракта.
**Evidence Gate:** Все 17 кадров из `data/` описаны в human-reviewed manifest и прогоняются тем же analyzer path, что live capture; на полном Sell-кадре занятые слоты выделены, пустые не выделены; на полном Buy-кадре работает отдельная область; после связанного tooltip ровно соответствующий слот меняет жёлтый на зелёный, а остальные остаются жёлтыми.
**Acceptance Evidence:** `npm run test:fixtures` сравнивает merchant/side/occupied slots/tooltip item с manifest для каждого файла и проверяет связанные store sequences; browser screenshots до/после tooltip для Sell и Buy при фактическом размере source card; unit tests occupied-slot classifier, unique-best/tie и active-shop reset; production build; calibration PUT→GET round-trip; отсутствие slot-данных в clipboard export/outbox.
**Evidence Lane:** Human-reviewed `data/fixtures.json`, локальный browser fixture runner, `node --test`, production build.
**Kill Criteria:** Нет второго overlay canvas, нет позиционного угадывания hovered slot, нет обязательной миграции старой калибровки, нет отправки thumbnail/data URL в backend; inline `analyzeFrame` удалён из `main.js`, и live capture/fixture runner импортируют один `analyzeFrame` из `src/frame-analyzer.js`.
**Architecture Slice:** `data/*.jpg`, `data/fixtures.json`, `index.html`, `src/main.js`, выделяемый production analyzer seam, `src/vision.js`, `src/parser.js`, fixture tests, `package.json`, `README.md`, `src/style.css`.
**Plan Review Gate:** Requires PRE review before execution.
## Product Direction
- Domain: private store, Buy/Sell grid, occupied slot, tooltip, catalog icon, captured frame, scan progress.
- Color world: тёмный игровой кадр, приглушённый amber ожидания, зелёный подтверждения, красный ошибки, нейтральный синий diagnostics.
- Signature: живой чек-лист непосредственно на слотах Lineage II, а не отдельный dashboard со списком.
- Rejected defaults: отдельная карточка каждого pending slot; анимированные пульсирующие рамки; окрашивание всех пустых клеток сетки.
- Direction: сохранить реальный кадр главным слоем и добавить только семантические рамки и компактный счётчик.
## Component Checkpoint
- Intent: владелец инструмента быстро видит, какие предметы в конкретном магазине ещё нужно обойти; интерфейс остаётся спокойным и утилитарным.
- Hierarchy: сам кадр и рамки слотов — focal point; textual status вторичен.
- Palette: существующие `pending` amber и `found` green; новые декоративные цвета не добавляются.
- Depth: borders-only поверх видео, без теней и glow.
- Surfaces: существующее preview surface; новый контейнер не создаётся.
- Typography: существующий маленький overlay status с tabular counter.
- Spacing: существующая плотность 4/8 px; рамка не перекрывает содержимое иконки.
- Readability: активное source preview разворачивается минимум до 640 CSS px на desktop; на узком viewport занимает доступную ширину без отдельного уменьшенного дубликата.
## Architecture Slice
- Files to create: `data/fixtures.json`, `data/fixture-calibration.json`, reviewed template/catalog assets under `data/fixture-assets/`, `src/frame-analyzer.js`, Playwright fixture regression spec and browser evidence images.
- Files to modify: `index.html`, `src/main.js`, `src/vision.js`, `src/parser.js`, fixture/unit tests, `package.json`, `README.md`, `src/style.css`; hosted calibration endpoint/schema добавляется в scope только если round-trip отбрасывает optional regions.
- Files to avoid: `src/market-outbox.js`, import proxy, backend payload, catalog API contract.
- Source of truth: `data/fixtures.json` владеет reviewed expected result/initial state/slot transitions каждого screenshot sequence; `data/fixture-calibration.json` и `data/fixture-assets/` владеют reviewed test geometry/templates/catalog bytes; production analyzer владеет фактическим результатом кадра; `result.activeShopKey` + `result.iconSlots` владеют live progress.
- Read path: fixture JPG + fixture calibration → тот же production analyzer, что live frame → actual result → semantic comparison with manifest; live frame → sale anchor → header side/merchant → side-specific grid → occupied slots → yellow overlay; tooltip → parsed item → catalog icon → saved grid snapshot → matched slot → green overlay.
- Write path: fixture manifest/calibration/assets редактируются только при добавлении или ручной перепроверке screenshots; тесты не переписывают expected values автоматически; calibration дополняется двумя optional regions; runtime slot state остаётся только в памяти источника.
- Contract boundary: `src/frame-analyzer.js` экспортирует единственный `analyzeFrame({frame,result,calibration,templates,source,services}) -> Promise<result>`; production OCR/OpenCV/parser импортируются внутри этого модуля или передаются только через production adapters, а `services.catalog` является единственной fixture substitution; `detectOccupiedSlots(frame, absoluteGridRect) -> [{slotId,row,column,frameRect,score}]`; `matchCatalogIconToSlot(slots, icon) -> slotId|null`; canvas wrappers live in `vision.js`.
- Integration points: fixture manifest loader/schema, production `analyzeFrame`, `emptyCalibration`, calibration UI/addRegion/rendering, `resolveCatalogItem` result, `drawSourceOverlay`, package verification scripts.
- Migration/cutover: existing v4 calibration is read unchanged; missing grid region disables only icon progress for that side.
- Displaced path: общий item status остаётся fallback только при отсутствии grid calibration.
- Acceptance evidence gate: full-window Buy and Sell fixtures plus tooltip/item catalog icon.
## Fixture Corpus and Ground Truth
Все кадры имеют размер 1560 x 1360 и сняты персонажем `Deela`. `merchant` ниже — персонаж открытого магазина. Номер slot считается с нуля слева направо по верхней строке. Значения `unknown` не угадываются по иконке: такой fixture проверяет заголовок и занятость слота, но не название предмета.
| File | Scene | Expected store | Visible/hovered contents |
| --- | --- | --- | --- |
| `Discord_2KrJ9GGjCP.jpg` | Buy + tooltip | `buy:KapayJI`, occupied `[0]` | slot 0: `Ancient Adena`, quantity `0`, each `2` Adena |
| `Discord_7xmx8OSuCH.jpg` | Sell, clean | `sell:Domestos`, occupied `[0]` | slot 0: `unknown` |
| `Discord_ARWzEIaayl.jpg` | Sell + tooltip | `sell:Gnumli`, occupied `[0,1]` | slot 0: `Soulshot: C-grade`, quantity `36,370`, price `20` Adena |
| `Discord_dxpCzaJEWD.jpg` | Sell + tooltip | `sell:Boroda4`, occupied `[0,1]` | slot 1: `Brigandine Helmet`, quantity `1`, price `800,000` Adena |
| `Discord_ezvlzpNHRg.jpg` | Sell + tooltip | `sell:shotD`, occupied `[0,1]` | slot 1: `Dimensional Fragment`, quantity `687`, price `3,500` Adena |
| `Discord_fb9f61z8Ek.jpg` | Sell + tooltip | `sell:shotD`, occupied `[0,1]` | slot 0: `Tutorial Guide`, quantity `1`, price `5,000,000` Adena |
| `Discord_fFbzX9vDhW.jpg` | Sell + tooltip | `sell:1SHOP`, occupied `[0]` | slot 0: `Ancient Adena`, quantity `1,793,000`, price `3` Adena |
| `Discord_ikzA9VhROZ.jpg` | Buy + tooltip | `buy:RAKOT`, occupied `[0,1,2]` | slot 2: `Coarse Bone Powder`, quantity `0`, each `1` Adena |
| `Discord_K9RkTrU6B2.jpg` | Sell, clean | `sell:Crom`, occupied `[0,1]` | slots 01: `unknown` |
| `Discord_KL0fnDcvr7.jpg` | Sell + tooltip | `sell:Boroda4`, occupied `[0,1]` | slot 0: `Spellbook: Prominence`, quantity `2`, price `50,000` Adena |
| `Discord_qy3EYIEcMV.jpg` | Sell + tooltip | `sell:Gnumli`, occupied `[0,1]` | slot 1: `Blessed Spiritshot: D-Grade`, quantity `7,245`, price `58` Adena |
| `Discord_sSJAGZLHvm.jpg` | Sell, clean | `sell:Boroda4`, occupied `[0,1]` | paired contents: `Spellbook: Prominence`, `Brigandine Helmet` |
| `Discord_tJFxvFi7DM.jpg` | Sell, clean | `sell:1SHOP`, occupied `[0]` | paired content: `Ancient Adena` |
| `Discord_TxbA20ZOQB.jpg` | Buy + tooltip | `buy:RAKOT`, occupied `[0,1,2]` | slot 0: `Scroll: Enchant Weapon (D)`, quantity `0`, each `200,000` Adena |
| `Discord_uqGcQhxIWc.jpg` | Sell, clean | `sell:shotD`, occupied `[0,1]` | paired contents: `Tutorial Guide`, `Dimensional Fragment` |
| `Discord_w3Smu4bOQK.jpg` | Sell, clean | `sell:Gnumli`, occupied `[0,1]` | paired contents: `Soulshot: C-grade`, `Blessed Spiritshot: D-Grade` |
| `Discord_wQc5FDayAK.jpg` | Buy, clean | `buy:RAKOT`, occupied `[0,1,2]` | slot 0: `Scroll: Enchant Weapon (D)`; slot 1: `unknown`; slot 2: `Coarse Bone Powder` |
Manifest records both human-facing `displayName` and parser-facing normalized `name`, for example `Spellbook: Prominence``Spellbook Prominence`. It also records `viewerCharacter`, `groupId`, `scene`, standalone vs contextual assertion mode, `side`, `merchant`, `occupiedSlots`, optional `hoveredSlot`, optional reviewed `initialState`, `expectedPendingSlots`, `expectedFoundSlots`, `catalogAssetId`, and optional `{displayName,name,quantity,priceAdena,priceMode}`. Every `data/*.jpg` must appear exactly once; missing files and stale manifest entries fail before OCR starts.
`data/fixture-calibration.json` is human-reviewed versioned truth for this 1560 x 1360 corpus: sale anchor, store header, tooltip search/anchor, name/price regions and Buy/Sell grid regions. Template images are committed as explicit files under `data/fixture-assets/`; runner never derives calibration from actual results. Catalog mappings point to committed API JSON plus original icon bytes by `catalogAssetId/path`, not to manifest-derived generated icons.
Sequence groups:
- `sell-gnumli`: clean → Soulshot tooltip → Blessed Spiritshot tooltip.
- `sell-boroda4`: clean → Spellbook tooltip → Brigandine Helmet tooltip.
- `sell-shotd`: clean → Tutorial Guide tooltip → Dimensional Fragment tooltip.
- `sell-1shop`: clean → Ancient Adena tooltip.
- `buy-rakot`: clean → Scroll tooltip → Coarse Bone Powder tooltip; middle slot intentionally remains unknown/pending.
- `sell-domestos`, `sell-crom`: standalone clean frames guarding the confirmed header and occupancy fields.
- `buy-kapayji`: tooltip-only partial group with human-reviewed `initialState` for `buy:KapayJI` and slot 0 pending; it validates tooltip parsing/transition only and does not claim standalone header/grid recognition.
## Fixture Test Contract
- Unit/schema lane (`node --test`): manifest validity, exact JPG coverage, unique ids, slot ranges, paired group consistency, normalization and pure state transitions.
- Screenshot lane (`npm run test:fixtures`): `@playwright/test` starts the Vite fixture page in bundled Chromium, loads real JPGs, applies committed fixture calibration, calls `src/frame-analyzer.js#analyzeFrame`, and compares only declared expected fields. It uses real production OpenCV/Tesseract/parser; only `/api/l2/items` transport is fulfilled from committed catalog JSON/icon bytes.
- Sequence lane: feed group frames in manifest order and assert remembered `side:merchant`, item aggregation and explicit `expectedPendingSlots`/`expectedFoundSlots` after every frame. Contextual-only fixtures start from their reviewed `initialState`; they are never reported as standalone full-frame passes.
- Diagnostics on failure: write an actual-vs-expected JSON report plus crop/overlay artifacts under an ignored test-output directory; never overwrite the manifest or source JPGs.
- Stable assertions: exact semantic merchant/side/name/quantity/price and slot ids; no pixel-perfect full-screen snapshots and no assertion on OCR confidence unless a threshold regression is specifically under test.
- Three-miss reset stays in the pure state-transition unit lane because the current corpus has no three-frame no-store sequence.
- Verification command: add `npm run check` for unit tests, all 17 fixture cases, production build and `git diff --check`; hosted calibration PUT→GET remains a separately recorded environment evidence gate because it requires the deployed backend.
## Runtime Invariants
- Buy и Sell используют разные calibrated grid regions; side выбирается только из распознанного заголовка.
- Grid region делится на фиксированную Interlude-сетку 6 x 3; пользователь выделяет внешний прямоугольник клеток без заголовка и scroll buttons.
- Пустые ячейки не получают рамку; занятые начинаются как `pending`.
- Occupancy считается только внутри фиксированного inset каждой ячейки, исключающего border; метрика и порог выбираются по измеренному разрыву между occupied/empty примерами обоих типов магазина и фиксируются константой с fixture-тестом.
- Grid snapshot обновляется только на кадре с распознанным заголовком и без найденного tooltip, чтобы tooltip не закрыл иконки.
- Смена `side:merchant` полностью сбрасывает runtime slots, но не исторические parsed items.
- Зелёный статус ставится только если лучший visual match превышает измеренный threshold и опережает второй результат не меньше чем на измеренный margin; tie/identical icons возвращают `null`, остаются жёлтыми и получают статус «неоднозначно».
- Повторный OCR того же предмета не перекрашивает другой одинаковый slot без нового однозначного совпадения.
- Slot thumbnails, grid snapshots и match scores не попадают в outbox или exported JSON.
- После трёх последовательных кадров без `saleAnchor`/нижнего маркера магазина (существующий `result.misses`) очищаются `activeShopKey`, grid snapshot и slots; отсутствие header OCR при видимом маркере, в том числе из-за tooltip, прогресс не сбрасывает; повторное открытие того же merchant строит состояние заново.
## Tasks
1. Зафиксировать corpus в `data/fixtures.json` и добавить schema/coverage tests.
- Allowed scope: существующие 17 JPG остаются неизменными; expected values вводятся вручную по таблице выше; unknown fields остаются явно unknown.
- Expected output: каждый JPG описан ровно один раз, связанные кадры объединены `groupId`, human display и parser-normalized item names разделены.
- Verification: `node --test` validates schema, coverage and group consistency.
- Parallel: no; establishes regression truth.
2. Выделить `src/frame-analyzer.js#analyzeFrame` и добавить Playwright browser fixture runner без дублирования pipeline.
- Allowed scope: перенести только ownership анализа кадра из DOM-heavy `main.js`; удалить inline implementation; live capture и fixtures импортируют один symbol; runner использует production OCR/OpenCV/parser, подменяя только внешний catalog transport committed fixture-ответом.
- Expected output: `npm run test:fixtures` обрабатывает JPGs, сравнивает только declared fields и сохраняет понятные failure artifacts.
- Verification: source search находит одно определение `analyzeFrame`; намеренно испорченный expected field даёт targeted diff; после возврата manifest corpus проходит.
- Parallel: no; establishes executable evidence lane.
3. Добавить optional Sell/Buy grid calibration в `index.html`, `src/main.js`, `src/parser.js`.
- Allowed scope: два поля относительно sale anchor; существующие шесть остаются обязательным минимумом.
- Expected output: старые сохранения работают; grid fields можно выбрать, удалить, сохранить и увидеть на calibration canvas.
- Verification: parser/calibration tests, browser selection smoke и hosted PUT→GET round-trip; если поля теряются, минимально расширить серверную схему.
- Parallel: no; extends the contract.
4. Добавить в `src/vision.js` минимальные slot helpers: 6 x 3 geometry, occupied-cell classifier и unique-best matching catalog icon к pending slots.
- Allowed scope: локальная grid image analysis; использовать существующий OpenCV loader/matcher.
- Expected output: provided Sell fixture returns exactly its occupied cells; empty grid returns none.
- Verification: Node tests для geometry/classifier seam, absolute threshold, second-best margin, identical-icon tie и browser fixture for canvas wrapper.
- Parallel: no; consumes tasks 1-3 contracts and uses both Sell/Buy fixture corpus.
5. Интегрировать runtime state в `src/main.js`.
- Allowed scope: active-shop reset, clean grid snapshot, slot match after `resolveCatalogItem`, no backend writes.
- Expected output: stable pending/found slots persist across frames and reset on shop change/close.
- Verification: focused state-transition tests, three-miss close/reset, reopening same merchant and two-frame browser flow; assert no slot/snapshot fields in clipboard export and outbox.
- Parallel: no; consumes tasks 3-4.
6. Развернуть активное live-превью и расширить существующий `drawSourceOverlay`/status copy.
- Allowed scope: selected source preview минимум 640 CSS px на desktop; draw slot rects before transient diagnostics; reuse `boxColor(pending/found)`.
- Expected output: игровые слоты читаемы в карточке источника, yellow/green outlines и `parsed/total` counter; no new canvas or motion.
- Verification: desktop screenshot именно при фактическом размере source card, narrow viewport screenshot, squint/token/state checks.
- Parallel: can start after runtime contract is fixed.
7. Обновить `README.md`, добавить единый `npm run check`, прогнать tests/build/diff-check и записать browser evidence в `EVIDENCE.md`.
- Acceptance evidence: manifest coverage report `17/17`, Sell and Buy yellow states, exact green transition, shop reset.
- Parallel: no.
## Non-goals
- Автоматически кликать или наводить курсор в игровом окне.
- OCR названий прямо с 32 px icon.
- Сохранять slot progress между перезапусками приложения.
- Менять каталог, import payload или исторический список результатов.
- Угадывать слот по положению tooltip или порядку обхода.
## Required Input Before Execution
- Реальный ответ `/api/l2/items` для одного известного предмета из Sell fixture, включая `iconUrl`, и доступные по этому URL bytes (либо контролируемый локальный/hosted fixture). Same-origin сам по себе не доказывает, что catalog artwork, alpha и размер совпадают с игровой иконкой.
- Buy fixture теперь есть: `Discord_wQc5FDayAK.jpg`; по нему и clean Sell fixtures во время исполнения измерить occupied/empty metric gap и только затем зафиксировать cell inset/threshold.
- По реальной паре slot crop ↔ catalog icon измерить absolute match threshold и best-vs-second margin; до этого зелёная привязка считается непроверенной.
## Risks
- Кастомный клиент может изменить размер/число slot cells; фиксированная 6 x 3 геометрия тогда потребует отдельной настройки.
- Catalog icon может отличаться рамкой, альфой или масштабом от клиентского asset; evidence должен зафиксировать реальный match score до выбора порога.
- Одинаковые иконки в нескольких слотах требуют дополнительного различителя; до него неоднозначные совпадения должны оставаться жёлтыми.