194 lines
12 KiB
Markdown
194 lines
12 KiB
Markdown
# Реализация карт и эффектов
|
||
|
||
Источники: `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<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
|
||
|
||
```csharp
|
||
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. Не тестируй геймплей вручную за пользователя; он проверяет сам.
|