| Строка 1: |
Строка 1: |
| /* | | /** |
| * Интерактивный каталог химии SSMC. | | * Интерактивный каталог химии SSMC. |
| * Страница: MC:Химия | | * Страница: MC:Химия |
| * Данные: DeferW/ssmc-wiki-data | | * Данные: MediaWiki:ScmcChemCatalog.js |
| */ | | */ |
| (function () { | | (function (mw, $) { |
| 'use strict'; | | 'use strict'; |
|
| |
|
| var ROOT_ID = 'scmc-chemistry-app'; | | var ROOT_ID = 'scmc-chemistry-app'; |
| var DATA_URL = 'https://raw.githubusercontent.com/DeferW/ssmc-wiki-data/main/data/chemistry-catalog.json'; | | var CATALOG_PAGE = 'MediaWiki:ScmcChemCatalog.js'; |
| var NUMBER = new Intl.NumberFormat('ru-RU', { | | var STYLE_PAGE = 'MediaWiki:ScmcChemistry.css'; |
| maximumFractionDigits: 3
| | var NUMBER = new Intl.NumberFormat('ru-RU', { maximumFractionDigits: 3 }); |
| });
| | var TABS = [ |
| | |
| var TAB_DEFINITIONS = [ | |
| { id: 'ordnance', label: 'Боевая химия' }, | | { id: 'ordnance', label: 'Боевая химия' }, |
| { id: 'medicine', label: 'Медицина' }, | | { id: 'medicine', label: 'Медицина' }, |
| Строка 20: |
Строка 18: |
| { id: 'other', label: 'Другие вещества' } | | { id: 'other', label: 'Другие вещества' } |
| ]; | | ]; |
| | | var ORIGIN_LABELS = { |
| var REQUIRED_CATALOG_SECTIONS = TAB_DEFINITIONS.map(function (tab) {
| | rmc14: 'RMC14', |
| return tab.id;
| | stories: 'Stories', |
| });
| | 'upstream-reference': 'Upstream' |
| | |
| var DAMAGE_LABELS = {
| |
| Airloss: 'урона от кислородного голодания',
| |
| Asphyxiation: 'урона от кислородного голодания',
| |
| Bloodloss: 'урона от кровопотери',
| |
| Blunt: 'тупого урона',
| |
| Brute: 'механического урона',
| |
| Burn: 'термического урона',
| |
| Caustic: 'кислотного урона',
| |
| Cellular: 'клеточного урона',
| |
| Cold: 'холодового урона',
| |
| Genetic: 'генетического урона',
| |
| Heat: 'ожогового урона',
| |
| Piercing: 'колотого урона',
| |
| Poison: 'токсического урона',
| |
| Radiation: 'радиационного урона',
| |
| Slash: 'режущего урона',
| |
| Toxin: 'токсического урона'
| |
| };
| |
| | |
| /*
| |
| * Смысловые фрагменты в описаниях эффектов. Цвет не заменяет текст:
| |
| * он лишь помогает быстро отличить лечение, вред и конкретный тип урона.
| |
| */
| |
| var EFFECT_TOKEN_RE = /(урона от кислородного голодания|урона от кровопотери|полностью лечит|дополнительно наносит|восстанавливает жизнеспособность|создаёт иммунитет|восстанавливает|возвращает|омолаживает|сокращает|удаляет|лечит|наносит|вызывает|заражает|поджигает|уничтожает|расходует|замедляет|снижает|повышает|механического(?: урона)?|тупого(?: урона)?|колотого(?: урона)?|режущего(?: урона)?|термического(?: урона)?|ожогового(?: урона)?|холодового(?: урона)?|токсического(?: урона)?|кислотного(?: урона)?|генетического(?: урона)?|клеточного(?: урона)?|радиационного(?: урона)?)/gi;
| |
| | |
| var EFFECT_ACTION_CLASSES = {
| |
| 'полностью лечит': 'beneficial',
| |
| 'лечит': 'beneficial',
| |
| 'восстанавливает': 'beneficial',
| |
| 'восстанавливает жизнеспособность': 'beneficial',
| |
| 'возвращает': 'beneficial',
| |
| 'омолаживает': 'beneficial',
| |
| 'сокращает': 'beneficial',
| |
| 'удаляет': 'beneficial',
| |
| 'создаёт иммунитет': 'beneficial',
| |
| 'наносит': 'harmful',
| |
| 'дополнительно наносит': 'harmful',
| |
| 'вызывает': 'harmful',
| |
| 'заражает': 'harmful',
| |
| 'поджигает': 'harmful',
| |
| 'уничтожает': 'harmful',
| |
| 'расходует': 'warning',
| |
| 'замедляет': 'warning',
| |
| 'снижает': 'utility',
| |
| 'повышает': 'utility'
| |
| };
| |
| | |
| var DAMAGE_TOKEN_CLASSES = [
| |
| { pattern: /кислородного голодания|кровопотери/, className: 'oxygen' },
| |
| { pattern: /механического|тупого|колотого|режущего/, className: 'physical' },
| |
| { pattern: /термического|ожогового/, className: 'burn' },
| |
| { pattern: /холодового/, className: 'cold' },
| |
| { pattern: /токсического/, className: 'toxin' },
| |
| { pattern: /кислотного/, className: 'acid' },
| |
| { pattern: /генетического/, className: 'genetic' },
| |
| { pattern: /клеточного/, className: 'cellular' },
| |
| { pattern: /радиационного/, className: 'radiation' }
| |
| ];
| |
| | |
| var STATUS_LABELS = {
| |
| Jitter: 'дрожь',
| |
| RMCUnconscious: 'потерю сознания',
| |
| StatusEffectSeeingRainbow: 'галлюцинации',
| |
| Unconscious: 'потерю сознания'
| |
| };
| |
| | |
| var EMOTE_LABELS = { | |
| Gasp: 'судорожный вдох', | |
| Laugh: 'смех', | |
| Scream: 'крик', | |
| Yawn: 'зевоту'
| |
| };
| |
| | |
| var PLANT_ATTRIBUTE_LABELS = {
| |
| PlantAdjustGrowth: 'возраст растения',
| |
| PlantAdjustHealth: 'здоровье растения',
| |
| PlantAdjustMutationLevel: 'уровень мутации',
| |
| PlantAdjustMutationMod: 'модификатор мутации',
| |
| PlantAdjustNutrition: 'уровень питания',
| |
| PlantAdjustPests: 'уровень вредителей',
| |
| PlantAdjustPotency: 'потенцию растения',
| |
| PlantAdjustToxins: 'уровень токсинов',
| |
| PlantAdjustWater: 'уровень воды',
| |
| PlantAdjustWeeds: 'уровень сорняков',
| |
| PlantAffectGrowth: 'возраст растения'
| |
| }; | | }; |
|
| |
| 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 element(tag, className, text) { | | 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 append(parent) { | | function add(parent) { |
| var children = Array.prototype.slice.call(arguments, 1); | | Array.prototype.slice.call(arguments, 1).forEach(function (child) { |
| | |
| children.forEach(function (child) {
| |
| if (child) { | | if (child) { |
| parent.appendChild(child); | | parent.appendChild(child); |
| } | | } |
| }); | | }); |
|
| |
| return parent; | | return parent; |
| } | | } |
| Строка 162: |
Строка 91: |
| } | | } |
|
| |
|
| function capitalizeFirst(value) { | | function normalize(value) { |
| var text = String(value || ''); | | return String(value || '').toLocaleLowerCase('ru-RU').replace(/ё/g, 'е').trim(); |
| | |
| if (!text) {
| |
| return text;
| |
| }
| |
| | |
| return text.charAt(0).toLocaleUpperCase('ru-RU') + text.slice(1);
| |
| } | | } |
|
| |
|
| function finishSentence(value) { | | function slug(value) { |
| var text = capitalizeFirst(String(value || '').trim()); | | return String(value || 'section') |
| | | .toLocaleLowerCase('ru-RU') |
| if (!text || /[.!?]$/.test(text)) {
| | .replace(/[^a-zа-яё0-9]+/gi, '-') |
| return text; | | .replace(/^-+|-+$/g, ''); |
| }
| |
| | |
| return text + '.';
| |
| } | | } |
|
| |
|
| function safeColor(value) { | | function safeColor(value) { |
| if (typeof value !== 'string') { | | var probe = document.createElement('span').style; |
| return '#7f8c8d';
| | probe.color = ''; |
| } | | probe.color = String(value || ''); |
| | | return probe.color ? value : '#7f8c98'; |
| if (/^#[0-9a-f]{3,8}$/i.test(value) || /^[a-z]+$/i.test(value)) {
| |
| return value;
| |
| }
| |
| | |
| return '#7f8c8d';
| |
| }
| |
| | |
| function tagName(tag) {
| |
| return String(tag || '').replace(/^!type:/, ''); | |
| } | | } |
|
| |
|
| function reagentById(id) { | | function getReagent(id) { |
| return state.data.reagents[id] || state.data.dependencies[id] || null; | | return state.data.reagents[id] || state.data.dependencies[id] || null; |
| } | | } |
|
| |
|
| function reagentName(id) { | | function getName(id) { |
| var reagent = reagentById(id); | | var reagent = getReagent(id); |
| return reagent && reagent.name ? reagent.name : id; | | return reagent && reagent.name ? reagent.name : id; |
| } | | } |
|
| |
|
| function normalizeSearch(value) { | | function getColor(id) { |
| return String(value || '').trim().toLocaleLowerCase('ru-RU'); | | var reagent = getReagent(id); |
| | return safeColor(reagent && reagent.properties && reagent.properties.color); |
| } | | } |
|
| |
|
| function matchesQuery(entry, reagent) { | | function indexReactions() { |
| if (!state.query) {
| |
| return true;
| |
| }
| |
| | |
| return normalizeSearch([
| |
| entry.id,
| |
| entry.name,
| |
| reagent && reagent.name,
| |
| reagent && reagent.description,
| |
| entry.sectionPath && entry.sectionPath.join(' ')
| |
| ].filter(Boolean).join(' ')).indexOf(state.query) !== -1;
| |
| }
| |
| | |
| function buildProductReactionIndex() {
| |
| 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 entriesForTab(tabId) { | | function validateCatalog(data) { |
| return state.data.catalogSections[tabId] || []; | | 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 effectValue(effect) { | | function loadStyle() { |
| var value = effect && effect.value; | | mw.loader.load(mw.util.getUrl(STYLE_PAGE, { |
| return value && typeof value === 'object' ? value : {};
| | action: 'raw', |
| | ctype: 'text/css' |
| | }), 'text/css'); |
| } | | } |
|
| |
|
| function lowerFirst(value) { | | function loadCatalog() { |
| var text = String(value || ''); | | window.ScmcChemCatalog = null; |
| return text ? text.charAt(0).toLocaleLowerCase('ru-RU') + text.slice(1) : text; | | return mw.loader.getScript(mw.util.getUrl(CATALOG_PAGE, { |
| }
| | action: 'raw', |
| | | ctype: 'text/javascript' |
| function withChance(text, probability) {
| | })).then(function () { |
| if (typeof probability !== 'number' || probability >= 1) {
| | if (!validateCatalog(window.ScmcChemCatalog)) { |
| return finishSentence(text); | | throw new Error('MediaWiki:ScmcChemCatalog.js не содержит поддерживаемый каталог'); |
| } | |
| | |
| return finishSentence('С шансом ' + formatNumber(probability * 100) + '% ' + lowerFirst(text));
| |
| }
| |
| | |
| function conditionText(condition) {
| |
| var type = tagName(condition && condition.yamlTag);
| |
| var value = effectValue(condition);
| |
| | |
| if (type === 'ReagentThreshold') {
| |
| if (value.min !== undefined && value.max !== undefined) { | |
| return 'при количестве от ' + formatAmount(value.min) + ' до ' + formatAmount(value.max); | |
| } | | } |
| if (value.min !== undefined) { | | return window.ScmcChemCatalog; |
| return 'при количестве от ' + formatAmount(value.min) + ' в организме';
| | }); |
| }
| |
| if (value.max !== undefined) {
| |
| return 'при количестве до ' + formatAmount(value.max) + ' в организме';
| |
| }
| |
| }
| |
| | |
| if (type === 'Temperature') {
| |
| if (value.min !== undefined && value.max !== undefined) {
| |
| return 'при температуре от ' + formatNumber(value.min) + ' до ' + formatNumber(value.max) + ' K';
| |
| }
| |
| if (value.min !== undefined) {
| |
| return 'при температуре не ниже ' + formatNumber(value.min) + ' K';
| |
| }
| |
| if (value.max !== undefined) {
| |
| return 'при температуре не выше ' + formatNumber(value.max) + ' K';
| |
| }
| |
| } | |
| | |
| if (type === 'MobStateCondition') {
| |
| return value.mobstate === 'Critical' ? 'в критическом состоянии' : 'в состоянии «' + (value.mobstate || 'неизвестно') + '»';
| |
| }
| |
| | |
| if (type === 'OrganType') {
| |
| return (value.shouldHave === false ? 'кроме организмов типа ' : 'для организмов типа ') + (value.type || 'неизвестно');
| |
| }
| |
| | |
| return type ? 'при выполнении условия «' + type + '»' : '';
| |
| } | | } |
|
| |
|
| function thresholdMinimum(effect) { | | function renderLoading(root) { |
| var conditions = effectValue(effect).conditions; | | root.textContent = ''; |
| var minimum = null;
| | add(root, add(el('div', 'scmc-chem__status scmc-chem__status--loading'), |
| | | el('span', 'scmc-chem__spinner'), |
| if (!Array.isArray(conditions)) { | | el('span', '', 'Загружаю химический каталог…') |
| return minimum;
| | )); |
| }
| |
| | |
| conditions.forEach(function (condition) {
| |
| if (tagName(condition && condition.yamlTag) !== 'ReagentThreshold') { | |
| return;
| |
| } | |
| | |
| var value = effectValue(condition);
| |
| if (typeof value.min === 'number' && (minimum === null || value.min > minimum)) {
| |
| minimum = value.min;
| |
| }
| |
| }); | |
| | |
| return minimum;
| |
| } | | } |
|
| |
|
| function effectTier(effect, reagent) { | | function renderError(root, error) { |
| var minimum = thresholdMinimum(effect); | | root.textContent = ''; |
| var properties = reagent.properties || {}; | | var block = add(el('div', 'scmc-chem__status scmc-chem__status--error'), |
| | | el('strong', '', 'Каталог не загрузился.'), |
| if (minimum === null) { | | el('span', '', ' Проверьте MediaWiki:ScmcChemCatalog.js.') |
| return 'normal'; | | ); |
| | 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); |
| } | | } |
| if (typeof properties.criticalOverdose === 'number' && minimum >= properties.criticalOverdose) {
| |
| return 'critical';
| |
| }
| |
| if (typeof properties.overdose === 'number' && minimum >= properties.overdose) {
| |
| return 'overdose';
| |
| }
| |
|
| |
| return 'normal';
| |
| } | | } |
|
| |
|
| function remainingConditions(effect, reagent) { | | function createHero() { |
| var conditions = effectValue(effect).conditions; | | var count = TABS.reduce(function (total, tab) { |
| | | return total + state.data.catalogSections[tab.id].length; |
| if (!Array.isArray(conditions)) {
| | }, 0); |
| return []; | | return add(el('header', 'scmc-chem__hero'), |
| } | | add(el('div', 'scmc-chem__hero-copy'), |
| | | el('p', 'scmc-chem__eyebrow', 'SSMC · справочник'), |
| return conditions.map(function (condition) { | | el('h1', 'scmc-chem__title', 'Химия'), |
| if (tagName(condition && condition.yamlTag) === 'ReagentThreshold' && effectTier(effect, reagent) !== 'normal') { | | el('p', 'scmc-chem__lead', 'Реагенты, рецепты, дозировки и эффекты. Данные хранятся прямо на вики.') |
| return ''; | | ), |
| } | | add(el('div', 'scmc-chem__stats'), |
| return conditionText(condition); | | el('span', 'scmc-chem__stat', count + ' записей'), |
| }).filter(Boolean);
| | el('span', 'scmc-chem__stat', Object.keys(state.data.reactions).length + ' реакций') |
| | ) |
| | ); |
| } | | } |
|
| |
|
| function damageChanges(damage, equalGroups) { | | function renderTabs(root) { |
| var entries = []; | | var tabs = root.querySelector('.scmc-chem__tabs'); |
| | | tabs.textContent = ''; |
| if (equalGroups && Array.isArray(damage)) { | | TABS.forEach(function (tab) { |
| damage.forEach(function (entry) {
| | var button = el('button', 'scmc-chem__tab'); |
| var key = Object.keys(entry || {})[0];
| | button.type = 'button'; |
| if (key) {
| | button.setAttribute('role', 'tab'); |
| entries.push({ id: key, amount: entry[key] });
| | button.setAttribute('aria-selected', String(tab.id === state.activeTab)); |
| }
| | add(button, |
| }); | | el('span', '', tab.label), |
| } else if (damage && typeof damage === 'object') {
| | el('span', 'scmc-chem__tab-count', state.data.catalogSections[tab.id].length) |
| ['groups', 'types'].forEach(function (key) { | | ); |
| var values = damage[key]; | | button.addEventListener('click', function () { |
| | | state.activeTab = tab.id; |
| if (!values || typeof values !== 'object') {
| | renderTabs(root); |
| return;
| | renderResults(root); |
| }
| |
| | |
| Object.keys(values).forEach(function (damageId) {
| |
| entries.push({ id: damageId, amount: values[damageId] });
| |
| }); | |
| }); | | }); |
| }
| | add(tabs, button); |
| | |
| return entries.map(function (entry) {
| |
| var action = entry.amount < 0 ? 'Лечит' : 'Наносит'; | |
| return action + ' ' + formatNumber(Math.abs(entry.amount)) + ' ед. ' + (DAMAGE_LABELS[entry.id] || ('урона типа «' + entry.id + '»')) + ' в секунду';
| |
| }); | | }); |
| } | | } |
|
| |
|
| function doseDescription(tier, text) { | | function groupEntries(entries) { |
| return { tier: tier, text: finishSentence(text) };
| | var groups = Object.create(null); |
| }
| | entries.forEach(function (entry) { |
| | | var path = Array.isArray(entry.sectionPath) && entry.sectionPath.length ? entry.sectionPath : ['Без раздела']; |
| function addDoseEffects(result, reagent, overdoseText, criticalText) {
| | var key = path.join(' › '); |
| var properties = reagent.properties || {}; | | if (!groups[key]) { |
| | | groups[key] = { title: path[path.length - 1], path: path, entries: [] }; |
| if (properties.overdose !== undefined && overdoseText) {
| |
| result.push(doseDescription('overdose', overdoseText));
| |
| } | |
| if (properties.criticalOverdose !== undefined && criticalText) {
| |
| result.push(doseDescription('critical', criticalText));
| |
| }
| |
| }
| |
| | |
| function rmcEffectDescriptions(type, value, reagent) {
| |
| var result = [];
| |
| var potency = Number(value.potency || 0);
| |
| var actual = potency * 0.5;
| |
| var perSecond = potency * 0.25;
| |
| var healing;
| |
| | |
| if (type === 'Neogenetic') {
| |
| healing = perSecond * (actual > 2 ? 1.5 : 1);
| |
| result.push(doseDescription('normal', 'Лечит ' + formatNumber(healing) + ' ед. механического урона в секунду'));
| |
| addDoseEffects(result, reagent, | |
| 'Наносит ' + formatNumber(perSecond) + ' ед. ожогового урона в секунду',
| |
| 'Дополнительно наносит ' + formatNumber(perSecond * 5) + ' ед. ожогового и ' + formatNumber(perSecond * 2) + ' ед. токсического урона в секунду');
| |
| return result;
| |
| }
| |
| | |
| if (type === 'Anticorrosive') {
| |
| healing = perSecond * (actual > 2 ? 1.5 : 1);
| |
| result.push(doseDescription('normal', 'Лечит ' + formatNumber(healing) + ' ед. термического урона в секунду'));
| |
| addDoseEffects(result, reagent,
| |
| 'Наносит по ' + formatNumber(perSecond) + ' ед. тупого и токсического урона в секунду',
| |
| 'Дополнительно наносит по ' + formatNumber(perSecond * 5) + ' ед. тупого и токсического урона в секунду');
| |
| return result; | |
| }
| |
| | |
| if (type === 'Biocidic') {
| |
| result.push(doseDescription('normal', 'Наносит ' + formatNumber(perSecond) + ' ед. механического урона в секунду'));
| |
| addDoseEffects(result, reagent,
| |
| 'Наносит ' + formatNumber(perSecond * 2) + ' ед. механического урона в секунду', | |
| 'Дополнительно наносит ' + formatNumber(perSecond * 5) + ' ед. механического урона в секунду');
| |
| return result;
| |
| }
| |
| | |
| if (type === 'Antitoxic') {
| |
| result.push(doseDescription('normal', 'Лечит по ' + formatNumber(perSecond * 2) + ' ед. токсического и генетического урона в секунду; удаляет 0,125 ед. токсичных веществ из крови в секунду'));
| |
| addDoseEffects(result, reagent, '', 'Дополнительно даёт 5% шанс потерять сознание на 10 секунд');
| |
| return result;
| |
| }
| |
| | |
| if (type === 'Oxygenating') {
| |
| result.push(doseDescription('normal', actual >= 3
| |
| ? 'Полностью лечит урон от кислородного голодания и удаляет ' + formatAmount(perSecond) + ' Лексорина из крови в секунду'
| |
| : 'Лечит ' + formatNumber(perSecond) + ' ед. урона от кислородного голодания и удаляет ' + formatAmount(perSecond) + ' Лексорина из крови в секунду'));
| |
| addDoseEffects(result, reagent,
| |
| 'Наносит ' + formatNumber(perSecond * 0.5) + ' ед. токсического урона в секунду',
| |
| 'Дополнительно наносит ' + formatNumber(perSecond) + ' ед. тупого и ' + formatNumber(perSecond * 2) + ' ед. токсического урона в секунду');
| |
| return result;
| |
| }
| |
| | |
| if (type === 'Electrogenetic') {
| |
| result.push(doseDescription('normal', 'При дефибрилляции лечит по ' + formatNumber(potency * 10) + ' ед. механического, термического и токсического урона; расходует 1 ед. препарата'));
| |
| return result;
| |
| }
| |
| | |
| if (type === 'Antihallucinogenic') {
| |
| result.push(doseDescription('normal', 'Удаляет по 2,5 ед. Токсина «Разрушитель разума» и космических наркотиков из крови; сокращает галлюцинации на ' + formatNumber(perSecond * 10) + ' сек. каждую секунду'));
| |
| addDoseEffects(result, reagent,
| |
| 'Наносит ' + formatNumber(perSecond) + ' ед. токсического урона в секунду',
| |
| 'Дополнительно наносит по ' + formatNumber(perSecond) + ' ед. тупого и ожогового урона, а также ' + formatNumber(perSecond * 3) + ' ед. токсического урона в секунду');
| |
| return result;
| |
| }
| |
| | |
| if (type === 'Focusing') {
| |
| result.push(doseDescription('normal', 'Удаляет ' + formatAmount(perSecond) + ' алкоголя из крови и сокращает опьянение, заикание и дрожь на ' + formatNumber(perSecond * 2) + ' сек. каждую секунду' + (actual >= 3 ? '; также снимает немоту и слепоту' : '')));
| |
| addDoseEffects(result, reagent,
| |
| 'Наносит ' + formatNumber(perSecond) + ' ед. токсического урона в секунду',
| |
| 'Дополнительно наносит ' + formatNumber(perSecond * 3) + ' ед. токсического урона в секунду');
| |
| return result;
| |
| }
| |
| | |
| if (type === 'Thermostabilizing') {
| |
| result.push(doseDescription('normal', 'Возвращает температуру тела к норме, изменяя её примерно на ' + formatNumber(perSecond * 60) + ' K в секунду'));
| |
| addDoseEffects(result, reagent,
| |
| 'Вызывает потерю сознания на 40 секунд',
| |
| 'Дополнительно даёт 5% шанс потерять сознание на 10 секунд');
| |
| return result;
| |
| }
| |
| | |
| if (type === 'Hemogenic') {
| |
| result.push(doseDescription('normal', 'Если персонаж не голоден, восстанавливает ' + formatNumber(perSecond) + ' сл крови и расходует ' + formatNumber(perSecond) + ' ед. питания в секунду'));
| |
| if (actual > 3) {
| |
| result.push(doseDescription('normal', 'При избытке крови наносит ' + formatNumber(perSecond) + ' ед. тупого и ' + formatNumber(perSecond * 2) + ' ед. урона от кислородного голодания в секунду, а также замедляет'));
| |
| } | | } |
| addDoseEffects(result, reagent, | | groups[key].entries.push(entry); |
| 'Наносит ' + formatNumber(perSecond) + ' ед. токсического урона в секунду',
| | }); |
| 'Дополнительно расходует ' + formatNumber(perSecond * 5) + ' ед. питания в секунду');
| | return Object.keys(groups).map(function (key) { return groups[key]; }); |
| return result;
| |
| }
| |
| | |
| if (type === 'Ketogenic') {
| |
| result.push(doseDescription('normal', 'Расходует ' + formatNumber(perSecond * 5) + ' ед. питания в секунду и ускоряет выведение алкоголя'));
| |
| addDoseEffects(result, reagent,
| |
| 'Дополнительно расходует ' + formatNumber(perSecond * 5) + ' ед. питания, наносит ' + formatNumber(perSecond) + ' ед. токсического урона в секунду и даёт ' + formatNumber(actual * 2.5) + '% шанс рвоты',
| |
| 'Вызывает потерю сознания на 40 секунд');
| |
| return result;
| |
| } | |
| | |
| if (type === 'Nutritious') {
| |
| var nutrition = ((Number(value.nutFactor || 0) * Number(value.nutMetabolism || 0)) + potency) * actual;
| |
| result.push(doseDescription('normal', 'Восстанавливает ' + formatNumber(nutrition) + ' ед. питания за цикл метаболизма'));
| |
| return result;
| |
| }
| |
| | |
| if (type === 'Toxic') { | |
| result.push(doseDescription('normal', 'Наносит ' + formatNumber(perSecond) + ' ед. токсического урона в секунду'));
| |
| addDoseEffects(result, reagent,
| |
| 'Наносит ' + formatNumber(perSecond * 2) + ' ед. токсического урона в секунду',
| |
| 'Дополнительно наносит ' + formatNumber(perSecond * 5) + ' ед. токсического урона в секунду');
| |
| return result;
| |
| }
| |
| | |
| if (type === 'Corrosive') {
| |
| result.push(doseDescription('normal', 'Наносит ' + formatNumber(perSecond) + ' ед. кислотного урона в секунду'));
| |
| addDoseEffects(result, reagent,
| |
| 'Наносит ' + formatNumber(perSecond * 2) + ' ед. кислотного урона в секунду',
| |
| 'Дополнительно наносит ' + formatNumber(perSecond * 5) + ' ед. кислотного урона в секунду');
| |
| return result;
| |
| }
| |
| | |
| if (type === 'Hypoxemic') {
| |
| result.push(doseDescription('normal', 'Наносит ' + formatNumber(perSecond * 2) + ' ед. урона от кислородного голодания в секунду и даёт 10% шанс судорожного вдоха'));
| |
| addDoseEffects(result, reagent,
| |
| 'Наносит по ' + formatNumber(perSecond) + ' ед. тупого и токсического урона, а также ' + formatNumber(perSecond * 5) + ' ед. урона от кислородного голодания в секунду',
| |
| 'Дополнительно наносит ' + formatNumber(perSecond * 5) + ' ед. тупого и ' + formatNumber(perSecond * 2) + ' ед. токсического урона в секунду');
| |
| return result;
| |
| }
| |
| | |
| if (type === 'Carcinogenic') {
| |
| result.push(doseDescription('normal', 'Наносит ' + formatNumber(perSecond * 0.5) + ' ед. клеточного урона в секунду'));
| |
| addDoseEffects(result, reagent,
| |
| 'Наносит ' + formatNumber(perSecond * 2) + ' ед. клеточного урона в секунду',
| |
| 'Дополнительно наносит ' + formatNumber(perSecond * 2) + ' ед. тупого урона в секунду');
| |
| return result;
| |
| }
| |
| | |
| if (type === 'Oxidizing') {
| |
| result.push(doseDescription('normal', 'Наносит ' + formatNumber(perSecond) + ' ед. ожогового урона в секунду; в огненной смеси повышает интенсивность, но уменьшает длительность и радиус'));
| |
| addDoseEffects(result, reagent,
| |
| 'Наносит ' + formatNumber(perSecond * 2) + ' ед. ожогового урона в секунду',
| |
| 'Дополнительно наносит ' + formatNumber(perSecond * 5) + ' ед. ожогового урона в секунду');
| |
| return result;
| |
| }
| |
| | |
| if (type === 'Fueling') {
| |
| result.push(doseDescription('normal', 'Повышает воспламеняемость тела на ' + formatNumber(perSecond) + ' ед. в секунду; в огненной смеси увеличивает длительность и радиус, но снижает интенсивность'));
| |
| addDoseEffects(result, reagent,
| |
| 'Повышает воспламеняемость тела на ' + formatNumber(perSecond * 2) + ' ед. в секунду',
| |
| 'Поджигает тело с высокой интенсивностью');
| |
| return result;
| |
| }
| |
| | |
| if (type === 'Flowing') {
| |
| result.push(doseDescription('normal', 'Делает огненную смесь более текучей: увеличивает радиус, но снижает интенсивность и длительность горения'));
| |
| return result;
| |
| }
| |
| | |
| if (type === 'Viscous') {
| |
| result.push(doseDescription('normal', 'Замедляет движение на ' + formatNumber(perSecond) + ' сек. за каждый цикл и уменьшает радиус огненной смеси'));
| |
| addDoseEffects(result, reagent,
| |
| 'Сильно замедляет движение на ' + formatNumber(perSecond * 2) + ' сек. за каждый цикл',
| |
| 'Практически останавливает движение на ' + formatNumber(perSecond * 3) + ' сек. за каждый цикл');
| |
| return result;
| |
| }
| |
| | |
| return null;
| |
| } | | } |
|
| |
|
| function genericEffectDescription(effect, reagent) { | | function entryMatches(entry) { |
| var type = tagName(effect && effect.yamlTag); | | if (!state.query) { |
| var value = effectValue(effect);
| | return true; |
| var probability = value.probability;
| |
| var tier = effectTier(effect, reagent);
| |
| var conditions = remainingConditions(effect, reagent);
| |
| var text = '';
| |
| | |
| if (probability === 0) {
| |
| return [];
| |
| }
| |
| | |
| var rmc = rmcEffectDescriptions(type, value, reagent);
| |
| if (rmc) {
| |
| return rmc;
| |
| }
| |
| | |
| if (type === 'HealthChange' || type === 'EqualHealthChange') {
| |
| text = damageChanges(value.damage, type === 'EqualHealthChange').join('; ');
| |
| } else if (type === 'AdjustReagent' && value.reagent) {
| |
| text = (value.amount >= 0 ? 'Добавляет ' : 'Удаляет ') + formatAmount(Math.abs(value.amount || 0)) + ' реагента «' + capitalizeFirst(reagentName(value.reagent)) + '» за цикл метаболизма';
| |
| } else if (type === 'MovespeedModifier') {
| |
| var walk = Math.round((Number(value.walkSpeedModifier || 1) - 1) * 100);
| |
| var sprint = Math.round((Number(value.sprintSpeedModifier || 1) - 1) * 100);
| |
| text = 'Изменяет скорость ходьбы на ' + (walk >= 0 ? '+' : '') + walk + '% и бега на ' + (sprint >= 0 ? '+' : '') + sprint + '%';
| |
| } else if (type === 'GenericStatusEffect') {
| |
| var status = STATUS_LABELS[value.key] || value.key || 'неизвестный статус';
| |
| text = value.type === 'Remove'
| |
| ? 'Сокращает ' + status + ' на ' + formatNumber(value.time || 2) + ' сек.'
| |
| : 'Вызывает ' + status + ' на ' + formatNumber(value.time || 2) + ' сек.';
| |
| } else if (type === 'ModifyStatusEffect') {
| |
| var modifiedStatus = STATUS_LABELS[value.effectProto] || value.effectProto || 'неизвестный статус';
| |
| text = value.type === 'Remove'
| |
| ? 'Сокращает ' + modifiedStatus + ' на ' + formatNumber(value.time || 2) + ' сек.'
| |
| : 'Вызывает ' + modifiedStatus + ' на ' + formatNumber(value.time || 2) + ' сек.';
| |
| } else if (type === 'Jitter') {
| |
| text = 'Вызывает дрожь на ' + formatNumber(value.time || 2) + ' сек.';
| |
| } else if (type === 'Emote') {
| |
| text = 'Вызывает ' + (EMOTE_LABELS[value.emote] || ('эмоцию «' + (value.emote || 'неизвестно') + '»'));
| |
| } else if (type === 'ChemVomit') {
| |
| text = 'Вызывает рвоту';
| |
| } else if (type === 'Drunk') {
| |
| text = 'Добавляет ' + formatNumber(value.boozePower || 3) + ' сек. опьянения за цикл метаболизма';
| |
| } else if (type === 'SatiateHunger') {
| |
| text = 'Восстанавливает ' + formatNumber(value.factor !== undefined ? value.factor : 3) + ' ед. насыщения за цикл метаболизма';
| |
| } else if (type === 'SatiateThirst') {
| |
| text = 'Восстанавливает ' + formatNumber(value.factor !== undefined ? value.factor : 3) + ' ед. жажды за цикл метаболизма'; | |
| } else if (type === 'ChemHealEyeDamage') {
| |
| text = 'Лечит повреждения глаз';
| |
| } else if (type === 'CauseZombieInfection') {
| |
| text = 'Заражает зомби-инфекцией';
| |
| } else if (type === 'CureZombieInfection') {
| |
| text = value.innoculate ? 'Лечит зомби-инфекцию и создаёт иммунитет' : 'Лечит зомби-инфекцию';
| |
| } else if (type === 'FlammableReaction') {
| |
| text = 'Покрывает цель горючим; интенсивность составляет ' + formatNumber(Number(value.multiplier || 1) * 100) + '% от стандартной';
| |
| } else if (type === 'PopupMessage') {
| |
| text = value.messages && value.messages.indexOf('rmc-body-stings') !== -1
| |
| ? 'Вызывает болезненное жжение'
| |
| : 'Вызывает заметное субъективное ощущение действия препарата';
| |
| } else {
| |
| text = 'Дополнительный игровой эффект «' + (type || 'неизвестно') + '»';
| |
| }
| |
| | |
| if (conditions.length) {
| |
| text += ' (' + conditions.join(', ') + ')';
| |
| } | | } |
| | | var reagent = getReagent(entry.id) || {}; |
| return [doseDescription(tier, withChance(text, probability))]; | | var haystack = [entry.id, entry.name, reagent.name, reagent.description] |
| | .concat(entry.sectionPath || []).join(' '); |
| | return normalize(haystack).indexOf(state.query) !== -1; |
| } | | } |
|
| |
|
| function semanticTokenClass(token) { | | function renderSectionNav(root, groups) { |
| var normalized = String(token || '').toLocaleLowerCase('ru-RU'); | | var nav = root.querySelector('.scmc-chem__section-nav'); |
| var actionClass = EFFECT_ACTION_CLASSES[normalized]; | | nav.textContent = ''; |
| | | nav.hidden = groups.length < 2; |
| if (actionClass) { | | groups.forEach(function (group, index) { |
| return 'scmc-chem__effect-action scmc-chem__effect-action--' + actionClass; | | var link = el('a', 'scmc-chem__section-link'); |
| }
| | link.href = '#scmc-chem-section-' + index + '-' + slug(group.title); |
| | | add(link, |
| for (var index = 0; index < DAMAGE_TOKEN_CLASSES.length; index += 1) {
| | el('span', '', group.title), |
| if (DAMAGE_TOKEN_CLASSES[index].pattern.test(normalized)) { | | el('span', 'scmc-chem__section-link-count', group.entries.length) |
| return 'scmc-chem__damage-type scmc-chem__damage-type--' + DAMAGE_TOKEN_CLASSES[index].className; | | ); |
| } | | add(nav, link); |
| } | | }); |
| | |
| return '';
| |
| } | | } |
|
| |
|
| function effectTone(text) { | | function renderMixture(items, className) { |
| var value = String(text || '').toLocaleLowerCase('ru-RU'); | | var list = el('div', className); |
| var beneficial = value.search(/полностью лечит|лечит|восстанавливает|возвращает|омолаживает|сокращает|удаляет|создаёт иммунитет/); | | (items || []).forEach(function (item) { |
| var harmful = value.search(/наносит|вызывает|заражает|поджигает|уничтожает/);
| | add(list, add(el('div', 'scmc-chem__mixture-item'), |
| var warning = value.search(/расходует|замедляет/);
| | el('span', 'scmc-chem__mixture-dot'), |
| var candidates = [];
| | el('span', 'scmc-chem__mixture-name', item.name || getName(item.id)), |
| | | el('span', 'scmc-chem__mixture-amount', formatAmount(item.amount)) |
| if (beneficial !== -1) {
| | )); |
| candidates.push({ index: beneficial, tone: 'beneficial' }); | | list.lastChild.firstChild.style.backgroundColor = getColor(item.id); |
| }
| |
| if (harmful !== -1) {
| |
| candidates.push({ index: harmful, tone: 'harmful' });
| |
| }
| |
| if (warning !== -1) {
| |
| candidates.push({ index: warning, tone: 'warning' });
| |
| }
| |
| | |
| candidates.sort(function (left, right) {
| |
| return left.index - right.index; | |
| }); | | }); |
| | | return list; |
| return candidates.length ? candidates[0].tone : 'utility'; | |
| } | | } |
|
| |
|
| function appendSemanticEffectText(parent, text) { | | function renderReaction(reaction) { |
| var value = finishSentence(text); | | var box = el('div', 'scmc-chem__reaction'); |
| var lastIndex = 0; | | var equation = add(el('div', 'scmc-chem__equation'), |
| var match;
| | renderMixture(reaction.reactants, 'scmc-chem__reactants'), |
| | | el('span', 'scmc-chem__arrow', '→'), |
| EFFECT_TOKEN_RE.lastIndex = 0;
| | renderMixture(reaction.products, 'scmc-chem__products') |
| while ((match = EFFECT_TOKEN_RE.exec(value)) !== null) {
| | ); |
| if (match.index > lastIndex) { | | add(box, el('div', 'scmc-chem__reaction-label', 'Рецепт'), equation); |
| parent.appendChild(document.createTextNode(value.slice(lastIndex, match.index)));
| | if (!reaction.products || !reaction.products.length) { |
| }
| | add(box, el('div', 'scmc-chem__reaction-warning', 'Реакция не создаёт реагент и может иметь опасный эффект.')); |
| | |
| var className = semanticTokenClass(match[0]);
| |
| parent.appendChild(className
| |
| ? element('span', className, match[0])
| |
| : document.createTextNode(match[0]));
| |
| lastIndex = EFFECT_TOKEN_RE.lastIndex;
| |
| }
| |
| | |
| if (lastIndex < value.length) { | |
| parent.appendChild(document.createTextNode(value.slice(lastIndex))); | |
| } | | } |
| | return box; |
| } | | } |
|
| |
|
| function renderEffectText(text) { | | function effectType(effect) { |
| var tone = effectTone(text); | | return String(effect && effect.yamlTag || '').replace(/^!type:/, '') || 'Эффект'; |
| var item = element('li', 'scmc-chem__effect scmc-chem__effect--' + tone);
| |
| var content = element('span', 'scmc-chem__effect-name');
| |
| appendSemanticEffectText(content, text);
| |
| item.appendChild(content);
| |
| return item;
| |
| } | | } |
|
| |
|
| function tierTitle(tier, reagent) { | | function describeValue(value) { |
| var properties = reagent.properties || {}; | | if (value === null || value === undefined || value === '') { |
| | | return ''; |
| if (tier === 'overdose') {
| |
| return 'Передозировка' + (properties.overdose !== undefined ? ' · от ' + formatAmount(properties.overdose) : ''); | |
| } | | } |
| if (tier === 'critical') { | | if (typeof value !== 'object') { |
| return 'Критическая передозировка' + (properties.criticalOverdose !== undefined ? ' · от ' + formatAmount(properties.criticalOverdose) : ''); | | 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 appendEffectTier(group, tier, effects, reagent) { | | 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 tierBlock = element('div', 'scmc-chem__effect-tier scmc-chem__effect-tier--' + tier);
| | var type = effectType(effect); |
| tierBlock.appendChild(element('div', 'scmc-chem__effect-tier-title', tierTitle(tier, reagent)));
| | var value = effect.value || {}; |
| var list = element('ul', 'scmc-chem__effect-list');
| | var harmful = /Damage|Ignite|Explosion|Electrocute|Vomit|Poison/i.test(type); |
| effects.forEach(function (description) {
| | var card = el('div', 'scmc-chem__effect' + (harmful ? ' scmc-chem__effect--harmful' : '')); |
| list.appendChild(renderEffectText(description.text)); | | 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); |
| }); | | }); |
| tierBlock.appendChild(list); | | return list; |
| group.appendChild(tierBlock);
| |
| } | | } |
|
| |
|
| 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 (!metabolisms || !Object.keys(metabolisms).length) { | | if (!keys.length) { |
| return null; | | add(wrapper, el('p', 'scmc-chem__empty', 'Метаболические эффекты не указаны.')); |
| | return wrapper; |
| } | | } |
| | | keys.forEach(function (key) { |
| var block = element('div', 'scmc-chem__card-section'); | | var metabolism = metabolisms[key] || {}; |
| var hasContent = false;
| | var block = el('div', 'scmc-chem__metabolism'); |
| block.appendChild(element('h4', 'scmc-chem__card-subtitle', 'Воздействие'));
| | var title = METABOLISM_LABELS[key] || key; |
| | |
| Object.keys(metabolisms).forEach(function (metabolismId) {
| |
| var metabolism = metabolisms[metabolismId] || {}; | |
| var tiers = { normal: [], overdose: [], critical: [] }; | |
| | |
| (metabolism.effects || []).forEach(function (effect) {
| |
| genericEffectDescription(effect, reagent).forEach(function (description) {
| |
| tiers[description.tier].push(description);
| |
| });
| |
| });
| |
| | |
| if (!tiers.normal.length && !tiers.overdose.length && !tiers.critical.length) {
| |
| return;
| |
| }
| |
| | |
| var group = element('div', 'scmc-chem__metabolism');
| |
| var title = METABOLISM_LABELS[metabolismId] || metabolismId; | |
| | |
| if (metabolism.metabolismRate !== undefined) { | | if (metabolism.metabolismRate !== undefined) { |
| title += ' · расход ' + formatAmount(metabolism.metabolismRate) + '/сек.'; | | title += ' · ' + formatAmount(metabolism.metabolismRate) + '/с'; |
| } | | } |
| | | add(block, el('div', 'scmc-chem__metabolism-title', title), renderEffects(metabolism.effects)); |
| group.appendChild(element('div', 'scmc-chem__metabolism-title', title)); | | add(wrapper, block); |
| appendEffectTier(group, 'normal', tiers.normal, reagent);
| |
| appendEffectTier(group, 'overdose', tiers.overdose, reagent);
| |
| appendEffectTier(group, 'critical', tiers.critical, reagent); | |
| block.appendChild(group);
| |
| hasContent = true;
| |
| }); | | }); |
| | | return wrapper; |
| return hasContent ? block : null; | |
| }
| |
| | |
| function plantEffectText(effect) {
| |
| var type = tagName(effect && effect.yamlTag);
| |
| var value = effectValue(effect);
| |
| var probability = value.probability;
| |
| var amount = Number(value.amount !== undefined ? value.amount : 1);
| |
| var attribute = PLANT_ATTRIBUTE_LABELS[type];
| |
| var text;
| |
| | |
| if (probability === 0) {
| |
| return '';
| |
| }
| |
| | |
| if (attribute) {
| |
| if (type === 'PlantAdjustHealth') {
| |
| text = amount >= 0
| |
| ? 'Восстанавливает здоровье растения на ' + formatNumber(amount)
| |
| : 'Снижает здоровье растения на ' + formatNumber(Math.abs(amount));
| |
| } else {
| |
| text = (amount >= 0 ? 'Повышает ' : 'Снижает ') + attribute + ' на ' + formatNumber(Math.abs(amount));
| |
| }
| |
| } else if (type === 'PlantCryoxadone') {
| |
| text = 'Омолаживает растение с учётом его возраста и времени роста';
| |
| } else if (type === 'PlantPhalanximine') {
| |
| text = 'Возвращает жизнеспособность растению, погибшему из-за мутации';
| |
| } else if (type === 'PlantDiethylamine') {
| |
| text = 'Повышает продолжительность жизни или базовое здоровье растения; шанс 10% на единицу реагента';
| |
| } else if (type === 'PlantRestoreSeeds') {
| |
| text = 'Восстанавливает семена растения';
| |
| } else if (type === 'PlantDestroySeeds') {
| |
| text = 'Уничтожает семена растения';
| |
| } else {
| |
| text = 'Дополнительный растительный эффект «' + (type || 'неизвестно') + '»';
| |
| }
| |
| | |
| return withChance(text, probability);
| |
| }
| |
| | |
| function renderPlantMetabolism(reagent) {
| |
| var effects = reagent.properties && reagent.properties.plantMetabolism;
| |
| | |
| if (!Array.isArray(effects) || !effects.length) {
| |
| return null;
| |
| }
| |
| | |
| var descriptions = effects.map(plantEffectText).filter(Boolean);
| |
| if (!descriptions.length) {
| |
| return null;
| |
| }
| |
| | |
| var block = element('div', 'scmc-chem__card-section');
| |
| block.appendChild(element('h4', 'scmc-chem__card-subtitle', 'Метаболизм растений'));
| |
| var group = element('div', 'scmc-chem__metabolism');
| |
| group.appendChild(element('div', 'scmc-chem__metabolism-title', '1 ед. реагента каждые 3 секунды (базово)'));
| |
| var list = element('ul', 'scmc-chem__effect-list');
| |
| descriptions.forEach(function (description) {
| |
| list.appendChild(renderEffectText(description));
| |
| });
| |
| group.appendChild(list);
| |
| block.appendChild(group);
| |
| return block;
| |
| }
| |
| | |
| function renderMixture(items, className) {
| |
| var list = element('div', className);
| |
| | |
| (items || []).forEach(function (item) {
| |
| var row = element('div', 'scmc-chem__mixture-item');
| |
| var dot = element('span', 'scmc-chem__mixture-dot');
| |
| var reagent = reagentById(item.id);
| |
| dot.style.backgroundColor = safeColor(reagent && reagent.properties && reagent.properties.color);
| |
| | |
| append(
| |
| row,
| |
| dot,
| |
| element('span', 'scmc-chem__mixture-name', capitalizeFirst(item.name || reagentName(item.id))),
| |
| element('span', 'scmc-chem__mixture-amount', formatAmount(item.amount))
| |
| );
| |
| list.appendChild(row);
| |
| });
| |
| | |
| return list;
| |
| }
| |
| | |
| function reactionEffectText(effect) {
| |
| var type = tagName(effect && effect.yamlTag);
| |
| var value = effect && effect.value;
| |
| value = value && typeof value === 'object' ? value : {};
| |
| | |
| if (type === 'SensitiveReactionExplosionEffect') {
| |
| return 'Взрыв при объёме от ' + formatAmount(value.threshold || 0) + '; интенсивность за единицу: ' + formatNumber(value.intensityPerUnit || 0) + '.';
| |
| }
| |
| if (type === 'ExplosionReactionEffect') {
| |
| return 'Вызывает взрыв; интенсивность за единицу: ' + formatNumber(value.intensityPerUnit || 0) + '.';
| |
| }
| |
| if (type === 'AreaReactionEffect') {
| |
| return 'Создаёт эффект в области' + (value.duration ? ' на ' + formatNumber(value.duration) + ' сек.' : '.');
| |
| }
| |
| if (type === 'CreateEntityReactionEffect') {
| |
| return 'Создаёт объект: ' + (value.entity || 'неизвестно') + '.';
| |
| }
| |
| | |
| return type || 'Дополнительный эффект реакции';
| |
| }
| |
| | |
| function renderReaction(reaction, index) {
| |
| var block = element('div', 'scmc-chem__reaction');
| |
| | |
| if (index > 0) {
| |
| block.appendChild(element('div', 'scmc-chem__reaction-label', 'Вариант ' + (index + 1)));
| |
| }
| |
| | |
| var equation = element('div', 'scmc-chem__equation');
| |
| append(
| |
| equation,
| |
| renderMixture(reaction.reactants, 'scmc-chem__reactants'),
| |
| element('div', 'scmc-chem__arrow', '→'),
| |
| renderMixture(reaction.products, 'scmc-chem__products')
| |
| );
| |
| block.appendChild(equation);
| |
| | |
| if (reaction.conditions && reaction.conditions.minTemp !== undefined) {
| |
| block.appendChild(element('div', 'scmc-chem__reaction-note', 'Минимальная температура: ' + formatNumber(reaction.conditions.minTemp) + ' K.'));
| |
| }
| |
| | |
| (reaction.effects || []).forEach(function (effect) {
| |
| block.appendChild(element('div', 'scmc-chem__reaction-warning', reactionEffectText(effect)));
| |
| });
| |
| | |
| return block;
| |
| }
| |
| | |
| function renderRecipes(reagentId) {
| |
| var reactions = state.productReactions[reagentId] || [];
| |
| | |
| if (!reactions.length) {
| |
| return null;
| |
| }
| |
| | |
| var block = element('div', 'scmc-chem__card-section');
| |
| block.appendChild(element('h4', 'scmc-chem__card-subtitle', reactions.length > 1 ? 'Рецепты' : 'Рецепт'));
| |
| reactions.forEach(function (reaction, index) {
| |
| block.appendChild(renderReaction(reaction, index));
| |
| });
| |
| return block;
| |
| }
| |
| | |
| function renderDoseBadges(reagent) {
| |
| var properties = reagent.properties || {};
| |
| var row = element('div', 'scmc-chem__dose-row');
| |
| | |
| if (properties.overdose !== undefined) {
| |
| row.appendChild(element('span', 'scmc-chem__dose scmc-chem__dose--warning', 'Передозировка: ' + formatAmount(properties.overdose)));
| |
| }
| |
| if (properties.criticalOverdose !== undefined) {
| |
| row.appendChild(element('span', 'scmc-chem__dose scmc-chem__dose--danger', 'Критическая: ' + formatAmount(properties.criticalOverdose)));
| |
| }
| |
| | |
| return row.childNodes.length ? row : null;
| |
| } | | } |
|
| |
|
| function renderCard(entry) { | | function renderCard(entry) { |
| var reagent = reagentById(entry.id); | | var reagent = getReagent(entry.id) || { |
| var details = element('details', 'scmc-chem__card');
| | id: entry.id, |
| var summary = element('summary', 'scmc-chem__card-summary'); | | name: entry.name, |
| var color = safeColor(reagent && reagent.properties && reagent.properties.color); | | origin: entry.origin, |
| var colorBar = element('span', 'scmc-chem__color'); | | properties: {} |
| var heading = element('span', 'scmc-chem__card-heading');
| | }; |
| var originLabel = entry.origin === 'upstream-reference' ? 'базовый' : entry.origin === 'stories' ? 'Stories' : 'RMC14';
| | 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); |
| colorBar.style.backgroundColor = color; | | var summary = el('summary', 'scmc-chem__card-summary'); |
| | | var heading = add(el('span', 'scmc-chem__card-heading'), |
| append( | | el('span', 'scmc-chem__card-name', reagent.name || entry.name || entry.id), |
| heading,
| | el('code', 'scmc-chem__card-id', entry.id) |
| element('span', 'scmc-chem__card-name', capitalizeFirst(entry.name || (reagent && reagent.name) || entry.id)), | |
| element('code', 'scmc-chem__card-id', entry.id) | |
| ); | | ); |
| append( | | add(summary, |
| summary,
| | el('span', 'scmc-chem__color'), |
| colorBar, | |
| heading, | | heading, |
| element('span', 'scmc-chem__origin scmc-chem__origin--' + (entry.origin || 'unknown'), originLabel), | | el('span', 'scmc-chem__origin scmc-chem__origin--' + (reagent.origin || entry.origin || 'upstream-reference'), ORIGIN_LABELS[reagent.origin || entry.origin] || reagent.origin || entry.origin), |
| element('span', 'scmc-chem__chevron', '') | | el('span', 'scmc-chem__chevron') |
| ); | | ); |
| details.appendChild(summary); | | summary.firstChild.style.backgroundColor = color; |
| | | var body = el('div', 'scmc-chem__card-body'); |
| var body = element('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; |
| if (!reagent) { | | var critical = reagent.properties && reagent.properties.criticalOverdose; |
| body.appendChild(element('p', 'scmc-chem__empty', 'Данные реагента не найдены.')); | | if (overdose !== undefined || critical !== undefined) { |
| details.appendChild(body); | | var doses = el('div', 'scmc-chem__dose-row'); |
| return details; | | 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 (reagent.description) { | | if (reactions.length) { |
| body.appendChild(element('p', 'scmc-chem__description', capitalizeFirst(reagent.description))); | | var recipeSection = el('div', 'scmc-chem__card-section'); |
| }
| | add(recipeSection, el('h4', 'scmc-chem__card-subtitle', 'Получение')); |
| | | reactions.forEach(function (reaction) { add(recipeSection, renderReaction(reaction)); }); |
| append(body, renderDoseBadges(reagent), renderRecipes(entry.id), renderMetabolisms(reagent), renderPlantMetabolism(reagent));
| | add(body, recipeSection); |
| | |
| if (!body.childNodes.length) {
| |
| body.appendChild(element('p', 'scmc-chem__empty', 'Для этого реагента нет дополнительных данных.')); | |
| } | | } |
| | | add(body, renderMetabolisms(reagent)); |
| details.appendChild(body); | | add(details, summary, body); |
| return details; | | return details; |
| }
| |
|
| |
| function sectionKey(entry) {
| |
| return (entry.sectionPath && entry.sectionPath.length ? entry.sectionPath : ['Без раздела']).join(' / ');
| |
| }
| |
|
| |
| function groupEntries(entries) {
| |
| var groups = [];
| |
| var byKey = Object.create(null);
| |
|
| |
| entries.forEach(function (entry) {
| |
| var key = sectionKey(entry);
| |
|
| |
| if (!byKey[key]) {
| |
| byKey[key] = { key: key, path: entry.sectionPath || ['Без раздела'], entries: [] };
| |
| groups.push(byKey[key]);
| |
| }
| |
|
| |
| byKey[key].entries.push(entry);
| |
| });
| |
|
| |
| return groups;
| |
| }
| |
|
| |
| function sectionId(group, index) {
| |
| return 'scmc-chem-section-' + state.activeTab + '-' + index;
| |
| }
| |
|
| |
| function renderSectionNavigation(root, groups) {
| |
| var navigation = root.querySelector('.scmc-chem__section-nav');
| |
| navigation.textContent = '';
| |
| navigation.hidden = groups.length < 2;
| |
|
| |
| groups.forEach(function (group, index) {
| |
| var title = capitalizeFirst(group.path[group.path.length - 1] || 'Без раздела');
| |
| var link = element('a', 'scmc-chem__section-link', title);
| |
| link.href = '#' + sectionId(group, index);
| |
| link.appendChild(element('span', 'scmc-chem__section-link-count', group.entries.length));
| |
| navigation.appendChild(link);
| |
| });
| |
| }
| |
|
| |
| function renderSection(group, index) {
| |
| var section = element('section', 'scmc-chem__section');
| |
| var title = capitalizeFirst(group.path[group.path.length - 1] || 'Без раздела');
| |
| var heading = element('div', 'scmc-chem__section-heading');
| |
|
| |
| section.id = sectionId(group, index);
| |
| heading.appendChild(element('h2', 'scmc-chem__section-title', title));
| |
|
| |
| if (group.path.length > 1) {
| |
| heading.appendChild(element('div', 'scmc-chem__breadcrumbs', group.path.slice(0, -1).map(capitalizeFirst).join(' › ')));
| |
| }
| |
|
| |
| heading.appendChild(element('span', 'scmc-chem__section-count', group.entries.length));
| |
| section.appendChild(heading);
| |
|
| |
| var cards = element('div', 'scmc-chem__cards');
| |
| group.entries.forEach(function (entry) {
| |
| cards.appendChild(renderCard(entry));
| |
| });
| |
| section.appendChild(cards);
| |
| return section;
| |
| } | | } |
|
| |
|
| function renderResults(root) { | | function renderResults(root) { |
| var results = root.querySelector('.scmc-chem__results'); | | var result = root.querySelector('.scmc-chem__results'); |
| var entries = entriesForTab(state.activeTab).filter(function (entry) { | | var counter = root.querySelector('.scmc-chem__result-count'); |
| return matchesQuery(entry, reagentById(entry.id));
| | result.textContent = ''; |
| });
| | var entries = state.data.catalogSections[state.activeTab].filter(entryMatches); |
| | | var groups = groupEntries(entries); |
| results.textContent = ''; | | counter.textContent = entries.length + ' найдено'; |
| | | renderSectionNav(root, groups); |
| root.querySelector('.scmc-chem__result-count').textContent = entries.length + ' из ' + entriesForTab(state.activeTab).length; | |
| | |
| if (!entries.length) { | | if (!entries.length) { |
| renderSectionNavigation(root, []); | | add(result, el('div', 'scmc-chem__no-results', 'Ничего не найдено. Измените запрос.')); |
| var empty = element('div', 'scmc-chem__no-results');
| |
| append(
| |
| empty,
| |
| element('strong', '', 'Ничего не найдено.'),
| |
| element('span', '', ' Попробуй другое название или ID реагента.')
| |
| );
| |
| results.appendChild(empty);
| |
| return; | | return; |
| } | | } |
|
| |
| var groups = groupEntries(entries);
| |
| renderSectionNavigation(root, groups);
| |
|
| |
| groups.forEach(function (group, index) { | | groups.forEach(function (group, index) { |
| results.appendChild(renderSection(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), |
| function renderTabs(root) {
| | el('span', 'scmc-chem__breadcrumbs', group.path.join(' › ')), |
| var tabs = root.querySelector('.scmc-chem__tabs');
| | el('span', 'scmc-chem__section-count', group.entries.length) |
| tabs.textContent = '';
| | ); |
| | | var cards = el('div', 'scmc-chem__cards'); |
| TAB_DEFINITIONS.forEach(function (tab) {
| | group.entries.forEach(function (entry) { add(cards, renderCard(entry)); }); |
| var count = entriesForTab(tab.id).length; | | add(section, heading, cards); |
| var button = element('button', 'scmc-chem__tab', tab.label);
| | add(result, section); |
| button.type = 'button';
| |
| button.dataset.tab = tab.id;
| |
| button.setAttribute('aria-selected', tab.id === state.activeTab ? 'true' : 'false');
| |
| button.appendChild(element('span', 'scmc-chem__tab-count', count));
| |
| | |
| button.addEventListener('click', function () { | |
| state.activeTab = tab.id;
| |
| root.querySelectorAll('.scmc-chem__tab').forEach(function (item) {
| |
| item.setAttribute('aria-selected', item.dataset.tab === state.activeTab ? 'true' : 'false');
| |
| });
| |
| renderResults(root);
| |
| });
| |
| | |
| tabs.appendChild(button); | |
| }); | | }); |
| } | | } |
| Строка 1109: |
Строка 433: |
| function renderApp(root) { | | function renderApp(root) { |
| root.textContent = ''; | | root.textContent = ''; |
| root.classList.add('scmc-chem'); | | add(root, createHero()); |
| | | var controls = el('div', 'scmc-chem__controls'); |
| var header = element('header', 'scmc-chem__hero');
| | var search = el('label', 'scmc-chem__search'); |
| var titleBlock = element('div', 'scmc-chem__hero-copy');
| | var input = el('input', 'scmc-chem__search-input'); |
| append(
| |
| titleBlock,
| |
| element('div', 'scmc-chem__eyebrow', 'SSMC · автоматический справочник'),
| |
| element('h1', 'scmc-chem__title', 'Химический каталог'),
| |
| element('p', 'scmc-chem__lead', 'Рецепты, дозировки и эффекты из актуальных прототипов RMC14 и Space Stories.')
| |
| );
| |
| | |
| var stats = element('div', 'scmc-chem__stats');
| |
| append(
| |
| stats,
| |
| element('span', 'scmc-chem__stat', state.data.counts.customReagents + ' реагентов'),
| |
| element('span', 'scmc-chem__stat', state.data.counts.customReactions + ' реакций')
| |
| );
| |
| append(header, titleBlock, stats);
| |
| root.appendChild(header);
| |
| | |
| var controls = element('div', 'scmc-chem__controls'); | |
| var search = element('label', 'scmc-chem__search'); | |
| search.appendChild(element('span', 'scmc-chem__search-icon', '⌕'));
| |
| var input = element('input', 'scmc-chem__search-input'); | |
| input.type = 'search'; | | input.type = 'search'; |
| input.placeholder = 'Название или ID реагента…'; | | input.placeholder = 'Название, ID или раздел…'; |
| input.autocomplete = 'off'; | | input.autocomplete = 'off'; |
| | input.setAttribute('aria-label', 'Поиск по химическому каталогу'); |
| input.addEventListener('input', function () { | | input.addEventListener('input', function () { |
| state.query = normalizeSearch(input.value); | | state.query = normalize(input.value); |
| renderResults(root); | | renderResults(root); |
| }); | | }); |
| search.appendChild(input); | | add(search, el('span', 'scmc-chem__search-icon', '⌕'), input); |
| controls.appendChild(search);
| | add(controls, search, el('span', 'scmc-chem__result-count')); |
| controls.appendChild(element('span', 'scmc-chem__result-count', ''));
| | add(root, controls, el('div', 'scmc-chem__tabs')); |
| root.appendChild(controls); | | var nav = el('nav', 'scmc-chem__section-nav'); |
| | | nav.setAttribute('aria-label', 'Подразделы каталога'); |
| var tabs = element('div', 'scmc-chem__tabs');
| | add(root, nav, el('main', 'scmc-chem__results')); |
| tabs.setAttribute('role', 'tablist'); | | var source = state.data.source || {}; |
| root.appendChild(tabs);
| | add(root, el('footer', 'scmc-chem__footer', 'Локальный каталог MediaWiki' + (source.commit ? ' · версия ' + String(source.commit).slice(0, 8) : ''))); |
| var sectionNavigation = element('nav', 'scmc-chem__section-nav'); | |
| sectionNavigation.setAttribute('aria-label', 'Подразделы каталога'); | |
| sectionNavigation.hidden = true; | |
| root.appendChild(sectionNavigation);
| |
| root.appendChild(element('main', 'scmc-chem__results'));
| |
| | |
| var footer = element('footer', 'scmc-chem__footer'); | |
| var sourceDate = new Date(state.data.generatedAt);
| |
| var dateText = isNaN(sourceDate.getTime()) ? state.data.generatedAt : sourceDate.toLocaleString('ru-RU'); | |
| footer.textContent = 'Данные собраны автоматически · ' + dateText + ' · коммит ' + String(state.data.source.commit || '').slice(0, 8);
| |
| root.appendChild(footer);
| |
| | |
| renderTabs(root); | | renderTabs(root); |
| renderResults(root); | | renderResults(root); |
| } | | } |
|
| |
|
| function renderLoading(root) { | | function start(root) { |
| root.textContent = ''; | | renderLoading(root); |
| var block = element('div', 'scmc-chem__status scmc-chem__status--loading');
| | loadCatalog().then(function (data) { |
| append( | | state.data = data; |
| block,
| | indexReactions(); |
| element('span', 'scmc-chem__spinner', ''),
| | renderApp(root); |
| element('span', '', 'Загружаю химический каталог…')
| | }).catch(function (error) { |
| );
| | renderError(root, error); |
| root.appendChild(block);
| |
| }
| |
| | |
| function renderError(root, error) {
| |
| root.textContent = '';
| |
| var block = element('div', 'scmc-chem__status scmc-chem__status--error');
| |
| append(
| |
| block,
| |
| element('strong', '', 'Каталог не загрузился.'), | |
| element('span', '', ' Проверь соединение и попробуй ещё раз.')
| |
| );
| |
| var button = element('button', 'scmc-chem__retry', 'Повторить');
| |
| button.type = 'button';
| |
| button.addEventListener('click', function () {
| |
| load(root); | |
| }); | |
| block.appendChild(button);
| |
| root.appendChild(block);
| |
| | |
| if (window.console && console.error) {
| |
| console.error('[ScmcChemistry]', error); | |
| }
| |
| }
| |
| | |
| function validateData(data) {
| |
| var hasSections = data && data.catalogSections && REQUIRED_CATALOG_SECTIONS.every(function (sectionId) {
| |
| return Array.isArray(data.catalogSections[sectionId]);
| |
| }); | | }); |
| var classificationGuide = data && data.classification && data.classification.guideFile;
| |
|
| |
| return data &&
| |
| data.schemaVersion === 1 &&
| |
| data.reagents &&
| |
| data.dependencies &&
| |
| data.reactions &&
| |
| data.guides &&
| |
| data.counts &&
| |
| hasSections &&
| |
| /\/RMCChemicals\.xml$/.test(classificationGuide || '');
| |
| }
| |
|
| |
| function load(root) {
| |
| renderLoading(root);
| |
|
| |
| fetch(DATA_URL, { cache: 'no-store' })
| |
| .then(function (response) {
| |
| if (!response.ok) {
| |
| throw new Error('HTTP ' + response.status);
| |
| }
| |
| return response.json();
| |
| })
| |
| .then(function (data) {
| |
| if (!validateData(data)) {
| |
| throw new Error('Неизвестная схема chemistry-catalog.json');
| |
| }
| |
|
| |
| state.data = data;
| |
| buildProductReactionIndex();
| |
| 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; |
| load(root); | |
| } | | } |
| | loadStyle(); |
| | start(root); |
| } | | } |
|
| |
|
| if (document.readyState === 'loading') { | | $(init); |
| document.addEventListener('DOMContentLoaded', init);
| | }(mediaWiki, jQuery)); |
| } else {
| |
| init();
| |
| }
| |
| }()); | |