Перейти к содержимому

Датасеты

Если вы делаете что-то поверх наших датасетов — редактор, импортёр, конвертер из своего формата, ИИ-генератор контента — начните отсюда. Страница самодостаточна: всё, что нужно, либо здесь, либо по публичному URL.

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

Внутри всё устроено на грантах: сущность не имеет полей «навыки» и «бонусы», она имеет массив типизированных грантов — «дай владение навыком», «дай +1 к Ловкости», «предложи выбрать один из…». Словарь грантов закрыт — 29 типов. Вы не пишете DSL и не исполняете чужой код, вы наполняете типизированный JSON.

Статические файлы, забираются curl’ом без ключей и регистрации.

Артефакт URL
Схема всего датасета https://longstoryshort.app/schema/v1/wizard-dataset.json
Схемы по сущностям …/schema/v1/dataset-{class,subclass,race,subrace,background,feat}.json
Схема гранта …/schema/v1/grant.json
SRD 5.1 https://longstoryshort.app/datasets/srd-2014.json (~209 KB)
SRD 5.2 https://longstoryshort.app/datasets/srd-2024.json (~274 KB)
Слаги заклинаний SRD 5.1 https://longstoryshort.app/datasets/srd-spell-slugs-2014.json (318 записей: слаг, id, имя, круг, классы)
Слаги заклинаний SRD 5.2 https://longstoryshort.app/datasets/srd-spell-slugs-2024.json (339 записей: слаг, id, имя, круг, классы)

JSON Schema — draft-07. Схема генерируется из тех же исходников, по которым работает приложение, и включает description на каждом типе гранта: она же и есть справочник по полям, отдельной таблицы полей нет и не нужно.

SRD-наборы — не игрушечные примеры: это ровно те данные, на которых работает продакшен. Вместе — 24 класса и 24 подкласса на 20 уровней, 18 видов с линиями, черты, счётчики, развилки снаряжения.

Правите JSON руками — одна строка даёт автокомплит и подсветку ошибок в VS Code, JetBrains и любом редакторе с поддержкой JSON Schema:

{
"$schema": "https://longstoryshort.app/schema/v1/wizard-dataset.json",
"id": "my-homebrew",
"name": "Мой хоумбрю",
"system": "dnd_5",
"edition": "2024",
"author": "Вы",
"license": "CC-BY-4.0",
"races": []
}

Пишете код — валидируйте чем угодно:

const validate = new Ajv().compile(await (await fetch(SCHEMA_URL)).json());
if (!validate(dataset)) console.error(validate.errors);

Хотите понять формат — откройте srd-2024.json и найдите варвара.

29 типов, дискриминация по полю type. Точные поля каждого — в grant.json.

Канал Типы
Владения skill-fixed, skill-choice, expertise-choice, tool-fixed, tool-choice, language-fixed, language-choice, armor-prof, weapon-prof, saving-throw
Числа bonus, asi-fixed, asi-flexible, asi-pool, hp-die, speed, size
Контент trait, resource, feat, feat-choice
Заклинания spellcasting, spell-fixed, spell-choice
Снаряжение equipment-fixed, equipment-choice, gold, gold-dice
Развилка pick-one

Заклинания — три разных вопроса, три гранта. spellcasting объявляет, что сущность кастует, и как (характеристика, прогрессия, из чьего списка — spellList, когда он отличается от id класса, как у гибридов вроде Мистического рыцаря). spell-fixed выдаёт одно именное заклинание по слагу — всегда подготовленное, вне лимита. spell-choice предлагает игроку выбрать count заклинаний заданного круга (0 — заговоры) из списка spellList. У spell-fixed/spell-choice общие поля выдачи: ability (характеристика, пусто — наследует листовую), uses (count/countExpr + per: 'long-rest' | 'short-rest'

  • shortRestRegain — свой счётчик заклинания, а не ячейка), withSlots (можно ли поверх счётчика заплатить ещё и ячейкой), alwaysPrepared (сегодня выразим только как true).

Счётчик заклинания — это Ресурс, просто заведённый другим каналом, и поля восстановления у него те же. per говорит, какой отдых его восстанавливает; shortRestRegain — сколько возвращает КОРОТКИЙ отдых, формулой над теми же переменными (1, [PROF], ceil([LVL]/2)). Без него короткий отдых наполняет счётчик целиком, и шаблон «одно применение на коротком отдыхе, все на длинном» приходилось врать в одну из сторон. Поле осмысленно только при per: 'short-rest' — при 'long-rest' его никто не читает, и линт об этом предупредит.

Ячейки заклинаний растут из progression. Это не справочное поле: персонаж, собранный визардом, получает ячейки сам — по классу, уровню и семейству таблиц. Их четыре: full (бард, жрец, друид, чародей, волшебник), half (паладин, следопыт), third (Мистический рыцарь, Арканный ловкач) и pact (колдун — свой пул на листе). Переписывать числа в персонажа не надо: игрок может поправить любое, но хранится разница с вашей таблицей, а не итог, и левелап её не затирает.

Если прогрессия вашего класса не совпадает ни с одной из четырёх — объявите собственную таблицу, slotsByLevel. Двадцать одна строка (индекс = уровень персонажа, нулевая — заглушка), в строке девять чисел по кругам 1…9; строка короче девяти дополняется нулями. Её числа побеждают встроенные, но progression рядом не отменяется: тип продолжает отвечать за то, в какой пул едут ячейки и с каким весом класс сложится в мультиклассе. Уровень выше последней строки значит «ячеек нет», а не «как на последней», — поэтому короткая таблица это ошибка, а не предупреждение. Классу, чья таблица совпадает со встроенной, поле заводить не надо: скопированные 20×9 чисел — ровно то место, где ваши данные разойдутся с правилами при следующей правке.

Ячейки к тому же стали целями системы бонусов — spellSlot.1…spellSlot.9 и pactSlot.1…pactSlot.9 (полный список таргетов, как всегда, в grant.json). Предмет или черта, дающие лишнюю ячейку, выражаются обычным грантом bonus.

Свой класс-заклинатель: spellList обязателен. Список заклинаний резолвится по id класса, а собственного списка заклинаний у вашего класса в базе нет — без этого поля игрок получит ячейки и пустой список, из которого нечего выбрать, без единой ошибки. Укажите, чей список класс использует: "cleric", "wizard", любой id класса. То же и у подкласса-гибрида вроде Мистического рыцаря: у него classId: "fighter", а spellList: "wizard".

«Сколько можно взять» — отдельные поля, и без них счётчика не будет. cantripsByLevel и knownByLevel — таблицы по уровням (индекс = уровень персонажа, нулевой элемент — заглушка); preparedFormula — там, где подготовленные считаются формулой, а не таблицей; bookAtFirst/bookPerLevel — книга волшебника, отдельный счёт от подготовленных. Отсутствие числа значит «не показываем», а не «ноль»: лист не станет угадывать лимит за вас.

У формулы три значения, и половина уровня в них округляется по-разному:

Значение Кто так считает На 1 уровне при модификаторе +3
mod+level жрец, друид, волшебник 4
mod+half-level паладин, следопыт — половина вниз 3
mod+half-level-up половина вверх 4

Разница не косметическая: на каждом нечётном уровне это целое заклинание. Общее у всех трёх — минимум одно, иначе кастер с отрицательным модификатором не смог бы подготовить ничего.

Слаг — не то, что вы придумаете. spell-fixed/выбранное игроком заклинание резолвятся по slug — слагифицированному английскому имени (hellish-rebuke), которое должно совпасть с тем, что реально лежит в базе заклинаний Long Story Short. Слаг ссылается на заклинание вне самого датасета — свой корпус заклинаний вы поставляете отдельным файлом (импорт заклинаний — отдельный путь от импорта датасета), а датасет лишь называет слаг. Ссылка на SRD-заклинание резолвится всегда — файлы слагов из таблицы выше говорят, какие это. Ссылка на НЕ-SRD заклинание резолвится только у игрока, который уже загрузил заклинание с этим слагом (свой хоумбрю или файл заклинаний, который вы выложили вместе с датасетом) — это не ошибка датасета, а нормальное «пока нет».

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

Большая часть интересного в персонаже — выборы: какое наследие, какой боевой стиль, какие воззвания. Визард умеет их не только показывать, но и применять: выбранное едет на лист целиком — числа, текст, счётчики, языки, чувства, заклинания, навыки, — а смена выбора убирает старое. Датасету хватает трёх грантов:

Механика Грант Для чего
Одно из нескольких именованный pick-one размер вида, драконье наследие, «владение ИЛИ экспертиза» у черты, договор колдуна, вариант классовой способности
Одна черта на выбор feat с featId: "any" черта происхождения, черта вариантного человека
Несколько из списка feat-choice воззвания колдуна, метамагия чародея, приёмы Мастера боя, боевые стили, инфузии, эпические дары, общие черты на уровнях

Эти гранты складываются друг с другом, и это главное, что стоит знать при проектировании механики:

  • Выбор, который растёт с уровнем. Слот feat-choice в уровневой строке появляется у игрока с этого уровня: «2 воззвания на 2 уровне, ещё одно на 5» — три гранта, а не таблица.
  • Выбор внутри выбора. Развилка внутри черты, слот внутри черты (черта «Адепт потусторонних сил» даёт одно воззвание), слот или развилка внутри ветки другой развилки («Договор гримуара» открывает свой выбор заговоров).
  • Своя механика «выбери из списка». Категория черты — произвольная строка. Метамагия, приёмы, руны, благословения — объявите варианты чертами своей категории и дайте классу слот этой категории. Отдельный тип гранта не нужен.
  • Порог по уровню и повторы. Черта с minLevel выбирается только с этого уровня; черта без repeatable берётся один раз на персонажа, и визард это соблюдает сам.
  • Способность класса, а не черта. Выбранное в слоте с classFeature: true ложится на лист в «Умения и способности», рядом с остальными умениями класса.
  • «Повышение характеристик ИЛИ черта». Развилку с веткой-слотом собрать можно, и ветка «черта» работает, но распределение характеристик внутри гранта от класса пока не применяется.
  • Требования, кроме уровня («знает заговор Мистический заряд», «Договор клинка»). Пишите их текстом в prerequisite — игрок увидит его на карточке, но визард его не проверяет.
  • Разные ответы развилки у копий повторяемой черты. Ответ хранится по id развилки, поэтому все копии делят один.

pick-one вкладывает гранты в свои options — обходя датасет, рекурсируйте внутрь, иначе половина контента пройдёт мимо вашей валидации.

Форм у развилки две, и различаются они наличием id:

  • Без id — развилка стартового снаряжения («набор ИЛИ золото»). Её читает только канал снаряжения; всё, что не снаряжение и не золото, внутри неё не сделает ничего.
  • С id и options[].id — именованная развилка, «выбери одно из»: размер вида, наследие, «владение ИЛИ экспертиза» у черты. Работает где угодно: у сущности, в уровневой строке, внутри черты, внутри выбранной ветки другой именованной развилки.

Выбранная ветка именованной развилки доезжает во все каналы ровно как гранты, выданные напрямую: числа, текст, счётчики, языки, инструменты, чувства, заклинания, навыки и экспертиза. До ответа игрока ветка не даёт ничего. Развилка сущности рисуется на её шаге визарда, развилка черты — под карточкой черты.

Черта с развилкой:

{
"id": "observant",
"label": "Наблюдатель",
"grants": [
{
"type": "pick-one",
"id": "observant-skill-benefit",
"label": "Наблюдатель",
"options": [
{
"id": "expertise",
"label": "Экспертиза",
"grants": [{ "type": "expertise-choice", "count": 1,
"options": ["insight", "investigation", "perception"] }]
},
{
"id": "proficiency",
"label": "Владение навыком",
"grants": [{ "type": "skill-choice", "count": 1,
"options": ["insight", "investigation", "perception"] }]
}
]
}
]
}

id развилки уникален на весь датасет, а не внутри сущности. Ответ игрока хранится по нему в одном пуле на персонажа. Отсюда два следствия:

  • если одну черту выдают несколько сущностей, развилка у неё одна и ответ общий;
  • проверяя уникальность у себя, считайте развилку черты один раз — там, где черта описана, а не на каждой ссылке feat на неё. Иначе черта, которую выдают две предыстории, покажется дублем.

options[].id уникален внутри своей развилки.

Смена ветки пересобирает текст черты. Трейты выбранной ветки попадают в блок черты на листе, и при смене ветки блок пересобирается. Правки игрока внутри этого блока при этом теряются. У черты без развилки блок не трогается.

Черта в датасете — не только «черта» из книги. Это любой именованный пакет грантов, который игрок может выбрать из списка: воззвание, вариант метамагии, приём, боевой стиль. Список задаёт category — произвольная строка. SRD-наборы пользуются origin, general, fighting-style, epic-boon; для воззваний принято invocation; свою категорию заводите просто тем, что пишете её у черт и у слота.

feat с featId: "any" — слот на одну черту. Игрок выбирает её сам из черт подключённых наборов, при заданном category — только из этой категории. Работает у вида, линии, предыстории, класса и подкласса.

feat-choice — слот на несколько черт:

Поле Что значит
id стабильный id слота: по нему хранится выбор игрока. Уникален на весь датасет, в одном пространстве с id развилок
count сколько черт выбрать в этом слоте
category из какой категории; без неё — из всех черт подключённых наборов
label заголовок слота в визарде, например «Таинственные воззвания»
classFeature true — выбранное это способности класса: текст ложится в «Умения и способности», а не в «Черты». Работает у класса и подкласса

У самой черты для слотов есть два поля:

  • minLevel — уровень персонажа, с которого черту можно выбрать. Ниже — карточка заблокирована с подписью «с N уровня»;
  • repeatable: true — черту можно взять больше одного раза.

Правила выбора визард соблюдает сам, и лист применяет ровно то же, что показал визард:

  • в слоте применяются первые count выбранных черт;
  • черта без repeatable применяется один раз на персонажа — ни в двух слотах, ни в слоте и одновременно выданная напрямую (feat или слот any). В визарде такая карточка заблокирована с подписью «уже выбрано»;
  • слот из уровневой строки появляется у игрока с этого уровня. Опустили уровень — слот исчезает, его эффекты уходят, но выбор сохраняется и вернётся с уровнем.

Выбранная черта едет на лист со всеми своими грантами: бонусами, текстом, счётчиками, заклинаниями, языками, чувствами, развилками.

Воззвания колдуна 2014 — три слота одного пула:

{
"id": "warlock",
"grants": [ … ],
"leveledGrants": [
{ "level": 2, "grants": [{ "type": "feat-choice", "id": "warlock-invocations-2",
"label": "Таинственные воззвания", "count": 2, "category": "invocation",
"classFeature": true }] },
{ "level": 5, "grants": [{ "type": "feat-choice", "id": "warlock-invocations-5",
"label": "Таинственные воззвания", "count": 1, "category": "invocation",
"classFeature": true }] }
]
}

Воззвание с требованием уровня:

{
"id": "thirsting-blade",
"label": "Жаждущий клинок",
"category": "invocation",
"prerequisite": "Колдун 5 уровня, Договор клинка",
"minLevel": 5,
"grants": [{ "type": "trait", "id": "thirsting-blade", "name": "Жаждущий клинок",
"description": "…" }]
}

Своя механика — метамагия чародея. Варианты — черты категории metamagic, классу — слоты этой категории:

{ "type": "feat-choice", "id": "sorcerer-metamagic-3", "label": "Метамагия",
"count": 2, "category": "metamagic", "classFeature": true }

Так же собираются приёмы Мастера боя, инфузии изобретателя, руны, благословения — всё, что устроено как «выбери N из списка, иногда ещё на уровне».

bonus бьёт по таргету. Схема содержит закрытый enum допустимых таргетов — в нём только те, которые лист реально читает. Всё остальное отклоняется наравне с опечаткой, и это то поведение, которое вам нужно: бонус на неподдержанный таргет так же невидим, как prof.skill.stelth. Открытые семейства, где ключ приходит из данных персонажа (weapon.<любое>.attack), выражены через pattern и проходят.

Схема проверяет форму, а не смысл. Валидный по схеме датасет всё ещё может молча ничего не делать — потому что оставшиеся правила межполевые и межсущностные, а JSON Schema так не умеет.

Схема поймает Схема НЕ поймает
Опечатку в имени поля или лишнее поле Всё из таблицы ниже
Неизвестный тип гранта
Опечатку в таргете бонуса или ключе навыка
Таргет, который лист не читает
Отсутствие обязательного поля, неверный тип значения

Проверки, которые надо сделать самим. Ровно этот список гоняет наш импортёр; error блокирует импорт, warning пускает — но почти каждый warning означает «грант записан и не сделает ничего»:

Проверка Что ломается
bonus несёт ровно одно из value | expr error
expr-формула вычисляется error
Ключи навыков, характеристик, брони и оружия — из известных error
Грант выбора предлагает хотя бы один слот (count > 0) error
id уникален внутри коллекции error
classId / raceId / featId ведёт в существующую сущность error
Безымянный pick-one не вложен в ветку другой развилки — там его не открывает никто error
id именованного pick-one уникален на весь датасет, options[].id — внутри развилки error
feat-choice несёт id и count > 0; id не совпадает с другим слотом или развилкой error
minLevel черты — целое от 1 до 20 error
classFeature у слота вида, линии или предыстории — флаг ничего не сделает warning
repeatable-черта с именованной развилкой — у всех копий один ответ warning
resource несёт ровно одно из max | maxExpr; формулы вычисляются error
Лицензия — из белого списка (см. «Жёсткие правила») error
trait несёт params и прозу — текст потеряется, канал не тот warning
Формула броска у trait парсится warning
pairId счётчика указывает на существующую черту (или явный null) warning
Счётчик стоит там, где его никто не читает: у самой предыстории или внутри безымянной развилки warning
Тип гранта реален, но канала-потребителя у него нет warning
Слот черты (feat any или feat-choice) категории, под которую нет ни одной черты — ловит и опечатку в категории warning
spell-choice предлагает хотя бы одно заклинание (count > 0), круг задан, spellList не пуст error
Два spell-choice одного круга на одной сущности — пики склеятся в один слот error
spell-fixed/spell-choice: uses без count и без countExpr, или сразу оба error
uses.shortRestRegain не вычисляется в число на каком-то уровне — короткий отдых молча восстановит счётчик целиком error
uses.shortRestRegain при per: 'long-rest' — поле не сработает warning
spell-fixed: слаг не входит в наш публичный список SRD-слагов (см. таблицу выше) warning
spellcasting: slotsByLevel — не массив строк по уровням, или короче 21 записи, или в строке не целое неотрицательное число error
spellcasting: slotsByLevel пуста, или строка длиннее девяти кругов warning

Ссылки между наборами легальны. Хоумбрю-книга подклассов вправе ссылаться classId на SRD-класс, которого в ней самой нет. Ваш валидатор должен резолвить ссылки не только внутри проверяемого файла, но и по всем подключённым наборам, иначе пометит битой каждую такую ссылку.

Готового пакета с этими проверками мы пока не отдаём — реализуйте у себя.

id сущности — внешний ключ, а не название. По нему резолвятся ссылки и привязываются выборы игрока. Переименовали id в опубликованном наборе — сломали чужие листы. Слаг английского названия, один раз и навсегда.

Лицензия обязательна и проверяется. Пустое поле отклоняет импорт так же, как неподходящее. Проходят только CC0, CC-BY и CC-BY-SA с версией (CC-BY-4.0, не CC-BY). OGL, MIT, свободный текст, любые NC/ND-варианты — нет. Это касается и вашего собственного оригинального контента.

Правка датасета ретроактивно меняет уже собранных персонажей. Поле version есть, но импорт его не читает, и защиты с нашей стороны пока нет. Ваше разделение «черновик / опубликовано» — единственный барьер. v1 в URL схемы — про версию формата, не про версию контента.

Публиковать датасет пока некуда. Реестра сообщества нет; подключение идёт через импорт JSON-файла.

Тексты черт лежат в description у trait-гранта. Это единственный канал; файлы самодостаточные, скачали — и у вас полный набор с текстами. Авторьте так же.

Всё описано. SRD 5.1 — 183 черты, SRD 5.2 — 232 — все с текстом.

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

Валидатор в своём пайплайне. Самое дешёвое и самое полезное: схема — в CI, проверки из раздела выше — рядом.

Визуальный редактор. Схема даёт формы и типы полей почти даром. Но enum таргетов — не украшение: никогда не давайте автору вводить таргет строкой, стройте пикер из списка в схеме.

Генерация датасетов моделью. Артефакты складываются в связку:

  1. По-сущностные схемы — не для удобства, а по необходимости. Полная схема — union из 29 вариантов с вложенностью в несколько уровней; для structured output это плохо. Генерируйте один класс за вызов против dataset-class.json — дешевле и заметно надёжнее.
  2. SRD как few-shot. Модель учится формату на примерах существенно лучше, чем на схеме, а у вас их 24 класса и 18 видов — эталонных, не выдуманных.
  3. Схема закрывает пространство выбора. Закрытый enum таргетов против свободной строки — это разница между «модель попадает» и «модель придумывает prof.skill.stelth».
  4. Проверки замыкают цикл. Сгенерировали → провалидировали → прогнали проверки → вернули сообщение в модель → перегенерировали. Без четвёртого шага вы получите датасеты, которые проходят валидацию и молча не работают.

Конвертер из своего формата. SRD-наборы — ваш golden file: гоняйте конвертацию в обратную сторону и сверяйтесь.