VanguardReMaster/.opencode/skills/vanguard-remaster/card-implementation.md

12 KiB
Raw Blame History

Реализация карт и эффектов

Источники: 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

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. Чеклист новой карты

  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. Не тестируй геймплей вручную за пользователя; он проверяет сам.