Files
l2-market-parser/docs/goals/store-icon-progress/PLAN.md
T

26 KiB
Raw Blame History

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