Статус: Draft (на ревью Макса)


Детальная архитектура Фазы 1 из роадмапа (9. Roadmap реализации): боевое ядро + нетворк-спайк. Кто за что отвечает, как классы взаимодействуют, какие сборки и сцены создаём. Опирается на 5. Стек и архитектура, 6. Боевая модель, 7. Флоу забега.

Это план для ревью, а не финальный код. Сначала Макс читает и даёт мнение → потом реализуем конкретные классы.


0. Решённые развилки (контекст этого дока)

ВопросРешение
asmdef-графПолный рефактор «как надо» — старые 5 asmdef были временными, не опираемся на них
Источник юнитов в Фазе 1Минимальные реальные SO (RelicData/VesselData/StatsConfig), а не C#-фикстуры. Полный Odin-авторинг — Фаза 4
Структура сценPersistent CoreScene + аддитивная BattleScene (паттерн из дока 7)
НетворкПроектируем шов сейчас — command-слой и host-authoritative граница закладываются так, чтобы спайк встал без переделок

1. Что Фаза 1 даёт и чего НЕ делает

Деливерабл: бьющиеся юниты в BattleScene, детерминированная тиковая симуляция 30 Гц, чистый пайплайн урона на IRngService, headless-тесты детерминизма, и спайк host-authoritative потока команд через сим в MPPM (2 инстанса).

✅ В Фазе 1⛔ НЕ в Фазе 1 (куда переезжает)
Тиковый цикл 30 Гц, детерминизмПолная модель эффектов → Фаза 2
RuntimeUnit + Stats (слоистые модификаторы)AI Filter/Score/Override → Фаза 3 (Фаза 1 = таргетинг «ближайший враг»)
Пайплайн урона (чистые функции)Odin-авторинг контента, полный StatTypeФаза 4
Непрерывное движение + spatial hashПолный GameFlow, карта, магазины → Фаза 5
Авто-атаки: ближний + снаряды (swept)Полная сеть: лобби, репликация RunState, реконнект → Фаза 6
Command-слой + event-queue (каркас)UI Toolkit экраны, MVVM → Фаза 7
Презентация: Shapes HP-бары, Feel-хуки, damage numbers
Нетворк-спайк: host-auth поток команд, 2× MPPM

Принцип границы: Фаза 1 строит движок и швы, а не контент. Эффекты, AI, контент подключаются в готовые точки расширения, не переписывая ядро.


2. Сборки (новый граф)

2.1 Граф зависимостей

Core
 ├─ Data            → Core
 ├─ Combat          → Core, Data
 ├─ Guild           → Core, Data
 ├─ MiniGames       → Core, Data
 ├─ Net             → Core, Combat
 ├─ Presentation    → Core, Data, Combat
 ├─ UI              → Core, Data, Guild
 ├─ DevTools        → Core, Data, Combat, Game
 └─ Game            → Core, Data, Combat, Guild, Net, Presentation, UI

Game — единственный, кто знает всех (composition root). Всё остальное зависит только вниз.

2.2 Таблица сборок

СборкаЗависит отЧистая C#?НазначениеФаза 1?
Guildmaster.Core✅ (no engine refs по возможности)IRngService, математика сим, тик-контракты, базовые интерфейсы команд/событий✅ создаём
Guildmaster.DataCoreSO (нужен UnityEngine)ScriptableObject-определения + полиморфные интерфейсы поведения (IEffectComponent), StatType, ScalableValue, RelicData, VesselData, EffectData, StatsConfig✅ минимум
Guildmaster.CombatCore, Data✅ чистая логикаДетерминированная симуляция: RuntimeUnit, Stats, тик-степпер, системы (движение/таргетинг/атаки/снаряды), пайплайн урона, ICombatContext, spatial hash✅ ядро
Guildmaster.GuildCore, DataРостер, RunState, ресурсы⛔ стаб (Фаза 5)
Guildmaster.MiniGamesCore, DataИзолированные мини-игры за IMiniGame⛔ стаб (позже)
Guildmaster.NetCore, CombatNGO/FacepunchHost-authoritative реле команд, инициализация транспорта✅ спайк
Guildmaster.PresentationCore, Data, CombatMono + Shapes/Feel/LitMotionWorld-space вид боя: спрайты, HP-бары, damage numbers, debug-draw, фидбэк. Только читает сим
Guildmaster.UICore, Data, GuildUI ToolkitЭкраны меню/HUD/карты (MVVM)⛔ стаб (Фаза 7)
Guildmaster.DevToolsCore, Data, Combat, GameQuantum ConsoleDebug-команды gm_*
Guildmaster.Gameвсе вышеVContainer + MonoComposition root (LifetimeScopes), GameFlow, загрузка сцен, пуск тик-цикла
Guildmaster.Tests.EditModeCore, Data, CombatДетерминизм, урон, статы, spatial hash, снаряды
Guildmaster.Tests.PlayModeCore, Data, Combat, GameИнтеграция: battle start→loop→end

2.3 Внешние пакеты по сборкам

СборкаВнешние ссылки
Core— (по возможности noEngineReferences)
DataOdin (Sirenix), UnityEngine
Combat— (только Core/Data; без VContainer/UniTask/Unity-физики — чистая, тестируемая)
NetNGO, Facepunch.Steamworks
PresentationShapes, Feel (MMFeedbacks), LitMotion, MessagePipe, UniTask
UIUI Toolkit, MessagePipe (+ R3 если понадобится)
GameVContainer, MessagePipe, UniTask, NGO
DevToolsQuantum Console

Почему Combat без VContainer и UniTask: симуляция — чистая детерминированная логика. Зависимости приходят через конструкторы, а не [Inject]-атрибуты. Реальный реалтайм-цикл (UniTask-accumulator) живёт в Game (CombatLoopService) и просто дёргает sim.Tick(dt). Так сим тестируется headless без движка и без DI.

Почему отдельный Data (а не «Units»): была мина — RelicData несёт способности, порождающие эффекты (EffectData/IEffectComponent). Если эффекты в Combat, а Relic в Units, то Units → Combat = запрещённая зависимость вверх. Решение: все определения-данные и полиморфные интерфейсы поведения — в одном слое Data, ниже симуляции. Combat ссылается на Data вниз. Relic ссылается на Effect внутри одной сборки — конфликта нет.

Почему отдельный Presentation (а не всё в «UI»): доковый принцип «разделение симуляции и презентации». World-space вид боя (Shapes/Feel/LitMotion, читает RuntimeUnit) — это не экранный UI Toolkit. Разные технологии, разные зависимости.


3. Сцены

3.1 Карта сцен Фазы 1

СценаТипЖивётНазначение
CoreScenePersistent (грузится первой, не выгружается)Всю сессиюComposition root, общие сервисы, постоянная камера, оверлей-слой, debug-консоль
BattleSceneАддитивнаяНа время бояБоевая арена, презентация боя, боевой scope
BootScene (опц.)Точка входаМигТолько грузит CoreScene и передаёт управление. Можно пропустить и сделать CoreScene точкой входа

Map / Lobby / Shop сцены — Фазы 5–6, здесь не делаем. «Глянуть карту в бою» по доку 7 = read-only UI-оверлей поверх (не сцена) — тоже позже.

3.2 CoreScene — что внутри

Постоянная сцена. Никогда не выгружается → всё, что должно пережить смену боёв, живёт здесь.

ОбъектКомпонент(ы)Роль
[Bootstrap]GameBootstrapТочка старта: поднимает RootLifetimeScope, грузит первую игровую сцену
[DI Root]RootLifetimeScope (VContainer)Регистрирует сессионные сервисы (см. §8)
[EventBus]MessagePipe (через scope)Глобальная шина сим→презентация/аудио
Main CameraCamera + (later) CinemachineBrainПостоянная камера. Боевая сцена задаёт лишь рамки арены и якоря
[Audio]IAudioService (Unity Audio стаб)За интерфейсом; FMOD — Фаза 9
[DevConsole]Quantum Console prefabgm_* команды
[OverlayRoot]UIDocument (пустой)Будущий слой постоянных оверлеев (карта-в-бою, пауза)

3.3 BattleScene — что внутри

Аддитивная. Грузится SceneLoader при старте боя, выгружается после. Никакой сим-логики в объектах сцены — симуляция это чистые C#-объекты, создаваемые CombatLifetimeScope. Сцена несёт только арену и презентацию.

ОбъектКомпонент(ы)Роль
[Combat DI]CombatLifetimeScope (child от Root)Регистрирует CombatSimulation, системы, ICombatContext, CombatLoopService (см. §8)
[Arena]ArenaConfigРамки поля (мин/макс), якоря спавна команд A/B, размер ячейки spatial hash
[Presentation]CombatPresenterПодписан на сим-события, спавнит/убирает UnitView, мост сим→вид
[DebugDraw]CombatDebugDraw (Shapes)Spatial hash, радиусы, траектории снарядов. Тогглится из QC
[WorldHUD]Canvas (uGUI, опц.) или ShapesHP-бары, damage numbers (world-space)
[Background]SpriteShape/Tilemap (позже)Декор арены, не геймплейная геометрия

Камеры в BattleScene нет — используется постоянная из CoreScene. Это упрощает мульти-сцену и NGO scene sync.


4. Слой данных (Guildmaster.Data) — минимум Фазы 1

Реальные SO, но скелетные. Полный авторинг — Фаза 4.

4.1 Статы

// Полный список в доке 6; Фаза 1 реализует подмножество, enum растёт без боли.
public enum StatType
{
    MaxHP, AutoAttackDamage, AttackSpeed, AttackRange, MoveSpeed,
    PhysArmor, MagicArmor, PhysPen, MagicPen,
    DamageDealtEff, DamageTakenEff,   // «эффективности» — старт 1.0, PercentMult
    // … всего 30 (полный канон — док 11); остальные — Фаза 2/4
    // Крита НЕТ (решение залочено, док 11 §1): стат крита не вводим.
}
 
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;   // для снятия модов источника РАЗОМ
}

4.2 SO-определения (скелеты)

SOПоля Фазы 1Примечание
StatsConfigдефолты всех StatType (глобальный шаблон)Единственный экземпляр, база стат-сборки
RelicDataстат-блок (StatModifier[]), AttackType (Melee/Ranged), AttackRange, ProjectileSpeed, теги«Чемпион». Активки/пассивки — поля есть, наполнение Фаза 2
VesselDataимя, ключ локализации, плейсхолдер перков«Пилот». Перки-модификаторы — Фаза 2/4
EffectDataId, Polarity, Tags, Duration, [SerializeReference] IEffectComponent[]Скелет интерфейсов сейчас, реализации компонентов — Фаза 2
// Объявляем интерфейсы и ScalableValue СЕЙЧАС (швы), реализации — Фаза 2.
public interface IEffectComponent { void OnApply(in EffectContext ctx); void OnExpire(in EffectContext ctx); }
public interface IPeriodicComponent : IEffectComponent { float Interval { get; } void OnTick(in EffectContext ctx); }
public interface IReactiveComponent : IEffectComponent { CombatEvent Events { get; } void OnEvent(in EffectContext ctx, in CombatEventData e); }
 
[Serializable] public struct ScalableValue { public float Base; public ScalingTerm[] Scalings; /* Resolve(IStatReader) */ }
[Serializable] public struct ScalingTerm { public StatType Stat; public float Ratio; }

5. Боевая симуляция (Guildmaster.Combat) — сердце Фазы 1

5.1 Тиковый цикл и детерминизм

CombatLoopService (Game, MonoBehaviour + UniTask)   ← реалтайм-пульс, host-only
   │  accumulator += Time.deltaTime (только ЗДЕСЬ Time допустим)
   │  while (accumulator >= TickDelta) { sim.Tick(TickDelta); accumulator -= TickDelta; }
   ▼
CombatSimulation.Tick(float dt)   (Combat, чистый)   ← детерминированный шаг
   1. ApplyDueCommands(currentTick)        // команды, чей TargetTick == currentTick
   2. systems в ФИКСИРОВАННОМ порядке:
        AbilityCooldownSystem → TargetingSystem → MovementSystem
        → AutoAttackSystem → ProjectileSystem → (EffectSystem — Фаза 2)
        → DamageResolutionSystem → DeathSystem
   3. DrainEventQueue()                     // FIFO, без рекурсии (док 6 §7)
   4. currentTick++
КлассСборкаОтветственность
CombatLoopServiceGameРеалтайм accumulator-пульс, пауза/возобновление, только хост тикает. Time.deltaTime живёт только тут
CombatSimulationCombatВладеет состоянием боя: список RuntimeUnit, spatial hash, command-queue, event-queue, currentTick. Метод Tick(dt) — чистый, детерминированный
SimConstantsCoreTickRate = 30, TickDelta = 1f/30f, частота AI = 10 Гц (для Фазы 3)

Чек-лист детерминизма (из доков 5/6, проверяется тестами): фиксированный dt; весь рандом через IRngService; детерминированный порядок итерации (списки + insertion order, не foreach по Dictionary/HashSet); ноль Unity-физики; добавления юнитов/эффектов — в pending-очередь, не в коллекцию во время итерации.

5.2 Юнит и статы

КлассОтветственность
StatsСлоистые модификаторы с источником. Get(StatType) с кэшем по dirty-флагу. AddModifiersFrom(source, mods) / RemoveModifiersFrom(source). Реализует IStatReader. Формула: final = (base + ΣFlat) × (1 + ΣPercentAdd) × Π(1 + PercentMult)
RuntimeUnitPOCO на один бой: Stats, CurrentHP/Resource/Shield, Position, PreviousPosition (для интерполяции вида), Team, ссылки на RelicData/VesselData, ActiveEffects[] (Фаза 2), EffectTagMask, AIProfile (Фаза 3). Не ScriptableObject.CreateInstance
RuntimeUnitFactory«Компиляция» SO → RuntimeUnit по шагам дока 6 §3: base из StatsConfig → моды Relic → (перки/предметы Фаза 2+) → init CurrentHP = Get(MaxHP). Единственная точка сборки юнита

5.3 Пространство и системы

КлассОтветственность
SpatialHashUniform grid поверх непрерывных координат. QueryRadius(center, r) без аллокаций (буфер). Перестраивается/обновляется на тике. Только ради перфа — поле остаётся непрерывным
MovementSystemИнтегрирование позиции к цели (ручная математика, без Rigidbody2D). Фаза 1 — простой move-to-target до дистанции атаки. IPositioningMode (гистерезис, support-смещение) — Фаза 3
TargetingSystemФаза 1 — «ближайший живой враг» через spatial hash. Полный Filter/Score/Override — Фаза 3 (этот класс станет тонкой обёрткой над AI-движком)
AutoAttackSystemКулдаун атаки от AttackSpeed. Melee → прямой DealDamage. Ranged → SpawnProjectile
ProjectileSystemСнаряды как данные. Swept circle-segment проверка (отрезок пути за тик), чтобы быстрые снаряды не «протыкали» цель между тиками. Попадание → DealDamage
DeathSystemHP ≤ 0 → пометить мёртвым, убрать из spatial hash, эмитнуть UnitDied, проверить win/lose

5.4 Урон — чистые функции

DamagePipeline (статический/чистый, рефактор DamageCalculator на IRngService). Конвейер дока 6 §6:

raw → ×Source.Get(DamageDealtEff) → −броня/резист (+пробивание) [True — пропуск]
    → ×Target.Get(DamageTakenEff) → поглощение Shield → вычет из HP
    → эмит OnDamageDealt / OnDamageTaken (триггеры вампиризма/шипов — Фаза 2)

Крита и уклонения нет (решение залочено, док 11 §1) — путь урона полностью детерминирован, без обращений к RNG. RNG входит в игру позже (процы эффектов — Фаза 2; лут/карта — Фаза 5).

Чистые функции (in-параметры, никакого состояния) → 100% покрываются EditMode-тестами.

5.5 ICombatContext — шов «логика ↔ мир»

Единственный реализатор CombatContext, но настоящий шов — делает будущие эффекты/способности юнит-тестируемыми с моком.

public interface ICombatContext
{
    void DealDamage(in DamageRequest req);
    void Heal(RuntimeUnit target, float amount, RuntimeUnit source);
    void SpawnProjectile(in ProjectileSpawn spawn);
    int  QueryUnitsInRadius(Vector2 center, float radius, Span<RuntimeUnit> buffer, TargetFilter filter);
    void ApplyEffect(RuntimeUnit target, EffectData def, RuntimeUnit source);  // тело — Фаза 2
    IRngService Rng { get; }
    int CurrentTick { get; }
}

5.6 Команды и события

Два разных механизма — не путать:

МеханизмГдеПриродаДля чего
Command-queueCombatДетерминированная, отсортированная по (TargetTick, PlayerId, Seq)Вход в сим: интенты игрока (каст ГМ, пауза). Применяются на границе тика. Основа host-auth + реплеев
Internal event-queueCombatFIFO в пределах тика, без рекурсииРеактивность эффектов (Фаза 2). Детерминированный порядок
MessagePipe (outward)Game/PresentationFire-and-forgetСим → презентация/аудио/UI. Презентация только читает
public interface ISimCommand { int TargetTick { get; } void Apply(CombatSimulation sim); }
// Фаза 1: PauseCommand, ResumeCommand, CastGuildmasterSpellCommand (стаб), SpawnUnitCommand (setup)

Почему команды важны уже сейчас: ВСЕ мутации сим во время боя входят через упорядоченные команды на границе тика. Это ровно тот шов, который даёт host-authoritative мультиплеер (хост упорядочивает, клиенты применяют те же команды на тех же тиках → синхрон) И реплеи (лог команд + сид) — «один механизм, три фичи» из дока 5.


6. Нетворк-шов (Guildmaster.Net) — host-authoritative

Архитектура закладывается сейчас; полная сеть — Фаза 6. Цель спайка — снять главный риск рано.

6.1 Поток команды (host-authoritative)

Клиент: игрок кликнул каст ГМ
   │  CommandIntent (локально)
   ▼ ServerRpc
Хост: NetworkCommandRelay
   │  присваивает TargetTick = currentTick + lookahead, PlayerId, Seq
   │  enqueue в CombatSimulation.commandQueue
   ▼ ClientRpc (broadcast той же команды с тем же TargetTick)
Все (хост+клиенты): применяют команду на TargetTick детерминированно
   → симуляции совпадают
КлассОтветственность
NetworkCommandRelay (NetworkBehaviour)Клиент → ServerRpc(intent); хост штампует tick/seq, кладёт в очередь и broadcast ClientRpc; все enqueue
FacepunchTransportBootstrapInit Steam + Facepunch transport (relay/NAT бесплатно)
SimSyncProbe (спайк)Периодический checksum состояния сим (хэш позиций+HP+tick) для проверки рассинхрона в тесте

6.2 Цель спайка (Definition of Done)

  • MPPM, 2 инстанса (хост + клиент).
  • Facepunch transport поднят.
  • Команда от клиента проходит host-auth путь и применяется на одном тике у обоих.
  • SimSyncProbe показывает совпадающий checksum после N тиков → детерминизм + сеть подтверждены.
  • Тикает только хост; клиент применяет команды/синк (в Фазе 1 — упрощённо: команд-эхо + checksum; полная репликация — Фаза 6).

7. Презентация (Guildmaster.Presentation) — только читает сим

КлассИнструментОтветственность
CombatPresenterMessagePipeПодписан на сим-события; спавнит/деспавнит UnitView; мост сим→вид. Точка входа презентации боя
UnitView (Mono)FeelПозиционирует спрайт по RuntimeUnitинтерполяцией между PreviousPositionPosition, т.к. сим 30 Гц, рендер 60+). На событиях дёргает MMFeedbacksPlayer.PlayFeedbacks() (Фаза 1 — пустые листы, no-op)
HealthBarViewShapesDraw.Arc/Draw.Ring по CurrentHP/MaxHP. Crisp на любом зуме, без атласов
DamageNumberSpawnerLitMotion + ObjectPoolПодписан на OnDamageDealt; всплывающие числа, zero-alloc твины, пулинг
CombatDebugDrawShapesSpatial hash grid, радиусы атак/AOE, swept-траектории снарядов. Тоггл из QC (gm_toggle_debug_draw). Бесценно при отладке сим без арта
RangeIndicatorViewShapesDraw.Ring радиуса атаки/AOE при наведении

Разделение ролей твинов: LitMotion — code-driven (damage numbers, lerp HP-бара). Feel — event-triggered bursts (вспышка удара, шейк, death-burst), конфигурится в Inspector, не кодом. Не конкурируют, дополняют.


8. DI / Composition (Guildmaster.Game, VContainer)

8.1 RootLifetimeScopeCoreScene, на всю сессию)

Регистрирует:

  • IRngServiceXorShiftRng (фабрика — суб-сид на бой создаётся в боевом scope).
  • MessagePipe (RegisterMessageBroker).
  • IAudioServiceUnityAudioService (стаб).
  • SceneLoader (аддитивная загрузка, NGO-aware).
  • GameFlow (Фаза 1 — тонкий: Boot → грузит BattleScene; полный флоу — Фаза 5).

8.2 CombatLifetimeScopeBattleScene, на один бой)

Child от Root. Регистрирует:

  • CombatSimulation + все системы (§5.3) через конструкторы.
  • ICombatContextCombatContext.
  • IRngService боя (суб-сид = runSeed + battleIndex + attempt, док 7 §6).
  • CombatLoopService (пульс).
  • CombatPresenter.

Сцена выгрузилась → scope задиспозился → бой и его аллокации ушли. Чисто.

8.3 Прочие сервисы

КлассСборкаРоль
GameBootstrapGameЭнтри: поднять Root scope, загрузить первую сцену
SceneLoaderGameLoadBattleAdditive() / UnloadBattle(). Под NGO scene management (Фаза 6) — один шов
BattleSetupBuilderGameМинимальный RunState/тест-конфиг → начальные юниты сим (через RuntimeUnitFactory)

9. DevTools (Guildmaster.DevTools, Quantum Console)

Команды, нужные с Фазы 1:

КомандаДействие
gm_rng_seed <seed>Зафиксировать сид боя (детерминизм-отладка)
gm_spawn_battle <a> <b>Поднять тест-бой N×M юнитов
gm_set_hp <unitId> <v>Выставить HP
gm_skip_battleМгновенно завершить бой
gm_toggle_debug_drawВкл/выкл Shapes debug-слой

10. Тесты (Фаза 1)

ТестСборкаЧто проверяет
Детерминизм симEditModeОдин сид + один лог команд → одинаковый checksum после N тиков (headless, без сцены)
Пайплайн уронаEditModeЧистые функции: броня/пробивание (физ/маг/True), эффективности, щит — без RNG
Стат-формулаEditMode(base+ΣFlat)×(1+ΣPercentAdd)×Π(1+PercentMult); add/remove по источнику; dirty-кэш
Spatial hashEditModeQueryRadius находит ровно тех, кто в радиусе; границы ячеек
Снаряд sweptEditModeБыстрый снаряд не «протыкает» цель между тиками
Battle integrationPlayModestart → loop → одна сторона побеждает → корректный teardown

11. Карта взаимодействия (один бой)

GameBootstrap → RootLifetimeScope (CoreScene)
   └─ GameFlow.Boot → SceneLoader.LoadBattleAdditive()
        └─ CombatLifetimeScope (BattleScene)
             ├─ BattleSetupBuilder → RuntimeUnitFactory → юниты в CombatSimulation
             ├─ CombatPresenter спавнит UnitView/HealthBarView на каждый юнит
             └─ CombatLoopService стартует пульс (host)
                  │
                  ▼ каждый кадр (host)
             accumulator → N× CombatSimulation.Tick(dt)
                  ├─ ApplyDueCommands  (вход интентов: ГМ-каст/пауза; в MP — от Net)
                  ├─ системы (фикс. порядок) → DamagePipeline (Rng)
                  ├─ DrainEventQueue
                  └─ emit MessagePipe-события → CombatPresenter → UnitView/HP-бар/числа/Feel
                  │
                  ▼ DeathSystem: одна сторона мертва
             GameFlow ← Outcome → SceneLoader.UnloadBattle() → scope dispose

12. Порядок реализации внутри Фазы 1 (предложение)

  1. Сборки + сцены — новый asmdef-граф, CoreScene/BattleScene, пустой RootLifetimeScope, зелёная компиляция и CI.
  2. Core-фундаментIRngService + XorShiftRng + тест детерминизма; SimConstants; базовые интерфейсы команд/событий.
  3. Data-минимумStatType (подмн.), StatModifier, StatsConfig, скелеты RelicData/VesselData; интерфейсы IEffectComponent/ScalableValue (без реализаций).
  4. Stats + RuntimeUnit + Factory — стат-формула + тесты.
  5. DamagePipeline — чистые функции + тесты (рефактор DamageCalculator на IRngService).
  6. Spatial hash + Movement + Targeting(lite) — + тесты.
  7. AutoAttack + Projectile(swept) — + тесты; ICombatContext.
  8. CombatSimulation.Tick — собрать системы в фикс. порядок; command/event-queue; battle integration-тест.
  9. CombatLoopService + DI scopes — реалтайм-пульс, пауза.
  10. ПрезентацияCombatPresenter, UnitView, HealthBarView (Shapes), DamageNumberSpawner (LitMotion), CombatDebugDraw.
  11. DevTools — QC-команды.
  12. Нетворк-спайкNet-сборка, NetworkCommandRelay, FacepunchTransportBootstrap, SimSyncProbe, тест в MPPM (2 инстанса).

Принцип темпа (док 9): тонкий вертикальный срез как можно раньше — после шагов 1–8 уже есть «бьющиеся юниты» в headless-тесте, после 9–10 — видимый бой в редакторе.


13. Открытые вопросы (на ревью)

  • Vector2: оставить UnityEngine.Vector2 в чистой сим (удобно, float-детерминизм ок по доку 5) или свой Vec2 в Core ради noEngineReferences на Combat? Склоняюсь к UnityEngine.Vector2 — прагматично.
  • Guild/UI/MiniGames: создать пустыми стабами сейчас (чтобы граф был полным) или заводить по мере надобности в своих фазах? Предлагаю стабы Guild/UI сейчас, MiniGames — позже.
  • BootScene: делать отдельную или CoreScene = точка входа? Предлагаю без BootScene (проще).
  • Lookahead команд в host-auth (на сколько тиков вперёд штампуем TargetTick) — подобрать в спайке под пинг Steam relay.
  • Подмножество StatType Фазы 1 — финальный список под авто-атаки (без эффектов). Уточнить перед шагом 3.

14. Следующий шаг

Макс читает → даёт мнение / правки → по согласованию начинаем с шага 1 (§12): новый asmdef-граф + сцены + пустой composition root + зелёный CI. После апрува — обновить 1. Сборки под новый граф.