Статус: Ready
Архитектура боевого юнита: композиция «Сосуд + Реликвия», стат-система и единая модель эффектов. Опирается на принципы из 5. Технологический стек и архитектура (детерминизм, SO-данные + поведение в коде, DI, дисциплина интерфейсов).
1. Модель идентичности
Аналогия из GDD — «игрок в LoL и его чемпион»:
| Сущность | Роль | Что несёт |
|---|---|---|
| Реликвия | «чемпион» — источник силы | Основной стат-блок, активки, пассивки, ресурс (тип/макс/старт/эффективность), категориальная конфигурация, теги |
| «Сосуд» | «пилот» | Перки — модификаторы поверх чемпиона (PerkModifiers). Сам статов не имеет. Супер-уник-слой «Судьба» — см. GDD: 5. Реликвии |
| Гильдмастер | «тренер на бровке» | Не юнит на поле. Кастует заклинания + даёт гильдие-широкие модификаторы. Задаёт стартовый набор забега |
Решения (залочено):
- Базовый стат-блок даёт Реликвия (поверх глобального шаблона-дефолта
StatsConfig). Сосуд без Реликвии небоеспособен. - Ровно 1 Реликвия на Сосуда в бою. Реликвии копятся на уровне гильдии; назначение — шаг подготовки к бою.
- Редкость ≠ множитель статов. Влияние на статы примерно равно у любой редкости; редкость = размер кита: Треш (минимум, 0–1 способность) → Обычная (≈1 пассивка + 1 активка) → Уникальная (расширенный кит именных героев). Прежняя градация Common/Rare/Epic отменена — см. GDD 5. Реликвии и GDD 12. Мета-прогрессия.
- Ресурс привязан к Реликвии (сменил Реликвию → сменился тип ресурса).
- Гильдмастер:
GuildmasterController(отдельный кастер со слотами, перезарядка по времени/по числу боёв) + гильдие-широкийIModifierSourceна всех юнитов.
2. Композиция RuntimeUnit (POCO, живёт один бой)
RuntimeUnit
├── Идентичность (ссылки на иммутабельные SO)
│ ├── VesselData — перки, внешность, имя
│ └── RelicData — «чемпион»: стат-блок, активки, пассивки, ресурс
├── Stats — вычисляемые статы (слои модификаторов, §3)
├── Abilities[] — AbilityRuntime: таймеры кулдауна/ресурса
├── ActiveEffects[] — RuntimeEffect: эффекты в бою (§5)
├── Runtime-состояние: CurrentHP, CurrentResource, CurrentShield, Position
├── EffectTagMask — битовая маска тегов активных эффектов (быстрый запрос для AI)
└── AI — скомпилированный AIProfile (Filter/Score/Override)
RuntimeUnit— обычный C#-класс, неScriptableObject.CreateInstance(мина прошлого проекта). SO — только иммутабельные определения, на них ссылаемся.
3. Стат-система
Слоистые модификаторы с источником — не плоский класс с 30 полями.
📋 Полный канонический список статов (ровно 30, EN+RU, операции, дефолты, клампы) — в отдельном доке 11. Стат-система. Ниже — только механика стат-системы; конкретные
StatTypeсм. там.
public enum ModifierOp { Flat, PercentAdd, PercentMult }
public readonly struct StatModifier
{
public readonly StatType Stat;
public readonly float Value;
public readonly ModifierOp Op;
public readonly object Source; // реликвия/перк/эффект — чтобы снять РАЗОМ
}
public sealed class Stats
{
public float Get(StatType stat); // кэш по dirty-флагу
public void AddModifiersFrom(object source, IReadOnlyList<StatModifier> mods);
public void RemoveModifiersFrom(object source); // снял источник → ушли его моды
}Формула (фиксированная — детерминизм + баланс):
final = (base + ΣFlat) × (1 + ΣPercentAdd) × Π(1 + PercentMult)
- «Эффективности» (урона/эффектов/перезарядки) — это
StatTypeсо стартом 1.0 иPercentMult. Никакого спец-кода в десяти местах. - Пересчёт по dirty-флагу, не каждый тик.
- Числовые vs категориальные: через модификаторы идут только числа.
DamageType(маг/физ/чист),AttackType(ближний/дальний/AOE/пробивающий),ResourceType— категориальная конфигурация наRelicData, не в стат-пайплайне. (ProjectilePierce— это числовой стат, см. док 11, чтобы реликвии/эффекты могли его менять.)
Сборка юнита («компиляция»)
base← глобальныйStatsConfig(SO).+модификаторы Реликвии (главный источник).+Перков →+Предметов →+Командных артефактов.- (опц.)
+гильдие-широкие модификаторы Гильдмастера. - Регистрируем пассивки и активки Реликвии (активки →
AbilityRuntime). - Инициализируем рантайм:
CurrentHP = Get(MaxHP),CurrentResource = StartResource. - Прикручиваем скомпилированный AI-профиль.
- Эффекты в бою добавляются/снимаются динамически — через тот же пайплайн (
Source = эффект).
4. Визуал
Решение (залочено): внешний вид — цельный на Реликвию (не модульный). «Сосуд» на поле не влияет на внешность; у него только портрет для UI (ростер/подготовка). Идея «узнаваемый Сосуд под разными реликвиями» из раннего черновика — отклонена.
Технология анимации — кадровая (sprite sheet). Конвейер презентации (живёт в Guildmaster.Presentation, только читает сим):
- Один
UnitView.prefabна всех юнитов:SpriteRenderer+Animator+ дочерние HP-бар (Shapes) + узел VFX/Feel. - Один базовый
AnimatorControllerсо стейт-машиной (Idle/Run/Attack/Cast/Hit/Death+ переходы) — описывается один раз. - На каждую Реликвию —
AnimatorOverrideController, подменяющий клипы (спрайт-шиты), не трогая стейт-машину. Новая Реликвия = SO + шит + override, без кода. - Визуальные ссылки — на
RelicDataчерез AddressablesAssetReference(спрайт-шит, override-контроллер, портрет). VFX способностей —AssetReferenceнаAbilityData/EffectData. - При спавне
CombatPresenterчитаетRuntimeUnit.Relic→ грузит override + спрайт через Addressables → вешает наAnimator.
⚠️ Анимация косметична и развязана с таймингом сим.
Animatorуправляется чтением сим-состояния (isMovingизPosition != PreviousPosition) и сим-событий (Attack/Hit/Dieчерез MessagePipe). Урон наносится на сим-тике (AutoAttackSystem), а НЕ на anim-event — иначе рассинхрон в ко-опе. Скорость attack-клипа масштабируется отAttackSpeed(косметика).
5. Единая модель эффектов
Ключевая идея: пассивки, «поведенческая» часть перков, баффы/дебаффы, DoT/HoT, регенерация, контроль, триггеры (вампиризм/шипы), гильд-бонусы ГМ — всё это один Effect = контейнер с жизненным циклом + список полиморфных компонентов.
public class EffectData : ScriptableObject // иммутабельное определение
{
public string Id; // "burn" — строковый ID для сейва/Workshop
public EffectPolarity Polarity; // Positive / Negative
public EffectTag Tags; // [Flags]: Control, DoT, Heal, Shield...
public float Duration; // 0 = мгновенный, -1 = постоянный (пассивка)
public StackRule Stacking; // None / Stack / Refresh / StackAndRefresh
public int MaxStacks;
public int CleanseTier; // устойчивость к диспелу (§5.4)
public bool Unremovable; // неснимаемо никаким диспелом
[SerializeReference] public IEffectComponent[] Components; // ЧТО он делает
}
public sealed class RuntimeEffect // POCO на юните
{
public EffectData Def;
public RuntimeUnit Source; // кто наложил — триггеры, скейл, атрибуция
public float RemainingTime; // в тиках симуляции
public int Stacks;
public float ScaledPotency; // посчитано при наложении (§5.2)
}5.1 Компоненты (интерфейсы «по делу»)
public interface IEffectComponent
{
void OnApply(in EffectContext ctx);
void OnExpire(in EffectContext ctx);
}
public interface IPeriodicComponent : IEffectComponent // тик каждые Interval
{
float Interval { get; }
void OnTick(in EffectContext ctx);
}
public interface IReactiveComponent : IEffectComponent // реагирует на события боя
{
CombatEvent Events { get; }
void OnEvent(in EffectContext ctx, in CombatEventData e);
}Библиотека (контент-словарь, переиспользуется и активками): StatModifierComponent, PeriodicDamageComponent, PeriodicHealComponent, ControlComponent, ShieldComponent, LifestealComponent (реактивный), ThornsComponent (реактивный), DispelComponent (§5.4)…
EffectContext(передаётсяin):{ Target, Source, ICombatContext Combat, IRngService Rng, float Dt }.ICombatContext— API влияния на мир (DealDamage,Heal,ApplyEffect,QueryUnitsInRadius,SpawnProjectile). Один реализатор, но настоящий шов: делает компоненты юнит-тестируемыми с мок-контекстом.
5.1.1 Скейлинг параметров (ScalableValue)
Скейл — по-параметрный, по-умению, опциональный (референс — LoL): у одного умения урон может скейлиться от AP, длительность контроля — от AP с другим коэффициентом, а радиус — не скейлиться вовсе. Поэтому скейл живёт на каждом числовом параметре, а не «на компоненте».
Любой масштабируемый параметр объявляется как ScalableValue (а не голый float):
[Serializable] public struct ScalableValue
{
public float Base;
public ScalingTerm[] Scalings; // пусто = не скейлится
public float Resolve(IStatReader src) // base + Σ(stat × ratio)
=> Base + sum(src.Get(t.Stat) * t.Ratio for t in Scalings);
}
[Serializable] public struct ScalingTerm { public StatType Stat; public float Ratio; }- Поддерживает скейл от любого
StatType(AP — частый случай, но можно от урона, бонус-HP и т.д.). - Пример:
ShieldComponent { ScalableValue BlockAmount; ScalableValue BlockPercent; }— сумма и процент блока скейлятся независимо или не скейлятся. StatsреализуетIStatReader. Резолв — по снимку статов источника на момент каста/наложения (детерминированно).- Порядок: потенция параметра резолвится через
ScalableValue(статы источника) + потенция-множители урона/исцеления; длительность — отдельно через эфф-эффекты (§5.2).
5.2 Четыре «эффективности» — масштабируют ТОЛЬКО длительность
Полная таблица семей множителей — 11. Стат-система §5.
Эфф-эффекты (Apply*/Receive*) меняют только длительность таймированного эффекта, не его силу:
длительность = base × Source.Get(ApplyEff[polarity]) × Target.Get(ReceiveEff[polarity])
polarity = Positive→ApplyBuffEff×ReceiveBuffEff;Negative→ApplyDebuffEff×ReceiveDebuffEff.- Потенцию (урон/тик, хил/тик, величину щита) масштабируют другие статы:
DamageDealt/TakenEff,HealShieldDealt/TakenEff+ScalableValue.
⚠️ DoT/HoT total масштабируется от длительности. Хранить per-tick rate (или
totalBase/baseDuration), не запечённый total. «160 HP за 5 сек» = 32 HP/сек; продлили до 10 сек → 320 HP total (rate тот же, тиков вдвое больше). Запекать готовый total при наложении нельзя — сломает масштабирование при смене длительности.
5.3 Стакинг и контроль
- Стакинг по
StackRule+MaxStacks. Компоненты читаютStacksдля величины. - Контроль =
ControlComponent, ставит флаги на юните (CanAct/CanMove/CanCast). Без diminishing returns — цикл контроля = заслуга игрока, не баг. Стакается/обновляется как любой эффект. - AI-фильтр «цель без эффекта X» спрашивает битовую маску тегов на юните (обновляется при add/remove), а не перебирает список — перф для staggered-AI.
5.4 Диспел / очищение (многоуровневый)
DispelComponent:
public EffectPolarity TargetPolarity; // снимать баффы / дебаффы / любые
public EffectTag TargetTags; // категории: только DoT / только Control / всё
public int DispelPower; // снимает эффекты с CleanseTier ≤ DispelPower
public int MaxCount; // 0 = все подходящиеРазрешение: из активных эффектов цели берём Polarity match И Tags match И CleanseTier ≤ DispelPower И !Unremovable → принудительный OnExpire (корректный teardown).
Покрывает требования дизайна:
- «снимает кровоток, но не оглушение» →
TargetTags = DoT. - «неснимаемое» →
Unremovable = true. - «уровни силы и развеивания» → лестница
CleanseTiervsDispelPower. - purge (снять баффы врага) и cleanse (снять дебаффы союзника) — один механизм через
TargetPolarity.
Канонические тиры CleanseTier (лестница снимаемости, она же ранг опасности):
| Тир | Что входит | Примеры |
|---|---|---|
| 1 — Базовые | слоу, −статы, метка, разлом брони, DoT | замедление, поджог, яд, метка охотника, разлом брони |
| 2 — Контроль | жёсткий CC | стан, заморозка, рут, оцепенение |
| 3 — Особые | редкие / долгие | боссовые проклятия, «Копия» (The Mirror), долгие штрафы |
Unremovable | вне лестницы | смещение (KnockUp), перманентные проклятия реликвий |
DispelPowerсравнивается с лестницей: сила 2 снимает T1–T2, но не T3.- Смещение —
Unremovable(нельзя снять в полёте, иначе рвётся комбо The Gale / The Tide): только предотвращается иммунитетом (§5.6), не снимается. - Баланс-интент:
DispelPower = 3— привилегия редких уников (The Paragon). Спец-клинсовики отличаются ШИРОТОЙ (AOE / все / проактивно) приDispelPower = 2, а не тиром, чтобы не обесценить уников. - Порядок при
MaxCount > 0(решено): высшийCleanseTierпервым; тай — по наибольшей оставшейся длительности. Детерминированно, соответствует «снять самый опасный».
5.5 Унификация
| Что в GDD | Как в модели |
|---|---|
| Пассивка Реликвии | Effect, Duration = -1, наложен при сборке юнита |
| Перк (поведенческая часть) | стат-модификаторы (§3) + опц. постоянный Effect |
| Бафф / дебафф / DoT / HoT | Effect с компонентами + длительность |
| Контроль | Effect с ControlComponent |
| Вампиризм / шипы | Effect с IReactiveComponent |
| Гильд-бонус ГМ | постоянный Effect на всех юнитов |
| Мгновенный урон активки | компонент с Duration = 0 (один OnApply) |
5.6 Иммунитет (предотвращение наложения)
Диспел (§5.4) снимает УЖЕ висящие эффекты. Иммунитет — противоположный инструмент: не даёт наложить новые. Отдельный механизм, не диспел и не tenacity.
public sealed class ImmunityComponent : IEffectComponent // висит как эффект-бафф
{
public EffectPolarity BlockPolarity; // обычно Negative — отвергать новые дебаффы
public EffectTag BlockTags; // всё / только Control / …
public bool BlockDamage; // премиум: неуязвимость к урону (The Paragon «Идеальная форма»)
}- Пока активен,
ICombatContext.ApplyEffectотвергает входящие эффекты, совпадающие поBlockPolarity/BlockTags— включая смещение (это тоже эффект, значит иммунитет блокит откидывание). BlockDamageпроверяется в пайплайне урона (§6) до митигации — весь урон обнуляется.- Бинарный и короткий по дизайну (The Paragon: 3 сек за ману). Тирить иммунитет — не сейчас.
Три РАЗНЫХ ответа на контроль — не путать: снять (диспел, §5.4) · сократить (tenacity = ReceiveDebuffEff < 1, §5.2) · не пустить (иммунитет, §5.6). Собираются игроком отдельно — это и есть разнообразие саппорт-слота.
6. Пайплайн урона
Урон — событие, проходящее детерминированный конвейер БЕЗ RNG (крита и уклонения нет — решение зафиксировано). Полная формула митигации (броня = %-формула K/(K+armor), пробивание = плоское + процентное) — в 11. Стат-система §4:
raw → ×Source.DamageDealtEff → митигация брони/резиста (с пробиванием; True — пропуск)
→ ×Target.DamageTakenEff → поглощение Shield → вычет из HP
→ событие OnDamageDealt/Taken (триггеры: вампиризм, шипы, «при уроне…»)
Крит отсутствует. Если введём позже — будет детерминированным («каждый N-й удар»), не через
ctx.Rng. Поэтому в пути урона Фазы 1 обращений к RNG нет; RNG входит через процы эффектов (Фаза 2) и генерацию/лут (Фаза 5).
7. Детерминизм: реентрантность
Эффекты порождают события, события — эффекты. Чтобы не словить недетерминизм/переполнение стека:
- очередь событий, обрабатываемая FIFO в пределах тика (не глубокая рекурсия);
- фиксированный порядок обработки событий и итерации эффектов (insertion order; добавления — в pending-очередь, не в коллекцию во время итерации);
- тики эффектов от тика симуляции (фиксированный
dt); - весь рандом компонентов — через
ctx.Rng.
Достраивает чек-лист детерминизма из 5. Технологический стек и архитектура.
8. Интерфейсы «по делу» (итог)
| Интерфейс | Почему оправдан |
|---|---|
IModifierSource | 6 реализаций (Relic/Perk/Item/CommandArtifact/Effect/Guildmaster), агрегатор итерирует разнородные источники |
IEffectComponent (+ IPeriodic, IReactive) | Много реализаций, [SerializeReference] в EffectData — основной контент-словарь |
ICombatContext | Шов между логикой эффектов и симуляцией; граница для мок-тестов |
Конкретные классы (без интерфейса): Stats, RuntimeUnit, RuntimeEffect — одна реализация, не плодим.
9. Связь с сейвом
UnitSaveData хранит vesselId, equippedRelicId, perkIds[], itemIds[] — строковые ID. RuntimeUnit пересобирается из них + SO при загрузке, не сериализуется напрямую. См. 2. Сохранения.
10. Движковые расширения под контент реликвий (бэклог)
Фазы 1–2 дали движок эффектов/способностей и библиотеку компонентов (
StatModifier,PeriodicDamage/Heal,Control,Shield,Lifesteal,Thorns,Dispel). Часть запланированных реликвий-классов (GDD 1. Контент - Реликвии) опирается на механики, которых в этой библиотеке ещё нет. Решение по скоупу: все 7 Обычных реликвий должны быть играбельны в бою к концу Фазы 3 (9. Roadmap §3), поэтому нужные им расширения затаскиваются в Фазу 3 (а не размазываются по Ф4).
| Расширение | Что нужно | Кому нужно (пример) | Фаза |
|---|---|---|---|
| Заряды способности | AbilityData: N зарядов + независимая перезарядка заряда (поверх кулдауна/ресурса) | Скрытный Убийца («Изворотливость», 2 заряда) | Ф3 |
| Атака в движении | Флаг на реликвии/AttackType: авто-атака не останавливает MovementSystem | Лесной следопыт, Монах вихря | Ф3 |
| Хил-автоатака | Инверсия таргета авто-атаки: выбор союзника + Heal вместо DealDamage | Светлый пастырь | Ф3 |
| Форма авто-атаки (линия/конус) | Melee-AOE авто-атака: расширить AttackType или компонент формы удара | Железный копейщик | Ф3 |
| Смещения (displacement) | Компонент knockback / pull / teleport, мутирующий Position детерминированно | Монах вихря (толчок + телепорт в спину) | Ф3 |
| Эффект «подкидывание» | Накладывается при любом перемещении вражеского юнита (отталкивание/притягивание/…). Не уменьшается контролем/tenacity. Реф — LoL knock-up | семейство контроллеров | Ф3 |
| Реактивный перенос метки | IReactiveComponent на UnitDied: метка/эффект перепрыгивает на ближайшую новую цель | Лесной следопыт (метка +25% урона) | Ф3 (реактив, Ф2-совместимо) |
| Empower next attack | Однострел-реактив: усилить следующую авто-атаку (×урон / доп. эффект), снять после срабатывания | Монах вихря, Маг-рыцарь (черновая идея) | Ф3 (реактив, Ф2-совместимо) |
Перенос метки и empower-next-attack ложатся на уже готовый
IReactiveComponent— это контент, а не новый движок. Остальное — реальные движковые доработки Фазы 3.
Открытые вопросы
Точный список— решено, см. 11. Стат-система. Дефолты вStatType(~30)StatsConfig— баланс (Фаза 4+).- Полный список
EffectTag(категории для тегов/диспела/AI-фильтров). Порядок выбора при— решено (§5.4): высшийMaxCountв диспелеCleanseTierпервым, тай — по наибольшей оставшейся длительности.- Стартовые наборы и уникальные механики Гильдмастеров — контент-дизайн.