| name | xgaida-x-nixi-combat-sim |
| description | Боевая симуляция Guildmaster: детерминированное ядро на 30 Гц, тик-ордер, система эффектов (producer→consumer, теги, стакинг), авто-атаки и способности, displacement и separation, боевой AI, RuntimeUnit, а также контракт развязки sim→presentation. Зови на любую правку боевой логики и всего под Assets/_Project/Scripts/Combat — даже если слово «симуляция» не звучит. НЕ применять к: определениям контента и баланс-числам в SO (data-authoring), визуальному фидбэку и slowmo (gamefeel-vfx), звуку (audio), тех-доке (tech-scribe). |
Combat Sim — рабочий контур Guildmaster
Этот скилл — процедура, а не справка. Он превращает разрозненные инварианты боя в
чеклист, который прогоняется на КАЖДОЙ боевой задаче. Цель — чтобы симуляция
оставалась детерминированной, headless-тестируемой и развязанной от презентации,
а новый боевой контент ложился в готовые контракты, а не рядом с ними.
Прежде всего: карта боя
Ядро уже построено и покрыто ~29 EditMode-тестами. Ничего не изобретай — читай,
продолжай, встраивайся в существующие швы.
| Что | Где |
|---|
Сердце симуляции (Tick, порядок систем, ComputeChecksum) | Assets/_Project/Scripts/Combat/CombatSimulation.cs |
Константы шага (TickRate=30, TickDelta=1/30, AiTickRate=10) | Assets/_Project/Scripts/Core/Simulation/SimConstants.cs |
Реалтайм-драйвер (accumulator, ЕДИНСТВЕННЫЙ Time.deltaTime) | Assets/_Project/Scripts/Game/Services/CombatLoopService.cs |
| Шов мутации мира | Assets/_Project/Scripts/Combat/ICombatContext.cs |
| Юнит sim-модель (POCO) | Assets/_Project/Scripts/Combat/Units/RuntimeUnit.cs |
| Фабрика/загрузка юнитов | Assets/_Project/Scripts/Combat/Units/RuntimeUnitFactory.cs, EncounterLoader.cs |
| Статы | Assets/_Project/Scripts/Combat/Stats/Stats.cs |
| Система эффектов | Assets/_Project/Scripts/Combat/Effects/EffectSystem.cs |
| Экземпляр эффекта (состояние) | Assets/_Project/Scripts/Combat/Effects/RuntimeEffect.cs |
| Контракты компонентов эффектов | Assets/_Project/Scripts/Combat/Effects/IRuntimeEffectComponent.cs |
| Компоненты-поведения (~20) | Assets/_Project/Scripts/Combat/Effects/Components/*.cs |
| Внутренняя шина событий (producer→consumer) | Assets/_Project/Scripts/Combat/Effects/CombatEvent.cs |
| Displacement / Separation / Movement | Assets/_Project/Scripts/Combat/Systems/*.cs |
| Способности / авто-атаки | Assets/_Project/Scripts/Combat/Abilities/*.cs, Systems/AutoAttackSystem.cs, Systems/AttackTiming.cs |
| Исход боя / стороны (teamId) | Assets/_Project/Scripts/Combat/BattleOutcome.cs |
| Мост sim→presentation | Assets/_Project/Scripts/Presentation/CombatPresenter.cs |
| MessagePipe-события боя | Assets/_Project/Scripts/Presentation/Events/CombatEvents.cs |
Единственный писатель Time.timeScale (GameSpeed/пауза/Cinematic, шов хрономанта) | Assets/_Project/Scripts/Game/Services/TimeScaleService.cs |
| Режиссёр значимости (global-feel) — не combat-sim, потребитель времени | CombatFeelDirector.cs → скилл gamefeel-vfx |
| Combat-тесты (~29, slice-паттерн) | Assets/_Project/Tests/EditMode/Combat/*.cs |
Слои (asmdef) — зависимость строго вниз: Core ← Data ← Combat; Presentation
ссылается на Combat и читает его; Game сшивает всё через DI (VContainer) и
MessagePipe. Guildmaster.Combat НЕ тянет Presentation, MessagePipe, VContainer,
FMOD, движковые presentation-либы. Это и есть headless-ядро.
Четыре правила, нарушение которых = переделка (HARD)
Каждое закрывает конкретный способ, которым бой незаметно загнивает. Пойми «почему» —
тогда не придётся заучивать «нельзя».
-
Бой headless: Guildmaster.Combat не зависит от презентации и движка.
Combat-код видит только Core и Data. Никаких MonoBehaviour, GameObject,
Time.*, VContainer, MessagePipe, FMOD внутри Combat. Единственный Time.deltaTime
во всём бою — в CombatLoopService (реалтайм-драйвер в слое Game).
Почему: как только sim дёргает движок, он перестаёт быть тестируемым без сцены и
воспроизводимым — а на этом стоят и ~29 EditMode-тестов, и кооп-детерминизм.
Граница: презентация СМОТРИТ на sim (подписка, чтение) — это нормально; sim на
презентацию не смотрит НИКОГДА.
-
Детерминизм тика. Внутри Combat запрещены источники недетерминизма:
UnityEngine.Random, System.Random-своя, Time.*, DateTime.Now,
недетерминированный порядок (нестабильная сортировка, обход HashSet/Dictionary
как источник порядка). RNG — только ICombatContext.Rng; шаг времени — только
SimConstants.TickDelta; порядок систем в Tick — фиксирован и менять его нельзя
без явного решения.
Почему: два клиента (host-authoritative кооп) обязаны прогнать одинаковый тик
одинаково; ComputeChecksum() ловит рассинхрон, но только если источник детерминирован.
Инструмент: правишь sim-систему — прогони checksum/replay-проверку (рекомендуется,
см. references/simulation-and-determinism.md).
-
Мир мутируется только через шов. Состояние боя меняется ТОЛЬКО внутри
CombatSimulation.Tick (в фиксированном порядке систем) и через ICombatContext
(DealDamage/Heal/ApplyEffect/Dispel/Displace/SpawnProjectile/…). Никто
снаружи не пишет в RuntimeUnit напрямую; способности и эффекты трогают мир только
через контекст.
Почему: единая точка мутации = единый порядок, единые события, единый checksum.
Прямая запись в обход контекста — это молчаливый источник рассинхрона и багов
«эффект сработал, а событие не поднялось».
-
Эффект = stateless-компонент, состояние — в RuntimeEffect. Новый эффект —
это класс по одному из контрактов IRuntimeEffectComponent (OnApply/OnExpire,
IPeriodicComponent, IReactiveComponent, IPreDamageComponent,
IStackableComponent, IScalablePotency), БЕЗ полей-состояния (компонент шарится
между носителями). Всё изменяемое живёт в RuntimeEffect (RemainingTicks,
Stacks, ScaledPotency[], …). Обязательны теги (EffectTag) и правило стака
(StackRule).
Почему: stateless-компонент безопасно переиспользуется на сотнях юнитов; поле в
компоненте — это общий mutable-стейт и мгновенный кросс-юнит-баг.
Плюс два сквозных инварианта (тоже HARD):
-
Реактив, требующий ДЕЙСТВИЯ носителя, гейтится дееспособностью — и читает СНИМОК на начало тика.
Маркер IRequiresAgencyComponent на компоненте, одна проверка в диспатче (RunPreDamage + реактивная
очередь) по RuntimeUnit.CanActAtTickStart. Помечены «Оплот» и «Изворотливость»; НЕ помечены шипы,
вампиризм, горение — это свойства, а не действия, и под станом они обязаны работать.
Почему снимок: RecomputeControl зовётся синхронно при наложении эффекта, то есть живой CanAct
меняется ПОСРЕДИ тика, а урон проходит раньше фазы эффектов. Живой флаг сделал реакцию зависимой от
места юнита в обходе, и зеркальные команды разошлись (MirrorMatchTests, тик 300).
Почему маркер, а не список эффектов: дееспособность уже имеет владельца (CanAct), список стал бы
вторым и разошёлся бы на первом новом контроле.
Ловушка: не помечать то, чья недееспособность вызвана СОБСТВЕННОЙ механикой. Комбо монаха сделано
смещением, смещение снимает дееспособность у летящего — гейт отменял фазы своей же способности.
-
Состояние, которое читают решения внутри тика, обязано быть снимком. Это тот же закон, что двухфазность
движения и PreviousPosition: список эффектов и флаги контроля меняются немедленно, поэтому любое
«посмотреть, что сейчас на юните» посреди тика зависит от порядка обхода. Признак дефекта — зеркальные
тесты падают на конкретном тике, а не «иногда».
-
Стороны — только teamId (int). Никакой «команды игрока» в бою. Исход —
«победила команда N» (BattleOutcome), сторон может быть >2 (шов под PvP).
Принадлежность игрока живёт выше боя (ILocalPlayer.Team, _localViewerTeam в презентере).
-
Значения — из данных/конфигов, не хардкод. Тюнеры боя — SimTuning (dev
gm_*), статы — через StatsConfig/Override, feel — CombatFeelConfig. Магические
числа в sim-коде = двойная работа при балансе. (Как в UI: не хардкодить, чтобы не
переделывать.)
Граница с data-authoring (эффект живёт на два дома)
Эффект — стык двух скиллов, режем чётко:
- combat-sim владеет ПОВЕДЕНИЕМ: компонент-логика (
IRuntimeEffectComponent),
тик, стакинг-механика, реактивы, порядок применения, CreateRuntime для системных
эффектов в коде.
- data-authoring владеет ОПРЕДЕЛЕНИЕМ:
EffectData SO, id (domain.name),
баланс-цифры, [SerializeReference]-состав компонентов, loc-ключи.
Задача «новый эффект» обычно трогает оба: логику пишу здесь, определение/баланс — по
контуру data-authoring. На стыке — взаимная ссылка «см. другой скилл», а не спор за задачу.
Развязка sim → presentation (шов, не визуал)
Скилл держит КОНТРАКТ развязки, но не визуальный полиш (его правит Макс):
- sim поднимает C#-события (
OnDamageDealt, OnUnitDied, …); CombatPresenter
— единственный, кто на них подписан со стороны презентации, спавнит views и
ретранслирует в MessagePipe (CombatEvents.cs). Audio/UI/feel слушают MessagePipe,
не sim.
- Джус и фидбэк — контур
gamefeel-vfx, не combat-sim. Global-feel (kill-slowmo, shake,
финишер) в CombatFeelDirector, per-hit фидбэк в презентере — их держит скилл gamefeel-vfx.
combat-sim владеет только боевым ВРЕМЕНЕМ (TimeScaleService), которое джус потребляет.
Time.timeScale пишет только TimeScaleService. Он же не трогает sim-время
(ElapsedSeconds = currentTick * TickDelta): slowmo/пауза меняют реальное время на
тик, но не детерминизм.
Детали контракта и антипаттерны — references/presentation-seam.md.
Как я авторю боевой код — ГИБРИД (файл + проверка через MCP)
- Пишу файлы напрямую (
Write/Edit) — контролирую код и его слой.
- После C#-правок —
read_console (Unity MCP): дождаться компиляции, ноль ошибок,
только потом использовать новые типы.
- Перед «готово» —
run_tests по Combat-подмножеству (EditMode/Combat) — быстро;
полный прогон отдаём CI.
- Проверяю слой: новый код в
Combat не тянет запрещённые зависимости (правило 1).
Чеклист сдачи боевой задачи
Прогнать перед тем, как сказать «готово»:
Справочные файлы (читать по надобности)
references/simulation-and-determinism.md — тик, фиксированный порядок систем,
ICombatContext, SimConstants, CombatLoopService, детерминизм, checksum/replay.
Читать перед правкой любой sim-системы.
references/effects-and-events.md — контракт эффектов, компоненты, RuntimeEffect,
теги/стакинг, шина CombatEvent, реактивы, displacement-как-эффект. Читать перед
созданием эффекта.
references/presentation-seam.md — развязка sim→presentation, MessagePipe-события,
CombatPresenter, TimeScaleService (боевое время), граница с джусом/фидбэком
(gamefeel-vfx).