Skip to content

Абилки

Абилки - это вторая по фундаментальности вещь после энтитей. Если энтити, это тот, кто действует, то абилка это то, как он действует.

В сущности, весь бой происходит из взаимного обмена абилками, по крайней мере, пока я не добавил перемещение. Абилки подразделяются на атакующие и защитные абилки.

  • Атакующие абилки - это те, что вы используете напрямую в бою. То есть вы применяете их на врагах.
  • Защитные абилки - это те, что вы используете, защищаясь от вражеских абилок. Они разделяются по эффекту, накладываемому на врага в случае успеха, а так же еще парой особенностей, про которые мы еще поговорим.

Таблица абилок (AbilitiesTable)

Таблица абилок присутствует во многих местах, помимо персонажа - она есть везде, где есть что-то, что дает дополнительные абилки или имеет собственные (в данный момент это только Entity). Обычно она лежит в поле abilities. Таким образом, если мы хотим получить к ней доступ, то это обычно будет foo.abilities.

Мы можем добавлять новые абилки через добавление новых частей тела, статусов или предметов. С точки зрения активации между ними нет никакой разницы. В момент активации use_ability проверяются все эти места на наличие абилок, и наличие ее хоть где-то делает ее доступной для применения (проверка на тэги делается уже потом). У таблицы абилок всего 2 поля:

  • abilities_list: list[AbilityContainer]
  • def_abilities_list: list[DefAbilityContainer]

Это чисто технический класс, но он важен для понимания, что если вы хотите достучаться до кулдауна нулевой абилки того или иного персонажа, вам придется сделать что-то такое: entity.abilities.abilities_list[0].ability.cooldown. У таблицы абилок есть свои методы, но в них лучше не лезть.

Контейнеры с абилками (AbilityContainer)

Все абилки хранятся в таблице абилок в контейнерах - то есть в классе, в котором внутри лежит референс на саму абилку. А в этом классе находятся динамические и изменяемые данные, обладающие полной индивидуальностью.

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

Поля класса AbilityContainer или DefAbilityContainer
ability: Ability или Def Ability
Обязательное и уникальное поле, айдишник абилки.
usages_left: int
Оставшееся кол-во применений для абилок с фиксированным временем применения. Чтобы выдать такую абилку, нужно этот параметр передать в метод add_ability, что находится в энтити.
time_left: int
Аналогично с пунктом выше, но это оставшееся время
allowed: bool
Буль, который говорит о том, можно ли использовать абилку. В данный момент это используется только для ежедневных абилок.

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

Особенности контейнеров

Бодипарты имеют time_left внутри самого бодипарта, и он устанавливается в PartContainer при добавлении части тела. Почему у абилок нет поля с собственной временностью, и его можно задать только через метод?

Если честно, не знаю. Это хороший вопрос. Я потерян и не знаю, что на него ответить. Скорее всего, дело в том, что у бодипарта еще есть буль summoned, дающий возможность дополнительного влияния на призванные конечности, и проще сразу задавать длительность. А в случае абилок там нет особой разницы между временной и постоянной. Достаточно убедительно? Нет? Ну и ладно.

Давайте обсудим работу временных и лимитированных абилок. Взглянем на аргументы метода add_ability у энтити:

def add_ability( ability: Ability | DefAbility, time_left: int = 0, use_left: int = 0):
    ...

При добавлении абилки, вы можете добавить ее как временную или с ограниченным кол-вом использований. Или можно и то, и то!

  • После использования каждой абилки (вне зависимости от успешности результата), каждая абилка теряет один usages_left, если он был больше нуля. Если он равен одному и должен потерять еще один, тогда абилка уничтожается.
  • В конце хода каждая абилка теряет один time_left, если он был больше нуля. Если он равен одному и должен потерять еще один, тогда абилка уничтожается.

Следовательно, указывая 0, вы делаете абилку вечной. Поэтому все постоянные абилки имеют 0 в time_left и в usages_left.

Атакующие абилки (Ability)

Абилки имеют собственный класс, как можно предположить.

Поля класса Ability:
uid: str
Обязательное и уникальное поле, айдишник абилки.
name: str
Отображаемое имя абилки. Обязательно, но может дублироваться.
short_description: str
Короткое описание абилки для внешнего отображения.
description: str
Текстовое описание абилки.".
func: str
Название функции абилки в виде строки1. Это буквально название функции в коде в файлах по пути content/ability_functions.py. Обязательное поле.
ability_type: AbilityType
Тип атаки, нужно для расчетов. Учтите, что это не стринг, а поле класса AbilityType в constants.py, подробнее об этом ниже. Обязательное поле.
target_type: TargetType
Вид абилки (целей). Учтите, что это не стринг, а поле класса TargetType в constants.py, подробнее об этом ниже. Обязательное поле.
roll_stat: CombatRollStat
Ключевой стат, на который делается бросок.
target_entity_type: list[EntityType]
Перечисление со списком возможных типов целей. Если пустое (а по умолчанию так), то работает на всех целях. Достаточно экзотическая вещь, полезна, если хотите сделать абилку, работающую только против роботов и отмена происходила еще до того, как доходит до метода абилки.
target_limit: int
Лимит целей для AOE-абилки. В случае, если это не AOE-абилка, ничего не делает. По умолчанию 0, т.е кол-во целей не ограничено.
locational: bool
Является ли абилка локационной, т.е. наносится обязательно по части тела. Если она не является локационной, то она наносится в целом по энтити.
skill_req: Skills
Инстанс класса скиллов, значения в котором это требования к скиллам для активации абилки.
attr_req: Attributes
Аналогично с пунктом выше, только это инстанс класса атрибутов.
tag_req: list[Tag]
Список тэгов, которые необходимы для активации абилки.
tag_stop: list[Tag]
Список тэгов, наличие которых запрещает активацию абилки.
min_distance: int
Минимальная дистанция, по умолчанию 0. Если вы активируете абилку и до цели расстояние меньше этого числа, то эта абилка не проходит.
max_distance: int
Максимальная дистанция, по умолчанию 32. Если вы активируете абилку и до цели расстояние больше этого числа, то эта абилка не проходит.
cooldown: int
Кулдаун абилки. После своего применения она накладывает кулдаун на указанный срок на использовавшую его энтитю.
hidden: bool
Является ли абилка скрытой. Не имеет применения в Spice, может быть нужно для "фронтэнда".
consume_od: int
Кол-во потребляемого, в случае успешного использования абилки, ОД.
consume_stamina: int
Аналогично с пунктом выше, но для стамины.
consume_focus: int
Я думаю, вы уже догадались.
traumatic: bool
Является ли абилка "травматичной", то есть может ли ее использование наложить травму.
post_actions: list
Постэкшены, т.е. список действий, которые движок должен выполнить после применения абилки. Подробнее ниже.
tags: list[Tag]
Тэги самой абилки.
tags_type: TagType
Константа, в случае абилки это TagType.ABILITY_TAG.
daily: bool
Является ли абилка ежедневной. Если да,то после каждого использования она будет в контейнере помечаться, как недоступная. Затем что-то (например, ежедневный цикл) должно это состояние снимать.

У абилок нет никаких методов. За пределами ее инициализации нет ничего.

Подклассы абилок

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

Поля класса WeaponAbility
durability_cost: float
То, сколько дурабилити снимается при активации абилки.
ammo_cost: int
То, сколько патронов снимается при активации абилки.
relative_ammo_cost: bool
Если это True, то, если атака AOE, то ammo cost умножается на кол-во целей.

Божественные абилки работают немного отлично от обычных. Их не нужно "выдавать", они появляются сами (в методе get_all_abilities), если у игрока достаточно того или иного благословения. В рамках самой абилки вы задаете то, сколько должно быть благословления для активации абилки.

Поля класса DivineAbility
ocean_req: OceanSide
Требования к благословениям ocean для того, чтобы заюзать эту абилку.
underside_req: UndersideSide
Требования к благословениям underside для того, чтобы заюзать эту абилку.

Создание атакующих абилок

Лучше всего показать это на примере. Делается это прямо в коде, в файле content/abilities.py

Ability(uid="united_with_waves",
        name="Единый с волнами",
        short_description="-",
        description="-",
        func="united_with_waves",
        ability_type=AbilityType.MANEUVER,
        roll_stat=CombatRollStat.ELECTRODYNAMICS,
        locational=True,
        skill_req=Skills(ELECTRODYNAMICS=1),
        min_distance=5.0,
        max_distance=10.0,
        cooldown=3.0,
        hidden=False,
        consume_od=4.0,
        consume_stamina=3.0,
        consume_focus=3.0,
        target_type=TargetType.SINGLE).save()
Вы можете добавить любые поля из общего списка полей (выше), или убрать что угодно, кроме обязательных полей, тогда они будут заняты дефолтными значениями или вовсе отсутствовать. В конце .save() нужен обязательно.

Функция абилки (func)

Функция абилки - это ее основное тело, которое ее активирует. Функции есть только у атакующих абилок.

Они пишутся самостоятельно в коде, однако, благодаря крутой магии декораторов и препроцессинга данных, обычно (до 80% случаев, но если вы хотите что-то мудреное, придется, конечно, раскошелиться на приколы) занимают по 2-6 строчек! Как это возможно?! Давайте узнаем!

Функция абилки выглядит как функция Python (def вот это все), в которую передается инстанс класса AbilityContext или AOEAbilityContext, описанный чуть ниже. Вы обязаны указать все эти аргументы аргументами для вашей функии абилки, даже если какие-то из них не будут использоваться, они в любом случае в полном составе будут передаваться.

Список полей классов AbilityContext и AOEAbilityContext
source: Entity
Источник абилки, т.е. та энтити, что ее применяет.
t_entity: Entity
Цель абилки, т.е. та энтити, на кого ее применяют.
atk_ability: AbilityContainer
Сама атакующая абилка. Она передается в контекст, так как, в теории, разные абилки могут вызывать одну функцию.
def_ability: DefAbilityContainer | None
Защитная абилка. Может быть None, если цель не защищается.
t_bodypart: PartContainer | None
Часть тела, куда должна наноситься абилка. Может отсутствовать, но только если абилка не locational.
ability_location: AbilityLocationEnum
То, откуда происходит абилка, например MAIN_ITEM, STATUS или OWN. Это энам и все варианты можно найти тут
distance: float
Расстояние между атакующим и защищающимся.
energy_shield: EnergyShieldContainer
Энергощит на цели. Можно вытащить его самостоятельно, конечно, но смотрите, как удобно! Это сделали за вас!
source: Entity
Источник абилки.
t_entities: list[Entity]
Список целей абилки.
atk_ability: AbilityContainer
Атакующая абилка, применяюемая источником.
def_abilities: list[DefAbilityContainer | None]
Защитные абилки, используемые целями.
t_bodyparts: list[PartContainer | None]
Части тела, куда должна наноситься абилка. Может отсутствовать, но только если абилка не locational.
ability_location: AbilityLocationEnum
То, откуда происходит абилка, например MAIN_ITEM, STATUS или OWN. Это энам и все варианты можно найти тут.
distances: list[float]
Расстояния до целей.
energy_shield: list[EnergyShieldContainer]
Энергощиты на цели.

Везде, где передаются массивы, как можно догадаться, их порядок полностью совпадает друг с другом. То есть, например первая энтити в массиве t_entities использует первую абилку в массиве def_abilities и так далее. Если вдруг энтити ничего не использует, то там будет None, то есть порядок не нарушен. Так что итерируйтесь и используйте zip смело, хотя за вас это уже делают в некоторых декораторах.

Вы можете делать с данными контекста что угодно в теле этой функции. Например, вы можете задать урон в зависимости от тэгов разных энтити или количества их частей тела. Все, что вы можете вообразить себе математически, вы можете реализовать в этой функции.

Подразумевается, что функция абилки не должна наносить непосредственно эффект, а выясняет, какие именно эффекты и с какими особенностями она накладывает. Если взять за основу обычное нанесение урона, то функция абилки не должна снижать запас здоровья, но должна определить, сколько именно снижается, если у цели есть/нет щита, если у цели есть или нет определенного тэга и так далее, вариантов проверок тут множество.

Нанесение эффектов

Для нанесения эффектов существуют отдельные функции, называемые эффектами. Хотя вам никто не запретит ими не пользоваться, это крайне рекомендуется.

Приведем пример боевой функции.

@single_entity_ars_ability
def electric_charge(ctx: AbilityContext):
    ed_skill = ctx.source.get_skill_value("electrodynamics")
    damage_target(ctx, "ENERGY", ed_skill)

Да-да, это вся абилка. Что можно сказать о данной абилке?

  1. Мы узнаем текущий навык электродинамики источника.
  2. Наносим энергетический урон по цели, равный этому навыку.

Как мы видим в коде абилки сверху, она весьма небольшая. Весь секрет - в декораторах и "финишерах", навроде damage_target.

Декораторы и финишеры

Pro-tip!

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

Все подобные "упрощающие жизнь" методы задаются в /content/ability_functions/common.py. В данный момент существует несколько готовых декораторов и финишеров, вкратце их опишем:

Описание декораторов абилок

single_tg_ars_ability
Нестареющая классика для всех одиночных абилок. Проверяет, хватает ли статов, затем извлекает релятивные баффы, производит рандомизацию части тела (в Spice есть шанс попасть не по той конечности, куда вы целитесь), затем высчитывает атакующий и защитный ролл, подсчитывая все-все баффы и дебаффы, затем, если защитный ролл был больше, то активирует эффект защитного действия на атакующем, если меньше, то активирует эффект - то есть вашу боевую функцию. Вау, да? Много работы, а всего одна строчка.
aoe_ars_ability_each
Создает AbilityContext для каждого набора целей и прогоняет его так же, как и декораторы выше, после чего каждый из этих отдельных контекстов передает в функцию абилки. То есть функция абилки вызывается столько раз, сколько там целей, и в нее передается обычный, не АОЕ-шный контекст с каждой целью. Это полезно для абилок в духе "наносит каждой цели 5 единиц урона", где вам не важно, сколько их, и вы каждый раз выполняете одни и те же действия.
aoe_ars_ability_trim
Создает новый AOEAbilityContext, исключая там тех, кто успешно защитился.
only_stats_ability_check
Проверяет только тот факт, хватает ли статов для активации абилок. Подходит для всех абилок без проверок на попадание, типа исцеления цели или самобаффа.

В будущем тут появятся декораторы для оружейных абилок. Их особенностью будет проверка на достаточность патронов и прочности оружия.

Pro-tip!

Проверки на статы происходят внутри декоратора, и это просто набор чисел. Если вы не пользуетесь ими, во первых, не забудьте сами их сделать, а во вторых, помните, что вы можете прикрутить к этому свою логику - например увеличивать или снижать траты ОД в зависимости от кол-ва целей или запаса стамины. Тут можно много чего придумать, было бы желание. Так же учтите, что вам нужно самостоятельно накладывать кулдауны. В обычных условиях в случае неудачной атаки (если защитный ролл выше атакующего), кулдаун не стартует, а вот косты списываются в любом случае. У АОЕ-абилок, кстати, кулдаун начинается если хотя бы одна атака была успешной.

В целом, декораторы не обязательны к использованию и даже не рекомендуются там, где вам нужна очень сложная логика. Если вы их не используете, в функцию абилки передается чистый и незапятнанный AbilityContext или AOEAbilityContext и вы можете сами написать абсолютно любую, нужную вам логику. Декораторы написаны для самых распространенных юзкейсов, где проходят все стандартные проверки - на статы, на защитный ролл и так далее. В будущем будут добавлены декораторы для оружия, где будет проверяться наличие патронов.

Но и на этом сахар не заканчивается! Помимо этого, есть еще и завершающие методы, которые тоже, по сути, сахар.

Описание финишеров абилок

damage_target(ctx: AbilityContext, damage_type: str, amount: float | int)
Дамажит цель на определенное значение. Автоматически добавляет к урону эффект от релятивных баффов.
damage_target_ignore_shield(ctx: AbilityContext, damage_type: str, amount: float | int)
Дамажит цель на определенное значение. Автоматически добавляет к урону эффект от релятивных баффов. Игнорирует щиты.
damage_self(ctx: AbilityContext, damage_type: str, amount: int | float)
Дамажит себя на определенное значение.

Что за damage_type, смотрите тут.

Вы так же можете ими не пользоваться.

Финишеры принимают в себя аргументы и все перечисленные финишеры выполняют эффект basic_damage. Давайте посмотрим на его код.

def damage_target(ctx: AbilityContext, damage_type: str, amount: float | int):
    from content.functions.effects import basic_damage
    relative_buff_instructions = get_relative_buff_instructions(ctx)
    amount = (amount + relative_buff_instructions.extra_damage) * relative_buff_instructions.mod_damage
    target = ctx.energy_shield if ctx.energy_shield else ctx.t_bodypart
    basic_damage(source=ctx.source, target_entity=ctx.t_entity, target=target, damage_type=damage_type,
                 amount=amount, traumatic=ctx.atk_ability.ability.traumatic)

Подробнее про сам basic_damage смотрите там, где описываются эффекты

Завершение абилки

Учтите, все что все абилки должны возвращать либо ничего, либо инстанс класса AbilityUsedContext, который находится там же в /content/ability_functions/common.py. В нем задаются две вещи:

Поля класса AbilityUsedContext

result: AbilityFinishStatus
Энам AbilityFinishStatus, подробнее о нем ниже.
post_actions: list
Словарь с пост-экшенами. Если этот аргумент не передан, то он будет пустой. Пост-экшены подробнее будут расписаны ниже.

Если вы не возвращает инстанс этого класса, то создается дефолтный, в котором AbilityFinishStatus.SUCCESS, а post_actions берется из атакующей абилки. Единственный смысл этого возврата - это создание динамических post_actions, о них речь пойдет ниже.

Энам AbilityFinishStatus

Есть следующие AbilityFinishStatus:

  • SUCCESS - успех.
  • CRITICAL_SUCCESS - критический успех, если вы используете криты или просто было какое-то важное сочетание (например, абилка наносит утроенный урон по нежити).
  • FAIL - неуспех.
  • CRITICAL_FAIL - критический неуспех.
  • NOT_ENOUGH_AMMO - недостаточно патронов.
  • NOT_ENOUGH_DURABILITY - недостаточно прочности предмета.
  • NOT_ENOUGH_STATS - не хватает каких-то статов (не получается заплатить цену ОД/фокуса или стамины).
  • ENTITY_ONLY - эта абилка работает только на энтитях.
  • BODYPART_ONLY - эта абилка работает только на бодипартах.
  • OTHER_REQUIREMENTS_NOT_MET - какие-то еще требования не были исполнены.

Защитные абилки (DefAbility)

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

Поля класса Ability:
uid: str
Обязательное и уникальное поле, айдишник абилки.
name: str
Отображаемое имя абилки. Обязательно, но может дублироваться.
short_description: str
Короткое описание абилки для внешнего отображения.
description: str
Текстовое описание абилки.
self_effects: list[dict]
Это список эффектов и их аргументов в следующем виде: [{"func": "debug_effect", "amount": 10, damage_type: "ENERGY"}] означает нанести 10 единиц энергоурона. Эти эффекты накладываются на самого защищающегося в случае успеха. Можно оставить этот массив пустым.
effects: list[dict]
Это список эффектов и их аргументов в виде, аналогичному выше. Эти эффекты накладываются на того, кто атаковал, подробнее о правилах наложения ниже. Можно оставить этот массив пустым.
ability_type: AbilityType
Тип атаки, нужно для расчетов. Учтите, что это не стринг, а поле класса AbilityType в constants.py. Обязательное поле.
roll_stat: CombatRollStat
Ключевой стат, на который делается бросок.
skill_req: Skills
Инстанс класса скиллов, значения в котором это требования к скиллам для активации абилки.
attr_req: Attributes
Аналогично с пунктом выше, только это инстанс класса атрибутов.
tag_req: list[Tag]
Список тэгов, которые необходимы для активации абилки.
tag_stop list[Tag]:
Список тэгов, наличие которых запрещает активацию абилки.
hidden: bool
Является ли абилка скрытой. Не имеет применения в Spice, может быть нужно для "фронтэнда".
consume_od: int
Кол-во потребляемого, в случае успешного использования абилки, ОД.
consume_stamina: int
Аналогично с пунктом выше, но для стамины.
consume_focus: int
Я думаю, вы уже догадались.
tags: list[Tag]
Тэги самой абилки.
tags_type: TagType
Константа, в случае абилки это TagType.ABILITY_TAG.

Как видите, защитные абилки очень похожи на атакующие, однако вместо функций у них есть эффекты, которые можно достаточно быстро накладывать либо на атакующего, либо на защищающегося. Список этих эффектов не ограничен и вы можете накладывать сколько угодно разных эффектов в любом случае. Но, разумеется, эффекты менее гибки, чем полноценные функции. Подробнее о них, кстати, ниже. Сейчас просто зафиксируйте, что это, так или иначе, название функции в строке под ключом func и ее аргументы в остальных ключах (их может быть сколько угодно).

Защитные косты и кулдауны

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

У защитных абилок есть один метод:

  • activate(attack_source: Entity, attack_target: Entity, attack_target_bodypart: Part = None)

Он по очереди активирует каждый из эффектов на определенный target с опциональной возможностью передать target_bodypart. Затем поочередно вызываются сначала эффекты на цель, затем - на себя.

Аргументы эффектов защитных абилок

Напомним, все аргументы в .activate() передаются из абилки, где таргет бодипарт - это часть тела защищающегося, который и активирует защитную абилку. Таким образом, указывать это в действиях против врага бесмысленно и даже опасно, если например передать эту часть тела в эффект нанесения урона, то урон нанесется по самому защищающемуся, который успешно выполнил защитное действие! Поэтому запомните, что передается:

  • Эффектам в effects передается target_entity и target и он равен источнику атаки (т.е. source), то есть вы можете продамажить его, но только в целом, а не конкретную часть тела (ну или написать эффект, выбирающий случайную часть тела)
  • Эффектам в self_effects передается сам защищающийся в target_entity и в target передается либо часть тела, куда был направлен удар (t_bodypart), либо сама энтити, если абилка не была направлена по конечности.

Все остальные аргументы передаются из словаря. Учтите, что если вы используете какие-то эффекты в защитных абилках, то в них обязательно должны быть поля target_entity и target (ну или хотя бы какой-то обработчик "лишних" именованных аргументов, вроде **_).

Пример защитной абилки
DefAbility(uid="healing_block", 
           name="Исцеляющая защита", 
           self_effects=[{"func": "basic_heal", "amount": 10}],
           consume_od=1, 
           consume_stamina=10, 
           consume_focus=10, 
           cooldown=10,
           ability_type=AbilityType.TECHNIQUE, 
           roll_stat=CombatRollStat.REBORIA).save()

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

Роллы (Rolls)

Все роллы хранятся в combat/rolls.py, рекомендуется использовать именно их, потому что они за вас считают сложные штуки

Роллы делятся на боевые и небоевые. Мы оба рассмотрим тут, хотя небоевой ролл не относится к абилкам. Вот такая загадочная документация!

Список CombatRollStat
  • W_INACCURATE
  • W_ACCURATE
  • W_INTELLIGENT
  • D_TECHNIQUE
  • D_MANEUVER
  • D_FEINT
  • HELITICS
  • REBORIA
  • SHAN_LIGIA
  • ELECTRODYNAMICS
  • VITAISM
  • SEFIROMANTICS
  • THEURGY
  • CATALYSTICS
  • COMBISTICS
  • DEMIPHYSICS
  • PHOTOKINETICS
  • BIOGENETICS
  • CYBERSYNTHETIC

Все боевые роллы работают от энама CombatRollStat, который, напомним, является полем в классе абилки. По текущему дизайну, туда входят все навыки и еще шесть видов действий:

  • Те, что начинаются c W_, в основном к действиям с применением оружия, а не навыков. Тут заюзана достаточно сложная логика, в общем, если вкратце, то все атрибуты (сила, ловкость и т.д) используются в роллах на оружие, однако в разной пропорции. Сейчас там есть неточное, точное и интеллектуальное оружие. Все они имеют свои, прописанные в коде, модификаторы к тем или иным статам, при этом суммарный модификатор = 1, то есть они вполне себе сравнимы с бросками на скиллы.
  • Те, что начинаются с D_, относятся к защитным действиям. У защитных действий их AbilityType (TECHNIQUE, MANEUVER и т.д) почти одноименный с роллом. Они работают по тому же принципу, что и предыдущая категория бросков.

За боевые роллы отвечает следующий метод:

do_combat_roll(entity: Entity, roll_stat: CombatRollStat | str) -> int
Этот метод возвращает, собственно, число-результат ролла. Так же он начисляет опыт за использование. Можно передать либо энам, либо его значение.

Небоевые роллы совершаются только на атрибуты и позволяют прикладывать усилия. Усилие - это число, которое добавляется к броску. В случае броска на силу, ловкость или выносливость, за каждую единицу усилия, вы тратите единицу стамины, а за каждый бросок на интеллект, решительность или восприятие вы тратите одну единицу фокуса.

За небоевые роллы отвечает следующий метод:

do_generic_roll(entity: Entity, roll_stat: GenericRollStat | str, effort: int) -> ReturnWrapper:
Он возвращает вам, собственно, ReturnWrapper, в котором в extra_datа будет словарь следующего содержания {"roll": 20}, где значение ключа "roll" это результат броска. Почему тут используется ReturnWrapper, а не просто число? Потому что этот метод (по идее) не используется в коде, но используется в API. А вот боевые роллы выше наоборот, не юзаются в апи напрямую, но юзаются в коде. Так же он начисляет опыт за использование.

И боевые, и небоевые броски делаются по следующей формуле:

d%текущий_стат% + d%максимальный_стат% - 1, плюс усилие, если оно есть.

В данный момент максимальное значение скиллов и атрибутов - это 20.

Опыт

Использование роллов повышает опыт соответствующего навыка или атрибута того, кто их бросал, но только если этот кто-то это инстанс класса PlayerEntity, а не обычная энтити. За каждый ролл начисляется случайное значение, выбираемое в рендже от 0.001 до константы EXPERIENCE_PER_GENERIC_ROLL и EXPERIENCE_PER_COMBAT_ROLL для небоевых и боевых роллов соотв-но. В случае СombatRollStat с W_ и D_, в которых используются все шесть атрибутов, поднимаются все шесть, но каждый из них умножается на свой коэффициент, то есть качаются ваще все атрибуты, но достаточно медленно и быстрее качаются те, которые используются в этих самых роллах. В случае с небовыми бросками, значение опыта умножается на потраченные усилия.

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

Последействия (post_actions)

По идее это просто словарь, который хранится в абилке или вы можете сами вернуть его из абилки, если засунете его в AbilityUsedContext (подробнее, на каком этапе и как это делается, уже было написано выше).

AOE плохо работает с пост-экшенами

Важно! AOE-абилка, если сделана с помощью декоратора @aoe_ars_ability_each, не поддерживает кастомные пост-экшены и всегда будет иметь дефолтные, т.е. напрямую взятые из абилки (если они там представлены)! Если вы хотите влиять на них из АОЕ-абилки, ее придется писать самому полностью или использовать @aoe_ars_ability_trim. К счастью, это не так сложно и редкий юзкейс. Это происходит потому, что декоратор each прогоняет функцию абилки столько раз, сколько там целей, создавая для каждой обычный AbilityContext из AOEAbilityContext, и непонятно, из какого из возвратов брать пост экшен за основу.

Смысл последействий - это передать игровой платформе, куда вы интегрируете Spice, какие-то дополнительные инструкции. Они могут иметь любой формат, поэтому дальше будет расписано исключительно для проекта Stargazer в целях демонстрации и упрощения работы с этим.

Успешность действий

Учтите, что если вы юзаете встроенные декораторы, то post_actions передастся только в случае успешного действия. Это может не быть тем, что вам нужно. Но вы сами решаете, что передавать, если пишете функцию абилки полностью самостоятельно.


В генерализованном виде пост-экшены выглядят так:

"post_actions": {"действие": [аргументы1, аргумент2]}

В данный момент подразумевается только один action и это move. Он двигает цель по определенным правилам.

<target> jump <towards|from|left|right> <acceleration> <angle 0-90>
<target> blink <towards|from|left|right|near|random> <distance or scale>

Возможные варианты аргументов у move:

  1. Первый аргумент говорит о том, кого мы двигаем. Их может быть два - это target, random_target, self. Таргет двигает цель абилки, селф двигает инициатора абилки, рандом таргет применим только для АОЕ и выбирает случайную цель.
  2. Второй аргумент задает тип действия. Их может быть два: jump и blink, джамп это буквальный физический бросок, а блинк это телепорт.
  3. Третий аргумент задает направление движения. Если первый аргумент target, то направление задается относительно инициатора абилки, если первый self то направление задается относительно таргета. Если выбран self и целей несколько, то действие выполняется последовательно итерируясь между целями с задержкой в пару секунд (но это экзотические случаи, впрочем полезно для всяких цепных молний)
    1. Варианты направления движения:
      1. "back" -движение от цели,
      2. "forward" - движение к цели,
      3. "left" - движение влево относительно цели,
      4. "right" - движение вправо относительно цели.
    2. Указанные ранее аргументы доступны как для jump так и для blink, а вот дальнейшие уже чисто для blink:
      1. "near" - перемещение в клетку перед целью.
      2. "random" - в рандомном направлении (влево-направо-вперед-назад).
  4. Четвертый аргумент зависит от типа.
    1. Для jump это будет acceleration, т.е. сила, с которой производится бросок персонажа.
    2. Для blink это будет кол-во блоков, на которые персонаж перемещается в указанном ранее направлении. Если это move, то число означает оффсет. Например, если 0, то это буквально телепортирует в ту же клетку что и цель, если -1, то за спину, если 1, то перед целью, если 2, то за две клетки перед целью и так далее.
  5. Пятый аргумент существует только у jump и означает угол броска. Может быть от 0 до 90.
Примеры пост-экшенов c движением!
  • {"move": [["self", "blink", "forward", 3], ["target", "blink", "back", 3]] - означает сначала передвинуть инициатора атаки на 3 клетки вперед, потом отодвинуть цель атаки на 3 клетки назад.
  • {"move": [["self", "blink", "left", 2]] - передвинуть атакующего на две клетки влево.
  • {"move": [["self", "jump", "forward", 2, 45]] - метнуть инициаторап абилки в направлении цели с силой 2 и под углом 45 градусов.

  1. Это делается через грязную питоническую магию по созданию словаря, где ключ это стринговое название функции, а значение - сама функция. Делается через getmembers и isfunction. Из минусов то, что новые файлы придется добавлять вручную, но их достаточно импортировать в инит модуля. 

  2. У энтити тоже могут быть статусы, дающие резисты. Те резисты, что лежат на энтити, считаются как резисты для всех частей тела. Если вы будете узнавать резист из самой части тела, то общий резист вам не будет известен, но, возможно, это вам и нужно. А может и нет.