Модуль:Магия
Материал из Лабиринт - база знаний
Дополнительные действия
Для документации этого модуля может быть создана страница Модуль:Магия/doc
-- Навигация и карточки раздела «Магия».
--
-- Ведётся в git: pages/Модуль/Магия.lua в репозитории playlabirint/wiki.
--
-- Дерево школ, направлений и навыков лежит на вики страницей данных
-- Модуль:Магия/дерево.json — её правят редакторы, когда добавляют навык или
-- направление. Раньше то же дерево было размазано по десятку шаблонов с
-- #switch, и добавление навыка означало правку в трёх местах.
--
-- Старые шаблоны («Магические школы», «Магические скиллы», «Меню магии» и
-- прочие) стали тонкими обёртками над этим модулем, поэтому статьи менять не
-- пришлось.
local character = require( 'Модуль:Персонаж' )
local icon = character.icon
local p = {}
local TREE_PAGE = 'Модуль:Магия/дерево.json'
-- Разделитель списков от #labmembers и #labparents расширения LabirintDesign.
local SEP = '‖'
-- Цвет школы: у каждой свой, как в макетах.
local SCHOOL_ACCENT = {
['Школа Стихий'] = 'lab-acc-amber',
['Школа Электро'] = 'lab-acc-cyan',
['Школа Света'] = 'lab-acc-green',
['Школа Тьмы'] = 'lab-acc-violet',
['Школа Разума'] = 'lab-acc-pink',
['Школа Измерений'] = 'lab-acc-teal',
}
-- Разделы верхнего меню.
local MENU = {
{ page = 'Магия:Магическая система', label = 'Магическая система' },
{ page = 'Магия:Магические классы', label = 'Классы' },
{ page = 'Магия:Магические школы', label = 'Школы' },
}
-- Тип заклинания: подпись и значок.
local SPELL_TYPES = {
['Направленная'] = { label = 'Направленная магия', ico = 'cast' },
['Защитная'] = { label = 'Защитная магия', ico = 'shield' },
['Нейтральная'] = { label = 'Нейтральная магия', ico = 'wand' },
['Ментальная'] = { label = 'Ментальная магия', ico = 'eye' },
}
local function nonEmpty( s )
if s == nil then return nil end
s = mw.text.trim( tostring( s ) )
return s ~= '' and s or nil
end
-- Сравнение названий: «тёмных» и «темных», разный регистр.
local function same( a, b )
if not a or not b then return false end
local function key( s )
return mw.ustring.lower( ( mw.ustring.gsub( tostring( s ), 'ё', 'е' ) ) )
end
return key( a ) == key( b )
end
-- Длина списка из loadJsonData: данные приходят обёрткой только для чтения,
-- и оператор # на ней всегда даёт ноль.
local function count( list )
local n = 0
for _ in ipairs( list or {} ) do n = n + 1 end
return n
end
local function tree()
local ok, data = pcall( mw.loadJsonData, TREE_PAGE )
if not ok or type( data ) ~= 'table' then return { ['школы'] = {}, ['классы'] = {} } end
return {
['школы'] = type( data['школы'] ) == 'table' and data['школы'] or {},
['классы'] = type( data['классы'] ) == 'table' and data['классы'] or {},
}
end
-- Где мы в дереве: школа, направление и навык текущей страницы.
local function locate( data, page )
for _, school in ipairs( data['школы'] ) do
if same( school['страница'], page ) or same( school['название'], page ) then
return school
end
for _, dir in ipairs( school['направления'] or {} ) do
if same( dir['страница'], page ) then
return school, dir
end
for _, skill in ipairs( dir['навыки'] or {} ) do
if same( skill['страница'], page ) then
return school, dir, skill
end
end
end
end
end
local function link( page, label, class )
local el = mw.html.create( 'span' ):addClass( class )
el:wikitext( '[[' .. page .. '|' .. label .. ']]' )
return el
end
local function tile( parent, class, item, active )
local el = parent:tag( 'span' ):addClass( class )
if active then el:addClass( 'is-on' ) end
local iconFile = nonEmpty( item['иконка'] )
if iconFile then
el:tag( 'span' ):addClass( 'lab-magic__ico' )
:wikitext( '[[Файл:' .. iconFile .. '|64px|link=|alt=]]' )
end
el:tag( 'span' ):addClass( 'lab-magic__name' )
:wikitext( '[[' .. item['страница'] .. '|' .. item['название'] .. ']]' )
return el
end
-- ---------- Меню и путь ----------
function p.menu( frame )
local data = tree()
local page = mw.title.getCurrentTitle().prefixedText
local school, dir, skill = locate( data, page )
local current = nonEmpty( frame:getParent().args[1] )
local bar = mw.html.create( 'div' ):addClass( 'lab-magic__bar lab-blk' )
local nav = bar:tag( 'span' ):addClass( 'lab-magic__menu' )
for _, item in ipairs( MENU ) do
local active = same( item.page, page ) or same( item.label, current )
or ( school and item.label == 'Школы' )
or ( current == 'Магические классы' and item.label == 'Классы' )
local el = nav:tag( 'span' ):addClass( 'lab-magic__menuItem' )
if active then el:addClass( 'is-on' ) end
el:wikitext( '[[' .. item.page .. '|' .. item.label .. ']]' )
end
-- Путь: школа → направление → текущая страница.
if school then
local crumbs = bar:tag( 'span' ):addClass( 'lab-magic__crumbs' )
local function crumb( item, last )
local el = crumbs:tag( 'span' ):addClass( 'lab-magic__crumb' )
local iconFile = nonEmpty( item['иконка'] )
if iconFile then
el:tag( 'span' ):addClass( 'lab-magic__crumbIco' )
:wikitext( '[[Файл:' .. iconFile .. '|24px|link=|alt=]]' )
end
if last then
el:addClass( 'is-current' ):wikitext( item['название'] )
else
el:wikitext( '[[' .. item['страница'] .. '|' .. item['название'] .. ']]' )
end
end
crumb( school, not dir )
if dir then crumb( dir, not skill ) end
if skill then crumb( skill, true ) end
end
-- Поиск по способностям: со страницы раздела можно попасть сразу в нужную,
-- не проходя школу и направление. Поле рисует скрипт, а список приходит
-- готовым — без скрипта он просто не показывается.
bar:tag( 'span' ):addClass( 'lab-magic__find' )
local list = bar:tag( 'span' ):addClass( 'lab-magic__findList' ):attr( 'hidden', 'hidden' )
for _, item in ipairs( data['школы'] ) do
for _, dir in ipairs( item['направления'] or {} ) do
for _, skill in ipairs( dir['навыки'] or {} ) do
local el = list:tag( 'span' ):addClass( 'lab-magic__findItem' )
:attr( 'data-where', item['название'] .. ' · ' .. dir['название'] )
el:wikitext( '[[' .. skill['страница'] .. '|' .. skill['название'] .. ']]' )
end
end
end
local root = mw.html.create( 'div' ):addClass( 'lab-magic' )
if school and SCHOOL_ACCENT[school['название']] then
root:addClass( SCHOOL_ACCENT[school['название']] )
end
root:node( bar )
return tostring( root )
end
-- ---------- Школы, направления, навыки ----------
-- На странице навыка плитки школ и направлений не нужны: там колонка школы
-- сбоку и путь в шапке, как в макете. Шаблоны зовут эти функции всегда —
-- решает модуль.
local function onSkillPage()
local data = tree()
local _, _, skill = locate( data, mw.title.getCurrentTitle().prefixedText )
return skill ~= nil
end
-- Карточки школ с направлениями: на обзорных страницах («Магическая система»,
-- «Магические школы») читателю нужен не переключатель, а что внутри школы.
local function schoolCards( data )
local grid = mw.html.create( 'div' ):addClass( 'lab-magic__scards' )
for _, school in ipairs( data['школы'] ) do
local card = grid:tag( 'div' ):addClass( 'lab-magic__scard ' .. ( SCHOOL_ACCENT[school['название']] or '' ) )
local iconFile = nonEmpty( school['иконка'] )
if iconFile then
card:tag( 'span' ):addClass( 'lab-magic__scardIco' )
:wikitext( '[[Файл:' .. iconFile .. '|72px|link=|alt=]]' )
end
local body = card:tag( 'div' ):addClass( 'lab-magic__scardBody' )
body:tag( 'div' ):addClass( 'lab-magic__scardName lab-display' )
:wikitext( '[[' .. school['страница'] .. '|' .. school['название'] .. ']]' )
local dirs = body:tag( 'div' ):addClass( 'lab-magic__scardDirs' )
for _, dir in ipairs( school['направления'] or {} ) do
dirs:tag( 'span' ):addClass( 'lab-magic__dirChip' )
:wikitext( '[[' .. dir['страница'] .. '|' .. dir['название'] .. ']]' )
end
end
return tostring( grid )
end
function p.schools( frame )
local data = tree()
local page = mw.title.getCurrentTitle().prefixedText
local current = nonEmpty( frame:getParent().args[1] )
-- Внутри школы плитки работают переключателем, снаружи — витриной.
local school = locate( data, page )
if not school and not current then
return schoolCards( data )
end
local grid = mw.html.create( 'div' ):addClass( 'lab-magic__stabs' )
for _, item in ipairs( data['школы'] ) do
local el = grid:tag( 'span' )
:addClass( 'lab-magic__stab ' .. ( SCHOOL_ACCENT[item['название']] or '' ) )
:attr( 'data-school', item['название'] )
if same( item['страница'], page ) or same( item['название'], current ) then
el:addClass( 'is-on' )
end
local iconFile = nonEmpty( item['иконка'] )
if iconFile then
el:tag( 'span' ):addClass( 'lab-magic__stabIco' )
:wikitext( '[[Файл:' .. iconFile .. '|44px|link=|alt=]]' )
end
el:wikitext( '[[' .. item['страница'] .. '|' .. item['название'] .. ']]' )
end
return tostring( grid )
end
-- Колонка школы слева: направления с числом навыков, у текущего направления
-- раскрыты сами навыки. Стоит на страницах школы, направления и навыка —
-- рисует её p.directions, чтобы колонка была одна на странице.
local function treeColumn( data, page )
local school, current = locate( data, page )
if not school then return nil end
local box = mw.html.create( 'div' )
:addClass( 'lab-magic__tree lab-blk ' .. ( SCHOOL_ACCENT[school['название']] or '' ) )
local head = box:tag( 'div' ):addClass( 'lab-blk__head' )
head:tag( 'span' ):addClass( 'lab-bar' )
head:tag( 'span' ):addClass( 'lab-blk__title' ):wikitext( school['название'] )
head:tag( 'span' ):addClass( 'lab-hline' )
head:tag( 'span' ):addClass( 'lab-mark' )
local body = box:tag( 'div' ):addClass( 'lab-magic__treeBody' )
for _, dir in ipairs( school['направления'] or {} ) do
local active = current and same( dir['название'], current['название'] )
local row = body:tag( 'span' ):addClass( 'lab-magic__treeDir' )
if active then row:addClass( 'is-on' ) end
local iconFile = nonEmpty( dir['иконка'] )
if iconFile then
row:tag( 'span' ):addClass( 'lab-magic__treeIco' )
:wikitext( '[[Файл:' .. iconFile .. '|28px|link=|alt=]]' )
end
row:wikitext( '[[' .. dir['страница'] .. '|' .. dir['название'] .. ']]' )
row:tag( 'span' ):addClass( 'lab-magic__count' ):wikitext( count( dir['навыки'] ) )
if active then
for _, skill in ipairs( dir['навыки'] or {} ) do
local el = body:tag( 'span' ):addClass( 'lab-magic__treeSkill' )
if same( skill['страница'], page ) then el:addClass( 'is-on' ) end
el:wikitext( '[[' .. skill['страница'] .. '|' .. skill['название'] .. ']]' )
end
end
end
return box
end
-- Шапка страницы школы или направления: значок, надзаголовок и название.
local function sectionHead( item, eyebrow )
local head = mw.html.create( 'div' ):addClass( 'lab-magic__head' )
local iconFile = nonEmpty( item['иконка'] )
if iconFile then
local art = head:tag( 'span' ):addClass( 'lab-magic__headIco' )
art:tag( 'span' ):addClass( 'lab-magic__headGlow' )
art:tag( 'span' ):wikitext( '[[Файл:' .. iconFile .. '|104px|link=|alt=]]' )
end
local body = head:tag( 'div' ):addClass( 'lab-magic__headBody' )
body:tag( 'div' ):addClass( 'lab-magic__headEyebrow' ):wikitext( eyebrow )
body:tag( 'div' ):addClass( 'lab-magic__headName lab-display' ):wikitext( item['название'] )
return head
end
-- Столбец направления: значок, название, число навыков и сами навыки
-- строками. Не карточка: колонка школы слева уже показывает направления, и
-- второй раз обводить их рамкой незачем — здесь нужен список способностей.
local function directionColumn( parent, dir )
local column = parent:tag( 'div' ):addClass( 'lab-magic__dircol' )
local head = column:tag( 'div' ):addClass( 'lab-magic__dircolHead' )
local iconFile = nonEmpty( dir['иконка'] )
if iconFile then
head:tag( 'span' ):addClass( 'lab-magic__dircolIco' )
:wikitext( '[[Файл:' .. iconFile .. '|24px|link=|alt=]]' )
end
head:tag( 'span' ):addClass( 'lab-magic__dircolName' )
:wikitext( '[[' .. dir['страница'] .. '|' .. dir['название'] .. ']]' )
head:tag( 'span' ):addClass( 'lab-magic__count' ):wikitext( count( dir['навыки'] ) )
local list = column:tag( 'div' ):addClass( 'lab-magic__dircolList' )
for _, skill in ipairs( dir['навыки'] or {} ) do
list:tag( 'span' ):addClass( 'lab-magic__dircolSkill' )
:wikitext( '[[' .. skill['страница'] .. '|' .. skill['название'] .. ']]' )
end
end
function p.directions( frame )
local args = frame:getParent().args
local data = tree()
local page = mw.title.getCurrentTitle().prefixedText
local schoolName = nonEmpty( args[1] )
local school, current, skill = locate( data, page )
school = school or ( function ()
for _, item in ipairs( data['школы'] ) do
if same( item['название'], schoolName ) then return item end
end
end )()
if not school then return '' end
local out = {}
local column = treeColumn( data, page )
if column then out[#out + 1] = tostring( column ) end
-- Шапка страницы: у школы — её значок и название, у направления — свои.
-- На странице навыка шапку рисует Шаблон:Тип магии вместе с типом.
if not skill then
if current then
out[#out + 1] = tostring( sectionHead( current, school['название'] ) )
else
out[#out + 1] = tostring( sectionHead( school, 'Магические школы' ) )
end
end
return table.concat( out )
end
-- Первая фраза статьи: короткое пояснение к способности в списке направления.
-- Текст берётся со страницы навыка — отдельно его никто не пишет.
local function summary( page )
local title = page and mw.title.new( page )
local text = title and title:getContent()
if not text then return nil end
-- Вызовы шаблонов, заголовки и разметку выбрасываем: нужна проза.
text = text:gsub( '%b{}', '' ):gsub( '<[^>]->', '' )
for line in ( text .. '\n' ):gmatch( '(.-)\r?\n' ) do
line = mw.text.trim( line )
if line ~= '' and not line:match( '^[=*:;#|_]' ) then
line = line:gsub( "'''?", '' ):gsub( '%[%[[^%]|]-|([^%]]-)%]%]', '%1' ):gsub( '%[%[([^%]]-)%]%]', '%1' )
-- До конца первого предложения, но не длиннее строки списка.
local sentence = mw.ustring.match( line, '^(.-[%.!%?])%s' ) or line
if mw.ustring.len( sentence ) > 180 then
sentence = mw.ustring.sub( sentence, 1, 177 ) .. '…'
end
return sentence
end
end
return nil
end
-- Тип заклинания со страницы навыка: {{Тип магии|Направленная}}.
local function skillType( page )
local title = page and mw.title.new( page )
local text = title and title:getContent()
if not text then return nil end
-- Класс символов сюда не годится: кириллица многобайтовая, и [Тт] в
-- шаблоне Lua разбирается по байтам.
local value = text:match( '{{%s*Тип[_ ]магии%s*|%s*([^|}]-)%s*[|}]' )
or text:match( '{{%s*тип[_ ]магии%s*|%s*([^|}]-)%s*[|}]' )
return value and nonEmpty( value ) or nil
end
-- Способности направления: название, тип и первая фраза статьи. Ставится под
-- текстом направления — Шаблон:Способности направления.
function p.directionSkills( frame )
local data = tree()
local page = mw.title.getCurrentTitle().prefixedText
local _, direction = locate( data, page )
if not direction then
local name = nonEmpty( frame:getParent().args[1] )
for _, school in ipairs( data['школы'] ) do
for _, dir in ipairs( school['направления'] or {} ) do
if same( dir['название'], name ) then direction = dir end
end
end
end
if not direction then
return '<strong class="error">Шаблон:Способности направления — направление не найдено</strong>'
end
local grid = mw.html.create( 'div' ):addClass( 'lab-magic__skilllist' )
for _, skill in ipairs( direction['навыки'] or {} ) do
local row = grid:tag( 'div' ):addClass( 'lab-magic__skillrow' )
local head = row:tag( 'div' ):addClass( 'lab-magic__skillhead' )
head:tag( 'span' ):addClass( 'lab-magic__skillname lab-display' )
:wikitext( '[[' .. skill['страница'] .. '|' .. skill['название'] .. ']]' )
local kind = skillType( skill['страница'] )
local spell = kind and SPELL_TYPES[kind]
if spell then
local chip = head:tag( 'span' ):addClass( 'lab-magic__skillkind' )
chip:node( icon( spell.ico ) )
chip:wikitext( spell.label )
end
local text = summary( skill['страница'] )
if text then
row:tag( 'div' ):addClass( 'lab-magic__skilltext' ):wikitext( text )
end
end
return tostring( grid )
end
-- Витрина направлений школы: ставится в конец страницы школы, под текстом,
-- поэтому отдельным шаблоном — Шаблон:Направления школы.
function p.schoolDirections( frame )
local data = tree()
local page = mw.title.getCurrentTitle().prefixedText
local school = locate( data, page )
if not school then
local name = nonEmpty( frame:getParent().args[1] )
for _, item in ipairs( data['школы'] ) do
if same( item['название'], name ) then school = item end
end
end
if not school then
return '<strong class="error">Шаблон:Направления школы — школа не найдена</strong>'
end
local grid = mw.html.create( 'div' )
:addClass( 'lab-magic__dircols ' .. ( SCHOOL_ACCENT[school['название']] or '' ) )
for _, dir in ipairs( school['направления'] or {} ) do
directionColumn( grid, dir )
end
return tostring( grid )
end
-- Список навыков направления: на странице направления и на самих навыках.
-- Кнопки навыков в начале страницы направления больше не нужны: под текстом
-- стоит список способностей с пояснениями, а рядом — колонка школы.
function p.skills()
return ''
end
function p.tree( frame )
local column = treeColumn( tree(), mw.title.getCurrentTitle().prefixedText )
return column and tostring( column ) or ''
end
-- Все способности одним списком: свёрнутый показывает школы, дальше
-- раскрывается до направлений и самих способностей. Так можно посмотреть
-- дерево целиком, не уходя со страницы.
--
-- Разметку отдаёт сервер целиком, сворачивает её скрипт (resources/magic.js):
-- без скрипта список просто открыт, и поисковик видит все названия.
function p.outline( frame )
local data = tree()
local root = mw.html.create( 'div' ):addClass( 'lab-outline lab-blk' )
local head = root:tag( 'div' ):addClass( 'lab-blk__head' )
head:tag( 'span' ):addClass( 'lab-bar' )
head:tag( 'span' ):addClass( 'lab-blk__title' )
:wikitext( nonEmpty( frame:getParent().args['Заголовок'] ) or 'Школы и способности' )
head:tag( 'span' ):addClass( 'lab-hline' )
head:tag( 'span' ):addClass( 'lab-mark' )
-- Список прокручивается внутри блока, заголовок остаётся на месте.
local list = root:tag( 'div' ):addClass( 'lab-outline__list' )
for _, school in ipairs( data['школы'] ) do
local item = list:tag( 'div' )
:addClass( 'lab-outline__school ' .. ( SCHOOL_ACCENT[school['название']] or '' ) )
local head = item:tag( 'div' ):addClass( 'lab-outline__row' )
head:node( icon( 'chev' ):addClass( 'lab-outline__chev' ) )
local iconFile = nonEmpty( school['иконка'] )
if iconFile then
head:tag( 'span' ):addClass( 'lab-outline__ico' )
:wikitext( '[[Файл:' .. iconFile .. '|28px|link=|alt=]]' )
end
head:tag( 'span' ):addClass( 'lab-outline__name' ):wikitext( school['название'] )
-- Сколько внутри: по числу видно, куда идти.
local dirs, skills = 0, 0
for _, dir in ipairs( school['направления'] or {} ) do
dirs = dirs + 1
skills = skills + count( dir['навыки'] )
end
head:tag( 'span' ):addClass( 'lab-outline__count' ):wikitext(
dirs .. ' ' .. character.plural( dirs, 'направление', 'направления', 'направлений' )
.. ' · ' .. skills .. ' ' .. character.plural( skills, 'способность', 'способности', 'способностей' )
)
-- Страница школы — отдельным значком: вся остальная строка раскрывает
-- список, и курсор по ней не прыгает со ссылки на обычный.
head:tag( 'span' ):addClass( 'lab-outline__go' )
:wikitext( '[[' .. school['страница'] .. '|→]]' )
local body = item:tag( 'div' ):addClass( 'lab-outline__body' )
for _, dir in ipairs( school['направления'] or {} ) do
local sub = body:tag( 'div' ):addClass( 'lab-outline__dir' )
local row = sub:tag( 'div' ):addClass( 'lab-outline__row lab-outline__row--dir' )
row:node( icon( 'chev' ):addClass( 'lab-outline__chev' ) )
local dirIcon = nonEmpty( dir['иконка'] )
if dirIcon then
row:tag( 'span' ):addClass( 'lab-outline__ico' )
:wikitext( '[[Файл:' .. dirIcon .. '|24px|link=|alt=]]' )
end
row:tag( 'span' ):addClass( 'lab-outline__name' ):wikitext( dir['название'] )
row:tag( 'span' ):addClass( 'lab-outline__count' ):wikitext( count( dir['навыки'] ) )
row:tag( 'span' ):addClass( 'lab-outline__go' )
:wikitext( '[[' .. dir['страница'] .. '|→]]' )
local list = sub:tag( 'div' ):addClass( 'lab-outline__body lab-outline__skills' )
for _, skill in ipairs( dir['навыки'] or {} ) do
list:tag( 'span' ):addClass( 'lab-outline__skill' )
:wikitext( '[[' .. skill['страница'] .. '|' .. skill['название'] .. ']]' )
end
end
end
return tostring( root )
end
-- ---------- Классы ----------
-- Порядок статусов: в колонке класса сначала те, кто в игре.
local STATUS_ORDER = { ['В игре'] = 1, ['Недоступен'] = 2, ['Вне игры'] = 3 }
local STATUS_PAGE = 'Модуль:Персонажи/статусы.json'
local NAMES_SHOWN = 16
-- Ключ оформления класса: по нему берутся фон шапки и цвет (Модуль:Персонаж).
local function classKey( name )
local cls = name and character.CLASSES[name]
return cls and cls.key or nil
end
-- Вкладки классов в шапке: по макету они лежат на фотографии, а не отдельной
-- полосой плиток над ней.
local function classTabs( parent, data, page )
local tabs = parent:tag( 'div' ):addClass( 'lab-magic__tabs' )
for _, cls in ipairs( data['классы'] ) do
local el = tabs:tag( 'span' ):addClass( 'lab-magic__tab' )
local key = classKey( cls['название'] )
-- Свой цвет вкладке, но не через lab-cls-*: этот класс перекрашивает
-- всю страницу, а вкладок на ней пять.
if key then el:addClass( 'lab-magic__tab--' .. key ) end
if same( cls['страница'], page ) then el:addClass( 'is-on' ) end
if key then
el:tag( 'span' ):addClass( 'lab-emblem lab-emblem--' .. key )
end
el:wikitext( '[[' .. cls['страница'] .. '|' .. cls['название'] .. ']]' )
end
end
-- Сколько персонажей в категории класса.
local function classCount( frame, name )
local category = character.CLASS_CATEGORY[name]
if not category then return nil end
local out = frame:callParserFunction( '#labmembers', { category, 'ns=0', 'limit=500' } )
local n = 0
for title in mw.text.gsplit( out, SEP, true ) do
if mw.text.trim( title ) ~= '' then n = n + 1 end
end
return n
end
-- Сколько кого в классе: «40 магов».
local CLASS_FORMS = {
['Маг'] = { 'маг', 'мага', 'магов' },
['Вампир'] = { 'вампир', 'вампира', 'вампиров' },
['Оборотень'] = { 'оборотень', 'оборотня', 'оборотней' },
['Крылатый'] = { 'крылатый', 'крылатых', 'крылатых' },
['Призрак'] = { 'призрак', 'призрака', 'призраков' },
['Человек'] = { 'человек', 'человека', 'людей' },
}
-- Карточки классов: иллюстрация класса, эмблема, название и сколько их в игре.
function p.classes( frame )
local data = tree()
local page = mw.title.getCurrentTitle().prefixedText
local grid = mw.html.create( 'div' ):addClass( 'lab-magic__ccards' )
for _, cls in ipairs( data['классы'] ) do
local name = cls['название']
local key = classKey( name )
local card = grid:tag( 'span' ):addClass( 'lab-magic__ccard' )
if key then
card:addClass( 'lab-magic__ccard--' .. key )
end
if same( cls['страница'], page ) then card:addClass( 'is-on' ) end
-- Ссылкой оборачивается вся карточка: внутри неё только текст, и
-- нажиматься должна плашка целиком, а не одно название.
local inner = mw.html.create( 'span' ):addClass( 'lab-magic__ccardBody' )
if key then
inner:tag( 'span' ):addClass( 'lab-emblem lab-emblem--' .. key )
end
inner:tag( 'span' ):addClass( 'lab-magic__ccardName lab-display' ):wikitext( name )
local count = classCount( frame, name )
local forms = CLASS_FORMS[name]
if count and forms then
inner:tag( 'span' ):addClass( 'lab-magic__ccardNum' )
:wikitext( count .. ' ' .. character.plural( count, forms[1], forms[2], forms[3] ) )
end
card:wikitext( '[[' .. cls['страница'] .. '|' .. tostring( inner ) .. ']]' )
end
return tostring( grid )
end
-- Строки «* Свобода в выборе направлений» из параметра «Особенности».
local function features( text )
local items = {}
for line in ( ( text or '' ) .. '\n' ):gmatch( '(.-)\r?\n' ) do
local item = line:match( '^%s*%*+%s*(.+)$' )
if item then items[#items + 1] = mw.text.trim( item ) end
end
return items
end
-- Статусы персонажей: та же страница данных, что у списка персонажей. Её раз
-- в день обновляет скрипт игры, и записи в ней бывают любыми.
local function statuses()
local ok, data = pcall( mw.loadJsonData, STATUS_PAGE )
if not ok or type( data ) ~= 'table' then return {} end
local people = data['персонажи'] or data
if type( people ) ~= 'table' then return {} end
local clean = {}
for title, person in pairs( people ) do
if type( title ) == 'string' and type( person ) == 'table' then
clean[character.nameKey( title )] = person
end
end
return clean
end
-- Колонка «Маги в игре»: имена из категории класса, сначала те, кто в игре.
local function players( parent, frame, category )
local out = frame:callParserFunction( '#labmembers', { category, 'ns=0', 'limit=500' } )
local status = statuses()
local list = {}
for title in mw.text.gsplit( out, SEP, true ) do
title = mw.text.trim( title )
if title ~= '' then
local record = status[character.nameKey( title )]
local value = record and nonEmpty( tostring( record['статус'] or '' ) ) or nil
list[#list + 1] = {
title = title,
order = value and STATUS_ORDER[value] or 9,
key = character.nameKey( title ),
}
end
end
if #list == 0 then return end
table.sort( list, function ( a, b )
if a.order ~= b.order then return a.order < b.order end
return a.key < b.key
end )
local box = parent:tag( 'div' ):addClass( 'lab-blk lab-magic__players' )
local head = box:tag( 'div' ):addClass( 'lab-blk__head' )
head:tag( 'span' ):addClass( 'lab-bar' )
head:tag( 'span' ):addClass( 'lab-blk__title' ):wikitext( category .. ' в игре' )
head:tag( 'span' ):addClass( 'lab-magic__playersCount' ):wikitext( #list )
head:tag( 'span' ):addClass( 'lab-hline' )
head:tag( 'span' ):addClass( 'lab-mark' )
local names = box:tag( 'div' ):addClass( 'lab-magic__names' )
for i, item in ipairs( list ) do
if i > NAMES_SHOWN then break end
names:tag( 'span' ):wikitext( '[[' .. item.title .. ']]' )
end
-- «Все маги»: название категории как есть, без склонения — оно бывает
-- любым, а счётчик и так стоит в заголовке блока.
box:tag( 'div' ):addClass( 'lab-magic__namesFoot' )
:wikitext( '[[:Категория:' .. category .. '|Все ' .. mw.ustring.lower( category ) .. ']]' )
end
-- Страница класса: шапка с фотографией и вкладками классов, под ней колонка с
-- особенностями и персонажами. Текст статьи обтекает колонку.
function p.class( frame )
local args = frame:getParent().args
local data = tree()
local page = mw.title.getCurrentTitle()
local name = nonEmpty( args['НазваниеКласса'] ) or page.text
local key = classKey( name )
local hero = mw.html.create( 'div' ):addClass( 'lab-magic__hero' )
if key then
hero:addClass( 'lab-magic__hero--' .. key ):addClass( 'lab-cls-' .. key )
end
classTabs( hero, data, page.prefixedText )
local body = hero:tag( 'div' ):addClass( 'lab-magic__heroBody' )
-- Эмблема класса — та же картинка, что в игровых профилях и на карточке
-- персонажа (blocks/character.css), а не значок раздела вики.
if key then
local emblem = body:tag( 'span' ):addClass( 'lab-magic__emblem' )
emblem:tag( 'span' ):addClass( 'lab-magic__emblemGlow' )
emblem:tag( 'span' ):addClass( 'lab-magic__heroIco lab-emblem lab-emblem--' .. key )
end
body:tag( 'div' ):addClass( 'lab-magic__heroName lab-display' ):wikitext( name )
local resource = nonEmpty( args['ОсновнойРесурс'] )
if resource then
body:tag( 'div' ):addClass( 'lab-magic__heroSub' )
:wikitext( 'Основной ресурс · ' .. mw.ustring.lower( resource ) )
end
-- Основного ресурса в плашках нет: он стоит строкой под названием.
local stats = {
{ 'Восстановление', args['ВосстановлениеРесурса'] },
{ 'Внешние отличия', args['ВнешниеОтличия'] },
{ 'Свобода выбора пути', args['СвободаВыбораПути'] },
{ 'Плата за способности', args['ПлатаЗаСпособности'] },
}
local grid = body:tag( 'div' ):addClass( 'lab-magic__stats' )
for _, stat in ipairs( stats ) do
local value = nonEmpty( stat[2] )
if value then
local box = grid:tag( 'div' ):addClass( 'lab-magic__stat' )
-- Плашка рассчитана на пару слов, но в статьях о призраке и
-- вампире там целые предложения: длинное значение показываем
-- мельче, чтобы шапка не разъезжалась.
if mw.ustring.len( value ) > 60 then
box:addClass( 'lab-magic__stat--long' )
end
box:tag( 'div' ):addClass( 'lab-magic__statValue' ):wikitext( value )
box:tag( 'div' ):addClass( 'lab-magic__statLabel' ):wikitext( stat[1] )
end
end
-- Колонка справа: особенности и персонажи класса. Текст статьи её
-- обтекает — как карточка на странице персонажа.
local aside = mw.html.create( 'div' ):addClass( 'lab-magic__aside' )
local items = features( args['Особенности'] )
if #items > 0 then
local box = aside:tag( 'div' ):addClass( 'lab-blk' )
local head = box:tag( 'div' ):addClass( 'lab-blk__head' )
head:tag( 'span' ):addClass( 'lab-bar' )
head:tag( 'span' ):addClass( 'lab-blk__title' ):wikitext( 'Особенности' )
head:tag( 'span' ):addClass( 'lab-hline' )
head:tag( 'span' ):addClass( 'lab-mark' )
local list = box:tag( 'div' ):addClass( 'lab-magic__feats' )
for _, item in ipairs( items ) do
local row = list:tag( 'span' ):addClass( 'lab-magic__feat' )
row:tag( 'i' )
row:tag( 'span' ):wikitext( item )
end
end
local category = nonEmpty( args['КатегорияПерсонажей'] )
if category then
players( aside, frame, category )
end
return tostring( hero ) .. tostring( aside )
end
-- ---------- Направления класса ----------
-- Все направления дерева по названию: у вампира, оборотня и крылатого в
-- статье перечислены заблокированные и усиленные, и по названию нужно найти
-- страницу и значок.
local function directionsByName()
local index = {}
for _, school in ipairs( tree()['школы'] ) do
for _, dir in ipairs( school['направления'] or {} ) do
local name = nonEmpty( dir['название'] )
if name then
index[mw.ustring.lower( ( mw.ustring.gsub( name, 'ё', 'е' ) ) )] = dir
end
end
end
return index
end
local function affinityGroup( parent, index, label, text, kind )
local items = {}
for line in ( ( text or '' ) .. '\n' ):gmatch( '(.-)\r?\n' ) do
local item = line:match( '^%s*%*+%s*(.+)$' )
if item then items[#items + 1] = mw.text.trim( item ) end
end
if #items == 0 then return end
local group = parent:tag( 'div' ):addClass( 'lab-magic__aff lab-magic__aff--' .. kind )
group:tag( 'div' ):addClass( 'lab-magic__affLabel' ):wikitext( label )
local row = group:tag( 'div' ):addClass( 'lab-magic__affRow' )
for _, name in ipairs( items ) do
local dir = index[mw.ustring.lower( ( mw.ustring.gsub( name, 'ё', 'е' ) ) )]
local chip = row:tag( 'span' ):addClass( 'lab-magic__affItem' )
local iconFile = dir and nonEmpty( dir['иконка'] )
if iconFile then
chip:tag( 'span' ):addClass( 'lab-magic__affIco' )
:wikitext( '[[Файл:' .. iconFile .. '|24px|link=|alt=]]' )
end
-- Неизвестное направление остаётся текстом: дерево правят редакторы,
-- и название в статье может разойтись с ним.
chip:wikitext( dir and ( '[[' .. dir['страница'] .. '|' .. name .. ']]' ) or name )
end
end
-- Что классу закрыто, а что даётся легче: Шаблон:Направления класса.
function p.affinity( frame )
local args = frame:getParent().args
local index = directionsByName()
local root = mw.html.create( 'div' ):addClass( 'lab-magic__affinity' )
affinityGroup( root, index, nonEmpty( args['ЗаголовокЗаблокированных'] ) or 'Заблокированные направления',
args['Заблокированные'], 'off' )
affinityGroup( root, index, nonEmpty( args['ЗаголовокУсиленных'] ) or 'Усиленные направления',
args['Усиленные'], 'up' )
return tostring( root )
end
-- ---------- Вопросы и ответы ----------
-- Панель со всеми вопросами класса. Внутри — группы («; Общие принципы»)
-- и вызовы Шаблон:ВопросОтвет.
function p.questions( frame )
local args = frame:getParent().args
local box = mw.html.create( 'div' ):addClass( 'lab-qa lab-blk' )
local head = box:tag( 'div' ):addClass( 'lab-blk__head' )
head:tag( 'span' ):addClass( 'lab-bar' )
head:tag( 'span' ):addClass( 'lab-blk__title' )
:wikitext( nonEmpty( args['Заголовок'] ) or 'Вопросы и ответы' )
head:tag( 'span' ):addClass( 'lab-hline' )
head:tag( 'span' ):addClass( 'lab-mark' )
-- Содержимое отдельной строкой: внутри списки определений и шаблоны.
return tostring( box ):gsub( '</div>$', '' )
.. '<div class="lab-qa__body">\n' .. ( args[1] or '' ) .. '\n</div></div>'
end
-- Один вопрос: заголовок раскрывает ответ. Раскрывает скрипт
-- (resources/magic.js); без скрипта ответ просто виден целиком.
function p.question( frame )
local args = frame:getParent().args
local title = nonEmpty( args['ЗаголовокВопроса'] ) or 'Вопрос'
local item = mw.html.create( 'div' ):addClass( 'lab-qa__item' )
local head = item:tag( 'span' ):addClass( 'lab-qa__q' )
head:tag( 'span' ):addClass( 'lab-qa__qText' ):wikitext( title )
head:node( icon( 'plus' ):addClass( 'lab-qa__sign' ) )
local answer = item:tag( 'div' ):addClass( 'lab-qa__a' )
local full = nonEmpty( args['ПолныйВопрос'] )
if full then
answer:tag( 'div' ):addClass( 'lab-qa__full' ):wikitext( full )
end
answer:tag( 'div' ):addClass( 'lab-qa__answer' ):wikitext( args['Ответ'] or '' )
return tostring( item )
end
-- ---------- Магическая система ----------
-- Классы во множественном числе: в колонке «Источники силы» и на карточках
-- подпись «Маги», а не «Маг».
local CLASS_PLURAL = {
['Маг'] = 'Маги', ['Вампир'] = 'Вампиры', ['Оборотень'] = 'Оборотни',
['Крылатый'] = 'Крылатые', ['Призрак'] = 'Призраки', ['Человек'] = 'Люди',
}
-- Основной ресурс класса берётся со страницы класса: там он стоит параметром
-- {{Магический класс|ОсновнойРесурс=…}}, и второй раз писать его не нужно.
local function classResource( page )
local title = page and mw.title.new( page )
local text = title and title:getContent()
if not text then return nil end
local args = character.templateArgs( text, 'Магический класс' )
return nonEmpty( args['ОсновнойРесурс'] )
end
-- Шапка страницы «Магическая система»: картинка, название, вступление и
-- колонка «Источники силы» — по одному классу на строку.
function p.system( frame )
local args = frame:getParent().args
local data = tree()
local page = mw.title.getCurrentTitle()
local hero = mw.html.create( 'div' ):addClass( 'lab-magic__system' )
local image = nonEmpty( args['Фон'] )
if image then
hero:tag( 'span' ):addClass( 'lab-magic__systemPhoto' )
:wikitext( '[[Файл:' .. image .. '|1600px|link=|alt=]]' )
else
-- Без своей картинки шапка берёт иллюстрацию раздела из оформления
-- (public/resources/assets/sections/magic).
hero:addClass( 'lab-magic__system--art' )
end
local body = hero:tag( 'div' ):addClass( 'lab-magic__systemBody' )
body:tag( 'div' ):addClass( 'lab-magic__systemEyebrow' )
:wikitext( nonEmpty( args['Надзаголовок'] ) or 'Иртмис' )
body:tag( 'div' ):addClass( 'lab-magic__systemTitle lab-display' )
:wikitext( nonEmpty( args['Заголовок'] ) or page.text )
local lead = nonEmpty( args['Вступление'] )
if lead then
body:tag( 'div' ):addClass( 'lab-magic__systemLead' ):wikitext( lead )
end
-- Источники силы: класс, эмблема и его ресурс.
local box = hero:tag( 'div' ):addClass( 'lab-blk lab-magic__sources' )
local head = box:tag( 'div' ):addClass( 'lab-blk__head' )
head:tag( 'span' ):addClass( 'lab-bar' )
head:tag( 'span' ):addClass( 'lab-blk__title' ):wikitext( 'Источники силы' )
head:tag( 'span' ):addClass( 'lab-hline' )
head:tag( 'span' ):addClass( 'lab-mark' )
local list = box:tag( 'div' ):addClass( 'lab-magic__sourcesBody' )
for _, cls in ipairs( data['классы'] ) do
local name = cls['название']
local key = classKey( name )
local row = list:tag( 'span' ):addClass( 'lab-magic__source' )
if key then
row:addClass( 'lab-magic__tab--' .. key )
row:tag( 'span' ):addClass( 'lab-emblem lab-emblem--' .. key )
end
local text = row:tag( 'span' ):addClass( 'lab-magic__sourceText' )
text:tag( 'b' ):wikitext( '[[' .. cls['страница'] .. '|' .. ( CLASS_PLURAL[name] or name ) .. ']]' )
local resource = classResource( cls['страница'] )
if resource then
text:tag( 'span' ):wikitext( resource )
end
end
return tostring( hero )
end
-- Типы способностей: направленная, ментальная, защитная, нейтральная. Текст
-- пишется списком определений, значок подбирается по названию.
function p.types( frame )
local args = frame:getParent().args
local items = {}
for line in ( ( args[1] or '' ) .. '\n' ):gmatch( '(.-)\r?\n' ) do
local title = line:match( '^%s*;%s*(.+)$' )
local desc = line:match( '^%s*:%s*(.+)$' )
if title then
local short, inline = title:match( '^(.-)%s+:%s*(.+)$' )
items[#items + 1] = { title = mw.text.trim( short or title ), desc = inline and mw.text.trim( inline ) }
elseif desc and #items > 0 then
local last = items[#items]
last.desc = last.desc and ( last.desc .. ' ' .. desc ) or mw.text.trim( desc )
end
end
if #items == 0 then
return '<strong class="error">Шаблон:Типы способностей — нет ни одного типа</strong>'
end
local grid = mw.html.create( 'div' ):addClass( 'lab-magic__kinds' )
for _, item in ipairs( items ) do
-- «Направленная магия» → значок из SPELL_TYPES, как на странице навыка.
local kind = SPELL_TYPES[( item.title:gsub( '%s+магия$', '' ) )]
local card = grid:tag( 'div' ):addClass( 'lab-magic__kind' )
card:tag( 'span' ):addClass( 'lab-magic__kindIco' ):node( icon( kind and kind.ico or 'book' ) )
card:tag( 'div' ):addClass( 'lab-magic__kindTitle lab-display' ):wikitext( item.title )
if item.desc then
card:tag( 'div' ):addClass( 'lab-magic__kindText' ):wikitext( item.desc )
end
end
return tostring( grid )
end
-- Терминология: карточки в две колонки и поиск по ним (magic.js).
function p.terms( frame )
local args = frame:getParent().args
local items = {}
for line in ( ( args[1] or '' ) .. '\n' ):gmatch( '(.-)\r?\n' ) do
local title = line:match( '^%s*;%s*(.+)$' )
local desc = line:match( '^%s*:%s*(.+)$' )
if title then
local short, inline = title:match( '^(.-)%s+:%s*(.+)$' )
items[#items + 1] = { title = mw.text.trim( short or title ), desc = inline and mw.text.trim( inline ) }
elseif desc and #items > 0 then
local last = items[#items]
last.desc = last.desc and ( last.desc .. ' ' .. desc ) or mw.text.trim( desc )
end
end
if #items == 0 then
return '<strong class="error">Шаблон:Термины — нет ни одного термина</strong>'
end
local root = mw.html.create( 'div' ):addClass( 'lab-terms' ):attr( 'data-total', #items )
local head = root:tag( 'div' ):addClass( 'lab-terms__head' )
head:tag( 'span' ):addClass( 'lab-terms__count' ):wikitext( #items )
-- Поле поиска рисует скрипт: форму в вики-разметке парсер не пропускает.
head:tag( 'span' ):addClass( 'lab-terms__search' )
local list = root:tag( 'div' ):addClass( 'lab-terms__list' )
for _, item in ipairs( items ) do
local card = list:tag( 'div' ):addClass( 'lab-term' )
:attr( 'data-term', mw.ustring.lower( item.title ) )
card:tag( 'div' ):addClass( 'lab-term__name lab-display' ):wikitext( item.title )
if item.desc then
card:tag( 'div' ):addClass( 'lab-term__text' ):wikitext( item.desc )
end
end
-- Строка «ничего не нашлось» прячется стилями: атрибут hidden вики из
-- разметки вырезает, и без скрипта строка висела бы на странице.
root:tag( 'div' ):addClass( 'lab-terms__empty' ):wikitext( 'Ничего не нашлось' )
return tostring( root )
end
-- ---------- Заклинание ----------
-- Шапка страницы навыка: значок направления, путь школы, название и тип
-- заклинания. Зовёт её Шаблон:Тип магии — он стоит в начале каждой статьи о
-- способности, менять статьи ради шапки не пришлось.
function p.spellType( frame )
local args = frame:getParent().args
local value = nonEmpty( args[1] )
local data = tree()
local page = mw.title.getCurrentTitle()
local school, direction, skill = locate( data, page.prefixedText )
local chips = mw.html.create( 'span' ):addClass( 'lab-magic__types' )
local spell = value and SPELL_TYPES[value]
if spell then
local chip = chips:tag( 'span' ):addClass( 'lab-magic__type' )
chip:node( icon( spell.ico ) )
chip:wikitext( spell.label )
elseif value then
chips:tag( 'span' ):addClass( 'lab-magic__type' ):wikitext( value )
end
if nonEmpty( args[2] ) == 'да' then
local chip = chips:tag( 'span' ):addClass( 'lab-magic__type lab-magic__type--beast' )
chip:node( icon( 'person' ) )
chip:wikitext( 'Доступно в облике зверя' )
end
-- Вне дерева (черновик, страница вне раздела) — только плашки типа.
if not skill then
return tostring( chips )
end
local head = mw.html.create( 'div' ):addClass( 'lab-magic__head' )
-- Идентификатор способности в сервисе магии: по нему выгрузка сходится с
-- его базой, даже если страницу переименуют.
local id = nonEmpty( args['ид'] )
if id then head:attr( 'data-skill-id', id ) end
local iconFile = direction and nonEmpty( direction['иконка'] )
if iconFile then
local art = head:tag( 'span' ):addClass( 'lab-magic__headIco' )
art:tag( 'span' ):addClass( 'lab-magic__headGlow' )
art:tag( 'span' ):wikitext( '[[Файл:' .. iconFile .. '|104px|link=|alt=]]' )
end
local body = head:tag( 'div' ):addClass( 'lab-magic__headBody' )
body:tag( 'div' ):addClass( 'lab-magic__headEyebrow' )
:wikitext( school['название'] .. ' · ' .. direction['название'] )
body:tag( 'div' ):addClass( 'lab-magic__headName lab-display' ):wikitext( skill['название'] )
body:node( chips )
return tostring( head )
end
-- Уровень способности: шкала, описание и плашки воздействия.
--
-- «Воздействие» и «Разрушения» — отдельные параметры: так их читает и выгрузка
-- для сервиса магии (docs/pages.md). В старых статьях они написаны строками
-- «*'''Воздействие:''' …» внутри описания — их разбирает тот же код, пока
-- миграция не перенесёт их в параметры.
function p.level( frame )
local args = frame:getParent().args
local level = nonEmpty( args['Уровень'] ) or '?'
local text = args['Описание'] or ''
local prose, effects = {}, {}
for line in ( text .. '\n' ):gmatch( '(.-)\r?\n' ) do
local label, rest = line:match( "^%*%s*'''(.-):?'''%s*(.*)$" )
if label then
effects[#effects + 1] = { label = mw.text.trim( label ), text = mw.text.trim( rest ) }
else
prose[#prose + 1] = line
end
end
-- Параметр «Эффекты» — строки определения: подписи у способностей разные
-- («Воздействие», «Разрушения», «Эффект по площади»), список закрытым не
-- сделаешь. Их же читает выгрузка для сервиса магии.
for line in ( ( args['Эффекты'] or '' ) .. '\n' ):gmatch( '(.-)\r?\n' ) do
local label = line:match( '^%s*;%s*(.+)$' )
local value = line:match( '^%s*:%s*(.+)$' )
if label then
local short, inline = label:match( '^(.-)%s+:%s*(.+)$' )
effects[#effects + 1] = { label = mw.text.trim( short or label ), text = inline and mw.text.trim( inline ) or '' }
elseif value and #effects > 0 then
local last = effects[#effects]
last.text = last.text ~= '' and ( last.text .. ' ' .. mw.text.trim( value ) ) or mw.text.trim( value )
end
end
local box = mw.html.create( 'div' ):addClass( 'lab-magic__level' ):attr( 'data-level', level )
-- Стоимость и доступность приходят из сервиса магии страницей данных; пока
-- её нет, значения берутся из параметров (docs/pages.md).
local cost = nonEmpty( args['Стоимость'] )
if cost then box:attr( 'data-cost', cost ) end
local head = box:tag( 'div' ):addClass( 'lab-magic__levelHead' )
local pips = head:tag( 'span' ):addClass( 'lab-magic__pips' )
for i = 1, 5 do
local pip = pips:tag( 'span' )
if tonumber( level ) and i <= tonumber( level ) then pip:addClass( 'is-on' ) end
end
head:tag( 'span' ):addClass( 'lab-magic__levelName' ):wikitext( 'Уровень ' .. level )
box:tag( 'div' ):addClass( 'lab-magic__levelBody' ):wikitext( '\n' .. table.concat( prose, '\n' ) .. '\n' )
if #effects > 0 then
local grid = box:tag( 'div' ):addClass( 'lab-magic__effects' )
for _, effect in ipairs( effects ) do
local el = grid:tag( 'div' ):addClass( 'lab-magic__effect' )
el:tag( 'div' ):addClass( 'lab-magic__effectLabel' ):wikitext( effect.label )
el:tag( 'div' ):addClass( 'lab-magic__effectText' ):wikitext( effect.text )
end
end
return tostring( box )
end
p['меню'] = p.menu
p['школы'] = p.schools
p['направления'] = p.directions
p['навыки'] = p.skills
p['дерево'] = p.tree
p['направления школы'] = p.schoolDirections
p['способности направления'] = p.directionSkills
p['классы'] = p.classes
p['класс'] = p.class
p['система'] = p.system
p['все способности'] = p.outline
p['типы способностей'] = p.types
p['термины'] = p.terms
p['направления класса'] = p.affinity
p['вопросы'] = p.questions
p['вопрос'] = p.question
p['тип'] = p.spellType
p['уровень'] = p.level
return p