Дизайним абилки как в X-COM

Через абилки в X-COM реализована большая часть взаимодействия оперативников с окружением во время миссий. Стрельба, перезарядка, навыки, активация предметов, важных для цели миссии, и даже открытие дверей сделано через абилки. Важно при дизайне вашей игры учесть и реализовать только то, что вам нужно для интересного геймплея, и не гнаться за всей возможной сложностью комбинации механик. В этой статье я затрону разные аспекты дизайна данных для абилок и возможные пути реализации этих аспектов в коде. Описанный опыт является субъективным, и путей дизайна и реализации одной и той же механики больше, чем один. Одна из целей этого дизайна — сделать насколько можно гибкую систему абилок, которая потребует минимум дополнительной разработки от программистов и даст гейм-дизайнерам возможность самостоятельно реализовать интересные игровые механики.

Отступление

Я разработчик с 16-летним опытом, который участвовал в разработке различных игр, включая мобильный клон X-COM. Помимо игр, я разрабатываю и продаю компоненты и инструменты для игр. Одним из таких инструментов является редактор игровых данных Charon, на основе которого в этой статье будут приведены примеры. Это не означает, что без этого редактора статья бесполезна, так как аналогичный результат вы можете получить, используя JSON/XML/YAML файлы и любой язык программирования для реализации.

Анатомия абилки

Сама по себе абилка представляет действие, которое приводит к эффектам на цели. У этого действия могут быть условия применения, разные режимы активации и разные эффекты для применяющего и цели.

Способом активации абилки может быть указание себя в качестве цели (Self), указание на некую игровую сущность (Target), указание точки на игровом поле (Ground) или событие (Triggered).

У абилки есть эффекты, каждый из которых указывает на того, на кого он применяется во время активации абилки. Он может применяться к инициатору действия (Self), на указанную цель (Target) или на цель в зоне поражения (Impact) для Ground абилок. К примеру, выстрел RPG может наносить урон всем, кто попал в зону поражения (Effect: Impact → -Damage / 2), и дополнительный урон по тому, в кого он нацелен (Effect: Target → -Damage).

X-COM похорошел за года, особенно на изображении справа

X-COM похорошел за года, особенно на изображении справа

Эффект применяется на игровые сущности и остается на них, если не указано, что он временный (см. ниже). Для Ranged Attack это будет урон, который вычитается из HP. После применения эффекта факт его применения записывается в игровую сущность и сохраняется до конца боя.

Для более сложных механик эффекты можно расширить дополнительными полями и поведением, и получить яды, бонусу, штрафы и статус-эффекты.

Только для реализации атаки вам понадобится в X-COM-подобной игре иметь игровые сущности с такими атрибутами, как HP, Action Points, Ammo In Magazine. Почему не «персонажи», а «игровые сущности»? Я использую этот термин для более широкого охвата того, на кого/что может быть нацелена абилка, например, персонажи, бочки, двери, мины, турели, которые тоже могут иметь HP, очки действия, патроны и т.д.

	public class GameEntity
	{
		public int HealthPoints; 
		public int MaxHealthPoints; 
		public int ActionPoints;
      
        // Only one ranged weapon is allowed in X-COM
        // So the magazine can be declared at the character level
		public int AmmoInMagazine;

		public readonly WeaponInstance RangedWeapon;

		public readonly List Effects;
	}

	public class EffectInstance
	{
		// reference to Effect's template from game data
		public readonly Effect Effect;
		// the amount by which the attribute has changed
		public readonly double Change;
		// how much time is left, -1 is indicator that effect has no expiration
		public readonly int Duration;
	}

Атака оружием [Activation → Target]

Для примера рассмотрим такую способность, как Ranged Attack, которая состоит из одного эффекта — HP = -context.Self.RangedWeapon.Damage. Конечно, чтобы эту способность не использовали много раз подряд за один ход, ей нужна «цена» (Cost) использования в каком-либо восполняемом ресурсе. Этим ресурсом будут очки действия (Action Points) и патроны в магазине (Ammo In Magazine).

022032f25dd4b521c8d549d7a65a2e8a.png

Формулы

Что такое context.Self.RangedWeapon.Damage? Это формула или выражение, без них сделать систему гибкой будет сложнее. Гейм-дизайнеры довольно быстро учатся писать простые формулы и выражения, особенно если им предоставить документацию и список готовых выражений. Не скупитесь на документацию формул.

Перезарядка [Activation → Self]

В пару к Ranged Attack дизайним Reload, которая будет восполнять патроны в магазине за счет одного очка действия. Эффектом перезарядки будет восполнение патронов в магазине до максимальной вместимости магазина (Magazine Capacity). Максимальная вместимость магазина — это еще один атрибут игровой сущности, на основе которого можно создать навыки/бонусы/штрафы к его вместимости. А через его временное обнуление можно реализовать запрет на стрельбу и подобные способности.

86ba0a91241fe05ca5d41edfccb3136c.png

Бросок гранаты [Activation → Ground]

Дополнением к атаке и перезарядке будет способность Throw Grenade, тип цели у нее — Ground, цена — в очках действия. Так как эта способность в игре появляется только когда оперативник берет с собой расходник Grenade, то мы ограничим ее применение одним разом за бой. Это будет еще один параметр способности «количество использований» (Uses Per Battle), который подходит для расходных материалов или чрезвычайно мощных способностей. У этой способности будет еще один новый параметр «зона поражения» (Impact Zone), который определяет форму и размер зоны поражения, в которой будут применены эффекты. Этот параметр можно выразить через построение геометрической фигуры — Impact Zone = shape.Sphere(min: 0, max: 10) или через список заранее определенных зон поражения, из которых можно выбирать.

26b22ceb9373cb1129f5e259acc26eec.png

На базе этих трех способностей можно собрать бедную на механики тактическую RTS-игру. Для более сложных механик потребуется расширить как способности, так и эффекты.

Мета игра

К вопросу: откуда вообще берутся абилки у оперативника? Они поступают с предметами, которые надеты на куклу оперативника на этапе подготовки к миссии.

699fd186085f29ed57a79cbe7c00455a.png

У предмета может быть список абилок, которые он предоставляет. К примеру, у винтовки это будет Ranged Attack и Reload, у гранаты — Throw Grenade. В дополнение к этому, навыки, полученные за уровень, тоже могут быть предметами, которые надеты на оперативника и предоставляют абилки.

Уже в бою абилки со всех этих предметов и псевдо-предметов собираются, сортируются и предоставляются внизу экрана для игрока.

Всё это часть мета-игры и выходит за рамки этой статьи. Если вам интересно, как проектировать и реализовывать мета-игру, то пишите в комментариях, что именно вас интересует, и я добавлю это в план статей.

Cooldown и Warmup

Некоторые абилки бывают настолько мощны, что для баланса игры их применение требует ограничений. Одно из таких ограничений — это «откат» (Cooldown). Это время, обычно в ходах, через которое можно повторно активировать абилку. Где Cooldown → 0, это значит, что активировать можно сразу, а Cooldown → 1 — только на следующий ход, Cooldown → 2 — после следующего хода и т.д.

Другое ограничение — это прогрев (Warmup). Это время, обычно в ходах, от начала боя, через которое абилку можно будет активировать в первый раз. Такое ограничение устанавливают на чрезмерно эффективные абилки, использование которых на первом ходе даёт слишком большое преимущество.

Use Condition / Target Condition / Effect Apply Condition

Иногда абилкам требуется дополнительное условие для активации, не только стоимость, тогда их можно расширить кастомным условием применения (Use Condition).

К примеру: Fire Rocket не может быть использована, если персонаж двигался.

Аналогично можно ограничить применимость абилки на цель и добавить условие применение на цель (Target Condition).

К примеру: Лечащая абилка может проверять, что цель — союзник, Executioner требует, чтобы цель имела меньше 50% HP.

Для абилок, действующих на площадь, важно, чтобы тип клетки подходил или был достижим. Тема поиска пути и вычисления видимости обширна и лучше оптимизировать эту часть игры в коде, применяя предикаты цели только к ограниченному списку игровых сущностей, которые видимы и достижимы.

Для эффектов можно добавлять условия их применения на цель через Effect Apply Condition.

К примеру: HEAT Ammo наносит двойной урон роботам.

Application Time → Instant + Duration & Duration Unit

Добавив в эффекты длительность (Duration) и единицы отсчета длительности (Duration Unit), можно получить временные дебафы и штрафы. В целом, временный урон будет выглядеть странно, а вот временная броня или HP вполне естественны. Для отмены эффекта понадобится запоминать, насколько изменился атрибут при его применении. Даже если это был множитель, вам всё равно придётся вернуть обратно абсолютное значение. Для процентных характеристик типа точности или вероятности уворота вам, возможно, придётся пересчитывать новое значение по текущим активным эффектам.

Application Time → Instant + Status Effect

Расширив эффекты с помощью поля Status Effect, можно ввести механику статусных эффектов. Статусные эффекты — это флаги на игровой сущности, меняющие её поведение, не в количественных параметрах. К примеру, «заморозка», уменьшающая дальность передвижения, «оглушение», отключающее обновление очков действия в начале хода, и т.д. Для «вдохновения» можно посмотреть Wiki по Dota 2 или League of Legends.

Эффект, который накладывает флаг статусного эффекта, обычно имеет длительность в ходах или времени. Через указанное Duration эффект удаляется с игровой сущности, и флаги статусных эффектов пересчитываются. Эффект можно не удалять, а только помечать как неактивный или пропускать при Duration = 0, т.к. потом его можно использовать для подсчёта статистики и достижений.

Application Time → Delayed

Эффекты с отложенным временем применения, которые можно использовать для стеков, проклятий и подобных механик.

Это из X-COM, точно

Это из X-COM, точно

Application Time → Every Duration Unit

Немного изменив поведение Duration и Duration Unit, можно получить яды, горение и другие механики урона, зависящего от ходов/времени. Также на этом же механизме можно построить проклятия, т.к. Duration Unit может быть и Shot, Got Hit, Got Miss, Reload и т.д. Каждый раз, когда происходит событие Duration Unit такого эффекта, из Duration вычитается единица, создаётся дубликат этого эффекта с изменённым Application Time на Instant и применяется к сущности, на которую был наложен оригинальный эффект.

f84a00c94fdc9c8808025b86f999b008.png

Бонусы, зависящие от событий/триггеров, лучше делать через пассивные способности (см. ниже).

Value Application Operation → Add, Multiply, Set

Самый простой и логичный способ применения значения эффекта к игровой сущности — это прибавление к текущему значению атрибута значения из эффекта. Этого может быть достаточно для урона/лечения и простых бонусов, например, +2 к точности. Для более масштабируемых бонусов лучше подойдут проценты, для которых требуется другой метод применения значения — умножение (Multiply). С помощью операции Multiply можно реализовать проценты, как множители, так и делители. Чтобы в вашей игре не появлялись магические константы типа 9999 урона и т.д., когда нужно убить персонажа, используйте операцию Set для установки атрибута сущности в определённое значение. Это позволит сбрасывать значения атрибутов (например, HP → 0 или HPMax HP), не привлекая внимания санитаров.

Формула расчёта финального значения атрибута:

  • Add → Attribute = Attribute + Effect Value

  • Multiply → Attribute = Attribute * Effect Value

  • Set → Attribute = Effect Value

Value Reduction Attribute

Для реализации резистов непосредственно из абилок можно добавить еще одно поле в эффекты — атрибут уменьшение значения эффекта (Value Reduction Attribute), указывающее на атрибут игровой сущности, который является множителем значения эффекта, тем самым снижая или увеличивая урон/лечение, которое повлечёт за собой эффект.

Value Reduction Attribute — это коэффициент, где 1.0 соответствует 100% резисту урону, 0.0 — 0% резисту урону, а -0.1 обозначает усиление на 10%.

Даже лечение стоит реализовывать через Value Reduction Attribute и предусматривать сопротивление к лечению, что позволит реализовать запрет на лечение или снизить его эффективность. К примеру, можно создать Arrow of Ilmater из BG3, которая отключает лечение на один ход.

Формула расчёта финального значения атрибута:

  • Add → Attribute = Attribute + Effect Value * (1 — min (1, Value Reduction Attribute))

  • Multiply → Attribute = Attribute + (Attribute * Effect Value — Attribute) * (1 — min (1, Value Reduction Attribute))

  • Set → Attribute = Effect Value

Пассивки и реакции

Некоторые абилки предоставляют постоянный бонус в начале боя и не требуют активации игроком. Другие абилки дают свой бонус при определенных событиях или условиях и должны быть активированы не в ход игрока. Такие абилки называются пассивками и реакциями. Для их реализации нужен явный запрет на активацию игроком в коде и механизм событий и триггеров, а в дизайне — дополнительное поле с указанием триггера. Таким триггером может быть начало боя, начало раунда, получение/нанесение урона, промах, перезарядка, движение и т.д. Каждый триггер, который вы реализуете, увеличит комбинаторное многообразие механик для геймдизайнера. Эта же логика может быть использована для отсчета Duration Unit на эффектах.

В зависимости от триггера контекст того, кто есть Self и кто Target, может меняться, и это надо тщательно документировать. К примеру, для триггера «Was Hit» непонятно, кто будет для эффектов Self — стреляющий или тот, в кого стреляли?

Желательно делать полные контексты для триггеров со всеми участниками события, чтобы можно было реализовывать более сложные механики, как возврат урона, ответные дебафы и т.д.

Сложные и уникальные абилки — Overwatch, Mines, Mind Control

У вас не получится сделать всё в игровых данных, всё равно придётся что-то реализовывать в коде, т.к. цена гибкости для таких абилок будет игровые данные, раздутые ненужными полями. Старайтесь найти баланс между гибкостью и хардкодом.

Например, как были сделаны мины в одной из наших игр. Установка мины — это Ground абилка, у которой нет эффектов. В коде при активации в указанной точке карты создается игровая сущность Mine, и она входит в команду активировавшего игрока. У мины с момента «рождения» есть 4 абилки: Ranged Attack, Overwatch, Throw Grenade и Martyr. Последняя абилка в разных играх называется по-разному, но суть её в том, что персонаж, умирая, бросает под себя гранату. Итак, наша мина после создания встаёт на Overwatch с радиусом, совпадающим с радиусом взрыва гранаты. При пробегании врага через Overwatch, мина стреляет в него и наносит себе (!) фатальный урон, после мина (!) достаёт гранату и кидает под себя. Вуаля.

Гейм-дизайнеры в одной из наших игр создали самостоятельно около 450 абилок (включая скучные на подобие +5% к меткости) и только 50 потребовали дополнительного кода.

Финальная схема абилки

{
	'id': string;
	'activationType': 'Self' | 'Ground' | 'Triggered';
	'trigger': 'BeginningOfTurn' | 'EndOfTurn' | '...';
	'usesPerBattle': number;
	'cost': [
		{
			'resource': string;
			'quantity': number;
		}
	];
	'cooldown': number;
	'warmup': number;
	'effects': [
		{
			'subject': 'Self' | 'Target' | 'Impact';
			'affectedAttribute': string;
			'applicationTime': 'Instance' | 'Delayed' | 'EveryDurationUnit';
			'duration': number | -1;
			'durationUnit': 'BeginningOfTurn' | 'EndOfTurn' | '...';
			'value': string;
			'valueApplicationOperation': 'Add' | 'Multiply' | 'Set';
			'valueReductionAttribute': string;
			'statusEffect': 'Poisoned' | 'Supressed' | '...';
		}
	];
}

JSON для импорта в Charon

  1. Копируете JSON в буффер обмена.

  2. Открываете Dashboard своего проекта.

  3. Нажимаете Import Documents.

  4. From → Clipboard, нажимаете Next.

  5. Mode → Create and Update, нажимаете Next.

  6. Collections → (all), нажимаете Next.

  7. Готово.

{
	"Collections": {
		"Schema": [
			{
				"Id": "65425b9cef43860001fbaea0",
				"Name": "AbilityCost",
				"DisplayName": "Ability Cost",
				"Type": 1,
				"Description": "",
				"IdGenerator": 3,
				"Specification": "displayTextTemplate=%7BResource%7D%20x%7BQuantity%7D&icon=emoji%2Fcoin",
				"Properties": [
					{
						"Id": "65425b9cef43860001fbaea1",
						"Name": "Id",
						"DisplayName": "Id",
						"Description": "",
						"DataType": 0,
						"DefaultValue": "",
						"Uniqueness": 1,
						"Requirement": 3,
						"ReferenceType": null,
						"Size": 0,
						"Specification": ""
					},
					{
						"Id": "65426bb3983a36374c916b26",
						"Name": "Resource",
						"DisplayName": "Resource",
						"Description": "",
						"DataType": 28,
						"DefaultValue": null,
						"Uniqueness": 0,
						"Requirement": 2,
						"ReferenceType": {
							"Id": "65425db4ef43860001fbaead",
							"DisplayName": "Game Entity Attribute"
						},
						"Size": 0,
						"Specification": ""
					},
					{
						"Id": "65425b9cef43860001fbaea3",
						"Name": "Quantity",
						"DisplayName": "Quantity",
						"Description": "",
						"DataType": 13,
						"DefaultValue": "1",
						"Uniqueness": 0,
						"Requirement": 2,
						"ReferenceType": null,
						"Size": 4,
						"Specification": null
					}
				]
			},
			{
				"Id": "65425db4ef43860001fbaead",
				"Name": "GameEntityAttribute",
				"DisplayName": "Game Entity Attribute",
				"Type": 0,
				"Description": "",
				"IdGenerator": 0,
				"Specification": "icon=emoji%2Finput_numbers",
				"Properties": [
					{
						"Id": "65425db4ef43860001fbaeae",
						"Name": "Id",
						"DisplayName": "Id",
						"Description": "",
						"DataType": 0,
						"DefaultValue": "",
						"Uniqueness": 1,
						"Requirement": 3,
						"ReferenceType": null,
						"Size": 0,
						"Specification": ""
					},
					{
						"Id": "65425db4ef43860001fbaeaf",
						"Name": "Name",
						"DisplayName": "Name",
						"Description": "",
						"DataType": 0,
						"DefaultValue": null,
						"Uniqueness": 0,
						"Requirement": 2,
						"ReferenceType": null,
						"Size": 0,
						"Specification": ""
					}
				]
			},
			{
				"Id": "65425e45ef43860001fbaec0",
				"Name": "Ability",
				"DisplayName": "Ability",
				"Type": 0,
				"Description": "",
				"IdGenerator": 0,
				"Specification": "icon=emoji%2Fcrossed_swords",
				"Properties": [
					{
						"Id": "65425e45ef43860001fbaec1",
						"Name": "Id",
						"DisplayName": "Id",
						"Description": "",
						"DataType": 0,
						"DefaultValue": "",
						"Uniqueness": 1,
						"Requirement": 3,
						"ReferenceType": null,
						"Size": 0,
						"Specification": ""
					},
					{
						"Id": "65426b68983a36374c916b1e",
						"Name": "ActivationType",
						"DisplayName": "Activation Type",
						"Description": "",
						"DataType": 18,
						"DefaultValue": "0",
						"Uniqueness": 0,
						"Requirement": 2,
						"ReferenceType": null,
						"Size": 4,
						"Specification": "pick.Self=0&pick.Target=1&pick.Ground=2&pick.Triggered=3"
					},
					{
						"Id": "65438f95983a366cd4839314",
						"Name": "Trigger",
						"DisplayName": "Trigger",
						"Description": "",
						"DataType": 19,
						"DefaultValue": "0",
						"Uniqueness": 0,
						"Requirement": 2,
						"ReferenceType": null,
						"Size": 4,
						"Specification": "pick.BeginningOfTurn=1&pick.Reload=5&pick.Move=6&pick.BeforeAttack=7&pick.AfterAttack=8&pick.AttackHit=9&pick.AttackMiss=10&pick.BeginningOfBattle=4&pick.EndOfTurn=2&pick.EndOfBattle=8&pick.BeforeWasAttacked=11&pick.AfterWasAttacked=12&pick.WasHit=13&pick.WasMissed=14&pick.WasHealed=15&pick.BeginningOfRound=16&pick.EndOfRound=17&pick.None=0"
					},
					{
						"Id": "65439447983a366cd483932d",
						"Name": "UsesPerBattle",
						"DisplayName": "Uses Per Battle",
						"Description": null,
						"DataType": 13,
						"DefaultValue": "0",
						"Uniqueness": 0,
						"Requirement": 2,
						"ReferenceType": null,
						"Size": 4,
						"Specification": null
					},
					{
						"Id": "65425e45ef43860001fbaec2",
						"Name": "Cost",
						"DisplayName": "Cost",
						"Description": "",
						"DataType": 23,
						"DefaultValue": null,
						"Uniqueness": 0,
						"Requirement": 2,
						"ReferenceType": {
							"Id": "65425b9cef43860001fbaea0",
							"DisplayName": "Ability Cost"
						},
						"Size": 0,
						"Specification": ""
					},
					{
						"Id": "6543892a983a366cd48392ff",
						"Name": "Cooldown",
						"DisplayName": "Cooldown",
						"Description": "",
						"DataType": 13,
						"DefaultValue": "0",
						"Uniqueness": 0,
						"Requirement": 2,
						"ReferenceType": null,
						"Size": 4,
						"Specification": null
					},
					{
						"Id": "6543892a983a366cd4839301",
						"Name": "Warmup",
						"DisplayName": "Warmup",
						"Description": "",
						"DataType": 13,
						"DefaultValue": "0",
						"Uniqueness": 0,
						"Requirement": 2,
						"ReferenceType": null,
						"Size": 4,
						"Specification": null
					},
					{
						"Id": "65426b68983a36374c916b20",
						"Name": "Effects",
						"DisplayName": "Effects",
						"Description": "",
						"DataType": 23,
						"DefaultValue": null,
						"Uniqueness": 0,
						"Requirement": 2,
						"ReferenceType": {
							"Id": "65426974983a36374c916b12",
							"DisplayName": "Effect"
						},
						"Size": 0,
						"Specification": ""
					}
				]
			},
			{
				"Id": "65426974983a36374c916b12",
				"Name": "Effect",
				"DisplayName": "Effect",
				"Type": 1,
				"Description": "",
				"IdGenerator": 3,
				"Specification": "displayTextTemplate=%7BSubject%7D%20%E2%86%92%20%7BAffectedAttribute%7D%20%3D%20%7BValue%7D%20%7BStatusEffect%20%21%3D%20null%20%3F%20%22%2B%22%20%2B%20StatusEffect%20%3A%20%22%22%7D&icon=emoji%2Fcollision",
				"Properties": [
					{
						"Id": "65426974983a36374c916b13",
						"Name": "Id",
						"DisplayName": "Id",
						"Description": "",
						"DataType": 0,
						"DefaultValue": "",
						"Uniqueness": 1,
						"Requirement": 3,
						"ReferenceType": null,
						"Size": 0,
						"Specification": ""
					},
					{
						"Id": "654277a8983a36374c916b44",
						"Name": "Subject",
						"DisplayName": "Subject",
						"Description": "",
						"DataType": 18,
						"DefaultValue": "0",
						"Uniqueness": 0,
						"Requirement": 2,
						"ReferenceType": null,
						"Size": 4,
						"Specification": "pick.Self=0&pick.Target=1&pick.Impact=2"
					},
					{
						"Id": "65426a50983a36374c916b18",
						"Name": "AffectedAttribute",
						"DisplayName": "Affected Attribute",
						"Description": "",
						"DataType": 28,
						"DefaultValue": null,
						"Uniqueness": 0,
						"Requirement": 2,
						"ReferenceType": {
							"Id": "65425db4ef43860001fbaead",
							"DisplayName": "Game Entity Attribute"
						},
						"Size": 0,
						"Specification": ""
					},
					{
						"Id": "65438f21983a366cd4839306",
						"Name": "ApplicationTime",
						"DisplayName": "Application Time",
						"Description": "",
						"DataType": 18,
						"DefaultValue": "0",
						"Uniqueness": 0,
						"Requirement": 2,
						"ReferenceType": null,
						"Size": 4,
						"Specification": "pick.Instant=0&pick.Delayed=1&pick.EveryDurationUnit=2"
					},
					{
						"Id": "65438f21983a366cd4839308",
						"Name": "Duration",
						"DisplayName": "Duration",
						"Description": "",
						"DataType": 13,
						"DefaultValue": "-1",
						"Uniqueness": 0,
						"Requirement": 2,
						"ReferenceType": null,
						"Size": 4,
						"Specification": null
					},
					{
						"Id": "65438f21983a366cd483930a",
						"Name": "DurationUnit",
						"DisplayName": "Duration Unit",
						"Description": "",
						"DataType": 18,
						"DefaultValue": "None",
						"Uniqueness": 0,
						"Requirement": 2,
						"ReferenceType": null,
						"Size": 4,
						"Specification": "pick.BeginningOfTurn=1&pick.Reload=5&pick.Move=6&pick.BeforeAttack=7&pick.AfterAttack=8&pick.AttackHit=9&pick.AttackMiss=10&pick.BeginningOfBattle=4&pick.EndOfTurn=2&pick.EndOfBattle=8&pick.BeforeWasAttacked=11&pick.AfterWasAttacked=12&pick.WasHit=13&pick.WasMissed=14&pick.WasHealed=15&pick.BeginningOfRound=16&pick.EndOfRound=17&pick.None=0"
					},
					{
						"Id": "65426a50983a36374c916b1a",
						"Name": "Value",
						"DisplayName": "Value",
						"Description": "",
						"DataType": 35,
						"DefaultValue": null,
						"Uniqueness": 0,
						"Requirement": 2,
						"ReferenceType": null,
						"Size": 0,
						"Specification": "param.context=AbilityApplyContext&resultType=System.Int32"
					},
					{
						"Id": "65438f21983a366cd483930c",
						"Name": "ValueApplicationOperation",
						"DisplayName": "Value Application Operation",
						"Description": "",
						"DataType": 18,
						"DefaultValue": "0",
						"Uniqueness": 0,
						"Requirement": 2,
						"ReferenceType": null,
						"Size": 4,
						"Specification": "pick.Add=0&pick.Multiply=1&pick.Set=2"
					},
					{
						"Id": "65438f21983a366cd483930e",
						"Name": "ValueReductionAttribute",
						"DisplayName": "Value Reduction Attribute",
						"Description": "",
						"DataType": 28,
						"DefaultValue": null,
						"Uniqueness": 0,
						"Requirement": 0,
						"ReferenceType": {
							"Id": "65425db4ef43860001fbaead",
							"DisplayName": "Game Entity Attribute"
						},
						"Size": 0,
						"Specification": ""
					},
					{
						"Id": "654390f7983a366cd4839323",
						"Name": "StatusEffect",
						"DisplayName": "Status Effect",
						"Description": null,
						"DataType": 18,
						"DefaultValue": "0",
						"Uniqueness": 0,
						"Requirement": 2,
						"ReferenceType": null,
						"Size": 4,
						"Specification": "pick.None=0&pick.Supressed=1&pick.Stasis=2&pick.ChryssalidPoison=3&pick.Panic=4&pick.Disoriented=5&pick.BleedingOut=6&pick.AcidBurn=7&pick.Burning=8&pick.Poisoned=9&pick.Stunned=10&pick.Unconscious=11"
					}
				]
			}
		]
	}
}

Реализация в коде

Хранение эффектов на игровой сущности

Хотя это кажется расточительным — хранить все экземпляры урона/бонусов на игровой сущности до конца боя, это может быть полезно для подсчёта игровой аналитики, выдачи ачивок, расчёта лута и т. д. Разделите в коде список эффектов на активные и неактивные, и вы сократите вычислительные ресурсы на их обход и обновление тикающих эффектов. Добавьте простой фильтр Блума и не проходите список эффектов для обновления Duration лишний раз.

Алгоритм применения абилки на цель

  1. Проверить, что инициирующий жив.

  2. Проверить, что инициирующий не имеет запрещающих статус-эффектов (Stun и т. п.).

  3. Проверить, что остались «заряды» в абилке.

  4. Проверить Use Condition на абилке.

  5. Проверить Cooldown/Warmup.

  6. Проверить, что цель валидна, достижима и соответствует Target Condition.

  7. Проверить, что есть все ресурсы из Cost.

  8. Списать Cost из ресурсов.

  9. Проверить, что инициирующий жив №2.

  10. Проверить, что инициирующий не имеет запрещающих статус-эффектов №2 (Stun и т. п.).

  11. Если это не пассивка, то прервать Overwatch.

  12. Применить эффекты на себя, проверяя Effect Apply Condition.

  13. Проверить, что инициирующий жив №3.

  14. Проверить, что инициирующий не имеет запрещающих статус-эффектов №3 (Stun и т. п.).

  15. Если это атака и попадает по цели, то:

    1. Применить эффекты на цель, проверяя Effect Apply Condition.

    2. Применить эффекты на зону поражения, проверяя Effect Apply Condition.

  16. Списать «заряды» использования с абилки.

  17. Установить Cooldown на абилке, если он не 0.

Кода с реализацией не будет, т. к. нет универсального варианта который подойдет для каждой.

© Habrahabr.ru