Слои, переводы и миграции наборов правил
Эта страница объясняет, как предложить варианты, которые игрок включает для одной игры, перевести текст набора правил и перенести сохраненные значения на новые id при изменении листа персонажа. Она предназначена для авторов наборов правил. Перед началом прочитайте руководство Создание наборов правил Game Mode.
Слои: варианты вашего набора правил
Слой – именованный вариант набора правил, который игрок включает при создании игры: мало магии, суровая зима, более жесткая сложность. Слои живут в файле набора правил в необязательном массиве layers, поэтому путешествуют с ним и никогда не пропадают из использовавшей их игры. Мастер настройки показывает их переключателями под набором правил; выбор закреплен на весь срок игры, как и сам набор.
"layers": [
{
"id": "hard_winter",
"label": "Hard winter",
"summary": "Cold, hunger and short days. Everything is harder.",
"conflicts": ["mud_season"],
"gm": {
"guidance": "Hard winter is on. Let a failed check cost warmth, food or daylight as well as progress.",
"worldGuidance": "Hard winter is on. Build a world of closed roads, thin stores and rationed settlements."
},
"fields": [{ "id": "calling", "removeValues": ["Sailor"], "default": "Hauler" }],
"difficultyLadder": [{ "label": "Easy", "dc": 7 }],
"catalogs": [{ "id": "knacks", "hide": { "filter": "grit", "above": 0 } }]
},
{
"id": "mud_season",
"label": "Mud season",
"summary": "Thaw, flooded roads and slow going."
}
]Что может слой. Список закрыт; каждый эффект что-то сужает либо добавляет текст:
gm.guidanceдобавляется кgm.checkGuidanceпосле вашего текста и текстов предыдущих слоев.gm.worldGuidanceаналогично добавляется кgm.worldGuidance.fieldsубирает значения из поля enum.removeValuesименует уже имеющиеся значения; хотя бы одно должно сохраниться, и еслиdefaultполя удаляется, слой вместо него именует сохраняющийсяdefault.difficultyLadderзаменяет вашу шкалу другой в форме вашего вида разрешения:{label, dc}дляdice-sum,{label, successes, target?}дляdice-pool. Проходит ровно те же проверки, что ваша шкала. Если ее объявляют несколько активных слоев, побеждает последний.catalogsскрывает записи из окна выбора редактора листа. Каждое правило именует один из объявленныхfiltersкаталога и ровно одно сравнение:aboveилиbelowдля фильтраnumber,equalsилиnotInдляtextилиtags. Запись, вообще не задающая этот фильтр, никогда не скрывается.currenciesубирает монеты:removeUnitsназывает отдельные монеты, аremoveFamilies– целые семейства. Самая мелкая монета семейства уходит только вместе с семейством, поэтому каждое оставшееся семейство по-прежнему может платить и давать сдачу. Пока слой включен, никто не зарабатывает, не платит, не выбирает и не получает в добыче убранную монету, а строка кошелька ее опускает. Цена, названная в ней, выражается в самой крупной оставшейся монете, которая покрывает ее ровно, при той же стоимости: долгая ночь в Gravewatch убирает crown, поэтому караульный пистолет за 3 crowns стоит 15 shillings. Предмет, чье семейство убрано целиком, не имеет цены.itemsменяет, где продаются вещи и что выпадает, но никогда не меняет то, что существует.market.removeSellersубирает продавцов с вашего рынка (хотя бы один всегда остается: рынок без продавцов позволяет любому продавать все, поэтому удаление, которое оставило бы его пустым, даже двумя слоями вместе, не выполняется).market.unsoldубирает товары у всех продавцов с помощью фильтров, как у самого рынка (добавляется кunsoldрынка).lootTablesзаменяет уже имеющиеся у вас таблицы добычи по id таблицами той же формы, поэтому каждое существо, уровень угрозы иstockпродавца, которые называют такую таблицу, используют заменяющую таблицу для бросков; когда несколько активных слоев заменяют одну таблицу, побеждает последний. Предмет, который не продается, по-прежнему остается предметом: его можно носить, использовать и находить в добыче, Game Master может его выдать, а таблицаstockпродавца все же может его выбросить.
"items": {
"market": { "removeSellers": ["magic_dealer", "scribe"], "unsold": [{ "tag": "magical" }] },
"lootTables": [{ "id": "treasure_low", "label": "Pocket treasure", "entries": [{ "coins": "sp", "weight": 1, "count": "4d6" }] }]
}Чего слой не может. Он не добавляет значение перечисления, поле, навык, запас или отдых, продавца или таблицу добычи, не меняет вид разрешения, текущее состояние или боевые числа, не добавляет вызов модели. Значение, _добавленное_ слоем, было бы неизвестно остальным читателям листа, поэтому значения могут только убираться. Все вне списка – изменение самого набора правил или второй набор.
Конфликты. conflicts именует слои, которые нельзя включать вместе. Достаточно указать одну сторону пары. Мастер настройки блокирует другой переключатель; если сохраненный выбор все же содержит оба, объявленный позже отбрасывается, поэтому те же два выбора всегда дают те же правила.
Лист, уже хранящий удаленное значение, сохраняет его. Ничто не переписывает персонажа. Редактор перестает предлагать значение; персонаж, уже имевший его, показывает его как есть. Выключите слой в новой игре – значение снова предлагается. То же со скрытой записью каталога: она отсутствует в окне выбора, но уже выбранная игроком строка остается в листе. Монеты, которые убирает слой и которые у кого-то уже есть, остаются в его сумке.
Ограничения. 12 слоев на набор правил и 4000 символов указаний на слой, считая обе строки вместе. Пакетный набор с layers или базовым gm.worldGuidance требует Capability API 1.25, набор, чьи слои убирают монеты, – 1.64, а набор, чьи слои меняют его items, – 1.813. Импортируемый набор проверяет читающий его Engine, поэтому ему ничего такого не нужно.
Слои другого автора (слой малой магии для чужого набора правил, поставляемый своим файлом) – будущее дополнение. Сегодня слой поставляется внутри набора, которому принадлежит.
Переводы
Переводы (Capability API 1.811). Отображаемые строки набора правил на другом языке составляют отдельный перевод для каждого языка, названный по коду языка, как и собственные языковые пакеты Engine (ru, pt-BR, zh-Hans). В официальном каталоге агентов каждый перевод представлен языковым пакетом рядом с файлом набора правил (ниже). Файл пакета набора правил, которым вы делитесь, может вместо этого содержать их внутри, по одному файлу на язык рядом с ruleset.json: locales/<language>.json, перечисленные в манифесте, как и остальные файлы. В любом случае каждый из них – каталог локализации пакета с разделом ruleset: каждый ключ – отображаемая строка набора правил или его каталогов, дословно на английском языке, а его значение – эта строка на языке файла.
{
"_meta": { "locale": "ru", "direction": "ltr" },
"package": { "name": "5e (SRD 5.1)" },
"ruleset": { "Prone": "Лежащий", "Fire Bolt": "Огненный снаряд", "Goblin": "Гоблин" }
}_meta.locale должен обозначать язык, по которому назван файл, а файл содержит только $schema, _meta, package и ruleset, и каждое значение – текст. Пустая строка еще не переведена и отображается на английском; файл без раздела ruleset (собственное английское описание пакета, locales/en.json) ничего не переводит. Переводы влияют только на отображение: правила, команды листа персонажа и все, что сообщается Game Master, по-прежнему используют английский текст. Экран боя (имена, состояния, действия, заклинания, журнал и порядок ходов) показывает каждую строку на языке игрока, если в переводе набора правил она есть, сначала из файла самого языка, затем из файла его базового языка (pt для pt-BR), а иначе – на английском; "Goblin 2" использует перевод "Goblin" с его номером. Файлу пакета с переводами нужен Capability API 1.811. При импорте сохраняется только перевод на язык, выбранный в Engine (или на его базовый язык), а остальные отбрасываются; чтобы читать набор правил на другом языке, смените язык Engine и импортируйте файл снова, заменив сохраненный перевод. Переводы не входят в версию, поэтому повторный импорт той же версии с исправленным переводом тоже заменяет его, а файл той же версии без переводов оставляет сохраненный перевод.
Набор правил из каталога никогда не содержит locales/<language>.json внутри своего файла (такой файл отклоняется еще до скачивания). Его переводы – языковые пакеты (Capability API 1.811): по одному файлу JSON на язык, <id>-<version>.<language>.json, опубликованному рядом с <id>-<version>.ruleset.json и содержащему ровно то же, что и locales/<language>.json (_meta.locale обозначает язык, по которому назван файл, и есть раздел ruleset). Запись каталога перечисляет их рядом со своим artifact:
"locales": {
"ru": {
"url": "https://raw.githubusercontent.com/Pasta-Devs/Marinara-Agents/main/artifacts/ember-roads-1.0.0.ru.json",
"sha256": "<64 hex>",
"bytes": 812345,
"name": "Угольные дороги",
"description": "<the store card's description in Russian>"
}
}Каждый пакет – это URL с https той же формы, что и URL артефакта, с именем, соответствующим его версии и языку, с его sha256 и размером (не более 8 MB); набор правил перечисляет не более 40 пакетов и никогда не включает en (английский текст – это сам набор правил), а его манифест объявляет Capability API 1.811. name и description – название и описание карточки магазина на этом языке, поэтому карточка отображается на языке Engine без загрузки пакета. При установке загружается файл набора правил, а затем только пакет на языке, выбранном в Engine (или на его базовом языке), проверяются его размер, контрольная сумма и язык, после чего он сохраняется вместе с установленной версией. При смене языка Engine (и при каждом открытии страницы в нем) каждый установленный набор правил из каталога, в котором указан пакет на новом языке, загружает его, а пакет на старом языке удаляется. Если пакет получить не удалось (нет подключения к сети, пакет не указан или файл не соответствует указанному в каталоге), набор правил остается на английском; следующая установка или смена языка повторяет попытку. Обновление сохраняет предыдущую версию для отката без ее пакета: после отката следующая смена языка снова загружает пакет той версии, если он все еще указан в каталоге.
Перенос значений на новые id: sheet.migrations
Добавление нового ничего больше не требует. Когда новая структура переносит значение в другое место (переименованное поле, один уровень, который становится уровнем для каждого класса, один запас костей хитов, который становится запасом для каждой кости), опишите это в sheet.migrations (Capability API 1.77), чтобы каждый персонаж, созданный по старой структуре, сохранил свои числа: в библиотеке, в играх, в старых сохранениях, ветках и контрольных точках и в импортированных карточках.
"migrations": [
{
"from": 3,
"steps": [
{ "rename": "field", "from": "xp", "to": "experience" },
{
"route": "level",
"by": "class",
"cases": [
{ "words": ["fighter"], "to": "level_fighter" },
{ "words": ["wizard"], "to": "level_wizard" }
],
"else": "level_other",
"default": 1
},
{
"routeLive": "pool",
"from": "hit_dice",
"cases": ["hit_dice_d6", "hit_dice_d8", "hit_dice_d10", "hit_dice_d12"],
"else": "hit_dice_d8"
}
]
}
]- Каждая запись переводит сохраненный лист с версии
fromнаfrom + 1и выполняется для каждого листа, сохраненного в этой версии или старше, по порядку. Никогда не удаляйте запись после выпуска: листу, сохраненному давно, она все еще нужна. Версии, которая только добавляет, записи не нужны. rename(ability,skill,saveилиfield): значение копируется на новый id. Навык или спасбросок забирает с собой свой бонус. Переименование может продолжать переименование из более ранней записи или питать поле, которое дальше отправляет более позднийroute; импорт следует по цепочке до ее конца.route: значение одного поля отправляется в поле, которое называет текст другого поля. Вариант побеждает, когда его слова встречаются в текстеbyкак целые слова (слово образуют только буквы, поэтому "Fighter3" называет воина), без учета регистра, и подходит ровно один вариант. Если не подходит ни один или подходят несколько ("Fighter 3 / Wizard 2", одно число между которыми нельзя разделить без вопроса), значение отправляется вelse.defaultзаменяет значение для листа, который его никогда не сохранял.by– поле, которое есть в листе, или поле, которое более ранняя структура переименовала. Цели – числовые или текстовые поля без значения по умолчанию или со значением по умолчанию 0 или пустым: значение попадает в одно из них, а все остальные сохраняют свои значения по умолчанию, поэтому любое другое значение по умолчанию читалось бы как уровень, который никто не давал. За целью, которую дальше переносит более позднее переименование, следуют туда.routeLive(poolилиtrackтекущего состояния): сохраненное значение отправляется в тот изcases, который показывает перенесенный лист (запас с максимумом больше 0, шкала, которую лист не скрывает), иначе вelse. Шкалы, в которые отправляется значение, – либо все шкалы ран (оно несет отметки), либо все обычные (оно несет число).setColumn(Capability API 1.794):{ "setColumn": { "list": "attacks", "where": { "name": ["Dagger", "Rapier"], "ability": ["str", "dex"] }, "column": "ability", "to": "finesse" } }задает значение одного столбца в каждой строке списка, в которой каждый из столбцовwhereсодержит одно из перечисленных значений (текст сравнивается без учета регистра и пробелов по краям; число должно совпадать точно). Используйте это, чтобы исправить строки, созданные до уточнения вашего набора правил, например строки атак, созданные до появления выбора характеристики оружия. Список и все указанные столбцы должны быть в листе персонажа.- Каждый другой шаг сборки копирует: старое значение остается на месте, поэтому более старая версия вашего набора правил по-прежнему читает лист так же, а шаг никогда не перезаписывает значение, которое уже есть в цели.
setColumn– единственный шаг, который перезаписывает значение. - Импорт отклоняет запись на уровне
sheet.versionили выше, две записи с одной версии, шаг, который пишет в id, которого нет в листе, вариант, указанный дважды, иrenameилиrouteLive, которые читают из id, все еще имеющегося в листе (перенесенный id больше никогда не используется, и именно поэтому эти шаги безопасно выполнять снова). - Листы переносятся при каждом чтении, а новая структура сохраняется при следующей правке. У текущего состояния нет собственной версии, поэтому
routeLiveвыполняется при каждом чтении: старый ключ отбрасывается, а его значение переносится, только пока нового ключа еще нет, поэтому значение, потраченное или восстановленное отдыхом с тех пор, никогда не перезаписывается. Куда оно попало, определяет первая запись после обновления: лист, исправленный позже (уровень класса, перенесенный вручную), не переносит его снова. Бой, уже идущий в момент прихода новой версии, по-прежнему завершается, как раньше.