# L2 Market Parser Кроссплатформенный локальный прототип для захвата нескольких окон Lineage II и сбора предметов из tooltip торгового окна. ## Прогресс иконок магазина Для жёлто-зелёного прогресса дополнительно выдели области `Сетка предметов Sell` и `Сетка предметов Buy`. Нужен только прямоугольник 6 × 3 с ячейками предметов, без заголовка и кнопок прокрутки. Поля необязательны, поэтому старые калибровки продолжают распознавать текст без оверлея иконок. На первом чистом кадре магазина занятые ячейки становятся жёлтыми. После успешного OCR и однозначного сравнения с иконкой из каталога соответствующая ячейка становится зелёной. При одинаково подходящих иконках ячейка остаётся жёлтой; приложение не угадывает слот по положению tooltip или порядку обхода. Выбранный источник разворачивается минимум до 640 CSS px, а экран калибровки показывает исходный скриншот в масштабе 2×, сохраняя координаты в исходных пикселях. Проверка включает обычные unit-тесты, 17 подписанных игровых скриншотов в Chromium, production build и проверку diff: ```bash npm run check ``` ## Запуск Нужен Node.js 20 или новее и Chrome либо Edge. ```bash npm install cp .env.local.example .env.local npm run dev ``` Открой адрес, который напечатает Vite, обычно `http://127.0.0.1:5173`. В основном приложении этот же интерфейс доступен в `Рынок → Аудит рынка`. Там сохранённая калибровка общая: сервер загружает её при открытии вкладки и заменяет после нажатия «Сохранить». На macOS браузеру потребуется разрешение **System Settings → Privacy & Security → Screen & System Audio Recording**. После выдачи разрешения браузер иногда нужно перезапустить. ## Как работает поиск Основное торговое окно считается единым объектом. Пользователь отмечает его нижний постоянный фрагмент, а приложение ищет этот фрагмент по всему кадру. Когда фрагмент найден, его координаты становятся точкой отсчёта для заголовка магазина и зоны tooltip. ```text Кадр └─ Маркер магазина найден? ├─ нет → заголовок и tooltip не проверяются └─ да ├─ OCR `Private Store(Buy/Sell) - имя` │ ├─ виден → запомнить тип сделки + имя торговца │ └─ закрыт tooltip → использовать последний запомненный магазин └─ поиск Price : внутри зоны tooltip ├─ нет → название и цена не читаются └─ да → OCR точной строки названия и точной строки цены ``` Заголовок и зона tooltip хранятся относительно маркера магазина. Поэтому всё торговое окно можно перемещать. Широкая зона tooltip используется только для поиска `Price :` и не передаётся целиком в OCR. После нахождения якоря приложение отдельно определяет фактическую ширину строк названия и цены по их светлым пикселям. Из заголовка `Private Store(Buy) - Hikvision` получится `side: "buy"`, `merchant: "Hikvision"`; из `Private Store(Sell) - Dwa` — `side: "sell"`, `merchant: "Dwa"`. Контекст хранится отдельно для каждого захватываемого окна. Пока нижний маркер магазина остаётся виден, tooltip относится к последнему успешно распознанному заголовку этого окна. После трёх кадров без маркера контекст сбрасывается, чтобы предметы следующего магазина не попали в предыдущий. Если заголовок ещё ни разу не распознан, предмет из tooltip не сохраняется. ## Сценарий 1. В разделе «Источники» нажми «Добавить окно» для каждого окна игры. 2. В разделе «Калибровка» сделай снимок или загрузи готовый скриншот. 3. Выдели постоянный нижний элемент торгового окна, например `Adena + Confirm`, как маркер магазина. По возможности не захватывай изменяемые числа. 4. Выдели заголовок магазина одной строкой: от `Private Store(` до конца имени торговца. Рамку и кнопку закрытия не включай. 5. Выдели широкую горизонтальную полосу всех возможных положений tooltip. Она может выходить далеко за пределы торгового окна, но не должна захватывать нижний `Price` основного окна. 6. Внутри tooltip обведи только постоянную надпись `Price :`. 7. Отдельно выдели левый край, высоту и начальную ширину строки названия и строки цены. Runtime сам продлит каждую строку вправо до фактического конца текста. 8. Сохрани калибровку, нажми «Начать сбор» и вернись в игру. 9. Сначала оставь курсор вне предметов, пока в результате не появятся тип сделки и имя торговца. 10. Наводи курсор на каждый предмет. Даже если tooltip закроет заголовок, предмет будет записан в последний найденный магазин. Наблюдения сначала попадают в IndexedDB браузера, затем отправляются пакетами раз в 30 секунд. При недоступном сервере пакет остаётся локально и повторяется с тем же ID после восстановления связи или перезапуска страницы; сервер не создаёт дубликат. Калибровку можно сделать по снимкам разного размера и с торговым окном в разных местах: 1. Без tooltip выдели нижний маркер и заголовок `Private Store(Buy/Sell) - имя`. 2. Добавь второй снимок с tooltip. Калибратор сам найдёт нижний маркер по всему новому кадру и перенесёт относительно него остальные области. 3. На втором снимке выдели зону tooltip, `Price :`, название и цену. Уже выбранные области сохраняются. Размер игрового окна и положение торгового окна могут отличаться. В калибраторе снимок показывается в масштабе 2× для точной ручной разметки, но шаблоны и сохранённые координаты остаются в исходном пиксельном размере Lineage II. После выбора всех шести областей нажми «Проверить снимки». Они обрабатываются по порядку: сначала кадр с видимым заголовком запоминает магазин, затем кадр с tooltip добавляет предмет. Подключать игровое окно для этого не нужно. Над подробными результатами выводятся уникальные строки вида `Покупка · Lui · Animal Skin · 18 шт. · 400 Adena`. Количество читается из числовых скобок в конце названия, например `(18)` или `(5,600)`; если их нет, используется `1`. Повторное распознавание той же комбинации торговца, типа сделки, предмета, количества и цены не создаёт новую строку. Во вкладке приложения OCR-название проходит неточный поиск по активному каталогу. Уверенное совпадение заменяется каноническим названием и показывается с иконкой предмета; слабое совпадение остаётся с вопросительным знаком и пометкой «Не найден в каталоге». Предметы группируются по имени торговца и типу сделки. Количеством считается только последняя числовая скобка. В `Blessed Spiritshot: D-Grade (D) (5,600)` часть `(D)` остаётся в названии, а количество равно `5600`. Если рамки перекрывают соседние элементы, выключи «Показывать выбранные рамки». Сохранённые области не удалятся, а новый пунктирный прямоугольник останется виден во время выделения. ### Что именно выделять | Область | Правильный выбор | Частая ошибка | | --- | --- | --- | | Маркер магазина | Небольшой постоянный фрагмент внизу, например `Adena` и `Confirm` | Захватить баланс Adena или другое изменяемое число | | Заголовок магазина | Вся строка `Private Store(Sell) - Dwa` или `Private Store(Buy) - Hikvision` | Обрезать `Buy/Sell` или имя; захватить рамку и кнопку закрытия | | Зона tooltip | Широкая полоса возможных положений всплывающего окна, в том числе за пределами окна трейда | Включить нижнюю строку `Price` основного окна | | Якорь Price | Только постоянные символы `Price :` внутри tooltip | Добавить цену, которая меняется у каждого предмета | | Название предмета | Левый край и точная высота верхней строки tooltip; ширина подгоняется автоматически | Захватить иконки, фон игры или строку цены | | Строка цены | Левый край и точная высота `Price : 3,000,000 Adena`, `For Each 400 Adena` или числа | Захватить дополнительную строку `(3 Million Adena)` | Калиброваться на самом длинном предмете больше не требуется: после поиска `Price :` границы названия и цены подгоняются отдельно и останавливаются до удалённого текста интерфейса. ## Диагностика В результате показывается последний обработанный кадр с рамками и каждый этап отдельно: - зелёная рамка означает, что шаблон прошёл заданный порог; - жёлтая показывает найденную область предмета, которую OCR ещё не разобрал; - красная показывает лучшее совпадение, которое не прошло порог; - синяя показывает вычисленную область следующего шага; - «Пропущено» означает, что предыдущий обязательный этап не прошёл; - рядом с OCR показываются фактический crop, текст и уверенность. Если не находится магазин, смотри crop «Маркер магазина» и процент совпадения. Если тип или имя ещё не определены, убери курсор с предмета и смотри фактический OCR этапа «Заголовок магазина». Надпись «Используется последний магазин» означает, что tooltip закрыл заголовок, но привязка предмета сохранена. Если магазин найден, но предмет не появляется, смотри «Якорь Price :». Если оба якоря найдены, проблема уже в точных crop названия или цены. Диагностические изображения существуют только в памяти открытой страницы. Они не попадают в JSON результата и не отправляются вместе с рыночными наблюдениями. В отдельном Vite-прототипе калибровка хранится в `localStorage`. Во вкладке `home-service` серверная калибровка является основной, а `localStorage` остаётся локальной резервной копией. При первом OCR Tesseract.js загружает английскую языковую модель и кеширует её в браузере. ## Ограничения прототипа - Источники выбираются вручную после каждого обновления страницы. - Одна калибровка рассчитана на один масштаб интерфейса Lineage II. - Сбор работает, пока открыта страница прототипа и он не остановлен кнопкой «Остановить сбор». - Курсор перемещает пользователь, приложение не управляет игрой и не эмулирует ввод. - Токен импорта хранится только в локальном Vite-процессе; в браузерный bundle он не попадает. ## Проверки ```bash npm test npm run build # Обновить встроенную копию в соседнем home-service: npm run build -- --outDir ../home-service/frontend/public/l2/market-audit --emptyOutDir ```