Changelog
Крестик закрытия рюкзака и кеш клиента
- Крестик закрытия рюкзака снова видно — и он снова нажимается. Владелец: «нету кнопки закрыть
рюкзак». Кнопка была в разметке (32×32 в углу окна), но её перекрывала липкая шапка рюкзака:
у
.inv-stickyсвойz-index: 3, у.modal__close—2, поэтому шапка рисовалась поверх, а тап по углу попадал в неё. Проверка на боевом это и показала:elementFromPointв центре кнопки возвращалinv-head, а не крестик. Теперь у крестика окна рюкзакаz-index: 4; проверено живьём — крестик сверху, настоящий щелчок мышью по его центру закрывает окно. index.htmlбольше не кешируется браузером — вторая, более неприятная половина истории. У файлов не былоCache-Control, и браузер кешировал их «по эвристике» (около 10% от возраста файла): игрок после выката ещё какое-то время играл старым клиентом. Именно поэтому правка крестика сначала «не доехала» — проверка виделаz-index: 2из старого CSS, хотя на сервере уже лежал новый. Вdeploy/nginx-game.confдобавлены заголовки:index.htmlи страницы вики —no-cache, must-revalidate, хешированные сборки в/assets/—public, max-age=31536000, immutable(новая сборка приезжает новым именем файла). Конфиг залит на боевой, заголовки проверены ответом сервера.- Проверка
deploy/check-bag-live.cjsстала строже: смотрит крестик (elementFromPointв его центре плюс настоящийInput.dispatchMouseEventвместо программногоclick(), который проходит сквозь перекрытие и ничего не доказывает) и запускает Chrome с выключенным кешем и свежим профилем — иначе проверка читает старый CSS и врёт. Снимок —models/сумка-боевой-крестик.png.
Профессии: лестница практики втрое короче, ранги V и VI дороже
- Прокачка профессий ускорена втрое: путь до мастера сократился с 7300 до 4105 практики, а до
четвёртого уровня — с 500 до 170. Шаги между уровнями стали
35, 50, 85, 800, 1200, 300, 400, 535, 700. - Ранги V и VI остались долгими: шаги к пятому и шестому уровням профессии взяты вдвое от прежних (800 вместо 400 и 1200 вместо 600), поэтому пятый уровень стоит 970 практики против 900, а шестой — 2170 против 1500. Порядок шагов намеренно не возрастает: после дорогого шестого уровня лестница снова дешевеет, и ранг VI остаётся отдельной целью.
- Потолки по уровню героя (
уровень героя = уровень профессии × 5) и накопленная практика героев не менялись — уровни просто подтягиваются вверх по новой лестнице. - Тесты: правило пересчёта проверяется отдельно (шаги ÷3, два названных ×2), а фикстуры теперь берут пороги из самой лестницы, а не пишут числа руками. Весь набор сервера — 52 файла — зелёный.
Сумка: 60 слотов, продажа и выброс, клетчатое окно
- Рюкзак считается слотами, а не предметами (ТЗ_Сумка_Слоты, 2). Клетка — стопка: сто
зелий занимают одну клетку, десять разных мечей — десять.
BAG_BASE_LIMIT200 → 60, и подпись стопки (slotSignatureвbag.ts) теперь одна на игру: по ней же рынок продаёт пачкой, а сервер отдаёт её клиенту полемstack. Полная сумка отказывает только новому виду вещи — такая же ложится в свою стопку и при «60/60». Крафт, разбор, огранка, руны и покупка лота считают слоты «после работы» (fitsInBagсremove), поэтому освободившаяся клетка идёт в зачёт. - Окно рюкзака — клетчатое. Шесть в ряд, пустые места видны, шапка с «занято N/M» и полоса вкладок липнут к верху. Вкладки — наши пять: Всё · Снаряжение · Расход · Материалы · Руны и камни (решение владельца 13.10.2026; чужие с экрана-референса не берём). Число у вкладки — стопки, а не предметы. Счётчик в верхней панели тоже по слотам.
- Профили с прежним пределом не заперты. Старые 200 мест больше ничего не значат: предметы
остаются на месте, свои стопки пополняются, а новый вид вещи ждёт свободной клетки. Окно
говорит об этом баннером, а
deploy/migrate-bag-slots.jsприводитbagLimitк 60 и печатает список героев сверх предела — это те, кому стоит написать. - Предел растёт только надетой сумкой (
bagSlots, до +30 на VI ранге). Самого предмета в игре нет с 12.10.2026 (владелец убрал сумку для тканей из крафта), поэтому у всех сейчас ровно 60 слотов, а механика ждёт свою вещь: надетая вещь соbagSlotsподнимет предел сама. - Надетое в сумке не лежит. Вещь на герое — клетку она не занимает:
usedSlotsиfitsInBagсчитают только ненадетое (wornUids). Надел — слот освободился; снял — вещь вернулась в рюкзак и слот попросила (если места нет, снятие всё равно проходит: стопок станет на одну больше, счётчик покраснеет, а баннер объяснит, что новый вид вещи не влезет). Кузня теперь работает только с сумкой: гнёзда и переплавка надетое не берут, из спискаForgeBlockоно убрано. - Каждую вещь можно выбросить или продать моментально (кроме квестовой) — решение владельца
13.10.2026. Новый модуль
game/vendor.ts: цена — четверть работы рецепта по рангу (50/75/175/375/750/1250), у дропа и ресурсов рецепта нет — им цена по редкости (50/150/500/1500). Уходит вся стопка («Продать 37 · 1 850»), продажа идёт сразу, выброс спрашивает «Да, выкинуть», а вещь с заточкой, камнями, рунами или чарами спрашивает и перед продажей — как разбор, чтобы одно нажатие не стоило вещи +16. Части сетов подземелий и тир-токены помеченыquest: их нельзя ни продать, ни выбросить, и карточка говорит об этом словами. Доход идёт в задание «Золотодобытчик» (gold_earned) — тем же счётчиком, что переплавка. Попутно закрыта дыра:noSellдо этого никто не читал, и тир-токен можно было выставить на рынок — теперьputLotтакие вещи отклоняет. - Выкат и боевая проверка (13.10.2026). Сервер и клиент залиты на боевой, вики обновлена, сервис
перезапущен (
deploy/redeploy.js,deploy/deploy-wiki.cjs). Миграция предела сумки прошла по живой базе: 95 героев, у всех теперь 60 слотов, сверх предела никого (самый полный профиль — 45 стопок), копии таблицыplayersв бэкапе. Живая проверка на боевом: временный герой,game/client/preview-bag.htmlне нужен — окно открывалось на самом сайте: счётчик «занято 4/60», пять вкладок, 60 клеток (56 пустых), надетый доспех в клетках не появился, у меча кнопки «Продать 2 · 300» и «Выкинуть», у тир-токена кнопок нет и написано почему. Снимки —models/сумка-боевой-рюкзак.png,models/сумка-боевой-продажа.png,models/сумка-боевой-квестовое.png; временный герой удалён, логов ошибок в сервисе нет. - Как проверял. Новый
game/server/test/vendor.js— 54 проверки: цена по рангу и редкости, «у каждого предмета цена есть или он личный», продажа и выброс всей стопкой, заточка как своя стопка, отказ на надетом и квестовом, надетое не занимает слот, рынок не берёт личные вещи. Новыйgame/server/test/bag-slots.js— 38 проверок: предел 60, стопки, подпись (заточка, камень, второе гнездо, чара, мастер, шаг силы риск-ковки, партия варки), полная сумка, освобождение слота работой, предел от надетой сумки. Обновлены тесты, которые считали предметы:tailoring.js,professions.js,professions-rules.js,alchemy.js,charms.js,enchant.js. Весь набор сервера — 52 файла — зелёный;tscсервера,tscиvite buildклиента — чисто. Вид проверен живьём: новая служебная страницаgame/client/preview-bag.html(npx vite, режимыdefault/detail/mat/quest/full/over), снимки headless-Chrome при ширине телефона 390 px —models/сумка-60-слотов.png,models/сумка-полная.png,models/сумка-вкладка-материалы.png,models/сумка-продажа-и-выброс.png,models/сумка-квестовая-вещь.png. Миграция проверена офлайн на дампе (node deploy/migrate-bag-slots.js --from dump.json): пределы 200/230/«нет поля» сходятся к 60, герой с 70 стопками попадает в отчёт.
Подземелья и рейды: отряды, живой бой, ступени, фазы боссов, сеты и Осколки
- Отряды 3 / 5 / 3×3 / 10. Новый модуль правил
game/party.ts: цель отряда задаёт предел состава, приглашения живут 90 секунд, готовность 30, обрыв держит место 2 минуты, лидер расставляет порядок хода, награда — только при вкладе от 5%. Состояние живёт в памяти: отряд — сессия на вечер. Клиент — окно отряда (PartyPanel) со списком онлайна и приглашением. - Живой пошаговый бой, 3 секунды на ход.
combat.tsполучилSquadState: несколько бойцов с каждой стороны, очередь хода, срок хода (3 с герою, 1,5 моба), авто-ход по истечении и кнопка «Авто», лечение и щит на союзника, приёмы с уроном по всем. Периодический урон тикает в начале хода цели — иначе в отряде из трёх он таял бы втрое быстрее.squad-run.tsведёт забег: волны, пауза 5 секунд, перенос здоровья между волнами, вклад каждого в итоге. Экран —SquadBattleView, бой переживает перезагрузку. - Подземелья: три ступени. Обычная, героическая и мифическая — с множителями, подобранными замером: статы мобов волн считаются от базы 60 уровня, а не от карточки моба (в волнах стоят мобы Раскола 63–69, и со своими статами они убивали отряд быстрее, чем он проходил волну). Итог замера для тройки со снаряжением: обычная 3,8–4,3 минуты, героическая 6,4–8,0, мифическая 9,0–11,3. Плюс скалирование от размера отряда (втроём HP волн ×2,4). На мифической в волне третий моб — элита. Соло-мифик разрешён.
- Фазы боссов.
boss-scripts.ts: пороги, щит, свита, ярость, лечение, замах по всем, пропуск ходов — и слабости из лора: «ответ на имя» у Костяного Владыки, изделие на наковальне у Голема (вещь Кузни реально тратится), запись кодекса «Багровый Чертог» у Кровавого Принца. - Сеты из пяти частей и тир-токены. 36 предметов: три сета по шлему, доспеху, перчаткам, сапогам и оружию (восемь классовых типов). Части на 15% лучше крафта, не продаются, ступени 2 / 4 / 5 части, полный комплект даёт щит на волне, +10% по боссам или второй шанс при смерти. Токены падают за первый в неделю килл героической и мифической ступени.
- Осколки Силы и Смотрители. Валюта подземелий (8 / 25 / 50 с босса, потолок 2000) и витрина обмена: часть сета за токен, токены, ключи, зелья, ресурс профессии. Безопасных свитков, рун и золота там нет.
- Рейды на пятёрку. Три ступени на 60 уровне, открываются по очереди, живой бой тем же
движком. Добыча: руны B–D, безопасные свитки, ресурсы ×10, косметика за первый килл, «Добыча
Сторожа» за первый килл недели (в ней же руна S с шансом 5%). Множители HP подобраны замером
raid-pacing.js: 32 / 40 / 49 кругов, 7,9 / 9,9 / 11,7 минуты. - Арена 3×3 вместо заглушки: отряд из трёх против трёх офлайн-снапшотов, подбор по мощи ±25%, попытку и золото тратит каждый, рейтинг считается по среднему команды.
- Клановый выход на 10: ростер до десяти, окно суток, фазы «клич» (три удара вполсилы) и «ярость» (урон по боссу на четверть слабее).
- Ключ разделён на два: «Ключ Подземелья» (старый счёт, подземелья) и «Ключ Рейда» (рейды, три стартовых на 60 уровне). Задания и клановая лавка выдают первый, рейды тратят второй.
Крафт: оружие, шлем, тело, перчатки и сапоги
- Из крафта убраны пояса и плащи (Бронное и Портняжное дело) и сумка для тканей — решение владельца 12.10.2026. Остались оружие, шлем, тело, перчатки и сапоги, у оружейника ещё тяжёлый щит: 72 рецепта у оружейника, по 48 у бронника и портного вместо 60 и 78.
- Семь предметов удалены из игры целиком, со всеми рангами и иконками: тяжёлый и кольчужный
пояса, мантия и пояс робы, лёгкие плащ и пояс, сумка для тканей. Профили героев чистятся двумя
способами: сервер убирает вещи с неизвестным
defIdпри загрузке (dropUnknownItems), а по базе прошла миграция с копией таблицыplayersв бэкап. - Сеты ужаты до четырёх частей, у тяжёлого пятая — щит: иначе бонус за полный комплект стал бы
недостижим. Набор считается по
SET_SLOTS, там же правило и живёт. - Побочный эффект, о котором стоит помнить: сумка для тканей была единственным предметом,
расширявшим рюкзак (до +30 мест). Теперь расширения нет — механика
bagSlotsцела и ждёт новой сумки. Пояса и плащи как дроп не тронуты:cloth_cloakиleather_beltпо-прежнему падают. - Тесты: новый раздел 13 в
professions-rules.js(пять слотов, счёт линий, доступность сетов, отсутствие снятых вещей, чистка профиля), обновленыforge-crafting.js,professions.js,tailoring.js,enchant.js. Весь набор сервера — 39 файлов — зелёный.
Профессии: разбор предметов, слова силы рун в бою, узлы на карте
- Разбор предметов у Зачарования (ТЗ 13, 3). Ненужная экипировка превращается в материалы чар по редкости: обычное — 1 магическая пыль, редкое — 3, эпическое — эссенция покрова и 2 пыли, легендарное — уголёк души и 3 пыли; у мастера на пыль больше. Открывается со второго уровня профессии (герой 10), практика 3 за предмет. Кнопка «Разобрать» — в карточке рюкзака, на экране Зачарования — блок «Разбор» со счётчиком ожидаемой пыли и массовыми кнопками по редкости: сорок предметов разбираются одним нажатием, но массовый разбор берёт только вещи без рун, камней и чар — вещь с руной S или камнем разбирается вручную и с подтверждением, которое говорит, что сгорит. Рецепта у разбора нет намеренно: ТЗ 13 требует ровно 26 рецептов Зачарования. Правило «разбор не выгоднее крафта» проверяется тестом на всех рецептах — самое щедрое соотношение ровно 60%.
- Слова силы трёх рун S заработали буквально (ТЗ 15, 5.1): «Неукротимость» даёт +5% урона, пока у
бойца меньше половины HP; «Возгорание» с шансом 10% подпаливает цель на три хода; «Неподвижность»
режет длительность оглушения на 10%. Слова переносятся на бойца полем
words, как таланты. - Оглушение дошло до игрока, и расовый
stunResistперестал быть мёртвым полем. Громила теперь чередует тяжёлый удар с «Оглушающим ударом»; сопротивление расы (таурен −30%) режет шанс, а слово камня — длительность. Оглушение заработало и на арене: раньше приём «оглушить» там был картинкой. - Исправлена находка разведки: у «Пожирания» (тень S) вампиризм стоял в процентах (
3) вместо доли (0.03), и руна лечила втрое больше нанесённого урона. Единицы приведены к остальным источникам, проверка ловит это отдельно. - Узлы добычи видны на карте мира: на метке зоны — значок вида, заряды и таймер; пустой узел
гаснет. Собирать можно прямо с карты, не заходя на экран профессии. Сервер отдаёт узлы всех зон
(
mapNodes) в ответ наgather:nodesи в ответе сбора. - Тесты: новый
game/server/test/rune-words.js(29 проверок) про слова силы, оглушение и сопротивление; раздел «Разбор предметов» вtest/charms.js; проверка узлов во всех зонах вtest/professions-rules.js. Весь набор сервера — 33 файла — зелёный.
Журнал изменений (свежие сверху).
Клан: экраны по референсу и баффы за казну
- Оформление взято у референса владельца (скриншоты Enchant World), механики — свои (ТЗ_Клан_Экраны.md). Вкладка «Клан» стала набором экранов с навигацией и «назад»: Главная · Состав · Развитие · Клан-баффы · Клановая охота (табы) · Задания · Лавка · Территория · Рейтинг · шит «Пополнить казну».
- Повторён и сам вид референса, а не только структура — дизайн-слой в конце
game/client/src/index.css: тёмный камень с кладкой и виньеткой в шапке, герб 78 px в золотой рамке в две линии со свечением, лента с тегом под ним, имя клана золотым градиентом, серифные заголовки (Georgia), статистики строкой с волосяными разделителями, плитки казны с бликом, карточки-переходы со значком в золотом кольце и золотой стрелкой (охота — тёплая карточка, как в референсе), золотые полосы прогресса, чипы-бейджи (звание, сила, «спит»), подиум золото/серебро/бронза и шит с золотой кромкой. Раскладку слой не трогает: медиазапрос на 380 px по-прежнему складывает статистику и плитки в две колонки. - Главная. Герб клана в рамке, имя и сторона, четыре статистики —
Уровень · Репутация · Участники · **Бонус клана ×1.04**(Знамя и баффы одним множителем), две плитки казны, три плитки-кнопки (Составс активными,Чат клана,Рейтингс местом клана) и карточки-переходы с «‹›»: Развитие, Клановая охота, Клан-баффы, Задания, Лавка, Территория. - Клан-баффы — новая механика. Четыре семейства по три ступени: Знамя охоты (золото и опыт), Гнев на босса (урон по общему боссу), Добычливая рука (шанс находок), Воля к бою (мощь в бою). Покупает офицер или лидер, платит казна, действует 24 часа на весь клан; следующую ступень берут, доплатив разницу, и срок начинается заново. Бафф «Воля к бою» работает через общую точку бойца, поэтому действует и в вылазке, и на арене, и в рейде, и в PvP.
- История казны. Журнал на 30 записей: взносы, подъём уровня, покупки баффов, награды за задания, победы над боссом, итоги недели и вступления. Видно, кто, что и когда сделал и что казна не убывает без записи.
- Состав и Развитие. В составе — мощь на последнем входе, значок класса, бейдж силы (Слабый ·
Ровня · Сильный · Мощный), вклад недели со званием, «в сети / N дн. назад» и «в клане N дн.».
В «Развитии» — условия уровня тремя полосами (золото, CP, репутация) со счётчиком
3 / 3, сведения о следующем уровне и шит пополнения казны с недельным лимитом вклада за взносы. - Рейтинг кланов — внутри клана: подиум топ-3 по очкам недели (сумма вклада) и список остальных с составом и репутацией, своё место подсвечено.
- Попутно в клиенте: на узком экране прятались подписи под-вкладок (
seg--labelled) — табы охоты и переключатель валюты в шите были бы невидимыми; у высшей ступени баффа убрана лишняя красная строка «Высшая ступень». - Как проверял.
tscсервера и клиента — чисто, 33 файла тестов сервера зелёные, из нихgame/server/test/clan-motivation.js— 194 проверки (баффы, история, условия, состав).npm run buildклиента — успешно. Живая проверкаdeploy/check-clan.cjsна настоящемClanTab: 113 проверок, провалов ноль — главная, все экраны с навигацией по карточкам и табам, права офицера и рядового, спящий, виды без клана, 12 уровень, кулдаун, «Меню», фон. Снимки —models/shot-clan-*.png(16 штук, включая-buffs,-progress,-hunt-damage,-rating). - Что осталось за кадром. Аукцион «залов» референса (владение на 7 дней за ставку) не делали — это отдельный контент; накопление добычи на территории не делали — территория осталась фракционной; удар по боссу по-прежнему стоит энергии, а не «попыток».
Профессии: у каждого материала появилось дело
- Кожа Кожевничества идёт в лёгкую броню Портняжного дела — решение владельца 12.10.2026.
Шесть линий лёгкой брони теперь требуют кожу по рангам:
rough_hide3 на I, дальше выделанная кожа и чешуя зверя, на VI — 18 кожи, 6 чешуи и сердце зверя. Магическая роба осталась тканевой. - Пряности Травничества тратит Алхимия с пятого ранга:
wild_stemиaroma_leafидут в зелья V,sun_fruitиwaste_honey— в VI. До этого вторая семья Травничества падала впустую. - Мясо и дичь убраны из игры целиком:
raw_meat,game_bird,fat_beast,monster_coreудалены из каталога, их иконки перенесены вart-source/отменённое/. - Скрещивание рун тратит материал ступени — как и было записано в ТЗ 15, 5, но первая редакция
кода скрещивала бесплатно: A → B стоит 2
rune_grit, B → C — 3rune_plate, C → D — 5rune_crystal, D → S — 2ancestor_seal. Панель «Печать рун» показывает цену и не даёт нажать без материала, а сервер отвечает «Нужно: …». - Инвариант вместо памяти: тест «нет материалов, которые не падают и не тратятся» (раздел 12
professions-rules.js) валит сборку, если в игре появится мёртвый материал. Правил профессий теперь 220 проверок, весь набор сервера — 32 файла — зелёный. - Возврат золота за отменённые профессии не нужен: запрос по боевой базе дал 0 задетых героев из 95. Решения закрыты в РЕШЕНИЯ-ВЛАДЕЛЬЦА.md, устаревший раздел 9 приложения к ТЗ переписан.
Клан: зачем вступать и зачем быть активным
- Проблема. Рядовой член клана не получал лично ничего: награда за кланового босса уходила в казну, уровень клана давал только места в составе, а бой с героем врага не приносил ни золота, ни опыта. Вступать было незачем, быть активным — тем более. Спецификация — ТЗ_Клан_Мотивация.md.
- Знамя клана. +1% золота, опыта и добычи за уровень клана (до +10%)
каждому, у кого есть вклад за неделю; спящим (неделя без входа) не
начисляется. Складывается с контролем зоны, общий потолок +35%
(
rewardMulвindex.ts, числа — вgame/clan.ts). - Личный вклад. Начисляется за взносы (кап 400 в неделю), удары по боссу,
подземелья, рейды, арену, работы по профессии, победы и поражения в PvP (кап
- и за задания клана. Дробный вклад копится: десять убийств дают один вклад.
- Клановая лавка. За вклад — зелья (ранга по тиру героя), свитки заточки, ключи, сундуки и косметика «Знамя клана»; недельные лимиты и требования по уровню клана и званию. Каждый покупает себе: казна на лавку не тратится.
- Задания клана. Три ежедневных и три недельных с общим прогрессом. Награда
идёт в казну (золото, CP, репутация), а пул вклада делится по личному участию
— минимум 50 тому, кто внёс хотя бы процент цели; вышедший из клана доли не
получает, остаток достаётся оставшимся. Прогресс двигает хук в
bump. - Клановая охота. Удар стоит 10 энергии (закрыта дыра: босса можно было бить сколько угодно раз подряд). Добыча делится 70/30: казна — золото и 150 CP, участники — остальное по урону, порог 1% HP; офлайн-участник получает долю при входе.
- PvP. Победитель получает золото (100 × уровень соперника), опыт зоны ×2 и вклад; в логе боя видны теги обоих кланов. Клан получает честь и очко витрины зоны, в своей зоне — два очка и +5% мощи «дома». Поражение в своей зоне снимает очко витрины (не чаще раза в 30 минут), во время Прорыва бонусов нет.
- Звания и неделя. Ветеран / Старейшина / Легенда клана дают наградные сундуки, «Легенда» — ещё +1% к Знамени. Очки недели — сумма вклада членов: топ-3 получают CP в казну; рейтинг по мощи в «Аллее героев» остался отдельно.
- Новичок и анти-абьюз. Приветственный набор при первом вступлении, +20% опыта до 20 уровня (клану — репутация за уровни новичка). После выхода и после кика сутки нельзя в другой клан, вклад при выходе сгорает.
- Как проверял.
tscсервера — чисто, все 32 файла тестов сервера зелёные: новыйgame/server/test/clan-motivation.js(151 проверка: Знамя, капы вклада, звания, лавка, задания, доли босса, PvP, недельный сброс, сохранение клановых полей профиля) и соседниеclans.js,territory.js,territory-pvp.js,quests.js,farm-zones.js,professions.js,socket-guard.js. Сервер поднимается и без базы — проверен запуск. Клиент:tsc --noEmitиvite build— чисто. - Живая проверка вкладки — check-clan.cjs на
настоящем
ClanTabизsrc: 59 проверок, провалов ноль, виды «лидер», «офицер», «рядовой», «без клана», «12 уровень», «кулдаун», «Знамя не действует», «Меню» и фон. Стенд получил свои данные (stand-clan.tsx) и свой каталог_stand-clan— раньше он делил_standс другими проверками, и параллельный прогон уносил файлы из-под ног. Снимки —models/shot-clan-leader.png,-officer,-member,-none,-bg,-menu. УDUMP=1проверка печатает текст вкладки: удобно, когда подпись переименовали. - Что осталось за кадром.
bg_clan.jpgне нарисован (промт — в ТЗ_Клан.md, раздел 5). Титул клана не делали: титулы — своё ТЗ, в лавке вместо него косметика «Знамя клана». Уровень 15 и цена создания (50 000 золота + 500 CP) не менялись. Знамя не умножает награды заданий и арены: их числа заданы таблицами и проверяются тестами.
Начертания: Рунное дело переехало из таверны в здание
- Решение владельца 12.10.2026: «Начертание» — рабочее место Рунного дела. Профессия больше не работает из таверны: там остаётся только учитель, который её продаёт (РЕШЕНИЯ-ВЛАДЕЛЬЦА.md).
- В здании два раздела (CapitalTab.tsx): «Печать рун» — скрещивание двух одинаковых рун и вставка в гнёзда, доступна любому; «Рунное дело» — верхняя градация S, разбор рун и слова силы, работает при профессии в сопутствующем слоте. У незнающего героя вторая вкладка не пустует: объясняет, где взять профессию, и ведёт к учителю.
- Экран профессии в таверне стал указателем. Для
runecraftingвместо работы показывается карточка со ссылкой в «Начертания» — механика больше не живёт в двух местах (RuneProfessionPanel.tsx). - Сверление гнёзд осталось в кузне (ТЗ 15, критерий 7) — не тронуто.
- Как проверял.
tscклиента чисто. Новый стенд stand-capital.tsx берёт настоящийCapitalTabс подставными данными, check-scriptorium.cjs снимает его в headless-Chrome: оба вида — с профессией и без — открывают здание, вкладки на месте, вкладка профессии показывает скрещивание в S, разбор рун и слова силы, у незнающего героя — объяснение и кнопка к учителю. Снимки —models/начертания/.
Рунное дело: профессия в таверне, верхняя градация S
- Профессия внедрена. Рунное дело живёт в таверне (сопутствующий слот) и доступна всем; здание «Начертание» осталось как было — скрещивание двух одинаковых рун и вставка в гнёзда по-прежнему доступны любому. Профессия не отбирает механику, а добавляет то, чего у здания нет.
- Градация S (runes.ts): пятая ступень рун, сила ×8 против ×5,5 у D, плюс «слово силы» семейства. Свободное скрещивание теперь останавливается на D — верхнюю собирает рунник: две руны D одного семейства и 2 Печати предков, удача 50% (65% у мастера), при провале одна руна D рассыпается, печати уходят.
- Разбор руны возвращает материал её градации (крошка, пластина, кристалл, печать), мастер —
вдвое. Усиление вставленных рун: рунник вставляет их на +5% с четвёртого уровня профессии и
на +10% с десятого — метка
forge.runeBoostвидна в статах вещи. - Ресурсы (runeprof.ts, harvest.ts, expedition.ts): крошка и пластина — побочно у Горного дела (15% и 5% при добыче руды и камня) и с мобов 40+, кристалл и печать — с боссов, элиты и из Сундука Древних (печать 0,06). Падают только знающему профессию, как и прочие материалы.
- Экран профессии (RuneProfessionPanel.tsx): скрещивание в S по семействам, разбор рун из сумки, список слов силы и покупка двух рецептов-услуг («Разбор руны» — ранг 2, «Скрещивание в S» — ранг 6). Числа приходят каталогом и состоянием с сервера, второй копии баланса на клиенте нет.
- Слова силы записаны и в формулировке ТЗ, и в том, чем выражены в игре. Буквально работают
«Пожирание» (вампиризм) и «Обновление» (лечение); «Неукротимость», «Неподвижность» и «Возгорание»
дают статы по смыслу семейства — механик поджога, урона при низком HP и среза оглушения в бою нет
(то же и с расовым
stunResistтаурена, который никем не читается). - Проверки:
professions-rules.jsвырос до 188 проверок — раздел 10 держит всю механику рун, раздел 11 — источники материалов. Весь набор сервера (31 файл) зелёный; заодно снята ловушка пустых профессий: Рунное дело больше не «Скоро», а Кулинария и Кожевенное дело — по-прежнему.
Профессии: экономика добычи, один гейт прогресса, узлы и шкуры
- Внедрено. Правки поверх ТЗ 03–15 записаны отдельно — 24_ТЗ_Профессии_Правки.md; он верен там, где расходится с карточками профессий.
- Дроп ресурсов (expedition.ts): 25% стали бюджетом каждой семьи (обычный 17,5%, редкий 5,5%, эпический 1,75%, легендарный 0,25%), а не общим котлом, который делился на число профессий. Раньше собранный герой получал каждый ресурс втрое реже, чем герой с одной профессией; теперь руда у Горного дела и у Оружейного — одна семья и считается один раз.
- Работа (forge.ts): обычная идёт без броска
(100%), «стабильной» больше нет, партия делает три изделия материалами за три, а у
мастера — четыре. Риск-ковка осталась только у Оружейного дела: 50% (55% у мастера,
65% с «наверняка»), при удаче вещь сильнее на шаг ранга (
forge.quality), при провале возвращается половина материалов. Заказы обычной работы на рынке выполняются всегда. - Один гейт прогресса (proflevel.ts): практика
→ уровень профессии 1..10 → ранг рецепта. Уровень требует героя
N × 5, пороги практики100 / 250 / 500 / 900 / 1500 / 2400 / 3600 / 5200 / 7300. Старый гейт «ранг открывается на 10/20/…/60 уровне героя» и своя лестница у чар убраны. У профессий из старых сохранёнок уровень не опускается ниже высшего выученного ранга — купленное не отбираем. - Мастерство (10 уровень) стало видимым: в партии мастер делает на изделие больше, а
его вещь помечена
forge.masteryи сильнее на 5%. У собирательных свои бонусы: «Жила» (30% двойной добычи), «Редкий побег» (18%), «Скорняк» (+1 шкура, 10% сердца зверя). - Узлы добычи и шкуры (harvest.ts): узел — заряд зоны (3 рудных + 2 каменных, 3 травяных + 2 пряных), восстановление 10 минут, добыча одним нажатием, градация по таблице зоны. Кожевничество узлов не имеет: «Снять шкуру» на итоге боя (одна попытка на моба) и галочка авто-снятия в автофарме. Порождения Пустоты оставлены снимаемыми — иначе в ярусах Раскола кожевнику нечего делать.
- Услуги jewelry.ts: создаёт профессия, пользуется любой. Огранка без риска, извлечение камня возвращает его целым за 30% цены огранки (раньше камень уничтожался, и услугой не пользовался никто).
- Клиент: экран профессии — три вкладки-слота (крафт / собирательная / сопутствующая). До этого он рисовал только крафтовый слот, поэтому панели ювелира, алхимии и чар были недостижимы, а у собирательных экрана не было вовсе. Появился GatherPanel.tsx, кнопка «Снять шкуру» в итоге боя и режимы работы в попапе крафта.
- Проверки: новый тест professions-rules.js
(115 проверок) плюс переписанные под новые правила
alchemy,tailoring,jewelry,charms,enchant,forge-crafting,professions,professions-migration,quests. Весь набор сервера (31 файл) зелёный. - Найдено и починено по дороге:
CRAFT_RESOURCESуказывал на несуществующий наборsmithing(то есть былundefined); комментарий «24 кузнечные линии» устарел — их 22 (12 линий и 72 рецепта у Оружейного дела, 10 линий и 60 у Бронного); чары начисляют практику и при провале; мастер-оружейник больше не платит Эйроны за галочку впустую;toSingleProfessionне дописываетprofession: nullтрёхслотовому профилю. - Пустые профессии закрыты от продажи. Кулинария, Рунное дело и Кожевенное дело пока
без рецептов, но учитель продавал их как обычные: 1 000 золота за пустой слот или
5 000 и сгоревшая рабочая сопутствующая — за профессию без единой работы. Теперь
professionReadyвcapital.tsотклоняет изучение пустой профессии, а в меню учителя она видна с пометкой «Скоро» и без кнопки. Рунное дело при этом осталось объявленным: руны по-прежнему живут в здании «Начертание» и доступны всем — перенос в профессию отложен отдельной задачей (нужны рунные ресурсы, градация S и решение судьбы здания).
Иконки интерфейса: 108 рисунков вместо эмодзи, листами
- Нарисовано и внедрено 11.10.2026. Владелец сгенерировал семь полотен по
промтам из ТЗ_Иконки_Интерфейса_Листами.md
(расклад
4x4на 2048×2048 и3x2на 1536×1024) плюс лист нижнего менюnav_all_3x2. Нарезано 108 иконок:deploy/slice-icon-sheet.pyрежет полотно по раскладке изmodels/emoji-sheets.json, обрезает каждую плитку до объекта, кладёт на квадрат и вырезает фон. Полные полотна изpublic/charactersубраны вart-source/— в бандл они попадать не должны. - Нарезчик выучил прозрачный фон. Полотна пришли не на зелёном хромакее, а с
прозрачным фоном: альфа листа теперь сохраняется как есть, повторная вырезка не
запускается. Это нашлось на живом примере — у
uiic_stat_dot(тёмно-зелёный череп «урона со временем») повторный зелёный ключ съедал сам предмет. - Нижнее меню нарисовано.
BottomNavбольше не рисует SVG-контур: вкладки — файлыuiic_nav_chat/capital/battle/hero/menu.png, эмодзи рядом остался только аварийным запасомGameIcon. Неактивную вкладку гасит CSS-фильтр (.bottom-nav__img), активную отдаёт как есть — цветом картинку не покрасить. Старыеui_nav_*.pngиui_btn_battle.pngс белым фоном удалены: хромакей у них не резался, и плитка «Клан» показывалась белым квадратом. Плитка «Клан» в подменю героя переведена наuiic_nav_clan. - Эмодзи с экранов убраны — около 250 мест в 40 файлах. Герой, бой и арена,
вылазки, столица, профессии (ювелир, алхимия, чары, кузня, начертание), задания,
рюкзак, сундуки, клан, рейтинги, обучение, меню, карта. Эмодзи нигде не удалён:
он передан в проп
emojiи показывается только если файл не загрузился. Новые общие классы —.ico-emoji(аварийный эмодзи по кеглю подписи) и.ico--xl(40 px под большие карточки). - Двенадцать иконок подключились сами. Код давно просил их динамически
(
uiIconPath('stat','hp'),uiIconPath('role','brute'),uiIconPath('hunt','stop')), но файлов не было — теперь они на месте, и правок в коде не потребовалось. Их не видел прежний учёт эмодзи:deploy/emoji-report.pyнаучен такие запросы находить, сейчас в отчёте ноль запросов без файла. - Живая проверка.
node deploy/check-icons-live.cjs— headless-Chrome на локальной площадке, три экрана: 35 + 21 + 17 нарисованных иконок, ни одной битой и ни одной нулевой. Эмодзи на экранах осталось два, и оба не иконки: подпись тоста «📜 Кодекс: …» и типографский✕в кнопке закрытия. Снимки —models/иконки-проверка/. - Новые проверки в репозитории.
deploy/check-icon-paths.pyловит путь к картинке, которого нет файлом (опечатку не видят ни типы, ни сборка); из 156 путей 21 пустой — и все 21 из других ТЗ: фоныbg_*, эффектыfx_*. Девять мест под доборный листextra(крит, общий значок подземелий, Немота, эпические ресурсы, два свитка заточки, правая стрелка, стороны Света и Тьмы) уже описаны промтом — осталось нарисовать.
Окно персонажа: иконка надетого предмета больше не выезжает из слота
- Владелец, 11.10.2026: «У тебя едет вёрстка на странице героя, если добавить оружие из сумки». Раскладка при этом не меняется ни на пиксель: ряд фигуры, фигура и сторона слота те же, что с пустыми слотами (159 px / 90×159 / 50 px). «Ехала» иконка в слоте — и причин было две.
- Стартовое оружие нарисовано файлом без приставки
uiic_. Топор, меч, молот, кинжал, копьё, лук, посох, кистень и остальные стартовые стволы лежат вpublic/charactersпод именем самого предмета —blunt_axe.png,rusty_sword.png. Клиент же просилuiic_<id>.png, файла не было, иGameIconуходил на эмодзи: в золотой рамке оказывался топор-эмодзи. ТеперьitemIconPathзнает такие линии (PLAIN_ICON_IDS). Сверка всего каталога: было 11 предметов «рисунок нарисован, а клиент его не просит», стало 0. - Эмодзи-запаска считалась от потолка, а не от слота. Кегль брался от
--cw-slot— это потолок стороны (121 px), а реальная сторона на 360×640 равна 50 px, поэтому глиф выходил 60,8 px и торчал за рамку на 10,5 px. Теперь сторона слота — контейнер размеров (.hero-slot { container-type: size }), а глиф и метка заточки меряются вcqh: глиф 24,9 px, за рамку не выходит ничего. Заодно метка «+N» перестала быть размером почти во весь слот (20,7 → 8,5 px при стороне 50). - Вёрстку запаска не двигает. Сторона слота, высота ряда и его верхняя граница одинаковы до и после появления эмодзи — размеры слота задаёт ряд, а не содержимое (это отдельная проверка в скрипте).
- 35 предметов каталога пока без рисунка — и будут показываться эмодзи, но
теперь по размеру внутреннего поля рамки. Это
iron_sword,arcane_staff,wooden_shield,iron_shield,leather_armor,chain_armor,leather_helm,leather_gloves,leather_boots,cloth_cloak,leather_belt,ring_of_power,ring_of_agility,earring_of_wisdom,pendant_of_spirit, четыре свитка заточки и пятнадцать материалов для сбора. Список предметов без рисунка стоит держать под рукой: он же — список того, что осталось нарисовать. - Как проверял.
tscклиента иvite build— чисто. Живая проверка —deploy/check-hero-slot.cjs(герой с надетым стартовым топором, 360×640): 11 проверок, провалов ноль — в слоте рисунокblunt_axe.png(1024 px) и это именно файл предмета; внутри рамки слота ничего не выходит; тот же рисунок в клетке сумки и сумка закрывается; по событию «файла нет» появляется эмодзи 24,9 px и тоже влезает; вёрстка при этом не двигается. Снимки —models/слот-оружие-рисунок-360.png,слот-оружие-сумка-360.png,слот-оружие-запаска-360.png. - Что осталось за кадром. Выката не было: правка лежит в рабочей копии. Отдельно измерить этот дефект на боевом клиенте нельзя — там экран героя всё ещё раздавлен лорной панелью (ряд фигуры 8 px, слоты нулевые), а это лечится переездом ЛОРа в раздел героя (запись ниже). Рисунки 35 предметов — работа владельца.
Лор: панель зоны переехала в раздел героя
- Владелец: «ЛОР на странице героя должен переехать в другое место, например в место раздела „Характеристики“, т.к. он больше не нужен». Речь про лорную панель зоны (зона, шкала Немоты, Прорыв, действия, кодекс): она стояла полоской над вкладками и была видна всюду — на карте, в столице, в чате и на экране героя. Замер боевого клиента до правки (360×640, обычный боец): панель 145 px, вкладка 371 px, ряд фигуры 8 px, фигура 5×8, слот круглый ноль — то есть на экране героя панель раздавила окно персонажа почти в ноль.
- Панель стала разделом «Лор». Открывается из подменю героя («☰ Разделы») и заняла место «Характеристик»: их числа дублировали панели «Основные» и «Магия» под фигурой, отдельный раздел был не нужен. Состав подменю снова шесть плиток: Герой, Атрибуты, Умения, Таланты, Лор, Клан.
- С других вкладок панель убрана совсем — так решил владелец в этой же правке. Вход в неё теперь один: раздел героя. Кодекс «Эхо Раскола» открывается кнопкой в шапке панели, поэтому шапка (зона, счётчик записей, Слово Смотрителя, Голос Немоты) рисуется всегда — даже там, где лорной карточки у зоны нет; иначе из нового места пропал бы единственный вход в кодекс.
- Иконка раздела — свиток, пока файла нет. Плитки подменю теперь рисует
GameIcon, а не голый<img>:uiic_tab_lore.pngещё не нарисован, поэтому видно 📜, а появившийся файл подхватится сам, без правок кода (как у остальных иконок набора). В CSS у панели появился режим карточки раздела (zone-lore--tab): в подменю ей не нужны отступы полоски над вкладками. - Стало (тот же герой, 360×640): вкладка 522 px, ряд фигуры 159, фигура 90×159, слот 50. Окно по-прежнему влезает в экран целиком, прокрутки нет.
- Как проверял.
tscклиента иvite build— чисто (в собранном бандле естьzone-lore--tabиuiic_tab_lore, аuiic_tab_statsи раздела «Характеристики» в подменю героя больше нет). Живая проверка —deploy/check-lore-tab.cjsна локальной площадке (deploy/serve-local.cjs, данные боевого сервера): 18 проверок, провалов ноль — панели нет ни на экране героя, ни на чате, ни в столице; между верхней панелью и вкладкой пусто; в подменю шесть разделов и «Лор» стоит пятым с иконкой 📜; раздел открывает панель зоны со шкалой Немоты, действием и кнопкой «📜 3 / 72»; кодекс открывается (шесть категорий) и закрывается; возврат на «Героя» даёт ту же фигуру. Снимки —models/лор-экран-героя-без-панели-360.png,лор-подменю-героя-360.png,лор-раздел-360.png,лор-кодекс-360.png,лор-чат-без-панели-360.png,лор-столица-без-панели-360.png,лор-возврат-на-героя-360.png. Замер «до» снят тем же скриптом в режимеMEASURE=1против боевого клиента, где панель ещё стояла над вкладками. - Что осталось за кадром. Выката на боевой не было: правка лежит в рабочей
копии.
uiic_tab_lore.pngне нарисован — в подменю свиток-эмодзи; промт на иконку стоит завести вместе с остальными иконками разделов. Временные герои живых проверок (tabs_lore_*,serve_*) убираютсяdeploy/clean-my-test-heroes.cjs(нуженSSH_PASS). - Одно число из убранного раздела нигде не повторяется. Кроме дублей
(«Атака», «Магия», «Хил/щит», «Защита», «Крит», «Уворот», «Вампиризм» — всё
это есть в панелях под фигурой) в «Характеристиках» были реген HP и реген MP
вне боя (
stats.passiveHpRegen,stats.passiveMpRegen). Сейчас их не показывает ни один экран. Сервер эти поля по-прежнему считает и отдаёт; решение владельца нужно: вернуть их строкой в панель «Основные» (панели станут по семь строк) или не показывать вовсе.
Зоны охоты: одиннадцать вместо тринадцати
- Одна зона на полосу уровней. Владелец: «мне нужны другие зоны охоты, с другими названиями; по одной зоне 1–10, 10–20, 20–30, 30–40, 40–50, 50–60; 60+ — пять зон; адаптируй под новые расы». Было тринадцать зон: пять расовых стартовых (по одной на расу), пять оспариваемых поясов и три эндгейм-зоны. Стало одиннадцать: общая «Долина Пяти Костров» (1–10, там сходятся дороги всех пяти рас — расовых зон больше нет), по одной зоне на каждую следующую полосу — Вересковые Пустоши (10–20), Гулкие Гроты (20–30), Смоляная Падь (30–40), Стылые Клыки (40–50), Пепельный Предел (50–60), — и пять ярусов Раскола 60+: Кромка Раскола, Осколочные Поля, Чертог Эха, Тлеющее Сердце, Ядро Раскола. Вид зоны
racialв коде сталstarter, полеraceIdу локации убрано,starterLocationId(raceId)заменён константойSTARTER_LOCATION_ID. - Ярусы Раскола крепче от зоны к зоне — множителем, а не уровнями. Первая версия правки делала ярусы разными полосами уровней (62–63, 64–65…), и живая проверка карты показала, что выше 60 уровня в них не попасть: потолок героя — 60 (
player.ts), дальше идёт только ранг. Поймал это новыйdeploy/check-map.cjs— «ни одна зона не заперта» упало на четырёх запертых. Теперь все пять ярусов открыты с 60, верхняя граница уровней мобов растёт (61 → 69, оттуда приходят «бледно-красные» с наградой ×1,3), а саму силу даёт полеLocation.power: ×1 → ×1,12 → ×1,25 → ×1,4 → ×1,6. Оно умножает HP, атаку, броню, опыт и золото мобов зоны, поэтому средний моб «Ядра Раскола» — это HP 5820, атака 430 и опыт 1238 против 3700 / 263 / 737 на «Кромке». Выше 69 уровни не поднимаем намеренно: с 70 моб для героя 60 становится «красным», а у красных множитель награды 0,05 — охота превратилась бы в пустую. - Восемь новых мобов эндгейма.
rift_hound«Гончая Раскола» (60),shard_golem«Осколочный голем» (61),echo_wraith«Призрак Эха» (63),void_cantor«Певец Пустоты» (64),ember_lich«Тлеющий лич» (66),scarred_champion«Изувеченный чемпион» (67),core_devourer«Пожиратель Ядра» (68),world_breaker«Сокрушитель Мира» (69). Уровни внутри яруса подобраны так, чтобы работал выбор архетипа:pickFromберёт ближайшего по уровню, и пул с разбросом в десять уровней давал бы одну и ту же тварь на весь ярус. Два прежних моба 60+ (chaos_lord,world_guardian) остались и стоят в «Осколочных Полях» и «Чертоге Эха». Рисунков у восьми новых пока нет — в бою видны эмодзи, промты лежат вТЗ_Промты_Мобов.mdиТЗ_Промты_Атака_Мобов.md. - Правила доступа стали проще и честнее. Стартовая долина открыта всем и всегда; все зоны с 10 уровня требуют выбранной стороны (раньше было наоборот: до выбора стороны — только расовые, после — только оспариваемые). Оспариваются десять зон: пять поясов и пять ярусов; стартовая долина в территории не входит — за неё не воюют. Встречи с героями противоположной стороны бывают только в поясах 10–60: долина и ярусы Раскола — PvE-земли.
- Карта снова одна — и во весь экран телефона. Две вкладки («Мир» и «Оспариваемые 10+») и центральный узел «10+» убраны: одиннадцать зон лежат спиралью — от общей долины внизу по внешнему кольцу к ядру в центре. Владелец: «почему карты горизонтальные? я хочу, чтобы они были на весь экран мобильного телефона» — и это была правда: карта рисовалась в 16:9, вписывалась по ширине и вставала горизонтальной лентой посреди экрана, а сверху и снизу оставалась пустота. Теперь карта — абсолютный слой на весь экран (
object-fit: cover), полоски интерфейса (ресурсы сверху, подпись зоны и кнопки снизу) плавают поверх неё на затемнении, а все маркеры держатся в полосе 20–75% высоты, чтобы не оказаться под панелями. Промт переписан на вертикальный формат телефона (9:16 … 9:19,5), координаты зон пересчитаны под вертикальную раскладку. Запертые зоны видны на карте и объясняют причину в подписи: «🔒 нужен 40» или «🔒 откроется после выбора стороны (с 10 уровня)», а у ярусов в подписи стоит «×1,6». Попутно нашлась старая поломка: клиент просил карту какworld_map.jpg, а файл лежит в.png— фон карты не показывался вообще. Там же у фонов арены было то же.jpg, и шесть нижних тиров стояли без фона, а файлlocation_tauren_plateauотсутствовал вовсе; теперь расширения стоят в именах файлов, а тауренский тир заменён существующим пейзажем. - Промты на арт новых зон.
models/zones.json+models/ИКОНКИ-ЗОН.md: одиннадцать иконок зон (99 кредитов, ни одна пока не нарисована — на карте эмодзи). Фоны зон —ТЗ_Фоны_Локаций.md, одиннадцать промтов вместо шести расовых (тоже вертикальные, 9:16); карта —ТЗ_Карта_Мира.md(одна карта во весь экран телефона, вертикальная), аТЗ_Карта_10плюс.mdпомечен устаревшим: его промт перечислял пятнадцать территорий, которых в игре не было ни одной. - Карта нарисована и подключена. Владелец нарисовал вертикальную карту (
world_map.png, 720×1280, 9:16) — все одиннадцать зон с подписями прямо на рисунке (Valley of the Five Fires, Heather Moors, Echoing Grottoes, Tar Ravine, Frozen Fangs, Ashen Verge, Rift Edge, Shard Fields, Echo Hall, Smoldering Heart, Rift Core). Маркеры встали чуть выше нарисованных подписей, чтобы читалось и название, и маркер с уровнями; исключения — «Ashen Verge» (его подпись закрывает полоска ресурсов) и «Valley of the Five Fires» (её закрывают кнопки), там маркер стоит на самой зоне. Маркеры теперь считаются в процентах от картинки, а не от экрана: на телефоне карта обрезаетсяcoverпо краям по-разному (у 390×780 — по бокам, у вытянутого экрана — сильнее), поэтому слой маркеров получает ровно бокс картинки; без этого зона уезжала бы с нарисованного места на другом телефоне. Попутно закрылись две ловушки: клиент просил карту только как.png, а выкат пережимает её в.jpgи PNG удаляет (теперь перебор.jpg→.png, как у фонов локаций), а сам выкат ужимал карту в горизонтальную коробку1400x800, то есть до 450×800 — коробка стала вертикальной (1440x2560), рисунок остаётся 720×1280. И ещё одно: снимки вкладки нельзя делать «всей страницей» — карта живёт в100vh, Chrome при такой съёмке пересчитывает вьюпорт, и кадр перестаёт соответствовать экрану; теперьcheck-map.cjsснимает только видимую область. - Как проверял.
farm-zones.jsпереписан под новые инварианты: одиннадцать зон, полосы стыкуются (1–10 → … → 50–60,LOCATIONS.length === 11), у героя 60 в стартовой долине мобы только 1–10, архетипы только из списка зоны, у ярусов растут множитель, потолок уровней, атака и опыт, красных мобов на верхнем ярусе нет (500 проверок).territory.js— состояний ровно столько, сколько оспариваемых зон (десять). Живая проверка карты —deploy/check-map.cjsна настоящемWorldTab(стендdeploy/stand-map.tsx, список зон берётся из собранных данных сервера): 60 уровень со стороной — 11 маркеров, ни одной запёртой, уровни на маркерах совпадают с данными, клик по зоне меняет подпись и даёт «Охотиться здесь»; 5 уровень без стороны — заперты десять из одиннадцати; 35 уровень — заперты семь; карта во весь экран — бокс карты совпадает с экраном телефона (390×780), картинка растянутаcoverи заполняет бокс целиком, слой маркеров совпадает с боксом картинки, каждый маркер стоит на своей точке изLOCATIONS, маркеры не налезают друг на друга и ни один не попал под полоски интерфейса, файл карты найден перебором расширений. Всего 31 проверка, провалов ноль, снимки —models/карта-11-зон-60.png,карта-11-зон-выбор-зоны.png,карта-11-зон-новичок.png,карта-11-зон-во-весь-экран.png.tscсервера и клиента собираются; зелёныеfarm-zones,territory,territory-pvp,autofarm,autohunt-check,races-classes. Восемь тестов профессий и чар падали и до этой правки — это незакрытая переработка трёх слотов профессий; в серверномtscостаются два чужих предупрежденияcombat.ts(redirectToPet,petTurn) из той же соседней работы. - Что осталось за кадром. Выката на боевой не было: правка лежит в рабочей копии. Рисунков новых зон и мобов нет — это работа владельца по промтам; иконки зон подхватятся сами, файлы
uiic_zone_<id>.pngклиент ищет по id. Скрипт живой проверки на боевомdeploy/test-farm-live.cjsпереписан под новые правила (стартовая долина открыта и со стороной, десять оспариваемых зон, у яруса виден множитель ×1,6), но не прогонялся: для него нуженSSH_PASSи доступ к боевому серверу.
Таверна: свой фон, фоны профессий и стилизация перековки с заказом
- У страницы профессии теперь свой фон. Раньше экран профессии был «голым списком» на затемнённом здании. Заведён отдельный слой
.prof-bgсdata-prof: пять картинокbg_prof_<профессия>.jpg— по одной на профессию, и берётся файл по профессии героя (capital.profession.profession), а не по месту. Подэкраны перековки и заказа — часть той же страницы, поэтому носят её фон. Завеса здесь плотнее, чем в зданиях:0.6 → 0.85вместо0.42 → 0.72, потому что на странице много текста и чисел; содержимое поднято над завесой обёрткой.prof-screen(z-index: 1) — без неё завеса легла бы поверх карточек. - Фон таверны подключается файлом, код уже готов. Здание таверны рисует тот же слой
.place-bg[data-place='tavern'], что и кузня: правило было заведено начертанием, правок не потребовалось. Пока файла нет, видна одна завеса, меню учителя работает — проверено блокировкой запроса картинки. - Перековка перестала быть двумя
select. Предмет выбирается плитками (иконка, имя, заточка и ранг), цель — плитками того же ранга и группы, и на каждой плитке сразу видно, что изменится («Атака -4 · Крит, % +2»). Выбранная пара показана карточкой с двумя иконками и стрелкой, а разница бонусов — колонками «сейчас / станет» с подсветкой: выросшее зелёным, потерянное красным. Материалы стали плитками «есть / нужно» с иконками ресурсов, недостающие подсвечены. Над кнопкой появилась строка «Не хватает: Железная руда ×1, Уголь ×1» — раньше кнопка просто гасла без объяснения. - Заказ на ковку собран в карточку. Рецепт — плитками (имя, ранг, шанс), сводка заказа — карточкой: что куётся, что уйдёт в резерв плитками материалов, золото за работу монетой и шанс ковки. «Ковать наверняка» — переключатель вместо мелкого чекбокса: под ним видно «Шанс ковки 50% → 60%». Цена вводится с монетой внутри поля и подсказкой минимума, рядом — комиссия и сколько получит продавец. Внизу — предпросмотр лота: то, что увидят покупатели, с метками «заказ», «наверняка» и ценой.
- Сервер не тронут, вызовы те же.
forgeReforgeиauctionOrderполучают ровно те же данные (проверено на стенде:forge-sword → smith_axe_1иsmith_axe_1, 1000, true). Выпадающих списков и чекбоксов на обоих экранах не осталось, на 320 px горизонтальной прокрутки нет. - Живая проверка — 63 утверждения, провалов ноль.
deploy/check-tavern.cjsсобирает стенд из настоящегоCapitalTab(боевую базу ради снимка не трогаем: столица открывается только у героя со стороной) и проходит путь игрока: столица → таверна → «Перейти» → «Перековка» / «Выставить заказ». Снимки:models/таверна-фон-360.png,таверна-фон-нет-360.png,профессия-smithing-фон-360.png,профессия-alchemy-фон-360.png,перековка-360.png,перековка-нехватка-360.png,заказ-360.png,заказ-полностью-360.png,перековка-320.png. - Стенд научен не спотыкаться о соседнюю сессию. Гасить модуль
/@vite/clientнельзя — стенд его импортирует, и страница не отрисовывается вовсе (это давало пустые кадры). Теперь модуль грузится, а живое соединение HMR подменяется пустышкой: правки соседа не перезагружают страницу посреди шага. Порт (9372) и папка стенда (_stand-tavern) свои, чтобы прогоны не затирали друг друга. - Фоны ещё не нарисованы — промты готовы.
models/rooms.json+models/ФОНЫ-КОМНАТ.md: шесть промтов (таверна и пять профессий), негатив отдельным полем, что проверить до заливки и куда положить файл. Нарисовать и разложить:node deploy/bg-rooms.cjs run all(6 × 9 = 54 кредита) иnode deploy/bg-rooms.cjs publish(PNG → JPEG 720 px, как у фона кузни). - Фон проверен в трёх состояниях, а публикация — не по документации. Файла нет — затемнение, экран работает (запрос картинки блокируется через CDP, нарисованный файл при этом не удаляется); файл временный — подхватился и убран после прогона; файл нарисован — остался на месте. Проверка с настоящим рисунком (в роли таверны взял нарисованный фон кузни) показала главное: панель читается, интерьер виден сквозь завесу.
publishпрогнан целиком: 1080×1920 → 720×1280 JPEG 20 КБ, какbg_forge.jpg; принимаетpng,jpg,jpeg,webpи называет по именам те исходники, которых не нашёл, — молчаливый пропуск заметили бы только по пустому экрану. - Выкачено на боевой 06.10.
node deploy/redeploy.js: клиент и сервер,service: active,game: 200,api: 200, все 10 файлов сборки сверены по размеру, боевойindex.htmlссылается на те же бандлы, что вdist. Нашлась ловушка в самом выкате:redeploy.jsжмёт все*.jpgв/charactersдо 512×768, аbg_*в исключениях не было — шесть свежих фонов уехали бы как 432×768, то есть мылом. Теперь уbg_*.jpgсвоя строка (-resize '720x1440>' -quality 82, как у фонов экрана героя), и на боевом все восемь фонов — в 720×1280 (провереноidentifyи HTTP: 200,image/jpeg, 138–195 КБ). Это же правило вылечило и уже лежавшие тамbg_forgeсbg_market. - Фоны нарисованы владельцем и подключены 06.10. Шесть картинок пришли PNG по 1,4–1,5 МБ прямо в
public/characters; исходники убраны вmodels/art/buildings/, аpublishужал их в JPEG 720 px — 123–172 КБ вместо 1,4–1,5 МБ (какbg_forge.jpg). Живая проверка прогнана заново уже на настоящем арте: все шесть фон распознаны как нарисованные (скрипт их не удаляет), 63 утверждения, провалов ноль, снимки обновлены; интерьер читается сквозь завесу, карточки панели — поверх неё. - Лот на сервере — тот же. Серверную половину последнего пункта ТЗ закрывает не только стенд, но и код:
game/server/src/game/capital.tsс 04.10 не менялся (коммитeeabbd9), аtest/professions.js§12 уже проверяет резерв, золото в резерве, метку заказа и клеймо мастера, шанс 0.6 с флагом «наверняка», возврат Эйронов при снятии заказа. Разовый прогонputOrderподтвердил и поля лота:reserveравен стоимости рецепта,gold— цене работы,sure— флагу,chance= 0.5 + 0.1. - Нашлось попутно, за рамками ТЗ. В таверну ведёт только раздел учителя:
enter()ставитsection = 'teacher', а переключателя разделов в здании нет — панель отдыха и товаров (section === 'tavern') недостижима из интерфейса. Это не входило в задание и не трогалось, но владельцу стоит знать.
Кузня: фон нарисован и подключён
- Рисунок владельца подключён, но файлом он не подхватывался. Фон пришёл как
bg_forge.png(1,5 МБ), а слой здания запрашиваетbg_forge.jpg— то есть сам по себе рисунок не появился бы. Исходник убран изpublicвmodels/art/buildings/bg_forge.png(рядом с исходниками столиц), а в клиент опубликован JPEG:ffmpeg -vf scale=720:-2 -q:v 5, как это делаетdeploy/art.cjsдля фонов героя и столиц. Было 1 504 174 байта, стало 178 788 — в 8,4 раза легче при той же ширине 720 (у фонов героя и столиц ровно столько же). - Живая проверка прогнана с настоящим фоном — 26 проверок, провалов ноль. Файл отдаётся как
image/jpeg 200и встаёт первым вbackground-imageслоя.place-bg[data-place='forge']; завеса::after0.42 → 0.72 осталась не глухой, интерьер виден, а панель и подписи читаются поверх него. Снимок —models/кузня-фон-360.png. - Отсутствие файла теперь проверяется блокировкой запроса, а не удалением. Старый
deploy/check-kuznya.cjsперед первым пунктом убиралbg_forge.jpg— с появлением настоящего арта это стало опасно: скрипт удалил бы рисунок владельца. Теперь он блокирует запрос через CDP, а временную заглушку (deploy/bg-forge-test.py) заводит и удаляет только тогда, когда рисунка нет вовсе. Нарисованный фон скрипт не трогает никогда. - Стенд стал устойчивым к соседней сессии. В этой же рабочей копии параллельно шла другая работа, и её сохранения вызывали HMR-перезагрузку страницы стенда — один прогон снял чужой экран (экран клана вместо кузни). Теперь проверка ждёт отметку готовности
#stand[data-ready]и повторяет заход до трёх раз, а исключения страницы печатает в лог. Блокировать клиент HMR нельзя: его импортирует сам модуль стенда (import.meta.hot), и с блокировкой React не монтируется — проверено, это и был тот случай «страница есть, а экрана нет». - Что осталось за кадром. Выката не было: правка ювелира и файл фона лежат в рабочей копии. Живого кадра с боевого экрана кузни нет — для входа нужен герой со стороной. Полная документация — в 04 — Экипировка и дроп.
Задания: экран читается по состояниям
- Было «плоско», стало по состояниям. Владелец: «в заданиях требуется переделка стилизации, она должна быть более современной». Три состояния теперь различимы боковым зрением: готово к награде — золотая карточка со свечением и кнопкой «Забрать награду» на всю ширину; в работе — обычная карточка с полосой и числом внутри; получено — приглушённая карточка с галочкой ✅, полной серой полосой и без кнопки. Раньше всё было одной яркости, а готовое отличалось только цветом рамки.
- Вкладки — подчёркиванием, а не рамкой. Активная вкладка подчёркнута золотом, у каждой есть иконка и пилюля-счётчик «3/5», а у вкладки с готовыми наградами рядом с названием горит золотое число — видно, куда идти, не читая подписи.
- Награда — пилюлями с нарисованными иконками. Была строка текста из эмодзи «💎1 · 💰5000 · 🔑1». Теперь новый
QuestReward.tsxрисует ряд пилюль, а иконки берутся настоящие:uiic_cur_eron,uiic_cur_key,uiic_gold. Ключ подземелья был нарисован и не использовался нигде — теперь он в наградах. Свитков заточки и набора K в файлах ещё нет, поэтому у них работает запасной путьGameIcon, и файлы подхватятся сами. У ресурсов профессии иконки нет намеренно: их набор зависит от профессии героя, один файл на четыре набора не подставить. - Категория задания — значок слева от имени (⚔️ охота, 🏹 фарм, ⚒️ ремесло, 🏰 клан, 💰 богатство…). Считается на клиенте по префиксу id, сервер не трогали.
- Описание обрезано двумя строками с многоточием: карточки одной высоты, список не «рвётся». Полоса прогресса стала толще и держит число внутри.
- Цепочки разовых переделаны. Полоса из сегментов — по одному на шаг: видно, сколько пройдено и сколько впереди, а не только «3 из 18». Пройденное свёрнуто в строку «✅ Пройдено: 3» и разворачивается списком с названиями и наградами (раньше его не было видно вовсе). Следующие шаги — приглушённые карточки с 🔒 вместо серого текста: видно, что они закрыты, а не «сломаны». Дальше двух шагов список не раздувается — остальное строкой «и ещё N».
- «Забрать всё» закреплена внизу и не уезжает при прокрутке: прокручивается только список. Раньше на длинном списке кнопку приходилось искать. Скелетон из трёх карточек заменил строку «Задания загружаются…». Появление карточек — лёгкая анимация, и она выключается при
prefers-reduced-motion. - Бонус за день получил работающую кнопку — и это единственная правка сервера. Требование ТЗ (2.5.2) — забирать бонус отдельной кнопкой. Кнопку было нечем нажать: бонус выдаётся внутри
claimQuestвместе с пятой ежедневной, аclaimAllидёт по готовым заданиям и голый бонус не подбирает — к моменту его готовности готовых ежедневных уже нет. Поэтому вclaimQuestдобавлена ветка дляbonus_daily: отдаёт ключ, если бонус собран и ещё не забран, и честную ошибку иначе. Существующее поведение не изменилось: пятая ежедневная по-прежнему выдаёт бонус сама. Получение ключа показывает тост «Бонус за полный день: +1 🔑». - Иконки заданий и сундуков нарисованы.
uiic_quests.pngиuiic_chests.pngпоявились вpublic/characters— заголовок экрана заданий показывает нарисованный свиток, а кнопка «Сундуки» на экране героя свой сундук. Заодно закрылся открытый вопрос №5 вТЗ_Промты_Иконок_Эмодзи.mdи последний пунктТЗ_Герой_Экипировка.md. - Как проверял.
tscклиента иvite build— чисто; в собранном бандле есть вся новая разметка (quest-head__title,quest-tab__pill,quest-reward,quest-pill__ico,quest-segs,quest-skel__line,quest-chain__lock,quest-foot,quest-bar__num), и все новые стили есть в собранном CSS, включая блокprefers-reduced-motion. Тестgame/server/test/quests.jsполучил раздел 10: бонус забирается отдельной кнопкой, повторно не выдаётся, неготовый не отдаётся, забранный уходит из счётчика готовых, а обычный путь через пятую ежедневную не сломан. Весь набор тестов сервера — без падений. - Что осталось за кадром. Иконок набора K (
uiic_ui_check,uiic_ui_lock,uiic_ui_sections) всё ещё нет — галочка, замок и иконки вкладок работают эмодзи ✅/🔒/📅, как и предписывает ТЗ. Сервер тронут на одну веткуclaimQuest, о чём сказано выше. Живой проверки в браузере не было. Полная документация — в 05 — Задания.
Кланы: создание без экипировки, заявки и передача лидерства
- Экипировка больше не пропуск в клан. До этой правки сервер не давал создать клан и вступить в него, пока не надеты оружие и четыре части брони — правило стояло одной проверкой
canClanна оба действия. Теперь спрашивают только сторону и 15 уровень. Это было единственное место в игре, где надетые вещи открывали механику, а не давали статы. - Требования видно до нажатия. Вкладка «Клан» показывает условия списком: сторона, 15 уровень (со своим уровнем рядом) и отдельной строкой «Экипировка не нужна», плюс цену создания — 50 000 золота и 500 CP.
- Вступление — по заявке. Открытого входа нет: герой подаёт заявку в списке кланов, офицер или лидер принимает её либо отклоняет. Заявка одна на героя, подача в другой клан снимает прежнюю, а принятому клан приходит сразу — без перезахода.
- Роли заработали. Офицер принимает заявки, исключает рядовых и поднимает уровень клана; лидер назначает и снимает офицеров, исключает любого, кроме себя, передаёт клан (👑 в строке участника) и распускает его кнопкой с подтверждением. Раньше офицер умел только поднимать уровень, кика не было вовсе, а выход лидера удалял клан у всех.
- Клановое собрано в одну вкладку. Клановая охота и территории переехали из «Меню» во вкладку «Герой» → «Клан»: шапка клана с казной и полосой до следующего уровня, охота с полоской HP общего босса, территории, заявки, состав карточками. В «Меню» клановых плиток не осталось.
- Фон клана подключается файлом. Слой
.place-bg[data-place='clan']берётbg_clan.jpgизpublic/characters/; пока картинки нет, видна затемнённая завеса, вкладка работает как раньше. - Правила клана вынесены в
game/clan.ts— чистые функции без сокетов и базы: пропуск в клан, заявки, кик, передача, роспуск, числа босса. Их проверяет новыйgame/server/test/clans.js(36 проверок), аindex.tsостался обработчиками событий. Заодно сервер при входе сверяет членство: заявку могли принять, пока герой был офлайн, а клан — распуститься. - Как проверял.
tscсервера и клиента — чисто;clans.jsи соседние тесты (territory.js,quests.js,farm-zones.js,socket-guard.js,territory-pvp.js) проходят. Живая проверка в браузере —deploy/check-clan.cjsна настоящемClanTab(виды: лидер, офицер, рядовой, без клана, 12 уровень, «Меню»; подставные данные, экипировки у героя нет вовсе): 38 проверок, провалов ноль. Снимки —models/shot-clan-leader.png,-officer,-member,-none,-bg,-menu. - Что осталось за кадром.
bg_clan.jpgне нарисован — промт вТЗ_Клан.md, раздел 5. Уровень клана по-прежнему даёт только места в составе: бонус членам (+1% золота и опыта за уровень) — предложение из ТЗ, владелец его пока не подтверждал. Иконок ролей и клановых разделов в арте нет — стоят эмодзи.
Кузня: живая проверка экранов и третий стухший текст
- Нашёлся текст, который всё ещё отправлял игрока в кузню за камнем. Правка 04.10 обещала, что ни один текст не отправляет из одного здания в другое, и это было верно для двух текстов из трёх: внизу карточки «Камень в предмете» у ювелира осталась подпись «💎 Вставка камня живёт в здании «Кузня»…». К этому времени вставка уже стояла на экране ювелира карточкой «Камни в экипировку» — подпись противоречила и ТЗ (2.3.2), и самой разметке под собой. Теперь она говорит правду: «Камни — работа ювелира целиком: огранка, гнездо, вставка, переогранка и извлечение стоят на этом экране… Рунные гнёзда — не сюда: их делает кузня». Заодно поправлен комментарий над
JewelryPanel, который утверждал то же самое. Больше вsrcнет ни одного текста про вставку камня в кузне: упоминания кузни остались только у рунных гнёзд — это её механика. - Живая проверка в браузере — 26 проверок, провалов ноль. Новый
deploy/check-kuznya.cjsсобирает стенд из настоящих компонентов: экран кузни открывается кликом по зданию на карте столицы (CapitalTab→ForgePanel), экран ювелира — панельюJewelryPanel; подставлены только игрок, каталог и рецепты. Стенд, а не боевой экран, потому что столица открывается лишь у героя со стороной и 10 уровнем, а заводить такого героя в боевой базе ради снимка нельзя — там живые игроки. Headless-Chrome, 360×720. - Что именно проверено. В кузне: слой
.place-bg[data-place='forge']есть, фон запрашивается файломbg_forge.jpg, завеса::afterровно 0.42 → 0.72, панель открывается кликом по зданию, кнопки «Создать» и «♻️» на месте, при отсутствии картинки панель работает как раньше; нет «Камни в экипировку», нет «Материалы» (хотя ресурс лежит в сумке), нет названий профессий у предметов. Услуга жива: «Создать» отвечает баннером, цена «Следующее: 6000 💰», на предмете с тремя гнёздами — «Все три гнезда созданы». У ювелира: карточка вставки стоит ниже «Особого» (гнезда) и выше «Камня в предмете», плитка камня погашена, пока предмет не выбран, после выбора включается, нажатие вставляет камень тем же действиемjewelryInsert, подсказка меняется на «Теперь нажми на камень», текста про «в кузне» нет. - Фон подключается файлом — проверено временным файлом. Картинок фонов в проекте нет, поэтому
deploy/bg-forge-test.pyрисует заглушку с подписью (не арт), скрипт кладёт её вpublic/characters/bg_forge.jpg, снимает кадр «интерьер виден сквозь завесу» и удаляет. Снимки:models/кузня-фон-нет-360.png,models/кузня-фон-360.png,models/кузня-гнездо-создано-360.png,models/ювелир-камни-360.png,models/ювелир-камни-полностью-360.png. Стенд (deploy/stand-kuznya.html,deploy/stand-kuznya.tsx) живёт вdeploy/, на время прогона копируется вgame/client/_stand/и удаляется — в сборку не попадает. - Как проверял.
tscклиента иvite build— чисто;jewelry.js,forge-crafting.js,professions.js,runes-stats.js,socket-guard.js— все проходят. Сервер не тронут:forgeSocket,forgeSmelt,jewelryInsertте же, что и были. - Что осталось за кадром.
bg_forge.jpgне нарисован — рисовать в этой среде нечем, промт вТЗ_Кузня.md, раздел 5, код к нему готов. Живого кадра с боевого экрана кузни нет: для входа нужен герой со стороной, а это запись в боевую базу. Полная документация — в 04 — Экипировка и дроп.
Экран охоты: компактно на телефоне
- Заголовок «🏹 Охота» убран — на телефоне он занимал строку, а смысла не нёс: и так видно, где игрок.
- Панель охоты больше не картинка в кованой рамке. Был фон
uiframe_panel_hunt.pngс жёсткимaspect-ratio: 768/573, из-за чего панель занимала почти пол-экрана и вытесняла список мобов. Теперь это обычная карточка: тонкая рамка, плотные отступы, таймер уменьшен с 76 до 54 px, а кнопка «Завершить» переехала в строку с таймером — отдельный ряд кнопок стоил ещё сорок пикселей по высоте. - Полоса автофарма поджата: таймер 17 px, кнопка-иконка «⏹» 24 px, счётчики одной строкой, длинная подпись про «эту вылазку» убрана, лента боёв показывает 3 строки вместо 8.
- Два вида фарма — карточками в одну строку. Были две широкие кнопки друг под другом, а под ними абзацы подписей; теперь рядом две карточки: иконка в плашке, название, цена и подпись живут внутри кнопки, у «до нуля энергии» рамка и заливка заметнее — это основной вид. Блок стал ниже на две строки, поэтому список мобов влезает целиком. Не хватает Эйронов — прямо в карточке видно «🔒 нужно 8» вместо цены, кнопка при этом неактивна.
- Итог: на экране 390×844 видно все шесть мобов вылазки, а не только их верхушку. Проверено снимками с боевого (
models/autofarm-1-buttons.png,models/autofarm-2-running.png); ошибок в консоли, в том числе 404 по картинкам, нет.
Аллея героев: значок класса и фракция в рейтинге
- В строке рейтинга появился значок класса. Владелец: «у каждого игрока в рейтинге должен стоять значёк класса. И к какой фракции относится». Слева от имени встал
GameIconс путёмuiic_class_<класс>.png— тот же файл и тот же приём, что на арене и на экране героя, поэтому нового арта не понадобилось: 36 иконок классов уже нарисованы. Размер 26 px (в ТЗ 24–28), имя класса для запасного эмодзи берётся из справочника сервера, а не из своей таблицы. Серверных правок не потребовалось:classIdв строке рейтинга был и раньше, клиент его просто не рисовал. У вкладки «Клан» значка нет — там строка клана, а не героя. - Фракция в строке — новое поле сервера.
rating:listтеперь отдаётfactionв строке игрока и в строке клана — через общийratingFaction(), который пускает дальше толькоlight/dark, а всё остальное (в том числе пустоту) превращает в пустую строку. Пустая строка, а неnull: выбор стороны доступен с 10 уровня, и новичок в топе по уровню — обычное дело, строка обязана это пережить. В клиенте поле добавлено вRatingRow(store.ts). - Сторону видно цветом, а не только эмодзи. Значок стороны — кружок после имени: у света золотое кольцо, у тьмы багровое (
--faction-*вindex.css). Картинка берётся файломuiic_faction_<сторона>.png; пока файла нет,GameIconпоказывает 🛡️/⚔️, а подсказка при наведении называет сторону («Орден Света» / «Легион Тьмы») по справочнику сервера. Пустая фракция — значка нет вовсе, и строка не съезжает. - Название клана уходит первым. Порядок строки:
место · [класс] имя [· клан] [фракция] · значение. На узком экране (max-width: 340px) скрывается название клана, значки остаются — они и есть новое требование. Порог 340, а не 360: на 360 клан ещё помещается, проверено замером, а не на глаз. - Фон аллеи — файлом, без правок кода. Слой
.place-bg[data-place='heroes']стоит с прошлой правки фонов: достаточно положитьbg_heroes.jpgвgame/client/public/characters/. Пока картинки нет, виден затемнённый градиент, панель работает как раньше. - Как проверял.
tscсервера и клиента иvite build— чисто, все 23 теста сервера проходят. Стенд на настоящемRatingsPanelс подставными данными (обе стороны, новичок без стороны, длинный ник с кланом, вкладка «Клан») и headless-Chrome с подменой метрик устройства: вьюпорт 360 и 320 — переполнения нет ни в одной строке, значки классов 26 px во всех пяти строках, во вкладке «Клан» их ноль, значков сторон четыре (у новичка нет), битых картинок ноль. Отдельно проверен путь нарисованных значков: с временно положенными файлами картинки подхватились и встали на 16 px, после проверки временные файлы удалены. Снимки —models/аллея-рейтинг-360.png,models/аллея-рейтинг-320.png,models/аллея-рейтинг-клан-360.png. Живая проверка данных — только чтение, без записи в базу: боевойrating:listотдаёт 47 строк по мощи и уровню, 35 по арене, кланов 0; у всех строк естьclassId, а поляfactionбоевой сервер пока не отдаёт вовсе — то есть значок стороны сейчас не рисуется, и это ровно тот случай, который строка переживает. Иконка есть у всех 47 живых строк, кроме одной: классorc_fire_mageв манифесте отсутствует (наследство старого героя) — там будет запасной эмодзи, как и на арене. Из 37 классов манифеста не нарисован ещёundead_death_knight_hammer— это дырка в арте, не в коде. - Выкат и живой кадр на данных боевого (05.10.2026).
deploy/redeploy.js— сервер и клиент, сжатие картинок, перезапускgame, перезагрузка nginx:service: active,game: 200,api: 200, в логе «Территорий загружено: 8»; боевой отдаёт ровно ту сборку, что в рабочей копии (assets/index-DewBfTG5.js,assets/index-B1Qddpqv.css). Вики залита целиком —deploy/deploy-wiki.cjs, все 10 страниц,wiki index=200. После выката полеfactionприходит у всех 47 строк: 6 за свет, 8 за тьму, 33 без стороны, и все 14 значков совпали со стороной героя. НастоящийRatingsPanelиз выкаченной сборки отрисован на живом ответеrating:list: на 360 и 320 px переполнения нет, значок класса картинкой у 46 строк из 47 (47-я — тот самыйorc_fire_mage), сторон 14 из 14, битых картинок ноль. Снимки —models/аллея-живые-данные-360.png,models/аллея-живые-данные-320.png,models/аллея-живые-данные-арена-360.png. - Что осталось за кадром. Не нарисовано три картинки:
bg_heroes.jpg,uiic_faction_light.png,uiic_faction_dark.png— промты лежат в ТЗ_Аллея_Героев.md, раздел 5, подключаются файлами. Пока их нет, у значка стороны эмодзи 🛡️/⚔️, а у фона затемнение. Живой кадр снят на реальном ответе сервера, но не на боевом экране аллеи: для входа нужен герой, а временных героев на боевом мы не заводим.
Кузня: фон, и только гнёзда под руны
- В кузне больше нет чужой механики. Владелец: «в кузне находится инфа от профессии кузнечное дело, это не связанные механики. Так же там есть инфа про камни, тоже не оттуда». Убраны оба блока.
- Камни переехали к ювелиру — целиком. Вставка камня жила в кузне, а огранка и «Гнездо под камень» — у ювелира: половины одной механики стояли на разных экранах, и оба текста честно признавались в этом («Камень вставляют в кузне», «Камни при этом не вставляются — только место под них»). Теперь огранка, вставка, гнездо, переогранка и извлечение — на одном экране профессии, и ни один текст не отправляет игрока в другое здание. Механика не менялась: то же действие
jewelryInsert, те же проверки на сервере,jewelry.jsбез правок проходит. - Блок «Материалы» убран. Четыре кузнечных ресурса и их количество в кузне были чистой справкой: ни
forgeSocket, ниsmeltItemресурсов не тратят — только золото. Ресурсы расходуют рецепты, а рецепты у учителя в таверне; там же и на экране профессии их количество и видно. - Слово «профессия» ушло из строки предмета.
professionName(it.forge!.craft)печатал «Кузнечное дело» рядом с именем кованого предмета. Пользы это не несло, а в кузне читалось как чужая механика — подпись и функция удалены. Имя предмета, полоска гнёзд, «Создать» и «♻️ Переплавить» остались. - В кузне осталось ровно две услуги: гнездо под руну (до трёх, цена в золоте, 30% удачи) и переплавка. Внизу панели честная подпись: «Гнёзда под руны. Камни и огранка — у ювелира, руны — в Начертаниях».
- Фон кузни. Требование закрыто той же инфраструктурой, что и у других зданий: внутри здания есть слой
.place-bg, и файлbg_forge.jpgвgame/client/public/characters/подхватывается без правок кода. Пока картинки нет, видна затемнённая подложка. Промт — вТЗ_Кузня.md, раздел 5. - Как проверял.
tscклиента иvite build— чисто;jewelry.js,forge-crafting.js,professions.js,enchant.jsпроходят. Мёртвых импортов и состояний вForgePanelне осталось: от него остались две услуги и ни одного неиспользуемого хука. Заодно удалены осиротевшие стили.res-chip__ico/.res-chip__emoji(их носила только табличка материалов) —.socket-row,.socket-pips,.rune-gridи.rune-tileна месте и по-прежнему нужны..socket-costне трогал: он был мёртвым до этой правки, к кузне отношения не имеет. - Что осталось за кадром.
bg_forge.jpgне нарисован — рисовать нечем, промт готов. Живой проверки в браузере не было. Раздел «Материалы» изForgePanelудалён вместе со спискомRESOURCES; если владелец захочет видеть остатки ресурсов именно в кузне, это одна карточка обратно, но по ТЗ её там быть не должно. Полная документация — в 04 — Экипировка и дроп.
Экран героя: статы по нажатию на надетую шмотку
- Надетую шмотку можно потрогать. Слот экипировки был
<div>без обработчика: что именно надето, игрок узнавал только изtitle, а на телефонеtitleне показывается вовсе — то есть на мобильном не узнавал никак. Теперь слот с предметом — кнопка (остаётся доступной с клавиатуры), нажатие открывает попап предмета: крупная иконка, имя, слот, заточка («+N (+N% к статам)»), статы, клеймо мастера, руны с их статами и камни. Кнопка «Снять» — там же; отдельного «Надеть» в попапе нет, он открывается только для надетого. Пустой слот остался некликабельным: пустой попап только шумит. - Рюкзак и герой показывают одно и то же. Карточка предмета вынесена в общие компоненты:
ItemPopup.tsx(ItemPopup— окно,ItemCardBody— статы, клеймо, руны, камни) иitemStats.ts(STAT_LABEL,PCT,plusColor,statRows,stonesText).STAT_LABELлежал четырьмя копиями в разных файлах — теперь карта одна на все четыре экрана (рюкзак, попап героя, ювелир, столица), а числа считает одинstatRowsс тем же множителем заточки, что и раньше в рюкзаке. Разметка статов из рюкзака удалена как дубль, а не скопирована. - Попап закрывается по фону, крестику и кнопкой «Снять»; клик внутри карточки не закрывает — иначе попап схлопывался бы при попытке пролистать длинные статы.
- Промты на две иконки перенесены в набор K.
uiic_quests.png(свиток с восковой печатью) иuiic_chests.png(сундук с приоткрытой крышкой) были описаны вТЗ_Герой.md; теперь они лежат вТЗ_Промты_Иконок_Эмодзи.mdвместе с остальными 24 иконками набора, в том же формате — квадрат 1:1, 256×256, ровный#00ff00под вырезку. Открытый вопрос №5 про эти две иконки закрыт. Код был готов заранее:HeroQuickButtons.tsxуже указывает на оба файла, аGameIconпоказывает эмодзи, пока файла нет, — достаточно положить картинки вgame/client/public/characters/. - Как проверял.
tscклиента иvite build— чисто; разметка попапа и класс кнопки слота есть в собранном бандле. Рюкзак после перевода на общие компоненты собирается и показывает те же строки статов, что и раньше (числа считает тот жеstatRows, что и до правки). Иконокuiic_quests.png/uiic_chests.pngв копии по-прежнему нет — эмодзи 📋 / 🎁 остаются запасным вариантом, как и задумано. - Живая проверка на боевом (05.10.2026). Новые
deploy/kit-test-gear.cjs(выдаёт временному герою надетый комплект: оружие +5, броня +3 с камнем «Атака +32 (A)» и руной «Ярость A», шлем, перчатки +1, сапоги, сумка) иdeploy/check-item-popup.cjs(открывает игру в headless-Chrome и жмёт по слотам). Результат: слот с предметом —<button>, пустой —<div>; попап открывается по нажатию и показывает имя, слот, «Заточка: +5 (+5% к статам)», статы, клеймо мастера («⚒️ Ковано: Проверка · гнёзда 3/3 · руны 1»), руну с её статами («Руна «Ярость» A: атака +6, сила +2, крит +0.4%», «Итого от рун») и камень «💎 Атака +32 (A)»; рюкзак для того жеРжавого мечапоказывает те же["Атака+4"]; пустой слот попап не открывает; закрывается по крестику и по фону. Снимок —models/попап-предмета.png. - Что осталось за кадром. Две иконки набора K ждут рисунка:
uiic_quests.pngиuiic_chests.png— рисовать в этой среде нечем, промты лежат в наборе, код готов (положить файлы вgame/client/public/characters/, правок не потребуется). Иконки рун, наоборот, все 20 на месте, поэтому в попапе видно нарисованную руну. Полная документация — в 04 — Экипировка и дроп.
Начертание: статы рун, блок рун у предмета и фон здания
- Руна рассказывает, что даёт — до вставки. Было: плитка руны показывала только имя и количество, а вставка была необратимой (
insertRuneпереносит руну из сумки в предмет навсегда) — то есть решение принималось вслепую. Стало: нажатие на руну открывает попап с крупной иконкой, семейством, ступенью и таблицей «Даст герою». Числа считает сервер новым событиемrune:preview(по образцуcharm:preview): во что превратится семейство, решает класс владельца, а в оружие усиление идёт на четверть выше — клиент этого знать не может и формулу не повторяет. В попапе подписано, для чего посчитаны числа: для выбранного предмета или для оружия по умолчанию. Там же осталось предупреждение, что вставленную руну достать нельзя. Пример строки: «Ярость B: атака +15, сила +5, крит +1%» — у кастера то же семейство даёт силу заклинаний вместо атаки. - После вставки видно, что получил предмет.
rune:insertтеперь возвращает статы вставленной руны и новый итог предмета, а тост вместо «Руна встала в гнездо 2» пишет «Руна «Ярость» B встала в гнездо 2: атака +15, сила +5, крит +1%». - В рюкзаке — имена рун и их статы, а не число. Было «⚒️ Ковано: автор · гнёзда 1/3 · руны 1». Стало: новый компонент
ItemRunes— строка на каждую руну с именем и усилением плюс «Итого от рун». Считает по-прежнему сервер: список рун с посчитанными статами приходит в самом предмете (runes,runesText), рядом с уже существующимиbreakRefund,gemsиcharms. Тот же компонент готов для попапа предмета на экране героя — его делает соседнее ТЗ (ТЗ_Герой_Экипировка.md, 2.2), поэтому один компонент на два места уже есть, а второго места пока нет. - Выпадающих списков в начертаниях и у ювелира больше нет. Все
<select>заменены сеткой плиток предметов (ItemPicker): начертания — «во что вставить», кузня — «камни в экипировку», ювелир — предмет для гнезда и предмет с камнем для переогранки; выбор камня из двух тоже стал сегментами. Выбранный предмет показывается карточкой с иконкой (PickedItemCard), а не строкой списка: игрок должен видеть, во что вставляет. Приём тот же, что уrune-grid, — нового не изобретали. Порядок действий не изменился: сначала предмет, потом руна; плитки рун гаснут, пока предмет не выбран. - Фон зданий. Внутри здания (кузня, начертания, рынок, таверна, аллея) теперь есть слой фона: файл
bg_<здание>.jpgвgame/client/public/characters/подхватывается без правок кода — это требование соседнего ТЗ по фонам. Пока картинки нет, видна затемнённая подложка и панель работает как раньше. Завеса не глухая (0.42 → 0.72), интерьер будет виден. - Как проверял.
tscсервера и клиента иvite build— чисто. Новыйgame/server/test/runes-stats.js— семь разделов: кастеру «Ярость» даёт силу заклинаний и интеллект вместо атаки и силы, в оружие усиление ровно ×1.25 (с допуском на округление до сотых), обещание попапа совпадает с прибавкой после вставки по каждому стату и каждой роли, итог от рун равен сумме одиночных эффектов, градации строго растут, строка статов подписывает проценты знаком «%», правила вставки не изменились (нет гнёзд, не руна, повтор, переполнение). Прогон всех 23 тестов сервера — без падений, включаяprofessions.jsс рунами. Поиском проверено, что в трёх панелях не осталось ни одного<select>. - Что осталось за кадром. Иконки рун нарисованы (все 20 файлов
uiic_rune_<семейство>_<ступень>.pngна месте), поэтому попап и плитки показывают их, а не эмодзи. Фонов зданий нет — промт наbg_scriptorium.jpgлежит вТЗ_Начертание.md, раздел 5. Живой проверки на боевом не было. Полная документация — в 04 — Экипировка и дроп.
Фарм: зоны охоты по уровням, фракционные территории и авто-PvP
- Зона охоты стала настоящей. Раньше
LOCATIONSс полямиlevelMin/levelMax/mobsне значили ничего:rollLevelдавалуровень героя + (−6…+9), аpickArchetypeискал по всему пулу мобов. Теперь уровень моба зажат границами зоны, а имя, иконка и добыча берутся из списка мобов зоны (при пустом — запасной вариант по всему пулу). В «Серебряных рощах» (1–10) герой 60 уровня встречает мобов строго 1–10 уровня, проверено по 200 мобов на каждую из 14 зон. Новая сокет-командаlocation:setменяет зону, доступ проверяется по уровню; зона живёт в профиле (player.locationId) и переживает перезаход. Карта показывает диапазон зоны, метку «здесь» и предупреждение «мобы не по уровню» (зона ниже героя на 7+ уровней — мобы синие, добыча урезана). - Вкладка «Мир» исчезает после выбора фракции. Вид карты больше не ручной переключатель, а производное от стороны: без фракции — только расовые зоны, с фракцией — только оспариваемые, кнопки «Мир» в интерфейсе нет вовсе (требование владельца дословно). После выбора стороны карта перерисовывается сразу, без перезахода.
- Территории отвоёвывает фракция, а не покупает клан. Новый модуль
game/server/src/game/territory.tsи таблицаterritoriesв базе. Победа над героем противоположной стороны даёт стороне +1 очко в 24-часовом окне; по закрытии зона уходит стороне с большим счётом, при равенстве — остаётся у текущего владельца, а в первом окне контроля нет ни у кого (ровно как просил владелец: «цикл сбора статистики 24 часа»). Старыйterritory:captureза 100 CP убран: зону нельзя купить. Клан-владелец остался витриной — им становится клан с большим вкладом в закрытом окне. - Авто-PvP во время фарма. В оспариваемой зоне с шансом 12% вместо моба приходит герой противоположной стороны, только из онлайна и только подходящий по мощи (±25%), причём находящийся в той же зоне: боёв с призраками из базы нет. Бой считается тем же движком, что арена, но без участия игроков (
autoResolvePvpвcombat.ts— там же, где живётpvpTurn, а не вarena.ts, который держит только цены и тиры). Победитель продолжает фарм без доплаты, проигравший теряет фарм (новый стоит 25 ⚡), но остаётся жив — HP не опускается ниже 1 (Math.max(1, hp)), воскрешение за Эйроны из-за случайной встречи было бы наказанием не по делу. Защита от накрутки: победа над одним и тем же соперником чаще раза в 10 минут очков не даёт. - Бонус за контроль — только в своей зоне. Старый клановый «+10% голда за владение» заменён, а не дополнен (иначе за одну зону капало бы и то, и другое): своим в контролируемой зоне +20% золота и опыта и +10% к шансу добычи, клану-владельцу +1 CP в час за зону, а на карте зона подкрашена цветом стороны и помечена гербом. Все числа собраны в
TERRITORYвdata.ts. - Автофарм больше не лезет на сильных ×5 и ×10. Владелец: «нападает на мобов х5 и х10 если только они на 5 лвл ниже героя». Реализован основной вариант: редкий моб берётся, только если он ниже героя минимум на 5 уровней (
mobLevel ≤ playerLevel − 5); обычные мобы бьются как раньше, без ограничения по уровню. Описания режимов в «Меню» приведены к этому правилу, а если подходящих целей нет — фарм пишет причину («Ждёт цель: все ×5/×10 выше героя более чем на 5 уровней») вместо молчания. - Таймер фарма без рамки.
.auto-boxперестал быть коробкой (ниborder, ниbackground, ниborder-radius, ниbackdrop-filter) — вместо неё строка состояния: пульсирующая точка, вид фарма, режим, счётчики, крупный моноширинный таймер до конца вылазки справа и кнопка-иконка «⏹» с подтверждением. Под строкой — тонкая полоса прогресса (сколько вылазок из запланированных). Окно итога фарма не тронуто. В шапке охоты добавлена строка зоны: владелец, счёт окна и пометка, что бонус стороны работает. - Как проверял.
tscсервера и клиента — чисто. Три новых теста:game/server/test/farm-zones.js(по 200 мобов на зону — все в границах, включая героя 60 в зоне 1–10; архетипы только из списка зоны; вылазка целиком; правило пяти уровней и причина ожидания),territory.js(окно 24 ч, победитель, ничья у прежнего владельца, вклад клана, кулдаун 10 минут, бонус только своим и только в своей зоне, чтение из базы с мусором и отрицательными очками),territory-pvp.js(бой всегда доходит до исхода, равные соперники делят победы, сильный побеждает почти всегда, лог читаемый, пустые очереди приёмов не ломают бой). Обновлён устаревшийautohunt-check.js— он проверял старое правило без уровня. Регрессииcombat-pacing,autofarm,arena-preview,pvp-check,quests— без падений. - Что осталось за кадром. Экран авто-PvP сделан карточкой на экране охоты, а не в
BattleView: автобой идёт без ходов игрока, и отдельный боевой экран ему не нужен. Итог окна уходит в лог сервера, отдельной новости в интерфейсе нет. Полная документация — в 02 — Вылазки и бой и 06 — Мир, фракции и кланы. - Выкачено и проверено на боевом (05.10.2026). Клиент и сервер залиты,
deploy/test-farm-live.cjsпрогнан по сокету: герой 60 уровня в зоне 1–10 встретил 24 моба уровней 1–10 (ни одного вне границ) и только архетипы зоны (волк,гоблин,бандит,орочий бугай,степной вождь…); без фракции оспариваемая зона отвечает «Оспариваемые земли откроются после выбора стороны», после выбора стороны расовая закрывается («…воюй на оспариваемых»), а в зоне 10–20 все мобы ровно 20 уровня;territory:listотдаёт 8 зон с владельцем, счётом окна и остатком времени, старыйterritory:captureбольше не принимается (обработчика нет). Авто-PvP сыгран живьём: два героя противоположных сторон (60 уровня, одна зона, оба в автофарме) встретились за четыре минуты, проигравший получил «Поражение в бою за территорию» и фарм встал, победитель продолжил, сторона победителя получила +1 очко в зоне (0 → 1). Снимок полосы фарма с боевого —models/autofarm-2-running.png: рамки нет, таймер моноширинный, рядом «⏹», под строкой полоса прогресса, в шапке зона «Золотые равнины».
Заточка снаряжения: уровни +1…+16, четыре свитка и риск разбития
- Старая шкала заточки удалена целиком. Было
ENCHANT_MAX = 196605, шесть грейдов (Обычная → Легендарная) и три свитка «+5 / +10 / +10000», которые никогда не подводили. Стало 16 дискретных уровней и +1% к статам предмета за каждый уровень (round(база × (1 + 0,01 × уровень))). Грейдов в игре больше нет: цвета предметов, подписи и кнопка «⏬ Грейд» ушли вместе с ними. - Шанс считается по целевому уровню, и провал ломает вещь. До +4 включительно провала нет вовсе, +5…+7 — 70%, +8…+12 — 30%, +13…+16 — 25%. При провале простым свитком предмет разбивается и возвращает 30% своего расхода (у кованой вещи — ресурсы её ранга с округлением вниз, у дроповой — один ресурс набора её слота по редкости). Если сумка полна, часть возврата не влезает: ответ сервера честно делит его на
refundиlost, а клиент пишет «не влезло в сумку: N». - Четыре свитка вместо трёх.
scroll_weapon(0,5% с мобов 10+) иscroll_gear(2% с мобов 10+) — простые;scroll_weapon_safe(5%) иscroll_gear_safe(15%) падают только с боссов рейдов и при провале оставляют предмет целым, но сгорают сами. Множитель риска на шанс свитков не действует — числа остаются ровно 0,5% и 2% даже на «Безумии». На уровнях +1…+4 интерфейс предупреждает, что безопасный свиток там не нужен. В таверне свитков нет вовсе (решение владельца), из сундуков падают только простые. - Требования по уровню при надевании сняты. Раньше грейд выше 10-го уровня мог снять предмет с персонажа; теперь такой проверки нет, а когда-то снятые вещи игрок надевает сам.
- Перенос живых игроков — по грейду, который они видели.
game/server/src/game/enchant-migration.ts: Особая → +4, Уникальная → +8, Героическая → +11, Мифическая → +14, Легендарная → +16; три старых свитка удаляются из сумок, экипировки и лотов. ОтметкаenchantMigratedделает перевод идемпотентным, а значения1…16читаются как уже новый уровень — профиль без отметки не откатится. Требования ТЗ (5.4) про пересчёт мощи и тира — ожидаемое следствие, отдельной компенсации нет. - Аукцион, чары, камни. Лоты хранят уровень как раньше: предметы с разными
+Nне складываются в стопку, уровень доезжает до покупателя. При разбитии камень, чары и ковка сгорают вместе с вещью — возвращаются только ресурсы. - Как проверял.
tscсервера и клиента иvite build— чисто. Новыйgame/server/test/enchant.js— 12 разделов: таблица шансов и границы групп по броску (не по случайности),+1%и округление, разбитие с возвратом ровно 30% и счётомlostпри полной сумке, безопасный свиток, отказ на +16, несовпадение вида свитка, запреты (сумка, материалы, руны, камни, зелья, сундуки, свитки), добыча у мобов 9 и 10 уровня, отсутствие множителя риска, источники безопасных свитков, миграция по таблице 5.1 и лотов, аукцион. Поправленыcharms.js,professions.js,alchemy.js,tailoring.js,jewelry.js; прогон всех тестов сервера — без падений. Новыйdeploy/migrate-enchant.js(по образцуmigrate-professions.js) переводит уровни, чистит сумки и снимает лоты старых свитков; проверен офлайн на дампе. - Что осталось. Иконки четырёх свитков (
uiic_scroll_weapon.pngи родственные) не нарисованы — работает запасной путьGameIconс эмодзи. Живой проверки на боевом не было, как и прогона миграции по боевой базе: их делаем после выката сервера с новой моделью.
Задания: 5 ежедневных, 15 еженедельных и 100 разовых
- Полная замена системы заданий. Было четыре ежедневных (вход, 40 убийств, бой на арене, рейд) и три счётчика, которые сбрасывались по дате. Стало 120 заданий на трёх горизонтах: 5 ежедневных (сброс в 00:00 UTC), 15 еженедельных (сброс в понедельник 00:00 UTC) и 100 разовых, которые не сбрасываются никогда. В каждой награде есть 1 💎 Эйрон — правило проверяется тестом списком по всем 120 заданиям, а не выборочно. Разовые собраны в 18 цепочек (уровни, ранги, охота, вылазки, боссы, подземелья, рейды, арена, профессия, заточка, камни и чары, гнёзда, сундуки, аукцион, богатство, клан, сторона, снаряжение): каждая начинается с «первого раза» и растёт, поэтому игрок всегда видит следующий шаг.
game/server/src/game/quests.tsпереписан целиком. - Счётчики в трёх слоях одним помощником.
bump(p, metric, amount)растит счётчик сразу вp.stats(за всю игру — для разовых),p.quests.dailyиp.quests.weekly. Точки событий зовут его вindex.ts: убийства, вылазки, боссы, подземелья, рейды, арена (бои и победы), работы по профессии, успешные заточки, камни, руны, руна D, чары и мастерская чара, гнёзда, сундуки, лоты и продажи на аукционе, заработанное золото, клановые охоты, территории, запуски автофарма, отдых в таверне и забранные награды. Вставка камня, руны или чары кормит ещё и ежедневное «Улучшение» — псевдоним метрикиinfusionsизбавил от второго вызова в каждой точке. Часть целей — не счётчик, а состояние (уровень, ранг, рейтинг, золото на руках, части сета, занятые слоты, максимальный +N, рецепты и ранги линий, CP): оно считается в момент получения, поэтому «1 000 000 золота» засчитывается и тратится дальше. - Сбросы — по UTC, без таймеров. Ключ дня
2026-10-03, ключ недели2026-W40(понедельник). Проверка идёт при любом обращении к заданиям и при входе, как раньше у ежедневных;bumpпроверяет сбросы до записи, иначе счётчик, набежавший после полуночи, попал бы в старый день и стёрся. Дневные и недельные id уходят изclaimed, разовые живут вdone. Профиль получилquests: { day, week, daily, weekly, claimed, done }иstats; три старых счётчика (kills,arena,raids) переносятся вstats.kills,stats.arena_fights,stats.raidsпри загрузке, поэтому прогресс живых игроков не сгорает, а мусорные id прежней системы вычищаются. - Награды закрывают узкие места. Кроме золота и Эйрона задания выдают ключи подземелья (рейды), ресурсы своей профессии (крафт; без профессии — золото по курсу «обычный 1 000, редкий 5 000, эпический 20 000»), свитки заточки снаряжения и оружия. За полный день (все пять ежедневных) сверху идёт 1 🔑 Ключ Подземелья — доплата за дисциплину, а не отдельное задание. Шесть финалов цепочек дают косметику, которую нельзя купить: фон героя, рамки Предвечного, охотника, мастера и чемпиона и аватар легенды. Они помечены
rewardOnly(data.ts): в магазине косметики не появляются,shop:buy-cosmeticих отклоняет, а для использования VIP не нужен — они заработаны. - Интерфейс: три вкладки и цепочки. Экран заданий открывается кнопкой 📋 на экране героя, из «Меню» раздел убран. Внутри — «Ежедневные» / «Еженедельные» / «Разовые» со счётчиком «готово N из M», карточки с условием словами, полосой прогресса и «412 / 500», награда иконками (в том числе 💎 1) и кнопками «Забрать» / «Получено». Разовые показаны цепочками: пройденное свёрнуто в строку, активный шаг раскрыт, следующие видны серым. Кнопка «Забрать всё» собирает ровно готовые награды — список берётся снимком, поэтому выдача одной награды не дописывает в него новые (иначе «Дисциплина» закрывалась бы тем же нажатием). Прогресс считает сервер и отдаёт вью целиком (
quests:get), клиент ничего не пересчитывает: счётчик готовых наград (questsReady) приходит в каждом обновлении игрока, по нему рисуется точка на кнопке и чип в шапке героя; рост счётчика клиент ловит подпиской стора и показывает тост «Задание выполнено: Большая охота». - Аукцион: продажи без продавца не теряются. Покупатель может купить лот, пока продавец офлайн, поэтому таблица
auction_payoutsполучила колонкуlots_sold: при входе выручка приходит вместе с числом проданных лотов, и задания «продать 3 лота» и «оборот 500 000» закрываются без присутствия продавца. Если продавец в игре, деньги и счётчики отдаются сразу в его сессию. - Как проверял.
tscсервера и клиента — чисто; новыйgame/server/test/quests.js— девять разделов: сброс по смене даты (и что в тот же день повторно не забрать), граница «воскресенье → понедельник» UTC и накопление внутри недели, «разовые не сбрасываются» (включая перезаход черезserializeForDb/hydratePlayer), правило «в каждой награде 1 Эйрон» по всем 120 заданиям, выдача наград с профессией и без (ресурс против золота), свиток и ключ, косметика за цепочку и запрет её покупки, три слоя счётчиков и состояния, перенос старых счётчиков, «Забрать всё» и бонус за полный день. Прогон остальных тестов сервера — без регрессий.vite buildклиента проходит. - Что осталось за кадром. Живой проверки на боевом не было: окно заданий, тост и цепочки собраны и собираются клиентом, но на боевой сервер эта правка ещё не выкатывалась. Сброс недели проверен подменой часов в тесте (
setQuestClock), а не сменой недели на копии базы. Соседнее ТЗ заточки (свитки в таверне, таблицы дропа,InventoryTab) на момент этой правки ронялоalchemy.jsиtailoring.js— теперь оно доделано, и все тесты сервера проходят. Полная документация системы — в 05 — Квесты.
Иконки интерфейса вместо эмодзи: 39 картинок подключены (наборы A, B, E, G, I)
- Владелец нарисовал первые 39 иконок из 121 и положил их в
game/client/public/characters/: валюты и счётчики (9), ранги (10), слоты экипировки (7), тиры арены вместе со стрелками дельты рейтинга (10) и сундуки (3). Все они подключены к интерфейсу; эмодзи из кода не убраны — они остались запасным вариантомGameIconна случай, когда файла нет. - Один помощник вместо таблицы путей.
iconPath.tsполучилuiIconPath(kind, key)→/characters/uiic_<набор>_<ключ>.png. Раньше пути пришлось бы прописывать руками в каждой панели; теперь валюты (uiic_cur_*), ранги (uiic_rank_<id>), тиры (uiic_tier_<id>), слоты (uiic_slot_<слот>) и дельты (uiic_arena_up/down) берутся одной строкой, а новый нарисованный набор подхватывается без правок кода. Сундуки отдельного правила не потребовали:chest_wandererуже разворачивается вuiic_chest_wanderer.pngобщимitemIconPath. - Что встало на место эмодзи: верхняя панель (рюкзак, VIP, Эйроны); экран героя — «Мощь · ранг», счётчики арены, энергии и золота, семь слотов экипировки и иконки надетых предметов; арена — значок своего тира, значки трёх соперников, «в запасе», вход за золото, место и рейтинг, ожидаемые дельты (
+8 / −8) и «Рейтинг»; «Аллея героев» — вкладки и значения; окно итога боя — тир и стрелка роста/падения рейтинга; сундуки — витрина, цена в золоте и Эйронах, строки в рюкзаке; вылазки — энергия и цена автофарма; «Меню» — плитки VIP и автобоя, цена подписок и косметики; клан — клановые очки. - Размеры картинок задаёт место, а не файл. Добавлены правила
index.css(.hero-slot__icon img,.arena-tier__badge img,.arena-opp__tier img,.hero-chip img,.stat-row img:not([class]),.ico--inline,.btn__icoи родственные): «квадратик» слота, чипа или значка задаёт размер, картинка его заполняет. Правила для плиток меню и строк характеристик сужены через:not([class])— иначе они перебивали бы по весу уже существующие классы иконок (.menu-tile__ico,.ico) и ужимали чужие картинки. - Ловушка, на которую легко попасться. Общее правило
.btn imgтрогать нельзя: у кнопок профессий иконки линий и рун заданы классами, а.btn imgпо весу (класс + тег) сильнее.rune-tile__ico(один класс) — плитки молча уменьшились бы. Поэтому инлайн-иконки в кнопках носят свой классbtn__ico. - Как проверял.
tscиvite buildклиента — чисто; сборка кладёт все 39 файлов вdist/characters/. Сверка литеральных путей вsrcсpublic/characters/— не хватает толькоuiic_quests.pngиuiic_chests.png(их ещё не рисовали) и четырёх картинок мира/фонов, которых в рабочей копии нет и раньше. - Живая проверка — новый
deploy/shot-ui-icons.cjs: временный герой, headless Chrome по CDP на локальной площадкеdeploy/serve-local.cjs(игра проксируется на боевой), восемь экранов — герой, модалка сундуков, рюкзак, арена, «Аллея героев», вылазки, «Меню», раздел сундуков. На каждом снимке аудит картинок: битых (naturalWidth = 0) и сжатых в ноль — нет ни одной, а нарисованных иконок в кадре — 36 из 39. Снимки —models/иконки-экран-героя.png,models/иконки-арена.png,models/иконки-сундуки.png,models/иконки-меню.png, отчёт —tools/tmp/ui-icons/report.json. - Выкат на боевой 04.10.2026, 21:47. Залито целиком (сервер + клиент, 580 МБ), картинки сжаты штатным
redeploy.js. На боевом теперь 39 иконок из 39 (проверено запросами: у каждойContent-Type: image/png), а бандлassets/index-Fh-c1B3p.jsсовпадает с локальным байт в байт и содержит 33 пути к нарисованным иконкам против 23 в прежнем. Контроль после выката:game: 200,api: 200, сервисactive. Повторная проверка уже на боевом (SITE=http://201.34.128.60 node deploy/shot-ui-icons.cjs): восемь экранов, 39 нарисованных иконок в кадре, битых и нулевых нет. Снимки вmodels/заменены на боевые. - Что выяснилось про прежний выкат. До этой перезаливки на боевом лежало 26 иконок из 39: валюты, ранги и слоты уехали прошлым выкатом, а тиры, стрелки дельты и сундуки — нет, потому что их нарисовали позже. При этом бандл на боевом был старый и разводку иконок не содержал вовсе — файлы лежали, но код их не звал. Отсюда вывод на будущее: проверять не только «файл отдаётся», но и «файл есть в бандле».
- Уборка. Временных героев живых проверок накопилось 13 (
ui_,sizes_,serve_,dbg_);deploy/clean-my-test-heroes.cjs --applyубрал их в два прохода (первый вернул 13 — сессии в памяти сервера успевали записать профиль обратно), после чего на боевом осталось 45 персонажей. - Что живая проверка не показала и почему.
uiic_rank_1…9иuiic_tier_0,1,2,5,6,7на снимки не попали по одной причине: у свежего героя ранг 0 и тир 3, а соперники подбираются рядом по мощи — тот же тир. Путь у них общий с увиденными (uiIconPath('rank', player.rankId)иuiIconPath('tier', tier)), поэтому доказательство подключения — сами файлы вdistи на боевом, а не снимок.uiic_cur_keyпока некуда ставить: ключи выдаются наградой за задания, а строка награды собирается текстом вgameQuests.ts— под неё нужна отдельная правка, и рисовать больше ничего не требуется. - Попутно: в выкат уехала чужая работа. Полная перезаливка увезла в бой не только иконки, но и работу соседней сессии по заданиям (
quests:get, карточки заданий) — так решил владелец. Проверено, что это связно: боевой сервер отвечает наquests:get(ok: trueсо списком дневных и недельных), то есть серверная и клиентская части заданий уехали вместе. Вredeploy.jsдля будущих случаев добавлен флаг--client-only: не льёт сервер, не перезапускаетgame, но картинки сжимает. - Что осталось владельцу (из открытых вопросов ТЗ). Нужны общие
uiic_quests.pngиuiic_chests.png: кнопки «Задания» и «Сундуки» на экране героя и заголовок модалки сундуков всё ещё на эмодзи 📋/🎁 — наборы C (роли героя), F (статусы боя), H (подземелья), K (меню), L (обучение), M (расы), J (косметика) и D (роли мобов) не рисовались.
Иконки профессий: 53 картинки, рюкзак без эмодзи
- Нарисованы все иконки четырёх профессий и их ресурсов — 53 файла в
game/client/public/characters/: 13 у шитья, 11 у алхимии, 8 у чар, 5 у ювелира и 16 ресурсов (четыре набора по четыре). У кузни иконки были готовы раньше. - Одна иконка на линию, ранг — цифрой. У камня картинка вида (
uiic_gem_war), у чары — семейства (uiic_charm_might, проклятия —uiic_charm_curse), у ковки и шитья — линии (uiic_forge_sword_1h): ранги_r2…_r6берут ту же картинку, римскую цифру и букву градации пишет интерфейс. Исключение — ресурсы: у каждого своя (uiic_res_herb_root). - Рюкзак и кузня больше не ищут разные файлы.
iconPath.tsзнает три правила — ресурс, вид камня, семейство чары. До этого всем подставлялсяuiic_<id>.png, поэтому камень уходил искатьuiic_gem_war_d.png, а чара —uiic_charm_might_d.png, и обе падали в эмодзи. Панели профессий иForgePanelтеперь ходят через одинitemIconPath; эмодзи остался запасным путём у оружейных линий. - Выкат стал легче. В
redeploy.jsвсеuiic_*жмутся в палитру 256 цветов и 256 px, а всё, что толще 120 КБ, — до 192 px и 128 цветов. Раньше через палитру шла только кузня, а новые иконки уезжали по 600 КБ каждая.
Арена: правки владельца — без 2D, без прокрутки, модели сверху
- 2D-картинок на арене больше нет. Убраны и портретные заглушки бойцов, и орнамент «vs» между панелями. Если модель класса не пришла или WebGL недоступен, половина остаётся пустой (с подписью имени), а не подменяется картинкой. Автоматический откат в портреты по кадрам удалён целиком: слабому железу уступаем разрешением, а не объёмом — если пара не тянет 20 кадров в секунду, полотно один раз переезжает на
pixelRatio 1. Ключиarena3d_off_v2иarena3d_forceиз настроек браузера больше не используются. - Кнопки ручного обновления соперников нет. Тройка соперников приходит на бой и перекатывается после каждого проведённого боя (
arena:listбольше не принимает флаг обновления; из ответа ушёлrefreshIn, из игрока — полеarenaRefreshAt, изarena.ts—ARENA_REFRESH_MS). В подписи списка так и написано: «обновятся после боя». - Экран боя пересобран под телефон. Сверху полотно с двумя объёмными бойцами, под ним сжатые панели (ник, класс, HP/MP/ярость полосами, статы), ниже журнал, умения и кнопки. Верхняя строка «Арена · ход · попытки · рейтинг · против» и строка темпа удалены — ход виден по бою, попытки и рейтинг остались на экране выбора соперника.
- Прокрутки на экране боя нет вовсе.
.app--arena— жёсткая колонка на высоту окна (overflow: hidden), свободное место забирает полотно, а на низких экранах ужимаются полосы, подписи умений и кнопка атаки (медиазапросmax-height: 560px). Проверено живьём на 360×780, 360×560 и 320×568:scrollHeightравенclientHeightи у боя, и у страницы. - Всплывающие числа и иконки приёмов теперь на моделях. Отдельный слой
.battle-stage__floatsлежит поверх полотна и делится на две половины: урон, лечение, щиты, криты и вспышки умений всплывают от пояса своего бойца вверх, а не под панелями. - Как проверял.
tsc --noEmitсервера и клиента,npm run build, тестarena-preview.js(28 проверок — из него убрана проверка кулдауна). Живьём на боевом:deploy/test-arena.mjs(в сводке арены больше нет поля ручного обновления, список не перекатывается сам и обновляется после боя, награды и рейтинг в итоге на месте) иdeploy/shot-arena.cjs— 45 из 45 в обычном прогоне и столько же на программном рендере (SWIFTSHADER=1, ~6 кадров/с: арена остаётся объёмной, портретов нет). Снимки —models/арена-бой-живая.png,models/арена-бой-320.png. - Найдено попутно (не входило в правки, требует решения владельца). Часть моделей классов «запечена» в своём файле повёрнутой:
orc_grinder(Громила) и, похоже,dwarf_iron_guardсмотрят вбок-назад и на экране героя, где камера стоит строго спереди, — это свойство моделей Meshy, а не арены. На арене такой боец встаёт спиной к игроку. Лечится либо картой доворотов по классам, либо пересборкой этих моделей.
Арена: данные на экранах, двое бойцов в 3D и возврат к соперникам
- После боя игрока больше не выбрасывает в «Вылазки». Раздел вкладки «Бой» жил в
useStateкомпонента: экран арены при завершении боя размонтировался, вкладка собиралась заново и показывала «hunt». Теперь раздел лежит в сторе (battleSection+setBattleSection), начало боя ставит «arena1», «Сдаться» и кнопка «К соперникам» возвращают к списку, а после перезагрузки посреди незавершённого боя раздел тоже арена. - Карточка соперника стала осмысленной. Раньше показывались имя, уровень, тир и мощь, хотя сервер уже отдавал расу, класс и рейтинг. Теперь есть иконки расы и класса (как на экране героя), рейтинг, место в рейтинге, винрейт и шанс победы с ожидаемыми дельтами. Место считает сервер по снапшотам арены; винрейт — из новых полей игрока
arenaWins/arenaLosses(game/server/src/game/player.ts), заведённых с нуля: старой истории боёв в профиле нет и восстановить её неоткуда. - Шанс победы и дельты считает сервер (
arenaPreviewвgame/server/src/game/arena.ts): разница мощи переводится в шкалу Эло (половина средней мощи = 400 очков, то есть десятикратные шансы), сверху добавляется разница рейтингов, шанс ограничен 3–97%. Дельты берутся тем жеratingDelta, которым награждает бой, поэтому «победа +12» в карточке — ровно то, что игрок получит. - Список соперников перестал перекатываться сам. Выданная тройка живёт до боя: раньше
arena:listвыдавал новых случайных соперников на каждый заход, и кнопка обновления была бы бессмысленной. Ручное «🔄 Обновить» — раз в 3 минуты (ARENA_REFRESH_MS), остаток кулдауна сервер отдаёт числом, клиент показывает таймером и гасит кнопку; после боя список перекатывается сам. Если рядом по уровню никого нет, кулдаун не тратится — ждать три минуты за пустой список было бы издевательством. - В шапке арены — место, винрейт, полоска попыток и таймер. Попытки показаны пипсами (мёртвые стили
.arena-pipsвернулись в дело), рядом «⏳ попытка через 12:34» изarenaFightsIn(остаток считаетarenaFightRegenLeftтем же способом, что иregenArenaFights), кнопка «🏅 Рейтинг» открывает «Аллею героев» (RatingsPanel) прямо с арены, а внизу — диапазон ожидаемых дельт по выданным соперникам. - Экран боя стал информативным. Строка статуса (ход, попытки, рейтинги обоих, ожидаемая дельта), ключевые статы обоих бойцов (атака, броня, крит, уворот), сворачиваемый журнал боя (
log.slice(-40)и раньше приезжал, но не рисовался нигде), тир и место соперника рядом с именем. - Оба бойца объёмные — на одном WebGL-контексте. Новый
ArenaStage3D: одна сцена, одинWebGLRenderer, одно полотно, два актёра со своими микшерами. Прежний вывод «телефон не потянет» верен только про число контекстов: здесь контекст один, моделей две. Игрок слева, соперник справа, оба развёрнуты друг к другу (камера 3/4, поворот — одна константаVIEW_YAW), каждый играет клип своего действия: удар по ходу,victoryу победителя,dyingу проигравшего. Пауза 1900 мс перед окном итога сохранена. - Код three.js больше не дублируется. Загрузчик GLTF/Draco, нормализация модели по росту, клипы, сцена со светом и вписывание в кадр вынесены в
components/hero3d/;HeroModel3DиArenaStage3D— две оболочки над общим кодом. Поведение одиночной модели (экран героя, PvE-бой) не изменилось.three.jsпо-прежнему грузится лениво: полотно арены приезжает отдельным куском черезReact.lazy, основной бандл не растёт. - Запасные пути. Модели класса нет в
MODELS_3D, файл не скачался или WebGL недоступен — эта половина показывает свой 2D-портрет, вторая остаётся объёмной; нет 3D у обеих — экран выглядит как раньше. Если средний FPS за первые 2,5 с ниже 24, полотно само уходит на два портрета и запоминает отказ (localStorage: arena3d_off), внизу остаётся кнопка «Включить 3D» — без неё откат был бы навсегда. - Окно итога показывает награды. Опыт, золото и SP раньше начислялись молча, и в окне их не было. Теперь их считает
arenaRewards(40 + уровень×5опыта,200 + уровень×20золота, 5 SP, VIP ×1.1 — те же числа, что начисляются), рядом новый рейтинг с дельтой, прогресс до следующего тира и короткий лог боя (10 строк). Кнопка называется «К соперникам» и возвращает на арену. - Убрано мёртвое. Недостижимый блок итога на экране выбора соперника (он лежал в ветке, которая никогда не выполнялась), поле
arenaRatingChangeв сторе (всегда 0 — сервер не шлётratingChangeдо финала) и стили.arena-cost*,.arena-tier__hint,.arena-result__log,.arena-vs. - Как проверял.
tsc --noEmitсервера и клиента,vite build. Новый тестgame/server/test/arena-preview.js— 29 проверок: у равных бойцов ровно 50% и равные дельты; вдвое большая мощь даёт 82% против 18%, а сумма шансов пары равна единице; крайности упираются в 3% и 97%; мощи дельта не зависит (начисляют по рейтингу), а разница рейтингов — зависит; дельты совпадают сratingDeltaначисления; награды по формуле ТЗ для 1, 30 и 60 уровня вместе с VIP-надбавкой; таймер попытки считается от последнего начисления и просроченная попытка не выдаётся. Старыеpvp-check,combat-checkиsocket-guardпроходят как раньше. - Примерка раскладки — стенд
tools/tmp/arena-tab-stand.html(кладётся вgame/client/public/): на 360×640 содержимое вкладки просит 533 px при доступных 533 — прокрутки нет; на узком 320×568 три карточки не влезают, и список соперников прокручивается внутри себя, а шапка с попытками остаётся на месте. Снимки —models/арена-выбор-360.pngиmodels/арена-выбор-320.png. - Пара бойцов проверена рендером, а не только сборкой. Временный стенд (вне
public/, после проверки удалён) отрисовал обоих бойцов в одном холсте на swiftshader: рост бойца на холсте 215 px при высоте 240 px — фит по высоте работает, силуэты читаются, оба развёрнуты друг к другу. Снимок —models/арена-3d-бойцы.png. Второй стенд поднимал настоящийArenaStage3D(React через esbuild +hero3d.css): с GPU в headless — 53–59 fps, холст есть, обе половины объёмные, ключа отказа нет; на swiftshader — 4.2 fps, и компонент сам ушёл в портреты, записалarena3d_off=1и показал кнопку «Включить 3D», а следующий заход с ключом вовсе не создавал контекст (55 fps, два портрета). - Живая проверка на боевом.
deploy/test-arena.mjs— состав списка, стабильность тройки, кулдаун обновления, награды и рейтинг в итоге, списание попытки, рост винрейта.deploy/shot-arena.cjs(headless Chrome по CDP; GPU не отключаем — на swiftshader пара моделей честно уходит в портреты) — 36 из 36: шапка с местом, винрейтом, пипсами и таймером; три карточки с расой, классом, рейтингом, местом, винрейтом, шансом и дельтами; «Рейтинг» открывает «Аллею героев»; в бою ровно один холст и обе половины объёмные (портретные заглушкиdisplay: none, откат по кадрам не сработал); строка статуса, статы обоих, сворачиваемый журнал; окно итога с рейтингом 1000 → 992 и дельтой «−8» красным; возврат на список арены с обновлённой тройкой. Снимки —models/арена-выбор-живая.png,models/арена-бой-живая.png,models/арена-итог-живая.png,models/арена-возврат-живая.png. - Что нашла живая проверка (и что исправлено). Первое: знак дельты.
ratingChangeприходит величиной без знака, и после поражения окно итога показывало «+8» зелёным — теперь знак берётся из самих рейтингов (rating − ratingBefore). Второе: заглушки-портреты перекрывали объёмных бойцов — у.arena-stage__sideсвоёdisplay: grid, и по специфичности оно бито браузерное[hidden] { display: none }; добавлено явное правило, а проверка теперь смотрит на вычисленныйdisplay, а не на атрибут (по атрибуту она эту ошибку пропускала и давала ложную зелёнку). Третье: у только что созданного героя не было снапшота арены — он не видел своего места в рейтинге и не попадал в чужие списки, пока не сделает первый ход; теперьcharacter:createпишет снапшот (persistвместо гологоsavePlayer), аarena:listзаводит его на месте, если профиль старше этой правки. - Откат по кадрам переделан — по жалобе владельца «на арене нету 3D моделей». Прежний замер стартовал с первой загруженной модели, шёл 2,5 с и требовал 24 кадра в секунду: то есть судил о железе по распаковке Draco и заливке текстур. У кого порог не проходил, отказ запоминался навсегда, а вернуть 3D можно было только мелкой серой пилюлей в углу полотна — её никто не замечал, и арена выглядела «без 3D». Теперь замер начинается, только когда готовы все запрошенные модели, после разогрева 0,9 с, окно 3 с и порог 20 кадров/с; ключ отказа переименован (
arena3d_off_v2), поэтому старые незаслуженные отказы забыты. Вместо пилюли — заметная строка с причиной («низкий FPS — 3D выключено · 6 кадр/с», «нет WebGL», «модель не загрузилась») и кнопкой «Включить 3D»; нажатие запоминает выбор игрока (arena3d_force) и больше не отменяется замером — иначе кнопка обещала бы объём и тут же его отбирала. - Живая проверка отката.
SWIFTSHADER=1 node deploy/shot-arena.cjs— 40 из 40: программный рендер (6 кадр/с) уводит арену в портреты, причина и кнопка на месте, отказ записан, а кнопка возвращает холст и запоминает выбор. Обычный прогон на GPU — 36 из 36: откат не срабатывает, обе половины объёмные, холст один. Снимок отказа —models/арена-откат-живая.png. - Чего живая проверка не покрыла. Настоящий телефон (pixelRatio 1.5 и живой FPS пары на железе) и десяток боёв подряд на утечки памяти — не проверялись: в headless был десктопный GPU.
- Грабли окружения.
vite buildпадал вvite:esbuild-transpileсremove …esbuild-…: Access is denied— esbuild не мог удалить свой временный файл в%TEMP%. Лечится тем, чтоTEMP/TMPна время сборки указывают внутрь проекта; к коду это отношения не имеет. И ещё:redeploy.jsходит поdist/и падает сENOENT, если в этот момент кто-то другой пересобирает клиент (Vite чистит каталог) — выкатывать надо, когда сборка не идёт.
Фоновая музыка: базовая тема и отдельные сцены для фарма и арены
- Музыка зазвучала на всех экранах. Новый клиентский модуль
game/client/src/music.ts— один на всю игру. Он запускается изmain.tsxдо первого рендера, поэтому тема играет уже на экране создания героя. Сервер и база не тронуты: музыка целиком клиентская, отдельного поля в профиле у неё нет. - Громкость 15% — половина от первой версии.
MUSIC_VOLUME = 0.15: 30% оказалось громко для фона. Константа одна — и для самого звука, и для подписи в интерфейсе. - Сцен три, и выбирает их экран.
App.tsxпо состоянию стора отдаётarenaприarenaBattle,farmприbattleилиexpedition(охота, вылазка и бой внутри неё), иначеbase. Смена сцены — плавный переход: старый трек уходит в ноль за 700 мс, новый нарастает, щелчков на стыке нет. - Отдельные треки для фарма и арены кода не требуют. Достаточно положить
farm.mp3иarena.mp3рядом с базовым вgame/client/public/audio/— подхватятся сами. Пока файла сцены нет, играет базовая тема, поэтому музыка не пропадает ни на одном экране. Файл, который не открылся, помечается сломанным: второй раз его никто не дёргает. - Трек и его подготовка.
129473_1791018236_1.mp3(144 с, 48 кГц, 188 kbps, 3,24 МБ) перекодирован вmenu.mp3: 44,1 кГц, 128 kbps, 2,20 МБ — на треть легче для мобильного Telegram. Мастер у трека ровный (−14,3 LUFS, true peak −0,4 dBFS), нормализация не понадобилась. - Музыка включается сама при открытии, а не по нажатию. Первая попытка — сразу при запуске, до первого рендера. Дальше она повторяется на каждом событии, которое может снять запрет автоплея:
pointerdown,mousedown,touchend,click,keydown, возврат окна в фокус,pageshow, «пробуждение» мини-аппа в Telegram (activated,viewportChanged), плюс две отложенные попытки (600 и 2000 мс) — страница могла быть ещё не готова принять звук. Где браузер автоплей разрешает (в Telegram обычно разрешает) — музыка играет с первой секунды без единого нажатия; после успешного старта слушатели снимаются, чтобы не работать на каждое касание игрока. - Переключатель — одно нажатие с любого игрового экрана. Кнопка 🔊 в верхней панели (
components/MusicToggle.tsx, стили.topbar__sound): одно нажатие — тишина, второе — обратно, флаг вlocalStorage(game_music_muted). Панель есть и на вкладках, и на охоте, поэтому заходить в «Меню» не нужно. Раздел «Звук» в «Меню» остался: он объясняет сцены и служит тем же переключателем. - Панель на узких экранах поджата под новую кнопку. Кнопка занимает 38 px и 8 px зазора — на 320 px это отнимало у чипов ресурсов всю их свободу, и последняя цифра в чипе обрезалась на 3,7 px. Добавлено правило
@media (max-width: 340px): отступы панели и чипов поджаты, кнопка музыки уступает до 30 px, рюкзак остаётся 38 px как основная кнопка. Замерено, что текст чипов после этого не обрезается ни на 320, ни на 360, ни на 390 px. - Свёрнутая вкладка — пауза, а не перезапуск. Раньше возврат на вкладку начинал трек с начала: голос, ушедший в тишину, помечался отработавшим и заменялся новым. Теперь «тишина» и «голос больше не нужен» — разные состояния (
pauseAtZeroиretire), поэтому музыка возвращается с того же места. Тик громкости при этом живёт только пока есть что двигать: дошли до цели — интервал остановлен. - Как проверял.
tsc --noEmitиvite buildклиента. Логику гонял Node-тестом с подставнымиAudio,document,windowиlocalStorage(tools/tmp/music-logic-test.mjs, модуль собран esbuild) — 36 проверок в двух частях. A. Запрет автоплея (как в обычном браузере): плеер создан сразу без нажатий, запуск отклонён и элемент на паузе, громкость всё равно доведена до цели, слушатели ждут действия, после касания музыка играет, слушатели сняты. B. Автоплей разрешён: сцены, фолбэк на базовый при отсутствии файла сцены, отсутствие повторных попыток, мьют и включение обратно, свёрнутая вкладка и возврат на том же элементе. Тест нашёл настоящую ошибку ещё в первой версии: голос с неоткрывшимся файлом не глушился и оставался «играющим» навсегда. - Живая проверка — headless-Chrome по CDP на собранном
dist(tools/tmp/music-live-check.cjs). Обычный прогон — 20 проверок: запрошен ровно один файл музыки (/audio/menu.mp3, 2 304 606 Б), сервер отдал200 audio/mpeg; плеер один,loopвключён, громкость ровно0.15; до касанияpaused, после настоящего клика играет —currentTime 2,46 с,duration 144 с, ошибок декодирования нет; кнопка музыки и стиль её выключенного состояния найдены в собранных бандле и CSS. Прогон сALLOW_AUTOPLAY=1(--autoplay-policy=no-user-gesture-required) — 21 проверка и главное: музыка играла до единого действия игрока,currentTime 4,36 св момент замера. Значит, задержка старта — это политика браузера, а не код: где автоплей разрешён, трек идёт с открытия. - Живая примерка панели —
tools/tmp/topbar-check.cjs: верхняя панель с кнопкой и без неё собирается из настоящегоindex.cssи меряется в headless-Chrome на 320, 360 и 390 px — 15 проверок, все зелёные. Мерил неscrollWidth(он врёт: при дробных ширинах округляется вверх и даёт постоянные 2 px), а ширину самого текста черезRangeпротив content-box чипа. Снимки —tools/tmp/topbar-320.png,-360.png,-390.png. - Ловушка CSS, на которой я попался. Первую версию правила я поставил рядом с кнопкой музыки, то есть выше правил
.topbar__chipи.topbar__res. При равном весе селекторов решает порядок, поэтомуpaddingиfont-sizeиз медиазапроса молча не применялись — правка «не работала», хотя выглядела верной. Блок перенесён в конец секции шапки, и в комментарии это записано, чтобы не повторить. - Выкат на боевой и живая проверка —
deploy/redeploy.js(клиент + сервер), затем новыйdeploy/shot-music.cjs: временный герой, headless Chrome по CDP, 25 проверок, все зелёные. Боевой отдаёт/audio/menu.mp3как200 audio/mpeg(2 304 306 Б), в залитом бандле есть и кнопка, и все три пути к файлам сцен. В живом интерфейсе: кнопка стоит в панели, ширина 38 px на экране 390, значок 🔊 и подпись «Выключить фоновую музыку»; плеер один,/audio/menu.mp3, зациклен, громкость ровно0.15; до касания браузер держит звук, после настоящего клика играет (currentTime 2,58 с,duration 144 с, ошибок декодирования нет). Одно нажатие кнопки — значок 🔇, рамка краснеет, громкость в ноль, воспроизведение остановлено, вlocalStorageлёгgame_music_muted=1; второе нажатие — всё вернулось, флаг0. Отсутствующийfarm.mp3на боевом даётerrorкод 4. Снимки —models/музыка-1-включена.pngиmodels/музыка-2-выключена.png. Временный герой удалён (моих 0, персонажей на боевом 45), его префиксmusictest_добавлен вdeploy/clean-test-heroes.cjs. - Чего живая проверка НЕ покрыла. Переключение сцен в живом интерфейсе — вход в охоту и на арену — не проверялось: для этого героя надо довести до вылазки и до боя на арене, а проверка касалась музыки. Сама смена сцены и фолбэк закрыты Node-тестом логики, а то, что отсутствующий файл сцены на боевом действительно даёт
errorкод 4, проверено живьём. Ещё один снимок,tools/tmp/music-check.png, — экран загрузки локальной сборки без бэкенда; доказательством звука он не является. - Ловушка, которую стоит помнить. В nginx стоит
try_files $uri $uri/ /index.html, поэтому запрос несуществующегоfarm.mp3возвращает не 404, а 200 сtext/html. Плеер всё равно уходит вerror(код 4), так что фолбэк работает, но полагаться на код ответа в таких проверках нельзя.
Чат: цвет текста по вкладке
- У каждого канала свой цвет. «Общий» — белый
#ffffff, «Торговля» — оранжевый#ff9a3c, «Клан» — голубой#6cc4ff, «Группа» — салатовый#8ce06a. Четыре переменные в:root, модификаторы.chat-msg--<канал>красят текст сообщения и имя автора, левая рамка карточки — тот же цвет. Оранжевый намеренно не золотой: торговое сообщение не должно выглядеть «дорогим», как интерфейсное золото. - Шапка осталась читаемой. Подпись «12 ур. · [ТЕГ]» и время не красятся — они приглушённые (
--muted), иначе цветной текст съедал бы разницу между автором и служебной строкой. - Свои сообщения по-прежнему на золоте (
chat-msg--mine): подложка сохраняется, а текст и имя красятся по каналу — в «Общем» это белое на золотистом. Правила каналов идут после--mine, поэтому рамка канала перекрывает золотую. - Цвет берётся из сообщения, а не из открытой вкладки.
channelесть и в истории, и в живом сообщении, а лежит он внутри JSONBdata— миграция не нужна: старые сообщения получают свой цвет сразу, и лента остаётся верной, даже если однажды в один список попадут разные каналы. Сервер, типы и база не тронуты. - Вкладки встали в один ряд. Правило
.seg--fourв CSS вообще не было описано — четыре кнопки жались по умолчанию. Теперь этоnowrapс равными долями, а подписи на узком экране не прячутся: у вкладок чата нет иконок, и общее правило≤560pxоставляло бы четыре пустые полоски. Активная вкладка красится в цвет канала с подложкой 10–35%, «Группа» без отрядов остаётся серой и недоступной — цвет за ней закреплён и включится сам. - Как проверял.
tsc && vite buildклиента. Локальная примеркаtools/tmp/chat-colors.htmlна 360, 320 и 300 px: четыре кнопки в один ряд (72, 62 и 57 px), страница не шире экрана. - Живая проверка —
deploy/shot-chat.cjsна боевом, 29 из 29: двое временных героев написали в «Общий» и «Торговлю», вычисленные цвета сверены с ТЗ (текст, имя, рамка, приглушённая шапка, золотая подложка своих), после перезагрузки страницы история осталась в своих цветах, «Клан» и «Группа» без клана и отряда недоступны. Снимки —models/чат-1-общий.pngиmodels/чат-2-торговля.png, временные герои удалены.
Падение сервера от одного запроса клиента
capital:infoсо старым кэшем клиента ронял игру для всех. 04.10.2026 в 07:16 боевой упал сTypeError: cb is not a function(dist/index.js:1867), systemd поднял его через три секунды, игроков выбросило. Причина не в самом событии: в обработчике стоялоcb?.({ ok: true, ... }), а опциональная цепочка спасает только отnull/undefined— если на месте колбэка приходит чужой аргумент,cb?.()бросает исключение, а любое исключение в обработчике сокета роняет весь процесс Node: одно событие от одного клиента убивало игру всем.- Обработчики больше не могут уронить сервер. Новый
game/socket-guard.ts: каждый слушатель оборачивается вguardListener— исключения (и синхронные, и отклонённые промисы) логируются и остаются внутри своего события, а мусор на месте колбэка подменяется пустышкой. Признак «обработчику нужен ответ» — параметр с именемcb, поэтому значения на месте последнего аргумента (inventory:equipс uid) не подменяются, а старый клиент просто не получает ответа вместо падения сервера. - Проверки:
test/socket-guard.js(распознавание колбэка, мусор вместо него, значение без колбэка, синхронное и асинхронное исключение) и живаяdeploy/test-socket-guard.cjsна боевом: после четырёх запросов с мусором сервер отвечает,/api/catalogи/healthживы, обычныйcapital:infoработает.
Чары: под класс, проклятия, резонанс и заказы
- Профессия переведена на общую модель. Одна профессия на персонажа, ранг = тир (тир I с 10 уровня, дальше каждые 10), рецепты покупаются по одному. У чар их 26: 20 семейств (5 × 4 градации), 4 проклятия и 2 надстройки. Скупить всё — 51 200 золота.
- Свои ресурсы.
arcane_dust,veil_essence,soul_ember,void_shardпадают только чародею, 25% с моба и ×10 с босса. Раньше чары брали ресурсы кузни, то есть чародей зависел от кузнеца. - Шанс 50% и потеря. Провал сжигает ресурсы и золото, чара в предмет не ложится. Галочка «наложить наверняка» добавляет 10 пунктов за 2 Эйрона.
- Чары под класс: таблица «семейство × группа ролей» (5 × 5). «Мощь» бойцу — атака и крит, танку — урон и броня, хилеру — лечение, кастеру — сила заклинаний. На градации D в оружии это
атака 12.75у бойца иатака 6.38 + броня 8.5у танка. - Проклятия (
charm_curse_a…_d) — четвёртая линия рецептов: заметно больше атаки при минусе здоровья, ресурсы и золото на треть ниже обычной чары. D в оружии:32.5атаки и−34здоровья. - «Двойная чара» (тир V) разрешает вторую чару на предмет — кузнечные гнёзда для этого не тратятся. Две чары разных семейств дают резонанс: по +5% к обеим; проклятие считается отдельным семейством.
- «Мастерская чара» (тир VI) — флаг
charmMaster: +20% к эффекту чар предмета. Вместе с резонансом это ×1.26. - Работа на заказ. Лот-услуга: чародей кладёт в резерв ресурсы, золото и Эйроны, покупатель выбирает свой предмет (профессия ему не нужна). Бросок — при покупке: успех — чара ложится, чародей получает цену минус 5%; провал — покупатель не платит, резерв чародея сгорает.
- Экран профессии. Сетка «5 семейств × 4 градации», отдельными строками проклятия и надстройки; в попапе — предмет, точный эффект под класс и слот, «нужно / есть», шанс и галочка «наверняка». «Начертания» вернулись к рунам: чар там больше нет.
- Что не тронуто: заточка, свитки,
ENCHANT_MAX,GRADES, руны и камни. Чара по-прежнему переживает продажу предмета вместе с ним, а переплавка уносит её вместе с вещью.
Перевод живых игроков. Чары на предметах не пересчитываются: id, слоты и поле charms те же, charmMaster ни у кого не выставлен. Но эффект у части ролей изменился — у танков, хилеров и кастеров клетка таблицы другая; это осознанное следствие чар под класс, а не ошибка переноса. У кого профессия не enchanting, чары продолжают работать: статы считает getEquipBonus по классу владельца, профессия нужна только чтобы накладывать новые. Рецептов у чародеев не было ни у кого — их покупают заново, компенсации нет.
Как проверял. game/server/test/charms.js — 18 разделов: 26 рецептов и цены тиров, разбор id (проклятия и надстройки чарой не считаются), вся таблица ролей по пяти группам, проклятия, отказы, бросок 50% с потерей, замена, «Двойная» и «Мастерская», резонанс, статы четырёх слоёв вместе, заказы (резерв, провал, снятие, полная сумка) и чистка профиля при загрузке.
Живая проверка — deploy/test-charms.cjs на боевом сервере: каталог чар, покупка пяти рецептов (200 + 200 + 3000 + 7000 + 13000), отказы, предпросмотр («Оберег D» танку — броня 21.25, здоровье 76.5), наложение с бруском 50% и подсчётом списаний, вторая чара с резонансом (+5%), мастерская (+20%) и заказ, купленный вторым персонажем со своим мечом. Снимки — models/чары-1-сетка.png и models/чары-2-попап.png.
Что показал первый живой прогон. Восемь пунктов «провалились», и все — из-за самого теста: human_knight оказался танком, а тест ждал от «Мощи» бойцовских 12.75 вместо танковых 6.38 + 8.5 брони. Тест стал роль-зависимым и считает ожидание из таблицы ТЗ, а заказ теперь пробуется несколько раз — бросок 50% живой.
Сверка перевода — deploy/compare-charm-stats.mjs: считает вклад чар по старой и новой формуле для каждого персонажа. По живой базе (44 профиля) чар нет ни у кого, потерь и флагов мастерской нет — перевод ничего не пересчитывает. Этой же утилитой сверяется копия базы перед выкатом.
Две ошибки в числах, которые поймал расчётом. 0.3 × 3.4 × 1.25 = 1.275, но в JavaScript 1.275 * 100 равно 127.49999999999999, поэтому крит «Мощи D» — 1.27, а не 1.28 (в первой редакции ТЗ стояло 1.28). И суммирование чар округляет каждую отдельно — как руны, — поэтому две «Мощи A» дают крит 0.76, а не 0.75.
Про сумку. Новый предел сумки ломал возвраты: addItem при полной сумке отказывает, а вызывающие этот отказ не смотрят. В заказах и покупках это закрыто: снятие заказа при полной сумке отказывает и не теряет резерв, а покупка вещи не проходит, пока нет места. Заказ услуги места не требует — вещь остаётся у покупателя, меняется только чара на ней. Остальные вызывающие addItem (награды, дроп, переплавка) стоит проверить отдельно: их отказ по-прежнему никто не смотрит.
Автофарм: два вида, оплата Эйронами, работа без вкладки
- Два вида вместо одной автоохоты. «▶ Разовый» — 2 💎 за текущую вылазку, живёт, пока открыта вкладка. «⏩ До нуля энергии» — цепочка вылазок до конца энергии, сессия живёт на сервере: закрытая вкладка, заблокированный телефон и перезапуск сервера её не останавливают. Подписка на автобой теперь отвечает только за кнопку «⚡ Авто» в бою, фарм её не требует.
- Цена — 2 💎 за каждую вылазку цепочки, списание сразу при запуске (решение владельца). Идущая вылазка входит в счёт первой, поэтому у полной шкалы (100⚡) выходит ровно 8 💎 за четыре вылазки: 25⚡ уже вложены в текущую, остальные 75 — это ещё три. При энергии меньше 25 и без вылазки запуск отклоняется. Остановка возвращает по 2 💎 за каждую не начатую вылазку (2, 4 или 6).
- Сессия живёт без сокета. Реестр
farmSessions(по id персонажа) плюсpersistPlayer: пока сессия активна,disconnectне выбрасывает игрока из памяти, а переносит его в реестр, и тот же секундный тикер ведёт цепочку вылазок. При возврате игрока сессия переезжает обратно вplayers— владелец в каждый момент один, двойных боёв нет. - Перезапуск сервера фарм не съедает. Сессия пишется в
players.data.farm(рядом сexpedition), поднимается на старте черезloadFarmPlayers()— фильтрdata->'farm'->>'kind' IS NOT NULLв самом SQL, чтобы не тащить в память все профили. Условие именно поkind:data->'farm' IS NOT NULLистинно и для JSON-null, то есть под него попал бы каждый профиль, сохранённый без фарма. По той же причине ключfarmне пишется вовсе, когда сессии нет. Если вылазка ещё идёт, цепочка продолжается; если нет — сессия закрывается и Эйроны за несыгранные вылазки возвращаются, а игрок видит итог при входе. - Разовый фарм намеренно не возобновляется после обрыва:
autoHuntубран изserializeForDb, при загрузке профиля всегдаnull. Раньше автоохота переживала переподключение — это и было отличие вида А от вида Б. - Вся логика — в
game/farm.ts(цена, план, возврат, запуск, остановка, шаг цепочки), поэтому она проверяется тестом без поднятия сервера;index.tsоставляет себе сокеты,persistи рассылку событий. - Интерфейс: две кнопки на экране «Охота» с подписями («остановится, если закрыть вкладку» / «идёт на сервере: вкладку можно закрыть»), панель прогресса (вид, «вылазка 2 из 4», остаток, побед/золото/опыт), кнопка «⏸ Остановить», блокировка с текстом «Нужно 8 Эйронов» и окно итога сессии — оно же показывается при возвращении к офлайн-фарму. Раздел «Меню → Автобой» переписан под подписку, настройки режимов переехали в «Автобой и автофарм».
Как проверял. Новый game/server/test/autofarm.js — 10 разделов: цена 2 и 8 (в том числе при неполной энергии и без вылазки), списание и невозврат для вида А, возврат 4 💎 за две несыгранные вылазки и защита от двойного возврата, цепочка chain → chain → chain → stop с полной длительностью каждой вылазки, отказы (нет Эйронов, нет энергии, мёртв, арена, повторный запуск), остановка при смерти, итоги и разбор сохранения. Весь остальной набор сервера прогнан — autohunt-check.js, combat-check.js, combat-pacing.js, pvp-check.js, professions.js, arena-pacing.js, sp-economy.js зелёные; пять тестов (алхимия, чары, ковка, ювелир, миграция профессий) красные до этой правки и к фарму отношения не имеют.
Живая проверка — выкачено на боевой 03.10.2026. deploy/test-autofarm.cjs: вид А списал 2 💎, набил бои в открытой вкладке и после закрытия вкладки не возобновился (это проверяется только на сервере с базой); вид Б списал 8 💎 и с закрытой вкладкой за 26 секунд добавил 3 победы, 27 золота и 75 опыта; остановка вернула 6 💎 за три несыгранные вылазки и оставила итог в профиле, «Закрыть» его убрало; повторный запуск при идущем фарме и запуск без Эйронов отклонены с текстом «Нужно 8 Эйронов». deploy/test-farm-restart.cjs: systemctl restart game посреди оплаченного фарма — в журнале «автофарм после перезапуска: продолжают 1, закрыто с возвратом 0», сессия поднялась с тем же планом, число побед выросло с 2 до 5, Эйроны второй раз не списались. Снимки интерфейса с боевого — models/autofarm-1-buttons.png и models/autofarm-2-running.png. Временные герои проверок удалены (deploy/clean-test-heroes.cjs).
Вылазки: ×5/×10 — это HP и награда, а не урон
- Редкость больше не множит все статы сразу.
generateExpeditionMob(game/server/src/game/expedition.ts) брала одинrarityMulна HP, урон, броню и награды: героик бил впятеро, мифик — вдесятеро, и редкий моб убивал героя за пару ходов. Теперь множители разведены:HEROIC = { hp: 5, dmg: 1.5 },MYTHIC = { hp: 10, dmg: 2.5 }. HP и награды остались ×5/×10, урон и броня у героика ×1,5, у мифика ×2,5. - Живые числа (голый боец 40 уровня без вещей и атрибутов, замер в
combat-pacing.js): обычный моб — 100% побед за 7,5 ходов; героик — было 0% побед и смерть на 8-м ходу, стало 30% побед за 35 ходов; мифик — было 4,6 хода и 0% побед, стало 16 ходов и по-прежнему 0%. Мифик остаётся не для голого героя: ×10 HP против ×2,5 урона — это долгая осада вместо секундной казни, и решает её снаряжение, а не рефлекс. - «Нет активного боя» больше не вылезает после победы. Сервер закрывает бой сразу после исхода, а клиент ещё 1900 мс держит финальный кадр — в этом окне кнопки «Атака» и «Авто» были живы, и нажатие возвращалось ошибкой. Теперь стор ставит
battleLockedвместе с исходом, кнопки (и умения) на это время гаснут, нажатие вообще не уходит на сервер, а сервер на запрос без боя отвечает{ ok: false, silent: true }— такой ответ тоста не рождает. - Текст ошибки остался честным. Если клиент считает бой живым, а сервер его уже потерял (перезагрузка посреди боя, бой снят с другой вкладки),
silentне глушит сообщение: тост показывается как раньше. Глушится только гонка интерфейса. - Та же гонка на арене закрыта тем же флагом:
ArenaBattleViewгасит кнопки поbattleLocked/result, аarena:actionотвечаетsilent(раздел 5 ТЗ_Арена). - Тексты совпали с формулой: вики (раздел «Героик и мифик»), подсказки бейджей «Героик ×5» / «Мифик ×10» на экране охоты и пояснение в «Меню → Автобой» теперь говорят, что ×5/×10 — это HP и награды, а урон и броня ×1,5/×2,5.
Как проверял. В combat-check.js добавлен раздел 0: 60 000 сэмплов сверяются с формулой (HP, урон, броня, опыт, золото, SP) плюс проверка шансов (героик 4,93%, мифик 0,94%) и показательная таблица 30 уровня — обычный hp 1440 / урон 130, героик 7200 / 195, мифик 14400 / 325. В combat-pacing.js добавлен замер редкости с колонкой «было» — той же формулой, что стояла до правки. combat-check.js заодно перекодирован из двойной перекодировки в нормальный UTF-8 (файл читался как «Р СџРЎР‚Р ВµР В·Р ВµРЎР‚Р С”Р В°…»).
Живая проверка — выкачено на боевой 03.10.2026, deploy/test-rarity.cjs: бой доведён до исхода, и сразу после него «Атака» и «Авто» вернули { ok: false, silent: true, error: 'Нет активного боя' }; статы сверены на 185 обычных, 15 героических и 4 мифических мобах — ноль расхождений (героик 2 уровня: hp 900 / урон 27 — это ×5 HP и ×1,5 урона, а не ×5 на всё).
Соседней сессии стоит знать. Пять тестов сервера красные до этой правки и к вылазкам отношения не имеют: alchemy.js падает на Cannot access 'ALL_RECIPES' before initialization (строка 87 использует константу, объявленную ниже), а charms.js, forge-crafting.js, jewelry.js, professions-migration.js — на p.forge.<профессия> (поле forge у игрока, судя по последним коммитам, переехало). combat-debug.js остался в двойной перекодировке: его текст восстанавливается не полностью (25 символов теряются), поэтому не трогал.
Профессии открываются с 10 уровня
- Учитель, кузница и чары съехали на одну ступень — 10 уровень. Все три порога стояли на 50-м (
PROFESSION_LEVEL_REQвcapital.ts,FORGE_LEVEL_REQвforge.ts,CHARM_LEVEL_REQвenchanting.ts): профессию покупали в конце игры, а между выбором стороны и первой ковкой лежали сорок уровней. Теперь сторона, профессия и работа открываются вместе, и купленная профессия не лежит без дела. - Порог кузницы больше не зашит в клиенте. Он уходит каталогом (
forgeLevelReqрядом сcraftCost), оттуда его берут подпись раздела «Кузница» в меню и заметка ювелира в панели чар. Раньше «с 50 уровня» стояло числом в двух местах клиента и разошлось бы с сервером при первой же правке. Заодно в панели чар учитель стал «в столице» — как в остальных подписях. - Числа не тронуты. Цена профессии (3000, каждая следующая +2000), практика (+10 ковка, +5 огранка, +15 переплавка), цены и шансы рецептов, таблицы чар и рун — те же. Сдвинулся только уровень входа.
- ТЗ на пять профессий обновлены — в разделе 0 «Общие правила» стояло «с 50 уровня»; там же теперь сказано, что ковка и чары открываются на том же уровне. Зеркало в
ТЗ_Экономика.md(состав каталога) дополненоforgeLevelReq.
Как проверял. game/server/test/forge-crafting.js, charms.js, professions-migration.js, alchemy.js, jewelry.js — зелёные. Отказы по уровню теперь считаются от самого порога (FORGE_LEVEL_REQ - 1, CHARM_LEVEL_REQ - 1, PROFESSION_LEVEL_REQ - 1): в professions-migration.js стоял зашитый 10 уровень, который после правки стал проходным и проверил бы не отказ, а успех. Остальные тесты сервера (бой, автобой, SP, арена, PvP) прогнаны — зелёные. tsc сервера и tsc && vite build клиента проходят.
Три живые проверки в deploy/ тоже ждали пятидесятого уровня: test-charms.cjs (порог чар в каталоге), test-capital.cjs (учитель не берёт новичка) и test-capital-rules.cjs (отказ до порога). В последней заодно починены id профессий — там со времён свёртки ремёсел остались dagger и staff, то есть проверка «профессия изучена» падала бы на «такой профессии нет», а не на уровне.
Наложение чар: четвёртый слой статов
- Профессия
enchantingнаконец что-то делает. Чародей в «Начертаниях» накладывает на предмет чару — постоянное усиление за ресурсы кузни и золото. Пометкаproduces: 'enchant'у профессии была с самого свёртывания ремёсел, а уметь профессия не умела ничего. - Чара — услуга, а не предмет. Её нельзя выбить, купить в таверне или передать отдельно от предмета: в сумке чар нет. Этим профессия и отличается от кузни и шитья (предметы) и от ювелира (камни-предметы).
- Пять семейств × четыре градации. Мощь (атака, крит), Оберег (броня, здоровье), Прозрение (сила заклинаний, мана), Порыв (уворот, крит), Вера (лечение, мана); градации A–D с силой ×1 / ×1.6 / ×2.4 / ×3.4. Усиление подстраивается под класс и слот: кастеру «Мощь» даёт силу заклинаний вместо атаки, в оружие идёт на четверть больше — как у рун.
- Одна чара на предмет, но замена разрешена. Новая чара вытесняет старую, старая пропадает. Это единственный слой, который можно переделать: руну и камень вынуть нельзя.
- Четвёртый слой намеренно маленький. База вдвое ниже рунной, шкала положе рунной, и в чарах нет атрибутов и вампиризма:
strвstats.tsумножается на 2.0 к атаке и сделал бы чары сильнее всего остального, а вампиризм складывается с одиннадцати предметов и ничем не ограничен. Полный набор D на все 11 слотов даёт по атаке примерно столько же, сколько три руны «Ярость» D в одном оружии. - Цена: A — 4 обломка, 1 слиток, 1000 золота; B — 12/4/1 и 5000; C — 25/8/2/1 и 15000; D — 60/15/3/1 и 40000, то есть ровно третье гнездо плюс золото. Провала нет: преграда — цена, а не бросок кубика.
- Своё поле
charmsу предмета, а неforge: чара ложится и на дроповый предмет, у которого метаданных ковки нет. Тем же приёмом ювелир завёлgems. - Заточка, свитки, руны и камни не тронуты — ни одного числа в
ENCHANT_MAX,GRADES,SOCKET_COSTиTAVERN_GOODS.
Попутно починен аукцион — и это не мой слой. Лот переносил только заточку и forge, а камни ювелира лежат в своём поле gems: продажа предмета с камнем стирала камень у покупателя. Теперь лот везёт и камни, и чары — одна функция applyLotLayers на покупку и на снятие, чтобы они не разъехались, — а в списке лотов предмет с чарой помечен ✨. Соседней сессии стоит знать: её камни ехали через аукцион с потерей.
Как проверял. game/server/test/charms.js — 13 разделов: структура и разбор id, совпадение списка слотов с FORGEABLE_SLOTS и GEM_SLOTS, вся таблица чисел на градации D, отказы (без профессии, до 50 уровня, свиток, чужие id, нехватка материалов, нехватка золота) с проверкой, что при отказе ничего не списано, наложение и замена, предпросмотр, статы четырёх слоёв вместе на дроповом предмете, чистка мусорных id при загрузке, неизменность заточки и аукцион. Остальные тесты сервера (ювелир, алхимия, ковка, миграция) прогнаны — зелёные.
Числа проверил расчётом, а не на глаз, и это спасло: 0.3 × 3.4 × 1.25 = 1.275, но в JavaScript 1.275 * 100 равно 127.49999999999999, поэтому округление даёт 1.27, а не 1.28. В первой редакции ТЗ стояло 1.28 — тест упал бы на «моих же» числах.
Живая проверка — deploy/test-charms.cjs, два временных героя на боевом сервере: каталог чар, отказ без профессии (золото не списано), учитель за 3000, отказ в свиток, предпросмотр (20 вариантов, D «Мощь» = атака 12.75 и крит 1.27), наложение A на дроповый меч (+3.75 атаки, крит 25.6 → 25.9), замена на D «Оберег» за 40 000 и продажа предмета вторым персонажем — чара пережила. Временные герои удалены.
Грабли окружения. vite build в этой сессии падает: esbuild не может удалить свой временный файл вне рабочего каталога («Access is denied»). Сборка проходит, если направить TEMP/TMP внутрь рабочего каталога, — на выкате делал так.
Зоны охоты: по одной оспариваемой на диапазон
- Оспариваемых стало 5 вместо 15 — по одной зоне на диапазон: 10–20, 20–30, 30–40, 40–50, 50–60. Раньше на каждый диапазон было по три зоны (центральная и две по бокам), остались центральные.
- Границы сдвинулись на единицу вниз: было 11–20, 21–30 и так далее, стало 10–20, 20–30. Теперь диапазоны идут подряд без дырок.
- Эндгейм стал «60+» вместо «61–70». Потолок игры — 60, дальше идёт ранг, а не уровни, так что «61–70» было неправдой. В подписях карты и карточки локации теперь пишется
60+, а не60–60. - Расовые зоны не тронуты — 6 штук по 1–10, как и были.
- Подпись переключателя сменилась с «Оспариваемые 11+» на «Оспариваемые 10+».
Пришлось поправить расстановку. Сначала я просто убрал лишние зоны, и пять оставшихся оказались в одной колонке по центру карты — маркеры налезли друг на друга и читались как одна каша. Развёл их «ромбом» вокруг центра, а на карте вылазок сделал маркеры компактнее: значок и диапазон уровней в одну строку вместо двух. Причина не в расстановке, а в пропорциях: карта горизонтальная, на вертикальном экране ей достаётся около 200 пикселей высоты, а маркеров четырнадцать.
Снимки — models/вылазки.png и models/вылазки-оспариваемые.png.
Закрытые разделы в подменю больше не теряются
«Арена 3х3», «Подземелья» и «Рейды» сливались с картой за подменю — их было почти не видно.
Причина оказалась в одной строке: у закрытых плиток стоял opacity: 0.5 на всей плитке. Прозрачность в CSS гасит элемент целиком вместе с его собственной подложкой — панель становилась полупрозрачной и сквозь неё просвечивал экран. Чем темнее фон за подменю, тем хуже было видно.
Починил так: подложку наоборот сделал плотнее (почти непрозрачной), а гашу только содержимое — иконку, подпись и текст. Иконка вдобавок обесцвечивается, поэтому закрытая плитка отличается от открытой не только яркостью. Замок и «с 60 ур.» остались яркими.
Вкладка «Бой»: подменю, вылазки на весь экран, арена в один экран
- Разделы уехали в подменю, как в «Герое»: Вылазки, Арена 1х1, Арена 3х3, Подземелья, Рейды. Заголовок «Бой» и полоска разделов сверху убраны — содержимому досталась их высота. Арена 3х3, подземелья и рейды до 60 уровня показаны закрытыми: плитка гаснет, на ней замок и подпись «с 60 ур.».
- Подменю стало общим компонентом
TabMenu— «Герой» и «Бой» теперь используют один и тот же код, а не две копии. - Вылазки: прокрутки нет. Карта занимает всю ширину, остальной экран — её же размытая копия, поверх счётчики, переключатель, карточка локации и кнопка вылазки.
- Арена: прокрутки нет, кнопка «Обновить» убрана. Три карточки соперников делят между собой всю оставшуюся высоту, так что пустого поля внизу нет. Новый список приходит после боя — кнопка «Закрыть и обновить соперников».
- Фон по тиру мощности. У твоей карточки и у каждого соперника фон соответствует его тиру арены. Новые картинки не рисовали: восемь тиров разложены по уже готовому арту — от мирных равнин новичка до столиц на верхних ступенях.
Чего не вышло. Карта в «Вылазках» занимает всю ширину, но не всю высоту: она горизонтальная (соотношение 1.75:1), а экран телефона вертикальный. Растянуть её на всю высоту можно только обрезкой, а маркеры расставлены в процентах от картинки — при обрезке часть локаций уехала бы за край и стала недоступной. Размытая копия закрывает остальной экран, так что пустого места не видно, но честно: это не «карта на весь экран», а «карта на всю ширину, экран занят ею же». Чтобы сделать по-настоящему на весь экран, нужна карта вертикальной компоновки — это новый арт.
Как проверял. Написал deploy/shot-tabs.cjs: он заводит временного героя, открывает игру в headless-Chrome и после каждого раздела замеряет прокрутку — scrollHeight против clientHeight. Это и есть требование «всё в один экран», и на глаз его не проверить. Во всех четырёх разделах прокрутка равна нулю. Снимки — models/бой-подменю.png, models/вылазки.png, models/арена.png.
Разделы героя уехали в подменю
- Полоска под-вкладок сверху убрана. По нажатию «Герой» в нижнем меню экран накрывается тёмно-серой заливкой с прозрачностью 40% — сквозь неё видно самого героя — и поверх появляется подменю: шесть плиток с иконкой и подписью. Внутри: Герой, Атрибуты, Умения, Таланты, Характеристики, Клан.
- Страница героя стала выше. Раньше под полоску под-вкладок уходила строка, теперь этой строки нет, и окно с фигуркой занимает всю высоту вкладки.
- Клан добавлен в разделы — он и раньше был отдельной вкладкой нижнего меню, теперь доступен ещё и отсюда.
- «Статы» переименованы в «Характеристики».
- Иконки экипировки выросли на 50% — с 20 до 30 пикселей. В мелком слоте иконку предмета было не разглядеть.
- Кнопка «☰ Разделы» на странице героя: без неё подменю открывалось бы только повторным нажатием вкладки в нижнем меню.
Ошибка, которую стоит помнить. Первый снимок подменю показал иконки разделов крошечными засечками, и я решил, что старые иконки под-вкладок не годятся. Начал генерировать новые — и зря потратил 27 кредитов (баланс 554 → 527): процесс остановили, три задачи успели уйти на Meshy, а файлы и записи о них не сохранились. Настоящая причина была другой: снимок делался через 900 мс после открытия подменю, а иконки по 700 КБ в этот момент ещё качались — раньше их никто не запрашивал. С паузой в 5 секунд видно, что иконки на месте и выглядят хорошо. Вывод: перед тем как что-то переделывать, убедись, что проблема настоящая — недогруженная картинка и плохая картинка на снимке выглядят одинаково.
Экран героя: только 3D и крупнее фигурка
- 2D-портрет на экране героя убран совсем. Он подставлялся дважды: пока грузился ленивый чанк three.js и пока качался сам GLB — а ещё оставался навсегда, если модель не загрузилась. Теперь на экране героя его нет ни в одном из этих случаев: на время загрузки показывается строка «Загружаю модель…», а при сбое — только причина. У всех 36 классов модель есть, так что пустого места не будет.
- В бою портрет оставлен — там он по-прежнему запасной вариант: второго WebGL-холста телефон не потянет, и соперник на арене рисуется 2D.
- Фигурка стала крупнее. Камера отступала по худшему случаю — самой широкой проекции модели при повороте (
max(size.x, size.z)), поэтому спереди модель выходила заметно мельче, чем позволяло место. Теперь горизонтальный габарит считается по текущему углу — опорной функцией коробкиhalfX·|cos| + halfZ·|sin|— и пересчитывается при повороте пальцем. Запас по краям уменьшен с 16% до 6%.
Как проверял. Статически тут ничего не докажешь: убрать портрет мало, модель должна реально отрисоваться. Написал deploy/shot-hero-live.cjs — он заводит временного героя, поднимает headless-Chrome с WebGL через SwiftShader, кладёт id героя в localStorage до загрузки приложения (иначе показывается экран создания) и снимает настоящий экран. Видно, что рисуется объёмная модель и что она стала крупнее. Тестовых героев после проверки удалил, снимок — models/экран-героя-3d.png.
Экран героя перестроен
- Фон теперь на всей вкладке, а не только под фигуркой: картинка класса растянута на весь экран героя, поверх неё градиентная вуаль — сверху и снизу плотнее (там текст и полосы), в середине почти прозрачно, чтобы фон было видно.
- Шапка собрана заново: иконка класса, ник, «раса · класс · уровень», иконка роли, Мощь и звание, значок стороны. Под шапкой три счётчика: бои на арене, энергия для вылазок, золото.
- Фигурка занимает всё свободное место. Слоты экипировки лежат поверх блока с моделью, но по краям: сцена отступает от краёв ровно на ширину слота, поэтому фигурка центруется в оставшемся месте и ничем не перекрывается. Ширина слота и высота полос вынесены в CSS-переменные
--hero-slot-wи--hero-vitals-h— сцена считает отступы по ним. - Слоты получили свою подложку с размытием: поверх фона иконки иначе терялись на светлых участках.
- Полосы HP/MP/EXP не тронуты — они устраивали и остались на всю ширину внизу.
Как проверял. Собрал стенд deploy/stand-hero.html — та же разметка и тот же index.css, что в игре, — и снял его headless-Chrome на 360 и 430 пикселях. Так нашлась вещь, которая иначе всплыла бы при проверке вслепую: headless-Chrome игнорирует --window-size, clientWidth оказался 484 вместо 360, и первый снимок «обрезал» правый край там, где на самом деле всё помещалось. Поэтому ширину стенда задаю в CSS, а не размером окна. Снимок — models/экран-героя.png.
Стенд намеренно лежит в deploy/, а не в public/: всё из public/ уезжает на сервер веб-ассетом, и отладочный файл оказался бы в проде. Проверил — на сервере его нет.
Чат, столица и подземелья
Три новых вкладки нижнего меню догнали содержимым.
Чат. Четыре канала: общий, торговля, клан и группа.
- Общий видят только свои. Это не отдельная проверка, которую можно забыть, а часть адресации: сообщение уходит не в канал, а в «комнату» —
general:lightилиgeneral:dark, — и видно его тому, у кого комната совпала. Торговлю видят обе стороны, клан — только свои, группы пока нет (отрядов в игре не существует). - Антиспам с лестницей наказаний. Первый мьют — 5 минут, дальше 30 минут, 2 часа, сутки и неделя. Уровень растёт, только если прошлое нарушение было меньше недели назад; иначе счётчик обнуляется. Мьюты лежат в базе, перезаход их не снимает.
- За что мьютим: флуд (больше пяти сообщений за десять секунд), три одинаковых подряд, больше 70% заглавных, ссылки, ряды повторяющихся символов.
- За быстрое сообщение не мьютим. Сначала было именно так — и это оказалось слишком строго: два сообщения с интервалом в секунду отправляли игрока в мьют на пять минут. Теперь такое сообщение просто не проходит с просьбой подождать.
- Лента переживает перезапуск — последние сто сообщений на комнату лежат в базе, старше недели вычищаются.
Столица. Своя у каждой стороны: Цитадель Зари у Ордена Света, Чёрный Острог у Легиона Тьмы.
- Аукцион между игроками. Лот выставляется с ценой, комиссия 5%, одновременно можно держать пять лотов. Одинаковые предметы уходят пачкой — продавать по одному пять зелий было бы мучением.
- Выручка не теряется, если продавец офлайн: покупатель платит сразу, а золото копится в отдельном счёте и выдаётся при следующем входе. Аукцион общий на обе стороны — иначе на каждой висело бы по паре предметов.
- Учитель профессий. Без изученной профессии чертёж бесполезен: кузница теперь отказывает. Первая профессия 3000 золота, каждая следующая на 2000 дороже. Берут с 50 уровня — там же открывается кузница.
- Таверна. Зелья и свитки за золото плюс отдых, восстанавливающий здоровье и ману; цена отдыха растёт с уровнем.
Подземелья (с 60 уровня). Три забега: Склеп Забытых, Кузня Глубин и Багровый Чертог. Внутри две волны обычных мобов и босс, вход стоит 40–50 энергии. Забег целиком решает сервер, здоровье между боями переносится — до босса доживают не все. С боссов падают чертежи тех ремёсел, которые иначе не достать.
Грабли. Выручка продавца записывалась в базу, не дожидаясь ответа покупателю, — и продавец, зашедший сразу после покупки, своих денег не находил. Поймал это интеграционный тест: он проходил через раз, потому что запись обычно успевала. Теперь запись выручки идёт до ответа.
Проверки: 18 правил видимости чата, 18 сценариев чата на живом сервере, 35 правил столицы, 31 сценарий столицы и подземелий, 32 проверки данных подземелий. Все зелёные.
Типы оружия, новое нижнее меню, фоны героя и стопки в рюкзаке
Крупная партия правок интерфейса и одна механика.
Оружие разложено по 13 типам. Раньше привязки не было вообще: маг мог надеть меч, лучник — посох. Теперь у класса и у предмета есть weaponType, и при надевании они должны совпасть. Посох — 12 классов, лук и кинжал — по 4, дальше по нисходящей; щит доступен только тем трём типам, у кого оружие одноручное. Подробная таблица — в 01 — Расы и классы.
- Четыре ремесла добавлены — двуручный топор, коса, арбалет, ружьё. Без них Берсерк и Топорщик вынуждены были носить молот, а Арбалетчик и Мушкетёр — лук. Теперь в кузнице 15 ремёсел, 13 из них оружейные.
- Новичку выдаётся оружие его типа. Раньше всем без разбора давали ржавый меч, и лучник не смог бы его надеть.
- Чертежи всех тринадцати ремёсел падают из «Сундука Древних» — до этого там лежали только посох и двуручник.
Нижнее меню перестроено: Чат · Столица · Бой · Клан · Герой · Меню. «Бой» — третья, круглая и поднятая над полосой, единственная цветная. Рюкзак остался кнопкой в верхней панели, «Герой» — шестой вкладкой.
- Вкладка «Бой» делится на четыре раздела: Вылазка, Арена, Подземелье, Рейд. Первые два работают как раньше, подземелье и рейд закрыты до 60 уровня и показывают, сколько до него осталось.
- Клан вынесен из «Меню» в отдельную вкладку, рейд уехал в «Бой» — в «Меню» он больше не дублируется.
Экран героя. Полосы здоровья, маны и опыта переехали внутрь сцены и стали по 14 px вместо ~70 px на три полосы — фигурке досталась освободившаяся высота, слоты экипировки сузились с 62 до 56 px. У каждого класса появился свой фон, а пока картинка не загрузилась, сцена держится на градиенте расы.
Рюкзак складывает одинаковые предметы в стопку с числом в углу клетки. Подпись стопки — всё, кроме uid: заточенный или перекованный предмет выходит из неё сам и занимает отдельную клетку.
Сгенерировано 45 картинок через Meshy: 36 фонов героя, 7 иконок меню и 2 панорамы столиц. Промты лежат в models/ПРОМТЫ.md и собираются скриптом deploy/art.cjs из models/classes.json и models/art.json. Фоны в вебе — JPEG по 720 px, все 36 занимают 2 МБ.
- Столицы: у Ордена Света — Цитадель Зари, у Легиона Тьмы — Чёрный Острог. Имена предложены, их можно поменять.
- Грабли заливки. Общее сжатие картинок на сервере режет JPEG до 512×768, и фоны героя потеряли бы половину разрешения — для них добавлена своя строка
720x1440.
Объёмные модели есть у всех 36 классов
Догнал через Meshy оставшиеся 30 классов — эльфы, люди, гномы, нежить и вампиры. Теперь объёмная модель есть у каждого класса в игре.
- Что вышло: 36 моделей по 1.29–1.92 МБ, в сумме 57 МБ. У каждой 60 000 треугольников, 24 кости и четыре клипа —
Idle,Attack,Victory_Cheer,dying_backwards. - Время: по 7 мин 54 с на класс, три класса параллельно — вся партия заняла около двух с половиной часов.
- Кредиты: 61 на класс, за 30 ушло 1830. Остаток 859.
- Список моделей больше не правится руками.
heroModels.tsгенерируется скриптомdeploy/gen-hero-models.cjsпрямо изmodels/classes.json— так он не разъедется с манифестом и в нём не будет опечаток в id. - Пакетная починка материала —
deploy/fix-all-materials.ps1. Проходит по всем каталогам классов, где естьanimated.glb, собирает материал скриптомmodels/fix_material.pyи кладёт результат вpublic/models/<id>.glb. Повторный запуск не пересобирает уже готовое — просто копирует. - Листы для проверки стиля:
models/classes/орки-модели.pngи ещё пять по расам (эльфы-,люди-,дварфы-,нежить-,вампиры-модели.png) — по шесть моделей в стойке, отрендерены Blender'ом из финальных файлов. - Проверка заливки —
deploy/verify-models.cjs. Сверяет три размера по каждому классу: локальный файл, файл на сервере и то, что реально отдаётся по HTTP. Сошлось 36 из 36. - Грабли с PowerShell. Windows PowerShell 5.1 читает
.ps1без BOM как cp1251, и путьC:\Игра\...превращается в мусор — скрипт падает с «Cannot find path». Лечится сохранением файла в UTF-8 с BOM. Заодно:Join-Pathв 5.1 принимает только два аргумента, а"$id.glb"внутри строки разбирается не как склейка — надо($id + '.glb').
Все шесть классов орков получили объёмные модели
Прогнал через Meshy всю расу орков — от концепт-арта до скелета с четырьмя действиями. Модели прописаны в heroModels.ts и уже в игре.
- Что вышло: 6 моделей по 1.6–1.9 МБ, 60 000 треугольников каждая, 24 кости, клипы
Idle,Attack,Victory_Cheer,dying_backwards. У каждой роли свой силуэт: Громила в тяжёлой броне, Шаман с рунами, Орк-лучник полуголый с ремнями колчана, Берсерк с шипастыми наручами, Душегуб в тёмной коже. - Время: один класс — 7 мин 54 с работы Meshy (концепт 21 с, модель 2:53, remesh 2:43, скелет 1:09, анимации 48 с). Пять классов параллельно по три — около 25 минут.
- Кредиты: 61 на класс, за пять ушло 305. Остаток 2689.
fix_material.pyпрогнан на всех — иначе все шесть блестели бы как металл (причина разобрана в записи ниже).stormcaller.glbудалён из веба, у Грозового шамана теперь своя сгенерированная модель. Сам исходник Штормколлера цел вmodels/stormcaller/.- Грабли при проверке заливки. nginx отдаёт
index.htmlпо SPA-правилу на любой несуществующий путь — с кодом 200 иContent-Type: text/html. То есть проверять наличие ассета по одному коду ответа нельзя: удалённый файл тоже вернёт 200. Спасает только сверка%{size_download}с размером исходника.
Материал модели: почему всё блестело и как починено
Модель выглядела как полированный металл или пластик. Разобрался — материал ломает сам Meshy, на шаге риггинга.
- Что показала проверка по этапам.
model.glb(Image to 3D) иremeshed.glb(Remesh) содержат полный набор: base color, metallic/roughness и normal. А вrigged.glbкарты metallic/roughness и normal исчезают, зато появляется emissive. То есть до риггинга всё было правильно. - Что именно остаётся после риггинга:
metallicFactorне задан вообще, а по спецификации glTF это 1.0 — сплошной металл;emissiveFactor: [1,1,1]вместе с той же картинкой, продублированной как emissive-текстура (в файле две записи текстур, обе указывают на одно изображение) — модель светится своим же альбедо; расширениеKHR_materials_specularсspecularColorFactor: [2,2,2]удваивает зеркальность; нормаль-карта выброшена, поэтому поверхность гладкая. Гладкая плюс блестящая — это и есть пластик. - Гипотезу о «запечённых бликах» проверил и отбросил. У Image to 3D есть параметр
remove_lighting, и я решил, что глянец нарисован в текстуре. Вытащил альбедо и посмотрел: оно плоское и тёмное, нарисованного блеска нет. - Починка —
models/fix_material.py. Материал собирается заново: карты берутся изremeshed.glb, где они уцелели, emissive убирается, specular возвращается к норме, множители к картам ставятся в единицу. Развёртки у обоих файлов одинаковые, так что карты ложатся без правок. - Файл вырос с 1.55 до 1.92 МБ — три нормальные карты вместо одной.
Действия модели подключены к бою, автоповорот убран
- Модель больше не крутится сама. Стоит на месте, играет стойку. Повернуть её можно пальцем или мышью, и после этого она плавно возвращается в исходный поворот — уже без самовращения.
- Атака, победа и смерть работают в бою. Клип переключается свойством
clipбез пересборки сцены: любой ход игрока (удар или умение) проигрываетAttack, а когда бой окончен —Victory_Cheerпри победе илиdying_backwardsпри поражении. Атака, отыграв, сама возвращается в стойку; победа и смерть замирают на последнем кадре. - Работает и в бою с мобом, и на арене. Объёмная модель ставится только игроку: два WebGL-контекста на одном экране телефон не потянет, поэтому соперник на арене остаётся портретом.
- Пауза перед окном итога выросла с 950 мс до 1900. Клип смерти длится 2.2 с, и старая пауза не давала ему прочитаться — окно накрывало анимацию.
- Логика вынесена в
HeroFigure. Один компонент решает, показать объёмную модель или портрет, и держит запасной вариант: если модель не загрузилась или в браузере нет WebGL, игрок видит обычный портрет. Основной бандл вырос всего на 0.4 КБ — всё остальное лежит в ленивом куске.
Громила-орк в игре: первая модель с анимациями
Класс orc_grinder («Громила», танк орков) получил объёмную модель, сгенерированную с нуля через Meshy — от концепт-арта до скелета и четырёх клипов. Это первая модель в игре, которая не просто крутится, а двигается.
- Цепочка генерации: концепт-арт в A-позе с пустыми руками → Image to 3D → Remesh → скелет → анимации. Стоимость 61 кредит на класс.
- Концепт-арт рисуется с
pose_mode: a-poseиremove_background— руки пустые, оружия и щитов нет. Это принципиально: авто-риггинг не понимает предметы в руках, и именно из-за посоха разваливалась модель Штормколлера. Оружие и щиты крепим отдельно и позже. - Remesh обязателен. Без него Image to 3D выдаёт 2 612 700 полигонов, и скелет ставить отказывается — лимит 320 000. Отдельный шаг режет до 60 000 и ужимает файл с 80 МБ до 28 МБ.
- Четыре клипа в одном файле (
action_idsв задаче анимации):Idle(4.0 с),Attack(2.8 с),Victory_Cheer(9.3 с),dying_backwards(2.2 с). Экран героя играетIdle; атаку, победу и смерть будет дёргать бой. - Draco дал 4.7-кратное сжатие. Меши от Meshy не сварены: 83 525 вершин на 62 125 треугольников — это 1.34 вершины на треугольник при норме 0.5, и геометрия занимала 5.3 МБ. Сварка убрала 52 693 вершины, но экспортёр разбивал их обратно, поэтому включено сжатие Draco: 5.32 МБ → 1.55 МБ. Декодер лежит в
public/draco/и скачивается только вместе с моделью. - Что осталось от Штормколлера. Его модель осталась в игре как есть — без анимаций, потому что у неё посох разваливается при движении. Перегенерировать его новым конвейером — отдельная задача.
Первая объёмная модель: Грозовой шаман
Присланный FBX «Stormcaller Warchief» превращён в игровой ассет, и на экране героя он теперь не картинкой, а настоящей геометрией — крутится сам, раскручивается пальцем. Подробности конвейера — в wiki/08-объёмные-модели.md.
- Что было в файле. 151 МБ, одна статичная сетка: 3 596 792 треугольника при 1 798 064 вершинах, четыре текстуры по 2048×2048. Ни скелета, ни одной анимации — значит, походки или замаха из неё не сделать, только вращение.
- Что стало. 3,2 МБ и 40 000 треугольников (в 90 раз меньше), base color и normal по 1024×1024, metallic и roughness сложены в одну ORM 512×512. Нарезка качество не испортила — проверочные рендеры до и после совпадают на глаз.
- Класс —
orc_storm_shaman, «Грозовой шаман»: орк с грозовым посохом, зелёная кожа, мех и кость, светящиеся синие руны. Совпадение с названием модели точное. - three.js подключён отдельным куском. Компонент
HeroModel3Dгрузится черезReact.lazy, поэтому основной бандл не вырос (274 КБ), а 620 КБ three.js скачивают только грозовые шаманы. Остальные 35 классов объёмной модели не имеют и показывают прежний портрет. - Экономия батареи. Рендер замирает, когда вкладка уходит в фон; при уходе с экрана сцена разбирается полностью — геометрии, материалы, контекст. Если WebGL недоступен, класс сам откатывается на портрет.
- Временная папка Blender уведена в рабочую директорию, иначе экспортёр glTF падает на переупаковке текстур с
Permission denied. - Исходный FBX убран из
game/client/public/characters/. Он лежал там, а Vite копируетpublic/вdist/— то есть 151 МБ уехали бы на сервер как веб-ассет. Исходники теперь только вmodels/. - Починена заливка. В
redeploy.jsсписок папок на сервере был зашит руками (assets,characters), и первая же новая папка —modelsс glTF — уронила деплой с «No such file». Теперь папки собираются из самой сборки, так что любая новая директория вpublic/поедет без правок скрипта. - Сервер лежал. После сборки VPS перестал отвечать и по SSH, и по HTTP — TCP открывался, но ни баннера SSH, ни ответа nginx не приходило. Помогла перезагрузка VPS из панели хостинга.
Экран героя переведён на графику
Пришли 12 ассетов из ТЗ_Экран_Героя.md — они вшиты.
- Полоса здоровья героя — своя рамка
uiframe_bar_heroс общей боевой заливкойuibar_hp. Рамка крупнее боевой (22 px) и режется так же, по горизонтали на три куска (0 95 0 95 fill) — у неё орнамент по торцам. - Слева от подписи «Здоровье» — знак
uiorn_hp(сердце-рубин в золотой оправе). Эмодзи ❤️ убран. - Под-вкладки героя получили иконки
uiic_tab_*: щит — Обзор, рука — Атрибуты, книга — Умения, созвездие — Таланты, астролябия — Статы. Иконка активной вкладки подсвечивается золотом. - Атрибуты получили иконки
uiic_attr_*: молот — Сила, крылатый сапог — Ловкость, кристалл-череп — Интеллект, кадило — Дух, башенный щит — Выносливость. - Под-вкладки больше не скроллятся вбок. С иконками пять вкладок не влезали в строку телефона, а горизонтальная прокрутка — это скрытый интерфейс. На узком экране (до 560 px) остаются одни иконки — они и нарисованы так, чтобы читаться с 20 px; на широком рядом с иконкой видна подпись.
- Ждём остаток: 12 иконок талантов
uiic_tal_*, 9 иконок характеристикuiic_stat_*и (по желанию) 6 заглушек пустых слотовuiic_slot_*.
Правка после первой примерки
Полоса здоровья выросла с 6 до 22 px, подпись получила иконку, а под-вкладки на телефоне встали в две строки — всё это съело высоту у модели героя. Что сделано: под-вкладки умещаются в одну строку (на узком экране без подписей), зазор между блоками экрана 10 → 8 px, иконка у подписи 18 → 16 px, а у модели появился max-width: 100% — раньше при высоком портрете height: 100% уводил ширину за край колонки и модель наезжала на слоты.
Боевой интерфейс переведён на графику
Ассеты из ТЗ_Боевой_Интерфейс.md больше не лежат мёртвым грузом — они вшиты в бой, арену, окно итога и экран охоты.
- Полосы здоровья и ярости — кованая рамка
uiframe_barс заливкамиuibar_hpиuibar_rage. Рамка режется по горизонтали на три куска (border-image-slice: 0 95 0 95 fill): торцы-ромбы остаются целыми на любой ширине, середина растягивается. Полосы в бою стали 15 px вместо 6 — иначе гравировка не читалась. - Круглая кнопка атаки —
uibtn_attack. Отклик на нажатие: кнопка проседает и темнеет (нажатого ассета пока нет). - Вместо текста «VS» — медальон
uiorn_vsсо скрещёнными мечами. - Окно итога боя — кованая рамка
uiframe_panel_victoryна подложкеuibg_victory. Награды подписаны иконкамиuiic_gold,uiic_xp,uiic_sp, добыча —uiic_loot. - Экран охоты — панель
uiframe_panel_huntна фонеuibg_hunt: таймер в кованом кольцеuiorn_timer, множитель риска со знакомuiorn_risk. Блок автоохоты вынесен под панель. - Знак
uiorn_riskстоит и в окне предупреждения о риске. - Почему рамки не 9-slice. У
uiframe_panel_victoryиuiframe_panel_huntорнамент с самоцветом стоит по центру верхней и нижней кромки — при растяжении он бы расплылся. Поэтому панели держат пропорции самой картинки (aspect-ratio), а содержимое садится в проём рамки в процентах. Девять кусков режется только полоса (uiframe_bar) — у неё орнамент по торцам. - Ассеты обрезаны по контенту. У рамок были большие пустые поля (у
uiframe_bar— 386 px сверху), из-за чего координаты 9-slice не сходились. Кроп сделан локально, оригиналы лежат вC:\Игра\art-source\.
Экран героя стал стартовым, новая верхняя панель
- Игра открывается на вкладке «Герой» — раньше стартовала «Мир».
- Модель героя на весь экран. Портрет тянется по доступной высоте (
height: 100%,object-fit: contain), поэтому занимает максимум места и не обрезается. - Слоты экипировки разведены по сторонам: слева оружие (у танков ещё щит), справа шлем, броня, перчатки и ботинки. Раньше все 13 слотов лежали сеткой поверх портрета.
- Полоса здоровья героя — крупная, во всю ширину, под именем. Мана и опыт — тонкими полосами внизу.
- Верхняя панель переделана. Вместо здоровья и уровня теперь: рюкзак (с числом предметов), золото, VIP со сроком, эйроны и ресурс умений (SP).
- Рюкзак открывается поверх любого экрана — кнопкой в верхней панели, поэтому в сумку можно заглянуть не выходя из вылазки.
- Панель показывается на всех страницах, кроме боя с мобом и арены.
- Промты на графику экрана героя — в
ТЗ_Экран_Героя.md(33 ассета).
Экран боя: без текста, крупные портреты, круглая кнопка атаки
- Убран текст боя — лог больше не показывается ни в бою с мобом, ни на арене. Что произошло за ход, видно по всплывающим числам и подписям над портретами.
- Убран заголовок «⚔️ Бой» — экран начинается сразу с бойцов.
- Портреты увеличены и тянутся по экрану:
clamp(84px, min(30vw, 17vh), 150px). Размер зависит и от ширины, и от высоты, поэтому на низком экране портрет ужимается сам и полосы, умения и кнопки помещаются без прокрутки. - Кнопка атаки стала круглой (76–104 px, зависит от высоты экрана), рядом — меньшая круглая кнопка «Авто». На арене вместо «Авто» — «Сдаться».
- Промты на графику боевого интерфейса — в
ТЗ_Боевой_Интерфейс.md(18 ассетов).
Риск-предупреждение, дорогая прокачка умений, плавная анимация
- Предварительный анализ боя. Перед атакой на моба сервер молча прогоняет бой 7 раз на копиях героя. Если он погибает в 40% случаев и чаще — бой не начинается, а игроку показывается окно: «Риск погибнуть», шанс поражения, текущее HP и выбор Атаковать или Уйти. Атака продолжается только после подтверждения. На слабых мобов бой стартует сразу, без вопросов.
- Прокачка умений стала дорогой. Было 20/30/40/50 SP, стало 100 / 500 / 2 500 / 12 500 — каждый следующий ранг в 5 раз дороже предыдущего. Все четыре умения до максимума: 62 400 SP (≈63 часа спокойной охоты или 21 час на риске на 30 уровне). Цены вынесены в каталог (
rankUpCosts), клиент больше не дублирует формулу. - Анимация боя переделана. Длительность выросла с 0.95 до ~1.5 с, элементы разведены по слоям и времени: сначала вспышка иконки приёма, через 0.26 с подписи (СВЯЗКА / УВОРОТ / ОГЛУШЁН), через 0.33 с числа урона — каждое ниже предыдущего на 24 px и с задержкой 0.15 с. Раньше урон и «СВЯЗКА» рисовались в одной точке одновременно и накладывались друг на друга.
Динамика боя, окно итога и уровень без лечения
- Визуальная динамика боя. Сервер вместе с состоянием боя отдаёт события за последний ход (урон, крит, лечение, щит, «Уворот», связка, оглушение, периодический урон, применение приёма) — клиент по ним рисует всплывающие числа и вспышку иконки приёма. Никакого разбора текста лога: события структурные.
- Бой завершается сам. Кнопки «Назад» и «Сражаться снова» убраны. Как только бой окончен, сервер закрывает его, клиент показывает последний удар и через ~1 секунду открывает окно итога с наградой, добычей и одной кнопкой «Закрыть». То же на арене.
- Всплывающее окно повышения уровня показывает, какой уровень достигнут и что он дал: очки атрибутов, очки талантов, до какого ранга теперь можно качать умения, открылись ли новые таланты.
- Восстановление HP/MP при повышении уровня убрано — и в бою, и в
grantXp(квесты, арена, клановая охота). Иначе уровень был бы бесплатным лечением и обесценивал риск на охоте. Проверено: после боя с уровнем HP осталось 536 при максимуме 592.
Длительность боя на арене ≈ 30 секунд
- Замер показал разброс 6–51 ход. Берсерк и разбойник убивали равного противника за 6 ходов, а некромант со жрецом лечили друг друга 40+ ходов и не могли закончить. Простое увеличение HP только усиливало разброс (некромант уходил за 300 ходов).
- Три ручки вместо одной:
- запас HP на арене ×2.6;
- урон растёт по ходам (+7% за ход, потолок ×2.6) — лекари больше не могут тянуть вечно;
- лечение на арене ×0.65 — иначе зеркальные хилеры не добивают друг друга.
- Темп хода 1.6 секунды на сервере. Результат хода отдаётся с паузой, поэтому длительность не зависит от скорости нажатий. Повторный клик во время хода отклоняется, кнопки на паузе гаснут, под сеткой умений виден счётчик хода.
- Итог: равные бойцы — в среднем 18–20 ходов, замер живого боя дал 20 ходов за 32.0 секунды (ровно 1.60 с на ход).
Бой на арене переживает обрыв связи
- Незавершённый бой на арене сохраняется в базе и возвращается игроку при переподключении — вместе с номером хода, HP/MP/яростью обоих бойцов и логом. Раньше обрыв связи посреди боя означал, что бой просто исчезал.
- Пока бой на арене идёт, посторонние действия запрещены: вылазка, выбор стороны, воскрешение, автоохота и второй бой на арене отклоняются с ответом «Сначала заверши бой на арене». После завершения боя всё снова доступно.
- Если снапшот соперника исчез, бой снимается (с сообщением), а не подвешивает игрока.
- Проверено сквозным тестом: сессия закрывается на 3-м ходу, при переподключении бой на месте с теми же HP и ходом, все посторонние действия отклоняются, после победы/поражения — снова разрешены.
Отдых убран
- Кнопка «Отдых» удалена отовсюду — с экрана охоты, с вкладки «Мир» и из меню. Вместе с ней удалён серверный обработчик
restи функция в сторе, так что бесплатного полного восстановления HP/MP больше нет. - Причина: «Отдых» обесценивал систему риска на охоте — можно было биться на 10% HP ради максимального множителя добычи и жать «Отдых» по кругу.
- Восстановление теперь только: пассивный реген (HP 6%/мин, MP 3%/мин), зелья на охоте (до 10 за вылазку) и воскрешение.
Вылазка переживает обрыв связи
- Найдена причина «выброса на выбор стороны» посреди охоты. Вылазка и автоохота не сохранялись в базу — в
serializeForDbих просто не было. Любой обрыв связи (свёрнутая вкладка, моргнувшая сеть, перезагрузка) пересоздавал персонажа из базы уже без вылазки: охота пропадала, а при 10 уровне без фракции клиент сразу показывал окно выбора стороны. ТеперьexpeditionиautoHuntсохраняются и восстанавливаются (вылазка — только если время ещё не вышло), а счётчик убийств/золота/опыта переживает переподключение. - Пока идёт вылазка, посторонние действия запрещены. Выбор стороны и вход на арену отклоняются с ответом «Сначала заверши вылазку» — и на сервере, и в интерфейсе. Окно выбора стороны не появляется, пока охота активна.
- Проверено сквозным тестом: сессия закрывается целиком, при переподключении вылазка на месте, автоохота продолжается, счётчик сохранён, выбор стороны отклоняется.
Жёсткий каркас экрана
- Страница больше не скроллится вообще.
html/body/#rootполучилиoverflow: hidden, а.app— высоту ровно в окно (100dvh) и flex-колонку. - Шапка и нижняя навигация стоят на месте.
TopBarи.bottom-navбольше неsticky/fixed, а обычные элементы каркаса — они не уезжают при прокрутке. - Скроллятся только внутренние области: у вкладок
.tab-content, у боя и арены — лог боя (забирает свободное место, поэтому бойцы и кнопки приёмов всегда на экране), у охоты —.scene-body, у создания героя —.create-body(панель с именем и кнопкой стала подвалом каркаса вместоposition: fixed). - Панель ошибки и фон локации остались оверлеями — это правильно.
Арена 2.0 — ручной PvP
- Вход в бой подешевел с 500 до 10 золота. При 500 две проигранные драки подряд запирали игрока вне арены: поражение не даёт наград, а на новый вход нужно было копить. Цена вынесена в константу
ARENA_COSTи отдаётся клиенту через каталог (arenaCost), чтобы не была прописана в двух местах. - Ошибки теперь видны. Стор писал ошибку в 45 местах, но показывал её только экран создания героя — любое неудачное действие молчало. Добавлено всплывающее сообщение на всех экранах.
- Цена входа и баланс видны на экране арены, кнопки «В бой» блокируются с пояснением, если не хватает золота или попыток.
- Автобой с арены убран. Бой стал пошаговым и ручным: ярость, связки, щиты, лечение, оглушение — те же механики, что в PvE.
- Соперник — офлайн-снапшот и играет авто-ротацией по своей очереди приёмов. Быть в сети не обязательно. Предупреждения о его приёме нет.
- Оба бойца входят в бой с полными HP/MP и яростью 30. Старые снапшоты достраиваются на лету (ярость, метка, оглушение, DoT).
- Бойцы подписаны именами игроков, а не названиями классов — раньше в логе было «Рыцарь-защитник против Рыцарь-защитник».
- «Сдаться» попытку и золото не возвращает, рейтинг не меняет.
- Мгновенный
pvpResolveудалён — теперь одна реализация PvP вместо двух. - Ники стали уникальными: раньше можно было создать двух героев с одинаковым именем.
Автоохота
- Три режима автоохоты, выбираются в «Меню → Автобой»: только обычные (игнор ×5/×10), + героические ×5, + мифические ×10.
- Приоритет цветов задаёт игрок стрелками ↑/↓; по умолчанию белый → оранжевый → зелёный → бледно-красный. Внутри цвета первым берётся моб ниже уровнем.
- Автоохота сама выбирает цель, дерётся и берёт следующего до конца вылазки. Один бой каждые 9 секунд (~46 за вылазку), иначе мгновенные бои выкосили бы всю охоту за секунду.
- Лента последних боёв и счётчик убито/золото/опыт на экране «Охота». Остановка при смерти, конце вылазки, отсутствии подходящих целей, окончании подписки или по кнопке.
- При HP < 35% автоохота сама пьёт зелье (лимит 10 за вылазку).
- Ручной бой прерывает автоохоту.
Боёвка 2.0
- Цвета мобов: синие, голубые и красные убраны из генерации. Спавн только −6…+9 (зелёный 20%, белый 40%, оранжевый 28%, бледно-красный 12%). Причина: у синих и красных множитель награды 0.05 — слот охоты был пустым.
- Ярость: новый ресурс (0–100, старт 30). Обычная атака даёт +20, приёмы тратят 20–40. Убраны «приём раз за бой» и общий кулдаун 3 хода — именно они превращали бой в жёсткий сценарий без выбора.
- Связки: открывающие приёмы (щит, баф, яд, оглушение, провокация) вешают метку на 3 хода, завершающие (урон, АОЕ) бьют по метке ×2.2. Лечение вне связок.
- Роли мобов: громила (тяжёлый удар), колдун (порча на игроке), жрец (самолечение), хитрец (серия). Все приёмы видны за ход: «⏳ готовит: …». Оглушение срывает замах.
- Провокация теперь ослабляет следующий удар врага на 40%.
- HP мобов в вылазке ×1.5 — бой длится 5–8 ходов вместо 3–4, чтобы связки и замахи успевали сыграть.
- Автобой по подписке: 1 день — 25💎, 7 дней — 150💎, 30 дней — 500💎. Без подписки кнопка заблокирована.
- PvP на арене переведён на ту же модель: ярость и связки вместо «раз за бой».
Текущая версия
- Классы: переработаны с 72 до 36 (6 на расу: 1 танк, 1 хилер, 1 РДД-маг, 1 РДД-лучник, 2 мили).
- Фракции: после 10 уровня выбор стороны (Орден Света / Легион Тьмы).
- Кланы: создание, вступление, выход, состав, привязка к стороне.
- Вылазки: энергия (100, +1/мин после вылазки), пул 6 мобов, респавн, цвета мобов, героик ×5 (
5%), мифик ×10 (1%). - Дроп: по слотам (оружие 0.01%, кольца/шлемы 0.1%, броня/плащ 0.05%).
- Смерть: воскрешение за 2 💎 Эйрона.
- Пассивный реген: HP 6%/мин, MP 3%/мин.
- Карта мира: 24 локации (6 стартовых + 15 оспариваемых + 3 эндгейм).
- Промпты: 36 персонажей в стиле WoW (оригинальный дизайн), зелёный хромакей.
Ранние этапы
- Первоначальная версия «Realm of Heroes» (9 классов, одиночный режим, localStorage) — заменена на «Эйрон».