# L2 Market Parser Кроссплатформенный локальный прототип для захвата нескольких окон Lineage II и сбора предметов из tooltip торгового окна. ## Запуск Нужен Node.js 20 или новее и Chrome либо Edge. ```bash npm install cp .env.local.example .env.local # Заполни L2_MARKET_IMPORT_TOKEN тем же значением, что HOME_SERVICE_L2_MARKET_IMPORT_TOKEN в Gitea Secrets. 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) - имя` │ └─ тип сделки + имя торговца └─ поиск Price : внутри зоны tooltip ├─ нет → название и цена не читаются └─ да → OCR точной строки названия и точной строки цены ``` Заголовок и зона tooltip хранятся относительно маркера магазина. Поэтому всё торговое окно можно перемещать. Внутри зоны tooltip отдельно ищется `Price :`, поэтому сам tooltip тоже может двигаться при наведении на разные предметы. Из заголовка `Private Store(Buy) - Hikvision` получится `side: "buy"`, `merchant: "Hikvision"`; из `Private Store(Sell) - Dwa` — `side: "sell"`, `merchant: "Dwa"`. ## Сценарий 1. В разделе «Источники» нажми «Добавить окно» для каждого окна игры. 2. В разделе «Калибровка» сделай снимок или загрузи готовый скриншот. 3. Выдели постоянный нижний элемент торгового окна, например `Adena + Confirm`, как маркер магазина. По возможности не захватывай изменяемые числа. 4. Выдели заголовок магазина одной строкой: от `Private Store(` до конца имени торговца. Рамку и кнопку закрытия не включай. 5. Выдели широкую область всех возможных положений tooltip, не захватывая нижний `Price` основного окна. 6. Внутри tooltip обведи только постоянную надпись `Price :`. 7. Отдельно выдели точную строку названия и точную строку цены. Для названия лучше использовать длинный предмет и оставить запас справа. 8. Сохрани калибровку, нажми «Начать сбор» и вернись в игру. 9. Наводи курсор на каждый предмет. Уникальные пары `название + цена` будут накапливаться в таблице, а каждое распознавание — сохраняться с датой и отправляться на сервер. Наблюдения сначала попадают в IndexedDB браузера, затем отправляются пакетами раз в 30 секунд. При недоступном сервере пакет остаётся локально и повторяется с тем же ID после восстановления связи или перезапуска страницы; сервер не создаёт дубликат. Для калибровки нужен скриншот, на котором одновременно видны: - нижняя часть торгового окна; - заголовок `Private Store(Buy/Sell) - имя`; - открытый tooltip предмета; - полная строка названия и полная строка `Price`. После выбора всех шести областей нажми «Проверить на скриншоте». Подключать игровое окно для этого не нужно. ### Что именно выделять | Область | Правильный выбор | Частая ошибка | | --- | --- | --- | | Маркер магазина | Небольшой постоянный фрагмент внизу, например `Adena` и `Confirm` | Захватить баланс Adena или другое изменяемое число | | Заголовок магазина | Вся строка `Private Store(Sell) - Dwa` или `Private Store(Buy) - Hikvision` | Обрезать `Buy/Sell` или имя; захватить рамку и кнопку закрытия | | Зона tooltip | Вся полоса возможных положений всплывающего окна над сеткой предметов | Включить нижнюю строку `Price` основного окна | | Якорь Price | Только постоянные символы `Price :` внутри tooltip | Добавить цену, которая меняется у каждого предмета | | Название предмета | Одна верхняя строка tooltip с запасом справа | Захватить иконки, фон игры или строку цены | | Строка цены | Одна строка `Price : 3,000,000 Adena` | Захватить дополнительную строку `(3 Million Adena)` | Лучше калиброваться на предмете с длинным названием и большой ценой. Тогда прямоугольники не обрежут более короткие варианты. ## Диагностика В результате показывается последний обработанный кадр с рамками и каждый этап отдельно: - зелёная рамка означает, что шаблон прошёл заданный порог; - красная показывает лучшее совпадение, которое не прошло порог; - синяя показывает вычисленную область следующего шага; - «Пропущено» означает, что предыдущий обязательный этап не прошёл; - рядом с OCR показываются фактический crop, текст и уверенность. Если не находится магазин, смотри crop «Маркер магазина» и процент совпадения. Если магазин найден, но тип или имя пустые, смотри crop и фактический OCR этапа «Заголовок магазина». Если магазин найден, но предмет не появляется, смотри «Якорь Price :». Если оба якоря найдены, проблема уже в точных crop названия или цены. Диагностические изображения существуют только в памяти открытой страницы. Они не попадают в JSON результата и не отправляются вместе с рыночными наблюдениями. Калибровка хранится в `localStorage` текущего браузера. При первом OCR Tesseract.js загружает английскую языковую модель и кеширует её в браузере. ## Ограничения прототипа - Источники выбираются вручную после каждого обновления страницы. - Одна калибровка рассчитана на один масштаб интерфейса Lineage II. - Сбор работает, пока открыта страница прототипа и он не остановлен кнопкой «Остановить сбор». - Курсор перемещает пользователь, приложение не управляет игрой и не эмулирует ввод. - Токен импорта хранится только в локальном Vite-процессе; в браузерный bundle он не попадает. ## Проверки ```bash npm test npm run build ```