# Реализация карт и эффектов Источники: `content/sets/*.yaml` (`textEn`), `docs/ability-implementation.md`, `server/src/Vanguard.Rules/Engine/Abilities/*`. ## 1. Данные карты - `CardDefinition` — логическая карта по `nameId`: имя, тип, grade, power, shield, critical, trigger, clan, `IsSentinel`, `IsRestraint`, `BoostOverride`, `InterceptOverride`, `CanSnipeBackRow`, тексты. - `CardPrinting` — конкретный принт: `printingId`, `nameId`, редкость, `imageKey`, параллельный арт. - В YAML добавляй/правь карту в `content/sets/{SET}.yaml`; текст — `textEn`. Русский — `content/translations/ru.yaml`, не дублируй в set-файле. - Статы/текст — не в ScriptableObject. SO в клиенте — визуал/конфиг, данные карт приходят из каталога/сервера. - Лимит 4 копий считается по `nameId`; `printingId` — единица коллекции/ принта. Параллельный SP — отдельный принт, но та же логическая карта. ## 2. Один текст — одна реализация Каждая текстовая способность карты должна иметь ровно одну реализацию: 1. `CardScript` — `Abilities` (AUTO), `Activated` (ACT), `Continuous` (CONT); 2. общий движок: Sentinel, Heal/Trigger, Tejas `CanSnipeBackRow`, Boost/Intercept, Restraint; 3. флаг данных, если поведение полностью описывается данными. Если текст есть, а реализации нет — это баг. Если реализация дублирует уже существующий общий механизм — это тоже баг. Реестр: `CardScripts.Default` в `CardScripts.cs` + `CardScriptsBT01.cs` + `CardScriptsTD.cs`. ## 3. AUTO ```csharp public override IEnumerable Abilities { get { yield return new AbilityDefinition { Trigger = AbilityTrigger.OnPlace, Optional = true, Condition = ctx => ctx.Engine.IsRearGuardOf(ctx.Source, ctx.Owner) && ctx.Catalog.CardOf(ctx.Source.PrintingId).Clan == Clan.Kagero, CanPay = ctx => ctx.Engine.CanCounterBlast(ctx.Owner, 2), Pay = ctx => ctx.Engine.CounterBlast(ctx.Owner, 2), Resolve = ctx => { /* менять состояние только через Engine */ }, }; } } ``` Правила: - `Trigger` — только реальное окно события из `AbilityTrigger`; не используй «примерно подходящий» триггер. - Проверяй зону источника: `IsVanguardOf`, `IsRearGuardOf`, `IsInHand`; `OnPlace` не должен срабатывать от VC, если текст требует RC. - `Condition` — проверка события/состояния; `CanPay` — можно ли оплатить; `Pay` — оплата; `Resolve` — эффект. Не смешивай оплату и проверку. - `Optional = true` — движок спросит «активировать/пропустить»; без оплаты эффект не выполняется. - `OnRideOver` — событие старой карты, `Other` — новая карта. `OnBoost` — бустер, `Other` — атакуемый; `OnBoosted` — атакуемый, `Other` — бустер. - `OnAttackHit` разрешается после урона/помещения карты в Damage и после damage-триггеров; не переноси это окно в момент объявления атаки. - `OnOpponentRetire` ищет способности на поле и в руке; не добавляй отдельный обход, если общий движок уже это делает. - AUTO не вызывает UI напрямую; он меняет состояние и добавляет события. Примеры: `ConroeScript` (`OnRideOver` + `AskEmptyRearGuardChoice`), `GancelotScript` (ACT из VC, CB2, soul condition), `BarcgalScript` (optional call + ACT search). ## 4. ACT ```csharp public override IEnumerable Activated { get { yield return new ActivatedAbility { CanActivate = ctx => ctx.Engine.IsVanguardOf(ctx.Source, ctx.Owner) && ctx.Engine.CanCounterBlast(ctx.Owner, 2), Resolve = ctx => ctx.Engine.CounterBlast(ctx.Owner, 2), }; } } ``` - `CanActivate` — только условия; оплата и эффект — в `Resolve` (или `Pay` для AUTO). - Флаги: `RestsSelf`, `RetiresSelf`, `RequiresDiscard`, `FromHand`, `FromDropZone`. - `RestsSelf`/`RetiresSelf` из руки/дропа не активируются, если текст этого не разрешает; движок проверяет это в `AddActivatedCommands`/`Activate`. - `RequiresDiscard` — клиент выбирает карту в руке; сервер получает `DiscardPrintingId` и проверяет наличие. - ACT из руки/дропа попадает в `LegalCommands` только в разрешённой фазе; клиент показывает действие у карты, не отдельным глобальным списком. - Несколько валидных ACT на карте: клиент показывает меню/оверлей выбора, а не отправляет случайную. ## 5. CONT - Сила/крит: `ContinuousAbility` с `Applies`, `PowerBonus`, `CriticalBonus`. - Исключения, не выражаемые бонусом: `RestrictionType` (`CannotAttack`, `CannotBoost`, `CannotStand`, `CannotIntercept`, `CannotBeChosen`) или `CardScript.CanBeBoosted`. - CONT пересчитывается из состояния каждый раз; не храни вычисленную силу как истину. - Источник/срок модификатора обязательны: `AddPower(..., effectId, ModifierDuration...)`. - Для «нельзя поддерживать этот юнит» используй `CanBeBoosted`, а не `CannotBoost` (это разные стороны). ## 6. Выборы и цели Разрешённые helper'ы: - `AskUnitChoice(player, opponent, prompt, slots, onChosen)`; - `AskUnitMultiChoice(...)`; - `AskHandCardChoice(...)`; - `AskEmptyRearGuardChoice(...)`; - `AskDamageChoice(...)` — внутренний для урона; - `SearchDeckToHand/ToCall/ToRide` — поиск с автоматическим исходом: игрок не просматривает колоду вручную; нет карт → shuffle + уведомление; одна → сразу в руку/на поле; несколько → выбор; разные арты одной карты (`printingId`) считаются разными результатами. Не выбирай карту/цель кнопками в UI вместо объекта. Клиент подсвечивает валидные объекты и отправляет индекс/`damageIndex`; сервер проверяет по своему состоянию. ## 7. Стоимости - **Counter Blast**: `CanCounterBlast`, `CounterBlast`; при выборе карт `DamageIndexes` (multi-target), старый `DamageIndex` — совместимость. - **Soul Blast/Charge, Counter Charge**: общие методы `GameEngine`; не дублируй их в скрипте карты. - **Discard**: `RequiresDiscard` + `DiscardPrintingId`; карта того же клана — только если текст/Perfect Guard это требует. - **Rest/Retire self**: флаги `RestsSelf`/`RetiresSelf`. - **Field units**: выбирай через `AskUnitChoice`/`AskUnitMultiChoice`, не списком кнопок. - **[O]** Multi-target Soul Blast и отдельный `CostPaymentOverlay` пока не реализованы; не описывай их как готовые. ## 8. Порядок, отмена, жизненный цикл - Движок запрещает посторонние команды во время `Step.Trigger/Ability/Choice`; каждая цепочка должна завершиться или отменить выбор. - `_resolvingDepth` не даёт новым эффектам исполняться рекурсивно; они попадают в `PendingQueue` и разрешаются после текущего. - Новые срабатывания внутри эффекта не запускай вручную; используй `TriggerFromCard`/`TriggerAll`. - `ModifierDuration` задаёт срок; не удаляй модификаторы вручную, если движок уже чистит их по фазе. - `Optional` без оплаты не должен оставлять состояние pending. - Если эффект просит выбор, `Resolve` должен дойти до `Step.Choice` и вернуться через `ChooseCommand`; не продолжай цепочку до выбора. - Ошибки/некорректные индексы — `CommandResult.Reject`; не бросай исключения наружу из игрового действия. - Не подписывай карты на Unity-события и не храни ссылки на UI в Rules. ## 9. Частые ошибки (из аудита) - `OnPlace` срабатывал от VC у карт, которым нужен RC (Starlight Unicorn, Hungry Dumpty, Clay Doll Mechanic) — проверяй зону. - Joka/Rakshasa получали бонус, находясь в руке — проверяй `IsRearGuardOf`. - Conroe не имел AUTO `OnRideOver` — добавляй оба окна, если текст требует. - Maiden of Libra не проверяла клан авангарда — проверяй условие текста. - Alfred: CONT-запрет поддержки авангарда — через `CanBeBoosted`, не через силу. - Perfect Guard: без оплаты 0 щита и нет nullify; не давай щит «на всякий случай». - Heal: не проси цель, если урон меньше соперника или зона пуста. - Draw: не открывай вторую цель; сразу тяни карту. ## 10. Чеклист новой карты 1. Добавь/исправь YAML, `textEn`; RU — в translations. 2. Разбери текст на AUTO/ACT/CONT/общие механики. 3. Реализуй ровно одну реализацию; проверь зону/условие/optional/стоимость. 4. Добавь тест хотя бы на один триггер/активацию/условие. 5. Проверь весь `CardScripts.Default` на текст без скрипта и дубли. 6. Запусти серверные тесты (на home), `validate`, `translations check`. 7. Для клиентских изменений — EditMode, smoke, при необходимости standalone. 8. Не тестируй геймплей вручную за пользователя; он проверяет сам.