MediaWiki:ScmcChemistry.js: различия между версиями
Страница интерфейса MediaWiki
Дополнительные действия
Defer (обсуждение | вклад) Нет описания правки |
Defer (обсуждение | вклад) Нет описания правки |
||
| Строка 1: | Строка 1: | ||
/* | /** | ||
* Интерактивный каталог химии SSMC. | * Интерактивный каталог химии SSMC. | ||
* Страница: MC:Химия | * Страница: MC:Химия | ||
* Данные: | * Данные: MediaWiki:ScmcChemCatalog.js | ||
*/ | */ | ||
(function () { | (function (mw, $) { | ||
'use strict'; | 'use strict'; | ||
var ROOT_ID = 'scmc-chemistry-app'; | var ROOT_ID = 'scmc-chemistry-app'; | ||
var | var CATALOG_PAGE = 'MediaWiki:ScmcChemCatalog.js'; | ||
var NUMBER = new Intl.NumberFormat('ru-RU', { | var STYLE_PAGE = 'MediaWiki:ScmcChemistry.css'; | ||
var NUMBER = new Intl.NumberFormat('ru-RU', { maximumFractionDigits: 3 }); | |||
var TABS = [ | |||
var | |||
{ id: 'ordnance', label: 'Боевая химия' }, | { id: 'ordnance', label: 'Боевая химия' }, | ||
{ id: 'medicine', label: 'Медицина' }, | { id: 'medicine', label: 'Медицина' }, | ||
| Строка 20: | Строка 18: | ||
{ id: 'other', label: 'Другие вещества' } | { id: 'other', label: 'Другие вещества' } | ||
]; | ]; | ||
var ORIGIN_LABELS = { | |||
rmc14: 'RMC14', | |||
stories: 'Stories', | |||
'upstream-reference': 'Upstream' | |||
var | |||
}; | }; | ||
var METABOLISM_LABELS = { | var METABOLISM_LABELS = { | ||
Alcohol: 'Алкоголь', | Alcohol: 'Алкоголь', | ||
| Строка 120: | Строка 31: | ||
Poison: 'Яд' | Poison: 'Яд' | ||
}; | }; | ||
var EFFECT_LABELS = { | |||
AdjustReagent: 'Изменяет количество реагента', | |||
AdjustTemperature: 'Изменяет температуру', | |||
AreaReactionEffect: 'Создаёт эффект в области', | |||
CauseStatusEffect: 'Вызывает статусный эффект', | |||
ChemVomit: 'Вызывает рвоту', | |||
CureDisease: 'Лечит заболевание', | |||
Drunk: 'Вызывает опьянение', | |||
Electrocute: 'Поражает электричеством', | |||
Emote: 'Вызывает эмоцию', | |||
ExplosionReactionEffect: 'Вызывает взрыв', | |||
GenericStatusEffect: 'Накладывает статусный эффект', | |||
HealthChange: 'Изменяет здоровье', | |||
Ignite: 'Поджигает', | |||
ModifyBloodLevel: 'Изменяет уровень крови', | |||
MovespeedModifier: 'Изменяет скорость движения', | |||
Polymorph: 'Изменяет форму', | |||
PopupMessage: 'Показывает сообщение', | |||
ResetNarcolepsy: 'Снимает приступ нарколепсии', | |||
SatiateHunger: 'Утоляет голод', | |||
SatiateThirst: 'Утоляет жажду', | |||
SensitiveReactionExplosionEffect: 'Может вызвать взрыв', | |||
SetSolutionTemperature: 'Задаёт температуру раствора', | |||
Sleep: 'Усыпляет' | |||
}; | |||
var state = { | var state = { | ||
data: null, | data: null, | ||
| Строка 128: | Строка 63: | ||
}; | }; | ||
function | function el(tag, className, text) { | ||
var node = document.createElement(tag); | var node = document.createElement(tag); | ||
if (className) { | if (className) { | ||
node.className = className; | node.className = className; | ||
} | } | ||
if (text !== undefined && text !== null) { | if (text !== undefined && text !== null) { | ||
node.textContent = String(text); | node.textContent = String(text); | ||
} | } | ||
return node; | return node; | ||
} | } | ||
function | function add(parent) { | ||
Array.prototype.slice.call(arguments, 1).forEach(function (child) { | |||
if (child) { | if (child) { | ||
parent.appendChild(child); | parent.appendChild(child); | ||
} | } | ||
}); | }); | ||
return parent; | return parent; | ||
} | } | ||
| Строка 162: | Строка 91: | ||
} | } | ||
function | function normalize(value) { | ||
return String(value || '').toLocaleLowerCase('ru-RU').replace(/ё/g, 'е').trim(); | |||
} | } | ||
function | function slug(value) { | ||
return String(value || 'section') | |||
.toLocaleLowerCase('ru-RU') | |||
.replace(/[^a-zа-яё0-9]+/gi, '-') | |||
.replace(/^-+|-+$/g, ''); | |||
} | } | ||
function safeColor(value) { | function safeColor(value) { | ||
var probe = document.createElement('span').style; | |||
probe.color = ''; | |||
probe.color = String(value || ''); | |||
return probe.color ? value : '#7f8c98'; | |||
return | |||
} | } | ||
function | function getReagent(id) { | ||
return state.data.reagents[id] || state.data.dependencies[id] || null; | return state.data.reagents[id] || state.data.dependencies[id] || null; | ||
} | } | ||
function | function getName(id) { | ||
var reagent = | var reagent = getReagent(id); | ||
return reagent && reagent.name ? reagent.name : id; | return reagent && reagent.name ? reagent.name : id; | ||
} | } | ||
function | function getColor(id) { | ||
return | var reagent = getReagent(id); | ||
return safeColor(reagent && reagent.properties && reagent.properties.color); | |||
} | } | ||
function | function indexReactions() { | ||
state.productReactions = Object.create(null); | state.productReactions = Object.create(null); | ||
Object.keys(state.data.reactions || {}).forEach(function (reactionId) { | |||
Object.keys(state.data.reactions).forEach(function (reactionId) { | |||
var reaction = state.data.reactions[reactionId]; | var reaction = state.data.reactions[reactionId]; | ||
(reaction.products || []).forEach(function (product) { | (reaction.products || []).forEach(function (product) { | ||
if (!state.productReactions[product.id]) { | if (!state.productReactions[product.id]) { | ||
state.productReactions[product.id] = []; | state.productReactions[product.id] = []; | ||
} | } | ||
state.productReactions[product.id].push(reaction); | state.productReactions[product.id].push(reaction); | ||
}); | }); | ||
| Строка 241: | Строка 136: | ||
} | } | ||
function | function validateCatalog(data) { | ||
return | return data && data.schemaVersion === 1 && data.catalogSections && | ||
data.reagents && data.dependencies && data.reactions && | |||
TABS.every(function (tab) { | |||
return Array.isArray(data.catalogSections[tab.id]); | |||
}); | |||
} | } | ||
function | function loadStyle() { | ||
mw.loader.load(mw.util.getUrl(STYLE_PAGE, { | |||
action: 'raw', | |||
ctype: 'text/css' | |||
}), 'text/css'); | |||
} | } | ||
function | function loadCatalog() { | ||
window.ScmcChemCatalog = null; | |||
return | return mw.loader.getScript(mw.util.getUrl(CATALOG_PAGE, { | ||
action: 'raw', | |||
ctype: 'text/javascript' | |||
})).then(function () { | |||
if (!validateCatalog(window.ScmcChemCatalog)) { | |||
throw new Error('MediaWiki:ScmcChemCatalog.js не содержит поддерживаемый каталог'); | |||
} | |||
if ( | |||
} | } | ||
return window.ScmcChemCatalog; | |||
}); | |||
} | |||
} | } | ||
function | function renderLoading(root) { | ||
root.textContent = ''; | |||
add(root, add(el('div', 'scmc-chem__status scmc-chem__status--loading'), | |||
el('span', 'scmc-chem__spinner'), | |||
el('span', '', 'Загружаю химический каталог…') | |||
)); | |||
} | } | ||
function | function renderError(root, error) { | ||
var | root.textContent = ''; | ||
var | var block = add(el('div', 'scmc-chem__status scmc-chem__status--error'), | ||
el('strong', '', 'Каталог не загрузился.'), | |||
if ( | el('span', '', ' Проверьте MediaWiki:ScmcChemCatalog.js.') | ||
); | |||
var retry = el('button', 'scmc-chem__retry', 'Повторить'); | |||
retry.type = 'button'; | |||
retry.addEventListener('click', function () { start(root); }); | |||
add(block, retry); | |||
add(root, block); | |||
if (window.console && console.error) { | |||
console.error('[ScmcChemistry]', error); | |||
} | } | ||
} | } | ||
function | function createHero() { | ||
var | var count = TABS.reduce(function (total, tab) { | ||
return total + state.data.catalogSections[tab.id].length; | |||
}, 0); | |||
return []; | return add(el('header', 'scmc-chem__hero'), | ||
} | add(el('div', 'scmc-chem__hero-copy'), | ||
el('p', 'scmc-chem__eyebrow', 'SSMC · справочник'), | |||
return | el('h1', 'scmc-chem__title', 'Химия'), | ||
el('p', 'scmc-chem__lead', 'Реагенты, рецепты, дозировки и эффекты. Данные хранятся прямо на вики.') | |||
), | |||
add(el('div', 'scmc-chem__stats'), | |||
el('span', 'scmc-chem__stat', count + ' записей'), | |||
el('span', 'scmc-chem__stat', Object.keys(state.data.reactions).length + ' реакций') | |||
) | |||
); | |||
} | } | ||
function | function renderTabs(root) { | ||
var | var tabs = root.querySelector('.scmc-chem__tabs'); | ||
tabs.textContent = ''; | |||
TABS.forEach(function (tab) { | |||
var button = el('button', 'scmc-chem__tab'); | |||
button.type = 'button'; | |||
button.setAttribute('role', 'tab'); | |||
button.setAttribute('aria-selected', String(tab.id === state.activeTab)); | |||
add(button, | |||
el('span', '', tab.label), | |||
el('span', 'scmc-chem__tab-count', state.data.catalogSections[tab.id].length) | |||
); | |||
button.addEventListener('click', function () { | |||
state.activeTab = tab.id; | |||
renderTabs(root); | |||
renderResults(root); | |||
}); | }); | ||
add(tabs, button); | |||
}); | }); | ||
} | } | ||
function | function groupEntries(entries) { | ||
var groups = Object.create(null); | |||
entries.forEach(function (entry) { | |||
var path = Array.isArray(entry.sectionPath) && entry.sectionPath.length ? entry.sectionPath : ['Без раздела']; | |||
var key = path.join(' › '); | |||
var | if (!groups[key]) { | ||
groups[key] = { title: path[path.length - 1], path: path, entries: [] }; | |||
} | } | ||
groups[key].entries.push(entry); | |||
}); | |||
return Object.keys(groups).map(function (key) { return groups[key]; }); | |||
} | |||
} | } | ||
function | function entryMatches(entry) { | ||
if (!state.query) { | |||
return true; | |||
} | } | ||
var reagent = getReagent(entry.id) || {}; | |||
var haystack = [entry.id, entry.name, reagent.name, reagent.description] | |||
.concat(entry.sectionPath || []).join(' '); | |||
return normalize(haystack).indexOf(state.query) !== -1; | |||
} | } | ||
function | function renderSectionNav(root, groups) { | ||
var | var nav = root.querySelector('.scmc-chem__section-nav'); | ||
nav.textContent = ''; | |||
nav.hidden = groups.length < 2; | |||
groups.forEach(function (group, index) { | |||
var link = el('a', 'scmc-chem__section-link'); | |||
link.href = '#scmc-chem-section-' + index + '-' + slug(group.title); | |||
add(link, | |||
el('span', '', group.title), | |||
el('span', 'scmc-chem__section-link-count', group.entries.length) | |||
); | |||
add(nav, link); | |||
} | }); | ||
} | } | ||
function | function renderMixture(items, className) { | ||
var | var list = el('div', className); | ||
(items || []).forEach(function (item) { | |||
add(list, add(el('div', 'scmc-chem__mixture-item'), | |||
el('span', 'scmc-chem__mixture-dot'), | |||
el('span', 'scmc-chem__mixture-name', item.name || getName(item.id)), | |||
el('span', 'scmc-chem__mixture-amount', formatAmount(item.amount)) | |||
)); | |||
list.lastChild.firstChild.style.backgroundColor = getColor(item.id); | |||
}); | }); | ||
return list; | |||
return | |||
} | } | ||
function | function renderReaction(reaction) { | ||
var | var box = el('div', 'scmc-chem__reaction'); | ||
var | var equation = add(el('div', 'scmc-chem__equation'), | ||
renderMixture(reaction.reactants, 'scmc-chem__reactants'), | |||
el('span', 'scmc-chem__arrow', '→'), | |||
renderMixture(reaction.products, 'scmc-chem__products') | |||
); | |||
add(box, el('div', 'scmc-chem__reaction-label', 'Рецепт'), equation); | |||
if (!reaction.products || !reaction.products.length) { | |||
add(box, el('div', 'scmc-chem__reaction-warning', 'Реакция не создаёт реагент и может иметь опасный эффект.')); | |||
if ( | |||
} | } | ||
return box; | |||
} | } | ||
function | function effectType(effect) { | ||
return String(effect && effect.yamlTag || '').replace(/^!type:/, '') || 'Эффект'; | |||
} | } | ||
function | function describeValue(value) { | ||
if (value === null || value === undefined || value === '') { | |||
return ''; | |||
return ' | |||
} | } | ||
if ( | if (typeof value !== 'object') { | ||
return | return String(value); | ||
} | } | ||
return Object.keys(value).filter(function (key) { | |||
return ' | return !['conditions', 'showInGuidebook', 'showInChat'].includes(key); | ||
}).map(function (key) { | |||
var item = value[key]; | |||
if (item && typeof item === 'object') { | |||
return key + ': ' + JSON.stringify(item); | |||
} | |||
return key + ': ' + item; | |||
}).join(' · '); | |||
} | } | ||
function | function renderEffects(effects) { | ||
if (!effects.length) { | var list = el('div', 'scmc-chem__effect-list'); | ||
return; | if (!effects || !effects.length) { | ||
add(list, el('p', 'scmc-chem__empty', 'Эффекты не указаны.')); | |||
return list; | |||
} | } | ||
effects.forEach(function (effect) { | |||
var type = effectType(effect); | |||
var value = effect.value || {}; | |||
var harmful = /Damage|Ignite|Explosion|Electrocute|Vomit|Poison/i.test(type); | |||
var card = el('div', 'scmc-chem__effect' + (harmful ? ' scmc-chem__effect--harmful' : '')); | |||
list | add(card, el('div', 'scmc-chem__effect-name', EFFECT_LABELS[type] || type)); | ||
var details = describeValue(value); | |||
if (details) { | |||
add(card, el('div', 'scmc-chem__effect-details', details)); | |||
} | |||
add(list, card); | |||
}); | }); | ||
return list; | |||
} | } | ||
function renderMetabolisms(reagent) { | function renderMetabolisms(reagent) { | ||
var wrapper = el('div', 'scmc-chem__card-section'); | |||
add(wrapper, el('h4', 'scmc-chem__card-subtitle', 'Воздействие')); | |||
var metabolisms = reagent.properties && reagent.properties.metabolisms; | var metabolisms = reagent.properties && reagent.properties.metabolisms; | ||
var keys = Object.keys(metabolisms || {}); | |||
if (!keys.length) { | |||
return | add(wrapper, el('p', 'scmc-chem__empty', 'Метаболические эффекты не указаны.')); | ||
return wrapper; | |||
} | } | ||
keys.forEach(function (key) { | |||
var metabolism = metabolisms[key] || {}; | |||
var block = el('div', 'scmc-chem__metabolism'); | |||
var title = METABOLISM_LABELS[key] || key; | |||
var metabolism = metabolisms[ | |||
var | |||
var title = METABOLISM_LABELS[ | |||
if (metabolism.metabolismRate !== undefined) { | if (metabolism.metabolismRate !== undefined) { | ||
title += ' · | title += ' · ' + formatAmount(metabolism.metabolismRate) + '/с'; | ||
} | } | ||
add(block, el('div', 'scmc-chem__metabolism-title', title), renderEffects(metabolism.effects)); | |||
add(wrapper, block); | |||
}); | }); | ||
return wrapper; | |||
return | |||
} | } | ||
function renderCard(entry) { | function renderCard(entry) { | ||
var reagent = | var reagent = getReagent(entry.id) || { | ||
id: entry.id, | |||
name: entry.name, | |||
var color = safeColor( | origin: entry.origin, | ||
var | properties: {} | ||
}; | |||
var color = safeColor(reagent.properties && reagent.properties.color); | |||
var details = el('details', 'scmc-chem__card'); | |||
details.style.setProperty('--scmc-chem-color', color); | details.style.setProperty('--scmc-chem-color', color); | ||
var summary = el('summary', 'scmc-chem__card-summary'); | |||
var heading = add(el('span', 'scmc-chem__card-heading'), | |||
el('span', 'scmc-chem__card-name', reagent.name || entry.name || entry.id), | |||
el('code', 'scmc-chem__card-id', entry.id) | |||
); | ); | ||
add(summary, | |||
el('span', 'scmc-chem__color'), | |||
heading, | heading, | ||
el('span', 'scmc-chem__origin scmc-chem__origin--' + (reagent.origin || entry.origin || 'upstream-reference'), ORIGIN_LABELS[reagent.origin || entry.origin] || reagent.origin || entry.origin), | |||
el('span', 'scmc-chem__chevron') | |||
); | ); | ||
summary.firstChild.style.backgroundColor = color; | |||
var body = el('div', 'scmc-chem__card-body'); | |||
var body = | add(body, el('p', reagent.description ? 'scmc-chem__description' : 'scmc-chem__empty', reagent.description || 'Описание не указано.')); | ||
var overdose = reagent.properties && reagent.properties.overdose; | |||
if (! | var critical = reagent.properties && reagent.properties.criticalOverdose; | ||
if (overdose !== undefined || critical !== undefined) { | |||
var doses = el('div', 'scmc-chem__dose-row'); | |||
if (overdose !== undefined) { | |||
add(doses, el('span', 'scmc-chem__dose scmc-chem__dose--warning', 'Передозировка: ' + formatAmount(overdose))); | |||
} | |||
if (critical !== undefined) { | |||
add(doses, el('span', 'scmc-chem__dose scmc-chem__dose--danger', 'Критическая: ' + formatAmount(critical))); | |||
} | |||
add(body, doses); | |||
} | } | ||
var reactions = state.productReactions[entry.id] || []; | |||
if ( | if (reactions.length) { | ||
var recipeSection = el('div', 'scmc-chem__card-section'); | |||
add(recipeSection, el('h4', 'scmc-chem__card-subtitle', 'Получение')); | |||
reactions.forEach(function (reaction) { add(recipeSection, renderReaction(reaction)); }); | |||
add(body, recipeSection); | |||
body | |||
} | } | ||
add(body, renderMetabolisms(reagent)); | |||
details | add(details, summary, body); | ||
return details; | return details; | ||
} | } | ||
function renderResults(root) { | function renderResults(root) { | ||
var | var result = root.querySelector('.scmc-chem__results'); | ||
var | var counter = root.querySelector('.scmc-chem__result-count'); | ||
result.textContent = ''; | |||
var entries = state.data.catalogSections[state.activeTab].filter(entryMatches); | |||
var groups = groupEntries(entries); | |||
counter.textContent = entries.length + ' найдено'; | |||
renderSectionNav(root, groups); | |||
if (!entries.length) { | if (!entries.length) { | ||
add(result, el('div', 'scmc-chem__no-results', 'Ничего не найдено. Измените запрос.')); | |||
return; | return; | ||
} | } | ||
groups.forEach(function (group, index) { | groups.forEach(function (group, index) { | ||
var section = el('section', 'scmc-chem__section'); | |||
section.id = 'scmc-chem-section-' + index + '-' + slug(group.title); | |||
var heading = add(el('div', 'scmc-chem__section-heading'), | |||
el('h2', 'scmc-chem__section-title', group.title), | |||
el('span', 'scmc-chem__breadcrumbs', group.path.join(' › ')), | |||
el('span', 'scmc-chem__section-count', group.entries.length) | |||
); | |||
var cards = el('div', 'scmc-chem__cards'); | |||
group.entries.forEach(function (entry) { add(cards, renderCard(entry)); }); | |||
var | add(section, heading, cards); | ||
add(result, section); | |||
}); | }); | ||
} | } | ||
| Строка 1109: | Строка 433: | ||
function renderApp(root) { | function renderApp(root) { | ||
root.textContent = ''; | root.textContent = ''; | ||
add(root, createHero()); | |||
var controls = el('div', 'scmc-chem__controls'); | |||
var search = el('label', 'scmc-chem__search'); | |||
var input = el('input', 'scmc-chem__search-input'); | |||
var controls = | |||
var search = | |||
var input = | |||
input.type = 'search'; | input.type = 'search'; | ||
input.placeholder = 'Название или | input.placeholder = 'Название, ID или раздел…'; | ||
input.autocomplete = 'off'; | input.autocomplete = 'off'; | ||
input.setAttribute('aria-label', 'Поиск по химическому каталогу'); | |||
input.addEventListener('input', function () { | input.addEventListener('input', function () { | ||
state.query = | state.query = normalize(input.value); | ||
renderResults(root); | renderResults(root); | ||
}); | }); | ||
add(search, el('span', 'scmc-chem__search-icon', '⌕'), input); | |||
add(controls, search, el('span', 'scmc-chem__result-count')); | |||
add(root, controls, el('div', 'scmc-chem__tabs')); | |||
var nav = el('nav', 'scmc-chem__section-nav'); | |||
nav.setAttribute('aria-label', 'Подразделы каталога'); | |||
add(root, nav, el('main', 'scmc-chem__results')); | |||
var source = state.data.source || {}; | |||
add(root, el('footer', 'scmc-chem__footer', 'Локальный каталог MediaWiki' + (source.commit ? ' · версия ' + String(source.commit).slice(0, 8) : ''))); | |||
var | |||
var | |||
renderTabs(root); | renderTabs(root); | ||
renderResults(root); | renderResults(root); | ||
} | } | ||
function | function start(root) { | ||
root | renderLoading(root); | ||
loadCatalog().then(function (data) { | |||
state.data = data; | |||
indexReactions(); | |||
renderApp(root); | |||
}).catch(function (error) { | |||
renderError(root, error); | |||
}) | |||
}); | }); | ||
} | } | ||
function init() { | function init() { | ||
var root = document.getElementById(ROOT_ID); | var root = document.getElementById(ROOT_ID); | ||
if (!root) { | |||
if (root) { | return; | ||
} | } | ||
loadStyle(); | |||
start(root); | |||
} | } | ||
$(init); | |||
}(mediaWiki, jQuery)); | |||
}()); | |||
Версия от 16:09, 14 августа 2026
/**
* Интерактивный каталог химии SSMC.
* Страница: MC:Химия
* Данные: MediaWiki:ScmcChemCatalog.js
*/
(function (mw, $) {
'use strict';
var ROOT_ID = 'scmc-chemistry-app';
var CATALOG_PAGE = 'MediaWiki:ScmcChemCatalog.js';
var STYLE_PAGE = 'MediaWiki:ScmcChemistry.css';
var NUMBER = new Intl.NumberFormat('ru-RU', { maximumFractionDigits: 3 });
var TABS = [
{ id: 'ordnance', label: 'Боевая химия' },
{ id: 'medicine', label: 'Медицина' },
{ id: 'drinks', label: 'Напитки' },
{ id: 'elements', label: 'Элементы' },
{ id: 'other', label: 'Другие вещества' }
];
var ORIGIN_LABELS = {
rmc14: 'RMC14',
stories: 'Stories',
'upstream-reference': 'Upstream'
};
var METABOLISM_LABELS = {
Alcohol: 'Алкоголь',
Drink: 'Напиток',
Food: 'Пища',
Medicine: 'Лекарство',
Narcotic: 'Наркотик',
Poison: 'Яд'
};
var EFFECT_LABELS = {
AdjustReagent: 'Изменяет количество реагента',
AdjustTemperature: 'Изменяет температуру',
AreaReactionEffect: 'Создаёт эффект в области',
CauseStatusEffect: 'Вызывает статусный эффект',
ChemVomit: 'Вызывает рвоту',
CureDisease: 'Лечит заболевание',
Drunk: 'Вызывает опьянение',
Electrocute: 'Поражает электричеством',
Emote: 'Вызывает эмоцию',
ExplosionReactionEffect: 'Вызывает взрыв',
GenericStatusEffect: 'Накладывает статусный эффект',
HealthChange: 'Изменяет здоровье',
Ignite: 'Поджигает',
ModifyBloodLevel: 'Изменяет уровень крови',
MovespeedModifier: 'Изменяет скорость движения',
Polymorph: 'Изменяет форму',
PopupMessage: 'Показывает сообщение',
ResetNarcolepsy: 'Снимает приступ нарколепсии',
SatiateHunger: 'Утоляет голод',
SatiateThirst: 'Утоляет жажду',
SensitiveReactionExplosionEffect: 'Может вызвать взрыв',
SetSolutionTemperature: 'Задаёт температуру раствора',
Sleep: 'Усыпляет'
};
var state = {
data: null,
activeTab: 'ordnance',
query: '',
productReactions: Object.create(null)
};
function el(tag, className, text) {
var node = document.createElement(tag);
if (className) {
node.className = className;
}
if (text !== undefined && text !== null) {
node.textContent = String(text);
}
return node;
}
function add(parent) {
Array.prototype.slice.call(arguments, 1).forEach(function (child) {
if (child) {
parent.appendChild(child);
}
});
return parent;
}
function formatNumber(value) {
return typeof value === 'number' ? NUMBER.format(value) : String(value);
}
function formatAmount(value) {
return formatNumber(value) + ' ед.';
}
function normalize(value) {
return String(value || '').toLocaleLowerCase('ru-RU').replace(/ё/g, 'е').trim();
}
function slug(value) {
return String(value || 'section')
.toLocaleLowerCase('ru-RU')
.replace(/[^a-zа-яё0-9]+/gi, '-')
.replace(/^-+|-+$/g, '');
}
function safeColor(value) {
var probe = document.createElement('span').style;
probe.color = '';
probe.color = String(value || '');
return probe.color ? value : '#7f8c98';
}
function getReagent(id) {
return state.data.reagents[id] || state.data.dependencies[id] || null;
}
function getName(id) {
var reagent = getReagent(id);
return reagent && reagent.name ? reagent.name : id;
}
function getColor(id) {
var reagent = getReagent(id);
return safeColor(reagent && reagent.properties && reagent.properties.color);
}
function indexReactions() {
state.productReactions = Object.create(null);
Object.keys(state.data.reactions || {}).forEach(function (reactionId) {
var reaction = state.data.reactions[reactionId];
(reaction.products || []).forEach(function (product) {
if (!state.productReactions[product.id]) {
state.productReactions[product.id] = [];
}
state.productReactions[product.id].push(reaction);
});
});
}
function validateCatalog(data) {
return data && data.schemaVersion === 1 && data.catalogSections &&
data.reagents && data.dependencies && data.reactions &&
TABS.every(function (tab) {
return Array.isArray(data.catalogSections[tab.id]);
});
}
function loadStyle() {
mw.loader.load(mw.util.getUrl(STYLE_PAGE, {
action: 'raw',
ctype: 'text/css'
}), 'text/css');
}
function loadCatalog() {
window.ScmcChemCatalog = null;
return mw.loader.getScript(mw.util.getUrl(CATALOG_PAGE, {
action: 'raw',
ctype: 'text/javascript'
})).then(function () {
if (!validateCatalog(window.ScmcChemCatalog)) {
throw new Error('MediaWiki:ScmcChemCatalog.js не содержит поддерживаемый каталог');
}
return window.ScmcChemCatalog;
});
}
function renderLoading(root) {
root.textContent = '';
add(root, add(el('div', 'scmc-chem__status scmc-chem__status--loading'),
el('span', 'scmc-chem__spinner'),
el('span', '', 'Загружаю химический каталог…')
));
}
function renderError(root, error) {
root.textContent = '';
var block = add(el('div', 'scmc-chem__status scmc-chem__status--error'),
el('strong', '', 'Каталог не загрузился.'),
el('span', '', ' Проверьте MediaWiki:ScmcChemCatalog.js.')
);
var retry = el('button', 'scmc-chem__retry', 'Повторить');
retry.type = 'button';
retry.addEventListener('click', function () { start(root); });
add(block, retry);
add(root, block);
if (window.console && console.error) {
console.error('[ScmcChemistry]', error);
}
}
function createHero() {
var count = TABS.reduce(function (total, tab) {
return total + state.data.catalogSections[tab.id].length;
}, 0);
return add(el('header', 'scmc-chem__hero'),
add(el('div', 'scmc-chem__hero-copy'),
el('p', 'scmc-chem__eyebrow', 'SSMC · справочник'),
el('h1', 'scmc-chem__title', 'Химия'),
el('p', 'scmc-chem__lead', 'Реагенты, рецепты, дозировки и эффекты. Данные хранятся прямо на вики.')
),
add(el('div', 'scmc-chem__stats'),
el('span', 'scmc-chem__stat', count + ' записей'),
el('span', 'scmc-chem__stat', Object.keys(state.data.reactions).length + ' реакций')
)
);
}
function renderTabs(root) {
var tabs = root.querySelector('.scmc-chem__tabs');
tabs.textContent = '';
TABS.forEach(function (tab) {
var button = el('button', 'scmc-chem__tab');
button.type = 'button';
button.setAttribute('role', 'tab');
button.setAttribute('aria-selected', String(tab.id === state.activeTab));
add(button,
el('span', '', tab.label),
el('span', 'scmc-chem__tab-count', state.data.catalogSections[tab.id].length)
);
button.addEventListener('click', function () {
state.activeTab = tab.id;
renderTabs(root);
renderResults(root);
});
add(tabs, button);
});
}
function groupEntries(entries) {
var groups = Object.create(null);
entries.forEach(function (entry) {
var path = Array.isArray(entry.sectionPath) && entry.sectionPath.length ? entry.sectionPath : ['Без раздела'];
var key = path.join(' › ');
if (!groups[key]) {
groups[key] = { title: path[path.length - 1], path: path, entries: [] };
}
groups[key].entries.push(entry);
});
return Object.keys(groups).map(function (key) { return groups[key]; });
}
function entryMatches(entry) {
if (!state.query) {
return true;
}
var reagent = getReagent(entry.id) || {};
var haystack = [entry.id, entry.name, reagent.name, reagent.description]
.concat(entry.sectionPath || []).join(' ');
return normalize(haystack).indexOf(state.query) !== -1;
}
function renderSectionNav(root, groups) {
var nav = root.querySelector('.scmc-chem__section-nav');
nav.textContent = '';
nav.hidden = groups.length < 2;
groups.forEach(function (group, index) {
var link = el('a', 'scmc-chem__section-link');
link.href = '#scmc-chem-section-' + index + '-' + slug(group.title);
add(link,
el('span', '', group.title),
el('span', 'scmc-chem__section-link-count', group.entries.length)
);
add(nav, link);
});
}
function renderMixture(items, className) {
var list = el('div', className);
(items || []).forEach(function (item) {
add(list, add(el('div', 'scmc-chem__mixture-item'),
el('span', 'scmc-chem__mixture-dot'),
el('span', 'scmc-chem__mixture-name', item.name || getName(item.id)),
el('span', 'scmc-chem__mixture-amount', formatAmount(item.amount))
));
list.lastChild.firstChild.style.backgroundColor = getColor(item.id);
});
return list;
}
function renderReaction(reaction) {
var box = el('div', 'scmc-chem__reaction');
var equation = add(el('div', 'scmc-chem__equation'),
renderMixture(reaction.reactants, 'scmc-chem__reactants'),
el('span', 'scmc-chem__arrow', '→'),
renderMixture(reaction.products, 'scmc-chem__products')
);
add(box, el('div', 'scmc-chem__reaction-label', 'Рецепт'), equation);
if (!reaction.products || !reaction.products.length) {
add(box, el('div', 'scmc-chem__reaction-warning', 'Реакция не создаёт реагент и может иметь опасный эффект.'));
}
return box;
}
function effectType(effect) {
return String(effect && effect.yamlTag || '').replace(/^!type:/, '') || 'Эффект';
}
function describeValue(value) {
if (value === null || value === undefined || value === '') {
return '';
}
if (typeof value !== 'object') {
return String(value);
}
return Object.keys(value).filter(function (key) {
return !['conditions', 'showInGuidebook', 'showInChat'].includes(key);
}).map(function (key) {
var item = value[key];
if (item && typeof item === 'object') {
return key + ': ' + JSON.stringify(item);
}
return key + ': ' + item;
}).join(' · ');
}
function renderEffects(effects) {
var list = el('div', 'scmc-chem__effect-list');
if (!effects || !effects.length) {
add(list, el('p', 'scmc-chem__empty', 'Эффекты не указаны.'));
return list;
}
effects.forEach(function (effect) {
var type = effectType(effect);
var value = effect.value || {};
var harmful = /Damage|Ignite|Explosion|Electrocute|Vomit|Poison/i.test(type);
var card = el('div', 'scmc-chem__effect' + (harmful ? ' scmc-chem__effect--harmful' : ''));
add(card, el('div', 'scmc-chem__effect-name', EFFECT_LABELS[type] || type));
var details = describeValue(value);
if (details) {
add(card, el('div', 'scmc-chem__effect-details', details));
}
add(list, card);
});
return list;
}
function renderMetabolisms(reagent) {
var wrapper = el('div', 'scmc-chem__card-section');
add(wrapper, el('h4', 'scmc-chem__card-subtitle', 'Воздействие'));
var metabolisms = reagent.properties && reagent.properties.metabolisms;
var keys = Object.keys(metabolisms || {});
if (!keys.length) {
add(wrapper, el('p', 'scmc-chem__empty', 'Метаболические эффекты не указаны.'));
return wrapper;
}
keys.forEach(function (key) {
var metabolism = metabolisms[key] || {};
var block = el('div', 'scmc-chem__metabolism');
var title = METABOLISM_LABELS[key] || key;
if (metabolism.metabolismRate !== undefined) {
title += ' · ' + formatAmount(metabolism.metabolismRate) + '/с';
}
add(block, el('div', 'scmc-chem__metabolism-title', title), renderEffects(metabolism.effects));
add(wrapper, block);
});
return wrapper;
}
function renderCard(entry) {
var reagent = getReagent(entry.id) || {
id: entry.id,
name: entry.name,
origin: entry.origin,
properties: {}
};
var color = safeColor(reagent.properties && reagent.properties.color);
var details = el('details', 'scmc-chem__card');
details.style.setProperty('--scmc-chem-color', color);
var summary = el('summary', 'scmc-chem__card-summary');
var heading = add(el('span', 'scmc-chem__card-heading'),
el('span', 'scmc-chem__card-name', reagent.name || entry.name || entry.id),
el('code', 'scmc-chem__card-id', entry.id)
);
add(summary,
el('span', 'scmc-chem__color'),
heading,
el('span', 'scmc-chem__origin scmc-chem__origin--' + (reagent.origin || entry.origin || 'upstream-reference'), ORIGIN_LABELS[reagent.origin || entry.origin] || reagent.origin || entry.origin),
el('span', 'scmc-chem__chevron')
);
summary.firstChild.style.backgroundColor = color;
var body = el('div', 'scmc-chem__card-body');
add(body, el('p', reagent.description ? 'scmc-chem__description' : 'scmc-chem__empty', reagent.description || 'Описание не указано.'));
var overdose = reagent.properties && reagent.properties.overdose;
var critical = reagent.properties && reagent.properties.criticalOverdose;
if (overdose !== undefined || critical !== undefined) {
var doses = el('div', 'scmc-chem__dose-row');
if (overdose !== undefined) {
add(doses, el('span', 'scmc-chem__dose scmc-chem__dose--warning', 'Передозировка: ' + formatAmount(overdose)));
}
if (critical !== undefined) {
add(doses, el('span', 'scmc-chem__dose scmc-chem__dose--danger', 'Критическая: ' + formatAmount(critical)));
}
add(body, doses);
}
var reactions = state.productReactions[entry.id] || [];
if (reactions.length) {
var recipeSection = el('div', 'scmc-chem__card-section');
add(recipeSection, el('h4', 'scmc-chem__card-subtitle', 'Получение'));
reactions.forEach(function (reaction) { add(recipeSection, renderReaction(reaction)); });
add(body, recipeSection);
}
add(body, renderMetabolisms(reagent));
add(details, summary, body);
return details;
}
function renderResults(root) {
var result = root.querySelector('.scmc-chem__results');
var counter = root.querySelector('.scmc-chem__result-count');
result.textContent = '';
var entries = state.data.catalogSections[state.activeTab].filter(entryMatches);
var groups = groupEntries(entries);
counter.textContent = entries.length + ' найдено';
renderSectionNav(root, groups);
if (!entries.length) {
add(result, el('div', 'scmc-chem__no-results', 'Ничего не найдено. Измените запрос.'));
return;
}
groups.forEach(function (group, index) {
var section = el('section', 'scmc-chem__section');
section.id = 'scmc-chem-section-' + index + '-' + slug(group.title);
var heading = add(el('div', 'scmc-chem__section-heading'),
el('h2', 'scmc-chem__section-title', group.title),
el('span', 'scmc-chem__breadcrumbs', group.path.join(' › ')),
el('span', 'scmc-chem__section-count', group.entries.length)
);
var cards = el('div', 'scmc-chem__cards');
group.entries.forEach(function (entry) { add(cards, renderCard(entry)); });
add(section, heading, cards);
add(result, section);
});
}
function renderApp(root) {
root.textContent = '';
add(root, createHero());
var controls = el('div', 'scmc-chem__controls');
var search = el('label', 'scmc-chem__search');
var input = el('input', 'scmc-chem__search-input');
input.type = 'search';
input.placeholder = 'Название, ID или раздел…';
input.autocomplete = 'off';
input.setAttribute('aria-label', 'Поиск по химическому каталогу');
input.addEventListener('input', function () {
state.query = normalize(input.value);
renderResults(root);
});
add(search, el('span', 'scmc-chem__search-icon', '⌕'), input);
add(controls, search, el('span', 'scmc-chem__result-count'));
add(root, controls, el('div', 'scmc-chem__tabs'));
var nav = el('nav', 'scmc-chem__section-nav');
nav.setAttribute('aria-label', 'Подразделы каталога');
add(root, nav, el('main', 'scmc-chem__results'));
var source = state.data.source || {};
add(root, el('footer', 'scmc-chem__footer', 'Локальный каталог MediaWiki' + (source.commit ? ' · версия ' + String(source.commit).slice(0, 8) : '')));
renderTabs(root);
renderResults(root);
}
function start(root) {
renderLoading(root);
loadCatalog().then(function (data) {
state.data = data;
indexReactions();
renderApp(root);
}).catch(function (error) {
renderError(root, error);
});
}
function init() {
var root = document.getElementById(ROOT_ID);
if (!root) {
return;
}
loadStyle();
start(root);
}
$(init);
}(mediaWiki, jQuery));