Статус: 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.Data | Core | SO (нужен UnityEngine) | ScriptableObject-определения + полиморфные интерфейсы поведения (IEffectComponent), StatType, ScalableValue, RelicData, VesselData, EffectData, StatsConfig | ✅ минимум |
Guildmaster.Combat | Core, Data | ✅ чистая логика | Детерминированная симуляция: RuntimeUnit, Stats, тик-степпер, системы (движение/таргетинг/атаки/снаряды), пайплайн урона, ICombatContext, spatial hash | ✅ ядро |
Guildmaster.Guild | Core, Data | ✅ | Ростер, RunState, ресурсы | ⛔ стаб (Фаза 5) |
Guildmaster.MiniGames | Core, Data | ✅ | Изолированные мини-игры за IMiniGame | ⛔ стаб (позже) |
Guildmaster.Net | Core, Combat | NGO/Facepunch | Host-authoritative реле команд, инициализация транспорта | ✅ спайк |
Guildmaster.Presentation | Core, Data, Combat | Mono + Shapes/Feel/LitMotion | World-space вид боя: спрайты, HP-бары, damage numbers, debug-draw, фидбэк. Только читает сим | ✅ |
Guildmaster.UI | Core, Data, Guild | UI Toolkit | Экраны меню/HUD/карты (MVVM) | ⛔ стаб (Фаза 7) |
Guildmaster.DevTools | Core, Data, Combat, Game | Quantum Console | Debug-команды gm_* | ✅ |
Guildmaster.Game | все выше | VContainer + Mono | Composition root (LifetimeScopes), GameFlow, загрузка сцен, пуск тик-цикла | ✅ |
Guildmaster.Tests.EditMode | Core, Data, Combat | ✅ | Детерминизм, урон, статы, spatial hash, снаряды | ✅ |
Guildmaster.Tests.PlayMode | Core, Data, Combat, Game | — | Интеграция: battle start→loop→end | ✅ |
2.3 Внешние пакеты по сборкам
| Сборка | Внешние ссылки |
|---|---|
| Core | — (по возможности noEngineReferences) |
| Data | Odin (Sirenix), UnityEngine |
| Combat | — (только Core/Data; без VContainer/UniTask/Unity-физики — чистая, тестируемая) |
| Net | NGO, Facepunch.Steamworks |
| Presentation | Shapes, Feel (MMFeedbacks), LitMotion, MessagePipe, UniTask |
| UI | UI Toolkit, MessagePipe (+ R3 если понадобится) |
| Game | VContainer, MessagePipe, UniTask, NGO |
| DevTools | Quantum 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
| Сцена | Тип | Живёт | Назначение |
|---|---|---|---|
CoreScene | Persistent (грузится первой, не выгружается) | Всю сессию | 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 Camera | Camera + (later) CinemachineBrain | Постоянная камера. Боевая сцена задаёт лишь рамки арены и якоря |
[Audio] | IAudioService (Unity Audio стаб) | За интерфейсом; FMOD — Фаза 9 |
[DevConsole] | Quantum Console prefab | gm_* команды |
[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, опц.) или Shapes | HP-бары, 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 |
EffectData | Id, 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++
| Класс | Сборка | Ответственность |
|---|---|---|
CombatLoopService | Game | Реалтайм accumulator-пульс, пауза/возобновление, только хост тикает. Time.deltaTime живёт только тут |
CombatSimulation | Combat | Владеет состоянием боя: список RuntimeUnit, spatial hash, command-queue, event-queue, currentTick. Метод Tick(dt) — чистый, детерминированный |
SimConstants | Core | TickRate = 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) |
RuntimeUnit | POCO на один бой: 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 Пространство и системы
| Класс | Ответственность |
|---|---|
SpatialHash | Uniform 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 |
DeathSystem | HP ≤ 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-queue | Combat | Детерминированная, отсортированная по (TargetTick, PlayerId, Seq) | Вход в сим: интенты игрока (каст ГМ, пауза). Применяются на границе тика. Основа host-auth + реплеев |
| Internal event-queue | Combat | FIFO в пределах тика, без рекурсии | Реактивность эффектов (Фаза 2). Детерминированный порядок |
| MessagePipe (outward) | Game/Presentation | Fire-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 |
FacepunchTransportBootstrap | Init 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) — только читает сим
| Класс | Инструмент | Ответственность |
|---|---|---|
CombatPresenter | MessagePipe | Подписан на сим-события; спавнит/деспавнит UnitView; мост сим→вид. Точка входа презентации боя |
UnitView (Mono) | Feel | Позиционирует спрайт по RuntimeUnit (с интерполяцией между PreviousPosition→Position, т.к. сим 30 Гц, рендер 60+). На событиях дёргает MMFeedbacksPlayer.PlayFeedbacks() (Фаза 1 — пустые листы, no-op) |
HealthBarView | Shapes | Draw.Arc/Draw.Ring по CurrentHP/MaxHP. Crisp на любом зуме, без атласов |
DamageNumberSpawner | LitMotion + ObjectPool | Подписан на OnDamageDealt; всплывающие числа, zero-alloc твины, пулинг |
CombatDebugDraw | Shapes | Spatial hash grid, радиусы атак/AOE, swept-траектории снарядов. Тоггл из QC (gm_toggle_debug_draw). Бесценно при отладке сим без арта |
RangeIndicatorView | Shapes | Draw.Ring радиуса атаки/AOE при наведении |
Разделение ролей твинов: LitMotion — code-driven (damage numbers, lerp HP-бара). Feel — event-triggered bursts (вспышка удара, шейк, death-burst), конфигурится в Inspector, не кодом. Не конкурируют, дополняют.
8. DI / Composition (Guildmaster.Game, VContainer)
8.1 RootLifetimeScope (в CoreScene, на всю сессию)
Регистрирует:
IRngService→XorShiftRng(фабрика — суб-сид на бой создаётся в боевом scope).- MessagePipe (
RegisterMessageBroker). IAudioService→UnityAudioService(стаб).SceneLoader(аддитивная загрузка, NGO-aware).GameFlow(Фаза 1 — тонкий: Boot → грузит BattleScene; полный флоу — Фаза 5).
8.2 CombatLifetimeScope (в BattleScene, на один бой)
Child от Root. Регистрирует:
CombatSimulation+ все системы (§5.3) через конструкторы.ICombatContext→CombatContext.IRngServiceбоя (суб-сид =runSeed + battleIndex + attempt, док 7 §6).CombatLoopService(пульс).CombatPresenter.
Сцена выгрузилась → scope задиспозился → бой и его аллокации ушли. Чисто.
8.3 Прочие сервисы
| Класс | Сборка | Роль |
|---|---|---|
GameBootstrap | Game | Энтри: поднять Root scope, загрузить первую сцену |
SceneLoader | Game | LoadBattleAdditive() / UnloadBattle(). Под NGO scene management (Фаза 6) — один шов |
BattleSetupBuilder | Game | Минимальный 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 hash | EditMode | QueryRadius находит ровно тех, кто в радиусе; границы ячеек |
| Снаряд swept | EditMode | Быстрый снаряд не «протыкает» цель между тиками |
| Battle integration | PlayMode | start → 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 (предложение)
- Сборки + сцены — новый asmdef-граф,
CoreScene/BattleScene, пустойRootLifetimeScope, зелёная компиляция и CI. - Core-фундамент —
IRngService+XorShiftRng+ тест детерминизма;SimConstants; базовые интерфейсы команд/событий. - Data-минимум —
StatType(подмн.),StatModifier,StatsConfig, скелетыRelicData/VesselData; интерфейсыIEffectComponent/ScalableValue(без реализаций). - Stats + RuntimeUnit + Factory — стат-формула + тесты.
- DamagePipeline — чистые функции + тесты (рефактор
DamageCalculatorнаIRngService). - Spatial hash + Movement + Targeting(lite) — + тесты.
- AutoAttack + Projectile(swept) — + тесты;
ICombatContext. - CombatSimulation.Tick — собрать системы в фикс. порядок; command/event-queue; battle integration-тест.
- CombatLoopService + DI scopes — реалтайм-пульс, пауза.
- Презентация —
CombatPresenter,UnitView,HealthBarView(Shapes),DamageNumberSpawner(LitMotion),CombatDebugDraw. - DevTools — QC-команды.
- Нетворк-спайк —
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. Сборки под новый граф.