- name
- xgaida-x-nixi-gamefeel-vfx
- description
- Джус и визуальный фидбэк Guildmaster: политика значимости (что достойно slowmo и тряски), per-hit фидбэк (hitstop, вспышка, сплющивание, боевые цифры), VFX, death-shatter, screen shake за IScreenShake, feel-конфиги и шов префаб-VFX (SO→префаб→пул→сокет). Зови на любую задачу про сочность, VFX, тряску, замедление, партиклы и всё под Presentation-визуалом. НЕ применять к: боевому времени и его контракту (combat-sim — здесь только потребитель), звуку (audio), определениям VfxData как контента (data-authoring).
# Gamefeel & VFX — рабочий контур Guildmaster
Этот скилл — процедура, а не справка. Он превращает разрозненные правила «сочности»
в чеклист, который прогоняется на КАЖДОЙ фидбэк-задаче. Цель — чтобы джус и VFX
оставались развязанными от боевого ядра (смотрят на sim, не мутируют его),
data-driven (значения и формы — в SO, эффекты — в префабах), а новый фидбэк ложился
в готовые швы, а не рядом с ними.
**Роль на этом слое:** я держу КОНТРАКТ фидбэка (кто на что подписан, где живёт
global-feel, как эффект попадает на экран, какие швы). Визуальную приёмку и полиш —
как именно выглядит бёрст, кривая шейка, тайминг «на глаз», сам арт — делает Макс.
Я кручу визуал-параметры только по его ТЗ и показываю результат.
## Дизайн-документация джуса — тоже мой контур (решено 2026-07-31)
**Кластер `docs/wiki/gdd/70-gamefeel/` принадлежит этому скиллу, а не `gdd-scribe`.** Писарь ГДД
владеет чистым геймдизайном — механиками, балансом, контентом; в джусе дизайн и реализация
неотделимы (тайминг эффекта — одновременно дизайн-решение и число в конфиге), и разводить их по
двум скиллам значило бы развести один факт по двум головам. Прецедент — `narrative` с `60-narrative`.
| Файл кластера | Что держит |
|---|---|
| `vfx-language.md` | **как выглядит событие**: роли-слои, тайминги, масштаб в долях роста юнита, словарь боевых событий |
| `vfx-color.md` | чем красим: главный цвет и палитра разброса, роли оттенков ростера |
| `asset-palette.md` | чем рисуем: связка «роль → текстура». **Не инвентарь ассетов** — тот живёт в `tech/10-reference/asset-inventory` |
| `backlog-gamefeel.md`, `backlog-vfx-particles-shaders.md` | банки идей: тактильный слой и визуальный |
Правила ведения те же, что у остальной ГДД: принятое решение — записью в
`gdd/00-meta/journal-adr.md` с датой и причиной (append-only, старое помечается `superseded`);
своё предложение — только как `proposed`; после правки прогнать оба гейта —
`check-wiki-frontmatter.ps1` и `check-wiki-links.ps1`.
**Эффект, показанный в бою, обязан иметь строку в словаре событий** (`vfx-language.md`). Не имеет —
значит его форму никто не выбирал, и следующий заход соберёт её заново по-своему.
## Модель боевого удара — принята 31.07.2026, читать ПЕРЕД любым разговором о формах
Канон целиком — `docs/wiki/gdd/70-gamefeel/vfx-language.md`; здесь только то, без чего разговор
начнётся с нуля. **Терминология обязательна к использованию** (заведена по жалобе Макса «терминов нет
норм, путаюсь»).
**Термины:** **форма** (серп / веретено / звезда) · **ядро** (радиальная вспышка, параметр — радиус) ·
**искры** (частицы прочь) · **порез** (красная метка на теле, накапливается) · **остаток** (пыль,
след) · **дуга за клинком** (VFX-сектор от плеча на взмахе) · **A** (начало взмаха, маркер
`StrikeStart` как геометрия) · **B** (точка попадания, маркер `Hit`). Слово «разрез» изъято.
**Две стадии, и они в разное время — поэтому не конфликтуют:**
| Стадия | Что | Когда |
|---|---|---|
| 1 · взмах | **дуга за клинком** — сектор от плеча, заметающий пройденный угол | strike-фаза, до хита |
| 2 · хит | **серп** A→B, **ядро**, **искры**, **порез** | от кадра `Hit` |
**Семь правил, которые ломаются легче всего:**
1. **Форма строится по ДВУМ ТОЧКАМ (A→B), а не по траектории клипа.** Точка хита — **середина**
формы у серпа и веретена (клинок проходит навылет), **конец** у звезды-дробящего.
2. **Нет контакта — нет серпа.** На промахе: дуга есть, серпа/ядра/искр/пореза нет, только «evade».
Дуга рисуется на каждом взмахе — она говорит «клинок прошёл здесь», и это правда даже при промахе.
3. **Три архетипа — это ПРАВИЛА ГЕНЕРАЦИИ, а не три готовых знака.** Процедурно гуляют прогиб,
толщина в коридоре, неровность, лучи звезды. **Размер не гуляет** — он несёт урон.
4. **Форма говорит КАК доставили, цвет говорит ЧЕМ ударили.** Отдельных форм под элементы нет и не
будет: элемент несёт палитра юнита. Дальний бой — линия-всполох, тот же колющий с нулевым прогибом,
после хита, по вектору снаряда.
5. **Всё рисуем сами шейдером на одном quad (SDF).** Спрайтовые листы эффектов — не наш путь: форма
параметрическая, потому что настраивается данными, а не перерисовывается. Пиксель-арт отменён
(`2026-08-01/14`), юнит теперь плоский сторибук — эффект и юнит в одной гладкости, а различает
их яркость.
6. **Порез — это СОСТОЯНИЕ тела** (`BodyVisualState` через `IUnitBodyVisual`), не событие. Лимит 12,
позиции из `IRngService`, хил гасит **самые старые** пропорционально их запасу HP.
7. **Количество и размер — пропорция урону с ПОТОЛКОМ.** Категорий «лёгкий/тяжёлый» нет. Искры:
`6 + доля_HP × 120`, не больше 48. Форма замирает вместе с hitstop.
**Живой стенд — `docs/gamefeel-demos/index.html`**, растущий одностраничник: показывает всё это в
движении на настоящем тайминге 30 Гц. Новая тема — новый раздел; после вердикта в разделе обязан
появиться стенд «принято». Правила ведения — `docs/gamefeel-demos/README.md`.
## Прежде всего: карта фидбэк-слоя
Слой уже построен и живёт в презентации. Ничего не изобретай — читай, продолжай,
встраивайся в существующие швы.
| Что | Где |
|---|---|
| Режиссёр значимости (global-feel: kill-slowmo, heavy-shake, финишер) | `Assets/_Project/Scripts/Game/Services/CombatFeelDirector.cs` |
| Мост sim→presentation + per-hit фидбэк (hitstop, цифры, спавн VFX) | `Assets/_Project/Scripts/Presentation/CombatPresenter.cs` |
| Тряска камеры за интерфейсом | `Assets/_Project/Scripts/Presentation/Camera/IScreenShake.cs`, `ScreenShake.cs` (Cinemachine-extension). Регистрация — `WorldLifetimeScope`; заглушки нет и не заводим |
| Пул + спавн VFX-префабов (ключ пула — `EntityId`) | `Assets/_Project/Scripts/Presentation/CombatVfx.cs`, `PooledVfx.cs` |
| Определение VFX (префаб, sorting, дефолт-направление) | `Assets/_Project/Scripts/Data/Definitions/VfxData.cs` — владеет `data-authoring` |
| Разлёт спрайта на осколки при смерти | `Assets/_Project/Scripts/Presentation/DeathShatter.cs`, `ShatterMesh.cs` |
| Всплывающие боевые цифры (урон/хил/evade) | `Assets/_Project/Scripts/Presentation/FloatingText.cs` |
| Вспышка арены/фон | `Assets/_Project/Scripts/Presentation/CombatAreaFlash.cs` |
| Вид юнита: вспышка/сплющивание/hitstop/death-хуки, точки-сокеты | `Assets/_Project/Scripts/Presentation/UnitView.cs`, `UnitAnimation.cs` |
| Единый feel-конфиг (impact-параметры, тумблеры `Enable*`) | `Assets/_Project/Scripts/Presentation/Design/CombatFeelConfig.cs` |
| Палитра боевого UI | `Assets/_Project/Scripts/Presentation/Design/CombatColorPalette.cs` |
| Боевое время (ПОТРЕБЛЯЕМ, не владеем) — slowmo/пауза/скорость | `Assets/_Project/Scripts/Game/Services/TimeScaleService.cs` → скилл `combat-sim` |
**Слой (asmdef):** всё это — `Guildmaster.Presentation` (визуал-компоненты) и
`Guildmaster.Game` (сервисы-режиссёры, сшивка через VContainer + MessagePipe).
`Presentation` ссылается на `Combat` и СМОТРИТ на него; `Combat` на презентацию не
смотрит НИКОГДА. `CombatFeelDirector` живёт в `Game`, потому что ему нужны и
`CombatSimulation` (события), и `TimeScaleService`/`IScreenShake` — их `Presentation`
не тянет (иначе цикл asmdef).
## Четыре правила, нарушение которых = переделка (HARD)
Каждое закрывает конкретный способ, которым фидбэк незаметно загнивает. Пойми
«почему» — тогда не придётся заучивать «нельзя».
1. **Все VFX — префабы. И точка.** Конечная форма любого боевого VFX (партиклы /
VFX Graph / ShaderGraph-материал) — самодостаточный префаб, лежащий ссылкой в SO,
который презентер спавнит в мировой точке-сокете через `ObjectPool` по боевому
событию. Эталон уже в проекте: `UnitData.ViewPrefab` — «визуал/анимация/размер
настроены ПРЯМО в нём, префаб самодостаточен» (`CombatPresenter.HandleUnitSpawned`).
Так же спавнятся `_bulletPrefab`, `_floatingTextPrefab`.
*Почему:* префаб — единственная форма, где художник собирает эффект без кода, а код
про эффект знает ровно одно: «заспавнить в точке». Кодовый меш плодит визуал в C#,
который нельзя приёмить глазами и нельзя отдать художнику.
*Где код остаётся законно:* `DeathShatter`/`ShatterMesh` (меш режется из спрайта конкретного
юнита — префабом не подменить) и dev-слой `CombatStatusOverlay`. `PixelBurst*` удалён.
Новый эффект кодовым мешем не делаем даже «на время».
*Шов* (`VfxData` SO → префаб → `CombatVfx`-пул → сокет) **построен** — им и пользуемся,
см. раздел ниже и `references/vfx-and-pooling.md`.
2. **Global-feel только в `CombatFeelDirector`; per-hit — в презентере.** Глобальные
эффекты значимости (kill-slowmo, heavy-shake, финишер-таймлайн, тряска) решает ОДНО
место — `CombatFeelDirector`, по политике «что достойно момента». Точечный фидбэк
пары «источник+цель» (hitstop, вспышка, сплющивание, искры, цифры) — в
`CombatPresenter`. Не размазывай глобальный feel по `UnitView` и не дёргай глобальную
тряску/время из точечного фидбэка.
*Почему:* значимость — это политика («килл щёлкает, царапина нет»). Размажешь по
вьюхам — потеряешь единую точку, где её крутят, и получишь slowmo на каждый тик DoT.
3. **Значения и формы — из feel-SO, не хардкод.** Тайминги, факторы, кривые —
в `CombatFeelConfig` (+ `VfxData` для префабных эффектов). Потребители тянут
значения ОТТУДА (`UnitView`, `CombatPresenter`, `ScreenShake`, `CombatFeelDirector`),
а не из чисел в коде. Крутить фидбэк = править SO.
*Почему:* джус настраивается итерациями «на глаз» — это работа Макса в инспекторе.
Число в коде = правка кода на каждый чих баланса фидбэка и недоступно дизайнеру.
**ЦВЕТ — исключение: он живёт НЕ в feel-SO** (с 30.07.2026). Ни `CombatFeelConfig`, ни
`CombatColorPalette` не хранят `Color` — они называют роли `--gm-color-combat-*`, значения лежат в
`UI/Theme/tokens.*.uss` → снимок `GuildmasterPalette` (пересобрать: Alebardium → Дизайн-система).
HDR туда не едет: в палитре LDR-база, силу свечения задают числа-множители в конфиге (осколки ×2.75,
вспышка смерти ×1.8 — один оттенок пересвета, два события). Новый цвет фидбэка = ступень в примитивах
+ роль в семантике + пересборка снимка + имя роли в `PaletteSnapshotTests`. Заводить `Color`-поле в
конфиге — прямой откат: семь таких полей уже дублировали токены и совпадали только потому, что их
никто не правил.
4. **Presentation читает sim только через события/MessagePipe и не влияет на
детерминизм.** Фидбэк СМОТРИТ на бой: подписка на C#-события `CombatSimulation`
(презентер) или на MessagePipe (`CombatFeelDirector`, как `AudioPresenter`). Никогда
не пишет в `RuntimeUnit`/sim и ничего не решает за бой. Всё визуальное живёт на
`Time.deltaTime`/твинах/unscaled, не на sim-тике.
*Почему:* как только фидбэк мутирует мир или влияет на порядок тика — рушится и
детерминизм (кооп-checksum), и headless-тестируемость боя. Это инвариант `combat-sim`,
здесь он в силе как граница.
**Плюс два инварианта реестра (30.07, заказ Макса «чтобы ни о ком не забыть»):**
5. **У КАЖДОГО эффекта есть выключатель, и он один по форме.** Тумблер `Enable*` в
`CombatFeelConfig`; длительность настраивает, а не выключает. До правки джус выключался
четырьмя разными способами, а пять эффектов (вспышка удара, сплющивание, вспышка телеграфа,
поза гвардии, разлёт на осколки) не выключались вовсе — «список тумблеров» отвечал на вопрос
«всё ли включено» неправдой.
6. **Перепись живёт в тесте, а не в SO.** `FeelToggleCoverageTests` держит таблицу
«вход → чем выключается» и роняет сборку, если появился новый `Play*`/`Show*`/`Raise*` без
записи. Отдельный SO-список стал бы вторым владельцем факта и разошёлся бы с кодом на первом
же эффекте; перепись в тесте расходиться не умеет — она падает.
*Самая ценная проверка там — текстовая:* тумблер обязан ЧИТАТЬСЯ в коде презентации. Рефлексия
этого не видит (свойство существует и возвращает значение, даже если его никто не спрашивает),
а именно так выглядит «завели ручку и забыли подключить».
*Законное исключение:* тумблер, применяемый ВНУТРИ конфига (цвет вспышки по школе) — он наружу
не торчит и помечен в переписи отдельным видом.
## Проверка «дружит ли с новой презентацией юнита» (30.07)
Скелетный юнит — не один спрайт, поэтому вопрос «работает ли фидбэк» разбивается на четыре, и все
проверяются без play-mode:
- **Материальное — только через шов** `IUnitBodyVisual` (тинт, вспышка, голограмма, контур, сортировка,
силуэт, осколки). Признак нарушения — обращение к `SpriteRenderer` в презентации вне `Body/`.
- **Материал вспышки обязан стоять на КАЖДОЙ части** (`_FlashAmount`): часть без него физически не умеет
вспыхивать. Считать: 16 из 16 у костяного.
- **Позиционное — по сокетам** (`FeetPoint`/`HeadPoint`/`ShotPoint`/`HitPoint`). Не разведён сокет —
фолбэк на центр юнита, и искры бьют в живот вместо груди. Проверять на префабе, а не на глаз.
- **Разлёт — по частям, сиблингом самой части**: осколки руки стартуют в пространстве руки, со счётчиком
на один финальный колбэк, а не шестнадцать.
## Свечение оружия и цвета юнита — карта швов перед заходом (30.07)
Следующий крупный заказ. Перед первой правкой знать вот это, чтобы не строить второй канал рядом с готовым:
- **Материальное состояние тела подаётся ОДНИМ куском** (`BodyVisualState` → `IUnitBodyVisual.Apply`) раз в
кадр, имена шейдерных параметров держит `BodyShaderIds`. Новый визуальный признак — поле туда, а не
отдельный писатель в `UnitView`.
- **Оружие и щит — отдельные части скелета** со своими рендерерами и материалом вспышки. Светить клинок
физически можно уже сейчас.
- **НО шов адресует ТЕЛО ЦЕЛИКОМ:** `Apply` кладёт одно состояние на все части. Это первое, что придётся
расширять, и делать это надстройкой поверх общего состояния (роль части: `Weapon`/`Shield`/`Body`), а не
вторым независимым каналом: меч обязан вспыхивать вместе со всеми при ударе И светиться сам. Выносить
оружие из тела в отдельный держатель — хуже: потеряются внутренняя сортировка группы и общий шаттер.
- **Линия дуги для трейла уже считается офлайн:** `RigSweep` отдаёт путь предмета с разбивкой на
windup / strike / recovery и площадь по спрайту. Слеш-трейл живёт на strike-фазе, её границы известны.
- **Цвет берётся с юнита, не из префаба:** `UnitData.VfxColor` (там, где цвет ровно один) и `VfxPalette`
(диапазон разброса частиц), плюс `ResolveVfxPalette()`. Хвост трейла — тот же свет, теряющий
прозрачность, а НЕ переход во второй цвет.
- **Bloom только на VFX, порог ≥ 1.0.** Свечение арта — отдельный заход; цвет ровно в 1.0 порог не пробивает.
## Дуга за клинком — где искать, если она «идёт не так» (07.08.2026)
Самый дорогой эффект по числу заходов: чинился четырежды, и три раза корень лежал НЕ в нём. Порядок
проверки — от эффекта вниз, потому что дуга — последний потребитель длинной цепочки.
| Симптом | Где смотреть на самом деле |
|---|---|
| след лежит мимо клинка | геометрия предмета: `UnitHeldItem` — длина объявлена, направление берётся с меша рабочей части. Мерить угол «хват → остриё» против угла нарисованного клинка |
| след не следует за мечом по ходу взмаха | клип крутит узел `Weapon_*` отдельно от кисти — запрещено, держит `WeaponFollowsTheHandTests` |
| след исчезает мгновенно | `SwingArcTrailSeconds` (длина хвоста ВО ВРЕМЕНИ) и `SwingArcFadeOut`. В стенде — ещё и фиктивное время: делегат фазы зовётся на каждой перерисовке, нулевой шаг не должен двигать часы |
| след «висит» дальше меча к концу удара | радиус сектора обязан СЛЕДОВАТЬ за рукой, а не помнить максимальный размах |
Владельцы: `SwingArcGeometry` (плечо и остриё), `SwingArcLaunch` (все числа запуска),
`SwingArcVfx.Tick` (шаг жизни). Все трое зовутся и боем, и стендом — считать что-либо из этого своими
руками запрещено, держит `SwingArcSingleOwnerTests`.
**Урок, который стоит помнить дольше самой дуги.** Симптом читался как дефект эффекта и трижды в нём
же чинился. Все правки были нужны, но корень лежал двумя слоями ниже — в риге, — и нашёлся не
рассуждением, а ЗАМЕРОМ: угол вектора «хват → остриё» против угла нарисованного клинка, два числа.
Когда эффект «почти правильный», спрашивать надо не его, а то, из чего он считается.
## Граница со смежными скиллами (режем чётко)
Фидбэк стоит на стыке трёх скиллов. На каждом стыке — взаимная ссылка «см. другой
скилл», а не спор за файл.
- **combat-sim** владеет **боевым временем**: `TimeScaleService` (единственный писатель
`Time.timeScale`, композит `GameSpeed × Cinematic`, пауза, шов под геймплейный
хрономант) и шов sim→MessagePipe. `CombatFeelDirector` (мой) — **потребитель**: дёргает
`TimeScaleService.CinematicPulse`/`PlayCinematicSequence` и `IScreenShake.Shake`. Тип
`CinematicSegment` живёт на шве в `TimeScaleService` (им владеет combat-sim), FeelDirector
его конструирует. Инвариант «`timeScale` пишет только `TimeScaleService`» — за combat-sim.
- **audio** владеет звуком: `IAudioService`, FMOD, стингеры, микс. Пересечение —
`TimeScaleService` толкает slowmo-питч через `IAudioService.SetGlobalParameter`
(`AudioParameters.TimeScale`): вызов геймплейный (combat-sim), параметр — audio. Джус
и звук на одно событие подписываются НЕЗАВИСИМО (оба слушают MessagePipe/sim), не через
друг друга.
- **data-authoring** владеет **ОПРЕДЕЛЕНИЕМ** контента-SO. Целевой `VfxData` (id, ссылка
на VFX-префаб, параметры, loc — если нужен) — как `EffectData`: определение за
data-authoring. **Спавн-механика** (пул, точки-сокеты, презентер, привязка к событию) —
моя. Ровно как эффект: определение — data-authoring, поведение/спавн — здесь.
## Шов префаб-VFX ПОСТРОЕН
Зеркало `UnitData.ViewPrefab`, и он уже работает:
1. **`VfxData`** SO (`Data/Definitions/`, владеет `data-authoring`): `id` (`domain.name`), ссылка на
префаб, sorting layer/order, `DefaultDirDeg`.
2. **`CombatVfx`** + `PooledVfx` (здесь): пул на префаб, `Get` → позиционировать в мировую
точку-сокет → `Play`, авто-`Release` по завершению. **Ключ пула — `EntityId`, не `int`.**
Относительный order детей приходит из самого префаба, а не из кода.
3. Презентер по боевому событию резолвит `VfxData` и просит проиграть в точке
(`ShotPoint`/`HitPoint`/`FeetPoint` на `UnitView`); `dirDeg = null` → `VfxData.DefaultDirDeg`.
`PixelBurst*` мигрирован и удалён. Кодом законно остаются `DeathShatter`/`ShatterMesh` (меш режется
из спрайта конкретного юнита) и dev-слой `CombatStatusOverlay`. Детали и антипаттерны —
`references/vfx-and-pooling.md`.
## Как я авторю фидбэк-код — ГИБРИД (файл + проверка через MCP)
1. **Пишу C#-файлы напрямую** (`Write`/`Edit`) — контролирую код и его слой (Presentation
Ver en GitHub