ЭйронMMORPG
—онлайн Играть

Журнал

Обновления

Технический журнал игры: что именно поменялось, где и зачем. Без маркетинга — по делу.

Всего записей: 90

Журнал в вики

Арена: в рамках — поясные портреты пар

  • Владелец нарисовал новый набор — portrait_<раса>_<класс>.png, по одному на каждую пару:

кадр «по пояс», вертикальный (3:4), с прозрачным фоном. Старые race_*.jpg (лица-бюсты) удалены, поэтому рамки показывали именно их — «портреты стоят старые».

  • FramedPortrait переведён на новый набор: в рамку идёт поясной портрет пары, и вписывается он

целиком (object-fit: contain). Арт уже обрезан по пояс, резать в нём нечего: при cover срезались бы макушка и пояс. По бокам остаются узкие поля, их закрывает подложка.

  • Запасной путь остался — полный портрет пары (<раса>_<класс>.png) с кадром «по грудь»: так

показывается пара, для которой поясного кадра ещё нет. Сейчас такая одна — undead_warlock (28 портретов из 29 пар). Герой без класса тоже идёт этим путём.

  • Проверки обновлены: стенд deploy/check-arena-bg.cjs (его подставные соперники теперь только

законные пары — раньше там был эльф-берсерк, и проверка честно ловила «портрет не загрузился») и deploy/check-arena-live.cjs (сверяет portrait_* в рамках на боевом).

Лор: обложка земли, галерея земель, кодекс и голос места

  • Раздел «Лор» перестал быть текстовой панелью. Была полоска со шкалой Немоты, Прорывом и

кнопками действий — теперь это четыре части с картинками.

  • Часть I — «Ты здесь»: обложка земли. Широкая иллюстрация земли

(location_<земля>, у всех одиннадцати земель уже нарисована), поверх неё — тип земли с уровнями, название и «эссенция»: строка, которой земля себя описывает. Ниже — чипы (Слово, Голос Немоты, кодекс), шкала Немоты, Прорыв и лорные действия.

  • Часть II — «Земли Эйрона»: галерея одиннадцати земель с картинками. Тап раскрывает

свиток земли: «Как выглядит», «Кто здесь», «Почему война» и «Тайна земли». Тайну видно только у земли, где герой стоит, — остальным «откроется, когда дойдёшь»; метка на плитке показывает, что карточка у земли есть.

  • Часть III — «Эхо Раскола»: шесть разделов кодекса плитками — иконка, название,

«открыто / всего» и полоса прогресса. Кодекс подгружается заранее, а не в момент открытия окна, и открывается оттуда же.

  • Часть IV — «Голос места»: ведущая ЛОРа Нелла и строка, которой земля встречает

вошедшего (enterTip).

  • Код: новый components/LoreTab.tsx; состояние земли вынесено из старой

ZoneLorePanel в components/ZoneStatus.tsx (она удалена); в каталог добавлено поле loreZones — карточки всех земель без secret, чтобы галерея не ходила на сервер за каждой землёй. Стили — новый раздел в lore.css.

Таланты: три вкладки веток и плитки навыков

  • Экран талантов переоформлен по референсу владельца. Раньше три ветки лежали одна под

другой сплошным текстом: у каждой шапка, ступени и по четыре подписанных навыка. Теперь сверху три вкладки — со своей иконкой и своим названием, а под ними только таланты открытой ветки: шесть рядов по четыре плитки.

  • Плитка как на скрине: название сверху, квадратная иконка, «ранг N/5» снизу. Красный

ромб значит «сюда можно вложить очко» — он горит только на первой незаполненной ступени, иначе при 30 свободных очках стоял бы на всех 24 плитках сразу. Зелёная галочка — ранг 5/5, замок на вкладке — ветка закрыта (очко уже вложено в другую), галочка — вложено в неё.

  • Очко тратится осознанно. Нажатие по плитке больше не вкладывает очко само: под этим

рядом открывается карточка с эффектом, рангом и кнопкой «Вложить очко». Так экран остаётся чистым, как на референсе, а на телефоне видно, что именно даёт талант (title там не показывается вовсе).

  • Сброс и правила на месте: кнопка сброса ветки за 50 Эйронов и строка правил

(очко за два уровня · ступени на 1/10/20/30/40/50 · один навык из четырёх до 5 ранга).

  • Код: экран вынесен из CharacterTab.tsx в components/TalentTree.tsx (вместе с загрузкой

дерева), старые классы .talent-node*, .talent-tier*, .talent-branch* из index.css удалены. Иконки веток и навыков пока эмодзи: файлов talent_<ветка>.png и talentskill_<id>.png ещё нет, GameIcon подхватит их сам, когда нарисуют. Стенд проверки — кадры на 320×568, 360×640 и 390×844.

Экран героя: без сундуков, «Задания» крупнее

  • Кнопку «Сундуки» с экрана героя убрали. В углу окна стояли две кнопки — «Задания» и

«Сундуки». Сундуки открываются там, где их покупают: «Меню → Сундуки», где у плитки остался счётчик неоткрытых.

  • «Задания» стали заметно крупнее. Освободившееся место отдано единственной кнопке:

сторона выросла с 26…40 px до 34…50 px, а рисунок внутри — почти вдвое (на 360×640 было 17 px, стало 30 px). Точка «ждёт награда» и блик по иконке остались как были.

  • Лишний код убран вместе с кнопкой: HeroQuickButtons стал HeroQuickButton,

ChestsModal и состояние chestsOpen в сторе удалены — модалка стала недостижимой.

  • Проверки переписаны под одну кнопку: deploy/check-cw.cjs, check-hero-ui.cjs,

check-icon-anim.cjs, check-icons-live.cjs, verify-quick-icons.cjs, shot-ui-icons.cjs и стенд deploy/stand-cw.html. Кадры стенда 320×568, 360×640 и 390×844: окно влезает, прокрутки нет; живой компонент проверен на стенде Vite (одна кнопка, иконка, точка, блик).

Ранее

Шесть правок по жалобам: прогресс профессии, выход из окна, гнёзда, чары и руны

  • «Крафчу, а профессия не прокачивается». Сервер практику начислял и раньше, а вот карточка

уровня брала данные из столицы, которая грузится один раз при входе, — после работы полоса оставалась старой. Теперь после любой работы, которая даёт практику (крафт, добыча, снятие шкур, чары, разбор, огранка, гнездо, вставка, руны), карточка обновляется, а сообщение о крафте показывает саму прибавку: «⚒️ Выковано: готово · практика +15 · уровень 2!».

  • Чары: выбор предмета плитками, а не выпадающим списком. На экране чар предмет выбирается так же,

как в рюкзаке — плиткой с иконкой, грейдом и подписью «+7 · чар 1 · мастерская».

  • Скрещивание рун: нажатие на плитку берёт руну в пару. Раньше оно открывало попап, и «выбрать

две» было неочевидно — кнопка скрещивания не загоралась. Теперь в режиме скрещивания плитка выбирается нажатием, а если материала ступени не хватает, кнопка прямо говорит чего и сколько.

  • Из окна профессии снова есть выход. У кузни, ювелира и Рунного дела в Начертаниях не было кнопки

«Назад» — выйти можно было только сменой вкладки. Кнопки появились, а слой фона профессии больше не перехватывает нажатия (pointer-events: none): раньше он мог лежать поверх кнопки.

  • Гнёзда под руны: у вещей появились иконки — по одному названию не понять, что это за предмет.
  • Надетые вещи больше не пропадают из создания гнёзд. Фильтр опирался на устаревшее правило

«надетое в сумке не лежит», хотя надетые вещи остаются в рюкзаке. Теперь они в списке с пометкой «надето», а переплавка для них погашена с подсказкой «сначала сними».

  • Тесты: новый раздел 20 в test/forge-crafting.js: крафт начисляет практику, порог второго уровня

берётся из лестницы, потолок героя не даёт перепрыгнуть уровень. Весь набор сервера — 65 файлов — зелёный.

Иконки навыков талантов: 1224 иконки нарезаны из 21 листа

  • Сделано: 21 лист сгенерирован, нарезан и стоит в игре — 1224 иконки из 1224 (ТЗ). Расклад — как владелец рисует спрайты: 60 плиток (10×6) на лист, ровно 21 лист вместо исходных 51 по 24 плитки (−30 запросов генерации). Модель вытянула 60 объектов в кадре без слипания. Плитки идут подряд по порядку «класс → ветка → ось → ступень», поэтому лист захватывает хвост одной ветки и начало следующей; порядок построчный, таблица каждого листа лежит в models/ПРОМТЫ-ТАЛАНТОВ-УМЕНИЯ.md.
  • Нарезка «как нарисовано»: 128×120, без обрезки и без растяжения. Полотно пришло 1280×720, сетка 10×6 делится нацело, поэтому плитка = готовая иконка 128×120 (соотношение 16:15). Набор весит 33 МБ вместо 97 МБ у прежнего квадратного варианта. Нарезчик берёт и PNG, и JPG (16 листов PNG, 5 JPG).
  • Карточка в игре — под пропорцию иконки. Было aspect-ratio: 1/1 + object-fit: cover: квадратный блок срезал иконке края, а прежняя нарезка ещё и растягивала 128×120 до 256 — вместе это выглядело плохо. Стало: блок aspect-ratio: 16/15, картинка object-fit: fill — иконка ложится точка в точку, ничего не обрезано и не мылится. Квадратные значки осей (запаска) растягиваются на 6,7 %, незаметно, зато без тёмных полос по бокам.
  • Сверка глазами, а не на доверии. Первые шесть иконок листа 1 — «Мощь» воина: Заточка клинка · Ратный пыл · Песнь секиры · Булатный вихрь · Печать возмездия · Поступь войны — совпали с таблицей листа и манифестом. Отчёт node tools/check-talent-icons.cjs: «иконок нарезано: 1224 из 1224».
  • Сырые листы убраны из клиента. 21 полотно лежали в game/client/public/characters (30 МБ) и ушли бы в выкат вместе с игрой; теперь они в models/talents/skills/raw/, а в клиенте только нарезанные иконки.
  • Промты и описания остались как были — 1224 английских описания объектов по 72 на класс (models/talents/skills/<класс>.json), без русских названий (кириллица в промте подталкивает модель рисовать буквы). Значки осей (12 файлов) остаются запасом: TalentTree.tsx ищет значок в три шага — свой файл навыка → значок оси → эмодзи.
  • Дальше дешевле не обязательно. Было 51 лист (по 24 плитки), стало 21 (по 60) — минус 30 запросов генерации. Если понадобится ещё: --grid=12x6 даёт 17 листов (ровно один класс на лист), но плитка там в полтора раза мельче проверенной — только с пробным листом.

Магазин: припасы, наборы, визуал, VIP-карта

ежедневные припасы, наборы, визуал и VIP-карта. Четыре механики в одном экране, состав и цены — в data.ts, правила — в новом game/server/src/game/shop.ts. Платежей в проекте до этого не было ни строки: слой оплаты (game/server/src/game/payments.ts) написан с нуля.

  • Ежедневные припасы. Цикл 28 дней = 4 недели, три тира, **награды тиров

суммируются, бонусы — нет (берётся лучший). Обычный тир бесплатный: золото, зелья, свитки, сундук в конце недели. Эпический (149 ₽ / 99 ⭐ за неделю, 449 ₽ / 299 ⭐ за цикл) добавляет Эйроны, ключи и сундук наёмника, плюс +20% опыта и +50% шанса ресурсов профессии; легендарный (349 ₽ / 229 ⭐ и 999 ₽ / 649 ⭐) — Осколки Силы, Ключи Рейда, сундук Древних, +35% и +75%, а в венец цикла — косметика. Пропущенное платный тир отдаёт до семи дней назад** (за подписку заплачено), бесплатный день не копится. Числа недель 2–4 умножаются на 1,25 / 1,5 / 2 — предметы множителем не трогаются.

  • Наборы. Шесть наборов с составом, перечёркнутой ценой и посчитанной

выгодой (401% у «Огненного», 503% у «Подарка Маммона»), три вида лимитов — всего, в сутки, в неделю, — и порог по уровню у новичка и рейдера. Лимиты считаются метками покупок в профиле (shopBuys), а не счётчиками: сутки и неделя берутся ключами dayKey / weekKey (UTC), как в заданиях.

  • Визуал и VIP. Шесть новых образов из лора (Рамка Прорыва, Фон Раскола,

Аватар Немоты, Рамка Горна, Фон Столицы, Аватар Смотрителя) — три типа, как в игре: рамка, фон, аватар. VIP-карта 1/10/30 дней за 15/135/400 ⭐ и 25/229/649 ₽, срок копится, преимущества списком. Старые пути за Эйроны (shop:buy-vip, shop:buy-auto, shop:buy-cosmetic) не тронуты: витрина добавляет второй способ оплаты, а не заменяет первый. Боевых бонусов за деньги не добавлено ни одного — принцип «донат не даёт преимущества в бою» (ТЗ, 8.4) держится.

  • Оплата. Провайдеры: Telegram Stars (createInvoiceLink, валюта XTR;

клиент открывает счёт Telegram.WebApp.openInvoice), рублёвый — заглушка под ЮKassa/CloudPayments, и manual для тестового контура и поддержки. Покупка живёт строкой в новой таблице purchases: pending до оплаты, выдача переводит её в paid условием в SQL, поэтому повторная доставка вебхука не выдаёт товар второй раз. Игрок офлайн получает покупку тоже: профиль поднимается из базы, товар кладётся в него и сохраняется. Вебхук POST /api/payments/telegram отвечает на pre_checkout_query (у Telegram на это 10 секунд) и отклоняется, если бот настроен, а TG_WEBHOOK_SECRET не задан: без этого счёт можно было бы «оплатить» запросом из браузера. Без ключей витрина видна, а покупка отвечает «Оплата пока не подключена».

  • Клиент. Пятая вкладка нижнего меню «Магазин» и экран

game/client/src/pages/tabs/ShopTab.tsx: сегменты «Припасы · Наборы · Визуал · VIP», лента семи дней с «СЕГОДНЯ», кнопка «Забрать», карточки наборов с выгодой и полосой остатка, сетка визуала с «Купить · Куплено · Надеть», сроки VIP с преимуществами, таймер «Новый день» и окно награды. Точка на вкладке — по новому полю профиля suppliesReady.

  • Проверки: game/server/test/shop.js — 120 проверок, 0 ошибок

(выдача дня и смена суток, окно семи дней, венец цикла, суммирование тиров и «лучший бонус», опыт +20/+35% и множитель добычи, лимиты наборов, состав покупки, продление VIP, разбор sku и цен, состояния витрины, секрет вебхука и разбор обновлений Telegram, согласованность остатка до нового дня). deploy/check-shop.cjs — 24 проверки по сокету на живом сервере. deploy/shot-shop.cjs — 0 ошибок: шесть вкладок нижнего меню влезают в 360 px, «Припасы» и «Визуал» собираются без прокрутки, «Наборы» и «VIP» прокручиваются как списки, «Забрать» выдаёт награду и показывает окно «Награда получена!» (золото в шапке 500 → 2 500). Съёмка нашла реальный баг: таймер «Новый день» показывал 497807:59:57, потому что клиент принял абсолютную метку за остаток времени; сервер теперь отдаёт newDayLeft, и проверка «метка − сейчас = остаток» стоит в тесте. Снимки — tools/tmp/shop/.

  • Что осталось: токен бота и секрет вебхука в службе (шаблон —

deploy/game.service), рублёвый провайдер, возвраты и арт (фон bg_shop.jpg, иконки вкладки и визуала — раздел 11 ТЗ).

«Поделиться боем» и «Лог боя» в окне итога

  • Требование владельца по референсу: в окне итога боя снизу две кнопки — «Лог боя» и «Поделиться

боем», а в «Поделиться боем» два пути: ссылкой и в чат игры (ТЗ_Поделиться_боем.md). Сделано и на арене, и в вылазке.

  • «Лог боя» открывает весь журнал боя, а не последние строки, и сразу на конце — развязка

важнее начала. Сервер отдаёт в итоге 60 строк вместо 24; окно итога по-прежнему показывает последние десять.

  • «Поделиться ссылкой» даёт ссылку …/?battle=КОД. Код — 6 знаков без 0/O и 1/I/L

(его диктуют голосом), живёт месяц, и открывается без персонажа: тот, кому бой прислали, видит карточку ещё до регистрации. Порядок поделиться — системное «Поделиться» → Telegram → буфер; если буфер закрыт (http без TLS), ссылка показывается руками, а не молча теряется.

  • Карточка боя — портреты обеих сторон в их рамках косметики (та же FramedPortrait, что на

арене), уровень, мощь, тир, награды с дельтой рейтинга и конец журнала. У вылазки на месте соперника моб со своим значком.

  • «Поделиться в чате игры» отправляет строку-карточку в мир, клан или отряд — и не переключает

вкладку чата. Личных сообщений в игре нет, поэтому «в личку» не предлагаем. Ссылки в чате запрещены антиспамом, поэтому уходит текст с кодом боя, а не URL: строка короче 200 знаков, а код из неё можно назвать голосом.

  • Карточка пишется в базу только по нажатию кнопки, а не на каждом бою: сервер запоминает

«черновик» последнего боя, а код и запись появляются, когда игрок действительно поделился. Повторное нажатие отдаёт тот же код. Таблица — shared_battles, всё старше месяца вычищается.

  • Проверки: game/server/test/battle-share.js (код, повтор, срок жизни),

deploy/check-battle-share-live.cjs (полный путь по сокету на живом сервере: герой → бой → код → открытие по коду) и deploy/check-battle-share.cjs (окна на настоящих компонентах, стенд deploy/stand-share.tsx). Снимки — models/share-*.png.

Арена: портреты бойцов в рамках косметики

  • **Решение владельца: «у героя стоит его портрет, он в рамке, так же портреты соперников в рамках —

хочу так же» (по референсу). На карточках арены слева вместо значка тира теперь стоит портрет бойца**, нарисованный в надетой рамке косметики: у героя — своя, у каждого соперника — его. Значок тира переехал в строку с именем, там же остались раса, класс и рейтинг.

  • Рамки уже были нарисованы (`uiic_cosmetic_frame_basic / gold / ruby / eternal / hunter / master /

champion, 256×256, с прозрачной серединой) — рисовать ничего не понадобилось, путь к ним даёт готовый uiIconPath('cosmetic', frameId)`.

  • В рамку идёт портрет расы (решение владельца, 15.10.2026: «в портрет в рамке мне надо вставить

то, что слева от надписи Воин»). Это race_<раса>.jpg — тот самый значок, что стоял в строке с классом. Он нарисован бюстом 1:1, поэтому в проёме рамки сидит как есть: ни кадрировать, ни подрезать его не нужно, лицо читается сразу. Полный портрет пары «раса × класс» остался запасным путём — он во весь рост, и для него в arena.css есть кадр «по грудь».

  • Значок расы убран из строки с классом — то же лицо теперь в портрете слева, а дважды подряд оно

выглядело случайной копией. Класс и его иконка на месте.

  • Рамка лежит снизу, портрет — поверх, в её проёме. Наоборот нельзя: у «Рамки мастера» и «Рамки

чемпиона» проём закрашен своим рисунком (наковальня и знамя), и положенная сверху рамка просто закрыла бы портрет.

  • Запасной путь тоже поправлен по замечанию владельца («иконки в Арене обрезаны»): окно кадра

больше не начинается выше макушки. Фигура в портретах пар начинается с 6% высоты, а прежнее окно — с 4,4%, поэтому голова упиралась в верхнее кольцо рамки. Теперь для пары окно идёт ровно от верха фигуры (центр 16,7%, высота треть), а плитки подросли: 60 px у героя и 50 px у соперника.

  • У соперников рамка приходит с сервера: snapshotOf кладёт в снапшот арены cosmeticFrame,

arena:list отдаёт его клиенту. Снапшоты, снятые до правки, поля не знают — им достаётся базовая рамка, ничего не ломается.

  • Проверка — та же deploy/check-arena-bg.cjs (стенд deploy/stand-arena.tsx): теперь она

дополнительно сверяет, что у героя и у всех трёх соперников портреты в рамках загрузились, что рамки разные, что портрет стоит в проёме (а не поверх рамки) и внутри карточки, и что экран по-прежнему без прокрутки. Снимок — models/arena-bg-портреты-в-рамках.png.

  • Осиротевшие стили значка тира (.arena-tier__badge, .arena-opp__tier и их img-правила) убраны

из index.css; описание — в ТЗ_Арена.md, разделы 2.1 и 2.2.

Таланты заново: 51 ветка, 6 ступеней, 4 навыка и одна прокачиваемая ветка

  • Система переписана целиком (ТЗ 26). Было 12 веток на четыре группы ролей по 5 плоских узлов: к 60 уровню игрок забирал почти всё, и выбора «это или то» не существовало. Стало: у каждого из 17 классов свои 3 ветки (наступление / стойкость / мастерство), в ветке 6 ступеней по 4 пассивных навыка, на ступени берётся ровно один, и он качается до 5 ранга. Прокачивать можно только одну ветку — её запирает первое вложение; сброс стоит 50 Эйронов и возвращает все вложенные очки.
  • Очко теперь приходит за каждые два уровня, начиная с первого: 1, 3, 5, …, 59 — ровно 30 очков к 60 уровню, то есть 6 ступеней × 5 рангов закрываются целиком, без остатка. Первый навык доступен сразу на 1 уровне, ступени открываются на 1 / 10 / 20 / 30 / 40 / 50. Выбор ступени не зависит от предыдущих: цепочек нет, сборка «шесть навыков одной оси» проходит тест.
  • 1224 навыка и 51 имя ветки написаны под класс: у рыцаря смерти лёд и некротика, у барда песни, у друида-медведя когти. Имена уникальны по всей игре — генератор tools/make-talents.cjs отказывается писать данные, если нашёл повтор. Названия живут в models/talents/draft/*.json, механика и числа — в генераторе: пять лестниц силы (урон, запас, проценты, крит. урон, вампиризм) задают прирост за ранг, поэтому баланс правится в одном месте, а пересборка переписывает все навыки разом. Таблицы всех 51 ветки вписаны в ТЗ 26.
  • Свободные очки считаются от уровня, а не копятся счётчиком: grantedTalentPoints(level) − spentTalentPoints(build). Профиль поэтому сам себя чинит — и после правки уровня админкой, и после переписанного дерева. Старый профиль с talents: string[] мигрирует в пустую сборку: прежних узлов в новом дереве нет, очки возвращаются полностью (у 60 уровня — все 30). Мусор в новой сборке отбрасывается при загрузке: навык должен существовать, стоять на своей ступени, принадлежать классу героя и иметь ранг 1..5; сборка из двух веток невозможна.
  • Экран талантов (CharacterTab.tsx) показывает три ветки карточками: активная подсвечена, закрытые притушены с пометкой «🔒 занята». Внутри — шесть ступеней, в ступени сетка 2×2 из четырёх навыков, у выбранного видны пять делений ранга. Сосед по ступени не нажимается: выбор делается один раз, менять — через сброс с подтверждением. Сокеты переименованы в talent:spend и talent:reset; админский сброс тоже возвращает вложенные очки, а не число навыков.
  • Дерево не кладём в каталог. /api/catalog грузится на каждом входе, а 1224 навыка — это 245 КБ (замер), тогда как герою нужны 24 навыка его класса. Поэтому дерево отдаёт отдельный GET /api/talents?class=<id> (14–15 КБ), а клиент догружает его один раз при открытии вкладки «Таланты» (loadTalents в store.ts). Пока дерево не пришло, экран показывает «Загружаю дерево талантов…», а не пустоту.
  • Иконки. Своей иконки у каждой ветки пока нет — экран ищет talent_<ветка>.png, затем общий значок оси (talent_atk / talent_def / talent_mastery), затем эмодзи. Генератор промтов переделан на 17 листов по классу, и первым делом просит три общих значка — они закрывают все 51 ветку сразу (ТЗ иконок талантов).
  • Как проверял. node tools/make-talents.cjs --check — 17 классов, 51 ветка, 1224 навыка, повторов имён нет. Новый game/server/test/talents.js — 10 разделов: очки от уровня, ступени по уровням, независимость ступеней, ранги и шестой отказ, эксклюзив ветки, эффект сборки по рангам, сброс за 50 Эйронов и отказ без Эйронов, миграция старого профиля и чужой сборки, дерево у всех 17 классов. Обновлены test/races-classes.js (3 × 6 × 4 у каждого класса) и test/abilities-check.js (в названиях нет «АОЕ»). Typecheck сервера и клиента чистый.

Админка: страница `/admin` — аналитика, статистика и управление игрой

  • Появилась служебная часть владельца (ТЗ_Админка.md): страница /admin, которую отдаёт сам Node-сервер (nginx получил отдельное правило location /admin, иначе запрос уходил бы в SPA-fallback игры). Вход по паролю (ADMIN_PASSWORD, иначе ADMIN_TOKEN), сервер выдаёт сессию на 12 часов; десять неудачных попыток входа за 15 минут — пауза. Десять разделов: Сводка · Игроки · Экономика · Бой и баланс · Прогресс · Источники · Кланы · Чат · Живые события · Техмониторинг.
  • Главная сложность была не в интерфейсе, а в данных. Исторических срезов в проекте не было вовсе: бой жил в памяти и умирал вместе с ней, p.quests.daily обнуляется при смене дня, входы и покупки не записывались нигде, а «онлайн» не существовало как понятие. Поэтому работа состоит из двух частей — сбор и показ.
  • Сбор идёт в три новые таблицы: events (построчный журнал заметного: регистрация, вход и выход с длительностью, новый уровень, бой вручную, PvP, арена, подземелье, рейд, покупка, крафт, заточка, лот, задание, смерть, ошибка, действие админа), metric_counters (день × область × метрика — всё массовое) и online_snapshots (снимок онлайна раз в 5 минут). Массовое — бой автофарма (около 40 000 в час на сотне игроков), дроп, любая сокет-команда, лаг и ошибки — пишется только счётчиками: строка на каждый такой случай превратила бы журнал в мусор. В журнал попадают бой вручную, PvP, арена, подземелье, рейд и каждый 25-й бой автофарма.
  • Точек записи в игре мало, и они выбраны там, где всё и так проходит. persistPlayer — единственное место сохранения профиля: отсюда бесплатно ловятся новые уровни, приход и расход золота, опыт, Эйроны, осколки, клановые очки, размер сумки и рост мощи. Обёртка слушателей в io.on('connection') даёт имя команды, время обработки, ошибку и присутствие игрока — без правок в ста тридцати обработчиках. Дальше только осмысленные точки: создание героя (регистрация и источник), disconnect (сессия), финал боя, забега, покупки и заточки.
  • Источник регистрации (отдельная просьба владельца) собирает клиент — game/client/src/source.ts: реферальный код, start_param Telegram, UTM-метки, реферер, платформа. Записывается один раз, при создании героя: это «первый источник», а не последний, иначе рефералка растворилась бы в рекламных переходах. Метки: ref, tg:<параметр>, utm:<источник>, link:<хост>, direct; у героев, созданных раньше, — unknown.
  • Управление полное. По игроку 28 действий: валюты и опыт, уровень, лечение, воскрешение и смерть, предметы, сброс атрибутов/талантов/заданий, профессии, VIP и автобой, переименование, смена зоны, мут и снятие, изгнание, бан и разбан, сохранение профиля и удаление героя. Бан действует сразу: он проверяется при входе, и «сырой» бан, выданный этим процессом, работает даже без базы. Снятие бана сделано до загрузки героя — забаненный герой это ровно тот случай, когда профиль может не загрузиться, и оставлять админа без ключа нельзя. По клану: сообщение, уровень, клановые очки, распуск. В чате: поиск по тексту и автору, удаление сообщения, ручной мут, снятие мута. Каждое действие пишется в admin_actions и в журнал событий.
  • Про «за всё время». Вкладка «Бой и баланс» показывает не только новые счётчики, но и пожизненные players.data->stats (41 метрика: убийства, PvP, боссы, подземелья, арены, крафты…), а «Прогресс» — воронку и когорты удержания. Это единственные числа, которые не зависят от того, когда админку включили, и они подписаны именно так.
  • Как проверял. Четыре автоматические проверки в deploy/: check-admin-web.cjs (разбор клиентских модулей до браузера: синтаксис, лишние кавычки внутри шаблонов, число разделов — 10), check-admin-api.cjs (поднимает сервер и сверяет формы ответов всех 19 эндпоинтов с контрактом: переименованное поле дало бы молчаливые нули на вкладке), check-admin-collect.cjs (сквозная: игровой клиент создаёт героя с источником, играет бой, делает покупку; админка видит регистрацию с меткой, вход, бой, присутствие и карточку; админ выдаёт золото — оно доходит до игрока живым обновлением, — банит и снимает бан), check-admin.cjs (страница /admin в headless-Chrome: вход, обход десяти вкладок, снимки в models/admin/, ловля исключений и переполнения по ширине на 1440 и 360 px). Все четыре — зелёные, без исключений на странице. Две ловушки нашлись именно проверками: раздел «Бой и баланс» вылезал за экран на 247 px (у ячейки сетки минимум по ширине auto, широкая таблица растягивала колонку вместо прокрутки — лечится min-width: 0), а в ответе /combat пропадали поля dungeons.n/dungeons.win (метрики не были запрошены, а undefined в JSON выбрасывает ключ целиком). Typecheck сервера и клиента чистый.
  • Чего проверить не удалось. PostgreSQL в этой среде нет, поэтому SQL-агрегаты (когорты удержания, retro, золото по уровням, источники, вехи уровней, часы активности, экспорт CSV) проверены только типами и разбором кода — на машине с базой их надо прогнать отдельно. Чтобы сломанный запрос не выглядел как «данных нет», в «Техмониторинг» добавлен блок «База»: счётчик ошибок запросов и текст последней. Три теста сервера (quests.js, vendor.js, clans.js) падают до этой работы и не из-за неё: прошлая сессия ввела goldIncome/GOLD_HARDER (цена продажи вдвое меньше), а ожидания в тестах остались старыми.

Выложено на боевой и проверено там же (15.10.2026)

  • Страница открыта владельцу: http://201.34.128.60/admin. В nginx добавлено правило location /admin (иначе запрос уходил в SPA-fallback клиента игры, и вместо аналитики отдавалась игра), конфиг сохранён в бэкап перед правкой. Задан отдельный пароль админки (Environment=ADMIN_PASSWORD в game.service): до этого единственным паролем был ADMIN_TOKEN со значением eron2026, а оно лежит в публичной сборке клиента — то есть админку с полным управлением мог открыть любой, кто прочитал JS игры. Редактор карты при этом не сломан: POST /api/map-positions по-прежнему сверяется с ADMIN_TOKEN.
  • Боевая база нашла две настоящие ошибки, которых не мог найти локальный прогон (Postgres там нет, и оба раза внешне всё выглядело рабочим):
  1. initAnalytics поднимался до initDb и проверял доступность базы через dbQuery, который до initDb всегда отвечает «базы нет». На боевом с живым Postgres таблицы events, metric_counters и online_snapshots не создались вовсе: 62 сбойных запроса, пустые вкладки. Порядок исправлен: сначала база, потом аналитика.
  2. В сборщиках SQL стояло data->>'level'::bigint — в Postgres :: связывает сильнее, чем ->>, поэтому выражение читалось как data->>('level'::bigint) и падало с invalid input syntax for type bigint: "level" сразу в семи запросах (retro, воронка, источники, золото по уровням, вехи, прогресс). Скобки вокруг выражения обязательны. Обе ошибки видны были только по счётчику сбоев SQL, который для этого и заведён.
  • Ещё одна правка по итогам проверки: удаление героя из админки не убирало его снапшот арены, и удалённый герой оставался в списке соперников — бой шёл бы с призраком. Теперь arena_snapshots чистится вместе с профилем.
  • Как проверял на боевом. node deploy/check-admin-api.cjs --base http://201.34.128.60 --read-only — все 19 эндпоинтов отдают обещанные поля и ноль сбоев SQL (это и есть проверка агрегатов на живой базе). node deploy/check-admin-collect.cjs --base … — сквозная проверка: временный герой регистрируется с меткой источника, играет бой, покупает; админка видит его в списке, в карточке и в живых событиях; выданное золото доходит до игрока событием player:update; бан закрывает вход (banned: true), снятие бана работает без загрузки профиля; тестовый герой удалён. node deploy/check-admin.cjs --base … — вход через страницу и обход десяти вкладок в headless-Chrome: без исключений и переполнений, снимки с живыми числами в models/admin-live/ (105 героев, 2,0 млн золота в обороте, 1 820 Эйронов, богатейший герой 777 714 золота). Следы проверки (28 событий, 34 счётчика, 16 записей журнала админа с префиксом adm_) вычищены из базы; профили живых игроков не тронуты.

Фоны арены подключены: восемь тиров на своих аренах

  • Владелец выложил восемь картинок — они легли в game/client/public/characters/ как

arena_tier_0…7.jpg, 720×1280 (9:16). Каждая совпала со своей сценой из промтов: двор за плетнём, частокол у берёз, заросшее каменное кольцо, каменный карьер, серый колизей, крепостной двор, базальт на вулкане и белый мрамор над облаками.

  • Своего фона у арены не было вовсе. Восемь тиров одевались в чужие картинки — четыре старых

пейзажа локаций и две столицы, причём «Крепкий» и «Сильный» делили один фон. Теперь arenaTiers.ts хранит имена без расширения и пробует .jpg → .png, а фон берут и карточки тиров, и экран боя.

  • Экран боя получил полноэкранный фон. Новый game/client/src/components/ArenaBackground.tsx

плюс слой .arena-bg в arena.css: фон ставится по тиру соперника (бой идёт на его земле, и тир и так подписан на экране), cover, кадр чуть выше центра, вуаль под интерфейс. Раньше сцена боя стояла на голом тёмном фоне.

  • Две ловушки загрузки, обе поймал тест. Скрытый <img> с onError (как в LocationBackground)

для проверки не годится: у картинки с display: none Chrome может не начинать загрузку, поэтому «файла нет» не обнаруживалось и мёртвая ссылка оставалась висеть фоном. А new Image() без ссылки собирается сборщиком мусора вместе с загрузкой — onload не приходил вовсе. Теперь попытки держатся в useRef, а состояние фона (loading · ready · none) видно снаружи в data-state.

  • Проверка — deploy/check-arena-bg.cjs на стенде deploy/stand-arena.tsx (настоящие ArenaTab

и ArenaBattleView, сервер не нужен): снимает бой на тирах 0 · 4 · 7 (models/arena-bg-бой-тир-*.png) и карточки (models/arena-bg-карточки-тир-*.png), сверяет адрес фона, его реальную загрузку, размер слоя, то, что бойцы стоят рядом, а панели и умения целы. Отдельно проверяется запасной путь: без файла должен быть градиент, а не мёртвая ссылка.

  • Документ промтов models/ПРОМТЫ-ФОНОВ-АРЕН.md дополнен разделом «Как это подключено».

Реферальная программа: «Пригласи друга» — 50 Эйронов за друга 30 уровня

  • Программа привлечения игроков (ТЗ_Реферальная_программа). Игрок получает личную ссылку

https://<адрес игры>/?ref=КОД; друг, который зарегистрировался по ней и взял 30 уровень, приносит пригласившему 50 💎 Эйронов — за каждого друга, ограничения на число нет.

  • Код — 6 знаков из алфавита 23456789ABCDEFGHJKMNPQRSTUVWXYZ (без 0/O, 1/I/L:

код диктуют голосом и переписывают руками). Выдаётся герою один раз и навсегда — уже разосланная ссылка не должна ломаться. Ссылка собирается на клиенте: сервер не знает, по какому адресу его открыли. Код читается из ?ref=, из #/?ref= и из start_param мини-аппа Telegram.

  • Зачитывается только регистрация (character:create): герой, созданный до программы,

приглашённым стать не может. Отбиваются свой собственный код, битая ссылка и повторное приглашение (ключ таблицы referrals — приглашённый, поэтому награда за него одна).

  • Три отметки вместо одной: строка создана → друг пришёл; reached → взял 30 уровень;

paid → Эйроны легли на счёт. Между вторым и третьим есть окно: пригласивший может быть офлайн, и тогда награда ждёт его входа — как выручка аукциона (takeAuctionPayout). При входе она приходит тостом «🎁 Друг взял 30 уровень: +50 💎 Эйронов».

  • Момент «30 уровень» ловится в одной точке — persistPlayer, через которую проходит любое

изменение героя (бой, вылазка, арена, автофарм, забег отряда). Проверка стоит одного обращения к Map, поэтому её не жалко на каждое сохранение, и разойтись с реальностью она не может.

  • Видно, но не мешает. Золотая кнопка 🎁 в верхней панели — размером с соседние, поэтому

раскладка панели не едет; на ней метка (число дошедших друзей, а до первого открытия — «!»). Плитка «Пригласи друга» стоит первой в меню. Раз в первый вход над нижним меню появляется тонкая полоса-приглашение — гаснет сама через 12 секунд и игру не перекрывает. Окно со ссылкой, кнопками «Поделиться»/«Копировать», счётчиками и прогрессом друзей открывается только по нажатию. Полосы на экране героя нет: он обязан влезать целиком (ТЗ_Герой, 2.3).

  • Окно влезает в телефон целиком. На 360×720 развёрнутые правила уводили его в прокрутку —

«Как это работает» свёрнуто в строку-кнопку и раскрывается нажатием. Замер на снимке: влезает: true. Копирование отвечает тремя состояниями: «Скопировано» · ничего не произошло · «браузер не дал доступ к буферу» — в последнем случае ссылка выделяется сама (на http без TLS буфер закрыт, и молчать об этом нельзя: игрок отправил бы другу пустоту).

  • Сервер: game/referral.ts (логика: коды, реестр, отметки, сводка), таблица referrals

в db.ts с индексом по пригласившему и индексом по data->>'refCode', поля refCode/referredBy в профиле, события referral:info и referral:reward в index.ts. При старте поднимаются приглашения и занятые коды — чтобы новый герой не получил чужой код.

  • Клиент: referral.ts (приём кода, сборка ссылки, «поделиться» через navigator.share →

Telegram → буфер), components/ReferralOverlay.tsx (окно и полоса), кнопка в TopBar, плитка в MoreTab, стили с единственным акцентом «золото + мягкое свечение» (свечение выключается при prefers-reduced-motion).

  • Что накрутить нельзя: себя пригласить, второй раз за одного игрока, до 30 уровня, дважды

за одного друга. Что осталось слабым — playerId в localStorage (общая слабость входа в игру, лечится только авторизацией через Telegram initData), но накрутка упирается в 30 уровней реальной игры, а не в клик.

  • Проверено живьём, а не только тестом. deploy/check-referral-live.cjs — цепочка

«ссылка → character:create → referral:info» на настоящем сервере плюс три отказа (свой код, повторное приглашение, битый код). Выплата проверена отдельно: с временно опущенным порогом в собранном dist — и когда пригласивший в игре (событие referral:reward, +50 на счёт), и когда он офлайн (referralPayout при входе, повторный вход не удваивает). deploy/shot-referral.cjs снимает четыре экрана на 360×720 и замеряет, что кнопка в панели 38×38 (раскладка не поехала), полоса не заходит на нижнее меню, а окно влезает целиком. Снимки-итоги лежат в models/: рефералка-кнопка-и-полоса.png, рефералка-окно-360.png, рефералка-плитка-в-меню.png.

  • Тесты: game/server/test/referral.js — шесть разделов (правило владельца, код, привязка,

уровень, сводка, база). Прогон всего набора сервера — 56 файлов; реферальный зелёный, девять чужих провалов (цены профессий, золото боя, награда за кланового босса) — от параллельной правки экономики, к реферальной программе отношения не имеют.

  • Осталось нарисовать три иконки — uiic_ref_gift, uiic_ref_share, uiic_ref_link

(сейчас эмодзи 🎁 🔗 📤; GameIcon подхватит файлы сам).

Фоны арены: восемь промтов 9:16

  • У арены не было своих фонов. Восемь тиров (Новичок … Сильнейший) одевались в чужие

картинки — четыре старых пейзажа локаций и две столицы (arenaTiers.ts), причём тиры 3 и 4 делили один и тот же фон. Своих арен не рисовали ни разу.

  • Промты собраны генератором tools/make-promts-arenas.cjs — по образцу фонов вылазок

(tools/make-promts-locations.cjs): сцены живут в коде генератора, стилевой хвост один на всех (ТЗ 16, разделы 3 и 6), тиры читаются из ARENA_TIERS с порогами мощи, чтобы подписи не разошлись с игрой. На выходе models/ПРОМТЫ-ФОНОВ-АРЕН.md, models/arenas/manifest.json и по одному .txt на тир в models/arenas/prompts/.

  • Лестница из восьми арен: деревенский двор → частокол у берёз → заросшее каменное кольцо →

каменный карьер → серый колизей → крепостной двор → базальтовая арена на вулкане → беломраморная арена над облаками. Вместе с постановкой меняется температура: тёплый песок и зелень в начале, холодный камень в середине, раскалённый оранжевый на «Грозном», золото с лавандой на «Сильнейшем».

  • Композиция под два места. Фон арены живёт и на карточке тира (широкая полоса с тёмной вуалью),

и на сцене боя (два бойца в середине кадра), поэтому в промтах архитектура и детали уходят в верхнюю треть и по краям, а середина и нижняя середина остаются спокойными. Отдельным блоком — общий негатив: без людей, зрителей, оружия на полу, крови и надписей.

  • Как включить: файлы кладутся в game/client/public/characters/arena_tier_<n>.png, после чего

список TIER_BG в arenaTiers.ts меняется на новые имена. Пока картинок нет, список не тронут — карточки живут на старых пейзажах. Пометка об этом добавлена в ТЗ_Фоны_Локаций.md.

Бафы вылазок: одиннадцать расходников и правило 5 %

  • Одиннадцать бафов трёх классов (ТЗ_Бафы_Вылазок). Класс I — сила (урон, физ атака,

маг атака, урон магических приёмов), класс II — крит (шанс и урон, отдельно физ и маг), класс III — добыча (ресурс умений, золото, опыт). Живут 7 минут — ровно вылазку.

  • **Решение владельца: «все бафы на статы, дают усиления не больше чем на 5 % от текущего

показателя». Прежние «+25 % к атаке», «+15 п.п. крита» и «+50 % золота» из первой редакции отменены: баф ровно умножает свой стат на 1,05. Это ставит его в один ряд с талантом** (+2 % за узел, «Неудержимый» — +5 % крит-урона), а не выше всей прокачки.

  • Крит-бафам добавлен минимум прибавки (+0,5 п.п. шанса, +0,05 урона): пять процентов от

крита героя 30 уровня — это 0,8 п.п. и 0,08, а Math.round до десятых почти всё съедает. Потолки (60 % и ×2,2) вынесены в MAX_CRIT_* и применяются последними — баф их не обходит.

  • Крит разведён по школам. Раньше effCrit отдавал одно число на физ и маг, а strike()

умножал урон на общий critDmgMul, используя признак «магия» только для сопротивления. Теперь у бойца свои spellCritChance и spellCritDmgMul, и школа удара выбирает своё.

  • Награды класса III считаются от копилки вылазки, а не от боя. Пять процентов на один бой

округляются в ноль (15 опыта → 0,75), поэтому баф копит «сколько положено» и отдаёт разницу с уже выданным: за 46 боёв по 180 золота выходит ровно 414 сверху, а пофайловое округление дало бы вдвое меньше.

  • Дроп — отдельной таблицей, а не в Mob.drops. Шансы незыблемые (решение владельца:

ни риск, ни контроль зоны их не поднимают), поэтому бафы бросаются своим циклом в finishBattle через buffDrops(). Тесты профессий проверяют состав Mob.drops целиком — и они правы: добыча моба и усиление вылазки это разные вещи.

  • Окно активации перед вылазкой (BuffPopup): плитки по классам, галочки, счётчик «×3»,

полупрозрачные плитки для отсутствующих бафов с подписью «Дроп · 1 %», зелёная плашка «Активен · 4:12» у действующего. Списание — только по «В путь»: закрыл окно, бафы целы. Класс III покупается тут же за 10 Эйронов; докупать посреди вылазки сервер не даёт.

  • Склад бафов — не предметы: Player.buffs (id → количество) и Player.buffsUntil

(id → срок). Сумку не занимают, не продаются, переживают перезапуск; просроченные сроки выбрасываются при входе. В окне итога боя бафы идут отдельной строкой «Усиления».

  • Тесты: новый test/buffs.js (8 разделов: каталог, правило 5 %, потолки, склад, срок,

награды, покупка, дроп, боец). Тест поймал настоящий баг: activeBuffs терял признак «какой стат правит баф», и «Печать силы» поднимала силу заклинаний. Весь набор сервера — 41 файл — зелёный; сервер и клиент собираются.

  • Осталось: одиннадцать иконок uiic_buff_*.png — пока подставляются эмодзи.

Баланс классов: крит больше не выше здоровья, победы 100% больше нет

  • Жалоба владельца: «маги наносят крит урон, который выше чем количество здоровья». Разбор по коду

и замерам показал, что дело было не в магах. На 60-м уровне с приёмами пятого ранга один крит со связкой снимал 147–254% здоровья обычного моба: крит (до ×3.0), связка (×2.2) и ранг приёма перемножались, а сила заклинаний растёт от ИНТ быстрее, чем здоровье цели.

  • Пять ошибок решали бой вместо класса. (1) Оглушение заканчивало бой: pvpSideTurn на пропуске

хода возвращал true, а вызывающий читает это как «цель погибла», — поэтому все шесть классов с оглушением стояли в самом низу таблицы. (2) У призывателя не было призыва: вид summon в игре есть, а «Зов слуги» был заведён обычным уроном; слуга не бил (PET_ATTACK_SHARE не читалась), а «Исцеление слуги» не лечило слугу (флаг healsPet не выставлялся). (3) Авто-бой не умел играть: классы без частых «открывашек» вообще не проводили приёмов (некромант — 0.33 приёма за ход против 0.83 у лучника), а классы с защитными «открывашками» тратили на них ходы (воин щитовался на полном здоровье и проигрывал паладингу 0% побед). (4) Ярость была штрафом: 0.42–0.50 приёма за ход против 0.83–0.88 у маны и энергии. (5) Раса решала за класс: рост ИНТ у орка 1.0, у андеда 2.9 — орк-чернокнижник бил вдвое слабее андеда-некроманта при том же классе.

  • Что изменено в механике. Крит: 1.5 + СИЛ×0.002, потолок ×2.2 вместо 1.6 + СИЛ×0.005 с

потолком ×3.0. Связка: ×1.6 вместо ×2.2. Новое правило крита (MAX_CRIT_SHARE = 0.6): крит не снимает больше 60% здоровья цели — убить с полного здоровья одним критом нельзя (обычный удар не ограничен, иначе переросший герой перестал бы добивать слабых мобов). Ярость: 40 на старте, +10 за ход, +30 за удар. Запас маны урезан до ~20 приёмов на бой. Крит-урон на арене ослаблен с 0.65 до 0.85. Здоровье: 400 + ВЫН×14 + уровень×14 вместо 80 + ВЫН×20 + уровень×16 — разброс между самой хрупкой и самой выносливой расой упал с 1.5–2.0 раза до ~1.25. Появился ROLE_GROWTH_FLOOR: рост ключевых атрибутов роли поднимается до пола, если у расы он ниже.

  • Авто-бой переписан. Приём выбирается по ожидаемой пользе, и польза считается в ходах: на

сколько ходов раньше падёт враг (урон) или на сколько ходов дольше проживу я (лечение, щит, оглушение, баф брони). Обычная атака равна ровно одному ходу. Ротация одна на три боя — вылазку, арену и отряд.

  • Наборы умений выровнены по четырём архетипам (танк, ближний бой, дальний бой и магия, хилер)

внутри коридоров abilities-check (урон 2.0–2.6, лечение 2.0–2.5, щит 4.0–4.4, яд 0.5, баф 0.22): выравнивание идёт составом и перезарядками, а не раздуванием чисел. Друид получил прямой урон «Гнев природы» вместо второго лечения (класс без единого урон-приёма выигрывал 8.8% боёв), ассасин — второй удар «Рассечение», чернокнижник — «Кровавую стрелу», паладин — лечение «Благословение», знахарь и жрец — оглушение. Пассивки выровнены так, чтобы компенсировать то, чего классу не даёт раса.

  • Замеры. Кольцевой турнир всех 17 классов на арене (уровень 30, 40 боёв на пару): разброс средних

побед упал с 90.8 п.п. (0.5% … 91.3%) до 27.4 п.п. (39.9% … 67.3%), пар с победой ≥90% или ≤10% — с 134 из 272 до 2 (на пятом ранге приёмов — 0). Обычный моб 30-го уровня умирает за 4.4–8.7 хода вместо 3.8–17.9. Подробности и оставшиеся неровности — ТЗ 25 — Баланс классов.

  • Проверки. Новый game/server/test/balance-check.js сторожит норму: правило крита, наличие урона

у каждого класса, коридор против эталона, разброс не шире 45 п.п., темп приёмов у трёх профилей, темп боя против обычного моба. Рабочий стенд — game/server/test/_balance.js. Весь набор сервера (54 теста) зелёный; pvp-check и abilities-check поправлены под новые числа.

  • Документация. Таблицы умений в ТЗ_Переработка_2026/23_ТЗ_Умения_Классов.md пересобраны из кода

(node tools/make-tz23-abilities.cjs — заодно починены жёсткие пути C:\Игра, из-за которых генератор не работал после переезда на E:). Вики обновлена: 01 — Расы и классы, 02 — Вылазки и бой.

Ранее

Автофарм: только одна вылазка, цепочки больше нет

  • Решение владельца: «Убери возможность цепочки, только по одной вылазке оплачивать автофарм».

Прежний вид «до нуля энергии» (2 💎 за каждые 25 ⚡, до четырёх вылазок, 8 💎 на полной шкале) удалён целиком: без цепочки он не отличается от разового ничем, кроме цены и счёта. Осталась одна кнопка «▶ Автофарм · 2 💎» с подписью «одна вылазка · вкладку нельзя закрывать».

  • Убрана вся обвязка цепочки: реестр офлайн-сессий farmSessions, подъём сессий после перезапуска

сервера (restoreFarmSessions), выборка loadFarmPlayers из базы, персист farm в профиле (serializeForDb / hydratePlayer), возврат Эйронов за несыгранные вылазки, итог сессии doneAt и окно FarmSummaryModal (файл удалён), событие farm:ack. Фарм живёт, пока открыта вкладка, — и поднимать после обрыва больше нечего.

  • Подтверждение остановки стало честным: вместо «Оплаченные вылазки сгорят» с расчётом возврата —

«Остановить автофарм? Оплаченная вылазка остаётся — её можно доиграть вручную».

  • Тест test/autofarm.js переписан под один вид фарма: 7 разделов вместо 10. Ушли проверки плана,

цепочки, возврата и разбора сохранения; добавились — цена не зависит от энергии, шаг сессии («дерись» или «стоп»), повторная остановка безопасна, лента держит пять боёв с номером из счётчика. Сервер и клиент собираются, тест зелёный.

Ранее

Карточка предмета: видно заточку, руны, камни и чары

  • Исправлена жалоба «зачаровал предмет, а чар не вижу». Карточка предмета (и в рюкзаке, и в

попапе героя — компонент один) получила блок «Улучшения»: заточка со своим вкладом в статы, руны, камни и чары — каждая строка с именем и посчитанным эффектом, плюс строка «итого».

  • Чары раньше не показывались вовсе: сервер отдавал только их количество. Теперь он отдаёт имя,

семейство, градацию и эффект — с учётом класса владельца и слота, как у рун (charmViewsOf). Эффект считается тем же кодом, что и в бою, поэтому подпись не может разойтись с реальностью.

  • Заточка перестала быть невидимой: раньше её процент был просто подмешан в общие статы вещи.

Теперь отдельной строкой видно, сколько она дала: «+7% к статам вещи · атака +3, броня +2».

  • Камни были одной серой строкой — стали строками с именем стата, значением и гранью; у

проклятого камня рядом видно и его минус к здоровью.

  • Тесты: новый раздел 19 в test/charms.js (имя и эффект чары, резонанс семейств, мастерская

чара, проклятие, пустая вещь и мусор в поле). Весь набор сервера — 53 файла — зелёный.

Ранее

Рынок: свои предметы стопками, продажа в попапе

  • Свои предметы на рынке лежат стопками, как в рюкзаке. Владелец: «Когда я захожу на рынок, я

должен видеть свои предметы стаками, а не каждый отдельно, как в рюкзаке сделай. Нажав на предмет, в Pop Up окне можно выставить этот предмет на продажу». Раздел «Выставить лот» рисовал плитку на каждый предмет: сто зелий давали сто плиток с именами, нужную вещь приходилось листать, а продажа всё равно уходила пачкой — то есть окно показывало не то, что уезжало в лот.

  • Клетка — стопка, счётчик в углу. Сетка та же, что в рюкзаке (inv-grid--bag, inv-slot):

иконка, метка заточки +N, грейд и счётчик ×N. Подписи-группы по слотам ушли — на каждую стопку они занимали больше места, чем сама клетка; порядок слотов остался (снаряжение первым, ресурсы в конце — SELL_ORDER в CapitalTab.tsx), поэтому зелья по-прежнему не мешаются с оружием.

  • Нажатие на клетку открывает попап продажи (SellPopup): карточка предмета со статами, строка

«В стопке N — на рынок уйдёт вся стопка, одной ценой», поле цены с монетой, «Продавцу после комиссии» и кнопка «Выставить». Попап стоит вне карточек: у .card есть backdrop-filter, а он делает потомка содержащим блоком — внутри карточки position: fixed считался бы от неё, и окно уехало бы вместе с ней.

  • Личную и квестовую вещь попап продать не даёт и говорит об этом словами (noSell, квесты):

кнопка гаснет заранее, игрок не ловит отказ сервера на пустом месте.

  • Правило стопки — одно на игру: новый game/client/src/itemStacks.ts (stackKey, toStacks)

общий для рюкзака и рынка; серверное поле stack остаётся главным путём, разбор на клиенте — запасным. У ItemPicker убрана раскладка плиток по слотам (groups) вместе со своим CSS: её просил рынок, а рынок на плитки больше не опирается. Сервер не тронут — putLot уже увозил в лот всю стопку, теперь это видно игроку.

  • Проверка: deploy/check-rynok.cjs — провалов 0. Стенд даёт 23 предмета в 10 клетках (стопки ×3

и ×12); проверяются счётчики, порядок слотов, попап поверх экрана, уход на сервер s-potion#0 за 1000, отказ на личной вещи и закрытие по фону. Скрипт считает пути от себя (__dirname/..), а не от C:\Игра: копию перенесли на E:, и жёсткий путь проверял бы старую копию, оставаясь зелёным. Снимки — models/рынок-выставить-лот-360.png, models/рынок-попап-продажи-360.png.

Ранее

Крестик закрытия рюкзака и кеш клиента

  • Крестик закрытия рюкзака снова видно — и он снова нажимается. Владелец: «нету кнопки закрыть

рюкзак». Кнопка была в разметке (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_LIMIT 200 → 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 уровне, открываются по очереди, живой бой тем же

движком. У каждого босса три фазы и своя слабость: запись кодекса о боссе (cx_person_boss_crypt/grove/abyss) снимает фазу щита и откусывает у босса 10–15% здоровья, подсказка видна в карточке рейда строкой «🔎 Слабость». Добыча: руны 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 по-прежнему складывает статистику и плитки в две колонки.

  • Значок клана: лидер ставит своему клану картинку ровно 512×512. Карандаш на гербе открывает

редактор: игрок выбирает файл, браузер сам обрезает его по центру и уменьшает до 512×512 (canvas), а сервер ещё раз проверяет тип, вес (до 640 КБ) и размеры по заголовку файла — другой размер отклоняется текстом «Нужен размер 512×512 (у файла W×H)». Файл лежит в clan-emblems/ рядом с сервером (в репозиторий не попадает) и отдаётся маршрутом GET /api/clan-emblem/<файл>?v=<время> (кеш навсегда, выход из каталога запрещён). Значок видно в гербе, в списке кланов, на подиуме и в списке рейтинга; у кланов без значка — общая иконка, а снять значок можно там же. Установка и снятие попадают в «Историю казны». Проверок: game/server/test/clan-emblem.js — 32, живьём — маршрут (200/404/400) и редактор в стенде.

  • Слабость босса и «Игра N дн.» У общего босса клана появилась слабость к ролям, как

строка «Боится магов и хилеров» в референсе: Боится стали (+15% воинам, берсеркам, разбойникам, паладинам), Боится чар (+18% магам и заклинателям), Боится веры (+25% жрецам, знахарям, друидам), Боится стрел (+35% лучникам). Слабость одна на весь клан и меняется с каждым новым боссом — поэтому в ростер зовут разные роли, а своя слабость подсвечена золотом. Надбавка складывается с баффом «Гнев на босса» и фазами выхода. Заодно в составе рядом с «в клане N дн.» появилось «игра N дн.» (от даты создания профиля; у героев старой базы строки нет, чтобы не показывать выдуманный стаж).

  • Проверка стенда стала устойчивее к параллельным прогонам: SITE, CDP_PORT и

CHROME_PROFILE читаются из окружения, а ожидание отрисовки стенда падает с понятным текстом, если dev-сервер лёг (раньше проверка висела минутами на пустой странице).

  • Главная. Герб клана в рамке, имя и сторона, четыре статистики — `Уровень · Репутация ·

Участники · Бонус клана ×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_hide 3 на 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 — 3 rune_plate, C → D — 5 rune_crystal, D → S — 2 ancestor_seal. Панель «Печать рун» показывает цену и не даёт нажать без материала, а сервер отвечает «Нужно: …».

  • Инвариант вместо памяти: тест «нет материалов, которые не падают и не тратятся» (раздел 12

professions-rules.js) валит сборку, если в игре появится мёртвый материал. Правил профессий теперь 220 проверок, весь набор сервера — 32 файла — зелёный.

  • Возврат золота за отменённые профессии не нужен: запрос по боевой базе дал 0 задетых героев

из 95. Решения закрыты в РЕШЕНИЯ-ВЛАДЕЛЬЦА.md, устаревший раздел 9 приложения к ТЗ переписан.

Ранее

Клан: зачем вступать и зачем быть активным

  • Проблема. Рядовой член клана не получал лично ничего: награда за

кланового босса уходила в казну, уровень клана давал только места в составе, а бой с героем врага не приносил ни золота, ни опыта. Вступать было незачем, быть активным — тем более. Спецификация — ТЗ_Клан_Мотивация.md.

  • Знамя клана. +1% золота, опыта и добычи за уровень клана (до +10%)

каждому, у кого есть вклад за неделю; спящим (неделя без входа) не начисляется. Складывается с контролем зоны, общий потолок +35% (rewardMul в index.ts, числа — в game/clan.ts).

  • Личный вклад. Начисляется за взносы (кап 400 в неделю), удары по боссу,

подземелья, рейды, арену, работы по профессии, победы и поражения в PvP (кап

  1. и за задания клана. Дробный вклад копится: десять убийств дают один вклад.
  • Клановая лавка. За вклад — зелья (ранга по тиру героя), свитки заточки,

ключи, сундуки и косметика «Знамя клана»; недельные лимиты и требования по уровню клана и званию. Каждый покупает себе: казна на лавку не тратится.

  • Задания клана. Три ежедневных и три недельных с общим прогрессом. Награда

идёт в казну (золото, 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 — чисто.

настоящем 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).

«Печать рун» — скрещивание двух одинаковых рун и вставка в гнёзда, доступна любому; «Рунное дело» — верхняя градация 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 видна в статах вещи.

expedition.ts): крошка и пластина — побочно у Горного дела (15% и 5% при добыче руды и камня) и с мобов 40+, кристалл и печать — с боссов, элиты и из Сундука Древних (печать 0,06). Падают только знающему профессию, как и прочие материалы.

скрещивание в S по семействам, разбор рун из сумки, список слов силы и покупка двух рецептов-услуг («Разбор руны» — ранг 2, «Скрещивание в S» — ранг 6). Числа приходят каталогом и состоянием с сервера, второй копии баланса на клиенте нет.

  • Слова силы записаны и в формулировке ТЗ, и в том, чем выражены в игре. Буквально работают

«Пожирание» (вампиризм) и «Обновление» (лечение); «Неукротимость», «Неподвижность» и «Возгорание» дают статы по смыслу семейства — механик поджога, урона при низком HP и среза оглушения в бою нет (то же и с расовым stunResist таурена, который никем не читается).

  • Проверки: professions-rules.js вырос до 188 проверок — раздел 10 держит всю механику рун,

раздел 11 — источники материалов. Весь набор сервера (31 файл) зелёный; заодно снята ловушка пустых профессий: Рунное дело больше не «Скоро», а Кулинария и Кожевенное дело — по-прежнему.

Ранее

Профессии: экономика добычи, один гейт прогресса, узлы и шкуры

  • Внедрено. Правки поверх ТЗ 03–15 записаны отдельно —

24_ТЗ_Профессии_Правки.md; он верен там, где расходится с карточками профессий.

бюджетом каждой семьи (обычный 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, кнопка «Снять шкуру» в итоге боя и режимы работы в попапе крафта.

(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']; завеса ::after 0.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 есть и в истории, и в живом сообщении, а лежит он внутри JSONB data — миграция не нужна: старые сообщения получают свой цвет сразу, и лента остаётся верной, даже если однажды в один список попадут разные каналы. Сервер, типы и база не тронуты.
  • Вкладки встали в один ряд. Правило .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) — заменена на «Эйрон».