Статус
Холдер статусов (StatusHolder)
Статус это определенная штука, накладывающаяся на:
- Энтити (
Entity) - Части тела (
Part)
Это возможно благодаря тому, что и Entity и Part наследуют класс StatusHolder. Все, что может StatusHolder, могут вызывать и энтити, и части тела.
Сам по себе StatusHolder нигде не встречается и по сути это просто способ сэкономить на копипасте и не дублировать одни и те же методы. Именно в нем лежит основная часть полезных штук для взаимодействия со статусами.
У этого класса одно поле, statuses: list[Status | Trauma | Revelation] (о том, что такое Revelation и Trauma, расскажем позже)
Методы класса StatusHolder
add_status(new_status: Status | str, custom_duration: int = None, custom_delay: int = None)- Добавить новый статус. Можно передать сам инстанс класса Status, можно его айди строкой. Так же можно задать кастомную длительность и отложенность, которые заоверрайдят встроенную в статус. В принципе, вы можете прямо в аргументе new_status создать новый инстанс класса, создав новый статус с нужной длительностью и отложенностью, так что это просто сахар. Но зато удобно!
evaluate_statuses_combinations() -> None- Сложная штука, подробнее в самом низу странички. Вручную это редко нужно вызывать.
remove_status_by_name(status_name: str)- Удалить статус по его uid в виде строки.
remove_status_by_tag(tag_name, each=True) -> None- Удалить все статусы, в которых есть определенный тэг, tag_name должен быть строкой с айди тэга. Если each True, то удаляет каждый, если нет - только первый.
get_single_status_by_name(name) -> Status- Получить инстанс класса Status по его имени.
get_all_statuses_names() -> list[str]- Получить все uid каждого статуса. Результат в виде списка (list)
activate_statuses_by_tag(searched_tag: str, entity=None, only_one=True)- Активировать все статусы по айди тэга, который передается в виде строки. В entity обязательно передавать энтити, если вы активируете статус со стороны энтити. Буль only_one определяет, активируете вы только один статус по тэгу (первый попавшийся) или все (если он False).
activate_status_by_name(name, entity=None)- Активировать статус по его uid. В entity нужно передать энтити1, если вы активируете статус со стороны энтити, а не части тела.
activate_all_statuses(entity=None)- Активировать все статусы. Если вы вызываете это из entity, то обязательно передавайте саму эту энтити в entity1, если вызываете из части тела, то можно забить. В целом не советую с этим вручную возиться.
Сами статусы
Статус - третья по фундаментальности вещь после энтитей и абилок. Основное назначение статуса - накладывать длительные эффекты на того или иного персонажа или часть тела. Статус может использоваться и в проверках.
В отличие от многих других сущностей Spice, статус является вложенным документом, то есть он генерируется каждый раз из копии при добавлении. Это имеет как плюсы, так и минусы: например, если исправить в коде какой-то статус, то у всех персонажей, у кого он есть, он не изменится на новый вариант, хотя добавленные с этого момента статусы будут уже новыми. Но из хорошего то, что в нем можно хранить любые динамические данные (та же длительность), или, например, можно создавать новые статусы прямо в рантайме и не захламлять ими базу данных - после того, как статус должен уничтожиться, конкретный инстанс навсегда исчезает.
Pro-tip!
Несмотря на то, что статус это вложенный документ, как класс он:
- Хэшируемый и даже сравним с другим классом по uid. То есть status == status будет True, если у них одинаковый айдишник.
- У вас технически не получится создать два статуса с одинаковым uid, кроме случая, если там есть буль dynamic, в таком случае вы можете наплодить сколько угодно статусов с одинаковым uid. Учтите, что проверка на одинаковость проходит только по uid.
Традиционно начнем с описания полей класса Status:
Поля класса Status
uid: str- Уникальный айдишник статуса.
display_name: str- Отображаемое имя.
duration: int- Длительность эффекта. По умолчанию 1. Снижается на 1 при каждой активации статуса.
delay: int- Означает то, отложен ли эффект и на сколько. В случае, если delay > 0, то при каждой активации, будет снижаться именно delay, а не duration.
endless: bool- Определяет, бесконечен ли эффект. По умолчанию False.
hidden: bool- Скрытый ли статус. Может пригодиться для фронтэнда.
override_by_name: bool- По умолчанию True. Если этот параметр True и статус должен наложиться туда, где уже есть статус с точно таким же uid, то новый статус его перезаписывает.
unique_by_name: bool- По умолчанию False. Если этот параметр True и статус должен наложиться туда, где уже есть статус с точно таким же uid, то новый статус не добавляется.
override_by_tag: bool- По умолчанию False. Если этот параметр True и статус должен наложиться туда, где уже есть статус с таким же (любым) тэгом
unique_by_tag: bool- По умолчанию False. Если этот параметр True и статус должен наложиться туда, где уже есть статус с точно таким же uid, то новый статус его перезаписывает.
stack_limit: int- То, сколько максимум может раз наложиться статус. Например, если он 3, то четвертый статус уже не наложится на персонажа. По умолчанию 0 - то есть бесконечное кол-во раз.
can_concat: bool- Статусы обладают способностью складываться по uid - новый эффект просто добавляет уже имеющемуся свою длительность и отложенность, если это разрешено can_concat. Проверяется конкатабельность именно того, нового статуса кому, а складываемость имеющегося (при совпалении uid) игнорируется. Хотя это и крайне маловероятно, чтобы с одинаковым uid были два разных статуса, технически это возможно, если один из них делается в рантайме.
turn_effects: list[dict]- Самое важное. Список всех эффектов, как он выглядит, будет расписано ниже.
auras: list[Aura]- массив (список) инстансов класса Aura. об аурах подробнее ниже.
events: dict- Эвенты, добавляемые статусов. Подробнее о них ниже.
tags: list[Tag]- Список (list) тэгов статуса.
tags_type: TagType- Тип тэгов, для статуса это всегда будет TagType.STATUS_TAG.
dynamic: bool- Динамические тэги не имеют проверок на уникальность uid. Их нет на стороне БД (потому что у статусов, как у вложенных документов, нет своей коллекции), но есть на стороне Spice. Все генерируемые в рантайме статусы должны иметь dynamic=True во избежание проблем.
buffs: Buffs- Баффы, которые дает статус. Подробности уже приводились на страничке энтити.
abilities: Abilities- Абилки, которые даются статусом. Работает, даже если статус был наложен на часть тела.
resists: ResistTable- Резисты, которые дает наличие статуса. В случае, если статус лежит на энтити, а не на части тела, то эффекты распространяются на все части тела (в случае использования
entity.get_bodypart_resistance(), из самой части тела узнать это не получится). Подробнее о резист_тейблах можно почитать в разделе с бодипартами.
По умолчанию, все статусы активируются автоматически в конце каждого хода каждого отдельного персонажа. У статуса самого по себе нет полезных методовс.
Как вытащить статус
Есть два основных способа вытащить статус. Первое, как в примере выше - присвоить ему переменную и ее импортировать. Это самый простой и надежный вариант. Второй - импортировать из класса /properies/status.py словарь all_statuses и вытащить его по ключу, что-то вроде all_statuses["bleeding"]. Ну и третье - вы можете сами создать статус. Но не забудьте тогда присвоить ему атрибут dynamic=True, а так же помните о том, что в словарь со всеми статусами динамические статусы не добавляются.
Свои эффекты статусы так же запускают при собственной активации, тогда же, когда снижается и их длительность. Если у них есть delay, то они не накладывают эффект.
Примеры создания двух статусов
small_resist = Status(uid="small_resist", display_name="Малый резист", duration=1,
resists=ResistTable(KINETIC=0.05, PIERCING=0.05, BURSTING=0.05, PSIONIC=0.05, WITHERING=0.05,
OCEANIC=0.05, CORRUPTIVE=0.05, ENERGY=0.05,
TOXIC=0.05, DIMENSIONAL=0.05, THERMAL=0.05))
bleeding = Status(uid="bleeding", display_name="Кровотечение", duration=10,
turn_effects=[{"func": "basic_damage", "damage_type": "KINETIC", "amount": 10}],
buffs=Buffs(attributes=Attributes(AGILITY=-2)))
Объясним еще раз, как работает turn_effects: это массив из словарей. Выглядеть он может, например, как-то так (в случае, если там несколько элементов, в случае одного элемента есть пример выше):
[{"func": "basic_damage", "damage_type": "ENERGY", "amount": 0.5}, {"func": "basic_damage", "damage_type": "PSINERGICS", "amount": 10}]
Определитесь со статусом
Статусы лежат как на части тела, так и на энтити. Когда создаете статус, сразу имейте в виду, где он лежит, если он должен накладывать какие-то эффекты, потому что не все эффекты могут поддерживать энтити, если поддерживают бодипарт и наоборот. Хотя универсальные эффекты тоже есть, например basic_damage().
Обязательная пара ключ-значение там func, которая и задает строкой название функции эффекта, который вызывается при активации статуса. Все остальные аргументы - это аргументы этой самой функции; когда эта функция вызывается под target и target_entity подставляется, собственно, сама энтити, на которой наложен статус. Статусы еще могут лежать на частях тела, в таком случае в target передается часть тела.
В целом это все важное, что можно сказать про статусы. Помимо статусов, есть еще травмы (являющиеся их подклассом), их и обсудим.
Что за damage_type, смотрите тут.
Аура (Aura)
Аура - это особые штуки внутри статуса, а если точнее лежащие в поле-массиве auras. Смысл аур в том, что они накладывают определенные эффекты по площади. В данном случае площадью считается просто расстояние до цели, для того, чтобы ауры работали, в конце каждого хода нужно сообщать дальность до каждой энтити, для которой нужно сделать проверку для ауры, делать это должен некий внешний клиент, потому что в Spice в данный момент вообще нет расстояния.
Поля класса Aura
duration: int- Длительность ауры.
delay: int- Отложенность ауры.
endless: bool- Бесконечна ли аура.
type: AuraType- Тип ауры. Вообще он в константах, но скажу вам по секрету, что этот энам состоит из FRIENDS, FOES и ALL. В первом случае аура накладывается только на тех, кто на вашей стороне (и если это не own), во втором на всех, кто не на вашей стороне, а в третьем случае вообще на всех. Если вам непонятно, что за стороны, почитайте про энкаунтеры
turn_effects: dict- Эффект, накладываемый аурой. Правила такие же, как и для обычных эффектов.
distance- Максимальное расстояние, на которое аура работает. То есть если до врага 25 метров, а distance 20, то ничего не получится.
from_entity- Передавать ли энтити самому эффекту. По сути, "хотите ли вы, чтобы эффект баффался от баффов энтити". Может быть оверпавер, поэтому по умолчанию False.
Как видите, аура может иметь свою обособленную длительность и delay, отдельный от статуса, который ее накладывает. Если вам это не нужно, и аура должна пропасть вместе со статусом, просто сделайте duration=1 и задайте endless=true.
Ауры накладываются сами во время триггера end_turn. Не нужно их вызывать не из статуса.
Активация эффекта ауры
Все происходит, как обычно - из словаря извлекается func и это считается эффектом, который вызывается, а остальные ключи и их значения передаются как аргументы этого метода. Следующие аргументы добавляются в этот момент:
- "source" - источник атаки, если у ауры
from_entity = True, то это будет сама энтити, если нет, то "aura" - "target_entity" - target, сама энтити-цель.
- "target" - target, сама энтити цель. Почему она передается дважды в разных местах, лучше посмотрите в описании эффектов на страничке абилок.
- "target_list" - передается список всех целей. Зачем? Ну, например, чтобы можно было влиять на урон исходя из кол-ва целей или чето такое.
- "distance" - расстояние до target.
В связи с этим еще раз напоминаю о рекомендации сделать в любых эффектах **_, чтобы не нужные вам аргументы не вызывали ошибки.
Травмы (Trauma)
Травмы - это подгруппа статусов, которые в основном (по текущей задумке) должны накладываться только на части тела. Их отличие от статусов в следующем:
- Стандартный метод
take_hp()у бодипарта накладывает случайную травму из списка подходящих травам. - Травмы не имеют длительности в ходах или отложенности (хотя из-за наследования, формально, эти атрибуты остались, они не будут работать), но вместо этого исцеляются со временем. Или, поскольку это, на самом деле, все еще статусы, их могут снимать другие статусы или абилки.
Поля класса Trauma
trauma_status: list[Tag]- Список тэгов, которые должны быть у части тела, чтобы там была эта травма. Например, если тут будет два тэга - "Head" и "Organic", то наложиться травма может только на конечность, где есть эти два тэга.
trauma_weight: int- Релятивный вес травмы. Если травма должна наложиться, и возможны несколько вариантов, то происходит рандомный выбор с весами; чем больше вес - тем больше шанс, что выпадет эта травма.
trauma_damage_type: str- То, от какого урона может быть эта травма. Сделано для того, чтобы от термального урона были именно ожоги, а не порезы. Для "общих" травм, возможных в любой ситуации, есть "GENERIC".
curing_current: float- Текущее исцеление. По умолчанию всегда 0, постепенно растет до единички.
curing_amount: float- То, сколько каждую проверку (в бою - каждый ход, вне боя - каждые 15 минут). К этому числу прибавляется бафф extra_timed_healing и умножается на бафф mod_healing.
curing_required: float- Требуемое лечение. По умолчанию - 1000.0. Тогда текущее исцеление становится больше или равно требуемому лечению, при следующей проверке травма снимается.
Травма не обязана накладываться сама, ее можно наложить вручную. Так же, формально, она может лежать не на части тела, а на энтити в целом, но автоматические травмы работают только по частям тела. Травмы это обычные статусы, то есть у них вполне может быть периодический урон (и любые другие периодические эффекты), но будьте осторожны с этим.
Пример травмы
Откровения
Откровения (Revelation) полностью наследуют класс Status и имеют одно дополнительное поле, weight: int, которое задает их относительный вес. Они изображают психологические надломы, похожая система была в Darkest Dungeon.
Каждый раз при проверке в конце хода (или каждые 15 минут вне боя), если у персонажа менталка (подробнее о менталке ищите на страничке энтити в потребностях) ниже 20%, то кидается скрипт, который накладывает рандомное "откровение" из списка.
Pro-tip!
Откровения, по задумке, могут быть как отрицательные, так и положительные. Для проекта Stargazer будет сделано так, что все положительные откровения будут временными. Они не просто дают бафф, но еще и защищают от наложения негативного статуса, поскольку одновременно может быть только одно откровение. Негативные же откровения будут накладываться навсегда - то есть до естественного исчезновения или применения расходника.
Если на персонаже уже есть откровение, то новое не появится. Все откровения снимаются, если менталки больше 80%.
Осторожно!
Не накладывайте откровения на части тела. Хотя, скорее всего, ничего страшного не случится, все же не делайте этого.
Комбинации статусов
В общем, это очень сложная и загадочная магия, позволяющая объединять статусы. Выглядит это как-то так:
status_incrementation = {super_bleeding.id: {bleeding.id: 2}} # ожидается айдишник, поэтому можно в строке
status_incrementation = {"super_bleeding": {"bleeding": 2}, "cursed_dancing": {"dancing": 1, "cursing": 1}}
Это позволяет вам объединить несколько статусов в один. Например, из примера выше мы превращаем 2 эффекта с юидом bleeding в super_bleeding, а так же 1 эффект dancing и 1 эффект cursing в cursed_dancing. Переоценка производится по методу evaluate_statuses_combinations(), она вызывается в конце каждого хода.
Сам по себе словарь status_incrementation захардкожен и переименовывать его не нужно. Но можно его расширять!
-
Ситуация следующая: у нас есть класс StatusHolder, дающий возможности взаимодействия со статусами. Статусы, в свою очередь, имеют эффекты, которые они накладывают при активации. Теперь, представим себе ситуацию, что какой-то эффект должен влиять на саму энтити. Например, на руку наложили эффект, который дает -5 к стамине каждый ход. Стамины нет у руки, но она есть у энтити. Но если мы будем вызывать этот эффект со стороны руки, то мы не сможем наложить его на энтити, если не создавать специальный метод по самостоятельному узнаванию рукой, какой энтити она принадлежит, но для этого нам нужно в каждом scope с частью дела держать в памяти саму энтити и ее в любом случае придется за собой "таскать". Итоговое решение следующее: мы активируем каждый статус конечности со стороны энтити, передавая при этом и цель (ей может быть как рука, так и энтити), и энтити, а сам эффект, исходя из внутренней логики, уже делает то, что нужно. Это, конечно, не самое "чистое" решение, но весьма экономное. ↩↩