12 KiB
12 KiB
Реализация карт и эффектов
Источники: 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. Один текст — одна реализация
Каждая текстовая способность карты должна иметь ровно одну реализацию:
CardScript—Abilities(AUTO),Activated(ACT),Continuous(CONT);- общий движок: Sentinel, Heal/Trigger, Tejas
CanSnipeBackRow, Boost/Intercept, Restraint; - флаг данных, если поведение полностью описывается данными.
Если текст есть, а реализации нет — это баг. Если реализация дублирует уже
существующий общий механизм — это тоже баг. Реестр:
CardScripts.Default в CardScripts.cs + CardScriptsBT01.cs +
CardScriptsTD.cs.
3. AUTO
public override IEnumerable<AbilityDefinition> 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
public override IEnumerable<ActivatedAbility> 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. Чеклист новой карты
- Добавь/исправь YAML,
textEn; RU — в translations. - Разбери текст на AUTO/ACT/CONT/общие механики.
- Реализуй ровно одну реализацию; проверь зону/условие/optional/стоимость.
- Добавь тест хотя бы на один триггер/активацию/условие.
- Проверь весь
CardScripts.Defaultна текст без скрипта и дубли. - Запусти серверные тесты (на home),
validate,translations check. - Для клиентских изменений — EditMode, smoke, при необходимости standalone.
- Не тестируй геймплей вручную за пользователя; он проверяет сам.