# 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`; 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 0–1: `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 до выбора порога. - Одинаковые иконки в нескольких слотах требуют дополнительного различителя; до него неоднозначные совпадения должны оставаться жёлтыми.