Единственный владелец правил написания кода. Как называть, что документировать, чего никогда не делать и почему.
CLAUDE.mdи.cursor/rules/project-context.mdcссылаются сюда и не повторяют содержимое: правило, живущее в двух местах, расходится в третьем.Связано: карта сборок (сами
.asmdef), Journal - Library Picks And The Alternatives We Turned Down, Reference - Editor Tools.
1. Инварианты — чего не делать никогда
Короткий список. Всё остальное в этом документе — объяснение, почему он такой.
GetComponent,Find,FindObjectOfTypeвUpdate()/FixedUpdate().Instantiate/Destroyв циклах и горячих путях — толькоObjectPool<T>.- Аллокации в горячих путях: LINQ, конкатенация строк.
- Хардкод игровых значений в C# — данные живут в
ScriptableObjectили конфиге. UnityEngine.Randomв игровой логике — толькоIRngService(seeded).Physics2D/Rigidbody2D/Time.deltaTimeв боевой симуляции — ломает детерминизм.- Синглтоны (
static instance) вместо DI. - Корутины для нового time-based кода — только
UniTask. - Legacy
UnityEngine.Input— только Input System заIInputService. [MenuItem]вне корняAlebardium/.- Дефолтный аргумент конструктора (
IFoo foo = null) у типа, который приходит из DI. - Тихий фолбэк на наш собственный конфиг, ассет сцены или DI-регистрацию (см. §5).
- Показ-рандом, утекающий в симуляцию, и свой шум там, где есть общий (см. §8).
- Свободное число там, где величина обязана быть ступенью (см. §9).
2. Именование и структура
- PascalCase — публичные члены, типы.
_camelCase— приватные поля. MonoBehaviour— логика GameObject,ScriptableObject— данные.- Ссылки на компоненты кэшируются в
Awake(). - Код пишется так, чтобы читаться как соседний: плотность комментариев, именование и идиомы берутся из окружающего файла, а не из общих привычек.
Карта сборок — карта сборок (сами .asmdef). Проверять перед созданием
нового скрипта: в какую сборку он попадёт. Новый модуль — обновить и карту.
Игровой код и контент лежат под Assets/_Project/ (Scripts/, ScriptableObjects/, Prefabs/,
Scenes/, Art/, UI/, Audio/), тесты — Assets/_Project/Tests/{EditMode,PlayMode}. Всё
остальное под Assets/ — вендор.
3. Документирование
- Публичные методы и свойства во всех сборках — XML Doc (
///). - Поля ScriptableObject —
[Tooltip("...")], он виден в Inspector дизайнеру. - Приватные поля и внутренняя логика — комментарий только для неочевидного алгоритма.
- Очевидное не документируется:
// increment counter— шум. <remarks>на типе — место для «почему». Неочевидное решение, отвергнутая альтернатива, условие, при котором класс сломается.<summary>отвечает «что делает»,<remarks>— «почему так, а не иначе». Doxygen публикует оба.
Код — владелец правды о коде (CLAUDE.md, раздел «Где живёт правда»). Практически это значит:
- Факт про один файл документируется в этом файле. Ссылаться на вики за объяснением того, что делает метод, — дефект: справочник вики заморожен и отстаёт молча.
- Инвариант между файлами идёт в тест, а не в комментарий. Комментарий виден одной стороне шва,
вторая нарушит и не узнает; тест падает. Пример —
GameDataPathTestsна кодовых именахGameDataPath. - Развилка «выбрали одно из двух» после коммита уезжает записью в
00-meta/journal/— порог и шаблон вCLAUDE.md. В комментарий пишем только то, что нужно читателю этого файла.
/// <summary>Применяет урон с учётом брони и активных эффектов.</summary>
/// <param name="damage">Базовый урон до модификаторов.</param>
/// <param name="source">Источник урона — для триггеров мементо.</param>
/// <returns>Фактически нанесённый урон.</returns>
public int TakeDamage(int damage, IUnit source) { ... }
[Tooltip("Базовая скорость атаки: секунды между ударами.")]
[SerializeField] private float _attackInterval = 1.5f;Отдельно: контентный класс объясняет не только что делает, но и что значит каждое его число — иначе баланс правится наугад.
4. Архитектура и детерминизм
- DI вместо синглтонов. Зависимости — через VContainer; ни
static instance, ниFind*ObjectOfTypeв рантайме. - Рандом — только
IRngService. Забег воспроизводим по сиду, глобальныйRandomэто ломает. - Боевая симуляция детерминирована: фиксированный тик, без физики Unity, детерминированный
порядок итерации (не
foreachпоDictionary/HashSetбез сортировки), отделена от презентации. - Интерфейсы — на швах, а не для вида.
IFooоправдан при двух и более реализациях или как граница для теста, мока, подмены.IFoo→Fooодин-к-одному без этого не заводить. - Кооперативная отмена в узловых флоу. Ждёшь чужой
TaskCompletionSource— неawait tcs.Task, аawait tcs.Task.AttachExternalCancellation(ct)(UniTask): иначе токен отмены не доходит и ожидание висит вечно. При отмене бросаетOperationCanceledException— это штатный путь выхода из флоу, а не ошибка. Паттерн всех узловых флоу забега после реворка UI-архитектуры.
5. Фолбэки: три полосы
Граница проходит не по важности кода, а по тому, откуда приходит отказ.
Фолбэк допустим только там, где отказ приходит извне игры — диск, ОС, локаль игрока, банк FMOD, пользовательский файл. Всё, что внутри нашего авторства — конфиг, ссылка в сцене, DI-регистрация, id контента, ключ локализации — фолбэка не имеет: значение либо есть, либо это баг разводки, и он обязан быть громким и пойманным guard-тестом до запуска.
Оставшийся фолбэк обязан деградировать в явно неправильно, а не в правдоподобно: выключенный
джус, магента, #key, лог с причиной — да; похожее число, похожий цвет, «примерно такая» тряска — нет.
| Полоса | Когда | Что делать |
|---|---|---|
| Удалить | Подменяет авторское значение (второй владелец правды) | Снести; отсутствие ловит guard-тест |
| Громкий отказ | Прикрывает неразведённую ссылку сцены или DI | Debug.LogError с именем поля; ссылку — в SceneWiringTests |
| Честная деградация | Отказ пришёл извне игры | Оставить, но с логом и видимым следом |
Проверка перед тем, как написать фолбэк: сработает — узнаю ли я об этом, не открывая код?
Нет — значит он маскирует. Помощники — ScopeWiring, MenuRouter.CannotShow. Разбор, которым правило
добыто, лежал в docs/fallback-audit.md — журнал удалён 30.07.2026 как леса, само правило
самодостаточно.
6. Меню редактора
Весь наш редакторный тулинг — под одним корнем Alebardium/. Не Tools/, не
Tools/Guildmaster/, не свой корень: пункты уже расползались по четырём корням, и нужное искали
перебором. Новый [MenuItem] заводить сразу под Alebardium/<Группа>/…, приоритеты — сотнями между
группами.
Исключение: [CreateAssetMenu] остаётся под Guildmaster/… — это меню создания игрового контента,
а не инструменты студии. Раскладка и таблица приоритетов —
Reference - Editor Tools.
7. Тексты и локализация
Игровой текст закладывается через лок-ключи сразу, EN + RU, а не «потом»: ретрофит ключей по готовым
экранам стоит дороже, чем их заведение, и до него никогда не доходят руки. Ключи контента —
{id}.name / {id}.desc.
8. Процедурная генерация: считаем, собираем, рисуем
У нас нет художника по интерфейсу и нет художника по эффектам. Поэтому вопрос «рисовать или считать» встаёт в проекте постоянно, и отвечать на него каждый раз заново — значит отвечать по-разному. Граница проходит не по сложности картинки, а по тому, несёт ли она смысл.
| Полоса | Что сюда попадает | Чем делается |
|---|---|---|
| Считаем | Поверхности, фактуры, каймы, скосы, дуги, свет, сетки, разделители, формы удара, разлёт осколков, задник карты | Формула: шейдер, Painter2D, Shapes, запекание в редакторе |
| Собираем | Лицо Сосуда, силуэт, снаряжение — то, что комбинируется из авторских частей по сиду | Детерминированная сборка из нарисованных человеком кусков |
| Рисуем | Иконки, знаки, лого, портреты-персоналии, иллюстрации | Человек или PixelLab, никогда не формула |
Средняя полоса — не компромисс, а отдельный способ: части рисует человек, комбинацию выбирает сид. Правило из двух полос («считаем или рисуем») ломалось на первом же нашем случае — face-DTO Сосуда, — поэтому полос три.
Граница между полосами зависит от ЯЗЫКА СТИЛЯ, а не от типа объекта. Одна и та же поверхность попадает в разные полосы при разной стилистике, и это не вкусовщина, а вопрос того, кто справится лучше. В пиксель-арте фактуру делает ручная работа — осмысленные кластеры на площади 32×32, — и формула там соревнуется с художником, проигрывая: поверхность уезжает в «рисуем». В плоском сторибуке (наш язык с 2026-08-01/14, для арены — с
2026-08-02/44) фактуры нет вовсе, стиль состоит из формы, её контура и света от движка — три считаемые вещи, и поверхность честно возвращается в «считаем». Прежде чем относить что-то к первой полосе, ответь: в этом языке у формулы есть конкурент-человек? Есть — полоса не та.
Иконки лежат в «рисуем» как текущее положение дел, а не как вердикт навсегда. Развилка открыта и живёт в
gdd/00-meta/open-forks.md; до её закрытия иконки не генерируются.
Детерминизм — три ступени, а не две. Вопрос не «случайно или нет», а кто сверяет результат.
| Ступень | Кто сверяет | Откуда берётся |
|---|---|---|
| Сим-рандом | Симуляция: бой, забег, карта, награды, магазин | Только IRngService (§4). Детерминизм между машинами обязателен — расхождение в последнем бите разводит два клиента коопа |
| Показ по общему сиду | Два игрока глазами: картинка обязана быть одной | Сид приходит из IRngService, дальше обычная шейдерная математика. Разойтись в последнем бите не страшно — страшно взять другой сид |
| Свободный показ | Никто | Любой дешёвый хеш от координаты, sin в том числе |
Прецеденты в коде: неровность краёв в SH_Vfx_HitForm — вторая ступень (сид из симуляции, поэтому
шума по времени там нет и быть не может); разброс осколков в SH_Sprite_Shatter — третья
(результат не сверяет никто, и sin-хеш дешевле проброса числа из C#).
Средняя ступень — та, на которой легче всего ошибиться в обе стороны: тянуть её в IRngService
целиком избыточно, а считать свободной — значит показать двум игрокам разные удары в одном бою.
Проверка, куда попадает число: читает ли его симуляция? Да — сим-рандом по определению, даже если выглядит визуальной мелочью. Нет — тогда второй вопрос: увидят ли это двое и должны ли увидеть одно и то же? Обратное течение — показ-рандом, попавший в симуляцию, — инвариант §1 и дефект.
Общие правила для всего, что считается:
- Общая формула для шейдеров живёт в
Art/Shaders/Lib/Procedural.hlsl, а не копируется в файл-потребитель. Матрица Байера и порог дизеринга уже стояли дословным дублем в двух местах. Речь именно про шейдеры: сводить к одной функции ещё иXorShiftRngс шумом запекателя незачем — это три разные задачи (воспроизводимость, дешевизна, фактура), похожие только на вид. - Цвет — из
GuildmasterPaletteпо имени токена, и в запечённом ассете тоже. Иначе смена палитры перестаёт доезжать до сгенерированного и палитра теряет статус единственного владельца. - Запечённое воспроизводимо на своей машине: тот же сид и те же параметры дают тот же файл. Это нужно не ради межмашинной сходимости (текстуры печёт один человек, CI их не трогает, игрок получает готовый PNG), а ради истории: PNG жмётся целиком, поэтому любой сдвиг переписывает файл полностью и даёт диф там, где визуально ничего не изменилось.
- Размер и вес не гуляют случайно. Вариация допустима в том, что несёт характер (прогиб, зерно,
неровность края), и запрещена в том, что несёт информацию, — иначе случайность отнимает у игрока
канал считывания. Прецедент:
_SeedвSH_Vfx_HitFormгуляет по форме, но никогда по размеру.
9. Ступенчатые величины: лестница вместо россыпи чисел
HARD (принято 2026-08-06). Величина, которая читается как категория и сравнивается с соседями, объявляется ступенью из закрытого именованного списка. Число за ступенью живёт в одном конфиге, а персональное отклонение задаётся долей от ступени, а не своим числом.
Правило существует потому, что свободное число у такой величины не имеет ни словаря, ни владельца. Оба его отказа уже случались:
- Незаданное поле молча уезжает на дефолт, и никто не узнаёт. Кровомант — дальник со снарядом — дрался вплотную, потому что его дальность осталась единицей из конфига. Ступень отвечает на вопрос «кто он по дистанции» ДО того, как кто-то назначит число.
- Величина расползается в россыпь несопоставимых чисел. Яркость свечения жила семью множителями (1.8 / 2.0 / 2.2 / 2.5 / 2.6 / 2.75 / 3.2) в двух конфигах: ни одно не объявлено базой, ни одно не объясняет, чем оно отличается от соседнего, а весь их разброс уложился меньше чем в полтора стопа — то есть язык света оказался плоским, и самое значимое событие боя (вспышка смерти, ×1.8) светило тусклее обычного удара.
Из чего состоит ступенчатая величина
- Закрытый список с именами (
enum). Имя отвечает на вопрос «кто это по такой-то оси», а не «сколько тут». Номер ступени именем не является:+1не говорит ничего,Событиеговорит. - Ровно одно место, где ступень превращается в число — конфиг. Больше нигде число не живёт.
- Отклонение — долей или множителем от ступени, и намеренно мелкое. Поправка в абсолютных единицах ломает главное свойство лестницы: сдвинули ступень — поехали все её носители, а носитель с «+1» остался на прежнем месте.
Шаг лестницы берётся в единицах восприятия величины, а не в единицах хранения. Для дальности это
метр арены, для яркости свечения — стоп (вдвое), и мерить его надо по той части, которая реально
доходит до глаза: у bloom это превышение над порогом, поэтому стоп считается от яркость − порог, а
не от числа в поле. Проценты от числа в поле в таких величинах врут: при пороге 1.0 «+20% к
множителю» (×2.0 → ×2.4) даёт +40% свечения.
Форма контрола — доля его размера, а не число
HARD (принято 2026-08-22). Всё, что элемент рисует ВНУТРИ себя как часть силуэта — скол угла, конец пластины, скос чипа, — задаётся долей его высоты, и контрол пересчитывает форму сам при отрисовке. Числа в теме на каждый размер запрещены.
Слова Макса: «Мы не можем автоматизировать подсчет значение сколов и тп, просто скейлить как и кнопки? Чтобы не заниматься математикой каждый раз» и следом «Тоже самое и с другими элементами. Возьми как хард правило».
Отказ, из-за которого правило заведено: скол 10 и конец 9 стояли числами и попадали на кнопки любого роста. На эталонном пункте меню (высота 64) это 15.6% и 14%, на мелкой кнопке 43 — уже 23% и 21%: одна и та же деталь читалась разной, а каждая новая ступень размера требовала пересчёта руками.
- Доли живут в примитивах (
--gm-plate-chamfer-ratio,--gm-plate-cap-ratio,--gm-chip-slant-ratio,--gm-slant-ratio) — по одному числу на всю игру, снятому с эталона. - Ноль разрешён: это выключение детали, а не её размер (у видов со значком концов нет вовсе).
- Абсолютное значение остаётся дорогой для исключений — там, где деталь принадлежит не силуэту,
а рисунку. Пример: угловая накладка
PanelFrame— оковка предмета, а оковка не растёт вместе с доской; у неё постоянный размер, но объявленный ОДНИМ токеном темы, а не числом в каждой разметке. - Держит гейт
ControlSizeGateTests.Форму_контрола_задаёт_доля_его_размера.
Где ступень не нужна
Признак — честный ответ на вопрос «почему тут именно столько»:
| Ответ | Вердикт |
|---|---|
| «потому что это важнее обычного удара» | ступень; число за ним случайное |
| «так посчиталось» | свободное число; ступень только помешает |
Не ступень: результат каскада статов, урон после множителей, тайминг, замеренный из клипа, — всё, что вычисляется, а не назначается автором. Не ступень и то, что не с чем сравнивать: величина в единственном экземпляре словаря не образует. Если список ступеней перевалил за семь — это уже не словарь, а шкала, и её место в кривой или формуле.
Прецеденты: AttackRangeBand и CastRangeBand (шесть ступеней дальности, число — в StatsConfig,
поправка долевая), лестница яркости свечения в CombatFeelConfig.